/* ─────────────────────────────────────────────────────────
   savings-card — the Runwise savings component, rebuilt, and
   the rail that walks through its twenty-four versions.

   Two namespaces, and the split is the whole point of the
   file:

     .sc-*  is the PRODUCT. Every value in it came off the
            Figma frames — #07AD00, #DCFBCC, the 20px card
            radius, the 32/38 headline. It is Runwise's
            design language, not this site's, and it is the
            only place on mekaelwr.com where a second visual
            system is reproduced rather than described.

     .sj-*  is the SITE talking about the product. It uses
            style.css's tokens — --ink, --rule, --u — so the
            frame around the card reads as the same page as
            the prose above it.

   Keeping those apart matters because the case study is an
   argument about colour. If the chrome around the card drifted
   green, the reader would lose the ability to tell what the
   product actually looked like from what I chose to put
   around it.

   No inline styles anywhere, per _headers: geometry rides on
   SVG attributes and CSS custom properties set through
   element.style, which is a CSSOM write and not what
   style-src blocks.
   ───────────────────────────────────────────────────────── */

/* ── Codec Warm ───────────────────────────────────────────
   Runwise's face, now shipped rather than merely named.

   It used to be first in the stack with no @font-face behind
   it, on the reasoning that nobody would be redistributing a
   licensed font. What that actually produced was a recreation
   that rendered in Inter for every reader, and rendered WRONG
   for the one machine that had the family installed — because
   Codec Warm is an eleven-weight family and its "Bold" is
   usWeightClass 500, not 700. Asking the installed family for
   700 gets Codec Warm HEAVY. The Figma text styles say Bold.

   Declaring the two faces here fixes both problems at once:
   the frames use the real face everywhere, and 700 resolves to
   the file the Figma styles actually name instead of to
   whatever the local family happens to have parked at 700.
   600 (one badge) falls up to 700, which is correct — the
   family has no semibold and the frames never asked for one.

   Subset to Latin-1 plus the punctuation the frames use, which
   is why two faces cost 42 kB rather than 250. Regenerating
   them is in tools/build-codec-subset.sh; the source OTFs are
   deliberately not in this repo.

   THE LICENCE. Codec Warm is Zetafonts’, licensed to Runwise.
   Two faces of it are now served from this domain, which is web
   distribution and a different permission from having it
   installed on a laptop. The previous version of this file
   avoided that question by shipping nothing — and paid for it
   by rendering the frames in the wrong face for everybody. If
   the licence turns out not to cover this, the fix is small and
   the page still works: delete the two @font-face blocks and
   the two preloads in savings-card.html, and the stack below
   falls back to Inter exactly as it used to.

   font-display: block, not swap. Every string in these frames
   sits in a fixed-width box at a coordinate read off a Figma
   node, and the two faces are not the same width: the hero
   headline measures 106.1px in Codec Warm and 125.1px in the
   Inter fallback, so a swap would visibly reset the card in
   front of the reader. 21 kB same-origin resolves inside the
   block period; if it somehow does not, the Inter fallback is
   the same picture the page shipped before this block existed.

   That 19px gap is not Codec Warm being narrow — its digits are
   the wider of the two. The site loads Inter at 400 and 600
   only, so every 700 in this file was being SYNTHESISED from
   Inter 600, and a synthetic bold is fatter than a real one.
   The stand-in was wrong twice over: wrong face, and a weight
   the browser had invented. */
@font-face {
  font-family: "Codec Warm";
  src: url("/figures/assets/codec-warm-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

@font-face {
  font-family: "Codec Warm";
  src: url("/figures/assets/codec-warm-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: block;
}

.sc {
  /* ── Runwise ink ──────────────────────────────────────── */
  --sc-ink: #191918;
  --sc-mute: #8e8d8c;
  --sc-hair: #f0f0f0;
  --sc-surface: #fafafa;

  /* ── The green pair the card finally settled on ────────
     Saved is the gradient, spent is the wash. The 0.25px
     inset borders in the Figma frames are what keep two
     tints of one hue from bleeding into each other; at 1px
     they read as outlines, so they stay hairline here. */
  --sc-saved-a: #07ad00;
  --sc-saved-b: #6cc768;
  --sc-saved-edge: #069500;
  --sc-spent-a: #dcfbcc;
  --sc-spent-b: #ffffff;
  --sc-spent-edge: rgba(7, 173, 0, 0.5);
  --sc-spent-flat: #effae9;

  /* ── The purple that came over from the fuel-bill chart ─
     Kept because three of the twenty-four frames are only
     legible as a rejection of it. */
  --sc-paid-a: #8f6fe9;
  --sc-paid-b: #e0d7f9;
  --sc-paid-edge: #5634b6;

  /* ── The two colours tried for a missing bill ──────────
     Red first, then amber, then neither. */
  --sc-miss-red: #ff6262;
  --sc-miss-red-wash: #fff1f1;
  --sc-miss-amber: #f2b705;
  --sc-miss-amber-wash: #fffbe8;

  /* ── Neutrals for projection and absence ───────────────── */
  --sc-ghost: #e3e3e2;
  --sc-ghost-ink: #b9b7b6;
  --sc-hatch: #e9e9e9;

  /* Codec Warm, from the two faces declared above. Inter stays
     in the stack as the fallback for a blocked or failed font
     load — it is what these frames looked like before the face
     was shipped, and it is close enough to keep every label
     inside its box.

     Weight 700 rather than 600 because that is what the Figma
     text styles specify: Codec Warm Bold. The @font-face above
     is what makes 700 mean Bold here. */
  font-family:
    "Codec Warm",
    Inter,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Helvetica,
    Arial,
    sans-serif;

  /* ── Per-pixel frames ──────────────────────────────────
     Every card is a fixed-size stage and every child inside it
     is absolutely positioned at the coordinate its Figma node
     reports. That is deliberate and it replaced a flow layout
     that looked approximately right and was wrong everywhere:
     a generic chart engine with this file's own spacing cannot
     reproduce a 33.25px bar on a 45.25px step, and the drift
     compounds across eight of them.

     The coordinates arrive through element.style, which is a
     CSSOM write and therefore allowed by the policy in
     _headers; a style="" attribute in markup would be dropped.

     Width and height come from the frame, so they are set per
     card rather than here. */
  position: relative;
  width: 436px;
  background: #fff;
  color: var(--sc-ink);
  border-radius: 20px;
  box-shadow:
    0 1px 3px rgba(0, 0, 0, 0.1),
    0 1px 2px rgba(0, 0, 0, 0.06);
  text-align: left;
  flex: none;
  /* Frames clip. Frame 2's legend is 649 wide inside a 396 card
     and its third key really is cut off by the card edge in the
     export — reproducing that means clipping here too. Every
     tooltip in the set sits inside the bounds, so nothing that
     should be visible is lost. */
  overflow: hidden;
}

.sc > * {
  position: absolute;
  margin: 0;
}

/* Text blocks. Figma reports a text node's box as top-of-line
   plus line-height, so each one is laid out as a flex box that
   centres its own line — which is what a CSS line box does and
   what makes the reported height the right thing to set. */
/* No vertical nudge here, and that is worth a note because
   there was one. Text measured a pixel low against the frame
   at every size, which looked like a real difference between
   how CSS centres a line box and how Figma places a cap in
   one — until the diff was aligned by solving for the offset
   instead of assuming it. The export carries 3px of bleed on
   the left and 2px on the top, not 3 and 3. The "error" was
   the measurement. Centring is correct; leave it alone. */
.sc-t {
  display: flex;
  align-items: center;
  white-space: nowrap;
}
.sc-t--r {
  justify-content: flex-end;
}
.sc-t--c {
  justify-content: center;
}
.sc-t--wrap {
  white-space: normal;
  align-items: flex-start;
}

.sc-s32 {
  font-size: 32px;
  font-weight: 400;
  letter-spacing: 0;
}
.sc-s32b {
  font-size: 32px;
  font-weight: 700;
  letter-spacing: 0;
}
.sc-s16 {
  font-size: 16px;
  font-weight: 400;
}
.sc-s16b {
  font-size: 16px;
  font-weight: 700;
}
.sc-s14 {
  font-size: 14px;
  font-weight: 400;
}
.sc-s14b {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.25px;
}
.sc-s12 {
  font-size: 12px;
  font-weight: 400;
}
.sc-s12c {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.sc-mute-t {
  color: var(--sc-mute);
}

/* ── Plot paint ──────────────────────────────────────────
   Structure is one weight and faint; the marks carry the
   colour. Every fill that is a gradient or a pattern comes
   from a def built in JS, so what lives here is the hairline
   treatment and the flat colours. */

.sc-grid line {
  stroke: var(--sc-hair);
  stroke-width: 1;
}
.sc-grid line.sc-grid-dot {
  stroke-dasharray: 2 3;
}
.sc-axis line {
  stroke: #d9d9d8;
  stroke-width: 1;
}
/* Full ink, and only on the last five frames. Sampled off the
   shipped frame, where the row under the bars is #191818 — not
   a grid line that happens to be at the bottom. Frames 6 and
   15 have no dark row there at all. */
.sc-axis-ink {
  fill: var(--sc-ink);
}

/* The 0.25px inset borders in the frames are what keep two
   tints of one hue from bleeding into each other. At 1px they
   read as outlines, so they stay hairline. */
.sc-bar-spent {
  stroke: var(--sc-spent-edge);
  stroke-width: 0.5;
}
.sc-bar-saved {
  stroke: var(--sc-saved-edge);
  stroke-width: 0.5;
}
.sc-bar-paid {
  stroke: var(--sc-paid-edge);
  stroke-width: 0.5;
}
.sc-bar-paid-muted {
  stroke: none;
}
.sc-bar-saved-wash {
  stroke: none;
}
.sc-bar-flat {
  stroke: none;
}
.sc-bar-ghost {
  fill: var(--sc-surface);
  stroke: none;
}

.sc-line {
  fill: none;
  stroke: var(--sc-saved-a);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sc-line--paid {
  stroke: var(--sc-paid-a);
}
.sc-line--ink {
  stroke: var(--sc-ink);
}
.sc-line--miss-red {
  stroke: var(--sc-miss-red);
  stroke-dasharray: 5 4;
}
.sc-line--miss-amber {
  stroke: var(--sc-miss-amber);
  stroke-dasharray: 5 4;
}
.sc-line--miss-grey {
  stroke: var(--sc-ghost-ink);
  stroke-dasharray: 5 4;
}
/* fill:none is load-bearing and was missing once. This is the
   only line class used WITHOUT .sc-line alongside it, so it
   cannot inherit that fill:none — and an open polyline with a
   default fill paints a black wedge across the card. It did,
   on four frames. */
.sc-line--ghost {
  fill: none;
  stroke: var(--sc-ghost-ink);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}
.sc-dot {
  fill: #fff;
  stroke: var(--sc-saved-a);
  stroke-width: 2;
}
.sc-dot--ink {
  stroke: var(--sc-ink);
}
.sc-drop {
  stroke: var(--sc-ink);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}

.sc-wash-red {
  fill: var(--sc-miss-red-wash);
}
.sc-wash-amber {
  fill: var(--sc-miss-amber-wash);
}
.sc-wash-grey {
  fill: var(--sc-surface);
}

/* ── Chrome, all absolutely placed ───────────────────────
   Sizes and weights are the Figma text styles; positions come
   from the frames and arrive via element.style. Nothing here
   sets a margin or a gap that could accumulate. */

.sc-s24 {
  font-size: 24px;
  font-weight: 700;
}
.sc-plot {
  display: block;
}

.sc-gap {
  margin-left: 10px;
}

.sc-rule {
  background: var(--sc-hair);
}
.sc-tipline {
  background: var(--sc-hair);
}

/* The ellipsis / question buttons and the small buttons share
   Runwise's white-button treatment: a #fafafa face with a 1px
   inner ring, a white top highlight and a hairline bottom
   shade. Three inner shadows, exactly as the Figma effect
   style lists them. */
.sc-menu,
.sc-btn {
  border: 0;
  border-radius: 10px;
  background: var(--sc-surface);
  box-shadow:
    inset 0 2px 0 #fff,
    inset 0 0 0 1px var(--sc-hair),
    inset 0 -1px 1px rgba(0, 0, 0, 0.05);
  color: var(--sc-ink);
  font: inherit;
  padding: 0;
}
.sc-menu {
  display: grid;
  place-items: center;
}
.sc-menu svg {
  width: 14px;
  height: 14px;
  display: block;
}
.sc-btn {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.25px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  white-space: nowrap;
}
.sc-btn svg {
  width: 14px;
  height: 14px;
  display: block;
  flex: none;
}

/* The full-width list rows on frames 20 and 21. */
.sc-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 12px;
  border-radius: 12px;
  background: var(--sc-surface);
  color: var(--sc-ink);
}
.sc-row > span:first-child svg,
.sc-row > svg {
  flex: none;
  display: block;
  width: 16px;
  height: 16px;
  color: var(--sc-mute);
}
.sc-row-caret {
  margin-left: auto;
  color: var(--sc-mute);
}
.sc-row-caret svg {
  display: block;
  width: 16px;
  height: 16px;
}

/* Legend key: a 12px circle, 4px gap, then the label. */
.sc-lg {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 14px;
  /* nowrap, because the widths below come from the frames and a
     label that wraps inside its own box pushes the row two
     lines tall and shifts everything under it. */
  white-space: nowrap;
}
.sc-lg svg {
  flex: none;
  display: block;
}
/* Frame 2 alone puts the label before the swatch. */
.sc-lg--rev {
  justify-content: flex-start;
}

.sc-chip {
  display: flex;
  align-items: center;
  gap: 6px;
}
.sc-warn svg,
.sc-warnbox svg {
  display: block;
  width: 14px;
  height: 14px;
}
.sc-warnbox {
  width: 12px;
  height: 12px;
}
.sc-warnbox svg {
  width: 12px;
  height: 12px;
}
.sc-warn {
  color: var(--sc-miss-red);
}
.sc-warn--amber {
  color: var(--sc-miss-amber);
}
.sc-warn--grey {
  color: var(--sc-mute);
}
.sc-warn--none {
  display: none;
}

.sc-alert-bg {
  background: #fff;
  border-radius: 12px;
  box-shadow: inset 0 0 0 1px var(--sc-hair);
}

.sc-badge {
  display: grid;
  place-items: center;
  background: var(--sc-saved-a);
  clip-path: polygon(
    50% 0,
    64% 8%,
    79% 6%,
    88% 19%,
    100% 28%,
    96% 44%,
    100% 59%,
    88% 69%,
    82% 84%,
    66% 84%,
    52% 100%,
    39% 89%,
    24% 91%,
    16% 77%,
    1% 69%,
    5% 52%,
    0 37%,
    13% 26%,
    19% 11%,
    36% 12%
  );
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
}

.sc-original-chart {
  display: block;
}

/* The hovered column on the two tooltip frames. Behind the
   bars, so it reads as the plot lighting up rather than as a
   band drawn over the data. */
.sc-hovercol {
  background: rgba(25, 25, 24, 0.035);
  border-radius: 4px;
}

.sc-tip {
  background: #fff;
  border-radius: 12px;
  border: 1px solid rgba(25, 25, 24, 0.02);
  box-shadow:
    0 4px 6px -1px rgba(0, 0, 0, 0.1),
    0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.sc-tip > * {
  position: absolute;
}
.sc-tiplink {
  color: var(--sc-ink);
  font-weight: 700;
  text-decoration: underline;
}

/* The calculation rows on frame 9. */
.sc-track {
  background: var(--sc-surface);
  border-radius: 4px;
  overflow: hidden;
  display: flex;
}
.sc-fill {
  height: 100%;
}
.sc-fill--grey {
  background: #e6e6e5;
}
.sc-fill--paid {
  background: linear-gradient(90deg, var(--sc-paid-edge), var(--sc-paid-a));
}
.sc-fill--saved {
  background: var(--sc-saved-a);
}

/* The two season blocks on frame 10. */
.sc-blockart {
  border-radius: 4px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
.sc-blockseg--grey {
  background: linear-gradient(180deg, #dcdcdb, #f2f2f1);
}
.sc-blockseg--paid {
  background: linear-gradient(180deg, var(--sc-paid-a), var(--sc-paid-b));
}
.sc-blockseg--saved {
  background: var(--sc-saved-a);
}

/* ═══════════════════════════════════════════════════════════
   The rail. Site chrome, site tokens.
   ═══════════════════════════════════════════════════════════ */

.savings-hero {
  padding: 0 0 calc(var(--u) * 2);
}

.savings-hero-stage {
  background: #f4f3f0;
  border-block: 1px solid rgba(22, 21, 15, 0.1);
  height: 487px;
  overflow: hidden;
  position: relative;
}

/* The hero card starts on the page line, like the drawing in every band.
   It used to be centred in the stage, which put its left edge at 366 —
   a number that belonged to nothing and moved with the window. */
.savings-hero-stage > .sc {
  left: 0;
  position: absolute;
  top: 48px;
  transform: scale(var(--sj-hero-scale, 1));
  transform-origin: top left;
}

/* Overrides only the top, so the shared caption rule in style.css keeps
   carrying the indent onto the content line. */
.savings-hero figcaption {
  margin-block-start: 8px;
}

/* Case-study visuals built from the real card renderer. They use the site's
   neutral diagram ground; only the embedded product cards carry Runwise green. */
.sj-visual-inner {
  margin-inline: 0 auto;
  max-width: 82rem;
}

.sj-compare-grid,
.sj-choice-grid {
  display: grid;
  gap: calc(var(--u) * 2);
}

.sj-compare-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  max-width: 62rem;
  margin-inline: 0 auto;
}

.sj-card-cell {
  min-width: 0;
}

.sj-card-label {
  align-items: baseline;
  display: flex;
  gap: 8px;
  justify-content: space-between;
  margin-block-end: var(--u);
}

.sj-card-label strong {
  font-weight: 600;
}

.sj-card-label span,
.sj-card-note {
  color: var(--ink-50);
  font-size: 14px;
}

.sj-card-note {
  line-height: 20px;
  margin-block-start: 12px;
}

.sj-card-stage {
  overflow: hidden;
  position: relative;
  width: 100%;
}

.sj-card-stage > .sc {
  left: 50%;
  position: absolute;
  top: 0;
  transform: translateX(-50%) scale(var(--sj-card-scale, 1));
  transform-origin: top center;
}

/* style.css deliberately gives editorial diagram SVGs a 34rem floor. Product
   cards contain many small SVGs of their own, so they opt back into their
   intrinsic dimensions when a card is embedded inside a diagram band. */
.diagram .sc svg {
  height: auto;
  margin: 0;
  max-width: none;
  min-width: 0;
  width: auto;
}

.diagram .sc .sc-btn svg,
.diagram .sc .sc-menu svg,
.diagram .sc .sc-warn svg {
  height: 14px;
  width: 14px;
}

.diagram .sc .sc-lg svg,
.diagram .sc .sc-warnbox svg {
  height: 12px;
  width: 12px;
}

.diagram .sc .sc-row svg {
  height: 16px;
  width: 16px;
}

.sj-change {
  align-items: center;
  background: #fff;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--dg-rule);
  display: flex;
  font-size: 14px;
  font-weight: 600;
  gap: 8px;
  margin: calc(var(--u) * 2) auto 0;
  padding: 8px 14px;
  width: max-content;
}

.sj-change span {
  color: var(--ink-50);
  font-weight: 400;
}

/* The wall of interview notes and the map sit in one band, in that
   order, because the order is the claim: these four notes are what
   arrived, and the row below them is what I made of it. Two bands
   would have read as two unrelated figures. */
.note-wall + .evidence-map {
  margin-block-start: calc(var(--u) * 2.5);
}

.evidence-map {
  display: grid;
  gap: var(--u);
  grid-template-columns: repeat(4, minmax(0, 1fr));
  list-style: none;
  margin-inline: 0 auto;
  max-width: 68rem;
  padding: 0;
}

.evidence-map li {
  background: var(--dg-surface);
  border-radius: var(--dg-radius);
  box-shadow: var(--dg-lift);
  display: grid;
  gap: 4px;
  min-height: 148px;
  padding: calc(var(--u) * 1.25);
  position: relative;
}

.evidence-map li:not(:last-child)::after {
  color: var(--dg-ink-40);
  content: "→";
  font-size: 20px;
  inset-inline-end: calc(var(--u) * -1.4);
  position: absolute;
  top: 62px;
  z-index: 1;
}

.evidence-map strong {
  font-weight: 600;
}

.evidence-map li > span:last-child {
  color: var(--ink-50);
  font-size: 14px;
  line-height: 20px;
}

.evidence-value {
  align-items: center;
  background: var(--dg-accent-wash);
  border-radius: 999px;
  color: var(--dg-accent-ink);
  display: flex;
  font-size: 13px;
  font-weight: 600;
  height: 30px;
  justify-content: center;
  margin-block-end: 8px;
  width: 48px;
}

.sj-choice-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-inline: 0 auto;
  max-width: 68rem;
}

.sj-choice-card {
  background: rgba(255, 255, 255, 0.52);
  border-radius: var(--dg-radius);
  box-shadow: inset 0 0 0 1px var(--dg-rule);
  padding: var(--u);
}

.sj-choice-card[data-chosen="true"] {
  background: var(--dg-accent-wash);
  box-shadow: inset 0 0 0 1px var(--dg-accent-rule);
}

.sj-choice-status {
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--dg-rule-strong);
  color: var(--ink-50);
  font-size: 12px;
  padding: 3px 8px;
}

.sj-choice-card[data-chosen="true"] .sj-choice-status {
  background: var(--dg-accent-wash);
  box-shadow: none;
  color: var(--dg-accent-ink);
}

.sj-decisions {
  display: grid;
  gap: var(--u);
  grid-template-columns: repeat(4, minmax(0, 1fr));
  list-style: none;
  /* The four-decision strip stands between its own section and the
     rail band, so it owes the band the same run-up style.css gives a
     block that sits directly above one. Without this the only band on
     the page approached on 32px of air instead of 64. */
  margin: var(--u) 0 calc(var(--u) * 4);
  padding: 0;
}

.sj-decisions li {
  display: grid;
  gap: 2px;
  padding-block-start: 12px;
  position: relative;
}

.sj-decisions li::before {
  background: var(--rule);
  content: "";
  height: 1px;
  inset-inline: 0;
  position: absolute;
  top: 0;
}

.sj-decisions li:first-child::before,
.sj-decisions li:last-child::before {
  background: rgba(96, 160, 0, 0.55);
}

.sj-decisions span,
.sj-decisions small {
  color: var(--ink-50);
  font-size: 13px;
}

.sj-decisions strong {
  font-weight: 600;
}

@media (max-width: 48rem) {
  .sj-compare-grid {
    grid-template-columns: 1fr;
  }

  .evidence-map,
  .sj-decisions {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .evidence-map li:nth-child(2)::after {
    display: none;
  }

  .sj-choice-grid {
    grid-auto-columns: minmax(260px, 80vw);
    grid-auto-flow: column;
    grid-template-columns: none;
    overflow-x: auto;
    padding-block-end: 8px;
    scroll-snap-type: x proximity;
  }

  .sj-choice-card {
    scroll-snap-align: start;
  }
}

@media (max-width: 30rem) {
  .evidence-map,
  .sj-decisions {
    grid-template-columns: 1fr;
  }

  .evidence-map li:not(:last-child)::after {
    display: none;
  }
}

/* ── The rhythm study ────────────────────────────────────
   A construction drawing over the shipped card: the sheet is a
   white surface with the card's own 20px module printed on it.
   Geometry is the RH block in savings-card.js; this file is
   paint only.

   The annotation ink is graphite, and the one accent is the
   site's red rather than its green — green is the card's data
   colour, and an annotation in it would have read as another
   series. Red appears three times and only three times: on the
   interval that repeats. It is never the only signal, since
   each mark is also a measured shape with a number beside it.

   No monospace, for the reason activity-grid.css gives: what a
   terminal face would be doing here is holding digits in a
   column, and tabular-nums does that in the site's own face. */
.rhythm {
  --rh-key: #d93025;
  --rh-line: rgba(22, 21, 15, 0.55);
}

.rhythm-stage {
  margin-inline: 0 auto;
  overflow: hidden;
  position: relative;
}

/* The card's content frame starts at 172, 44 inside the sheet,
   so the module is offset to put a grid intersection on that
   corner. The card is built on twenties; the bill columns are
   not, and the drawing is more honest for showing it. */
.rhythm-frame {
  background-color: var(--dg-surface);
  background-image:
    linear-gradient(to right, rgba(22, 21, 15, 0.055) 1px, transparent 1px),
    linear-gradient(rgba(22, 21, 15, 0.055) 1px, transparent 1px);
  background-position: 172px 44px;
  background-size: 20px 20px;
  box-shadow: inset 0 0 0 1px var(--dg-rule);
  height: 452px;
  position: relative;
  transform: scale(var(--rh-scale, 1));
  transform-origin: top left;
  width: 660px;
}

.rhythm-frame > .sc {
  position: absolute;
}

/* style.css sizes editorial figures to their column. This one
   is placed in CSS pixels over a card measured in the same
   pixels, so it keeps its own dimensions. */
.rhythm .rhythm-overlay {
  height: 452px;
  inset: 0;
  max-width: none;
  min-width: 0;
  pointer-events: none;
  position: absolute;
  width: 660px;
}

.rhythm-overlay path {
  fill: none;
}

.rhythm-overlay text {
  font-variant-numeric: tabular-nums;
}

/* The cadence ruler above the plot: a beat for each bill, a
   filled rest for each gap. */
.rh-beat {
  fill: var(--rh-line);
  fill-opacity: 0.07;
  stroke: var(--rh-line);
  stroke-opacity: 0.35;
  stroke-width: 1;
}

.rh-lead {
  stroke: var(--rh-key);
  stroke-opacity: 0.45;
  stroke-width: 1;
}

.rh-rep {
  fill: var(--rh-key);
  fill-opacity: 0.16;
  stroke: var(--rh-key);
  stroke-opacity: 0.55;
  stroke-width: 1;
}

.rh-edge {
  fill: none;
  stroke: var(--rh-line);
  stroke-opacity: 0.35;
  stroke-width: 1;
}

.rh-box {
  fill: none;
  stroke: var(--rh-line);
  stroke-dasharray: 4 4;
  stroke-opacity: 0.6;
  stroke-width: 1;
}

.rh-mark {
  stroke: var(--rh-line);
  stroke-width: 1;
}

.rh-guide {
  stroke: var(--rh-line);
  stroke-dasharray: 2 5;
  stroke-opacity: 0.45;
  stroke-width: 1;
}

.rh-bracket,
.rh-rest {
  stroke: var(--rh-line);
  stroke-opacity: 0.7;
  stroke-width: 1;
}

.rh-witness {
  stroke: var(--rh-line);
  stroke-dasharray: 2 4;
  stroke-opacity: 0.4;
  stroke-width: 1;
}

.rh-dim {
  stroke: var(--rh-line);
  stroke-opacity: 0.7;
  stroke-width: 1;
}

.rh-kicker {
  fill: var(--dg-ink);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.rh-sub {
  fill: var(--ink-50);
  font-size: 11px;
}

.rh-band-t {
  fill: var(--dg-ink);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.rh-rest-t {
  fill: var(--ink-50);
  font-size: 11px;
}

.rh-dim-t {
  fill: var(--ink-50);
  font-size: 11px;
}

.rh-note {
  font-size: 11px;
  font-weight: 600;
}

.rh-key {
  fill: var(--rh-key);
  font-weight: 600;
}

.sj {
  width: 100vw;
  max-width: none;
  margin-inline: calc(50% - 50vw);
  /* Same mirror-the-margin padding .diagram uses, so this band's
     content box lands on .wrap's rather than on the viewport's. */
  padding-block: calc(var(--u) * 3);
  padding-inline: calc(50vw - 50%);
  background: #f4f3f0;
  border-block: 1px solid rgba(22, 21, 15, 0.1);
}

/* No cap. The 60rem one left this band's content 16px short of the page
   edge that every rule above and below it lands on — near enough to look
   like a mistake rather than a margin. The band already sits on the page
   measure, which is the widest anything here gets. */
.sj-inner {
  margin-inline: 0 auto;
}

/* Stage and caption sit side by side once there is room for
   the card at its native 436 plus a readable column beside
   it. Below that the caption goes under the card. */
.sj-stage {
  display: grid;
  gap: calc(var(--u) * 2);
  align-items: start;
  justify-items: center;
}
@media (min-width: 60rem) {
  .sj-stage {
    grid-template-columns: 436px minmax(0, 1fr);
    gap: calc(var(--u) * 3);
    justify-items: stretch;
  }
}

.sj-note {
  max-width: var(--measure);
}
.sj-count {
  font-size: 14px;
  color: var(--ink-50);
  margin: 0 0 calc(var(--u) * 0.5);
}
.sj-act {
  font-size: 14px;
  color: var(--ink-50);
  margin: 0 0 calc(var(--u) * 0.5);
}
.sj-title {
  font-size: 16px;
  font-weight: 600;
  margin: 0 0 calc(var(--u) * 0.5);
  line-height: var(--line);
}
.sj-why {
  margin: 0;
  line-height: var(--line);
}
.sj-why + .sj-why {
  margin-top: calc(var(--u) * 0.75);
}
.sj-verdict {
  margin: calc(var(--u)) 0 0;
  padding-top: calc(var(--u) * 0.75);
  border-top: 1px solid var(--rule);
  color: var(--ink-50);
}

/* ── Controls ────────────────────────────────────────────── */

.sj-controls {
  display: flex;
  align-items: center;
  gap: var(--u);
  margin-top: calc(var(--u) * 2);
  flex-wrap: wrap;
}
.sj-btn {
  font: inherit;
  padding: 6px 14px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: #fff;
  color: var(--ink);
}
.sj-btn[disabled] {
  color: var(--ink-50);
  opacity: 0.5;
}
.sj-btn:not([disabled]):hover {
  border-color: var(--underline);
}

/* One tick per frame. A button, because it is one — arrow
   keys move between them and each is individually reachable,
   which a row of divs with a click handler would not be. */
.sj-ticks {
  display: flex;
  gap: 3px;
  flex-wrap: wrap;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sj-tick {
  width: 18px;
  height: 26px;
  padding: 0;
  border: 0;
  background: none;
  display: grid;
  place-items: center;
}
.sj-tick i {
  display: block;
  width: 100%;
  height: 4px;
  border-radius: 2px;
  background: rgba(22, 21, 15, 0.18);
}
.sj-tick:hover i {
  background: rgba(22, 21, 15, 0.4);
}
.sj-tick[aria-current="true"] i {
  background: var(--ink);
  height: 8px;
}
/* The frames that survived into the shipped card. Marked so
   the rail shows, without being read, that the journey has
   four or five load-bearing moments and nineteen others. */
.sj-tick--key i {
  background: rgba(7, 173, 0, 0.55);
}
.sj-tick--key:hover i {
  background: rgba(7, 173, 0, 0.85);
}

.sj-legendkey {
  font-size: 14px;
  color: var(--ink-50);
  margin: calc(var(--u)) 0 0;
}

/* ── Contact sheet ───────────────────────────────────────── */

/* Wider than .sj-inner's 60rem on purpose. The rail is one
   card next to a paragraph and wants a reading measure; the
   sheet's whole argument is that you can see all twenty-four
   at once, and capping it at the rail's width would cost two
   columns to no benefit. Still bounded, so it does not become
   a single row of thumbnails on a very wide monitor. */
.sj-sheet {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: calc(var(--u) * 2);
  max-width: 82rem;
  margin-inline: 0 auto;
}

/* If the script never runs, the sheet's band is an empty
   strip of grey with nothing in it and no way to know why.
   The rail above keeps its fallback paragraph and explains
   for both; this one just gets out of the way. */
.sj:empty {
  display: none;
}
.sj-cell {
  margin: 0;
}
/* The card is drawn at 436 and shown smaller. Scaling the
   whole card rather than re-laying it out at a small size is
   deliberate: it keeps every proportion exactly as it is in
   the stage above, which is the only thing a contact sheet is
   for. */
/* Height is set in JS from the drawn card's own height times
   the measured scale. It cannot be written here: the card is
   436 wide and however tall its contents make it, and that
   varies by eight frames' worth between the shortest version
   and the tallest. */
.sj-cell-stage {
  width: 100%;
  overflow: hidden;
  position: relative;
}
.sj-cell-stage .sc {
  position: absolute;
  top: 0;
  left: 0;
  transform: scale(var(--sj-scale, 1));
  transform-origin: top left;
  width: 436px;
}
.sj-cell figcaption {
  margin-top: calc(var(--u) * 0.75);
  font-size: 14px;
  line-height: 20px;
  color: var(--ink-50);
}
.sj-cell figcaption b {
  color: var(--ink);
  font-weight: 600;
  display: block;
}

.sj-fallback {
  max-width: var(--measure);
  margin-inline: 0 auto;
  color: var(--ink-50);
}

@media (prefers-reduced-motion: no-preference) {
  .sj-stage .sc {
    transition: none;
  }
}

/* The late frames carry a "?" beside the ellipsis. Two 32px
   buttons in the header slot, not one. */
.sc-menus {
  flex: none;
  display: flex;
  gap: 8px;
}
