/* ============================================================
   CASE STUDY SYSTEM (v2) — Figma-matched
   ============================================================
   Reusable layout + components for case-study pages
   (Website Redesign, Banking Products, AI Products,
   Research & Extras) and the About page.

   Namespaced `.study-*` so it can live alongside the older
   `.cs-*` classes until every page is migrated.

   Design source: Figma "Portfolio" → Case Study/Website Redesign

   ⚑ NO UPPERCASE, TITLE CASE INSTEAD, SITE-WIDE (Daniel,
   2026-08-28). Every kicker, eyebrow, caption, tag, device label
   and button on this site is Title Case. 34 rules across about/
   case-study/research-viz/resume.css lost `text-transform:
   uppercase` in one sweep, then all ~82 label strings were retyped
   in title case. Only real acronyms stay in caps: SC 2.5.8, WEVO,
   DG, AI, CEO, MFA, FAQ, UX, AVP.
   Title case here is AP style: capitalise everything except
   articles, coordinating conjunctions and prepositions of three
   letters or fewer (a, an, the, and, as, at, by, for, in, of, on,
   or, to, up), and always capitalise the first word, the last word
   and the word after a colon. Four-letter prepositions DO take a
   capital (From, With, Into, Over, Behind, Across, Against).
   Prefix hyphens keep the second half lowercase ("Non-text
   Content"); real compounds capitalise both ("Runner-Up").

   ⚑ LABEL SIZE (Daniel, 2026-08-29): a label sitting in open page
   space is 11px (0.6875rem). This SUPERSEDES the 13px call of
   2026-08-28, which shipped for one day. Daniel saw both and chose
   11: at 13px a kicker that is really a seven-word phrase reads as
   a second headline rather than as a marker.

   The other half of the rule is unchanged: a label sitting INSIDE a
   figure keeps that figure's own type scale, because a figure is a
   self-contained drawing with its own hierarchy and a page-space
   size steps on it. So all of research-viz.css stays small, and so
   do .wv-ctl-label, .ia-tmpl and the .gps-punch rail in this file.
   Before changing a size, compare the label to the ONE pair that
   carries the hierarchy, not to every neighbour: the two failure
   shapes are inversion (the label outgrows the text it labels) and
   flat rail (a bold number loses its lead over its own label).

   The tracking goes with the caps. Every one of those rules paired
   uppercase with .06–.16em, which exists to open up capitals and
   sprays sentence case; all of them are now .01em. Do not reinstate
   either half.
   ============================================================ */

/* ── Theme tokens ──────────────────────────────────────────────
   The rust gradient + rainbow line are the case-study theme.
   Set per-page on .study-page so each category can swap palette.
   ────────────────────────────────────────────────────────────── */
/* Hide the native scrollbar (scrolling still works) so layout
   centers on the full window width — identical to the homepage,
   whose above-fold lock means it never shows a scrollbar. Without
   this, a classic scrollbar shifts the whole page ~7px left and
   the nav visibly jumps when switching pages. */
html:has(body.study-page) {
  scrollbar-width: none;
}
html:has(body.study-page)::-webkit-scrollbar {
  display: none;
}

.study-page {
  /* Theme scale — rust (Website Redesign) by default. Other pages
     swap the whole palette with a body class (see .theme-* below);
     the 4-step color scales themselves live in nav.css :root. */
  --study-1: var(--rust-1, #492109);
  --study-2: var(--rust-2, #924311);
  --study-3: var(--rust-3, #c95c18);
  --study-4: var(--rust-4, #e7732d);

  /* Rainbow footer line */
  --rainbow-1: #076a7f;
  --rainbow-2: #efaf21;
  --rainbow-3: #e7732d;
  --rainbow-4: #ad2831;

  /* This page's accent = brightest theme line */
  --study-accent: var(--study-4);

  /* Vertical page guides — same grid as the header (1440px
     container + content pad). The logo's left edge sits exactly
     on the left guide; the frame verticals hug the guides from
     the inside and never extend past them. */
  --page-guide: calc(max((100vw - 1440px) / 2, 0px) + var(--content-pad));

  /* The exit-right corner (.sfr-hero-br-out) pokes past the right
     edge on narrow desktop widths (guide < 89px) — clip instead of
     growing a horizontal scroll. */
  overflow-x: clip;
}

/* ── Per-page theme swaps ──
   Pair with the matching logo variant (assets/logos/dg-logo-*.svg
   hexes inlined in each page's header) + corner-*-<theme>.svg
   frame corners, since <img> SVGs can't read these variables. */
.study-page.theme-crimson {
  --study-1: var(--crimson-1);
  --study-2: var(--crimson-2);
  --study-3: var(--crimson-3);
  --study-4: var(--crimson-4);
}
.study-page.theme-gold {
  --study-1: var(--gold-1);
  --study-2: var(--gold-2);
  --study-3: var(--gold-3);
  --study-4: var(--gold-4);
}
.study-page.theme-teal {
  --study-1: var(--teal-1);
  --study-2: var(--teal-2);
  --study-3: var(--teal-3);
  --study-4: var(--teal-4);
}
/* About page — Figma ThemeBorder Property 1=Rainbow (node 1393:390):
   one stripe per case-study hue, crimson outermost → teal hugging
   the content, so the page-top entry reads in the same top-to-
   bottom order as the homepage rainbow band. */
.study-page.theme-rainbow {
  --study-1: var(--rainbow-4);   /* crimson #ad2831 */
  --study-2: var(--rainbow-3);   /* rust    #e7732d */
  --study-3: var(--rainbow-2);   /* gold    #efaf21 */
  --study-4: var(--rainbow-1);   /* teal    #076a7f */
  /* brightest-line accent reads poorly for the rainbow scale
     (rust is the liveliest) */
  --study-accent: var(--rainbow-3);
}

/* ══════════════════════════════════════════════════════════════
   CASE-STUDY FOLDER TABS (Banking Products, AI Products)
   ══════════════════════════════════════════════════════════════
   Pages holding several case studies get manila-folder tabs
   riding the hero band's top ribbon run — one tab per study.
   Anatomy from Figma Frame 5143: icon + Roboto Slab Light 13/1.3.
   The active tab is "the open folder": it fills with the theme's
   darkest line (--study-1) so it reads as part of the ribbon's
   outer stripe it sits on; inactive tabs are outlined and muted.
   js/tabs.js drives the swapping (+ ?study= deep links).
   ══════════════════════════════════════════════════════════════ */
.study-tabs {
  width: 100%;
  max-width: 1440px;
  margin: 40px auto 0;
  padding: 0 calc(var(--content-pad) + 44px);
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 8px;
  position: relative;
  z-index: 1;
}

.study-tab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 18px;   /* 44px tall — WCAG touch-target minimum */
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 10px 10px 0 0;
  background: transparent;
  cursor: pointer;
  font-family: var(--font-mono);          /* Roboto Slab */
  font-weight: 300;
  font-size: 0.6875rem;                   /* 11px */
  letter-spacing: 1.3px;
  color: var(--text-muted);
  transition: color 0.2s ease, background-color 0.2s ease,
              border-color 0.2s ease,
              box-shadow 0.2s ease;
  /* padding is deliberately NOT transitioned (2026-09-10). The
     selected tab is 5px taller (padding-top 16px vs 11px). When
     that was animated, the outgoing tab shrank while the incoming
     one grew, so for ~200ms neither was full height, the strip
     (align-items: flex-end) dipped 48 -> 46 -> 48px, and every
     band below bounced 2px. Measured frame by frame in headless
     Chrome. Swapping the padding in one style recalc keeps the
     strip's height constant, so the content never moves. */
}
/* Hover: the folder edge lights up with the 4-ring bloom the
   dropdown items use (nav.css .nav-item:hover) — the tab's own
   border plays the dark outer ring, three more bloom inside it.
   Where the tab meets the ribbon, the ring order mirrors the
   ribbon's dark-on-top stripes. */
.study-tab:hover:not([aria-selected="true"]),
.study-tab:focus-visible:not([aria-selected="true"]) {
  color: var(--text-primary);
  background: var(--surface);
  border-color: var(--study-1);
  box-shadow:
    inset 0 0 0 1px var(--study-2),
    inset 0 0 0 2px var(--study-3),
    inset 0 0 0 3px var(--study-4);
  font-weight: 700;
}
.study-tab:focus-visible {
  outline: 2px solid var(--study-3);
  outline-offset: 2px;
}
.study-tab[aria-selected="true"] {
  background: var(--study-1);             /* continues the ribbon's dark outer stripe */
  border-color: var(--study-1);
  color: var(--text-bright, #eeedeb);
  font-weight: 700;
  padding-top: 16px;                      /* the open folder sits a step taller */
}

/* Width reserve: an invisible zero-height bold twin stacked
   under the label locks the box at bold width, so bolding on
   hover/select can't nudge the neighboring tabs. (Not the grid-
   overlay trick — an anonymous text box can't share the pseudo's
   cell and gets bumped to a second row.) */
.tab-label {
  display: inline-block;
  white-space: nowrap;
}
.tab-label::after {
  content: attr(data-text);
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: 700;
}

/* "In progress" flag on unwritten studies */
.tab-flag {
  font-size: 0.6875rem;                   /* 9px */
  font-weight: 300;
  letter-spacing: .01em;
  opacity: 0.65;
}

/* Tab icon — same 4-stack accordion ripple as the nav dropdown
   (nav.css .nav-ico), keyed to this page's theme scale. Base icon
   follows the label color; 2–4 peek out theme-tinted on hover. */
.tab-icon-wrap {
  position: relative;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  overflow: visible;
}
.tab-ico {
  position: absolute;
  top: 0; left: 0;
  width: 20px; height: 20px;
  transition: transform 0.32s var(--ease-out), opacity 0.2s ease;
  stroke-width: 2;
}
.tab-ico svg {
  width: 20px; height: 20px;
  stroke: currentColor;
  stroke-width: 2;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  display: block;
}
/* Ripple copies drop tiny dot details — lucide draws them as
   "h.01" stub paths (the ghost's eyes) or small <circle>s (the
   map-pin's center dot): stacked dots read as mush. The top
   icon keeps its details. */
/* (lucide swaps the <i> for the svg itself, so .tab-ico-N IS the
   svg — select the shapes as descendants, not via an svg child) */
.tab-ico-2 path[d$="h.01"],
.tab-ico-3 path[d$="h.01"],
.tab-ico-4 path[d$="h.01"],
.tab-ico-2 circle,
.tab-ico-3 circle,
.tab-ico-4 circle { display: none; }

.tab-ico-1 { z-index: 4; color: currentColor; }
.tab-ico-2 { z-index: 3; opacity: 0; color: var(--study-2); }
.tab-ico-3 { z-index: 2; opacity: 0; color: var(--study-3); }
.tab-ico-4 { z-index: 1; opacity: 0; color: var(--study-4); }

/* Ripple invites a click — the open folder doesn't need one */
.study-tab:hover:not([aria-selected="true"]) .tab-ico-2,
.study-tab:focus-visible:not([aria-selected="true"]) .tab-ico-2 { opacity: 1; transform: translateX(-1px); transition-delay: 0s; }
.study-tab:hover:not([aria-selected="true"]) .tab-ico-3,
.study-tab:focus-visible:not([aria-selected="true"]) .tab-ico-3 { opacity: 1; transform: translateX(-2px); transition-delay: 0.05s; }
.study-tab:hover:not([aria-selected="true"]) .tab-ico-4,
.study-tab:focus-visible:not([aria-selected="true"]) .tab-ico-4 { opacity: 1; transform: translateX(-3px); transition-delay: 0.10s; }

/* In-progress placeholder study (single band, centered).
   Doubled class beats .study's later `padding` shorthand, which
   would otherwise zero out the vertical padding here. */
.study.study-wip {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 20px;
  padding-top: var(--sp-7);
  padding-bottom: var(--sp-7);
}

/* Below the frame breakpoint the ribbon is gone — close the tabs
   into free-standing pills and pull them onto the content grid. */
@media (max-width: 1100px) {
  /* margin-bottom: pills no longer touch a ribbon, so give the
     hero headline air — without it they collide when tabs wrap */
  .study-tabs { padding: 0 var(--content-pad); margin-bottom: var(--sp-4); }
  .study-tab,
  .study-tab[aria-selected="true"] {
    border-bottom: 1px solid var(--border);
    border-radius: 10px;
    padding-top: 11px;
  }
  .study-tab[aria-selected="true"] { border-color: var(--study-1); }
}

/* ⚑ 2026-09-11, DANIEL'S CALL, from a phone: on a study with more than one
   sub-study, stack the tabs instead of wrapping them. They were a wrapping
   flex ROW, so at 390px "Transfer & Pay Hub" and "Banking GPS" shared line one
   and "Kiosk Redesign" sat alone on line two, which reads as two groups rather
   than one set of three. Column + stretch puts each on its own full-width row,
   in source order, and every button becomes the same width and an easier
   target. ⚑ 640, not 1100: between them the row still fits its tabs on one
   line, and stacking there would waste three rows of height for nothing. */
@media (max-width: 640px) {
  .study-tabs { flex-direction: column; align-items: stretch; }
  .study-tab,
  .study-tab[aria-selected="true"] { width: 100%; justify-content: flex-start; }
}

/* ── Page shell ────────────────────────────────────────────────
   Figma frame is 1440 wide; content column ≈ 1200 with side
   padding — the shell now matches that frame exactly.
   ────────────────────────────────────────────────────────────── */
.study {
  width: 100%;
  max-width: 1440px;      /* same container as the header */
  margin: 0 auto;
  /* content sits 44px inside the guide → 24px clear of the
     frame's inner edge (guide + 20px of rust lines) */
  padding: 0 calc(var(--content-pad) + 44px);
}

/* 40px of air between the nav and the first rust line */
#main-content > .study-band:first-child { margin-top: 40px; }

/* ── Typography primitives ─────────────────────────────────────
   Text/Heading/XL  Merriweather 40 / 100%
   Text/Heading/L   Merriweather 32 / 100%
   Body             Roboto 16 / 1.65
   Meta             Roboto 13 / 1.65
   Label            Roboto Slab, letter-spaced
   ────────────────────────────────────────────────────────────── */
.study-title {          /* hero headline — Text/Heading/XL */
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.9rem, 3.4vw, 2.5rem);   /* 40px */
  line-height: 1.15;
  color: var(--text-primary);
  text-align: center;
  max-width: 1068px;
  margin: 0 auto;
}

.study-h2 {             /* section heading — Text/Heading/L */
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.6rem, 2.6vw, 2rem);     /* 32px */
  line-height: 1.2;
  color: var(--text-primary);
}

.study-body {
  font-family: var(--font-sans);
  font-size: 1rem;                            /* 16px */
  line-height: 1.65;
  color: var(--text-primary);
  /* No last line left holding one short word. Rewording fixes a
     paragraph at one column width and orphans it at another, and this
     column is fluid, so the rule belongs here rather than in the copy.
     `balance` is wrong for body text: it evens every line and is
     capped at a few lines by most engines. Browsers without it wrap
     as before. */
  text-wrap: pretty;
}
.study-body b, .study-body strong { font-weight: 700; }

.study-lead {           /* bold Merriweather lead-in inside body */
  font-family: var(--font-serif);
  font-weight: 700;
  font-size: 1.125rem;                        /* 18px */
  line-height: 1.65;
}
/* Solution decision titles break onto their own line (Daniel 2026-08-19:
   "Problem tackled" running on inline "just looks weird"). Scoped to
   .study-solution so Impact's inline leads ("Business outcome:") and the
   Learnings leads keep flowing with their sentences; every .study-lead
   inside a Solution section is a decision title, on all three tabs and
   the other case-study pages alike. */
.study-solution .study-richtext .study-lead { display: block; margin-bottom: .3em; }

/* Prose that has a graphic beside it. The two-column process layout
   never said which sentence a given visual belonged to, so a reader
   had to infer the pairing from vertical proximity, which the row
   centring routinely broke.

   Deliberately not a link and not a callout. Anything louder would
   turn six paragraphs of prose into a field of buttons. The arrow
   follows the layout, since at phone width the visual is underneath
   rather than beside.

   No underline. Weight plus a hairline plus a trailing arrow is the
   complete hyperlink costume, and this page carries real links a few
   inches away in the report library. A reader clicks it, nothing
   happens, and a cue that reads as a dead link is worse than no cue.
   Weight marks the phrase, the arrow does the pointing, and neither
   promises a destination. */
.study-seealso {
  font-weight: 600;
}
/* ⚑⚑⚑ THE ARROW WAS THINNED THREE WAYS AND IS NOW STROKED. Daniel, 2026-08-31:
   "it kind of gets lost in the comparison of how skinny or thin the arrow is
   compared to the bolded font." Measured, and he was right: ink density 0.46
   against the 600-weight phrase's 0.875, because the glyph was quieted THREE
   times over — 0.85em, 0.8 opacity, and U+2192 carrying about half the ink of a
   bold letter even when set bold.
   ⚑⚑ SIZE IS NOT THE LEVER. At full text size the density is still 0.467. It is
   a STROKE fault, not a scale fault, so anything that only grows the arrow will
   disappoint. Do not "fix" this again by raising font-size.
   ⚑ This is CANDIDATE C of six built and measured in
   sandbox/seealso-arrow-weight.html. It takes density 0.46 -> 0.54 and its
   advance stays 16.62px, byte-identical to the shipped metric, so it CANNOT move
   a line break on any of the 20 ties site-wide.
   ⛑ THE COUNT MOVED TWICE ON THE DAY IT WAS WRITTEN and will move again: it read 19
   when this rule shipped, then D4's tie was dropped (-1) and website-redesign gained two
   (+2). 13 in banking-products, 5 in uxresearch, 2 in website-redesign. Recount it.
   ⚑⚑ E IS A LIVE FALLBACK, NOT A REJECTED OPTION. Daniel: "Let's go with C and if
   I don't end up liking it - we can always go with E." E is
   `opacity:1; font-size:1em; -webkit-text-stroke:.4px currentColor` — denser and
   more present, and it reflows NOTHING today, measured across all 13 ties in this
   file, 5 on uxresearch and 2 on website-redesign, on 2026-08-31.
   ⛑ THE MEASUREMENT'S SCOPE, stated because a stale one is worse than none: the
   original run covered 19 ties. banking-products has since dropped to 13, and a REMOVAL
   cannot introduce a reflow, so that direction is safe by construction. website-redesign's
   2 were added AFTER the run and were re-measured separately under C: zero movement on
   either, both is-below. Its only cost is headroom: 19.38px
   advance against C's 16.62, so a later copy edit near a line end could tip a tie
   onto a new line with E where C would not, surfacing inside a locked band with no
   obvious cause. Swapping to E is a one-line change; do not rebuild the sandbox.
   ⚑ Two glyph routes were measured and rejected: U+279C is denser (0.64) but is a
   Dingbat with NO matching down arrow and can render as a colour emoji, and a
   masked-SVG arrow gives exact stroke control but is an inline-block, so the line
   can break between the arrow and the sentence's own full stop. A font glyph never
   does that. */
.study-seealso::after {
  content: "\2192";                             /* → */
  margin-left: 0.3em;
  font-size: 0.85em;
  color: var(--study-2);
  opacity: 1;
  -webkit-text-stroke: 0.5px currentColor;
}

/* .is-below — the visual is UNDER this sentence at every width, so the arrow
   never flips. The base rule points → above 860px, which would aim a full-width
   figure's tie at the empty margin beside it. Measured 2026-08-30 across all 25
   prose-and-visual pairings on the site: 22 sit beside, 3 sit below, and NONE sits
   above. Two directions is the whole vocabulary; a visual above its own sentence
   is a layout bug, not a third arrow. Specificity (0,2,1) beats both the base rule
   and the stacked-layout override, so source order here does not matter. */
.study-seealso.is-below::after { content: "\2193"; }   /* ↓ */

@media (max-width: 860px) {
  .study-seealso::after { content: "\2193"; }   /* ↓ */
}

.study-body + .study-body { margin-top: var(--sp-3); }

/* ══════════════════════════════════════════════════════════════
   HERO
   ══════════════════════════════════════════════════════════════ */
.study-hero {
  padding-top: var(--sp-6);
  padding-bottom: var(--sp-7);
}

.study-hero-headline {
  margin-bottom: var(--sp-6);
}
/* ⚑⚑ PHONE-SCOPED BALANCE, added 2026-09-02, and the scoping is the whole point.
   The 09-02 hedge ("Same traffic" -> "Nearly the same traffic") pushed the
   website-redesign H1 to five lines at 380-404px and stranded "visits:" on a line
   measuring 24-25% of the column. That band is iPhone 12/13/14/15 (390/393) and
   16 (402); 360, 375, 412 and 430 were all clean, and so was every sibling H1 —
   measured at 386/393, their shortest lines run 54-84% of the measure.
   ⚑ THE HEDGE CAUSED IT, PROVEN NOT ASSUMED: restoring "Same" in the same DOM at
   the same widths returns four clean lines with a 56-57% minimum.
   ⚑⚑ THREE OTHER REMEDIES WERE MEASURED AND ALL THREE ARE WORSE. Do not retry
   them: `account-opening&nbsp;visits:` changes nothing at 386-393 and adds a
   SIXTH line at 360/375; wrapping that phrase in .nb OVERFLOWS the measure at 360
   (107%) and 375 (102%); and unscoped text-wrap:balance fixes the phones but
   DESTROYS THE DESKTOP COLON BREAK, rendering "...22x the account-opening /
   visits: what changed..." at 1280 — which is the exact mid-clause break the two
   nbsp binds in this H1 exist to prevent.
   ⚑ House vocabulary, not a new device: .gpr-cap already uses text-wrap:balance
   for a one-word orphan "without touching the wording". Same problem, same tool.
   ⚑ THE TRADE, stated because it is real: balancing moves the colon off a line
   end at 386-402, so the two halves blend mid-line. That is already what 360 and
   375 do today, so it is consistent rather than new.
   ⚑ 1280 and above are untouched by construction. Verify there before widening
   this breakpoint.
   ⚑⚑ THIS RULE REACHES SEVEN HEADLINES ACROSS FOUR FILES, NOT ONE. A class plus a
   media query scope by ELEMENT and VIEWPORT, never by page: .study-hero-headline
   appears 3x in banking-products, 2x in ai-products, and once each in
   uxresearch and website-redesign. ⚑ I claimed to a parallel session that the rule
   "cannot reach your heroes" and that was simply wrong; a shared stylesheet has no
   page scope. GREP THE SELECTOR ACROSS ALL SIX FILES BEFORE SAYING A RULE HERE
   CANNOT REACH SOMETHING.
   ⚑ ALL SEVEN WERE THEN MEASURED AT 386px, with the rule and with it forced off in
   the same DOM. The three banking-products heroes are IDENTICAL either way (last
   lines at 79%, 82%, 74%) because they already break evenly. ai-products'
   FindaFan headline IMPROVES, its shortest line going 42% to 66%; its ReelScared
   headline is one line and unaffected. Zero horizontal overflow anywhere. So the
   rule is a net gain beyond the page it was written for, which is an argument for
   leaving it global rather than scoping it to one study. */
@media (max-width: 430px) {
  .study-hero-headline { text-wrap: balance; }
}

/* Two-column body: text left (≈ 645), visual right (≈ 566) */
.study-hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.14fr) minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}

.study-hero-col {
  display: flex;
  flex-direction: column;
  gap: 28px;
}

/* The defining noun in a hero's first line: the thing the study is ABOUT.
   ⚑⚑ BOLD ONLY. NO UNDERLINE, AND NO LINKS, IN ANY HERO. Daniel's call,
   2026-08-08, and it applies to every case study without exception.
   It used to be bold + underline on all seven instances across the portfolio,
   of which only two were anchors. His catch: bold + underline is how the whole
   web spells "hyperlink", so five were promising a click that did not exist.
   Making all seven links was the other way out and is not available: three have
   no destination that can exist (Kiosk's "primary digital access point" is
   hardware standing in a branch, uxresearch's is a practice rather than a
   product) and the ai-products pair are unshipped. Rather than run two
   treatments in the same slot, the slot now means one thing everywhere.
   ⚑ The two anchors that were here are gone: Banking GPS's "financial literacy
   course" and website-redesign's "public website" are spans again.
   ⚑ Do not re-add an <a> here, and do not re-add the underline. A study that
   needs to send people somewhere uses a component built for it: the .flip-live
   pill on the visual, or a terminal .study-cta. Both are elsewhere in this file.
   ⚑⚑ CLOSED 2026-09-01, and the flag was WRONG ON ITS FACTS. It read: "website-
   redesign lost its ONLY live-site link to this change. If that page should link
   woodforest.com, it needs a .study-cta or a .flip-live, not the hero noun. Not
   done here; flagged to Daniel 2026-08-08." The page had NOT lost its only
   live-site link. The wireframe viewer carries FIVE live woodforest.com URLs, one
   per tier, in js/wireframe-viewer.js's `live:` keys. What the page actually
   lacked was a live link a reader meets at the HERO rather than deep in Process,
   which is a smaller gap than the flag claimed and is why it sat open 24 days
   reading worse than it was.
   ⚑ THE FIX FOLLOWED THE SIBLING'S OWN RULE, not this note's suggestion. Banking
   GPS PULLED its .flip-live on 2026-08-28 because that URL still served the build
   its redesign replaces, leaving "RESTORE IT ON LAUNCH DAY, not before". This
   redesign has launched and woodforest.com serves it, so the same rule that
   removed the pill there restores it here. A .flip-live pill now sits in the
   website-redesign hero's flip-tabs row.
   ⚑ ADDING IT REQUIRED RESTRUCTURING THAT PAGE'S TABS, because role="tablist" was
   still on .flip-tabs there while this file's guard below already described it as
   living on .flip-tabs-list. That description was true of banking-products and
   false of website-redesign for three weeks. Both files now match, which is what
   keeps the pill out of the tablist and out of a critical aria-required-children
   violation. */
.study-hero-intro .u {
  font-weight: 700;
  color: inherit;
}
/* .u outside a hero can still be an anchor (Process on website-redesign links a
   real XD prototype through it), which is why the hover colour stays. */
a.u:hover,
a.u:focus-visible {
  color: var(--study-3);
}

/* ── Stat metrics row ──────────────────────────────────────────
   Four blocks separated by thin rust dividers (rust 1→4).
   Value: Roboto Slab Bold 24. Label: Roboto Slab Light 10 / +1px.
   ────────────────────────────────────────────────────────────── */
.study-metrics {
  display: flex;
  align-items: stretch;
  gap: var(--sp-2);
  min-height: 70px;
  flex-wrap: nowrap;      /* single row on desktop, packed left */
}

.study-metric {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1 1 0;        /* equal widths — keeps labels to 2-3 lines */
  min-width: 0;
  max-width: 130px;
}

.study-metric-value {
  font-family: var(--font-mono);   /* Roboto Slab */
  font-weight: 700;
  font-size: 1.5rem;               /* 24px */
  line-height: 1.1;
  color: var(--text-primary);
}

.study-metric-label {
  font-family: var(--font-mono);
  font-weight: 300;
  font-size: 0.6875rem;            /* 11px — these carry real info */
  line-height: 1.25;
  letter-spacing: 1px;
  color: var(--text-primary);
}

.study-metric-divider {
  width: 2px;
  align-self: stretch;
  flex-shrink: 0;
}
.study-metric-divider.d1 { background: var(--study-1); }
.study-metric-divider.d2 { background: var(--study-2); }
.study-metric-divider.d3 { background: var(--study-3); }
.study-metric-divider.d4 { background: var(--study-4); }

/* ── Role / Contribution / Constraint meta ─────────────────────
   Roboto 13 / 1.65, secondary color. Bold inline labels.
   ────────────────────────────────────────────────────────────── */
.study-meta {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.study-meta p {
  font-family: var(--font-sans);
  font-size: 0.6875rem;            /* 11px */
  line-height: 1.65;
  color: var(--text-secondary);
}
.study-meta b { font-weight: 700; }

/* ── Hero visual slot ──────────────────────────────────────────
   Placeholder frame for the hero image/mockup.
   ────────────────────────────────────────────────────────────── */
/* The `hidden` attribute is a UA default (display:none) and loses to the
   class rule below, so a retired visual keeps rendering. .tpb sat visible on
   the page for exactly that reason after being retired 2026-08-05. */
.study-visual[hidden] { display: none; }

.study-visual {
  position: relative;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  text-align: center;
  padding: var(--sp-3);
}
.study-visual img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.study-visual.is-hero { aspect-ratio: 566 / 560; }

/* ── THEN/NOW flip frame ───────────────────────────────────────
   Before/after comparison for redesign studies: two mini folder
   tabs (same language as the page-level study tabs) ride a
   .study-visual stage holding one pane per era. The NOW pane is
   a plain <img> today and becomes the live prototype embed
   later — the chrome doesn't change. js/flipframe.js swaps.
   ────────────────────────────────────────────────────────────── */
.study-flip { display: flex; flex-direction: column; }

/* Outer row: tabs on the left, "View Live" pushed right. Carries NO aria role.
   ⚑ role="tablist" is on .flip-tabs-list, which owns only the two tabs. Putting
   the pill inside the tablist (as it was briefly on 2026-08-08) is a critical
   aria-required-children violation. See the note in banking-products.html. */
.flip-tabs {
  display: flex;
  align-items: center;
  padding-left: 14px;
}
/* ⚑ A FLOOR UNDER THE GAP, ONLY WHERE A PILL EXISTS. .flip-live is pushed right by
   margin-left:auto, and auto collapses to ZERO once the row is full: measured at 320px
   the pill's left border sat at 221.7 against the last tab's right edge at 221.8, so it
   rendered as a third tab welded onto the pair. Clear at 360 (25.8px) and every width
   above. 320 is the WCAG 1.4.10 reflow width and nothing is clipped there, so this is
   visual rather than a failure, but it reads as a defect.
   ⚑ SCOPED WITH :has() ON PURPOSE, and this is the one place in this file where that is
   the safe choice rather than the risky one. A bare `gap` on .flip-tabs would reach BOTH
   frames on banking-products.html, including .gpv, which runs its own 3-column grid with
   gap:4px on the list. Scoping to rows that actually hold a pill touches only the
   website-redesign hero today, and it will apply itself to Banking GPS for free when that
   pill is restored on launch day. */
.flip-tabs:has(.flip-live) { gap: 12px; }
/* ⚑⚑ AND THE ROW WRAPS BELOW 350px, BECAUSE THE GAP RULE ABOVE FIXED THE WELD AND
   MISSED A CONTENT CLIP UNDERNEATH IT. With the pill, .flip-tabs has a min-content of
   309.19px against a 280px track; without it, 201.73px. So the pill alone raised the
   floor past the column, and `body { overflow-x: clip }` swallowed the difference with
   NO SCROLLBAR: at a 320px viewport, scrollWidth measured 329 against clientWidth 316
   and NINE CHARACTERS rendered past the edge, including the closing bracket of
   "CMS Build (Umbraco)", the hyphen in "QA/UAT Co-Lead", and the "I" in "a full-team
   decision that I led and structured" — the pronoun carrying the whole ownership claim.
   ⚑ THAT IS WCAG 1.4.10 REFLOW, LEVEL AA, and it is the FAILING form of it: content
   lost rather than a scrollbar offered. 320 CSS px is the SC's reference width.
   ⚑ WHY 320 IS IN SCOPE, since no 2026 phone is 320 CSS px: the page had already
   decided. The gap rule directly above was written for this width, and the study holds
   itself to WCAG elsewhere (two Level A fixes shipped 2026-09-01). A page cannot ship a
   rule for a width and then call that width out of scope.
   ⚑ THE GAP RULE IS NOT THE BUG AND MUST STAY: it accounts for only 24 of the 29px of
   overflow, so even at gap:0 this would still clip. Two separate defects at one width.
   ⚑ Nothing at 350 and above is touched; 360 measured scrollWidth 356 = clientWidth 356,
   zero clipped characters, before and after. */
@media (max-width: 349px) {
  .flip-tabs:has(.flip-live) { flex-wrap: wrap; row-gap: 8px; }
}
.flip-tabs-list {
  display: flex;
  gap: 6px;
}
/* "View Live" on the tab row. The sibling in website-redesign puts this in the
   mock browser chrome (.wv-live below); this component has no chrome bar, so the
   tab row is the equivalent shelf. margin-left:auto pushes it to the far right
   of .flip-tabs, of which it is a SIBLING of the tablist, not a child.
   ⚑ Deliberately NOT a .study-cta under the frame, which is what it was until
   2026-08-08: that class is section-terminal and read as an action on the
   section rather than on the visual. */
.flip-live {
  margin-left: auto;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-mono);
  font-size: .72rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--study-2);
  border-radius: 6px;
  padding: 5px 11px;
  text-decoration: none;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.flip-live:hover { background: var(--study-1); border-color: var(--study-1); color: var(--bg); }
.flip-live:focus-visible { outline: 2px solid var(--study-1); outline-offset: 2px; }

.flip-tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;                      /* icon ↔ label */
  padding: 13px 15px;            /* 43px tall measured. See note below. */
  /* ⚑ THIS SAID "≥44px tall — touch minimum" AND THE NUMBER WAS WRONG: both flip tabs
     measure exactly 43.00px (95.0x43 and 86.8x43), so the claim was false by 1px and had
     been for as long as it stood. Corrected 2026-09-01 rather than padded, because 44px is
     WCAG 2.5.5 AAA and the AA target (2.5.8) is 24px, which this clears comfortably. There
     was no failure to fix, only a comment asserting a number nobody had measured.
     ⚑ If you ever DO want 44, it is padding: 14px 15px. Do not change it to make the old
     comment true; the old comment was the defect. */
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 8px 8px 0 0;
  background: transparent;
  cursor: pointer;
  font-family: var(--font-mono);
  font-weight: 300;
  font-size: 0.6875rem;          /* 11px */
  letter-spacing: 1px;
  line-height: 1.3;
  color: var(--text-muted);
  transition: color 0.2s ease, background-color 0.2s ease,
              border-color 0.2s ease, box-shadow 0.2s ease;
}
/* Hover mirrors the folder study-tab: the edge lights up with the same
   4-ring theme bloom, fills to surface, bolds the label (width held by
   .tab-label::after), and the icon fans its 3 theme-tinted echoes. */
.flip-tab:hover:not([aria-selected="true"]),
.flip-tab:focus-visible:not([aria-selected="true"]) {
  color: var(--text-primary);
  background: var(--surface);
  border-color: var(--study-1);
  box-shadow:
    inset 0 0 0 1px var(--study-2),
    inset 0 0 0 2px var(--study-3),
    inset 0 0 0 3px var(--study-4);
  font-weight: 700;
}
.flip-tab:focus-visible { outline: 2px solid var(--study-3); outline-offset: 2px; }
.flip-tab[aria-selected="true"] {
  background: var(--study-1);
  border-color: var(--study-1);
  color: var(--bg);
  font-weight: 700;
}
/* Flip-tab icons run a touch smaller than the folder tabs' 20px */
.flip-tab .tab-icon-wrap,
.flip-tab .tab-ico,
.flip-tab .tab-ico svg { width: 16px; height: 16px; }
.flip-tab:hover:not([aria-selected="true"]) .tab-ico-2,
.flip-tab:focus-visible:not([aria-selected="true"]) .tab-ico-2 { opacity: 1; transform: translateX(-1px); transition-delay: 0s; }
.flip-tab:hover:not([aria-selected="true"]) .tab-ico-3,
.flip-tab:focus-visible:not([aria-selected="true"]) .tab-ico-3 { opacity: 1; transform: translateX(-2px); transition-delay: 0.05s; }
.flip-tab:hover:not([aria-selected="true"]) .tab-ico-4,
.flip-tab:focus-visible:not([aria-selected="true"]) .tab-ico-4 { opacity: 1; transform: translateX(-3px); transition-delay: 0.10s; }

/* Stage: panes stack and crossfade */
.flip-stage { position: relative; }
.flip-pane {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* ⚑ 2026-09-10: visibility rides with opacity so an inactive pane's descendants leave
     the tab order and the accessibility tree. Needed the day a real link went INSIDE a
     pane (the GPS Then pane's Live Today pill): a focusable node inside aria-hidden
     content is axe aria-hidden-focus (serious). The 0.22s delay on the way out keeps the
     crossfade; the way in is immediate. flipframe.js's tabindex toggle is unchanged. */
  visibility: hidden;
  transition: opacity 0.22s ease, visibility 0s linear 0.22s;
}
.flip-pane.is-active { opacity: 1; visibility: visible; transition-delay: 0s; }
/* ⚑ LIVE TODAY, 2026-09-10 (Daniel's call). The old "View Live" pill was pulled from the
   tab row 2026-08-28 because the URL serves the OLD course. Restored INSIDE the Then pane
   instead: the link belongs to the screen that is live, appears and leaves with it, and
   never sits beside the Redesign tab. Sits on the pane's dark ground (#2a2420) so it takes
   the sibling .wv-live's light treatment: #f4efe9 on #2a2420 is 13:1. On launch the
   trigger's checklist (tools/gps-launch-check.sh) moves it to the After side. */
.flip-pane .flip-live {
  position: absolute; right: 14px; top: 14px; margin-left: 0;   /* top-right: the bezel corner is empty; bottom-right has the shot's own small print */
  color: #f4efe9; background: rgba(244, 239, 233, .10); border-color: rgba(244, 239, 233, .5);
}
.flip-pane .flip-live:hover { background: #f4efe9; border-color: #f4efe9; color: #2a2420; }
.flip-pane .flip-live:focus-visible { outline: 2px solid #f4efe9; outline-offset: 2px; }
.flip-pane img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  display: block;
}
/* THEN is a landscape device shot in a portrait frame — pin it to
   the top edge, ground the rest on the site's dark bar color, and
   code it as a vintage artifact */
.flip-pane-then { background: #2a2420; }
.flip-pane-then img {
  object-fit: contain;
  object-position: top;
  filter: sepia(0.22) saturate(0.85);
}

/* Visual-led hero variant (all full studies): the hero visual is
   the dominant element — wider column, landscape stage so the
   whole thing lands above the fold — and the metrics pack 2×2 in
   the text column instead of one skinny row. */
.study-hero-grid.is-visual-lead {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
}
.is-visual-lead .study-metrics {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
  gap: 20px var(--sp-2);
  min-height: 0;
}
.is-visual-lead .study-metric { max-width: none; }
.is-visual-lead .study-visual.is-hero { aspect-ratio: 640 / 470; }
/* Banking GPS flip stage opts out of the landscape crop: it takes
   the NOW asset's native ratio (intro-now.jpg, 1992×2648 @2x = 996×1324
   logical) so the whole redesign shows uncropped — trading the fold for
   it; THEN pins to the top of the taller frame, dark ground below.
   ⚑ THIS RATIO MUST TRACK THE FILE. History, so nobody re-derives it:
   1035x1363 until 2026-08-08, then 1013x1341 after the first crop, now
   996x1324 logical. Both earlier exports carried baked-in dead space on
   all four sides (a symmetric 11px near-white margin the first time, a
   17px canvas-gray surround plus a 1px dark shadow ring the second),
   which read as a gap between the frame border and the start of the
   design. It was never in this CSS: the pane is position:absolute
   inset:0, so it covers .study-visual's 24px padding, and the pane, the
   img and the stage all measured within 1px of each other. Cropping the
   asset is what fixes it. Originals kept beside it as .bak.
   ⚑ Every re-export so far has carried a margin. Measure the file before
   blaming the layout. */
.is-visual-lead .study-visual.flip-stage { aspect-ratio: 996 / 1324; }
/* …but the stage takes the ACTIVE pane's ratio, not one asset's for both.
   Added 2026-08-08. The two captures are different shapes on purpose: the old
   course is one fixed 980x660 device screen (intro-then.jpg, 1960x1320 = that
   screen at 2x) and the
   redesign is a tall scrolling page (intro-now.jpg, 996x1324 logical). Pinned to the
   NOW ratio, THEN drew at 667x451 inside a 669x881 stage and left 431px of
   #2a2420 under it, 48.9% of the frame. That is the state the auto-reveal shows
   FIRST, so the first thing many visitors saw of this study was a half-empty
   dark box that reads as an image that failed to load.
   :has() rather than a JS hook: flipframe.js is shared with website-redesign's
   flip frame, and that one wants a fixed square. Three other rules in this file
   already use :has(). Where it is unsupported the stage simply keeps the NOW
   ratio, which is the old behaviour, so this degrades to exactly what shipped
   before.
   ⚑ The height changes when the tabs are switched. Do not "fix" that by padding
   one capture to match the other: the shape difference is the evidence, and the
   caption says so.
   ⚑ DO NOT ADD `transition: aspect-ratio`. Tried 2026-08-08 to soften the
   resize; it silently broke the rule. With the transition present the computed
   value stayed at the NOW ratio (0.759354/1) instead of settling on the target,
   so THEN kept its 431px of dead ground and the fix looked like it had not
   applied. Removing the transition resolves it correctly to 1200/808, giving a
   669x451 stage with 2px of slack. The pane crossfade already covers the change
   visually. */
/* ⚑ 1200/808 until 2026-08-08. Same framing, same source, just the 2x capture
   instead of a downscale of it: 1960x1320, which is the course's fixed 980x660
   screen at 2x. The ratio is unchanged to four decimals, so this rule moved
   only to keep naming the file's real dimensions. */
.is-visual-lead .study-visual.flip-stage:has(.flip-pane-then.is-active) {
  aspect-ratio: 1960 / 1320;
}
/* Kiosk hero flip stage. Both panes are recent internal screens, so no vintage
   treatment and no dark ground — the Banking GPS .flip-pane-then styling is
   deliberately not reused here.
   2026-08-11: the stage no longer changes shape between panes. It used to,
   because the "before" was a 1920x1199 screen capture and the "after" was a bare
   1280x900 canvas — two different things, which made the redesign look like it
   went full-screen when it did not. Both panes are now the same design rendered
   at 1280x900, the resolution the fleet's floor was raised to, so the comparison
   is like for like: the old layout uses 46.6% of the width, the new one 93.3%.
   Do not reintroduce a per-pane aspect-ratio. */
.is-visual-lead .study-visual.flip-stage.ki-flip-stage { aspect-ratio: 1280 / 900; }
.ki-flip-stage .flip-pane { background: #FFFFFF; }
.ki-flip-stage .flip-pane img { object-fit: cover; object-position: center; }

/* Website-redesign flip stage: full-page homepage captures in a
   scrollable window — a square shows a generous slice, scroll for the
   rest. No portrait crop, no vintage treatment (the "before" is a real
   recent site, not an artifact). Only the active pane takes pointer
   events, so the hidden pane stacked over it can't swallow the scroll. */
.is-visual-lead .study-visual.flip-stage.flip-scroll { aspect-ratio: 1 / 1; }
.flip-scroll .flip-pane {
  overflow-y: auto;
  overflow-x: hidden;
  pointer-events: none;
  scrollbar-width: thin;                       /* Firefox */
  scrollbar-color: var(--study-2) transparent;
}
.flip-scroll .flip-pane.is-active { pointer-events: auto; }
.flip-scroll .flip-pane img { width: 100%; height: auto; }
/* Rust-tinted scrollbar so the frame reads as part of the page, not a
   raw OS chrome. Transparent border + padding-box inset the thumb into
   a slim pill; darkens on hover. */
.flip-scroll .flip-pane::-webkit-scrollbar { width: 12px; }
.flip-scroll .flip-pane::-webkit-scrollbar-track { background: transparent; }
.flip-scroll .flip-pane::-webkit-scrollbar-thumb {
  background: var(--study-2);
  border-radius: 999px;
  border: 3px solid transparent;
  background-clip: padding-box;
}
.flip-scroll .flip-pane::-webkit-scrollbar-thumb:hover { background: var(--study-1); }

/* Auto-reveal (js/flipframe.js data-auto-reveal): prime jump to
   THEN happens with no transition; the scripted THEN→NOW flip
   gets a slower, more theatrical crossfade than manual taps. */
.study-flip.is-priming .flip-pane { transition: none; }
.study-flip.is-revealing .flip-pane { transition-duration: 0.5s; }

.flip-caption {
  margin: 10px 0 0;
  font-family: var(--font-mono);
  font-weight: 300;
  font-size: 0.6875rem;
  letter-spacing: 1px;
  line-height: 1.4;
  color: var(--text-muted);
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .flip-pane { transition: none; }
}

/* ══════════════════════════════════════════════════════════════
   DECORATIVE THEME FRAME
   ══════════════════════════════════════════════════════════════
   The signature stepped rust border that snakes around each
   section. Straight runs are 4×5px striped gradients (darkest
   line always outermost); corners are SVG assets exported from
   Figma (assets/frame/corner-*.svg) whose strokes match the runs.

   Each page section lives in a .study-band (full-width,
   position:relative); frame segments are positioned against the
   band's edges so they track content height automatically.
   ══════════════════════════════════════════════════════════════ */
.study-band { position: relative; }
.study-band > .study { position: relative; z-index: 1; }

/* ── Scroll-driven ribbon (js/ribbon.js) ──
   When JS is available on desktop, the static frame segments are
   swapped for one continuous SVG ribbon that trims in on scroll. */
#main-content { position: relative; }
#ribbon-svg {
  position: absolute;
  top: 0; left: 0;
  pointer-events: none;
  z-index: 0;
}
.ribbon-js .study-frame { visibility: hidden; }

.study-frame {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
}

.sfr { position: absolute; display: block; }

/* Straight runs — darkest stripe on the side away from content */
.sfr-h { height: 20px; }
.sfr-v { width: 20px; }
.sfr-h.dark-top    { background: linear-gradient(to bottom, var(--study-1) 0 25%, var(--study-2) 25% 50%, var(--study-3) 50% 75%, var(--study-4) 75% 100%); }
.sfr-h.dark-bottom { background: linear-gradient(to top,    var(--study-1) 0 25%, var(--study-2) 25% 50%, var(--study-3) 50% 75%, var(--study-4) 75% 100%); }
.sfr-v.dark-right  { background: linear-gradient(to left,   var(--study-1) 0 25%, var(--study-2) 25% 50%, var(--study-3) 50% 75%, var(--study-4) 75% 100%); }
.sfr-v.dark-left   { background: linear-gradient(to right,  var(--study-1) 0 25%, var(--study-2) 25% 50%, var(--study-3) 50% 75%, var(--study-4) 75% 100%); }

/* Corner pieces — native SVG size (109.5×108.5) so the 5px
   strokes stay exactly 5px and tails overlap the runs seamlessly */
.sfr-corner { width: 109.5px; height: 108.5px; }
.sfr-corner img { display: block; width: 100%; height: 100%; }

/* ── Hero band segments (Figma: ThemeBorder H-1 → Corner-3) ──
   Corner SVG strokes sit flush against specific box edges, and
   their straight tails are ~71px long — runs deliberately extend
   ~35px underneath the tails so junctions can never show a seam.
   The rotated corner compensates ±0.5px for its non-square box. */
.sfr-hero-top    { top: 0; left: 0; right: calc(var(--page-guide) + 73px); }
.sfr-hero-tr     { top: 0.5px; right: calc(var(--page-guide) - 0.5px); transform: rotate(-90deg); }
.sfr-hero-right  { top: 80px; bottom: 70px; right: var(--page-guide); }
.sfr-hero-br     { bottom: 0; right: var(--page-guide); }
.sfr-hero-bottom { bottom: 0; left: calc(var(--page-guide) + 73px); right: calc(var(--page-guide) + 73px); }
.sfr-hero-bl     { bottom: -88.5px; left: var(--page-guide); }   /* turns down into next band */

/* ══════════════════════════════════════════════════════════════
   TWO-COLUMN SECTION LAYOUT (Problem/Users, Impact, …)
   ══════════════════════════════════════════════════════════════ */
.study-two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
  align-items: start;
}

.study-col {
  display: flex;
  flex-direction: column;
  gap: 20px;
  align-items: flex-start;
  min-width: 0;
}
.study-col .study-body + .study-body { margin-top: 0; }  /* gap handles spacing */

/* ── Small letter-spaced CTA link (Card/CTA in Figma) ── */
.study-cta {
  font-family: var(--font-mono);   /* Roboto Slab */
  font-weight: 300;
  font-size: 0.6875rem;             /* 10px */
  letter-spacing: 1px;
  line-height: 1.4;
  color: var(--text-primary);
  text-decoration: none;
  /* padding-top gets the target to 24px tall (6 + 14 line-box + 4) for
     WCAG 2.2 AA 2.5.8. These are standalone block links, not inline-in-a-
     sentence, so the inline exception does not apply and they were rendering
     18px. Added on top rather than below so the underline stays 4px from the
     text; .study-step compensates on its margin so that context is unmoved. */
  padding-top: 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--border);
  transition: color 0.2s ease, border-color 0.2s ease;
}
.study-cta:hover,
.study-cta:focus-visible {
  color: var(--study-3);
  border-color: var(--study-3);
}
/* .study-col spaces its CTA via flex gap; steps aren't flex, so match it here */
.study-step .study-cta {
  display: inline-block;
  margin-top: 14px;   /* 20 − the 6px padding-top added for target size */
}

/* Bottom "Next case study →" cross-link (tab pages): compact,
   pinned to the content column's right edge — without this it
   stretches across the flex column and underlines the full width */
.study-learnings .study-cta {
  align-self: flex-end;
}

/* ── Problem band ── */
.study-problem {
  padding-top: 40px;
  padding-bottom: 40px;
}

.study-visual.is-users { aspect-ratio: 580 / 147; }

/* ══════════════════════════════════════════════════════════════
   PROCESS SECTION
   ══════════════════════════════════════════════════════════════ */
.study-process {
  padding-top: 40px;
  padding-bottom: 60px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.study-process-steps {
  display: flex;
  flex-direction: column;
  gap: 60px;
}

/* Step block: bold Merriweather lead + body (Figma ProcessRow text) */
.study-step { min-width: 0; }
/* Lone process step / featured intro: capped to a readable measure so a
   single column can't run the full page width and blow past the
   ~65–75-char line-length sweet spot. */
.study-step-solo { max-width: 620px; }
/* Two short steps sharing one column, paired against a taller neighbour. */
.study-step-stack { display: flex; flex-direction: column; gap: 32px; }

/* Grid cells, not a flow of paragraphs: the inherited paragraph top-margin
   would sit inside each cell and drop every item except the first below its
   row-mate. Row spacing is the grid gap's job instead. */
.study-richtext.study-two-col > p { margin-top: 0; }
/* Featured step: readable intro text stacked above its own FULL-WIDTH
   visual. Keeps text and visual out of the same row, so the visual gets
   the whole width and no tall image is pinned beside the paragraph. */
.study-feature { display: flex; flex-direction: column; gap: 24px; }
.study-step-title {
  font-family: var(--font-serif);
  font-weight: 700;
  font-size: 1.125rem;           /* 18px */
  line-height: 1.65;
  color: var(--text-primary);
  margin: 0;
}

/* Visual slot proportions from Figma */
.study-visual.is-gallery { aspect-ratio: 580 / 590; }
/* Full-width component-library board (Process step 3) — its own aspect so
   the 14101×12454 export fills the band without cropping. */
.study-visual.is-board { aspect-ratio: 14101 / 12454; }
/* Transfer & Pay's own board (Process step 2). Same treatment, different
   source: Figma section 3112:35921, exported at its 4750×2864 proportion.
   Grew from 3804 wide on 2026-08-05 when the FAQs and Main Menu families were
   added; if the section changes again this ratio changes with it. */
.study-visual.is-board-tp { aspect-ratio: 4750 / 2864; }
/* Banking GPS's board (Process, under the Step 2 row). Same treatment again:
   Figma section 1138:294771 in the portfolio file, exported at its 7372×3761
   proportion. It is much wider than the T&P section, which is why its labels
   are composed at 56pt against T&P's 37pt: every natural point shrinks harder
   when a wider board is fit to the same band, and both land near 9 CSS px
   rendered. If the section changes, re-read get_metadata and change this ratio
   with it. */
.study-visual.is-board-gps { aspect-ratio: 7372 / 3761; }
/* The Online Banking Zelle history (Process, last row), 1440×1024: the
   WHOLE frame, at Daniel's direction 2026-08-06.
   ⚑ HISTORY, so the crop is not reinstated by someone reading only the
   review notes. The Process review found this exhibit overweighted and
   unreadable, and it was briefly cropped to the nav bar, title and seven
   rows (1440×560) to halve the row. Daniel then updated the Figma frame so
   it carries the proposed chevron, and asked for the whole frame. The
   crop's job was to stop the screen being the tallest thing in Process
   while illustrating the wrong half of the aside; that argument still
   stands and the whole frame costs it. It is his call and it is recorded
   as his call.
   ⚑ NOTE THE CHEVRON IS NOT LEGIBLE AT THIS SIZE. The glyph is ~7px in
   the 1440 frame and this slot renders 0.465, so it lands near 3px. The
   frame swap buys ACCURACY (the screen now contains the affordance the
   aside describes) rather than visibility. Do not "fix" that by shrinking
   the aside column; see the ratio note on .is-olb-row.
   It is a 1x export, deliberately. ⚑ This clause used to say "that node at 2x",
   left over from the first build, and it contradicted the long note in
   the HTML explaining that the 2x export is precisely what rendered this
   frame blank. Two comments in one component disagreeing about the one
   fact that broke it is how it breaks again. Corrected 2026-08-06.
   Unlike the two boards above this is a SCREEN, not a credential, so it
   is not wrapped in .study-gallery: there is nothing to open that the
   page is not already showing at full size.

   ⚑ NO MARKS ON THIS ONE, unlike every other screenshot on the tab,
   and the reason is factual rather than stylistic. A dashed mark over
   the chevron column was built 2026-08-06 and cut the same hour: its
   coordinates came from Daniel's annotated frame (33:438), where the
   rows are shortened to 693 to RESERVE that column, but the frame
   published here (3:5) runs its rows the full 735 with no column
   reserved. Placed on this asset the mark lands on the Amount figures
   and asserts a layout the artifact does not contain. The aside states
   the chevron in prose; the screen is not obliged to draw it.

   padding:0 and display:block because the base rule's flex centering
   and --sp-3 padding inset the image inside the frame. */
.study-visual.is-olb { aspect-ratio: 1440 / 1024; padding: 0; display: block; }

/* ── Gallery hover state ──────────────────────────────────────
   Figma: WorkGallery/WebsiteComponents · State=Hover
   Image blurs (2px) under a 50% black overlay; the rust
   Link to Work button (Figma 1330:340) pops in at the center;
   a toaster pill appears near the bottom. Sequence is staggered
   on hover-in (dim → button → toaster) but collapses together
   on hover-out. Also triggers on keyboard focus.
   ────────────────────────────────────────────────────────────── */
.study-gallery { cursor: pointer; padding: 0; }

.study-gallery .gallery-img {
  transition: filter 0.5s ease;
}

.study-gallery .gallery-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  opacity: 0;
  transition: opacity 0.5s ease;
}

.study-gallery .gallery-btn {
  position: absolute;
  top: 50%; left: 50%;
  width: 64px; height: 64px;
  transform: translate(-50%, -50%) scale(0.85);
  transition: transform 0.6s var(--ease-out);
}
.study-gallery .gallery-btn .rings {
  position: absolute;
  inset: -25%;              /* ring glow spills past the core */
  width: 150%; height: 150%;
  max-width: none;          /* escape global img { max-width:100% } cap */
  overflow: visible;
}
/* Visibility lives on the rings + arrow (not the container) so the
   rings can bloom inside-out on hover, echoing the logo reveal
   (nav.css .lr-*): bright innermost first, then outward. */
.study-gallery .gallery-btn .gr,
.study-gallery .gallery-btn .arrow {
  opacity: 0;
  transition: opacity 0.3s ease;
}
.study-gallery .gallery-btn .arrow {
  position: absolute;
  inset: 10%;               /* Figma ratio: arrow is 80% of the core */
  width: 80%; height: 80%;
  max-width: none;
}

/* Toaster styled like the homepage Little Daniels fun-fact tip,
   in this page's theme (brightest line bg / ink text — same combo
   as the homepage's fun-fact tips) */
.study-gallery .gallery-toast {
  position: absolute;
  /* grouped with the button: ring glow ends 48px below center
     (64px core × 150% / 2), toast sits 12px under that */
  top: calc(50% + 60px); left: 50%;
  transform: translateX(-50%) translateY(4px);
  width: max-content;
  max-width: calc(100% - 32px);
  padding: 6px 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--study-4, #e7732d);
  color: #2a2420;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.04em;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
/* Ink on the brightest line assumes a light one. Crimson-4 is dark enough
   that #2a2420 falls under 2:1 on it, so this theme flips to the cream the
   selected study tab already uses on its dark plate (≈5.6:1). */
.theme-crimson .study-gallery .gallery-toast { color: var(--text-bright, #eeedeb); }

.study-gallery:hover .gallery-img,
.study-gallery:focus-visible .gallery-img { filter: blur(2px); }

.study-gallery:hover .gallery-overlay,
.study-gallery:focus-visible .gallery-overlay { opacity: 1; }

.study-gallery:hover .gallery-btn,
.study-gallery:focus-visible .gallery-btn {
  transform: translate(-50%, -50%) scale(1);
  transition-delay: 0.1s;
}

/* inside-out bloom: arrow + innermost ring first, then outward.
   Delays only on hover-in; hover-out fades everything together. */
.study-gallery:hover .gallery-btn .arrow,
.study-gallery:hover .gallery-btn .gr-4,
.study-gallery:focus-visible .gallery-btn .arrow,
.study-gallery:focus-visible .gallery-btn .gr-4 { opacity: 1; transition-delay: 0.1s; }
.study-gallery:hover .gallery-btn .gr-3,
.study-gallery:focus-visible .gallery-btn .gr-3 { opacity: 1; transition-delay: 0.2s; }
.study-gallery:hover .gallery-btn .gr-2,
.study-gallery:focus-visible .gallery-btn .gr-2 { opacity: 1; transition-delay: 0.3s; }
.study-gallery:hover .gallery-btn .gr-1,
.study-gallery:focus-visible .gallery-btn .gr-1 { opacity: 1; transition-delay: 0.4s; }

.study-gallery:hover .gallery-toast,
.study-gallery:focus-visible .gallery-toast {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  transition-delay: 0.45s;   /* lands as the outermost ring settles */
}
.study-visual.is-tall    { aspect-ratio: 580 / 654; }
.study-visual.is-wide    { aspect-ratio: 1200 / 221; width: 100%; }

/* Live prototype embed. Full width because the kiosk is a fixed 1280x900
   canvas: in a 578px column it would render at half scale and its 16px labels
   would land at 8px. */
.study-visual.is-prototype { width: 100%; padding: 0; border: 0; background: transparent; }
.study-visual.is-prototype iframe { width: 100%; height: 100%; border: 0; display: block; }
/* Prototype label, same shape as the research study's artifact card:
   eyebrow names the kind of thing, label names it, meta says what is in it. */
/* Label and frame are one flex child, so the Process grid's 60px row gap sits
   around the pair instead of pushing the kiosk away from its own label. */
.proto-block { width: 100%; }
.proto-head { display: flex; flex-direction: column; gap: 6px; margin: 0 0 14px; }
.proto-eyebrow {
  display: inline-flex;
  align-self: flex-start;
  align-items: center;
  gap: 7px;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: .01em;
  color: var(--study-2);
}
/* Status dot. Static on purpose: a pulsing red one is the recording/broadcast
   convention and would claim something is streaming, which nothing here is.
   Decorative, so it is a pseudo-element and stays out of the accessibility
   tree — "Live prototype" already carries the meaning for a screen reader. */
.proto-eyebrow::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--study-2);
}
.proto-label {
  font-family: var(--font-serif);
  font-size: 1.02rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--study-1);
}
/* ⚑ 11px, folded 2026-08-30. This was 0.68rem = 10.88px, caught by the Impeccable
   detector once its HTML parser modules were installed (it had been running in a
   degraded regex mode that never evaluated computed sizes). It is a caption in open
   PAGE SPACE under the embedded prototype, so the 11px rule at the top of this file
   binds it; it is not one of the figure-internal carve-outs. */
.proto-meta {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  line-height: 1.5;
  color: var(--text-muted);
}
/* ⚑ Added 2026-09-11 at Daniel's direction. Both prototypes on this page were
   built by him, for these case studies, in Claude Code, and the page never said
   so. Provenance rides with the artifact; AUTHORSHIP is carried in the prose
   instead (Decision 4 names him as the builder), because the T&P head is hidden
   below 860px and a phone reader would otherwise never learn whose it is.
   A step quieter than .proto-meta: this is a credit, not part of the claim. */
/* ⚑⚑ 2026-09-11, BOTH OF MY FIRST TWO CHOICES HERE WERE WRONG, caught by the pass:
   the 0.625rem was 10px in OPEN PAGE SPACE, which the rule at the top of this file
   forbids (11px minimum, Daniel 2026-08-29) and which .proto-meta two rules up had
   already been corrected for; and `opacity: .85` blended --text-muted from 4.91:1
   down to 3.64:1 on the page ground, FAILING WCAG AA at 10px, on the tab whose
   second mandate is accessibility and a few hundred pixels from Step 2's own
   contrast sentence. ⚑ THE QUIET COMES FROM SPACE NOW, not from size or fade: the
   4px margin makes the gap above the credit 10px against the 6px binding eyebrow,
   label and meta into one block, so it reads as a footnote to the artifact rather
   than a fourth line of its claim. Do not reintroduce an opacity or a sub-11px size. */
.proto-cred {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  /* .01em, not .02em: every other muted 11px mono line in this file tracks at
     .01em (and .proto-meta, right above, is normal). .02em is reserved here for
     accent labels in --study-2. The quiet line should not be the wide one. */
  letter-spacing: .01em;
  line-height: 1.5;
  color: var(--text-muted);
  margin-top: 4px;
}
/* Below the tab's 860px collapse the live iframe would render the 1280x900
   canvas at ~27% on a phone (16px labels at ~4px), so the block swaps to
   stills of all five screens. Daniel's call, 2026-08-16. */
.proto-shots { display: none; }
/* The proto-head carries two wordings: .pd for the live iframe (desktop),
   .pm for the stills ("Live prototype" over static JPEGs was disprovable). */
.proto-head .pm { display: none; }
/* ⚑ The status dot is deliberately ABSENT from the stills eyebrow below 860px
   (decided 2026-09-11, after a pass found it was an accident of specificity:
   .proto-head .pm at (0,2,0) blockifies .proto-eyebrow's inline-flex, so the
   ::before dot collapses). Keeping it absent is the correct behaviour, not a
   bug to fix: the dot is the LIVE-status convention, and the mobile block is
   static JPEGs under a "Prototype Stills" eyebrow. Do not "restore" it. */
/* Transfer & Pay embed (Decision 4 column). Fixed height rather than the
   kiosk's aspect-ratio: the phone stage scales itself inside the frame, so
   the frame just needs enough height for the ~0.9x phone plus its bar. */
.tp-frame { height: 740px; }
/* The "one feed, two sorts" pair the embed replaced, kept as the small-screen
   fallback, same approach as the kiosk's proto-shots. */
.study-figure.tpv-fallback { display: none; }
@media (max-width: 860px) {
  .study-visual.is-prototype { display: none; }
  /* Transfer & Pay's head has no .pd/.pm pair: the whole head goes, because the
     fallback below it is two shipped design screens with their own kicker and
     captions, not stills of this prototype. Leaving the head would have put a
     "Live Prototype" eyebrow over static images, which is the one claim a phone
     reader can disprove by trying. The kiosk keeps its head and swaps the text. */
  .proto-head.is-desktop-only { display: none; }
  .proto-head .pd { display: none; }
  .proto-head .pm { display: inline; }
  .proto-shots { display: grid; gap: 12px; }
  .study-figure.tpv-fallback { display: flex; flex-direction: column; }
  .proto-shots img {
    width: 100%;
    height: auto;
    display: block;
    border: 1px solid rgba(28, 60, 30, 0.25);
  }
}

/* ── Band frame patterns ──────────────────────────────────────
   The ribbon alternates between two wraps as it snakes down:

   LEFT-wrapped band (Problem, Solution, Learnings):
     ribbon arrives at top-left, runs down the LEFT guide,
     turns right at the bottom (corner-4 ⟳90), runs under the
     section (dark stripe on TOP — ribbon continuity), and
     dives down at the right guide (corner-1 ⟲90).

   RIGHT-wrapped band (Process, Impact):
     ribbon arrives at top-right, runs down the RIGHT guide,
     turns left at the bottom (corner-6), runs under the section
     (dark stripe on BOTTOM), and dives down at the left guide
     (corner-7 ⟳180).
   ────────────────────────────────────────────────────────────── */
/* left-wrapped */
.sfr-wl-left   { top: 40px; bottom: 70px; left: var(--page-guide); }
.sfr-wl-bl     { bottom: 0.5px; left: calc(var(--page-guide) - 0.5px); transform: rotate(90deg); }
.sfr-wl-bottom { bottom: 0; left: calc(var(--page-guide) + 73px); right: calc(var(--page-guide) + 73px); }
.sfr-wl-br     { bottom: -89px; right: calc(var(--page-guide) - 0.5px); transform: rotate(-90deg); }

/* right-wrapped */
.sfr-wr-right  { top: 40px; bottom: 70px; right: var(--page-guide); }
.sfr-wr-br     { bottom: 0; right: var(--page-guide); }
.sfr-wr-bottom { bottom: 0; left: calc(var(--page-guide) + 73px); right: calc(var(--page-guide) + 73px); }
.sfr-wr-bl     { bottom: -88.5px; left: var(--page-guide); transform: rotate(180deg); }

/* final run (Learnings) exits stage right toward the footer */
.sfr-wl-bottom.is-final { right: 0; }

/* Single-band page (hero only, e.g. AI Products): after the
   right vertical the ribbon turns outward (corner-4 ⟳90 — the same
   down→right turn as wl-bl, translated to the right guide) and
   exits off the window's right edge, opposite the full-bleed
   entrance at top-left. Corner box left edge = vertical's left
   edge (−0.5px rotation compensation), so right = guide + 20.5px
   − 109.5px box width. Run tail matches the wl-bottom overlap. */
.sfr-hero-br-out     { bottom: 0.5px; right: calc(var(--page-guide) - 89px); transform: rotate(90deg); }
.sfr-hero-exit-right { bottom: 0; left: calc(100% - var(--page-guide) + 52.5px); right: 0; }

/* ══════════════════════════════════════════════════════════════
   SOLUTION / IMPACT / LEARNINGS
   ══════════════════════════════════════════════════════════════ */
.study-solution,
.study-impact,
.study-learnings {
  padding-top: 40px;
  padding-bottom: 60px;
}

/* Solution's H2 is a DIRECT child of the section (hoisted out of the first
   column 2026-08-07 so Decision 1's prose and its aside start at the same y).
   Everywhere else on the page the 20px under an H2 comes from a flex gap:
   .study-col's in the column-nested sections, .study-process's own on the one
   other section-level H2. .study-solution is a plain block, so hoisting the
   heading dropped that gap to ZERO and jammed "Solution" onto Decision 1.
   ⚑ A margin, not a flex gap on the section. Making .study-solution a flex
   column would also insert 20px between every decision row, on top of the 48px
   .study-two-col + .study-two-col margin below, silently respacing the whole
   section. The child combinator keeps this off the other tabs, whose Solution
   headings are still nested in a .study-col.
   Measured, not guessed: Process, The Problem, Impact and Learnings all sit at
   20px, so this is parity rather than a new value. */
.study-solution > .study-h2 { margin-bottom: 20px; }

.study-two-col.is-center { align-items: center; }
/* Online Banking row (Process, last row): screen LEFT, aside RIGHT, at
   Daniel's direction 2026-08-06.
   ⚑ NOT 1fr 1fr. ⚑ MEASURED 2026-08-06, replacing an estimate that was
   simply wrong: at a 1192px row an even split renders the 1440px frame at
   0.40 scale and the weighting lands it at 0.465, so the ratio buys about
   0.065, not the jump the first version of this note claimed. Keep the
   weighting, since 0.465 beats 0.40 and the aside still clears its
   measure, but do NOT cite it as the thing that makes the screen legible.
   It does not. At 0.465 the table, the link rows and the status column are
   all sub-7px, and at 386px the whole screen is texture. If the screen has
   to be readable, the answer is CROPPING the asset to the nav bar, title
   and rows, not adjusting this ratio.
   ⚑ Centered, not start-aligned: the aside runs shorter than the screen,
   and pinned to the top it left a visible well of empty band under it. */
.study-two-col.is-olb-row {
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  align-items: center;
}

/* TEMPORARY, 2026-08-06. Collapses a two-col row to a single measured column
   while its visual half is empty. Used by Solution on banking-products.html
   between the .tpa diagram being removed and the per-decision visuals landing.
   640px, not band width: Learnings was moved TO two columns on 2026-08-05
   because prose at band width is too wide to read, so widening is not the fix.
   Delete this rule once Solution becomes per-decision rows. */
.study-two-col.is-single { grid-template-columns: minmax(0, 640px); }

/* Rich text: paragraphs with inline bold-serif lead-ins + lists */
.study-richtext { min-width: 0; }
.study-richtext p { margin: 0; }
.study-richtext p + p,
.study-richtext ul + p,
.study-richtext p + ul { margin-top: 1.65em; }
.study-richtext ul {
  margin: 0;
  padding-left: 1.4em;
}
.study-richtext li {
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.65;
  color: var(--text-primary);
}

/* Which study a figure came from, and its sample size. Its own line and its
   own voice: mono and muted, so the claim reads first and the provenance sits
   under it as apparatus rather than competing as prose. Blocking it also
   stops an n= colliding with the next line's percentage mid-sentence. */
/* Two lines per bullet needs air between bullets, or the source line reads as
   belonging to the claim below it rather than the one above. */
.study-richtext li:has(.study-src) + li:has(.study-src) { margin-top: 0.7em; }
.study-src {
  display: block;
  margin-top: 0.2em;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  line-height: 1.45;
  color: var(--text-muted);
}

/* Solution mockup image (Figma: Placeholder/SolutionMockup, 590×497) */
.study-mockup {
  width: 100%;
  height: auto;
  display: block;
  filter: drop-shadow(0 4px 4px rgba(0, 0, 0, 0.25));
}

/* Learnings: single wide column of lead-in paragraphs */
.study-learnings {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* ── Responsive ────────────────────────────────────────────────── */
@media (max-width: 1100px) {
  .study-frame { display: none; }   /* frame is a desktop treatment */
  /* horizontal only — a `padding: 0 …` shorthand here would wipe the
     vertical rhythm every section sets for itself (hero, problem, …) */
  .study {
    padding-left: var(--content-pad);   /* no frame → no extra inset */
    padding-right: var(--content-pad);
  }
}

@media (max-width: 860px) {
  .study-hero-grid,
  .study-hero-grid.is-visual-lead { grid-template-columns: 1fr; gap: var(--sp-5); }
  .study-metrics { flex-wrap: wrap; gap: var(--sp-3); }
  .study-metric { max-width: 140px; }
  .study-two-col { grid-template-columns: 1fr; gap: var(--sp-5); }
  /* .is-olb-row sets its own template at 0,2,0 and would survive the
     collapse above on specificity alone. Stacked, the screen leads and the
     aside follows, which is the reading order the row wants anyway. */
  .study-two-col.is-olb-row { grid-template-columns: 1fr; }
}

/* Phone (matches the homepage's 640 breakpoint): four metrics in a
   2×2 grid instead of one skinny row. DOM order is metric, divider,
   metric, divider — so each grid row is text | rule | text | rule. */
@media (max-width: 640px) {
  .study-metrics {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
    gap: var(--sp-3) var(--sp-2);
    min-height: 0;
  }
  .study-metric { max-width: none; }
  /* single column again — non-flip hero visuals go back to the
     base squarish frame; the flip stage keeps its portrait ratio
     so the full NOW design stays visible on phones too */
  .is-visual-lead .study-visual.is-hero:not(.flip-stage) { aspect-ratio: 566 / 560; }
}

/* ════════════════════════════════════════════════════════════════
   EXPLORE THE SYSTEM  ·  process wireframe viewer (js/wireframe-viewer.js)
   "Two toggles, one frame" — one scroll-framed window per IA tier.
   Inherits the study accent tokens (--study-1..4), so it themes with
   the case study. Mirrors the THEN/NOW .flip-scroll scroll-in-frame idea.
   ════════════════════════════════════════════════════════════════ */
.wv{ width:100%; }

/* Section heading */
.wv-head{ margin:0 0 22px; }
.wv-head-title{ font-family:var(--font-serif); font-weight:700; font-size:1.5rem; line-height:1.2;
  margin:0 0 8px; color:var(--text-primary); }
.wv-head-sub{ font-family:var(--font-sans); font-size:.95rem; line-height:1.55;
  color:var(--text-secondary); margin:0; }
.wv-head-sub b{ color:var(--text-primary); font-weight:600; }

/* Tier tabs */
.wv-pages{ display:flex; flex-wrap:wrap; gap:6px; margin-bottom:18px;
  border-bottom:1px solid var(--border); padding-bottom:14px; }
.wv-page{ font-family:var(--font-mono); font-size:.82rem; letter-spacing:.01em;
  padding:7px 13px; border:1px solid var(--border); border-radius:999px;
  background:var(--surface); color:var(--text-secondary);
  cursor:pointer; transition:all .18s ease; white-space:nowrap; }
.wv-page:hover{ border-color:var(--study-3); color:var(--text-primary); }
.wv-page[aria-selected="true"]{ background:var(--study-1); border-color:var(--study-1); color:#f4efe9;
  box-shadow:inset 0 0 0 1px var(--study-2), inset 0 0 0 2px var(--study-3); }

/* Control rows (the two toggles) */
.wv-controls{ display:flex; flex-wrap:wrap; align-items:center; gap:20px 40px; margin-bottom:16px; }
.wv-ctl{ display:flex; align-items:center; gap:10px; }
.wv-descriptor{ margin-left:auto; display:flex; align-items:baseline; gap:8px;
  flex-wrap:wrap; justify-content:flex-end; }
.wv-desc-value{ font-family:var(--font-mono); font-size:.72rem; font-weight:700;
  letter-spacing:.01em; color:var(--text-muted); }
.wv-descriptor .wv-ctl-label{ text-transform:none; letter-spacing:.01em; font-size:.72rem; }
.wv-ctl-label{ font-family:var(--font-mono); font-size:.7rem;
  letter-spacing:.01em; color:var(--text-muted); }
.wv-seg{ display:inline-flex; border:1px solid var(--border); border-radius:var(--radius-sm);
  overflow:hidden; background:var(--surface); }
.wv-opt{ font-family:var(--font-sans); font-size:.82rem; font-weight:500;
  padding:6px 14px; border:0; background:transparent; color:var(--text-secondary);
  cursor:pointer; transition:all .16s ease; border-right:1px solid var(--border); }
.wv-opt:last-child{ border-right:0; }
.wv-opt:hover:not([disabled]){ color:var(--text-primary); background:var(--bg); }
.wv-opt[aria-pressed="true"]{ background:var(--study-4); color:#2b1206; font-weight:700; }
/* Strikethrough removed 2026-08-01. The only disabled option is the mobile
   WIREFRAME, and a struck-through word asserts "this was never made", which
   contradicts Step 3's "all three breakpoints from the start". Daniel confirmed
   the mobile wireframes WERE made; he simply cannot locate the files in his
   company's archive. So this is a missing ASSET, not a gap in the work, and the
   control should read as unavailable rather than as never-existed. Dimming and
   not-allowed carry that on their own. */
.wv-opt[disabled]{ opacity:.32; cursor:not-allowed; }

/* The scroll-framed window */
.wv-stage{ display:flex; justify-content:center; }
.wv-frame{ width:100%; transition:width .32s cubic-bezier(.4,0,.2,1);
  border:1px solid var(--study-1); border-radius:10px; overflow:hidden; background:var(--surface);
  box-shadow:0 1px 0 var(--study-2), 0 10px 30px -18px rgba(73,33,9,.6); }
.wv-frame.is-tablet{ width:640px; max-width:100%; }
.wv-frame.is-mobile{ width:330px; max-width:100%; }

/* Browser / device chrome */
.wv-chrome{ display:flex; align-items:center; gap:8px; padding:9px 12px;
  background:var(--study-1); border-bottom:1px solid rgba(0,0,0,.2); }
.wv-dots{ display:flex; gap:6px; }
.wv-dots i{ width:10px; height:10px; border-radius:50%; background:rgba(244,239,233,.45); }
.wv-url{ flex:1; min-width:0; font-family:var(--font-mono); font-size:.72rem; color:#e7d9c9;
  background:rgba(0,0,0,.22); border-radius:5px; padding:4px 10px;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Live link lives in the chrome, right side — reads as the toolbar's "open" action */
.wv-live{ flex-shrink:0; display:inline-flex; align-items:center; gap:5px;
  font-family:var(--font-mono); font-size:.72rem; font-weight:600; white-space:nowrap;
  color:#f4efe9; background:rgba(244,239,233,.12);
  border:1px solid rgba(244,239,233,.4); border-radius:6px;
  padding:4px 11px; text-decoration:none;
  transition:background .15s ease, border-color .15s ease, color .15s ease; }
.wv-live:hover{ background:var(--study-4); border-color:var(--study-4); color:#2b1206; }
.wv-live:focus-visible{ outline:2px solid var(--study-4); outline-offset:2px; }
.is-mobile .wv-dots{ display:none; }
.is-mobile .wv-chrome{ padding:7px 12px; }
.is-mobile .wv-url{ text-align:left; }

/* Scroll body — the height-taming core */
.wv-scroll{ height:clamp(360px,58vh,560px); overflow-y:auto; overflow-x:hidden;
  scrollbar-width:thin; scrollbar-color:var(--study-3) transparent; background:#fff; }
.wv-scroll::-webkit-scrollbar{ width:9px; }
.wv-scroll::-webkit-scrollbar-thumb{ background:var(--study-3); border-radius:9px; border:2px solid #fff; }
.wv-shot{ display:block; width:100%; height:auto; }

/* Caption under the frame */
.wv-cap{ display:flex; justify-content:flex-start; align-items:baseline; gap:12px;
  margin-top:12px; font-family:var(--font-mono); font-size:.74rem; color:var(--text-muted); }
.wv-cap b{ color:var(--text-secondary); font-weight:600; }

/* "What to notice" line per tier */
.wv-notice{ margin:0 2px 12px; font-family:var(--font-sans); font-size:.92rem; line-height:1.5;
  color:var(--text-secondary); }

/* ── Process aside ────────────────────────────────────────────────
   A titled callout for material that isn't a numbered step but shaped
   the work (e.g. the denied research request). Full-bleed within the
   step column so it reads as a break in the sequence rather than a
   mis-numbered entry, with the text held to a readable measure. */
/* Fills its grid column, so it sizes to whatever it's paired against. */
.study-aside{ padding:20px 24px; border-left:3px solid var(--study-4);
  background:var(--surface); border-radius:0 var(--radius-sm) var(--radius-sm) 0; }
.study-aside-title{ margin:0 0 10px; font-family:var(--font-mono); font-size:1rem;
  font-weight:700; letter-spacing:.01em; color:var(--study-2); }
.study-aside p{ margin:0 0 14px; font-family:var(--font-sans);
  font-size:1rem; line-height:1.65; color:var(--text-secondary); }
.study-aside p:last-of-type{ margin-bottom:0; }
.study-aside em{ color:var(--text-primary); }
.study-aside .study-cta{ margin-top:16px; }

@media (max-width:640px){
  .study-aside{ padding:16px 18px; }
}

@media (max-width:560px){
  .wv-controls{ gap:14px 24px; }
  .wv-descriptor{ margin-left:0; justify-content:flex-start; width:100%; }
}

/* ══════════════════════════════════════════════════════════════
   FOUR-TIER IA DIAGRAM  ·  Process, Step 2
   A tier ladder rather than an org chart: a left rail names each
   tier, a spine descends through them, and one real page
   is marked per row (.is-spec) so the diagram names the same five
   pages the viewer opens. Node chips reuse the .wv-page
   selected treatment so this and the viewer below read as one
   system.
   ══════════════════════════════════════════════════════════════ */
.ia{ width:100%; margin:0 0 4px; }

.ia-head{ margin:0 0 22px; }
.ia-title{ font-family:var(--font-serif); font-weight:700; font-size:1.5rem; line-height:1.2;
  margin:0 0 8px; color:var(--text-primary); }
.ia-sub{ font-family:var(--font-sans); font-size:.95rem; line-height:1.55;
  color:var(--text-secondary); margin:0; }
.ia-sub b{ color:var(--text-primary); font-weight:600; }

.ia-tiers{ list-style:none; margin:0; padding:0; }
.ia-tier{ display:grid; grid-template-columns:158px 1fr; gap:0 20px; }

/* Left rail — tier number over the template name it maps to */
.ia-rail{ display:flex; flex-direction:column; gap:2px; padding-top:8px; text-align:right; }
.ia-num{ font-family:var(--font-mono); font-size:.85rem; font-weight:700;
  letter-spacing:.02em; color:var(--study-2); }
.ia-tmpl{ font-family:var(--font-mono); font-size:.66rem;
  letter-spacing:.01em; line-height:1.45; color:var(--text-muted); }

/* Spine — the border IS the connector, so its padding-bottom sets the gap
   between tiers and the line stays continuous across them. It draws the IA
   trunk. Every row carries one specimen chip, so there is no branch for it
   to trace; the chips mark which page each template was designed on. */
.ia-nodes{ position:relative; display:flex; flex-wrap:wrap; gap:8px; align-items:flex-start;
  padding:0 0 26px 24px; border-left:2px solid var(--study-3); }
/* Last tier: shrink to its chips so the spine can't dangle past them
   (the rail is two lines tall, and a stretched grid item would run on). */
.ia-tier:last-child .ia-nodes{ padding-bottom:0; align-self:start; }
/* Tick joining the spine to that tier's row */
.ia-nodes::before{ content:""; position:absolute; left:0; top:17px; width:16px; height:2px;
  background:var(--study-3); }

/* Node chips */
.ia-node{ font-family:var(--font-mono); font-size:.8rem; line-height:1.2;
  padding:8px 14px; border:1px solid var(--border); border-radius:999px;
  background:var(--surface); color:var(--text-secondary); }
.ia-node.is-spec{ font-weight:700; background:var(--study-1); border-color:var(--study-1);
  color:#f4efe9; box-shadow:inset 0 0 0 1px var(--study-2), inset 0 0 0 2px var(--study-3); }
.ia-node.is-ghost{ background:transparent; border-style:dashed; color:var(--text-muted);
  font-style:italic; }

/* IA number from the source workbook — carries the hierarchy on its own
   (1.1 under 1.0, 1.1.1 under 1.1), so it stays legible but recessive. */
.ia-no{ font-style:normal; font-weight:400; margin-right:7px; color:var(--text-muted);
  font-variant-numeric:tabular-nums; }
.ia-node.is-spec .ia-no{ color:rgba(244,239,233,.62); }

@media (max-width:860px){
  .ia-tier{ grid-template-columns:1fr; }
  .ia-rail{ flex-direction:row; align-items:baseline; flex-wrap:wrap; gap:4px 10px;
    text-align:left; padding:0 0 8px; }
  .ia-nodes{ padding-left:18px; }
  .ia-nodes::before{ display:none; }
}
@media (max-width:560px){
  .ia-node{ font-size:.75rem; padding:7px 12px; }
}

/* ══════════════════════════════════════════════════════════════
   SOLUTION DEVICE SHOWCASE
   Replaces the single iMac mockup in the Solution right column with a
   3-device composition (desktop browser + tablet + phone) so the
   "built across desktop, tablet, and mobile" claim is visible.
   ══════════════════════════════════════════════════════════════ */
.sd-stack{
  --sd-bezel:#241f1b;
  --sd-shadow:0 22px 46px -24px rgba(73,33,9,.42), 0 7px 18px -12px rgba(0,0,0,.2);
  align-self:start;
}
/* ⚑⚑ THE STICKY CAME OFF 2026-08-31, and with it the last sticky visual on the
   site. It read `position:sticky; top:96px` with a prefers-reduced-motion
   fallback to static, and it existed because the decisions column ran much
   taller than the mockups, so they rode along instead of ending partway.
   Two reasons it went:
   (1) Daniel scrolled the four studies side by side and this was the only
       visual anywhere that pinned. Every other study answers a decision with a
       figure that scrolls past with it.
   (2) Solution became per-decision rows the same day, so the travel was gone
       anyway. Measured before the change, with Decision 3 pulled into its own
       row the remaining track left the stack 75px of travel at 1440 and 204px
       at 1280. That is a twitch, not a ride, which is worse than static.
   ⚑ DO NOT REINSTATE IT without re-measuring the travel first: a sticky with
   almost no track to run in looks like a bug. */
/* ⚑ align-items:stretch KEPT, deliberately, though the sticky it was written
   for is gone. It is NOT scoped to this page: `.study-solution .study-two-col
   .is-center` reaches banking-products.html too, where .is-arrangements opts
   back out of it by hand and where it is the documented cause of the Kiosk
   Decision 3 arrow landing below its figure. Removing it here would silently
   restyle three locked Solution bands on another page. That is a separate
   change with its own review, not a tidy-up to ride along with this one. */
.study-solution .study-two-col.is-center{ align-items:stretch; }
/* Solution became per-decision rows 2026-08-06, so consecutive grids need air
   between them. .study is a plain block container with no gap of its own. */
.study-solution .study-two-col + .study-two-col{ margin-top:var(--sp-5); }
.sd-screen{ position:relative; overflow:hidden; background:#fff; }
.sd-screen img{ display:block; width:100%; height:auto; }

/* ══════════════════════════════════════════════════════════════
   DECISION 3 — the hero band, before and after
   Two archive captures stacked, bare on the page per the no-frames rule.
   No border, no card, no captions: the two states are unmistakable and the
   .study-seealso tie beside them names both in order.
   ⚑ The gap is the only thing separating them, so it must stay large enough
   to read as two images rather than one tall one.
   ══════════════════════════════════════════════════════════════ */
.wrd3{ margin:0; display:flex; flex-direction:column; gap:14px; }
.wrd3 img{ display:block; width:100%; height:auto; }

/* Desktop = the hero, in a browser window */
.sd-browser{ width:100%; border-radius:11px; overflow:hidden; background:#fff;
  border:1px solid var(--border); box-shadow:var(--sd-shadow); }
.sd-bar{ height:30px; display:flex; align-items:center; gap:7px; padding:0 13px;
  background:#e9e6e1; border-bottom:1px solid var(--border); }
.sd-dot{ width:10px; height:10px; border-radius:50%; flex:none; }
.sd-dot.r{ background:#d8695b; } .sd-dot.y{ background:#e3b24d; } .sd-dot.g{ background:#89b86a; }
.sd-url{ flex:1; height:16px; margin-left:8px; border-radius:8px; background:#f4f2ef;
  border:1px solid #ded9d2; }
/* ⚑⚑ 16/13, NOT 16/10, as of 2026-08-31, and the reason is the arrow rather
   than the picture. Daniel: "make the visual support maybe just a tad bit
   taller in height so that it could be anchored at the top, but also maybe
   have a little bit of overage of decision two ... so it's still getting
   pointed at." A taller window is the only lever that does that without
   bottom-anchoring the figure, which he had rejected on the kiosk keypads
   hours earlier. It also shows MORE of the shipped homepage, which is the
   right kind of side effect for a decision about how the site looks: the four
   marketing card slots now come into view (settled 2026-08-04 as a deliberate
   swappable slot, NOT a defect - see Decision 3's guard before re-raising it).
   ⚑ HEIGHT WAS MEASURED AT EVERY CANDIDATE. Composition height against the
   arrow's position, at the four two-column tracks:
     16/10 (was)  312 / 280 / 240 / 210  · arrow inside at 1600 only
     1600/1197    366 / 328 / 281 / 244  · SHIPPED. Set by the artwork's cut
                                           line, not by height. Arrow inside at
                                           1600 by 51px; at 1280 it sits ON the
                                           bottom edge, within a pixel or two
     16/13        395 / 355 / 303 / 263  · more margin at 1280, but the crop
                                           runs past the "Learn More" links and
                                           Daniel read it as too tall
     16/15        451 / 405 / 344 / 299  · 43% overage, well past "a little bit"
   ⚑⚑ THIS PARAGRAPH IS SUPERSEDED AND IS KEPT ONLY TO EXPLAIN THE NUMBERS
   ABOVE. It used to end "Do not fix it with margin-top:auto", and the block
   further down now does exactly that, with Daniel's approval given later the
   same day. Review pass 3 flagged the contradiction; a standing prohibition
   sitting above the code that breaks it is how a later editor undoes a
   deliberate decision.
   What was true and stays true: the arrow's drift CANNOT be closed by
   geometry, because the prose column GROWS as the track narrows (315 -> 485)
   while the figure SHRINKS with it (395 -> 263). What changed is the remedy.
   The bottom-anchor was rejected when it was proposed on the kiosk keypads and
   accepted here once Daniel had seen why the keypads carried it. See the
   .sd-col rule below for the measurements that closed it. */
/* ⚑⚑ THE CUT LINE IS SET BY THE ARTWORK, NOT BY A TIDY RATIO, 2026-08-31.
   16/13 was chosen for height alone and it landed the crop below the card row's
   "Learn More" links, which Daniel read as too tall on sight. His instruction:
   cut "equidistant from the 'Learn More' and the three lines of copy".
   MEASURED IN THE SOURCE by scanning rows for text pixels. The rule is the
   same at every page; the numbers are not, so they were re-measured when the
   desktop pane changed page on 2026-08-31.
   NOW (Online & Mobile Banking): the first feature row's copy ENDS at y=1102
   and the second row's icons BEGIN at y=1149. Midpoint 1125.5, rounded to 1126.
   BEFORE (the homepage, superseded): copy ended y=1178, "Learn More" began
   y=1215, midpoint 1197.
   The ratio is written as 1600/1197 rather than reduced, because 1600 is the
   capture's real width, so the two numbers read as "this window shows the top
   1197px of the source". Reduce it and that meaning is gone.
   ⚑ RE-MEASURE IF THE CAPTURE IS EVER RESHOT OR THE PANE CHANGES PAGE. These
   are pixel positions in omb-after-launch-desktop2.jpg, not proportions of the
   design, and they did not survive the page change.
   ⚑ THE CAPTURE WAS RESHOT 2026-09-01 (search-icon fix, see website-redesign.html)
   and these were re-measured against the new file: copy still ends y=1102, icons
   still begin y=1149, midpoint still 1125.5. The reshoot changed one icon in the
   header and nothing below it, so 1126 stands. Re-measured, not assumed. */
.sd-browser .sd-screen{ aspect-ratio:1600/1126; }

/* ⚑⚑ ONE COMPOSITION, NOT A STACK, as of 2026-08-31. Daniel: the three mockups
   join on one row, "the baseline of where they stay ... can stay the same",
   and the phone and tablet do NOT have to sit inside the desktop's frame.
   So: the desktop is right-aligned and NARROWER than the track, and the duo
   lives to its LEFT, outside that frame, overlapping it and each other, with
   all three bottom edges on one line and nothing overhanging.
   ⚑⚑ THE PROPORTIONS ARE NOT TASTE, THEY ARE SET BY THE PROSE. Decision 2's
   paragraph measures 315px at the design width, and the .study-seealso tie is
   its LAST clause, so the arrow draws at the very bottom of the column. Three
   sets of proportions were measured against it: a 290px composition and a
   268px one both left the arrow drawing BELOW the figure, which is the exact
   Kiosk Decision 3 failure. 78% / 34% / 19.5% lands the composition at 312px
   against 315px of prose and puts the arrow inside it.
   ⚑ SO: IF THIS PROSE IS EVER EDITED, RE-CHECK THE ARROW. A paragraph two
   lines longer puts it back in empty column. Do not fix that by bottom-
   anchoring the figure - Daniel rejected exactly that trick on the kiosk
   keypads the same day. Re-tune the widths instead. */
/* ⚑⚑⚑ THE COMPOSITION IS PINNED TO THE BOTTOM OF ITS COLUMN, 2026-08-31, on
   Daniel's explicit approval: "we can override a rule and/or recreate the rule
   if we need to pin this graphic down at the bottom of the paragraph to make it
   fit next to it."
   WHY IT IS NEEDED. The .study-seealso tie is Decision 2's last clause, so its
   arrow draws at the very bottom of the prose column. The composition's height
   is percentages and an aspect-ratio, so it SHRINKS as the track narrows, while
   the paragraph GROWS as text rewraps. They diverge, and the arrow ends up
   pointing at empty column: measured 239px past the figure at 861, 118px at
   1000, 17px at 1280. Widening the browser cannot close it - at 861 even a
   100%-width browser leaves the arrow ~197px short, so the geometry is not
   recoverable by size.
   Anchored, the arrow lands 14px inside the figure at 861, 1000 AND 1280.
   ⚑ THE COST IS A HOLE ABOVE THE FIGURE: 253px at 861, 132px at 1000, 31px at
   1280, and effectively none at 1600. Daniel saw that hole on the kiosk keypads
   first and it threw him, because every other figure on the site is top-pinned;
   he accepted it here once the reason was clear. It is a deliberate trade, not
   an oversight.
   ⚑ SAME MECHANISM AS .ksf-keys AND .ksf-lineage on banking-products.html, and
   it needs a FLEX PARENT to work. margin-top:auto on its own does nothing here,
   because this figure's column is a plain block; .sd-col is what supplies the
   flex context. A first attempt set margin-top:auto alone and measured
   identical numbers before and after, which is what caught it.
   ⚑ NO TIE MAY POINT AT AN ASIDE, still. Daniel restated it while approving
   this: the anchor is about where a FIGURE sits, and changes nothing about
   Decision 1's aside, which takes no tie. */
.sd-col{ display:flex; flex-direction:column; }
.sd-col .sd-stack{ margin-top:auto; }
.sd-stack{ display:block; position:relative; }
.sd-stack .sd-browser{ width:78%; margin-right:auto; }
.sd-duo{ position:absolute; right:0; bottom:0; width:100%;
  display:flex; align-items:flex-end; justify-content:flex-end; }
/* ⚑⚑ THE ROW READS DESKTOP, TABLET, PHONE — large to small, left to right —
   and it was the other way round until 2026-08-31. Daniel asked for small to
   large originally and then asked to try the flip, because of what the old
   order did to THIS page: the duo covered the desktop's left third, and the
   Online & Mobile Banking hero is LEFT-aligned, so the biggest type in the
   figure read "re You Are!" - a broken word on the one figure whose job is
   proving the visual design is good.
   Flipped, the duo covers the PHOTOGRAPH instead, and the hero survives whole:
   eyebrow, headline, subline and the Enroll in Online Services button all
   readable, plus two full feature tiles.
   ⚑ THE OLD ORDER WAS NOT WRONG, THE PAGE CHANGED UNDER IT. It was set when
   the desktop pane was the homepage, whose hero is RIGHT-aligned and therefore
   survived a left-side overlap. The pane changed page the same day to kill a
   duplicate-image finding, and the arrangement did not follow. If this pane is
   ever repointed at a right-aligned hero, the small-to-large order becomes
   available again - check which side the type sits on FIRST.
   ⚑ Source order is tablet then phone and stays that way, so alt text still
   reads tablet then phone; `order` does the visual arrangement. */
.sd-tablet{ order:1; z-index:2; width:34%; border-radius:13px;
  background:var(--sd-bezel); box-shadow:var(--sd-shadow); }
.sd-tablet .sd-screen{ aspect-ratio:3/4; border-radius:6px; }
.sd-phone{ order:2; z-index:3; position:relative; width:19.5%; margin:0 0 0 -6%;
  border-radius:17px; background:var(--sd-bezel); box-shadow:var(--sd-shadow); }
.sd-phone .sd-screen{ aspect-ratio:9/19; border-radius:13px; }
/* ⚑ THE BEZELS ARE A PERCENTAGE, NOT A PIXEL COUNT, and that is the whole fix
   Daniel asked for: "make the outline of the mockups ... a little thinner,
   because they're reading a little thick now that they're on the same line."
   They were 10px and 7px of fixed padding, set when these devices rendered at
   230px and 135px in their own full-width row. On one line they render smaller,
   and a fixed bezel around a smaller device reads heavier at exactly the moment
   it should recede. The bezel now shrinks with the track instead of looking
   right at one width only: 6.0px and 4.9px at the design width, against 10px
   and 7px before.
   ⚑⚑ THESE PERCENTAGES RESOLVE AGAINST .sd-duo's WIDTH, NOT THE DEVICE'S.
   That is the CSS rule for percentage padding and it bites hard here: the
   first attempt used 3% and 4.2%, reading them as "3% of the tablet", and
   shipped a 17.3px and 24.2px bezel - nearly TRIPLE what it replaced, in a
   change whose entire purpose was to make them thinner. Measured, not
   eyeballed, which is the only reason it was caught. If the duo's width ever
   stops being 100% of the track, these two numbers have to move with it. */
.sd-tablet{ padding:1.05%; }
.sd-phone{ padding:0.85%; }

/* ══════════════════════════════════════════════════════════════
   TRANSFER & PAY HERO  ·  two real screens, colour-annotated
   Replaces the .tph scatter diagram (chips + a five-strand SVG
   connector), removed 2026-08-01.

   Why the diagram went. Its "before" half was a drawing, which
   meant the study ASSERTED the scatter rather than showing it.
   The real menu proves it and needs no caption: Transfer, Bill
   Pay and Send Money sit together in the white group, and
   Western Union is stranded four rows down in the GREY group,
   between Tax Documents and Account Alerts. The app's own
   two-tone grouping carries the argument.

   Why not a flip frame (the first proposal): Banking GPS
   already uses one and both studies are TABS ON THIS PAGE, so a
   reader tabbing across would meet the identical component
   twice at one URL. A static pair is the distinct form.

   Both phones BLEED off the bottom of the frame on purpose. A
   portrait 390x844 screen fitted whole into a 590x497 landscape
   box lands near 160px wide, too small to read either the menu
   rows or the quick-action row, and those are the entire proof.
   .study-visual already clips.
   ══════════════════════════════════════════════════════════════ */
.tps{
  width:100%; height:100%;
  display:flex; align-items:flex-start; justify-content:center;
  gap:clamp(12px, 4%, 44px);
  letter-spacing:normal; text-align:left;
}
.tps-side{
  flex:1 1 0; min-width:0;
  display:flex; flex-direction:column; align-items:center; gap:11px;
}
.tps-kicker{
  margin:0; font-family:var(--font-mono); font-size:0.6875rem;
  letter-spacing:.01em;color:var(--text-muted);
  line-height:1.3; text-align:center;
}
.tps-phone{
  width:100%; max-width:300px; flex:none;
  padding:6px 6px 0; border-radius:22px 22px 0 0;
  background:#241f1b;
  box-shadow:0 22px 46px -24px rgba(73,33,9,.42), 0 7px 18px -12px rgba(0,0,0,.2);
}
/* .tps-screen is the crop window, .tps-shot is the image at natural height and
   the containing block for every mark. Keeping them separate is what lets the
   stacked breakpoint shorten the window without moving the marks. */
.tps-screen{ position:relative; border-radius:16px 16px 0 0; overflow:hidden; }
.tps-shot{ position:relative; }
.tps-shot img{ display:block; width:100%; height:auto; }

/* ── Annotation marks ──
   TWO categories, not five. The first pass gave each feature its own step of
   the crimson ramp so a reader could colour-match it across the two screens.
   At a 2px border those four steps are indistinguishable, which is the same
   failure the old chip diagram hit with 9px dots. The mapping was redundant
   anyway: every one of these features is LABELLED in both screenshots, so the
   reader matches by name unaided. Colour only has to answer "which of these
   twenty rows matter" and "which one is new". So crimson for the four that
   moved, dashed purple for the Zelle that arrived. Four marks on the left,
   five on the right, and the extra one is visibly marked.

   The white outer ring is what keeps a 2px border legible over BOTH the white
   menu group and the grey one. Percentages resolve against .tps-screen, which
   is the image's own 390x844 box, so every mark scales with the phone. */
.tps-mk{
  position:absolute;
  border:2px solid var(--crimson-4);
  border-radius:5px;
  box-shadow:0 0 0 2px rgba(255,255,255,.55);
}
.tps-mk.is-new{ border-color:#6D28D9; border-style:dashed; }

/* Coordinates below are MEASURED, not eyeballed: both PNGs were drawn to a
   canvas and scanned for ink runs, so every figure traces to a real pixel
   bound in a 390x844 image. Re-derive that way if either screenshot is
   replaced. Menu panel starts at x=117; its rows have a 30.5px pitch with
   text runs at y=192-207 (Transfer), 221-239 (Bill Pay), 252-267 (Send
   Money) and 395-414 (Western Union). Hub quick-actions: icons y=89-118 and
   labels y=124-145 for row 1, icons y=175-205 and labels y=213-234 for row 2.

   Within a row the marks share one height and one width, centred per cell,
   rather than hugging each label. Zelle and Send Money carry a second label
   line, so hugging made those two boxes taller than their neighbours and the
   row read as a mistake rather than a set. */

/* left phone — tp-menu-before.png. Height 3.20% against a 3.61% row pitch
   leaves a visible gap between the three stacked marks; any taller and
   Bill Pay's bottom edge meets Send Money's top and the three read as one
   block with dividers. */
.tps-m-transfer{ left:31.54%; width:66.41%; top:22.04%; height:3.20%; }
.tps-m-billpay { left:31.54%; width:66.41%; top:25.65%; height:3.20%; }
.tps-m-send    { left:31.54%; width:66.41%; top:29.15%; height:3.20%; }
.tps-m-wu      { left:31.54%; width:66.41%; top:46.33%; height:3.20%; }

/* right phone — tp-hub.png */
.tps-h-zelle   { left:13.21%; width:17.95%; top:9.83%;  height:7.94%; }
.tps-h-transfer{ left:40.26%; width:17.95%; top:9.83%;  height:7.94%; }
.tps-h-billpay { left:67.82%; width:17.95%; top:9.83%;  height:7.94%; }
/* Row 2 runs wider than its measured ink: Send Money's second label line,
   "(Woodforest P2P)", is the widest string in the panel and a box sized to
   its exact bound touches the parentheses at both ends. */
.tps-h-wu      { left:24.15%; width:23.50%; top:20.02%; height:8.29%; }
.tps-h-send    { left:51.58%; width:23.50%; top:20.02%; height:8.29%; }

/* The frame goes square at <=640px, but the pair still FITS side by side
   there: at a 614px viewport the visual is 574 wide, so two phones land near
   250px each, which is still readable. Only the gap needs to come in. */
@media (max-width: 640px){
  .tps{ gap:clamp(8px, 3%, 20px); }
  /* The square frame fits the hub screen WHOLE, which is worse than the desktop
     crop: the reader gets the FAQ list, ~190px of blank white and the 5px Zelle
     limits disclaimer, against a left phone that is dense to its edge. Matching
     the desktop crop, which lands just under the Send Money FAQ row. */
  .tps-side:last-child .tps-screen{ aspect-ratio:390/620; }
}

/* Below ~520px the visual is narrow enough that a side-by-side pair puts each
   phone under 160px, where neither screenshot is legible and the marks ring
   things a reader cannot identify. So the pair stacks.
   Two full 390x844 screens stacked run past 1000px, well beyond the square
   frame, so each screen is cropped to the part that carries its claim: the
   menu down through Western Union (its ink ends at y=414), and the hub down
   through the quick-action panel (y=240), which the original build already
   identified as the entire proof of the "one hub" claim.
   justify-content must also be reset. It is CENTER on the row axis, but once
   the direction turns to column that same declaration centres the overflow
   and clips the top of the first phone as well as the bottom of the last. */
@media (max-width: 520px){
  /* ⚑ The frame MUST release its aspect-ratio here, or the whole fix is
     cosmetic. .study-visual.is-hero keeps 566/560 at every width, so the frame
     height shrinks with the viewport while the stacked pair's height is pinned
     by .tps-phone{max-width:216px}. Measured before this rule, the "One hub"
     panel was 88.7% visible at 515px, 30.3% at 426px, 10.6% at 396px and 0% at
     356px: on every current iPhone the hero showed the broken app and clipped
     the fix out of frame entirely, in a case study about a phone app.
     Precedent on this page: .tpa drops its aspect-ratio for the same reason. */
  #study-transfer-pay-hub .study-visual.is-hero{ aspect-ratio:auto; }
  .tps{ flex-direction:column; align-items:center; justify-content:flex-start; gap:16px; }
  .tps-side{ flex:none; width:100%; }
  .tps-phone{ max-width:216px; }
  .tps-side:first-child  .tps-screen{ aspect-ratio:390/445; }
  .tps-side:last-child   .tps-screen{ aspect-ratio:390/288; }
}

/* ══════════════════════════════════════════════════════════════
   .tpk — THE TRANSFER & PAY MARK, INTRODUCED ONCE (Process Step 1)
   Lives inside the Step 1 .study-aside. Its job is orientation, not
   evidence: it names the new icon and the places it took over, so
   that when the reader meets the mark again in the hero, the
   component library and the Solution diagram, they already know
   what it is.

   ⚑ Drawn as the quick-action ITEM it actually is (tile plus label),
   not as a bare glyph, because that is the form it takes in the app.

   ⚑ NO BOX. It sits directly on the aside. A bordered card here was
   built first and cut by Daniel 2026-08-05: it made a panel inside a
   panel, and it gave a one-line label the visual weight of a
   component spec. The tile alone is enough of a container.

   ⚑ .tpk-name sets its size explicitly because .study-aside p is
   1rem/1.65 and would otherwise swallow it.
   ══════════════════════════════════════════════════════════════ */
/* on the title line: "The way in:  [mark] Transfer & Pay" */
.tpk-head{ display:flex; align-items:center; flex-wrap:wrap; gap:6px 10px;
  margin-bottom:14px; }
.tpk-mark{ flex:none; width:38px; height:38px; display:grid; place-items:center;
  border:1.5px solid var(--study-4); border-radius:8px; background:#fff; }
.tpk-mark img{ width:23px; height:23px; display:block; }
.tpk-name{ margin:0; font-family:var(--font-sans); font-size:0.9375rem;
  font-weight:600; line-height:1.3; color:var(--text-primary); }

/* ══════════════════════════════════════════════════════════════
   .tpb — UNUSED as of 2026-08-05. Kept one round, not deleted.
   The before-only audit graphic that briefly held the Step 1 slot.
   Daniel replaced it with a prose aside (.study-aside + .tpk): the
   reader's problem there was never that the fragmentation needed
   proving, it was that the new icon arrives unannounced in every
   later visual. An aside answers that; a graphic of the old state
   does not. Markup is in git and at scratchpad/ if it is wanted back.
   Delete this block once the aside has survived a review.

   .tpb — THE FRAGMENTED STATE (T&P Hub, Process Step 1)
   The three real navigation surfaces, BEFORE only, annotated as an
   audit. Rebuilt from scratch 2026-08-05 after ten rounds of a
   before/after band in this slot.

   ⚑ NO "AFTER" HERE, AND THAT IS THE WHOLE IDEA. Step 1 is titled
   "Mapping the fragmented state": nothing has been designed yet, so
   a Transfer & Pay icon in this slot leaks the ending into the first
   beat and the strikethroughs assert a deletion the step has not
   earned. It also caused every sizing problem this graphic ever had:
   a before/after PAIR of 390px bars needs ~798px, which forced the
   crop, which cut Deposit out, which made the picture a fragment of
   a fragment. One bar alone fits the ~557px slot at FULL SCALE. The
   size war was a symptom of showing the wrong thing.
   Do not re-add an after half. It lives in the Solution band.

   ⚑ MARKED, NOT STRUCK. A wash marks the money-movement features as
   the audit's subject. A strikethrough would say "removed", and at
   this point in the story nothing has been removed; even later, only
   the entry points consolidated. Same reasoning as the .tpn note.

   ⚑ THE MENU CROP IS TALL ON PURPOSE. It runs Transfer through
   Western Union so the FOUR unrelated rows between them are visible.
   The distance is the finding: the fourth money mover was not even
   filed with the other three. Do not tighten this crop to save
   height; tightening it deletes the argument.

   ⚑ THE STEP'S OWN SENTENCE IS DRAWN, NOT CAPTIONED. "Four features,
   four menu items, three quick-action icons, no Zelle anywhere" is
   checkable against the picture: four washed rows in the menu, three
   washed icons on each bar, and Zelle in an empty slot at the foot.
   ⚑ Say "three of the four FEATURES", never "three icons" — both
   bars show FOUR icons, because Deposit is there and is not one of
   this study's four features. A reader counts what is on screen.

   ⚑ No loading="lazy" on the images: <main> starts at opacity 0 under
   a pageFadeIn animation, and lazy images in a subtree the browser
   treats as invisible are never fetched.
   ══════════════════════════════════════════════════════════════ */
.tpb{ width:100%; display:flex; flex-direction:column; gap:15px;
      letter-spacing:normal; text-align:left; }

.tpb-row{ display:flex; flex-direction:column; gap:5px; }
.tpb-name{ margin:0; font-size:0.6875rem; color:var(--text-primary); line-height:1.2; }
.tpb-name small{ color:var(--text-muted); }

/* Shots stack, note runs full width underneath. A gutter note beside the
   shot was built first and does not fit: the slot's INNER width is 507px,
   not the 557px the outer box measures, so a 390px bar leaves 101px, which
   is not a text column. Full-width notes read better anyway and cost one
   line each. Measure the padding box, not the border box. */
.tpb-line{ display:flex; flex-direction:column; align-items:flex-start; gap:5px; }
.tpb-note{ margin:3px 0 0; align-self:stretch;
  font-size:0.625rem; line-height:1.5; color:var(--text-muted); }
.tpb-note b{ color:var(--text-primary); font-weight:600; }

.tpb-shot{
  position:relative; overflow:hidden;
  /* content-box: every percentage inside resolves against the padding box,
     while --w/--h describe the crop region itself */
  box-sizing:content-box;
  width:calc(var(--w) * 1px); max-width:100%;
  aspect-ratio:var(--w) / var(--h);
  flex:0 1 auto; min-width:0;
  border:1px solid var(--border); border-radius:3px; background:#fff;
}
.tpb-shot img{
  position:absolute; display:block; border:0; border-radius:0;
  /* styles.css:140 sets a global img{max-width:100%} that would clamp a zoomed
     crop back to its window and land correct offsets on the wrong picture */
  max-width:none;
  width:calc(var(--iw) / var(--w) * 100%);
  height:auto;
  left:calc(var(--x) / var(--w) * -100%);
  top:calc(var(--y) / var(--h) * -100%);
}

/* whole bars, nothing cropped away */
.s-ao  { --iw:390; --ih:76;  --x:0; --y:0;   --w:390; --h:76; }
.s-ad  { --iw:390; --ih:76;  --x:0; --y:0;   --w:390; --h:76; }
/* menu: Transfer down to Western Union, both groups. Row geometry measured
   off the PNG's ink, not the Figma box: text bands sit at y125.5, 155, 186
   and 330 in 273-space, a 30px pitch broken by the group gap. */
.s-menu{ --iw:273; --ih:779; --x:0; --y:118; --w:273; --h:240; }

/* the audit's subject, marked rather than struck */
.tpb-mark{ position:absolute; background:rgba(173,40,49,.10);
  border:1px solid rgba(173,40,49,.42); border-radius:4px; pointer-events:none; }

/* account overview bar: Transfer x130/36.5, Bill Pay x204.5/31.5, Send Money x276/54 */
.m-ao-1{ left:32.05%; width:11.79%; top:18.42%; height:65.79%; }
.m-ao-2{ left:51.15%; width:10.51%; top:18.42%; height:65.79%; }
.m-ao-3{ left:69.49%; width:16.28%; top:18.42%; height:65.79%; }
/* account details bar: same three, shifted left by the chevron's tenancy */
.m-ad-1{ left:26.03%; width:11.79%; top:18.42%; height:65.79%; }
.m-ad-2{ left:47.44%; width:10.51%; top:18.42%; height:65.79%; }
.m-ad-3{ left:66.54%; width:16.28%; top:18.42%; height:65.79%; }
/* menu rows, x15-220 in 273-space */
.m-mn-1{ left:5.49%; width:75.09%; top:1.46%;  height:11.04%; }
.m-mn-2{ left:5.49%; width:75.09%; top:13.75%; height:11.46%; }
.m-mn-3{ left:5.49%; width:75.09%; top:26.67%; height:10.63%; }
.m-mn-4{ left:5.49%; width:75.09%; top:86.67%; height:11.46%; }

/* the chevron: account details could not fit its own actions */
.tpb-chev{ position:absolute; border:1px solid var(--crimson-4);
  border-radius:50%; pointer-events:none;
  left:89.74%; width:7.05%; top:26.32%; height:36.18%; }

/* the four rows standing between Send Money and Western Union */
.tpb-gap{ position:absolute; pointer-events:none;
  border-left:1px dashed var(--crimson-4);
  left:84.25%; top:37.29%; height:49.38%; }
.tpb-gap::before, .tpb-gap::after{ content:""; position:absolute;
  left:-4px; width:9px; border-top:1px solid var(--crimson-4); }
.tpb-gap::before{ top:0; }
.tpb-gap::after{ bottom:0; }

/* Zelle, drawn as the vacancy it was. An empty dashed slot with the mark
   ghosted inside says "belongs here, is not here"; a footnote sentence,
   which is where this fact used to live, says it without showing it. */
.tpb-zelle{ margin:0; display:flex; align-items:center; gap:11px;
  padding:9px 11px; border:1px dashed var(--crimson-4); border-radius:4px;
  background:rgba(173,40,49,.045);
  font-size:0.6875rem; line-height:1.5; color:var(--text-primary); }
.tpb-slot{ flex:none; width:32px; height:32px; display:grid; place-items:center;
  border:1px dashed rgba(173,40,49,.5); border-radius:6px; background:#fff; }
.tpb-slot img{ width:15px; height:auto; opacity:.3; filter:grayscale(1); }

/* ══════════════════════════════════════════════════════════════
   .tpq — WHAT CHANGED ON EACH SURFACE (T&P Hub, Process Step 1)
   Three surfaces, each with its own before and after, in the real
   shipped components.

   ⚑ BESIDE THE PROSE, and the bars are CROPPED to the items that
   changed. Whole bars were tried at Daniel's request and are only
   possible full width: a 390px bar cannot scale below ~0.9 without
   its 12px labels dying, so a pair needs 798px against a ~544px
   column. Full width worked and was rejected as "way too big for
   such a short design decision", which is the right call. Step 1 is
   a short audit step and a full-bleed band over-weights it. Cropping
   Deposit out of the bars is framing, not misstatement; a full-bleed
   band that dominates the section is a proportion error. Do not
   restore whole bars without moving this full width again.

   ⚑ EACH SURFACE KEEPS ITS OWN PAIR. A previous build hoisted one
   shared "before" out to save space and introduced two errors: the
   scroll chevron belongs to account details alone but sat in a panel
   labelled "on all three", and the menu ended up with a bar as its
   before and a list row as its after, which are different components.
   The three befores look repetitive because the same three features
   left every surface. That repetition is the finding. Do not collapse
   it again.

   ⚑ Both bar rows are cropped past their unchanged Deposit icon, so
   the two are directly comparable and neither reads as "this surface
   had no Deposit".
   ══════════════════════════════════════════════════════════════ */
.tpq{ --k:0.87;
      width:100%; display:flex; flex-direction:column; gap:9px;
      letter-spacing:normal; text-align:left; }

.tpq-stack{ display:flex; flex-direction:column; gap:10px; }
.tpq-row{ display:flex; flex-direction:column; gap:3px; }
.tpq-name{ margin:0; font-size:0.6875rem; color:var(--text-primary); line-height:1.2; }
.tpq-name small{ color:var(--text-muted); }
.tpq-pair{ display:flex; align-items:center; gap:9px; min-width:0; }
.tpq-arrow{ flex:none; color:var(--crimson-4); display:grid; place-items:center; }

.tpq-crop{
  position:relative; overflow:hidden;
  /* content-box: every percentage inside resolves against the padding box,
     while --w/--h describe the crop region itself */
  box-sizing:content-box;
  width:calc(var(--w) * var(--k) * 1px);
  max-width:100%;
  aspect-ratio:var(--w) / var(--h);
  flex:0 1 auto; min-width:0;
  border:1px solid var(--border); border-radius:3px; background:#fff;
}
.tpq-crop img{
  position:absolute; display:block; border:0; border-radius:0;
  /* styles.css:140 sets a global img{max-width:100%} that would clamp a zoomed
     crop back to its window and land correct offsets on the wrong picture */
  max-width:none;
  width:calc(var(--iw) / var(--w) * 100%);
  height:auto;
  left:calc(var(--x) / var(--w) * -100%);
  top:calc(var(--y) / var(--h) * -100%);
}

/* cropped past each bar's unchanged Deposit icon */
.c-ao-before  { --iw:390; --ih:76; --x:115; --y:6; --w:231; --h:64; }
.c-ao-after   { --iw:390; --ih:76; --x:150; --y:6; --w:161; --h:64; }
.c-ad-before  { --iw:390; --ih:76; --x:92;  --y:6; --w:290; --h:64; }
.c-ad-after   { --iw:390; --ih:76; --x:96;  --y:6; --w:272; --h:64; }
/* menu: the rows that changed. Measured off the PNG's ink, not the Figma box */
.c-menu-before{ --iw:273; --ih:779; --x:8; --y:119; --w:257; --h:94; }
.c-menu-after { --iw:273; --ih:779; --x:8; --y:116; --w:257; --h:42; }

/* what left each surface */
.tpq-gone{ position:absolute; background:rgba(173,40,49,.09);
  border-radius:3px; pointer-events:none; }
.tpq-gone::after{ content:""; position:absolute; left:6%; right:6%; top:50%;
  border-top:1.5px solid var(--crimson-4); transform:translateY(-50%); }
/* account overview, window x115-346: Transfer x125.5/45, Bill Pay x200.5/39, Send Money x269.5/66 */
.g-ao-1{ left:4.55%;  width:19.48%; top:15.63%; height:68.75%; }
.g-ao-2{ left:37.01%; width:16.88%; top:15.63%; height:68.75%; }
.g-ao-3{ left:66.88%; width:28.57%; top:15.63%; height:68.75%; }
/* account details bar: Transfer x102/45, Bill Pay x186/39, Send Money x264/55 */
.g-ad-1{ left:26.15%; width:11.54%; top:15.63%; height:68.75%; }
.g-ad-2{ left:47.69%; width:10.00%; top:15.63%; height:68.75%; }
.g-ad-3{ left:67.69%; width:14.10%; top:15.63%; height:68.75%; }
/* menu rows: 201x20 at x21, y125/155/185 */
.g-mn-1{ left:5.06%; width:78.21%; top:6.38%;  height:21.28%; }
.g-mn-2{ left:5.06%; width:78.21%; top:38.30%; height:21.28%; }
.g-mn-3{ left:5.06%; width:78.21%; top:70.21%; height:21.28%; }

/* what arrived, ~6px clear of its content on every side */
.tpq-ring{ position:absolute; border:1.5px solid var(--crimson-4); border-radius:5px;
  box-shadow:0 0 0 2px rgba(255,255,255,.55); pointer-events:none; z-index:1; }
.r-ao  { left:5%;   width:43.5%; top:6.25%;  height:87.5%; }
.r-ad  { left:1.4%; width:27.2%; top:6.25%;  height:87.5%; }
.r-menu{ left:3.5%;  width:81.3%; top:14.3%;  height:64.3%; }

.tpq-foot{ margin:0; font-size:0.6875rem; line-height:1.5; color:var(--text-muted); }
.tpq-foot b{ color:var(--text-primary); font-weight:600; }

/* below ~880 a pair no longer fits side by side at full size, so it stacks
   and the arrow turns down rather than shrinking the bars into illegibility */
@media (max-width: 560px){
  .tpq-pair{ flex-direction:column; align-items:flex-start; gap:5px; }
  .tpq-arrow{ transform:rotate(90deg); margin-left:16px; }
}

/* ══════════════════════════════════════════════════════════════
   .tpa — ICON ARRANGEMENTS (Transfer & Pay Hub, Solution visual)
   Five entitlement states of the quick-action row, 5 down to 1.

   Why a diagram and not a screenshot: the arrangements are a
   design-system artifact, and the shipped MVP screen only ever
   shows ONE of the five states, so a screenshot could not carry
   the claim. Slot boxes, not chips: the tokens here are the
   grid positions themselves.

   Slot sizing is driven off the 3-across row so every state is
   measured against the same unit, which is the point being made.
   ══════════════════════════════════════════════════════════════ */
/* No aspect-ratio on this frame, unlike every other .study-visual. Seven
   slot-rows deep by three slots wide is an intrinsically PORTRAIT shape, and
   the inherited 590/497 landscape box left two thirds of the frame empty
   while squeezing the slots to 34px. The panel sizes to its content and
   centres in the track instead.

   .study-solution .study-two-col.is-center is forced back to
   align-items:stretch (see above) for the website-redesign sticky stack, and
   that global rule reaches this page too: it stretched this frame to the full
   1490px height of the four decisions. Opting back out, then riding along
   like the redesign's mockups do, since the decision list is much taller. */
/* ⚡ DEAD AS OF 2026-08-06 — .tpa and .is-arrangements below.
   The arrangements diagram was removed from banking-products.html at Daniel's
   direction; the Solution slot is being replaced by per-decision visuals.
   Kept rather than deleted, on this page's standing convention for dead
   case-study CSS (same as .tpb and .tpq): delete once the replacement visuals
   have passed a review. Markup is in git.
   If it is ever revived, two defects go with it: the kicker read "One row,
   five entitlement states" while the 5- and 4-states each draw TWO rows
   (Decision 1 calls it a three-then-two grid), and the 4-slot geometry was
   never confirmed by Daniel (drawn 2+2, shorter rows drawn centred). */
.study-solution .study-visual.is-arrangements{
  width:min(100%, 316px); margin-inline:auto;
  align-self:start; position:sticky; top:96px;
  padding:clamp(14px, 5%, 22px);
}
@media (prefers-reduced-motion: reduce){
  .study-solution .study-visual.is-arrangements{ position:static; }
}

.tpa{
  width:100%;
  display:flex; flex-direction:column;
  gap:clamp(9px, 3%, 15px);
  letter-spacing:normal; text-align:left;
}
.tpa-kicker,
.tpa-note{
  margin:0; font-family:var(--font-mono); font-size:.6875rem;
  letter-spacing:.01em;color:var(--text-muted);
  line-height:1.3;
}
.tpa-note{ text-transform:none; letter-spacing:.02em; }

.tpa-set{
  list-style:none; margin:0; padding:0;
  display:flex; flex-direction:column;
  gap:clamp(9px, 3%, 14px);
}
/* The count sits in its own gutter so every grid starts on the same x, which
   is what makes the set read as one row collapsing rather than as five
   unrelated layouts. */
.tpa-case{
  display:flex; align-items:center; gap:clamp(8px, 3%, 12px);
}
.tpa-n{
  flex:none; width:1.4em;
  font-family:var(--font-mono); font-size:.8125rem; line-height:1;
  color:var(--study-1); text-align:right;
}
/* Sub-rows are explicit in the markup rather than wrapped by a 3-column grid.
   Centring a short row inside a grid means spanning two tracks from opposite
   edges, the spans overlap, and auto-placement then pushes each slot onto its
   own row: the 4-state rendered four rows tall. Flex rows, centred, are what
   the shipped screen actually does anyway (tp-hub.png: three across, then
   Western Union and Send Money centred beneath), and that centring is the
   reason the arrangements had to be designed rather than left to wrap. */
.tpa-grid{
  flex:1 1 auto; min-width:0;
  display:flex; flex-direction:column; gap:clamp(5px, 3%, 10px);
}
.tpa-row{
  display:flex; justify-content:center; gap:clamp(5px, 3%, 10px);
}

/* Constant square at every count, so the five states stay comparable: this
   visual is a size-invariant comparison, and slots that stretched to fill
   their row would make the 1-state a banner and defeat it. */
.tpa-slot{
  width:clamp(34px, 15vw, 58px); aspect-ratio:1 / 1;
  display:flex; align-items:center; justify-content:center;
  border-radius:var(--radius-sm);
  background:var(--bg); border:1px solid var(--border);
  box-shadow:0 6px 16px -13px rgba(73,33,9,.5);
}
/* Dashed and unfilled: an empty slot is a POSITION, not a missing icon. */
.tpa-slot:not(.is-real){ background:transparent; border-style:dashed; box-shadow:none; }
.tpa-slot img{
  height:46%; width:auto; flex:none;
  object-fit:contain; display:block;
}

@media (max-width: 640px){
  .tpa-kicker, .tpa-note{ font-size:0.6875rem; }
  .tpa-slot{ width:clamp(30px, 13vw, 46px); }
}

/* ── .tpd — Transfer & Pay device frames ──────────────────────
   The full-vision hub is 390x2674: a screen you cannot show at a
   legible width without it becoming the tallest thing on the page,
   which is exactly why the first attempt at this was cut on 8/4.
   A device with an internal scroll decouples the artwork's height
   from the page's, so the screen renders near 1:1 (its 12px UI type
   has a hard floor around 0.9 scale) inside a fixed window.
   Sized for a PROCESS COLUMN, not a full-width band: it sits beside
   the step whose design it is.
   The app chrome (status bar + title bar, Figma 2157:54756) is
   PINNED and the body scrolls under it, which is what the real app
   does and what makes the frame read as a phone rather than a tall
   picture.
   Both devices share one window proportion, so the shipped screen
   fills its window exactly and never scrolls while the designed one
   runs 3.4 windows deep. That comparison is the whole point; do not
   let the two window sizes drift apart, and do not caption it.
   ────────────────────────────────────────────────────────────── */
.tpd{ width:100%; display:flex; flex-direction:column; align-items:center; gap:10px; }
/* .study-figure gives a non-device visual the same labelled treatment: a
   kicker over the thing, centered. The component-library board uses it, since
   an unlabelled artifact between two labelled ones reads as decoration. */
.study-figure{ display:flex; flex-direction:column; gap:10px; }

/* Small label over each device (and, via .study-figure, over the board too).
   Same treatment as the hero's .tps-kicker,
   which does the same job for the same reason: two phone screens that mean
   nothing as a pair until each one is named. Above, not below, so the reader
   knows what they are looking at before they look. */
.tpd-kicker{
  /* ⚑ NOT UPPERCASE (Daniel, 2026-08-28). text-transform:uppercase was shouting
     these; it and the .09em tracking that came with it are gone (that much
     tracking exists to open up caps). Do not reinstate either.
     ⚑ CASE IS SET IN THE MARKUP AND IT IS TITLE CASE, not sentence case. This
     comment said sentence case for part of 2026-08-28, from an earlier edit
     made before the site-wide sweep landed; Daniel reversed to AP-style title
     case in the same day's sweep and the strings were retyped. The rule of
     record is [[feedback-sentence-case-labels]]; follow that file, not this
     comment, if the two ever disagree. */
  margin:0; font-family:var(--font-mono); font-size:0.6875rem;
  letter-spacing:.01em; color:var(--text-muted);
  line-height:1.3; text-align:center;
}
.tpd-phone{
  /* 402 -> 340 -> 300, all 2026-08-05. Everything else in the device is sized
     off this one number (the chrome is an img at width:100%, the window is an
     aspect-ratio), so the phone stays a phone as it scales.
     ⚑ The ~0.9 scale floor from the .tpb work does NOT bind here, and assuming
     it did is what kept this oversized. That floor came from a cropped bar
     where 12px UI type was the evidence being read. On a whole screen the type
     that has to survive is section headers and list labels (14-17px), and the
     vision body is a 2x asset, so it downsamples clean. Rendered at 1x, the
     non-retina worst case, both screens were still legible at 268 (0.687):
     "Transaction Activity", "All Accounts", "January", "To: Jane Doe",
     amounts, statuses, every FAQ row. 300 keeps margin on that.
     Tested floor is ~280. Below it the phone stops reading as a designed
     artifact and starts reading as a thumbnail, and it begins to look lost in
     a 582px column. Re-test at 1x before going lower; do not just assume.
     min(), not a flat width, or a fixed size pushes a horizontal scrollbar
     onto the whole page on narrow viewports. */
  width:min(300px, 100%); flex:none;
  padding:6px; border-radius:22px;
  background:#241f1b;
  box-shadow:0 22px 46px -24px rgba(56,4,14,.42), 0 7px 18px -12px rgba(0,0,0,.2);
}
.tpd-device{ border-radius:16px; overflow:hidden; background:#fff; }
/* Chrome is a real 2x export, not a drawn bezel, and both devices use
   the same file: same app, same header. */
.tpd-chrome{ display:block; }
.tpd-chrome img{ display:block; width:100%; height:auto; }

/* The scroll window. aspect-ratio, not a pixel height: it makes the window
   exactly the shipped screen's proportion at ANY width, so "the shipped one
   fills it and does not scroll" holds on every viewport and platform instead
   of only on the machine a fixed height was measured on.
   tabindex lives on the markup: a scrollable region has to be reachable by
   keyboard (WCAG 2.1.1). */
.tpd-screen{
  position:relative;
  aspect-ratio:390 / 779;
  overflow-y:auto; overflow-x:hidden;
  background:#fff;
  /* No native scrollbar. A permanent track down the side of a phone reads as a
     browser window; js/device-scroll.js draws the phone version instead:
     overlaid, thin, and only while scrolling. */
  scrollbar-width:none;
}
.tpd-screen::-webkit-scrollbar{ width:0; height:0; }
.tpd-screen:focus-visible{ outline:2px solid var(--study-4); outline-offset:3px; }

/* Phone-native scroll indicator. Absolutely positioned inside the scroll box
   and pushed down by scrollTop in JS, so it stays pinned to the visible window
   instead of scrolling away with the content. */
.tpd-bar{
  position:absolute; top:0; right:2px;
  width:3px; height:100%;
  pointer-events:none;
  opacity:0; transition:opacity .35s ease;
}
.tpd-bar.is-on{ opacity:1; transition-duration:.12s; }
.tpd-thumb{
  display:block; width:100%;
  background:rgba(0,0,0,.28);
  border-radius:2px;
}
@media (prefers-reduced-motion: reduce){
  .tpd-bar{ transition:none; }
}
/* .tpd-shot is the image's own box and the containing block every layer
   bracket resolves against. Keep it separate from the scroll window: put the
   height on the element that also parents the marks and every percentage
   starts pointing at a different number of pixels. */
.tpd-shot{ position:relative; }
.tpd-shot img{ display:block; width:100%; height:auto; }

/* Layer brackets. Percentages of the shot's own height, so they hold as the
   phone narrows. Offsets are Figma geometry, not measured off the picture:
   quick-action menu 0-242, common actions 266-554, transaction history
   578-2674, over a 2674 screen.
   Updated 2026-08-07 with the Send Money change (frame 3135:51548). Send Money
   picked up a "(Woodforest P2P)" second line, which grew the quick-action menu
   222 -> 242 and pushed everything under it down 20. The first three numbers
   are from the node metadata; 578 was confirmed against the asset itself, at
   the top of the black Transaction Activity bar (row 1155 of the 2x file).
   ⚑ The screen's layout is 2694 tall now but the Figma frame was left at 2739
   (65 chrome + 2674), so the export still ends at 2674 and history closes 20
   short of its own box. That is why layer 3 is 2096 and not 2116. It costs the
   tail of a row that was already cut mid-row: the feed is meant to run past
   the bottom. If the frame is ever resized, re-read all four numbers. */
.tpd-layer{
  position:absolute; left:0; right:0;
  border:2px solid var(--study-4);
  border-radius:6px;
  pointer-events:none;
}
/* Badges ride the top-RIGHT corner, all of them (Daniel 8/5). Every layer on
   these screens opens with its own section title on the LEFT, so a badge in
   that corner clips the first letter: the "T" of Transaction Activity, the "F"
   of FAQs. Right is not free either (it lands on the Date sort control and on
   a row chevron) but a covered control costs less than a section title a
   reader is using to identify the layer. Keep them all on one side. */
.tpd-layer::before{
  content:attr(data-n);
  position:absolute; top:-1px; right:-1px;
  min-width:18px; height:18px; padding:0 5px;
  display:flex; align-items:center; justify-content:center;
  background:var(--study-4); color:#fff;
  font-family:var(--font-mono); font-size:10px; font-weight:700;
  border-radius:0 5px 0 6px;
}
.tpd-l1{ top:0;      height:9.05%; }
.tpd-l2{ top:9.95%;  height:10.77%; }
.tpd-l3{ top:21.62%; height:78.38%; }

/* Shipped screen: same brackets, its own geometry, and the numbers carry the
   story. 1 is the layer that survived, 4 is the FAQs added during rescoping,
   and 2 and 3 are visibly absent because they were cut. Offsets are Figma
   symbol heights (Scope=MVP quick action 197, FAQs Rows=4 328, summing to the
   525 of Scope=FAQs/MVP) over the 779 the shipped asset is tall; both
   boundaries were checked against the asset itself. */
.tpd-s1{ top:0;       height:25.29%; }
.tpd-s4{ top:25.29%;  height:42.11%; }

/* The prose carries the explanation, so the phone only needs to be findable
   from it. These are the same badges as the brackets, inline in Step 2's
   sentence, replacing a legend column that repeated the paragraph beside it. */
.tpd-ref{
  display:inline-flex; align-items:center; justify-content:center;
  min-width:16px; height:16px; padding:0 4px; margin:0 1px 0 3px;
  vertical-align:2px;
  background:var(--study-4); color:#fff;
  font-family:var(--font-mono); font-size:10px; font-weight:700; font-style:normal;
  border-radius:4px;
}

/* An inline badge creates a break opportunity BEFORE the punctuation that
   follows it, so a line can start with ":" or ",". Caught in the 2026-08-05
   Process review; it reproduced at three different widths on three different
   badges, so it is a property of the badge, not of one measure.
   Tie the badge to the word before it and the punctuation after it. Keep the
   tie to ONE word so it can never be wider than the column. */
.tpd-tie{ white-space:nowrap; }

@media (max-width: 900px){
  .tpd{ margin-top:8px; }
}

/* ── .tpv — VARIANT PAIR ──────────────────────────────────────────────
   Solution's visual register, and it is deliberately NOT the device register
   above. Process owns whole screens: the vision phone, the shipped phone, the
   before menu. All three are compositions, one screen at one moment, and they
   carry the arc. Solution's claims are about the OTHER axis, one component
   across the states it has to survive, which no screenshot of a screen can
   show. So these are bare panes with a label: no phone chrome, no bezel, no
   scroll. A fourth and fifth phone on this page would read as phone soup and
   would also lie about what is being shown, since these are component
   variants, not screens a customer navigates between.
   Built 2026-08-06 for Decision 4. The pair is the argument: same feed, same
   data, one sort control, two information architectures. That is what
   "specified to build rather than sketched" looks like, and it is why the two
   panes must stay side by side wherever there is room for them. */
.tpv{ width:100%; }
.tpv-pair{
  display:flex; gap:20px; justify-content:center; flex-wrap:wrap;
  /* wrap, not a stacked media query: below ~640 the two panes drop to one
     column on their own, and the captions keep the comparison legible even
     when the reader can no longer see both at once. */
}
/* 50% of the column, not a fixed 290: the pair moved into the 515px right
   column 2026-08-06 and two 290s wrap there, which stacked the panes and made
   the figure 927px tall against ~390px of prose. Side by side is the whole
   point of a comparison, so the panes give up width to keep it. */
.tpv-col{ margin:0; width:calc(50% - 10px); flex:none;
  display:flex; flex-direction:column; gap:7px; }
/* The crop is the point. These variants run 2088 and 2158 tall and the
   difference between them is fully made in the first few rows: by date opens
   on a month header with four different features under it, by feature opens on
   a feature header with its own rows under it. 420 shows that twice over.
   Masked rather than cut, so the bottom edge reads as "continues" instead of
   as a badly measured box. No "scroll for more" anywhere: there is nothing to
   scroll, and narrating the crop would be instructional text. */
/* Rebuilt 2026-08-06 to start at the black "Transaction Activity" bar instead
   of at the first row of the feed, per Daniel. The reason is the whole argument
   of the figure: cropped to the feed alone, the panes show an EFFECT with no
   visible cause, and the reader has to take the caption's word for it. Starting
   at the header puts the sort control, the account dropdown and the
   Upcoming/History tabs inside the frame, so the control that produces the
   difference is visible in both panes.
   Geometry lives on the markup, not here, because the two assets are different
   heights and a percentage offset resolves against each image's own height:
     --h    visible height in SOURCE px (aspect-ratio does the rest)
     --top  where the header bar starts, as a % of that image's height
   The bar sits at source y=559 in both screens, measured off the exports. If
   the quick-action or common-actions blocks above it ever change height, both
   --top values move together. */
.tpv-shot{
  position:relative;
  aspect-ratio:390 / var(--h);
  overflow:hidden;
  /* var(--border), the same hairline .tpb-shot uses. NOT --study-2: that token
     is the theme's saturated crimson, and two of them boxing a white pane would
     read as a callout rather than as a crop. */
  border:1px solid var(--border); border-radius:3px; background:#fff;
  -webkit-mask-image:linear-gradient(to bottom, #000 78%, transparent 100%);
          mask-image:linear-gradient(to bottom, #000 78%, transparent 100%);
}
/* Assets are Daniel's own 2x exports, swapped in 2026-08-06 over the 1x MCP
   renders this was built against. They are the UPCOMING pair (3112:35923 by
   date, 3112:35991 by pay type), not the posted pair the first build used: both
   panes have to differ by the SORT ALONE, or the reader cannot tell whether
   what changed was the sort or the tab. If either is ever re-exported, keep
   both on the same tab. */
.tpv-shot img{
  position:absolute; top:0; left:0;
  display:block; width:100%; height:auto;
  transform:translateY(var(--top));
}
.tpv-cap{
  margin:0; font-family:var(--font-mono); font-size:0.6875rem;
  letter-spacing:.01em;color:var(--text-muted);
  line-height:1.3; text-align:center;
}

/* ── .gpr — Banking GPS Step 4, the points of interest before and after ──
   ⚑ REVIVED 2026-08-16 as the FIFTH form of this figure, on Daniel's own
   spec: one old overlay window beside one live redesigned page, side by
   side, in Step 4's right cell. (The 8/14 original paired a 3x3 scroll-tile
   grid with a full-page export; both panes changed, the arithmetic is the
   same.)
   ⚑⚑ THE EQUAL-HEIGHT LOCK IS DELIBERATELY GONE, 2026-08-16, Daniel's
   call reversing the 8/14 rule: "the overlay in old course can be shrunk
   down, and the redesign one can be made taller. I don't care if there are
   different heights, because one's an overlay, the other one is a full
   screen." The mismatch is the message now: a small cramped window beside
   a tall full page. Panes are plain width shares (50/50 of the pair minus
   gap since 2026-08-16; 40/60 before), tunable by eye, top-aligned. Do not reinstate the aspect
   arithmetic without his say-so; its record stays in the session memory's
   POI-figure note if a matched pair is ever wanted again. */
/* (The 820px cap is inert in the 576px cell; kept in case the pair ever
   returns to a full-width band, where it was Daniel's sizing call.) */
.gpr-pair{ display:flex; gap:24px; align-items:flex-start; justify-content:center;
  max-width:820px; margin-inline:auto; }
.gpr-cap{
  font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em; color:var(--text-muted); line-height:1.35;
  text-align:center; text-wrap:balance; /* both captions orphaned one word
  ("...OPENS AN / OVERLAY", "...ITS OWN / PAGE") in the 276px panes at every
  desktop width; balance splits them evenly without touching the wording */
}
.gpr-col{ margin:0; display:flex; flex-direction:column; gap:7px; min-width:0; }
/* ⚑ OUTLINE, NOT BORDER, and it is not a style preference. With the global
   border-box, a 1px border eats into width:100%, and 2px is 1% of the 195px
   page pane against 0.2% of the 973px grid pane. That asymmetry alone put the
   two images 4px out of alignment and broke the equal-height guarantee the
   percentages above exist to provide. An outline draws inside the box and
   costs no layout, so both panes render at exactly width / aspect. */
.gpr-col img{ display:block; width:100%; height:auto; border-radius:3px;
  outline:1px solid var(--border); outline-offset:-1px; }
/* ⚑ 50/50 since 2026-08-16 (was 40/60): with the "now" asset cropped at the
   RESOURCES runner (see the img comment in banking-products.html), an even
   split brings the row from 485px to 362 (measured at 1920; 367 at 1440,
   327 at 1280) against Step 4's 267px of prose.
   That was the answer to "which lever": the pair's height, not more
   negative margin. */
.gpr-col.is-then{ flex:0 0 calc((100% - 24px) * 0.50); }
.gpr-col.is-now { flex:0 0 calc((100% - 24px) * 0.50); }
/* Below this the "now" pane is under ~130px wide, where the page stops reading
   as a page and becomes a grey stripe. Stack instead, both panes at ONE shared
   width, and let the aspects do the work: the overlay (1450/727) renders about
   half as tall as the page (995/1092), so "small cramped window vs tall full
   page" survives the stack with no arithmetic. ⚑ Until 2026-08-17 this rule
   was width:100% on the overlay and min(230px,60%) on the page, a cap written
   for the deleted 3x3 grid pane; on every phone the cramped overlay rendered as
   the BIGGEST image in the row and inverted the comparison (386px: 346x173 vs
   208x228). The captions carry the pairing when the reader can no longer see
   both at once, same reasoning as .tpv-pair. */
@media (max-width:820px){
  .gpr-pair{ flex-direction:column; align-items:center; gap:18px; }
  .gpr-col.is-then,
  .gpr-col.is-now { flex:0 0 auto; width:min(300px, 88%); }
}

/* ── .gpv — Banking GPS Step 4, the reference content as a three-tab flip ──
   Third form of this figure, 2026-08-16: the .gpe grid below made the copy
   "really, really tiny" (Daniel), so the versions became tabs on one frame,
   each rendering at full cell width. Same machinery as the heroes
   (js/flipframe.js is generic over N tabs); differenced from them at his
   direction: the tabs are a FULL-WIDTH segmented row spanning the stage,
   not the heroes' small pill pair.
   ⚑ The stage takes the ACTIVE pane's ratio, the hero rule: the redesign
   pane is one 995x1322 live screen; the old-course pane stacks the three
   overlay windows (1450x727 each) with 2.08%-of-width margins, total
   1000/1546. The margins are percentages OF WIDTH (margin-% resolves
   against width), which is what keeps the stacked pane's aspect true at
   every viewport. Do not switch them to px or to gap.
   ⚑ The shelved pane shares the redesign's ratio ON PURPOSE: a full-size
   empty dashed room is the evidence for "had dropped them altogether". It
   reuses .gpe-none, which stays LIVE for this reason. */
/* ⚑⚑ .gpv IS DEAD, 2026-08-16, same day it was built: Daniel, after the
   50%-shrink + weld revision: "This isn't really what I envisioned. This
   isn't it." The figure landed as the revived .gpr pair above instead (his
   own spec). Left in place beside .gpe like .tpo-list. The full four-form
   rejection trail is in the session memory's POI-figure note; do not
   resurrect any form of it without his sketch. */
.gpv .study-flip{ max-width:300px; width:100%; margin-inline:auto; }
.gpv .flip-tabs{ padding-left:0; margin-bottom:0; }
.gpv .flip-tabs-list{ flex:1; display:grid; grid-template-columns:repeat(3,1fr); gap:4px; }
.gpv .flip-tab{ justify-content:center; text-align:center; padding:11px 6px; }
.gpv-stage{ position:relative; aspect-ratio:995/1322;
  border-top-left-radius:0; border-top-right-radius:0; }
.gpv-stage:has(.gpv-pane-old.is-active){ aspect-ratio:1000/1546; }
.gpv-stage:has(.gpv-pane-none.is-active){ aspect-ratio:995/420; }
.gpv-pane-old{ display:flex; flex-direction:column; }
.gpv-pane-old img{ height:auto; object-fit:unset; }
.gpv-pane-old img + img{ margin-top:2.08%; }
.gpv-none{ width:100%; height:100%; }

/* ── .gpe — ⚑ FULLY DEAD since 2026-08-16 ────────────────────────────
   The 3x3 grid was replaced by the .gpv flip (Daniel: the copy read too
   small), and .gpv died the same day; the figure landed as the revived
   .gpr pair. Everything here (.gpe/.gpe-label/.gpe-row/.gpe-none) is now
   unused, left in place .tpo-list style.
   Original comment, kept for the build record: ──────────────────────────
   .gpe — Banking GPS Step 4, the reference content version by version ──
   The POI rethink (Daniel, 2026-08-15): the removed .gpr figure was one
   overlay before/after; this is all three pieces of reference content across
   all three versions, in Step 4's deliberately-empty right cell. Three rows:
   the old course's overlay windows, the shelved version (which HAD none:
   showing its consolidation-board pages here would violate the unstamped-frame
   rule, so the row is an honest empty state), and the redesign's LIVE screens.
   ⚑ CORRECTED 2026-08-15, same day as the first build: the redesign row
   first used the "(Full)" frames, which Daniel identified as flattened
   copy-review artifacts for the developer and legal/compliance, not shipped
   screens. The row now shows the live 7.0/8.0/9.0 pages, all 1000x1328
   (the content sits in a scrolling frame inside the course page), so the row
   is uniform and the first build's true-relative-length rag went away with
   the wrong assets. align-items:start stays; it costs nothing and keeps any
   future mixed-height row honest.
   ⚑ No numbers anywhere in the figure: Step 4's prose owns "280 pixel
   window" and "nine times taller" (same rule as the removed .gpr). */
.gpe{ display:flex; flex-direction:column; gap:8px; }
.gpe-label{
  margin:8px 0 0; font-family:var(--font-mono); font-size:0.6875rem;
  letter-spacing:.01em;color:var(--text-muted);
  line-height:1.3;
}
.gpe-row{ display:grid; grid-template-columns:repeat(3,1fr); gap:12px; align-items:start; }
/* outline not border, per the .gpr lesson above: mixed-aspect images in one
   figure go out of alignment when a border eats asymmetrically into width. */
.gpe-row img{ display:block; width:100%; height:auto; border-radius:3px;
  outline:1px solid var(--border); outline-offset:-1px; }
.gpe-none{
  border:1px dashed var(--border); border-radius:3px; min-height:46px;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em; color:var(--text-muted);
}

/* ── .gps-lessons — three shipped lesson screens, 3-up ───────────────
   ⚑ RENAMED from .gps-signs 2026-08-16 when Daniel swapped the three
   section dividers for three lesson screens he picked himself (Online
   Banking / Tune Up reconciliation / Pre-Authorizations). Same rules,
   same slot under the component-library board; the divider era's history
   lives in the HTML comments beside the figure. All three new exports are
   995x1322, the same aspect as the old sign crops, so the row still needs
   no aspect arithmetic. */
.gps-lessons-row{ display:grid; grid-template-columns:repeat(3,1fr); gap:24px; }
.gps-lessons-row img{ display:block; width:100%; height:auto; border-radius:6px;
  outline:1px solid var(--border); outline-offset:-1px; }
@media (max-width:720px){
  .gps-lessons-row{ grid-template-columns:1fr; gap:18px; justify-items:center; }
  .gps-lessons-row img{ max-width:min(360px, 88%); }
}

/* ── .gps-solo — one shipped screen beside a Solution decision ───────
   Added 2026-08-19 with the three GPS Solution figures (quiz review /
   certificate / glossary). Same img treatment as .gps-lessons.
   ⚑ TOMBSTONE, 2026-08-30: the 400px width cap is GONE and the argument that
   justified it went with it. It read "the width cap is the height lever: 400px
   keeps the tall certificate (998x1553 source) from towering over its
   paragraph." That was true when written and is not true now: the certificate
   was cropped afterwards and renders at ratio 1.09, not 1.56. A future editor
   who read the case FOR the cap before reading that it was lifted would put it
   straight back. Same species as the .ksf-title tombstone: when a thing is
   deleted, its justification goes with it. */
/* ⚑⚑ LEFT-ALIGNED, NOT CENTRED, 2026-08-30. margin:0 auto centred a 400px
   figure inside a 576px column, which put 88px of dead space on the INSIDE
   edge, between the paragraph and the visual it belongs to. Added to the
   grid's own 40px gap that made the measured prose-to-visual distance 128px
   on every Solution row, more than three times the designed gutter, and
   Daniel read it exactly as it renders: the visual stops feeling connected to
   the argument it supports. Left-aligning spends that space on the OUTSIDE
   edge, next to the frame, where it reads as air instead of a gap.
   ⚑ THE PARAGRAPH THAT SAT HERE WAS DELETED 2026-08-30 (pass 5, finding C1).
   It read "The 400px cap stays and is still the height lever ... Do not widen
   the figure to close the gap": a live instruction to undo the fix Daniel
   asked for by eye, sitting eight lines below the tombstone recording that the
   cap is GONE and twenty-five above the note recording it RETIRED, in front of
   a rule that reads max-width:none. Same species as the .ksf-title tombstone:
   when a thing is deleted, its justification goes with it. */
.gps-solo{ margin:0; width:100%; max-width:none; }

/* ⚑⚑ THE FIGURES FILL THEIR COLUMN, 2026-08-30, and the prose is vertically
   centred beside them. Both halves of that are one fix and neither works alone.
   THE PROBLEM: left-aligning a 400px figure in a 576px column closed the
   prose-to-visual gap to the designed 40px, but left a 176px hole down the
   whole right side of the Solution, while the Process aside directly above it
   runs the full 576px to the frame. Daniel read it as the section going
   left-heavy, and it was.
   ⚑ NARROWING THE FIGURE WAS NEVER THE PROBLEM AND WIDENING IT IS NOT FREE.
   Measured at the four decisions: 400px leaves 176px dead but sits only ~38px
   taller than its paragraph; 576px kills the dead space and runs ~230px taller.
   So filling the column moves the leftover height from right-of-figure to
   below-prose. That is the better place for it: white space under a paragraph
   reads as ordinary spacing between decisions, while a 176px column down the
   right side reads as a hole.
   ⚑ CENTRING THE PROSE WAS TRIED AND REVERTED THE SAME MINUTE. align-items:
   center balanced the two halves optically but pushed "Decision 1" 115px BELOW
   the top of its own figure, so the image led and the decision title floated
   in from nowhere under the Solution heading. Top-aligned keeps the reading
   order: heading, decision title, then the evidence beside it. The rows stay
   on the inherited align-items:stretch; do not add a centring override.
   ⚑ THE 400px CAP IS RETIRED, and the note it carried ("the width cap is the
   height lever ... keeps the 998x1553 certificate from towering over its
   paragraph") is now stale on its own terms: the certificate was cropped after
   that was written and renders at ratio 1.09, not 1.56. Do not restore the cap
   to solve a height problem that no longer exists.
   ⚑ WHAT THIS BUYS BEYOND BALANCE: at 400px the lesson copy inside the D1 and
   D3 screenshots is too small to read, which is the recurring finding across
   this tab's figures. At column width it is legible. */

.gps-solo img{ display:block; width:100%; height:auto; border-radius:6px;
  outline:1px solid var(--border); outline-offset:-1px; }
/* ⚑⚑ NO WIDTH CLAMP HERE, AND DO NOT ADD ONE BACK. 2026-08-30, Solution pass 5
   finding 1. A @media (max-width:720px) rule capped .gps-solo at
   min(360px, 88%), which dropped D1/D3/D4 from 649px to 360px on a ONE PIXEL
   change of viewport and left a 288px ragged right edge down three of the four
   decisions while .gps-lineage beside them still filled the column. This is the
   SAME defect pass 4 wrote up on .gps-lineage (the clause cell going 449px to
   160px at the same breakpoint); it was deleted there and left live here.
   It also inverted this block's own stated payoff: 360px is NARROWER than the
   retired 400px cap, at exactly the widths where the lesson copy inside the
   screenshots is already hardest to read.
   ⚑ THE RULE, twice learned: never clamp one of these figures' total width to
   solve a small-screen problem. The column is already the constraint. */

/* ── .gps-lineage — Decision 2's sourced→restyled evidence ───────────
   ⚑⚑ REBUILT 2026-08-29 as the .ksf-ihead/.ksf-irow icon-lineage pattern
   (the Kiosk Solution figure), renamed to the gps- prefix so a kiosk-named
   class does not appear on the GPS tab. Same 64px grid, same arrow column,
   same name+clause cell. The figure it replaces measured 634px; this is 326.
   ⚑ TOMBSTONE: .gps-lin-row, .gps-lin-uses, .gps-lin-pair and .gps-lin-cell
   (with .is-comp) are DELETED. They built four stacked rows running three
   different reading rules in one column; the full case against that structure
   is on the figure in banking-products.html. Do not re-add them.
   ⚑ The old .gps-lin-uses carried a documented overflow bug (four images sized
   by HEIGHT alone summing wider than their container, which gave the whole
   case study a horizontal scroll at 386px). The rule it produced still governs
   this block and every sibling: NEVER SIZE A MULTI-IMAGE ROW BY HEIGHT ALONE
   WITHOUT A WIDTH BOUND. This pattern sizes by a bounded 64px cell instead, so
   the bug class cannot recur here.
   ⚑ No frames anywhere: every asset is a transparent cutout sitting on the
   page, except the Getty thumbs-up, whose teal square is part of the licensed
   artwork rather than a frame this design added. */
.gps-lineage{ margin:0; width:100%; max-width:none;
  display:flex; flex-direction:column; gap:4px; }
.gps-lin-head,
.gps-lin-irow{ display:grid; grid-template-columns:64px 30px 64px 1fr; gap:14px;
  align-items:center; }
.gps-lin-head{ padding:0 0 8px; border-bottom:1px solid var(--border); }
.gps-lin-head span{ font-family:var(--font-mono); font-size:0.6875rem;
  letter-spacing:.01em; color:var(--text-muted); }
.gps-lin-irow{ padding:9px 0; border-bottom:1px solid var(--border); }
.gps-lin-irow:last-child{ border-bottom:0; }
.gps-lin-arrow{ font-family:var(--font-mono); font-size:1rem;
  color:var(--text-muted); text-align:center; }
.gps-lin-name{ font-family:var(--font-sans); font-size:.875rem; line-height:1.45;
  color:var(--text-secondary); }
.gps-lin-name b{ display:block; color:var(--text-primary); font-weight:600; }
.gps-lin-ic{ width:64px; display:flex; align-items:center; justify-content:center; }
.gps-lin-ic img{ display:block; max-width:64px; max-height:64px;
  width:auto; height:auto; }
/* ⚑ THE AWARD PAIR IS MATCHED ON WIDTH, NOT HEIGHT, and it is not a style
   preference. The medal circle is the widest element in both icons and the
   circle is what the row compares; matching total height renders the restyled
   medal's circle smaller, because its ribbon tails are proportionally longer. */
.gps-lin-irow.is-award .gps-lin-ic img{ width:54px; max-height:none; height:auto; }
/* ⚑⚑ THE 720px CLAMP WAS DELETED 2026-08-30 and must not come back. It read
   `.gps-lineage{ max-width:min(360px,88%) }`, copied from .gps-solo, where it
   is correct: a portrait SCREENSHOT at full column width would be enormous.
   This is not a screenshot. It is a text-bearing grid, and the clamp starved
   the one column carrying the argument. Measured at the breakpoint: the row
   has already collapsed to one column by 720px, so the figure had 648px
   available and voluntarily took 360, dropping the clause cell from 449px to
   160px on a ONE PIXEL change of viewport. At 386px "The Good Effort Icon"
   broke over two lines above five lines of 104px-wide text.
   ⚑ The fix is the sibling's, not a different number: .ksf-ihead/.ksf-irow
   shrink the ICON COLUMNS and leave the clause its width. Measured at the same
   720px viewport before this change: Kiosk clause 492px, GPS clause 160px,
   same page, same pattern. Never clamp a lineage figure's total width to solve
   a small-screen problem; shrink its fixed tracks instead. */
@media (max-width:860px){
  .gps-lin-head,
  .gps-lin-irow{ grid-template-columns:52px 22px 52px 1fr; gap:10px; }
  .gps-lin-ic{ width:52px; }
  .gps-lin-ic img{ max-width:52px; max-height:52px; }
  .gps-lin-irow.is-award .gps-lin-ic img{ width:44px; max-height:none; }
}

/* ── .kct — the resolution tiers, beside Process Step 2 ──────────────
   Added 2026-08-19. Step 2 is the one step whose entire subject is a set
   of numbers (three resolution tiers, and which one got bought), and it
   was one of SIX steps on this tab carrying no visual at all.
   Drawn, not photographed, because the artifact that would have carried
   it (the branch resolution spreadsheet the product owner brought) is
   gone and Daniel cannot get it back quickly. SVG rather than jpg for a
   reason the study itself argues: the labels have to stay legible from
   820px down to 320, which a raster of 16px type does not.
   Full-width row, not a column: at half width the labels fall under 8px. */
.kct{ margin:0; width:100%; }
.kct img{ display:block; width:100%; height:auto; }

/* ── .study-lead-in — Process Step 1, full width above the numbered rows.
   Daniel 2026-08-19: the branch round "is not a part of my design process".
   He is right, and it still cannot be cut: the LOCKED Problem band spends
   its ONLY customer evidence on it ("The branch staff I asked... At three
   branches"), so deleting it strands "I asked" with no antecedent. Rendered
   full width instead of as a half column so it reads as where the evidence
   came from, a lead-in to the numbered work rather than the first item. */
.study-lead-in{ margin: 0 0 var(--space-lg, 2rem); max-width: 66ch; }
/* ⚑ 66ch, not full width. Daniel 2026-08-19: a paragraph run across the
   whole 1192px band broke the line-length rule (it measured ~150 characters
   per line against the 45-75 the measure asks for) and "is such a weird way
   to read it". Full width was the wrong way to signal "not a numbered step";
   the signal is that it sits alone above the rows, not that it is wide. */

/* ── .tpo — MARKED DEAD 2026-08-07 ───────────────────────────────────
   Built and removed the same night. It failed for a reason worth keeping: four
   of its five rows were Decision 1's own prose reformatted and set twelve
   inches to its right, word for word in places ("the feature customers were
   closing accounts over", "the least-used of the four the app had"). A figure
   that restates the paragraph beside it makes the reader read one argument
   twice. Same bar as Impact's bullets: every line must carry something that
   appears nowhere else.
   Kept, not deleted, because the next attempt at this row needs to know this
   one was tried. Delete once Decision 1's row is settled. Same convention as
   .tpa, .tpb, .tpq and .tpi-note.

   Original note follows.

   ── .tpo — THE ORDER, AS AN ARGUMENT ────────────────────────────────
   Decision 1's figure, and the only one in this section that is not a picture
   of the product. That is deliberate and it took four tries to get here.
   What was tried and why each failed: an entry-points strip (evidenced where
   the hub is reachable FROM, while the decision argues the ORDER inside it);
   an empty right column (next to three filled rows it reads as unfinished); a
   cropped quick-action menu (that menu is already in the hero, both Process
   phones and the component-library board, so a fifth appearance is repetition,
   and blown up to the full 515 column a 780px export is 1.5x where every other
   pane here is 3x, so it was visibly soft); and a single 640px column with no
   figure at all, which in a 1286px band reads as a missing half, not as an
   opening statement.
   The resolution: Decision 1's claim is a RANKING, and the artifact that
   carries a ranking is the menu the page has already spent. So the figure
   shows the argument instead of the artifact. Five slots, each with the reason
   it sits where it sits, and a rule marking the two demotions, which the prose
   itself calls "the part that needed a reason".
   ⚑ SLOT 3 IS DELIBERATELY BLANK. The copy gives no reason for bill pay
   anywhere, and prose can glide past that while a list cannot. Do not invent
   one to fill the hole. Either it stays blank and honest, or Daniel supplies
   the real reason. */
.tpo{ width:100%; }
.tpo-list{ margin:0; padding:0; list-style:none;
  display:flex; flex-direction:column; }
.tpo-row{ display:grid; grid-template-columns:1.6rem 1fr; gap:0 14px;
  padding:11px 0; border-top:1px solid var(--border); align-items:baseline; }
.tpo-row:first-child{ border-top:0; padding-top:2px; }
.tpo-n{ font-family:var(--font-mono); font-size:.8125rem; font-weight:700;
  color:var(--study-2); }
.tpo-name{ font-family:var(--font-sans); font-size:1rem; font-weight:600;
  color:var(--text-primary); line-height:1.35; }
.tpo-why{ grid-column:2; font-size:.8125rem; line-height:1.5;
  color:var(--text-secondary); margin-top:3px; }
/* The demotion gets the only structural mark in the figure, because it is the
   only part of the ranking the prose says had to be defended. */
.tpo-row.is-cut{ border-top:2px solid var(--study-4); margin-top:6px;
  padding-top:14px; }

/* ── .tpi — ICON ARRANGEMENTS ─────────────────────────────────────────
   Decision 2's claim that the menu stays composed at every entitlement count,
   which was the section's one fully unevidenced sentence. Five panes, one per
   count, cropped to the quick-action block alone.
   Why cropped to the menu and not the screen: at the 515px this column gives,
   two panes land at ~245 each, and a whole 390-wide screen at 245 puts its body
   type near 7px. Cropping to the icons removes every piece of type the reader
   does not need. What has to survive is the ICON GRID REFLOWING (3+2, then 2+2,
   then one row of three, two, one), and the icons are 30px in the source, so
   they hold. The count is carried by the caption, not by reading a label.
   Why five and not four: the copy says five, four, three, two and one. A visual
   that samples the claim invites the reader to ask which one was left out.
   ⚑ The Figma board these come from carries a red note that the icons shown are
   EXAMPLES and the real set depends on what the customer is entitled to. So the
   captions name counts only. Never caption these with feature names. */
.tpi{ width:100%; }
/* justify-content:center only bites on the LAST row, since a full row of two
   panes already fills the width exactly. That is the intent: five panes leave a
   single pane alone at the end, and left-aligned it reads as a layout accident
   rather than as the end of a progression. */
.tpi-set{ display:flex; flex-wrap:wrap; gap:14px 20px;
  align-items:flex-start; justify-content:center; }
.tpi-case{ margin:0; width:calc(50% - 10px); flex:none;
  display:flex; flex-direction:column; gap:6px; }
/* Stacked, full-column variant. Decision 1's panes are quick-action BARS: 390
   wide by 76 tall in source. Side by side at half a column they render about
   48px high, which reads as two slivers rather than as a designed placement.
   Full width they get the shape they actually have, and stacking them is also
   the argument: the same destination, twice, in two different contexts. */
.tpi.is-stack .tpi-case{ width:100%; }
/* --h is the source-pixel height of that arrangement's menu block, measured off
   the exports, not guessed. The panes are deliberately UNEVEN: the block really
   does shrink, and forcing one height would either crop an icon row or pad the
   short ones with the FAQ bar underneath. */
.tpi-shot{
  position:relative; overflow:hidden;
  aspect-ratio:390 / var(--h);
  border:1px solid var(--border); border-radius:3px; background:#fff;
}
/* translateY percentages resolve against the IMAGE's own height, so this offset
   holds at every pane width without a single hardcoded pixel: the menu starts
   65px down a 844px-tall screen, 65/844 = 7.7014%. If the title bar height ever
   changes in Figma, this is the one number to change.
   --top is overridable because these panes now serve two figures: the
   arrangements strip, which crops into a whole screen and needs the default,
   and Decision 3's FAQ pair, whose assets are bare component variants that
   start at their own top edge and pass --top:0%. */
.tpi-shot img{
  position:absolute; top:0; left:0; width:100%; height:auto;
  transform:translateY(var(--top, -7.7014%));
}
.tpi-cap{
  margin:0; font-family:var(--font-mono); font-size:0.6875rem;
  letter-spacing:.01em;color:var(--text-muted);
  line-height:1.3; text-align:center;
}

/* The one line of prose the strip needs. Without it a reader concludes that
   three features MEANS Zelle, Transfer and Bill Pay, which is false and is the
   exact thing Daniel's dev note on the Figma board guards against.
   FULL WIDTH, not a measure. It was set to 42ch and centered, which put it
   directly under the centered ONE pane at almost the same width, so it read as
   that pane's caption rather than as a note on the set. Width is what carries
   scope here: the kicker spans the whole strip and is read as belonging to all
   five, and this has to do the same. */
/* ⚑ MARKED DEAD 2026-08-06, same convention as .tpb and .tpq. Both notes it
   was built for are gone: the arrangements note went with the arrangements
   strip, and the turn-away note was cut as a restatement of Decision 2's prose.
   Kept because the reasoning below is the standard for any future figure note,
   and because the bar it encodes is worth keeping: a note earns its place only
   by preventing a false inference, never by restating the prose. Delete once a
   review confirms no Solution figure needs one.

   Treatment chosen 2026-08-06 over the .study-aside card Daniel asked about.
   The aside is a titled callout with a crimson rule and a filled background,
   and on this page it carries narrative that breaks a sequence (the denied
   research request in Process). Under a small figure it inverts the weight:
   the annotation would out-shout the artifact it annotates, and it would read
   as a second voice arriving mid-decision when this line belongs to Decision
   2's prose.
   A full-width hairline does the same job for nothing: it separates the note
   from the ONE pane's caption directly above it, and because the rule spans the
   whole strip it says "this is about all five" structurally rather than by
   asking the reader to infer it from the text.
   Not a "NOTE:" label either. That is a word of pure furniture, and in mono
   caps it would compete with the kicker at the top of the same figure.
   --text-secondary, not --text-muted: the captions are muted because they are
   labels, but this is a sentence that has to be read. */
.tpi-note{
  margin:14px 0 0; width:100%;
  padding-top:12px; border-top:1px solid var(--border);
  font-size:.8125rem; line-height:1.5; text-align:left;
  color:var(--text-secondary);
  /* LEFT, not centered, 2026-08-06. Centered it read as a third label: the
     right column ran caption (mono caps, centered), rule, this, then the next
     figure's kicker (mono caps, centered), and a sentence wearing the labels'
     alignment gets read as one. Centering suits labels and fails running prose.
     The full-width rule still carries the scope; alignment now carries the
     register. */
}

/* Half of a 515px column is a legible pane. Half of a phone is not: below this
   the two-column grid has already collapsed to one, so the panes get the whole
   width instead of splitting a screen that cannot afford it. */
@media (max-width: 600px){
  .tpv-col, .tpi-case{ width:100%; }
}

/* ── Banking GPS Process, Step 5: the audit punch list ──────────────────────
   (Step 6 when added; the section renumbered 2026-08-14.)
   Added 2026-08-12 at Daniel's direction. The step carried four success criteria,
   six measurements and four fixes in one 250-word paragraph: the densest block
   on the tab, all of it earned, and the only step with no visual relief. The
   content was never the problem, the presentation was.

   Why a ledger and not the hero's metric-card pattern: the cards would sit one
   screen below the hero's own metric row and read as a second, competing
   scoreboard, and at half-column width their labels crop. A ledger also carries
   what a card cannot, which is the PAIRING. Every row here is a measured
   failure set against the thing the design does about it, and that pairing is
   the whole argument of the step.

   Hierarchy does the work, not decoration. No arrow glyphs, no icons, no rules
   between the two lines of a row: the criterion is crimson and small, the
   failure is secondary ink, the fix is primary ink. A reader scanning only the
   dark lines gets the four fixes; a reader scanning only the crimson gets the
   four criteria. Nothing here is furniture.

   ⚑ The fixes stay in Daniel's voice and scoped to the DESIGN ("I redrew",
   "I gave"), for the same reason hero card 2 says "designed out": nobody has
   audited the shipped build. Do not rewrite a fix into a claim that the shipped
   course passes the criterion. */
.gps-punch{
  list-style:none; margin:var(--sp-3) 0 0; padding:0;
  border-top:1px solid var(--border);
}
.gps-punch > li{
  padding:10px 0;
  border-bottom:1px solid var(--border);
}

/* Full-width Step 5. The step is a direct child of .study-process-steps rather
   than a cell in .study-two-col, so it spans the band the same way the wide
   visual strip does. */
.gps-step-wide{ grid-column:1 / -1; }
/* The sentence above the ledger keeps a reading measure even though its
   container does not. ~68ch is the page's body measure at two columns. */
.gps-step-wide .gps-step-lead{ max-width:68ch; }

/* Two-up at the band's width: four rows become two, which is the whole reason
   the step went full width. The grid is row-flowed, so 1 and 2 sit side by
   side, then 3 and 4 — pairing controls with controls and content with
   content, which is the order the rows were already in. */
/* ⚑ A Row-4 restructure lived here for part of 2026-08-16 (Step 5's title
   and lead in Row 4's left column under Step 4, the POI pair spanning two
   grid rows via `grid-template-rows:auto 1fr`) and was REVERTED the same
   evening at Daniel's call: it made Step 5 read as part of Step 4's visual
   and the ledger "pop up randomly". Step 5 is the header of its own ledger;
   do not split them again. */

@media (min-width: 861px){
  /* ⚑ PULLED UP 2026-08-16 at Daniel's ask ("bring up step five closer to
     step four"). The parent flex gap is 60px; -40px leaves 20px between the
     .gpr pair's caption and this step's title, the page's heading rhythm.
     Scoped to the two-col breakpoint because the distance being closed is
     created by the tall "now" pane in Step 4's right cell; stacked, the
     rows keep the standard 60. ⚑ This is the only spacing lever left: the
     white space under Step 4's prose is the pair's height minus the prose,
     and shrinking the pair is Daniel's sizing call, not a CSS one. */
  /* ⚑ -60 since the same night, at his "just 20 more pixels": the flex gap
     is fully cancelled, so Step 5's title now sits directly under the
     pair's caption line box (0px of gap; the caption's own line-height is
     the only air). This is the floor: past it the title overlaps the pair. */
  .gps-step-wide{ margin-top:-60px; }
  .gps-step-wide .gps-punch{
    display:grid; grid-template-columns:1fr 1fr;
    column-gap:var(--sp-4, 32px);
  }
  /* Each cell carries its own top rule instead of the list carrying one, or
     the single full-width rule would sit above only the left-hand row. */
  .gps-step-wide .gps-punch{ border-top:0; }
  .gps-step-wide .gps-punch > li{
    border-top:1px solid var(--border);
    padding:12px 0;
  }
}

/* ── Colour ─────────────────────────────────────────────────────────────────
   Added 2026-08-12 at Daniel's direction. Three deliberate constraints:

   1. ⚑ NO RED/GREEN PASS-FAIL CODING, and this is not a preference. This step
      criticises a course for carrying meaning in colour (SC 1.4.1 lives two
      steps away). A ledger that said "failure" in red and "fix" in green would
      encode its own state in hue alone, in the one section where a reader is
      primed to notice. Every row here reads identically in greyscale: the chip
      is a filled shape, the fix is marked by a rule and by weight.
   2. The palette is the tab's own crimson ramp, not a new one. crimson-1 fills
      the chip, crimson-4 draws the fix spine.
   3. Colour marks WHAT A THING IS, never what it scored. The chip says "this
      is a criterion"; the spine says "this line is the answer". */
.gps-punch-sc{
  display:flex; align-items:baseline; gap:.6em;
  margin-bottom:6px;
  font-family:var(--font-sans);
  font-size:.6875rem; letter-spacing:.01em;
  color:var(--text-secondary); font-weight:400;
}
/* The criterion number as a filled chip: the repeating crimson marks down the
   list are what turn four paragraphs into four things you can count. */
.gps-punch-sc > .gps-punch-id{
  flex:none;
  background:var(--study-1); color:var(--text-bright, #eeedeb);
  font-weight:700; letter-spacing:.06em;
  padding:3px 7px; border-radius:3px;
}
.gps-punch-was{
  display:block;
  font-size:.9375rem; line-height:1.5; color:var(--text-secondary);
  text-wrap:pretty;
}
/* The fix carries a crimson spine so the answers are findable down the left
   edge without reading a word. Rule + darker ink + indent, so it survives
   greyscale and a colour-blind reader alike. */
.gps-punch-fix{
  display:block; margin-top:7px;
  padding-left:11px; border-left:2px solid var(--crimson-4);
  font-size:.9375rem; line-height:1.5; color:var(--text-primary);
  text-wrap:pretty;
}


/* ── .gps-punch.is-rail — the ledger in the .ia-tier language ───────────────
   Built 2026-08-12 so Daniel could compare. The chip treatment was invented for
   this step; this one is not invented at all, it is the grammar the IA diagram
   in website-redesign already uses and ships: a right-aligned rail carrying a
   mono number over an uppercase mono label, a coloured spine as the connector,
   and the content to the right of it.
   Same tokens as .ia (--study-2 for the number, --text-muted for the label,
   --study-3 for the spine), so the two read as one system across two studies
   rather than as two one-offs.
   ⚑ The greyscale rule still holds: the spine and the rail mark WHAT a row is,
   never how it scored, and every row is identical in hue. */
.gps-punch.is-rail{ border-top:0; }
.gps-punch.is-rail > li{
  display:grid; grid-template-columns:104px 1fr; gap:0 18px;
  border-top:0; border-bottom:0; padding:0 0 20px;
  /* ⚑ Without this the two cells in a two-up grid row stretch to equal height,
     and the taller one pushes the shorter one's FIX line far below its own
     failure line, so the pairing the ledger exists to show comes apart. Rows
     size to content; the spine ends where the row ends. */
  align-content:start;
}
.gps-punch.is-rail .gps-punch-sc{
  grid-column:1; grid-row:1 / span 2;
  display:flex; flex-direction:column; align-items:flex-end; gap:2px;
  padding-top:1px; text-align:right; margin:0;
}
.gps-punch.is-rail .gps-punch-id{
  background:none; padding:0; border-radius:0;
  font-family:var(--font-mono); font-size:.85rem; font-weight:700;
  letter-spacing:.02em; color:var(--study-2);
}
.gps-punch.is-rail .gps-punch-name{
  font-family:var(--font-mono); font-size:.66rem;
  letter-spacing:.01em; line-height:1.45; color:var(--text-muted); margin:0;
}
/* The spine is one continuous line down both rows because they are adjacent
   grid rows with no gap: the border IS the connector, exactly as in .ia-nodes. */
.gps-punch.is-rail .gps-punch-was{
  grid-column:2; grid-row:1;
  padding:0 0 6px 20px; border-left:2px solid var(--study-3);
}
.gps-punch.is-rail .gps-punch-fix{
  grid-column:2; grid-row:2;
  margin:0; padding:0 0 2px 20px;
  border-left:2px solid var(--study-3);
  color:var(--text-primary); font-weight:600;
}

/* ══════════════════════════════════════════════════════════════════════════
   KIOSK SOLUTION FIGURES  (.ksf-*)                          added 2026-08-29
   Three figures in the Solution's right track: the loans before/after at
   Decision 2 (.ksf-screens), the two shortlisted keypads at Decision 3
   (.ksf-keys), and the icon lineage plus the shared-mark block at Decision 4
   (.ksf-lineage, .ksf-shared).
   ⚑ NO FRAME on any of it. Standing rule: visual support sits bare on the
     page, no card, border or background behind it. Set on the research study
     ("a frame around a frame around a label was three levels of container for
     one line of text") and generalised site-wide. Do NOT wrap these in
     .study-visual. The grounds INSIDE the icons are the buttons they sit on,
     which is evidence, not a frame.
   ══════════════════════════════════════════════════════════════════════════ */
.ksf{ margin:0; width:100%; display:flex; flex-direction:column; gap:14px; }
/* ⚑ width:100% is load-bearing. .study-col is align-items:flex-start, so without
   it a figure shrinks to its content and sits flush left with dead space on the
   right — the keypad pair rendered 428px wide in a 576px column, 148px of it
   empty and all of it on one side. With the figure full width the flex row can centre the
   pair inside the track instead of leaving it flush left.
   ⚑ This said "1fr 1fr grid" until 2026-08-30 (pass 19, finding 4). It is flex,
   and has been since the gap fix twenty lines below, which exists specifically to
   warn against the grid. File order handed an editor the dead mechanism first.
   WHEN A MECHANISM CHANGES, ITS JUSTIFICATION GOES WITH IT. */

/* ⚑⚑ TOMBSTONE: .ksf-title is DELETED (Daniel, 2026-08-29). "Those titles are not
   needed at all." It was added at pass 11 to title all three Solution figures and
   removed with them; zero occurrences remain in the HTML. Do not re-add it without
   him asking. ⚑ What it cost and how that was covered is recorded on the loans
   figure in banking-products.html: the pane kickers now read "Personal Loans,
   Before" and "Personal Loans, After", because with bare Before/After a measured
   reader defect returns.
   ⚑ The 11px rule it was folded into at pass 13 still governs everything below:
   a label in open page space is 0.6875rem, and .tpd-kicker renders at exactly that
   twice on this same tab. Do not put a 13px label back into this block.
   ⚑⚑ THE FULL PASS-11 CASE *FOR* THE TITLES USED TO SIT DIRECTLY ABOVE THIS
   TOMBSTONE, IN THE PRESENT TENSE, and is deleted with it (pass 16, finding 2). It
   argued that every other figure carries a title and that a reader met the D2 pair
   with nothing but Before and After. A future editor read the case for the titles
   before reading that they were gone, which is the one argument this block must not
   act on. It was stale on its own terms too: pass 13 made title and kicker the same
   SIZE and carried the two levels by weight and tint, so "one step darker ... so the
   two levels read as a hierarchy" had stopped describing anything.
   ⚑ Same species as the .ksf-states comment (pass 14) and the `Now "..."` guard
   (pass 15): when a thing is deleted, its justification goes with it. */

/* Label above, never below. Same reason .tpd-kicker gives: two things side by
   side mean nothing until each is named, and the name has to arrive first.
   ⚑ Title Case at .01em. Not uppercase, not open tracking. */
.ksf-kicker{ margin:0; font-family:var(--font-mono); font-size:0.6875rem;
  letter-spacing:.01em; color:var(--text-muted); line-height:1.3; text-align:center; }

/* ⚑ FLEX, not a 1fr 1fr grid. With even halves the keypads centre inside their own
   cells, so the space a reader SEES between them is the 36px gap plus 37px of
   centring slack on each side: 110px, not 36. Flex at a fixed column width makes
   the gap the real gap, so halving it is one number instead of three. */
/* ⚑⚑⚑ BOTTOM-ALIGNED, 2026-08-31 (Solution pass 28, the pass's only finding).
   ⚑ CENTRING WAS TESTED AND FAILS. Pass 29 measured it: centred, D3's tie falls
   OUTSIDE the figure at 861 (62px past), 900 (41), 1000 (9) and 1050 (7). Bottom is
   the only alignment that holds at every two-column width. Do not revisit it.
   ⚑ CORRECTION, pass 29: the numbers below are the >=1512 case and this guard used
   to state them as if they were the case. The empty column above the figure actually
   runs 620px at 861, 578 at 900, 457 at 1000, 358 at 1050, 330 at 1100, 274 at 1280
   and 218 from 1512 up - at the tightest two-column width it is 2.8x what was
   recorded here. Judged and KEPT anyway: rendered, the keypads enter the right column
   at the sentence where the keypad enters the argument, so it reads as staging rather
   than as a hole, and the void sits beside the design-system half of D3, which has no
   figure of its own by design. ⚑ MEASURE BEFORE QUOTING A GAP: one width is not the
   figure.
   .ksf-keys is 405px in a 623px column at 1512, and the global
   `.study-solution .study-two-col.is-center{ align-items:stretch }` above (added for
   website-redesign's sticky stack, which reaches this page too) left it TOP-aligned.
   So the keypad figure sat beside the design-system half of D3 while the keypad half
   of the prose ran past its bottom edge, and D3's .study-seealso arrow drew in the
   empty right column BELOW the figure it points at: measured 73px past it at 2560 and
   1512, 102px at 1280, 257px at 1000. That is the exact failure rule 1 of the seealso
   pattern was written from, and D2's and D3's arrows both land inside their figures.
   ⛑ D4's TIE WAS DROPPED 2026-08-31 and must not be restored. This line used to say
   "D2's and D4's", a live present-tense claim that a tie existed where one had been
   deliberately removed, sitting in the exact block an editor reads before touching tie
   geometry - the shape that invites someone to put it back. The rule: a tie points at a
   SINGLE ARTIFACT, never at a multi-row figure. D4's target is a four-row lineage table and
   its arrow drew level with the Automobile row while the clause is about ReLi.
   ⚑ SCOPED TO .ksf-keys ON PURPOSE. It is Kiosk-only (2 occurrences, this page), so
   this does NOT touch the shared is-center rule or website-redesign's sticky stack.
   Do not "simplify" it up to .ksf or .study-col.
   ⚑ Verified after: the tie lands inside the figure at 2560, 1280 and 1000, and so do
   all three other candidate sentences in the passage. Inert when the columns stack,
   because the column is then the figure's own height and there is no slack to absorb.
   ⚑ Must sit AFTER `.ksf{ margin:0 }`: same specificity, so source order decides. */
.ksf-keys{ margin-top:auto; }
/* ⚑⚑ .ksf-lineage JOINS IT 2026-08-31, for the same reason and after Daniel
   accepted the mechanism on the keypads: "I actually understand now why the
   keypads were anchored there so they could be there with the arrow."
   Decision 4 got a .study-seealso tie the same day. Its target is .ksf-shared,
   the two buttons wearing one mark, which sits at the BOTTOM of this figure,
   and the tied clause sits late in D4's Why. Top-aligned, the arrow drew
   inside .ksf-shared at 1600 but fell 136px past the whole figure at 1280 and
   220px at 1000. Anchored, the figure's bottom meets the column's bottom and
   .ksf-shared arrives where the arrow already is.
   ⚑ THIS IS NOT THE 2026-08-31 MEASUREMENT THAT SAID BOTTOM-ALIGNING THIS
   FIGURE "BUYS ONLY 110px". That one was about reaching the ReLi ROW inside
   the lineage table, which anchoring genuinely cannot do - it moved the arrow
   from Automobile to Mortgage and no further. The tie that shipped points at
   .ksf-shared instead, BELOW every row, so the same 110px is enough. The old
   note is about a different target, not a contradiction. */
.ksf-lineage{ margin-top:auto; }

.ksf-kps{ display:flex; justify-content:center; align-items:flex-start; gap:55px; }
.ksf-kp-col{ display:flex; flex-direction:column; align-items:center; gap:11px; width:196px; }
.ksf-kp{ width:100%; height:auto; max-width:196px; display:block; }

/* The two ratios replace a caption. As a data pair they compare down the
   column; as a sentence they had to be parsed. The bolded value is the one
   that wins that row, so the trade reads without prose. */
.ksf-data{ margin:0; width:100%; max-width:196px; display:flex; flex-direction:column; gap:4px; }
.ksf-data div{ display:flex; justify-content:space-between; align-items:baseline;
  gap:10px; border-top:1px solid var(--border); padding-top:5px; }
.ksf-data dt{ font-family:var(--font-sans); font-size:0.6875rem; color:var(--text-muted); }
.ksf-data dd{ margin:0; font-family:var(--font-mono); font-size:0.6875rem; color:var(--text-muted); }
.ksf-data dd.is-win{ color:var(--text-primary); font-weight:700; }

.ksf-icons{ display:flex; flex-direction:column; gap:4px; }
.ksf-ihead,
.ksf-irow{ display:grid; grid-template-columns:64px 30px 64px 1fr; gap:14px; align-items:center; }
.ksf-ihead{ padding:0 0 8px; border-bottom:1px solid var(--border); }
.ksf-ihead span{ font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-muted); }
.ksf-irow{ padding:9px 0; border-bottom:1px solid var(--border); }
.ksf-irow:last-child{ border-bottom:0; }
.ksf-arrow{ font-family:var(--font-mono); font-size:1rem; color:var(--text-muted); text-align:center; }
.ksf-iname{ font-family:var(--font-sans); font-size:.875rem; line-height:1.45; color:var(--text-secondary); }
.ksf-iname b{ display:block; color:var(--text-primary); font-weight:600; }

/* Each icon keeps the ground it actually has on the screen it came from. */
.ksf-ic{ width:64px; height:64px; border-radius:4px; display:flex;
  align-items:center; justify-content:center; overflow:hidden; }
.ksf-ic img{ display:block; }
.ksf-ic.is-old img{ width:64px; height:auto; }   /* charcoal baked into the crop */
.ksf-ic.is-new{ background:#AED581; }            /* the button it sits on */
.ksf-ic.is-new img{ width:40px; height:auto; }

/* ⚑ TOMBSTONE. .ksf-states, .ksf-ic.is-press and .ksf-note were deleted 2026-08-29
   (pass 14, finding 3). They styled the default/hover pair, which was moved to
   Decision 3 and REMOVED the same day (pass 12, finding 4), and the shared-mark
   caption, which was cut in the same pass. This block went on describing a
   two-state component "under Decision 3's keypads" for two passes after it stopped
   existing, which is the same defect as a stale prose claim, aimed at the next
   editor instead of at the reader. The SVGs are still on disk; the figure comment
   in banking-products.html says why the pair cannot come back without a second
   side to compare against. */

/* Screen pairs (Decision 2's loans before/after).
   Stacked, so each screen gets the full width of the track instead of half of it.
   ⚑ It ran at 474px for one day and is now the FULL 576px track. The two comments
   that used to disagree about this (474px "chosen, not default" above, "full track
   width now" below) have been reconciled into this one.
   ⚑ NO LIVE PERCENTAGE HERE ON PURPOSE. This comment carried one, it drifted, and
   so did the one on .ksf-shared: a balance figure goes stale on every prose edit and
   the stylesheet is the last place anyone thinks to update. The balance table lives
   in project_kiosk_solution.md, which is maintained every pass. Keep the reason
   here, keep the number there.
   ⚑ No border on the images. The images are already trimmed of their letterboxing
     (see the figure's own comment); do not add a frame to compensate. */
.ksf-pair{ display:flex; flex-direction:column; gap:14px; }
.ksf-pane{ display:flex; flex-direction:column; gap:6px; }
.ksf-screens .ksf-kicker{ text-align:left; }
.ksf-screens img{ display:block; width:100%; height:auto; }
/* The crop to the UI band is what paid for the full width: it made the figure
   SHORTER while making the interface bigger, so the width that used to cost
   overhang no longer does. */
@media (max-width:860px){ .ksf-screens img{ max-width:100%; } }

/* Decision 4's shared-mark block: the SAME mark on two differently named
   buttons, SIDE BY SIDE so the two marks land at the same y and the eye crosses
   from one to the other. Stacked full-width was tried first and made the figure
   overhang its prose badly; side by side is much shorter. Both crops are cut to the
   same box, which is the only thing proving the two products wear one mark.
   ⚑ This used to say it protected the word "identically" in the copy. That word left
   the prose in the 44-word D4 trim of 2026-08-29; the claim is carried now by the
   "One Mark, Two Products" subhead and by the crops themselves.
   ⚑ Under 860px .ksf-spair collapses to one column and they stack. */
.ksf-shared{ margin-top:14px; display:flex; flex-direction:column; gap:10px; }
.ksf-subhead{ margin:0 0 4px; padding:0 0 8px; border-bottom:1px solid var(--border);
  font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-muted); }
.ksf-spair{ display:grid; grid-template-columns:1fr 1fr; gap:14px; align-items:start; }
.ksf-shared img{ display:block; width:100%; height:auto; }

@media (max-width:860px){
  .ksf-spair{ grid-template-columns:1fr; }
  .ksf-kps{ gap:26px; }
  .ksf-ihead, .ksf-irow{ grid-template-columns:52px 22px 52px 1fr; gap:10px; }
  .ksf-ic{ width:52px; height:52px; }
  .ksf-ic.is-old img{ width:52px; }
  .ksf-ic.is-new img{ width:34px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   KIOSK PROCESS · STEP 1 TIER FIGURE  (.kpf-*)              added 2026-08-30
   The annotation layer for assets/kiosk/canvas-tiers.svg, which is now drawing
   only. Daniel picked this out of five candidates in
   sandbox/kiosk-process-figures.html.
   ⚑ WHY THE LABELS LIVE OUT HERE: inside the SVG they could not obey the 11px
     page-space label rule at the top of this file, and they scaled with the
     drawing, so they hit ~8px on a 386px phone. The previous fix pushed them to
     20-26px, which then outranked 16px body copy on desktop. There is no good
     answer to that trade while the type is inside the file. Do not put it back.
   ⚑ Positions are PERCENTAGES of the 576x330 viewBox and are set inline on each
     label, because they belong to the drawing's geometry rather than to a
     reusable class. The 1920 label sits on the midline of the band it owns
     (y30-75, so y52.5); all three share the left edge the rectangles are
     anchored to.
   ⚑ NO FRAME. Standing rule: visual support sits bare on the page. */
.kpf{ position:relative; width:100%; }
.kpf img{ display:block; width:100%; height:auto; }
.kpf-lab{ position:absolute; font-family:var(--font-mono); font-size:0.6875rem;
  letter-spacing:.01em; line-height:1.45; color:var(--text-muted); }
.kpf-lab b{ display:block; font-weight:500; color:var(--crimson, #ad2831); }
.kpf-lab.is-out b{ color:var(--text-muted); }

/* Under 700px the drawing is small enough that absolutely positioned 11px type
   would overrun its own band, so the labels un-position and stack beneath it in
   source order: outermost, approved, oldest. That is the order they read in
   anyway, so nothing is lost but the pointing. */
@media (max-width:700px){
  .kpf-lab{ position:static; display:block; width:auto !important;
    left:auto !important; top:auto !important; margin-top:10px; }
}

/* ══════════════════════════════════════════════════════════════════
   KIOSK PROCESS STEP 2 — the system-lineage figure, rebuilt as
   drawing + HTML annotation and SHIPPED 2026-08-30, replacing
   assets/kiosk/system-lineage.svg?v=10.

   ⚑ WHY IT CHANGED: the old figure carried its annotation INSIDE the
   SVG, so its labels rendered at 15-19px against 16px body copy, its
   WEBSITE / KIOSK eyebrows were uppercase against the site-wide rule,
   and the file had to carry an embedded base64 Roboto subset or fall
   back to Helvetica on anyone else's machine. All three go away when
   the type is page type. Measured: 807px to 500px.

   ⚑ THE LEFT COLUMN IS 372px BECAUSE THAT IS THE WEBSITE BUTTON'S REAL
   WIDTH. Both buttons carry intrinsic width/height and are capped with
   max-width, never stretched with width:100%. An earlier candidate used
   a 352px column with width:100% and was SHRINKING the website button
   to 0.946x while STRETCHING the kiosk button to 1.054x — a figure whose
   whole job is comparing two buttons, scaling them by different factors.
   The kiosk button now stops 38px short of the website button's right
   edge, which is the "38px narrower" fact drawn instead of stated.
   Do not set a width on these SVGs.
   ══════════════════════════════════════════════════════════════════ */
.kpe-v{ border-radius:4px; padding:7px 9px; display:block; }
.kpe-v code{ font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  display:block; }
.kpe-v span{ font-family:var(--font-sans); font-size:0.75rem; line-height:1.3;
  display:block; opacity:.8; }
.kpe-v.on-light{ color:#2A2420; }
.kpe-v.on-dark{ color:#FFFFFF; }
.kpe-v.is-white{ border:1px solid var(--border); }
/* the radius is not a colour, so it does not get a colour block */
.kpe-rad{ display:flex; gap:9px; align-items:center; margin-top:3px;
  font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-muted); }
.kpe-rad i{ width:15px; height:15px; border-radius:5px; display:block;
  border:1.5px solid var(--text-muted); }
/* ── Options E and E2: EVIDENCE left, a two-part LEDGER right. ⚑ The halves were
     first named after the shipped kicker ("what came across ... and what did not")
     and that framing failed on first read, because nothing in the second half was
     dropped: an icon was ADDED and the button was re-proportioned. They are now
     "What Carried Over" and "What Was Refit". Do not rename them back to the
     kicker's wording. ⚑ The left column is EVIDENCE (buttons adjacent, so the
     anatomy difference reads DOWN), the right is a LEDGER split in two. Same
     372px true-size column as B2: the website button's real width, SVGs capped
     with max-width and carrying intrinsic width/height, so the kiosk button
     stops 38px short and "38px narrower" is drawn rather than stated. */
.kpe{ display:grid; grid-template-columns:372px 1fr; gap:24px; align-items:start; }
.kpe-col{ display:flex; flex-direction:column; gap:20px; }
.kpe-grp{ display:flex; flex-direction:column; gap:6px; }
.kpe-name{ font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-muted); display:flex; justify-content:space-between; margin:0; }
.kpe-name b{ font-weight:500; color:var(--text-secondary); }
.kpe-st{ font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-muted); margin:0; }
.kpe svg{ display:block; max-width:100%; height:auto; }
.kpe-ledger{ display:flex; flex-direction:column; gap:20px; }
.kpe-set{ display:flex; flex-direction:column; gap:7px; }
.kpe-h{ font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-secondary); font-weight:500; margin:0 0 1px;
  border-bottom:1px solid var(--border); padding-bottom:5px; }
/* ⚑ A 3-column grid was tried first and wrapped every row to three lines: the
   ledger column is only 180px (576 track - 372 button column - 24 gap), so a
   fixed `1fr` value column left ~100px for the payoff. Flowing text uses the
   full width and wraps to two. */
.kpe-d{ font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-muted); margin:0; line-height:1.5; }
.kpe-d i{ font-style:normal; padding:0 3px; color:var(--text-muted); }   /* was #A9A29A, 2.16:1 on the cream; the same glyph in the D4 table (.ksf-arrow) sits at 4.91:1. Review 2026-09-10. */
.kpe-d b{ font-weight:500; color:var(--text-secondary); }
.kpe-fine{ font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-muted); margin:2px 0 0; line-height:1.4; }
/* ⚑ WITHOUT THIS THE ROWS DO NOT SAY WHICH SIDE IS WHICH. Daniel, 2026-08-30, on
   "one 24px label -> a 16px title and a 12px description": "I still don't know
   which one you're talking about." The arrow implied a direction that nothing
   stated. One line states it once for all three rows. */
/* ⚑ THE HEADER MUST CARRY THE SAME ENCODING AS THE ROWS, not its own. Daniel,
   2026-08-30: the direction line bolded BOTH sides while every row below it mutes
   the website side and bolds the kiosk side, so the legend contradicted the thing
   it was labelling. Now "Website" takes the rows' muted weight and "Kiosk" takes
   their emphasis, which makes this line a demonstration of the encoding rather
   than a second one. ⚑ These three declarations must stay identical to .kpe-d and
   .kpe-d b: if the row treatment changes, change this with it. */
.kpe-dir{ font-family:var(--font-mono); font-size:0.6875rem; letter-spacing:.01em;
  color:var(--text-muted); font-weight:400; margin:-1px 0 3px; }
.kpe-dir b{ font-weight:500; color:var(--text-secondary); }
.kpe-dir i{ font-style:normal; padding:0 3px; color:var(--text-muted); font-weight:400; }   /* same fix as .kpe-d i, 2026-09-10 */
/* ⚑ THE SWATCHES USED TO STRETCH THE FULL LEDGER WIDTH (170px), leaving a band of
   colour to the right of every label doing no work. Daniel boxed it 2026-08-30.
   ⚑⚑ MEASURED, NOT GUESSED: with width:fit-content the four labels come out 64 /
   87 / 82 / 140px. ⚑ Capping at exactly 140 made the longest one wrap: fit-content
   was measured on a content-box element, so 140 there meant 122 of text plus 18 of
   padding, and subpixel rounding then pushed it over. 144 gives it the 4px of
   headroom it needs. That is 26px of slack removed from every swatch.
   ⚑ box-sizing MATTERS HERE: .kpe-v is content-box, so a bare max-width:152 rendered
   at 170 once the 9px padding was added on each side. Setting border-box makes the
   number mean the width you see. */
.kpe-ledger .kpe-v, .kpe-ledger .kpe-rad{ box-sizing:border-box; max-width:144px; }
/* ⚑⚑⚑ RAISED 860 -> 1050 ON 2026-08-31, because the figure was OVERFLOWING THE PAGE
   between 861px and ~1010px. `grid-template-columns:372px 1fr` is a FIXED first
   column, so once the two-column page layout squeezed this figure's track under
   ~540px the ledger was crushed and its contents punched out past the viewport:
   measured documentElement.scrollWidth 936 at 861, 956 at 900, 1006 at 1000, clean
   from 1050. ⚑ The bug was invisible at every width anyone had probed: 386 (single
   column already), 1280 and up (track wide enough). It only lives in the band
   between. PROBE 861 AND ~1000 AFTER TOUCHING THIS FIGURE, not just 386 and desktop.
   ⚑ Found by the Solution pass-28 reviewer as an out-of-band note, in a band it was
   not grading. A finding outside the section is still a finding.
   ⚑⚑ max-width IS LOAD-BEARING AND IS NEW WITH THE RAISE. Below 860 the track is
   narrower than 372 so `width:100%` could only ever shrink the buttons. Between 861
   and 1050 the track runs 367-453px, so width:100% alone would have UPSCALED the
   website button to 1.16x its true size — breaking the "both buttons at true size"
   premise the whole figure rests on, which is the same class of defect as the two
   below. Capped at the real 372 and 334, the pair scales together AND never exceeds
   true size. Recompute all three numbers together if either button's size changes.

   ⚑⚑ THE NARROW BREAKPOINT ONCE RE-INTRODUCED THE EXACT DEFECT THIS FIGURE EXISTS
   TO AVOID. `.kpe svg{ width:100% }` in a single column made BOTH buttons 342px, so
   the website button (372 viewBox) was shrunk to 0.919x while the kiosk button (334)
   was stretched to 1.024x - the two things the figure compares, scaled by different
   factors, which is the Option B bug caught on 2026-08-30 reappearing at mobile
   width. Caught in a 386px probe on the day this shipped.
   ⚑ The kiosk button is pinned to 334/372 = 89.78% of the column so the pair scales
   TOGETHER and the "38px narrower" relationship survives at every width. If either
   button's real size ever changes, recompute this percentage; do not eyeball it. */
@media (max-width:1050px){
  .kpe{ grid-template-columns:1fr; }
  .kpe svg{ width:100%; max-width:372px; }
  .kpe .kpe-grp.is-kiosk svg{ width:89.78%; max-width:334px; }
}

/* ═══════════════════════════════════════════════════════════════════
   KIOSK IMPACT METRIC  (.kim-*)                        added 2026-09-01
   The one measured outcome on the banking-products page, promoted out of
   prose to skim altitude on Daniel's call.
   ⚑ BARE ON PURPOSE: no card, no border, no background. The standing
   no-frames-on-visuals rule applies, and this is the treatment he chose
   from three (sandbox/kiosk-impact-metric.html). The .study-aside option
   was louder; reusing .study-metrics was rejected because the hero cards
   are a skim surface and their labels wrap to four lines at column width.
   ⚑ THE VALUE FACE IS THE HERO METRIC FACE (Roboto Slab 700 / 24px) so it
   reads as the same species of fact, at body scale in the flow rather than
   in a card.
   ⚑ "to", NOT AN ARROW GLYPH: a literal arrow in .study-metric-value is
   read aloud by screen readers, which is already flagged on the research
   card. The word carries the same meaning and reads correctly.
   ⚑ THE LABEL IS 11px per the standing page-space label size, and it is
   LOAD-BEARING, not a caption: it carries the window asymmetry (12 months
   against 8) that stops the figures overclaiming. Never trim it to fit.
   ═══════════════════════════════════════════════════════════════════ */
.kim{ margin:22px 0 26px; }
.kim-row{ display:flex; align-items:baseline; gap:14px; flex-wrap:wrap; }
.kim-v{
  font-family:var(--font-mono);
  font-weight:700;
  font-size:1.5rem;
  line-height:1.1;
  color:var(--text-primary);
}
.kim-to{
  font-family:var(--font-mono);
  font-weight:300;
  font-size:1.125rem;
  color:var(--text-muted);
}
.kim-l{
  font-family:var(--font-mono);
  font-weight:300;
  font-size:0.6875rem;
  line-height:1.45;
  letter-spacing:.01em;
  color:var(--text-secondary);
  margin:10px 0 0;
  max-width:46ch;
}

/* Keeps a hyphenated compound from being severed at its hyphen. Added 2026-09-01
   for "bank-wide)" in the website-redesign hero, which rendered as "bank-" / "wide)"
   at 393-430px, i.e. across most of the phone fleet. Generic on purpose: any label
   holding a compound the reader must take as one token can use it. */
.nb { white-space: nowrap; }

/* ══════════════════════════════════════════════════════════════
   THE CLOSER (2026-09-09): the vinyl button as "Next Case Study"
   ══════════════════════════════════════════════════════════════
   Replaces the hairline .study-cta that ended Learnings on five studies
   (T&P, Banking GPS, Kiosk, Website Redesign, UX Research; AI Products
   is the end of the chain and has none). It lives in .study-coda, a plain
   block AFTER the Learnings .study-band, so it sits UNDER the ribbon's
   final run: js/ribbon.js draws that run along the LAST .study-band's
   bottom edge, so a closer inside the band would sit above the ribbon,
   and a coda that was itself a .study-band would get wrapped by it.
   The component (.about-btn / .rec / .lbl) is in styles.css; this is the
   wide variant. Face and edge in the NEXT study's colour (the last thing
   on a rust page is a gold button naming where you go). The record is
   assets/images/vinyl-dg-closer.svg: the About record with its label and
   logo scaled by 330/740 so they hold About's on-screen size inside a
   740px box, and no spindle dot. Centre 14px inside the left edge, so the
   grooves reach 384px (widest label ~322) and the whole DG shows; the
   cream reaches 48px, so the label steps 22px (text starts at 66) and the
   side padding is 42, not 36. The record rests ENTIRELY outside the
   button (translate -390, clipped by the button's clip-path) and slides
   to its seat in 800ms on a soft ease-out; on leave it slides back out.
   Nothing fades. One turn in 6.4s, half About's 3.2. Colours: cream on
   crimson-3 and teal-3 passes AA; gold-3 keeps ink letters at rest (the
   label goes cream once the vinyl is under it); rust uses rust-2 because
   cream on rust-3 fails. History: sandbox/closer-vinyl/index.html (A6;
   the sleeve card, the grow, the open centre and the punched spindle hole
   were all built and killed). */
.study-coda { position: relative; z-index: 1; margin-top: 40px; }
.study-coda .study { display: flex; justify-content: flex-end; }
.about-btn.is-next { --face: var(--next); --edge: var(--next); color: var(--bg); padding: 14px 42px; }
.about-btn.is-next.to-crimson { --next: var(--crimson-3); }
.about-btn.is-next.to-rust    { --next: var(--rust-2); }
.about-btn.is-next.to-gold    { --next: var(--gold-3); color: var(--text-primary); }
.about-btn.is-next.to-teal    { --next: var(--teal-3); }
.about-btn.is-next:focus-visible { outline-color: var(--next); }
.about-btn.is-next .rec,
.about-btn.is-next::after {
  width: 740px; height: 740px;
  left: -356px; margin-top: -370px;
  translate: -390px 0;
  transition: translate .8s cubic-bezier(.3, .85, .3, 1), opacity .25s var(--ease-out);
}
.about-btn.is-next .rec { opacity: 1; }   /* the clip hides it at rest; the slide is the reveal */
.about-btn.is-next:hover .rec, .about-btn.is-next:focus-visible .rec, .about-btn.is-next:active .rec,
.about-btn.is-next:hover::after, .about-btn.is-next:focus-visible::after, .about-btn.is-next:active::after { translate: 0 0; }
.about-btn.is-next:hover .rec, .about-btn.is-next:focus-visible .rec, .about-btn.is-next:active .rec { animation-duration: 6.4s; }
.about-btn.is-next:hover .lbl, .about-btn.is-next:focus-visible .lbl, .about-btn.is-next:active .lbl { transform: translateX(22px); }
@media (prefers-reduced-motion: reduce) {
  .about-btn.is-next .rec, .about-btn.is-next::after { transition: none; }
}

/* ══════════════════════════════════════════════════════════════
   THE DOCK (2026-09-10): the mid-scroll study switcher, js/dock.js
   ══════════════════════════════════════════════════════════════
   Option D from sandbox/study-switcher.html, gated at Process (see
   js/dock.js for the rules). One floating object per page, bottom-left.
   body.sw-on       past the active study's Process band (or the tab strip
                    on a page with no Process band yet)
   body.sw-near-cta the study's closer (.study-coda) is in view
   body.sw-down     (touch widths) the reader is scrolling down
   .sw-dock.has-bot the chatbot avatar's reserved slot is rendered
                    (?dock=bot, HELD until there is a bot)
   Proof sheet: sandbox/dock/index.html. */

.sw-dock {
  position: fixed;
  left: 24px;
  bottom: 24px;
  z-index: 95;
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  opacity: 0; visibility: hidden; transform: translateY(6px);
  transition: opacity .22s ease, transform .26s var(--ease-out, cubic-bezier(.2,.8,.3,1)), visibility .22s ease;
}
body.sw-on .sw-dock { opacity: 1; visibility: visible; transform: none; }
body.sw-on.sw-near-cta .sw-dock,
body.sw-on.sw-down .sw-dock { opacity: 0; visibility: hidden; transform: translateY(6px); }

/* the study pill */
.sw-float { position: relative; }
.sw-float-btn {
  display: inline-flex; align-items: center; gap: 9px;
  height: 36px; padding: 0 16px;
  border: 1px solid var(--study-1); border-radius: var(--radius-pill, 999px);
  background: var(--study-1); color: var(--bg);
  font-family: inherit; font-weight: 700; font-size: .6875rem; letter-spacing: 1.3px;
  cursor: pointer; white-space: nowrap;
  box-shadow: 0 8px 32px rgba(42,36,32,.10);
  transition: box-shadow .2s ease;
}
.sw-float-btn:hover { box-shadow: 0 10px 34px rgba(42,36,32,.22), 0 3px 10px rgba(42,36,32,.14); }
.sw-float-btn:focus-visible { outline: 2px solid var(--study-3); outline-offset: 3px; }
.sw-float-chev { width: 10px; height: 10px; flex-shrink: 0; transition: transform .22s var(--ease-out, cubic-bezier(.2,.8,.3,1)); }
.sw-float[data-open="true"] .sw-float-chev { transform: rotate(180deg); }
.sw-float-menu {
  position: absolute; left: 0; bottom: calc(100% + 8px); min-width: 100%;
  display: flex; flex-direction: column; gap: 4px; padding: 5px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 9px;
  box-shadow: 0 8px 32px rgba(42,36,32,.10);
  opacity: 0; visibility: hidden; transform: translateY(4px);
  transition: opacity .18s ease, transform .22s var(--ease-out, cubic-bezier(.2,.8,.3,1)), visibility .18s ease;
}
.sw-float[data-open="true"] .sw-float-menu { opacity: 1; visibility: visible; transform: none; }
.sw-pill {
  display: inline-flex; align-items: center; gap: 8px; padding: 7px 15px;
  border: 1px solid transparent; border-radius: 6px; background: transparent; cursor: pointer;
  font-family: var(--font-mono); font-weight: 300; font-size: .6875rem; letter-spacing: 1.3px;
  color: var(--text-muted, var(--text-secondary)); white-space: nowrap; justify-content: flex-start;
}
.sw-pill:hover { color: var(--text-primary); background: var(--surface); }
.sw-pill[aria-selected="true"] { border-color: var(--study-1); color: var(--text-primary); }

/* the avatar: the chatbot's launcher, quiet at rest, reserved slot */
.sw-avatar {
  width: 44px; height: 44px; flex-shrink: 0; padding: 0;
  border: 2px solid var(--study-1); border-radius: 50%;
  background: var(--bg) url("../../assets/images/little-daniels/daniel-1.png") 42% 4% / 190% no-repeat;
  box-shadow: 0 8px 32px rgba(42,36,32,.10);
  cursor: pointer;
}
.sw-avatar:focus-visible { outline: 2px solid var(--study-3); outline-offset: 3px; }

/* touch widths: one grouped pill, 44px tall, above the home indicator */
@media (max-width: 1100px) {
  .sw-dock { left: 16px; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); gap: 0; }
  .sw-float-btn { height: 44px; padding: 0 18px; }
  .sw-dock.has-bot { background: var(--study-1); border-radius: 999px; padding: 3px; box-shadow: 0 8px 32px rgba(42,36,32,.10); }
  .sw-dock.has-bot .sw-avatar { width: 38px; height: 38px; border-color: var(--bg); box-shadow: none; }
  .sw-dock.has-bot .sw-float-btn { height: 38px; padding: 0 16px 0 12px; box-shadow: none; }
}
@media (prefers-reduced-motion: reduce) {
  .sw-dock, .sw-float-menu, .sw-float-chev { transition: none; }
}
/* the breakpoint sheet holds the dock visible inside scaled iframes, which
   Chrome render-throttles so transitions never start; drop them there */
body.sw-forced .sw-dock { transition: none; }
/* desktop: on the left-wrapped bands (Problem, Solution, Learnings) the
   ribbon's vertical run sits at --page-guide (64px at 1280, 72px at 1440),
   which the 24px-in pill crosses. The site's own precedent is the study
   tabs riding the hero ribbon, so the dock rides it too, with a page-colour
   halo so the crossing reads as layering, not collision. */
@media (min-width: 1101px) {
  .sw-float-btn, .sw-avatar { box-shadow: 0 0 0 3px var(--bg), 0 8px 32px rgba(42,36,32,.10); }
}
