/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   THE FOUR-HORIZON SHELL.   Contract: docs/ia/shell.md · design-system-v2.md

   ⛔ NO NEW TOKENS. design-system-v2.md rule 4: no kit introduces a colour or a typeface that is not
   already one. Every value below resolves to a :root token in style.css, and the two literals that
   are not tokens are the bar's translucency (white at 95 percent) and its blur, both of which the
   spec states as numbers rather than as colours.
   ⛔ REVIEWED AT 440 x 956. Hit targets never below 44px, no body text below 12.5px, and the bar
   sits above the safe-area inset. All three are acceptance items, and all three are held here.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */

/* ── Act-as ribbon: ABOVE the header, so it cannot be scrolled past ────────────────────────────
   ⚠ IT IS IN FLOW NOW, NOT FIXED (#449 `5c`). The shell is a flex column, so a `flex: none` band at
   the top of it is above the header by construction rather than by a z-index and a `top` offset the
   header then had to dodge. `body.ia-acting .ia-header { top: auto }` retired with that dodge. */
.ia-ribbon {
  flex: none;
  background: var(--accent-dkr); color: #fff;
}
.ia-ribbon-inner {
  padding: 6px 16px;
  font-size: 0.78125rem; font-weight: 600; text-align: center; line-height: 1.35;
}

/* ── The per-screen header ───────────────────────────────────────────────────────────────────────
   ⛔ HIS Q1 ANSWER, 2026-08-28: *"Each screen gets its own header. The fixed bar goes. Back and the
   profile button move into each screen's header."* So this is no longer a 56px bar that every screen
   scrolls under; it is a full-bleed block that BELONGS to the screen under it, carrying that screen's
   eyebrow, its title and a line underneath. `docs/plans/plans-of-record/PROTOTYPE-CONFORMANCE.md` G1
   is the row it closes, and G3's eyebrow exists at all because this gave it somewhere to live.

   ⛔ IT STILL NEVER SCROLLS AWAY (shell.md rule 3, unchanged). `flex: none` above a `flex: 1` column
   does what `position: fixed` did, without the body having to pad for a height nothing measured.
   ⚠ AND THAT IS WHY THE PADDING MOVED OFF `.ia-body`: a fixed header's height had to be repeated as
   a body padding, in two places, at two widths. In flow it is repeated nowhere. */
.ia-header {
  flex: none;
  padding: 14px 20px 20px;
  background: linear-gradient(135deg, var(--navy) 0%, var(--navy-mid) 100%);
  color: #fff;
}

/* The top row: what the screen is (or the way back) on the left, the athlete's own controls right.
   ⛔ 34px IS THE DESIGN'S CIRCLE AND 44px IS THE ACCEPTANCE FLOOR, so the row is sized by the floor
   and the circle is painted inside it (see .ia-avatar). Shrinking the button to 34 would fail
   shell.md's own acceptance list, which is not a fidelity question. */
.ia-shead-top {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  min-height: 44px;
}
.ia-shead-lead { flex: 1 1 auto; min-width: 0; }
.ia-shead-tools { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }

/* ── The screen's own identity ───────────────────────────────────────────────────────────────────
   Eyebrow, then a title or a big mono number, then a line underneath. A surface fills these through
   IA.setHead; a surface that has not registered one yet still gets its title, so every screen
   announces itself from the first commit rather than after thirteen of them. */
.ia-eyebrow {
  /* ⛔ 12.5, NOT THE DESIGN'S 12, AND THAT IS HIS Q2 ANSWER BOUNDING HIS Q1 ANSWER. The design draws
     the eyebrow at 12px; "set the floor" put the floor at 12.5, and shell.md's own acceptance list
     has said "no body text below 12.5px" since the shell landed. Half a pixel is invisible at this
     tracking and a 415th declaration under the floor is not. */
  font-size: 0.78125rem; font-weight: 700; letter-spacing: .10em; text-transform: uppercase;
  line-height: 1.3; color: var(--head-eyebrow);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ia-shead-title {
  font-family: var(--font-display);
  font-size: 1.375rem; font-weight: 800; letter-spacing: -.3px; line-height: 1.2;
  margin-top: 9px;
}
.ia-shead-sub {
  font-size: 0.84375rem; font-weight: 500; line-height: 1.5;
  color: rgba(255,255,255,.74);
  margin-top: 8px;
}
/* ── A TITLE WITH A GLYPH BESIDE IT (#652, and Admin is its first and only wearer) ────────────────
   The design's `admin` artboard puts a shield beside the word. Both numbers are the prototype's own,
   read off its markup rather than chosen here: the glyph is 19px and the gap is 9px. It declares no
   size and no colour of its own — the glyph strokes in `currentColor`, which is the header's white,
   so nothing here can drift from the title it sits beside.
   ⛔ A KIND ARRIVES WITH ITS FIRST WEARER, which is this file's standing rule: a rule with nothing
   wearing it is a rule that styles nothing, and `tests/dead-css.js` fails on exactly that. */
.ia-shead-title--glyph { display: flex; align-items: center; gap: 9px; }
.ia-shead-glyph { flex: 0 0 auto; display: block; width: 19px; height: 19px; }
.ia-shead-glyph svg { width: 100%; height: 100%; display: block; }

/* The eyebrow leads a pushed screen from BELOW the back control rather than beside it, because the
   back control has already taken the top row. At a horizon root it IS the top row. */
.ia-shead-body > .ia-eyebrow { margin-top: 12px; }
.ia-shead-body > .ia-eyebrow + .ia-shead-title { margin-top: 8px; }

/* ── The Profile screen's identity block (#449 phase 7 `7f`, conformance row 5) ──────────────────
   settings-and-account.md's Structure line: "Header: avatar, name, tier and renewal, Manage." It
   fills the head slot in place of the title, so the sizes here are the head's own: the circle is the
   prototype's 46, the name is `.ia-shead-title`'s weight and family one step down, and the sub is
   `.ia-shead-sub`'s colour so a renewal date reads the same as any other head subline.
   ⛔ THE PILL IS 44px TALL THOUGH IT LOOKS SMALLER. The prototype's is 8px of padding on a 12.5px
   line, which measures 33 and misses shell.md's own acceptance floor; the padding is the drawing's
   and the height floor is the acceptance's, so the two are reconciled by a min-height rather than by
   picking one. That is the same reconciliation `.ia-shead-top` records for the 34px avatar. */
.ia-ident { display: flex; align-items: center; gap: 13px; margin-top: 14px; }
.ia-ident-mark {
  flex: 0 0 auto; width: 46px; height: 46px; border-radius: 50%;
  background: rgba(255,255,255,.16); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 1rem; font-weight: 700; letter-spacing: .01em;
}
.ia-ident-who { flex: 1 1 auto; min-width: 0; }
.ia-ident-name {
  font-family: var(--font-display);
  font-size: 1.1875rem; font-weight: 800; letter-spacing: -.3px; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ia-ident-sub {
  font-size: 0.78125rem; font-weight: 500; line-height: 1.4; margin-top: 3px;
  color: rgba(255,255,255,.74);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ia-ident-manage {
  flex: 0 0 auto; min-height: 44px; padding: 8px 14px;
  background: rgba(255,255,255,.16); border: 0; border-radius: 20px; cursor: pointer;
  color: #fff; font-family: var(--font-body); font-size: 0.78125rem; font-weight: 700;
}
.ia-ident-manage:active { opacity: .8; }

/* Race centres its countdown, and since #538 it centres the race name on the top row above it.
   ⚠ THE SURFACE, NOT THE HORIZON. `.ia-header--race` is the horizon, and Your races, Race week,
   Summit and Goal pace all sit inside it; centring on the horizon centred four pushed screens whose
   design does not centre anything. The countdown is the ROOT's.
   ✎ THIS RULE ONCE SAID RACE CARRIES NO EYEBROW. It carries one — the race name and its star — and
   until #538 that line sat in the identity block BELOW the avatar's row, which is what made this
   header 219px against the artboard's 187px.

   ⛔ THE TOOLS COME OUT OF FLOW HERE AND ONLY HERE, AND THAT IS THE ARTBOARD'S OWN ARRANGEMENT. Its
   race header positions the profile button absolutely at the top right and centres the race name
   across the FULL 440, so the name's centre is the screen's centre. Left in the flex row the lead
   would centre over the width the tools leave, putting the name off centre by half the avatar.
   ⚠ THE ROW STILL RESERVES ITS HEIGHT, because `.ia-shead-top`'s `min-height: 44px` is what holds it
   open once its only in-flow child is a 16px line. Take that away and the avatar overhangs the name.
   ⚠ AND THE SYNC PILL RIDES WITH THE AVATAR rather than being hidden: shell.md's carried ruling is
   that no width and no screen may hide it, and it is in the same tools box, still on screen. */
.ia-header[data-ia-surface="race"] .ia-shead-body { text-align: center; }
.ia-header[data-ia-surface="race"] .ia-shead-top { position: relative; }
.ia-header[data-ia-surface="race"] .ia-shead-lead { text-align: center; }
.ia-header[data-ia-surface="race"] .ia-shead-tools {
  position: absolute; right: 0; top: 0; height: 100%;
}

/* ── Back control: the title of the screen beneath, never a generic Back ─────────────────────────
   ⛔ 44px MINIMUM, and the chevron is decorative, so the TITLE is the label a screen reader reads. */
.ia-back {
  display: inline-flex; align-items: center; gap: 4px;
  min-height: 44px; min-width: 44px; padding: 0 10px 0 0;
  background: none; border: 0; cursor: pointer;
  color: rgba(255,255,255,.78); font: inherit; font-size: 0.8125rem; font-weight: 600;
  max-width: 100%;
}
.ia-back-chev { display: block; width: 15px; height: 15px; flex: 0 0 auto; }
.ia-back-chev svg { width: 100%; height: 100%; display: block; }
.ia-back-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ia-back:active { opacity: .7; }

/* ── Avatar: opens Profile, and is the only door to it ──────────────────────────────────────────
   ⛔ THE BUTTON IS THE TAP TARGET AND THE SPAN IS THE CIRCLE. They are different sizes on purpose:
   44px is shell.md's acceptance floor, 34px is the design's circle, and painting the button itself
   would have made one of those two wrong. */
.ia-avatar {
  flex: 0 0 auto;
  width: 44px; height: 44px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; cursor: pointer;
  color: #fff; font: inherit;
}
.ia-avatar-mark {
  width: 34px; height: 34px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.28);
  font-size: 0.8125rem; font-weight: 700; letter-spacing: .02em;
}
.ia-avatar:active .ia-avatar-mark { background: rgba(255,255,255,.26); }

/* ── The content column: scrolls beneath the header, above the bar ───────────────────────────────
   Cards are 14 to 16px radius, 14px apart, 16px padding (shell.md, Layout). Those three numbers are
   the card rhythm and live on .ia-card so every surface inherits them rather than restating them. */
.ia-body {
  flex: 1 1 auto; min-height: 0;
  /* ⛔ PROD-0914/sideways-drag — `overflow-x: hidden` IS LOAD-BEARING AND MUST NOT BE DROPPED. Setting ONE axis to a
     non-`visible` value computes the OTHER to `auto`, so `overflow-y: auto` alone made this column a
     HORIZONTALLY scrollable box. Every other surface in the app is a vertical list, so the moment any
     descendant exceeded the column by a hair the whole screen could be dragged sideways and
     rubber-banded far past the content — which is what he reported on This week, where the overflow
     measured TWO pixels (`.day-list-note`'s 2px side margins against the global `width: 100%`,
     corrected in style.css in the same commit). ⚠ THE OVERFLOW IS NEVER THE POINT: two pixels of
     scrollable width buys the same drag a hundred does, so removing the offender is not the fix and
     this line is. The `overflow-x: hidden` on `body` cannot help — the body does not scroll in the
     shell, so nothing ever overflows it.
     ⚠ NO CLOSING BRACE MAY APPEAR ANYWHERE IN THIS COMMENT, and the warning itself had to be
     rewritten to obey its own rule. `tests/449-5d-countdown-hub.test.js` reads this rule with a
     negated-brace character class, which a closing brace inside the block terminates early — so a
     perfectly true sentence containing one makes that gate report a declaration missing that is
     sitting right here, and the test then accuses the app of a defect the comment caused.
     ⚠ A LEGITIMATELY WIDE BLOCK STILL SCROLLS, on its own: the admin tables already carry their own
     `overflow-x: auto` container, which is the pattern anything wide must use. */
  overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch;
  /* ⛔ THE COLUMN CENTRES ITSELF, ONCE. It used to centre each CHILD, through `margin-inline: auto`
     on `.ia-body > *` in style.css, and any component whose own rule set a `margin:` SHORTHAND reset
     that to zero: `.ia-door` is `margin: 14px 0 0` and `.primary-btn` the same shape.
     ✎ `.plan-seg` was the third example and its rules are gone (register #491, 2026-09-01). The
     measurement below was taken while it still existed and is not restated.
     Both selectors carry equal specificity, so which one won was decided by the order the
     stylesheets happen to load, and NOTHING FAILED when the wrong one did.
     ⚠ Measured at 1280 on 2026-08-25, after he reported it: Today, This week and Race each rendered
     TWO distinct left edges, the offenders 186px hard left. Progress was clean by luck, all three of
     its blocks being cards. ⛔ It was invisible at 440 — the column fills a phone and both margins
     resolve to zero either way — which is why five surfaces of §13b capture at BOTH widths missed it.
     ⇒ Centring here means a block cannot opt out by accident, whatever margin it sets on itself.
     `R4` in docs/work/400-…; the drawing is scripts/mockup-399-desktop-column.js. */
  display: flex; flex-direction: column; align-items: center;
  /* ✎ **THE 64px BAR ROOM IS GONE, 2026-09-14 (PROD-0914/bar-pinned).** It read: "the bar is position: fixed, so
     nothing in the flow measures it and this padding IS the reservation". That was true and is not
     any more — the bar is the shell's last flex child at phone widths, so the FLOW measures it and a
     reservation on top of it would hold 64px of nothing open above it. The variable survives for the
     desktop rail only, defaulting to zero here; `--ia-rail-room` is untouched.
     ⚠ THE SAFE-AREA INSET IS NOT PART OF IT and never was: it is the phone's home indicator, which is
     there whether or not this app draws a bar. It stays, because a PUSHED screen draws no bar and its
     content would otherwise run under the indicator; on a horizon root the bar now carries its own
     copy and sits below this column, so the two cannot double up. */
  padding: 14px 14px calc(var(--ia-bar-room, 0px) + env(safe-area-inset-bottom, 0px) + 14px);
  background: var(--bg);
}
/* ⛔ NOTHING IN THE COLUMN SHRINKS, AND THIS IS A DEFECT FIX RATHER THAN A TIDY-UP (#449 `5d`).
   A flex column whose content is taller than the box shrinks its children, and a child's automatic
   minimum size protects it ONLY while its overflow is `visible`. `.pro-tease` sets `overflow: hidden`
   — so on any scrolling screen the upgrade card collapsed to a fraction of its height and clipped its
   own button, which is what it did the first time the Race hub drew one. Measured across all 13
   registered surfaces before this line was added: nothing else was being shrunk by more than 2px,
   so nothing here depends on the shrinking that is being turned off.
   ⚠ It is the CHILDREN, not `.ia-body` itself, which keeps its own `flex: 1 1 auto` in the shell. */
.ia-body > * { flex-shrink: 0; }

.ia-card {
  background: var(--card); border: 1px solid var(--border); border-radius: var(--r);
  padding: 16px; margin-bottom: 14px; box-shadow: var(--shadow-sm);
}
.ia-card:last-child { margin-bottom: 0; }

/* A route with no home renders loudly rather than blank — see IA.renderInto. */
.ia-missing {
  margin: 24px 0; padding: 16px; border-radius: var(--r);
  background: var(--card); border: 1px solid var(--red); color: var(--red);
  font-size: 0.84375rem; font-weight: 600;
}

/* ── The bottom bar: white at 95 percent with a 20px backdrop blur, four line icons ────────────── */
/* ⛔ PROD-0914/bar-pinned — THE BAR IS THE SHELL'S BOTTOM ROW, NOT A FIXED ELEMENT, AND THAT IS HIS INSTRUCTION:
   *"Before this redesign it worked as intended with the tab bar at the very bottom of the screen …
   It must pin to the bottom of the screen."* (2026-09-14, rejecting a colour change as the fix.)

   ⛔ WHAT ACTUALLY CHANGED IN THE REDESIGN, READ OUT OF THE OLD STYLESHEET RATHER THAN REMEMBERED.
   The retired `.tab-bar` was `position: fixed; bottom: 0` with this same safe-area padding, so the
   BAR's own rules are not what regressed. `.app-shell` is: it was
   `display: flex; min-height: 100vh` in normal flow, with the DOCUMENT scrolling, and it is now
   `position: fixed; inset: 0; overflow: hidden` with the document at zero height and an inner column
   scrolling instead. `position: fixed` resolves against the LAYOUT viewport, which on iOS is not the
   visible one — it does not shrink when a toolbar hides and it does not follow the safe area — and a
   page that never scrolls at the document level gives the browser nothing to correct against. So the
   bar sat at the layout viewport's foot with the page showing beneath it.
   ⇒ FLOW ANSWERS IT WITH NO VIEWPORT ARITHMETIC AT ALL: the shell is the column, the bar is its last
   child, and "at the bottom" becomes a layout fact rather than a coordinate that has to resolve
   correctly. `100dvh` on the shell is the other half — the DYNAMIC viewport is the visible one.

   ⚠ WHAT IT COSTS, STATED RATHER THAN DISCOVERED: content no longer scrolls UNDER the bar, so the
   translucent blur has nothing behind it to blur. The blur stays because the rail at desktop width
   still sits over the column, and because removing it would be a second change inside a fix.
   ⚠ MEASURED OFF-DEVICE ONLY. There is no iOS here (no Xcode), so what is verified is that the bar
   is the shell's last child and its foot is the shell's foot at 440x956 — which is what makes the
   coordinate unnecessary. The behaviour this replaces is the one no amount of local measurement
   could reproduce, which is exactly why it is being replaced rather than tuned.
   ⛔ THE RAIL AT 900px AND UP IS STILL FIXED and is untouched: it spans the full height down the
   left, which flow in a column cannot express. Its rules re-declare `position: fixed` below. */
.ia-bar {
  flex: none; z-index: 40;
  display: flex; align-items: stretch;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: rgba(255,255,255,.95);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
  border-top: 1px solid var(--border);
}
.ia-bar-btn {
  /* ⛔ 61px, WHICH IS HOW THE STRIP REACHES THE 62 EVERY ARTBOARD DRAWS (#709, his ruling 2026-09-09
     off two drawn routes). Measured live at 440 before either was proposed: the strip was 57px, each
     button 110 x 56, the icon 24px, the label 12.5px, and 8.25px of air above and below.
     ⛔ HE TOOK THE AIR, NOT THE SHRINK. The other route reached the same 62 by taking the icon to 22px
     and the label to 10px, which is the design's own inside; it was drawn beside this one and refused,
     because 10px would be the smallest type anywhere in the app against a declared floor of 12.8.
     ⚠ THE FLOOR IS NOT CLEAN EITHER WAY AND HE WAS TOLD SO: today's 12.5px label is already a fraction
     under it. What separated the two was degree, not principle.
     ⛔ THE TAP TARGET WAS THIS ITEM'S OWN RISK AND IT ANSWERS THE OTHER WAY: each button goes from
     110 x 56 to 110 x 61, so nothing became harder to press. Measured on both routes, not reasoned.
     ⚠ INERT AT DESKTOP BY SOURCE ORDER, WHICH IS ALSO HIS RULING: the desktop block below sets its own
     48px on this selector, so the list down the left is untouched. He was shown that list and chose to
     leave it alone. A rule added AFTER that block would have changed it silently. */
  flex: 1 1 0; min-height: 61px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  background: none; border: 0; cursor: pointer;
  color: var(--muted); font: inherit;
  padding: 6px 2px;
}
.ia-bar-ic { display: block; width: 24px; height: 24px; }
.ia-bar-ic svg { width: 100%; height: 100%; display: block; }
.ia-bar-label { font-size: 0.78125rem; font-weight: 600; line-height: 1; letter-spacing: .005em; }
.ia-bar-btn.active { color: var(--brand); }
.ia-bar-btn:active { opacity: .65; }

/* ── A PUSHED SCREEN HAS NO BAR, AND RELEASES THE ROOM ONE WAS HOLDING (#645) ────────────────────
   ⛔ THE `display` RESTATEMENT IS LOAD-BEARING, NOT DEFENSIVE. `.ia-bar` above sets `display: flex`,
   and a class selector outranks the user agent's `[hidden] { display: none }` — so `bar.hidden` on
   its own would empty the bar and leave a 1px hairline band sitting on the bottom of the screen.
   ⚠ `ia/shell.js` empties the nav as well, so nothing is merely painted over: at rest a pushed
   screen's `#ia-bar` holds no buttons at all, which is what the test asserts on.

   ⛔ AND THE TWO ROOMS ARE ZEROED HERE RATHER THAN AT EACH PADDING. Both reservations exist because
   the bar is fixed and therefore unmeasured by the flow: 64px at the foot on a phone, 220px down the
   side at the rail's width. Neither number is repeated — each padding reads its own variable.
   ⚠ SAME RULE AT BOTH WIDTHS, DELIBERATELY. This file's own standing line is that a wider viewport
   gets more room and never a different information architecture; applying the design's rule only at
   440 would make the app's navigation depend on the width, which is the thing that rule forbids.
   ⚠ RECORDED RATHER THAN SMOOTHED OVER: the design has no artboard above 440, so the DESKTOP
   reading is an application of that rule and not a picture anybody has approved. It is a finding in
   `docs/work/645-…` and goes to him. */
.ia-bar[hidden] { display: none; }
/* ⛔ RULED 2026-09-07 (#645): ON A PUSHED SCREEN THE PHONE'S BOTTOM BAR GOES AND THE DESKTOP RAIL
   STAYS, AND THEY ARE THE SAME ELEMENT. So which one disappears cannot be decided in code, where
   there is no width: `ia/shell.js` now always fills the bar, and this hides it below the rail's
   breakpoint only. Above it the same element is the rail and is left alone.
   ⚠ WITHOUT THIS LINE THE PHONE GETS ITS BOTTOM BAR BACK on every pushed screen, which is the
   design's own reading at 440 reversed by accident while answering a question about 1280. */
@media (max-width: 899px) {
  .app-shell[data-ia-chrome="pushed"] .ia-bar { display: none; }
}
/* ✎ RULED 2026-09-07 (#645): `--ia-rail-room` NO LONGER COLLAPSES ON A PUSHED SCREEN. The four
   sections stay down the left at desktop width, so the column does not shift 220px sideways when a
   screen opens and back when it closes. `--ia-bar-room` still collapses, because the PHONE's bottom
   bar really does go on a pushed screen: that is the design's own reading at 440 and it is unchanged.
   ⛔ THE TWO WERE ONE DECLARATION AND ARE NOW TWO, WHICH IS THE WHOLE CHANGE. Reserving the rail's
   room without refilling the bar would have held 220px of nothing open, so `ia/shell.js` refills it
   in the same commit. ⚠ The bar and the rail are the same four items in two layouts; which one is on
   screen is decided below by width, not by whether a screen is pushed. */
.app-shell[data-ia-chrome="pushed"] { --ia-bar-room: 0px; }

/* ── Desktop: the same four horizons, laid out as a rail ─────────────────────────────────────────
   ⛔ THE SPEC IS REVIEWED AT 440 AND THAT IS THE AUTHORITY. Wider viewports get more room, never a
   different information architecture: the same four horizons, the same stacks, the same back
   control. Nothing here adds or removes a surface. */
@media (min-width: 900px) {
  .ia-bar {
    /* PROD-0914/bar-pinned — THE RAIL IS THE ONE PIECE OF CHROME THAT IS STILL FIXED, and it re-declares the
       positioning the phone's bar gave up: a full-height column down the left cannot be expressed as
       a row in the shell's own flex column. `.app-shell`'s `padding-left` below is its reservation. */
    position: fixed; left: 0; top: 0; bottom: 0; right: auto; width: 220px;
    flex-direction: column; justify-content: flex-start; gap: 2px;
    padding: calc(16px + env(safe-area-inset-top, 0px)) 12px 12px;
    border-top: 0; border-right: 1px solid var(--border);
    background: rgba(255,255,255,.95);
  }
  .ia-bar-btn {
    flex: 0 0 auto; flex-direction: row; justify-content: flex-start; gap: 12px;
    min-height: 48px; padding: 0 12px; border-radius: 12px;
  }
  .ia-bar-btn.active { background: var(--brand-50); }
  .ia-bar-label { font-size: 0.875rem; }
  /* ⛔ THE RAIL IS THE ONE PIECE OF CHROME STILL FIXED, so the whole column steps aside for it, once.
     The header used to do that for itself with `left: 220px`; a header in flow cannot, and giving it
     a margin would have left the ribbon and the body to repeat the same number twice more.
     ⚠ Seen in the browser at 1280 when the header DID span the full width: the rail sat on top of it
     and the eyebrow disappeared underneath, which a 440 capture can never show. */
  /* ⛔ 220px IS THE RAIL'S ROOM AND IS NOW A VARIABLE, for the reason the bottom padding above
     carries: the rail is the one piece of chrome still fixed, so this padding IS its reservation,
     and a pushed screen that no longer draws the rail must not keep holding 220px open for it. */
  .app-shell { padding-left: var(--ia-rail-room, 220px); }
  .ia-header { padding: 20px 24px 24px; }
  /* ⛔ #460 — THE HEADER TAKES THE COLUMN'S WIDTH, SO BOTH START AT ONE EDGE. His ruling 2026-09-01,
     from both sides drawn at 1280 with the phone case beside them: `header-in`, and the cards keep
     their current size. Measured before the change at 1280 on all four horizons: the header's
     leftmost content sat at 244px and the column's first block at 430px, and ALL of that 186px was
     `.ia-body`'s own centring of a 640px child — the two paddings were already equal. So this is one
     declaration rather than a padding sum, and it is the same declaration `.ia-body` already makes.
     ⛔ THE COST HE ACCEPTED, STATED TO HIM WHEN HE CHOSE: the full-bleed gradient's CONTENT indents
     with the header on every screen. The gradient itself still spans the viewport; only what is
     written on it moves in. The other side, `column-out`, would have widened every card, and that is
     the half not being paid.
     ⛔ IT IS CENTRED ON THE CONTAINER, NEVER ON THE CHILDREN, which is #400 `R4`'s lesson one block
     up: `margin-inline: auto` on a child is silently reset by any component that sets a `margin:`
     shorthand of its own, at equal specificity, with nothing failing.
     ⚠ AND IT IS INSIDE THIS BREAKPOINT ON PURPOSE. At 440 the two edges are 6px apart in the
     OPPOSITE direction and every pixel of that is the padding difference, not centring. His ruling
     covers the desktop case only; anything this does at 440 would be a regression. */
  .ia-header { display: flex; flex-direction: column; align-items: center; }
  .ia-header > * { width: 100%; max-width: 640px; }
  /* ⛔ AND THE LAST 7.5px IS THE SCROLLBAR, WHICH ONLY SHOWED UP ONCE THE OTHER 186 WERE GONE.
     `.ia-body` scrolls and the header does not, so a classic scrollbar eats 15px off the column's
     content box and its centred child lands HALF that to the left: measured at 1280 with the header
     already brought in, header 430px, column 422.5px. ⚠ It is worse than a constant offset — the
     column moves 7.5px sideways the moment a screen becomes long enough to scroll, which is the
     defect this rule also removes. Reserving the gutter on BOTH edges makes the content box
     concentric with the padding box, so the centre stops depending on whether a scrollbar is there.
     ⚠ Where the scrollbar is an overlay (every phone, and macOS by default) the gutter is zero and
     nothing moves, which is why the 440 capture is unchanged. */
  .ia-body { scrollbar-gutter: stable both-edges; }
  .ia-body { padding: 24px 24px 24px; }
}

/* ── Doors ───────────────────────────────────────────────────────────────────────────────────────
   A door is a card that opens another screen. Every horizon is mostly doors, which is the shape the
   four-horizon IA takes: a horizon answers one question and points at everything else.
   ⛔ 44px MINIMUM, and the whole card is the target rather than a chevron in the corner.

   ⛔ A DOOR SPACES ITSELF BELOW, EXACTLY AS A CARD DOES (#703, his ruling 2026-09-08: the same 14
   pixels everywhere). It used to space itself ABOVE, and the convention behind that — "a card
   spaces itself below and a door spaces itself above" — is right only when two cards meet. The
   content column is a FLEX context, so nothing collapses and the four adjacencies landed at
   14 · 28 · 0 · 14: a card above a door doubled to 28, and a door above anything gave nothing at
   all. Measured live before the change at 440 and 1280, on six instances across three screens.
   ⚠ HE RULED ON THE REACH SEPARATELY and said yes to every screen, including Today and Progress,
   which he had already signed off. It is one edit rather than one patch because of that answer.
   ⛔ TWO COMPENSATIONS TRAVEL WITH THIS AND MUST NOT BE LEFT BEHIND: ia/today.css put a top margin
   back on two doors (in a sheet loaded after this one, so it wins), and ia/race.css zeroed a bottom
   margin that did not exist until now. Both are corrected in the same commit; leaving either would
   double the gap this change exists to fix. */
.ia-door {
  display: block; width: 100%; text-align: left;
  min-height: 60px; padding: 14px 16px; margin: 0 0 14px;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--r);
  box-shadow: var(--shadow-sm); cursor: pointer; font: inherit; color: var(--text);
}
.ia-door-title { display: block; font-size: var(--fs-body); font-weight: 700; line-height: 1.3; }
.ia-door-sub {
  display: block; margin-top: 3px;
  font-size: 0.78125rem; font-weight: 500; line-height: 1.4; color: var(--muted);
}
.ia-door:active { background: var(--surface); }

/* An honest empty state, used where a surface exists but the athlete has nothing in it yet. */
.ia-empty {
  margin: 0; padding: 18px 16px; border-radius: var(--r);
  background: var(--card); border: 1px solid var(--border);
  font-size: 0.84375rem; line-height: 1.5; color: var(--muted);
}

/* ── THE CARD HEAD WITH A SCOPE ──────────────────────────────────────────────────────────────────
   A card title with the window it is drawn from on the same baseline, right-aligned. It is the shape
   `progress.md`'s Rule 1 asks for — "every chart states what it is drawn from" — and the reason it
   lives here rather than beside one card is that #449 phase 7 `7e` part two gave it a second wearer.
   ✎ **RENAMED FROM `.pa-head` / `.pa-scope` ON 2026-09-02.** It was declared by the planned-against-
   actual chart in part one and named for it; a class named after the first card to wear it is how a
   shared rule ends up copied under a second name. No declaration changed. */
.ia-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.ia-card-head h3 { margin: 0; }
.ia-card-scope { font-size: var(--fs-caption); color: var(--muted); white-space: nowrap; }

/* ── THE CARD EYEBROW ────────────────────────────────────────────────────────────────────────────
   #449 phase 7 `7a`, building the conformance pass's G3. The prototype's card eyebrow is a small,
   heavy, widely tracked uppercase label above a card's content, coloured by KIND: #7c3aed for a
   prompt, #b45309 for a caution, and the app's own secondary grey where a card is neither.

   ⛔ 12.5px, NOT THE PROTOTYPE'S 11px, AND THAT IS THE SAME BOUNDING `.ia-eyebrow` RECORDS. His Q2
   answer set a 12.5px floor and shell.md's acceptance list has carried "no body text below 12.5px"
   since the shell landed; the prototype's 11 sits under it. The TRACKING is the prototype's, in `em`
   rather than px, so it is the same optical tracking the screen eyebrow already uses.

   ⛔ THE KINDS ARE THE ROW'S OWN AND THERE ARE ONLY TWO. G3 names a prompt colour and a caution
   colour. A warning eyebrow and a summit eyebrow would be inventions: the rail below carries those
   two families because G5 names them, and the eyebrow does not because G3 does not. */
/* ⛔ THE SECOND SELECTOR IS LOAD-BEARING AND IS NOT A TIDINESS CHOICE. `.card h3` in style.css sets
   the display face, 16px, 700 and a NEGATIVE tracking, and an element-qualified descendant selector
   outranks a single class — so a heading given only this class kept every one of those and the
   eyebrow silently did not happen. Measured, not predicted: the Legal heading is an `h3` inside a
   `.card`. The family is reset for the same reason `.ia-eyebrow` never sets one: an eyebrow is body
   type, and inheriting the display face here would have been invisible in a diff of class names. */
.ia-card-eyebrow,
.card .ia-card-eyebrow {
  font-family: inherit;
  font-size: 0.78125rem; font-weight: 700; letter-spacing: .10em; text-transform: uppercase;
  line-height: 1.3; color: var(--muted);
  margin: 0 0 10px;
}
.ia-card-eyebrow--prompt,  .card .ia-card-eyebrow--prompt  { color: var(--brand); }
/* ⛔ THE APPLIED KIND, DECLARED BY THE SCREEN THAT FIRST DRAWS ONE (#449 phase 7 `7c` part three,
   conformance row 11), which is this file's own rule for a kind arriving late: a rule with nothing
   wearing it is a rule that styles nothing, so the kind lands with its first card and not before.
   ⛔ IT ADDS NO TOKEN. `--success` is already declared in style.css as the design system's own green
   and already carries its legibility note, so acceptance 3's count of tokens this app has and the
   registered project does not is unmoved. What IS new is a fifth rail kind and a second eyebrow
   colour, and that widening goes in front of him in phase 8 rather than being absorbed here.
   ⚠ Measured rather than assumed, and the first three figures written here were wrong: #15803d is
   5.02:1 on --card, 4.65:1 on --bg and 4.76:1 on --surface, which clears SC 1.4.3 on all three light
   surfaces a card can sit on. --bg is the tightest of the three, which is why it is the one quoted. */
.ia-card-eyebrow--applied, .card .ia-card-eyebrow--applied { color: var(--success); }
/* ⛔ THE CAUTION KIND, DECLARED BY THE SCREEN THAT FIRST DRAWS ONE (#547) — Today's load card, which
   lands its modifier in this same commit. G3 names this ink and the artboard's `LOAD` wears it; the
   token is the app's own `--accent-dk` (#ea580c), so nothing new is introduced and
   `ia-design-system-v2`'s token count does not move. ⚠ DECLARING IT WITHOUT A WEARER WOULD REDDEN
   TWO GUARDS AT ONCE — the dead-rule check and the declared-equals-worn check — which is why the
   two halves are one change. */
.ia-card-eyebrow--caution, .card .ia-card-eyebrow--caution { color: var(--accent-dk); }
/* ⛔ THE CAUTION KIND IS NOT DECLARED HERE YET, AND THAT IS THIS REPO'S RULE RATHER THAN AN OMISSION.
   G3 names two kind colours, and the app draws four card eyebrows: three neutral and one prompt. A
   `--caution` rule with nothing wearing it is a rule that styles nothing, which `tests/dead-css.js`
   fails on for a reason it can recite — seven such rules sat in style.css for 25 days.
   ⛔ ITS COLOUR IS MEASURED AND RECORDED RATHER THAN DECLARED: the prototype's own #b45309 clears
   WCAG 2.1 SC 1.4.3 at 4.65:1 on the worst of the three light surfaces it can sit on, and
   `scripts/449-legibility-derive.js`'s darkening returns it UNCHANGED — it needs no step at all. The
   session that builds the first caution card eyebrow declares it then, and takes the design-system
   addition to him rather than absorbing it (acceptance 3 counts every token style.css carries that
   the registered project does not). Which cards are cautions is a reading of each screen and belongs
   to that screen's own phase 7 group. */

/* ── THE RAIL ────────────────────────────────────────────────────────────────────────────────────
   #449 phase 7 `7a`, building G5. The prototype marks a card that asks something with a 4 to 5px
   coloured left rail, and the colour is the card's kind: purple asks a question, orange cautions,
   red warns, green is the summit family.

   ⛔ THE APP ALREADY DRAWS ELEVEN OF THESE BY HAND, AT 2, 3 AND 4px, IN SEVEN COLOURS. That is the
   `5l` shape one level up: a meaning encoded in a literal at each site, so nothing can be checked and
   nothing moves together. This is the named component; routing the existing hand-drawn rails through
   it is a per-surface job, because each one has to be READ to know which kind it is claiming.

   ✎ **RETRACTED IN PLACE 2026-09-09 (#542, his ruling off a drawn pair).** This paragraph read "THE
   LEFT CORNERS SQUARE OFF, WHICH IS THE APP'S OWN ESTABLISHED PATTERN rather than a new idea: every
   hand-drawn rail already pairs `border-left` with `border-radius: 0 <r> <r> 0`, because a 4px bar
   following a 16px curve reads as a smear rather than a rail." Both halves were true and neither
   settles it: the design draws all four corners rounded with a slightly thicker bar inside the curve,
   and shown both pictures at 440 and 1280 he took the rounded one. The smear the old note warns about
   is what the 5px bar answers, and it was looked for in the rendered result rather than argued about.
   ⛔ AND HE RULED THE REACH IN THE SAME SITTING: EVERY SCREEN THAT DRAWS ONE, not only the four being
   signed off, because one component drawn two ways is how two screens drift apart unnoticed. */
/* ⛔ THE SECOND SELECTOR IS LOAD-BEARING, THE SAME WAY THE CARD EYEBROW'S IS, AND "shell.css LOADS
   LAST" IS NOT AN ARGUMENT. Order only settles a tie: `.card.card-danger` and `.set-group-body .card`
   both score two classes and both set the whole `border` shorthand, so a single-class rule loses to
   them outright. Measured in the running app rather than predicted — the first build shipped a rail
   that computed to 1px of the settings group's hairline grey, and the census read it as unrailed. */
.ia-rail,
.card.ia-rail {
  /* ⛔ 5px, AND THE RADIUS IS THE APP'S OWN TOKEN, NEVER A NUMBER TYPED HERE (#542, 2026-09-09).
     `--r` is what every card in this app already rounds by, so this is "the left corners get the
     radius the right corners already have" rather than a radius invented for one component. */
  border-left-width: 5px;
  border-left-style: solid;
  border-top-left-radius: var(--r);
  border-bottom-left-radius: var(--r);
}
.ia-rail--prompt,  .card.ia-rail--prompt  { border-left-color: var(--brand); }
.ia-rail--caution, .card.ia-rail--caution { border-left-color: var(--accent); }
.ia-rail--warning, .card.ia-rail--warning { border-left-color: var(--red); }
.ia-rail--summit,  .card.ia-rail--summit  { border-left-color: var(--vert); }
/* The prototype rails its Applied card green, and green here is `--success` rather than `--vert`:
   `--vert` is the SUMMIT family's colour (the vert gauge, the Summit hub), and borrowing it would
   make an accepted suggestion read as a climbing surface. #449 phase 7 `7c` part three. */
.ia-rail--applied, .card.ia-rail--applied { border-left-color: var(--success); }
/* ⛔ THE NOTE KIND, DECLARED BY THE SCREEN THAT FIRST DRAWS ONE (#449 phase 7 `7d` part two,
   conformance row 31), which is this file's own rule for a kind arriving late. The prototype's Add a
   race screen ends in TWO railed notes and only one of them asks anything: the recovery collision is
   a caution and the Primary explainer is a plain statement, railed grey in the artboard.
   ⛔ IT ADDS NO TOKEN — `--muted` is already this file's own eyebrow colour — but it IS a SIXTH rail
   kind, and that widening goes in front of him in phase 8 rather than being absorbed here, exactly as
   `--applied` did. ⚠ A rail is a NON-TEXT mark, so nothing about SC 1.4.3 applies to it; `--muted` is
   carrying its own text contrast figures one section up and they are unaffected. */
.ia-rail--note,    .card.ia-rail--note    { border-left-color: var(--muted); }

/* ── THE CHIP ────────────────────────────────────────────────────────────────────────────────────
   #449 phase 7 `7h` part two, promoting the shape a THIRD surface was about to hand-draw. The
   prototype picks a value or opens a tool from a row of small filled buttons, and this app drew that
   row twice: `.ed-chip` on the race editor (2026-09-02) and `.adj-chip` on the adjust screen
   (2026-09-02). Both sites carried the same written rule — two is a repeat, a third promotes it —
   and the Type picker on Change this day is that third.

   ⛔ IT IS A PROMOTION, NOT A NEW SHAPE, AND THE TWO COPIES DISAGREED ON EXACTLY TWO DECLARATIONS,
   so the resolution is measured rather than picked:
   · `text-align: left` is `.adj-chip`'s and is taken, because it is the wearer with the long labels.
     Measured in the running app at 440 and 1280 before the change: NOT ONE chip on either surface
     wraps, so this declaration is inert at every wearer today and changes no pixel. It is kept for
     the chip that eventually does wrap, where a centred second line reads as a mistake.
   · the row gap is 8px, `.ed-chips`'s, where `.adj-chips` had 9px. 8px is also what the prototype
     draws on this screen's own chip rows. It moves adjust-week's chips by 1px per gap and that is
     stated rather than absorbed.
   ⚠ `.adj-chips`'s `margin-top: 13px` is NOT here: the space between a card's sub and its controls
   belongs to that card, not to the chip, and `.ed-chips` had none. It is declared at its one site.

   ⛔ 44px, WHERE THE DRAWING'S CHIP IS 35px, and this is the measurement both copies already
   carried: every one of these is tapped, and 44px is this app's own minimum for a tap target.

   ⛔ THE QUIET CHIP IS NAMED FOR HOW IT LOOKS, NOT FOR ITS ONE WEARER. It was `.ed-chip--more`,
   after the `More…` chip that opens the searchable picker; a shared rule named after the first card
   to wear it is exactly what `.pa-head` had to be renamed for, ten rules up in this same file. */
.ia-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.ia-chip {
  min-height: 2.75rem; padding: .625rem .875rem;
  border: none; border-radius: var(--r-sm); cursor: pointer; text-align: left;
  background: var(--surface-2); color: var(--text);
  font-family: var(--font-body); font-size: var(--fs-meta); font-weight: 600;
}
.ia-chip:active { background: var(--border); }
/* The selected chip is the drawing's: the brand fill, white, one weight heavier. */
.ia-chip--sel { background: var(--brand); color: var(--card); font-weight: 700; }
.ia-chip--sel:active { background: var(--brand-dk); }
/* Outline only, for a chip that opens something rather than choosing anything. */
.ia-chip--quiet { background: transparent; color: var(--muted); box-shadow: inset 0 0 0 1px var(--border); }

/* ── THE VALUE ROW AND ITS KEYPAD ────────────────────────────────────────────────────────────────
   #449 phase 7 `7a`, building G6. The row stands where the bare field stood; the input is still
   there, still wired, and simply not drawn.
   ⛔ `display: none` RATHER THAN A VISUALLY-HIDDEN CLASS, DELIBERATELY. An off-screen input is still
   focusable and still announced, so a keyboard or a screen reader would meet two controls for one
   value with only one of them labelled. The row is the control; the input is the store's doorway. */
.ia-field-behind { display: none !important; }
/* ⛔ THE ROW FILLS ITS CELL, OR A PAIR IN THE SAME GRID ROW COMES OUT UNEVEN. `Actual time` carries a
   two-word label and a seven-character value, so it wraps and grows; the grid stretches the LABEL to
   match its neighbour and, without this, the button inside it does not follow. Seen at 440. */
.ia-field-wrapped { display: flex; flex-direction: column; }
.ia-field-wrapped > .ia-valuerow { flex: 1 1 auto; }

/* ⛔ IT WRAPS RATHER THAN SHRINKING, BECAUSE THE CELL IT SITS IN GETS NARROW ON THE DESKTOP. The
   week's day detail is a two-column grid inside a two-column layout, so at 1280 each of these boxes
   is about 138px wide — narrower than at 440. Measured, not assumed. With the label held at a floor
   the value drops to its own line instead of squeezing the label into an ellipsis, and the athlete
   still reads a name and a number. */
.ia-valuerow {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; width: 100%;
  min-height: 46px; padding: 10px 12px;
  background: var(--surface, #f8f9fc); border: 1.5px solid var(--border); border-radius: 10px;
  font: inherit; color: var(--text); text-align: left; cursor: pointer;
}
.ia-valuerow-l { flex: 1 1 auto; min-width: 5.5rem; font-size: 0.84375rem; font-weight: 600; color: var(--muted); }
.ia-valuerow-v { margin-left: auto; font-family: var(--font-mono); font-size: 1rem; font-weight: 700; color: var(--text); }
.ia-valuerow-v--empty { font-family: inherit; font-weight: 600; color: var(--muted); }
.ia-valuerow-chev { color: var(--muted); font-size: 1rem; line-height: 1; }
.ia-valuerow:active { background: var(--surface-2, #e7e9f2); }

/* ── THE STEPPER ─────────────────────────────────────────────────────────────────────────────────
   #449 phase 7 `7f` part five, conformance row 27: the design draws the two run-walk intervals as
   `-`/`+` steppers rather than as bare boxes, which is G6 one control further on.
   ⛔ IT WRAPS THE FIELD RATHER THAN REPLACING IT, which is the opposite of `.ia-valuerow`'s
   arrangement above and is deliberate: the published run segments include 15, 10, 8 and 5 seconds,
   none of which a fixed step can land on, so the box has to stay typable (#370). ⇒ no
   `.ia-field-behind` here, and the input keeps its own label, its own id and its own focus ring.
   ⚠ THE BUTTONS ARE 46px, THE SAME FLOOR EVERY INPUT IN THIS APP CARRIES, so a stepper row is the
   same height as the plain field beside it in the same grid. */
.ia-stepper { display: flex; align-items: stretch; gap: 6px; }
.ia-stepper > input { flex: 1 1 auto; min-width: 0; text-align: center; }
.ia-stepper-b {
  flex: 0 0 auto; width: 46px; min-height: 46px;
  border: 1.5px solid var(--border); border-radius: 10px;
  background: var(--surface, #f8f9fc); color: var(--text);
  font-size: 1.125rem; font-weight: 600; line-height: 1; cursor: pointer;
  transition: border-color .15s, background .15s;
}
.ia-stepper-b:hover { border-color: var(--navy); }
.ia-stepper-b:active { background: var(--surface-2, #e7e9f2); }

/* ⛔ THE STATIC TWIN OF `.ia-valuerow`, AND THE DISTINCTION IS THE WHOLE POINT (#449 phase 7 `7f`
   part four). `.ia-valuerow` is a CONTROL: a 46px tap target with a pointer cursor, a pressed state
   and room for a chevron. A label beside a number that opens nothing must not borrow it, or a screen
   ships three boxes that look like buttons and do nothing — measured on the fueling screen's race-day
   table, which is what made this component necessary.
   ⛔ IT IS `.summit-row`'s RULES, PROMOTED RATHER THAN COPIED. That class did exactly this job on the
   summit page and was named for one screen; a second name for the same three declarations is the
   drift §10 forbids, so the summit page calls this now and `ia/summit.css` lost its four rules.
   ⚠ ITS SIZE IS THE TOKEN, NOT THE 13px `.summit-row` CARRIED. `5k`'s sweep did not reach that rule
   and the promotion is where it gets converted; the value keeps `--navy`, which is what summit drew. */
.ia-factrows { display: flex; flex-direction: column; gap: .5rem; margin-top: .5rem; }
.ia-factrow { display: flex; justify-content: space-between; align-items: baseline; font-size: var(--fs-meta); }
.ia-factrow-l { color: var(--muted); }
.ia-factrow-v { font-family: var(--font-mono); font-weight: 700; color: var(--navy); }

/* ── THE SCREEN HERO ─────────────────────────────────────────────────────────────────────────────
   #449 phase 7 `7g` part one, promoting the rules `style.css` carried as `.wk-hero*` and
   `.ses-hero*`. THREE screens draw a big mono figure with a small suffix beside it in the header —
   This week's `7.5 of 48 mi` (`7c`), the session screen's `18 mi` (`7c` part four) and now the
   Summit page's `31,200 ft to climb` — and the first two already shared one declaration under two
   names. A third screen was the point at which the shared rule needed a shared name, which is the
   promotion `.summit-row` → `.ia-factrow` made one day earlier and for the same reason.
   ⛔ NOT ONE DECLARATION CHANGED IN THE MOVE. The figure's tight negative tracking is what makes a
   run of mono digits scan, and it is on the FIGURE alone: `7a`'s G4 rule is that the suffix is a
   separate node at its own weight, because that tracking closes a two-letter word up.
   ⚠ `suffix`, NOT `unit`. The three wearers put a unit (`mi`), a target (`of 48 mi`) and a phrase
   (`ft to climb`) in that slot, and G4's own row calls the slot a suffix.
   ⚠ .74 RATHER THAN THE PROTOTYPE'S .6, and the bounding is the eyebrow's own: on the gradient's
   lighter end (#4c1d95) white at .6 composites to 4.06:1, under SC 1.4.3's 4.5. .74 is the value
   `.ia-shead-sub` already carries on this exact ground and it measures 6.03:1. */
.ia-hero {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-top: 12px;
}
.ia-hero-num {
  font-family: var(--font-mono); font-size: 2.75rem; font-weight: 800;
  line-height: 1; letter-spacing: -.03em; color: #fff;
}
.ia-hero-suffix {
  font-family: var(--font-mono); font-size: 1.125rem; font-weight: 600;
  line-height: 1.2; letter-spacing: normal; color: rgba(255,255,255,.74);
}

.ia-kp-overlay { position: fixed; inset: 0; z-index: 1200; background: rgba(6,8,20,.42); }
.ia-kp {
  position: fixed; left: 0; right: 0; bottom: 0;
  background: var(--card, #fff); border-radius: 20px 20px 0 0;
  box-shadow: 0 -12px 42px rgba(11,18,48,.24);
  padding: 4px 14px calc(14px + env(safe-area-inset-bottom));
  max-height: 88vh; overflow: auto;
}
/* On a wide screen a full-width sheet is a band of nothing with a keypad in the middle of it, so the
   panel centres and takes the width of the app's own content column. */
@media (min-width: 641px) {
  .ia-kp {
    left: 50%; right: auto; bottom: auto; top: 50%;
    transform: translate(-50%, -50%);
    width: 360px; border-radius: 16px;
    box-shadow: 0 16px 42px rgba(11,18,48,.24);
  }
}
.ia-kp-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 2px 6px; }
.ia-kp-head b { font-size: var(--fs-body); font-weight: 800; color: var(--navy); letter-spacing: -.2px; }
.ia-kp-x { background: none; border: none; font-size: var(--fs-body); color: var(--muted); cursor: pointer; padding: 8px; line-height: 1; min-width: 44px; min-height: 44px; }

.ia-kp-readout {
  display: flex; align-items: baseline; justify-content: center; gap: 8px;
  padding: 10px 0 4px;
}
/* The same proportion the hero uses after G4: the figure large and mono, the unit a smaller suffix. */
.ia-kp-figure { font-family: var(--font-mono); font-size: 2.5rem; font-weight: 800; letter-spacing: -.02em; color: var(--text); }
.ia-kp-figure--empty { color: var(--faint); }
.ia-kp-unit { font-size: 1.125rem; font-weight: 600; color: var(--muted); }
/* The refusal line. ⛔ `--accent-dkr`, THE APP'S OWN CAUTION INK, RATHER THAN A NEW COLOUR: this
   message is the keypad saying no, and the prototype draws no keypad note to take a colour from.
   Adding a token here would have been a colour invented for a string, and every token style.css
   carries that the registered design project does not is counted by acceptance 3. */
.ia-kp-note { min-height: 20px; text-align: center; font-size: 0.78125rem; color: var(--accent-dkr); }

.ia-kp-pad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 6px 0 12px; }
.ia-kp-key {
  min-height: 52px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface, #f8f9fc); color: var(--text);
  font-family: var(--font-mono); font-size: 1.25rem; font-weight: 700; cursor: pointer;
}
.ia-kp-key:active { background: var(--surface-2, #e7e9f2); }
.ia-kp-key--blank { visibility: hidden; }

.ia-kp-actions { display: flex; gap: 10px; }
.ia-kp-ok {
  flex: 1 1 auto; min-height: 48px; border: none; border-radius: 12px;
  background: var(--brand); color: #fff; font: inherit; font-weight: 700; cursor: pointer;
}
.ia-kp-ok:disabled { background: var(--surface-2, #e7e9f2); color: var(--faint); cursor: default; }
.ia-kp-clear {
  flex: 0 0 auto; min-height: 48px; padding: 0 16px;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--card, #fff); color: var(--muted); font: inherit; font-weight: 600; cursor: pointer;
}
.ia-kp-sign {
  min-width: 44px; min-height: 44px; padding: 0 10px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--card, #fff); color: var(--muted);
  font: inherit; font-size: 1.125rem; font-weight: 700; cursor: pointer;
  align-self: center;
}
.ia-kp-sign:active { background: var(--surface-2, #e7e9f2); }
