@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/manrope-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/manrope-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Bricolage Grotesque — the display face for h1/.w .title (2026-09-19,
   "bold fonts" direction). Manrope stays the body/UI face everywhere else;
   this is only for the one or two things per screen meant to read as THE
   headline. Self-hosted the same way as Manrope (two subset files, no
   Google Fonts CDN dependency) rather than a <link>, matching this app's
   existing offline-capable-fonts convention. */
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/bricolage-grotesque-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/bricolage-grotesque-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Root type scale. Every size and gap in this file and every
   template's <style> block is in rem, tied to this one root value — nothing
   else declares a font-size on html/body, so bumping this one line scales
   every font size AND every rem-based padding/margin/gap across the whole
   app together, proportionally, in one place. 112.5% = 18px base instead of
   the browser default 16px. */
html { font-size: 112.5%; }

:root {
  /* Repainted 2026-09-19 ("Atlas Bold" direction — bold sans, coral/teal,
     off-white/near-black — replacing the original warm-paper/forest-green
     "umano" palette this file's own comments below still describe). Every
     value re-verified against the same WCAG bar the original palette held
     itself to, not just eyeballed: computed contrast ratios for every pair
     actually used as text-on-background before picking the final hex. */
  --bg: #fbf7f0; --surface: #ffffff; --surface-2: #f0e9dc;
  /* --text-muted must clear WCAG AA (4.5:1) for normal-size text on EVERY
     ground in this palette — it is the token behind every hint, caption,
     deck state, gloss and eyebrow in the app, so a failure here is
     app-wide. #5f665b (unchanged from the original palette) still clears
     4.5:1 on all four new grounds: 5.56 / 5.94 / 4.92 / 5.10 — verified by
     computing relative luminance directly, not assumed carried over.
     Dark mode's own --text-muted (#ada398, below) measures 7.49 / 6.92 /
     6.36 on the dark grounds.
     Rule: de-emphasis is carried by weight, size and border, never by
     dropping text below a contrast minimum. */
  --text: #191714; --text-muted: #5f665b; --border: #e5dfd3;
  /* Terracotta (2026-09-22, principal: "the orange/coral is too hard" →
     "then use terracotta"), replacing the #d6350f coral. The same warmth
     fired darker: clay, not a signal light. Still one hex for both jobs, the
     way the coral and the forest green before it were: white text on it
     measures 4.95:1, and used as text on --bg it measures 4.64:1 — the coral
     had slipped to 4.49 there, a hair under AA. Soft and deep are mixed from
     it (94 % white, 45 % black; deep on soft 10:1). Still distinct from
     --red below (due-now, and nothing else): terracotta is brown-orange,
     --red a cool saturated crimson, so a due badge never reads as a button. */
  --accent: #b4533a; --accent-text: #ffffff; --accent-soft: #faf5f3; --accent-deep: #632e20;
  /* Radius scale, not ad-hoc per-component px — umano leans hard into big,
     confident rounding (and full pills for anything button-shaped), not
     Talkuma's old 12-16px "slightly rounded" default. */
  --radius: 24px; --radius-pill: 100px; --radius-sm: 14px;
  /* Flattened on purpose — the old design layered 3-4 soft drop-shadows on
     every card/button (floaty/cozy). Umano barely shadows anything;
     separation comes from bold color and a border, not elevation. --shadow
     is now a single thin line, kept only for things that must read as
     "above" the page (the sticky nav). */
  /* Flattened vs. the old triple-layer shadows, but NOT to zero — light
     mode's --bg/--surface are close enough in tone (warm paper on warm
     paper) that a truly flat card had nothing separating it from the page.
     Verified by actually looking at light mode (forced the tokens on in a
     real browser, not assumed dark-mode-only testing generalized) — a
     first pass at "flatten" went too far and made cards disappear. */
  --shadow: 0 2px 6px rgba(43,36,32,.1), 0 1px 2px rgba(43,36,32,.06);
  --nav-shadow: 0 3px 10px rgba(43,36,32,.12), 0 1px 3px rgba(43,36,32,.08);
  --hero-shadow: 0 3px 10px rgba(43,36,32,.1);
  /* ------------------------------------------------------------------
     TYPE SCALE
     ------------------------------------------------------------------
     Seven steps. Values are the most-used member of their cluster, not
     new numbers, so adopting the scale is close to a no-op visually —
     which is the point: this codifies the design that was approved
     rather than restyling it.

     Everything is rem, so all of it still scales from the single
     `html { font-size: 112.5% }` above. */
  --t-label: .72rem;    /* uppercase eyebrows, tags, meta rows */
  --t-small: .85rem;    /* hints, secondary links, captions */
  --t-body: .95rem;     /* body copy, card glosses */
  --t-action: 1rem;     /* button and control labels */
  --t-lead: 1.2rem;     /* secondary display, supporting sentence */
  --t-display: 2rem;    /* THE sentence — the one thing on the screen */
  --t-hero: 3.4rem;     /* the caught-up countdown */

  /* Two weights, because the font only really has two that matter, and
     a stroke for the one place that needs to go past the top of them.
     Manrope's @font-face declares `font-weight: 400 800`, so 900 and
     1000 clamp — measured: 800/900/1000 render the same string at
     exactly 543px. --stroke-max is how the display type gets heavier
     than the font's own ceiling, the trick h1 already used. */
  --w-strong: 700;
  --w-max: 800;
  --stroke-max: .4px;

  /* ------------------------------------------------------------------
     SPACE SCALE
     ------------------------------------------------------------------
     Deliberately partial — these six are the values that recur; they
     are applied only where the substitution is exact. Not every
     padding/gap in the app is snapped to this scale — some layouts are
     measured to the pixel and signed off, and rounding to make a table
     look tidy would move a layout that's already correct. They get
     reconciled page by page as each surface is rebuilt. */
  --sp-1: .3rem;
  --sp-2: .6rem;
  --sp-3: .9rem;
  --sp-4: 1.2rem;
  --sp-5: 1.5rem;
  --sp-6: 2rem;

  --sky: #3e6fa6; --sky-soft: #e1eaf4;
  /* Forest-teal, the Atlas Bold palette's secondary accent (2026-09-19) —
     used sparingly, same role it always had: contrast/variety against the
     coral --accent, never the primary action color. */
  --moss: #0c6b54; --moss-soft: #e7f0ee;
  --red: #b91c1c; --red-soft: #fbe4e4;
  --amber: #b45309; --amber-soft: #fdf1e0;
  --green: #16a34a; --green-soft: #dcfce7; --green-text: #166534;
}
/* Dark values live in two places on purpose (same values, two selectors):
   the media block respects system dark when nothing has been chosen
   explicitly; the [data-theme="dark"] block lets the in-app toggle force
   dark even on a light system, or force light even on a dark system via
   :not([data-theme="light"]) below. theme.js sets data-theme from
   localStorage; its absence means "follow the system," matching how a
   three-state toggle should behave. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #141311; --surface: #1e1b17; --surface-2: #262220;
    --text: #f3ede5; --text-muted: #ada398; --border: #3a342e;
    /* A lighter terracotta than light mode's #b4533a — it needs luminance
       against near-black: 7.44:1 on --bg, 6.88:1 on --surface. Text on it
       flips to dark (#1c120d, 7.37:1); white would not pass here. */
    --accent: #e28f75; --accent-text: #1c120d; --accent-soft: #4e362d; --accent-deep: #efc1b3;
    --shadow: 0 1px 2px rgba(0,0,0,.35), 0 10px 24px rgba(0,0,0,.4);
    --nav-shadow: 0 4px 10px rgba(0,0,0,.4), 0 16px 32px rgba(0,0,0,.45);
    --hero-shadow: 0 6px 16px rgba(0,0,0,.4);
    --sky: #7fa8d6; --sky-soft: #2a3542;
    --moss: #4fd9b0; --moss-soft: #2d3f37;
    --red: #e57373; --red-soft: #3a2424;
    --amber: #e0a458; --amber-soft: #3a2f1c;
    --green: #6fcf82; --green-soft: #2a3a2c; --green-text: #8fd39c;
  }
}
:root[data-theme="dark"] {
  --bg: #141311; --surface: #1e1b17; --surface-2: #262220;
  --text: #f3ede5; --text-muted: #ada398; --border: #3a342e;
  --accent: #e28f75; --accent-text: #1c120d; --accent-soft: #4e362d; --accent-deep: #efc1b3;
  --shadow: 0 1px 2px rgba(0,0,0,.35), 0 10px 24px rgba(0,0,0,.4);
  --nav-shadow: 0 4px 10px rgba(0,0,0,.4), 0 16px 32px rgba(0,0,0,.45);
  --hero-shadow: 0 6px 16px rgba(0,0,0,.4);
  --sky: #7fa8d6; --sky-soft: #2a3542;
  --moss: #4fd9b0; --moss-soft: #2d3f37;
  --red: #e57373; --red-soft: #3a2424;
  --amber: #e0a458; --amber-soft: #3a2f1c;
  --green: #6fcf82; --green-soft: #2a3a2c; --green-text: #8fd39c;
}

/* ══════════════════════════════════════════════════════════════════════
   THE COMPONENT LAYER.

   One definition per object, for the screens of the redesign: the Learn
   loop, the deck screen, the card editor, manual add, the generate question
   and browse — rather than each template defining its own copy in a local
   <style> block, which drifts (two screens sizing the same object
   differently, a template copying a neighbour instead of reading the rule).

   SCOPED TO .w, which the redesigned screens carry on <body>. That is not
   tidiness: an unscoped version of this block measurably restyles pages
   that use the same class names for unrelated things (e.g. .count already
   means something different on other screens). Screens still on the
   legacy nav are each due their own pass, and restyling a screen nobody
   has designed yet is how a refactor becomes a redesign nobody asked for.
   ══════════════════════════════════════════════════════════════════════ */

/* One exit, top right, a word rather than a back arrow. */
.w .top { display: flex; justify-content: flex-end; }
.w .top.split { align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.w .top .lead { flex: 1 1 auto; min-width: 0; font-size: var(--t-small); font-weight: var(--w-max);
             color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w .exit { flex: 0 0 auto; font-size: var(--t-small); font-weight: var(--w-strong); color: var(--text-muted);
        text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 3px;
        min-height: 44px; display: inline-flex; align-items: center; }

/* Pressed state (static/pending.js). One ring, drawn in the
   control's own colour; .busy dims and blocks a second tap; the bar is the
   "we are leaving" signal along the top edge. Reduced motion keeps the ring
   still — the open arc plus the dimming still reads as "pressed".
   Deliberately NOT scoped to .w: this is behaviour, not the design layer,
   and several screens have no .w body yet but still trigger actions that
   need the pressed state. */
.ring { display: inline-block; flex: 0 0 auto; width: .7rem; height: .7rem; box-sizing: border-box;
        border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
        animation: w-spin .8s linear infinite; vertical-align: -.05rem; margin-right: .45rem; }
@keyframes w-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .w .ring { animation: none; } }
.busy { opacity: .7; cursor: progress; pointer-events: none; }
form[aria-busy="true"] [type="submit"] { pointer-events: none; }
.exit.busy .ring { margin-left: .45rem; margin-right: 0; }
.busy > svg:first-of-type { display: none; } /* an icon button: the ring takes the icon's place */
.nav-progress { position: fixed; top: 0; left: 0; height: 3px; width: 0; background: var(--accent);
                border-radius: 0 2px 2px 0; z-index: 100; transition: width 1.2s ease-out; }
.nav-progress.on { width: 72%; }

/* Text. Four steps and no more. */
.w .eyebrow { font-size: var(--t-label); font-weight: var(--w-max); letter-spacing: .04em;
           text-transform: uppercase; color: var(--text-muted); margin: var(--sp-4) 0 0; }
.w .title { font-family: 'Bricolage Grotesque', Manrope, "Segoe UI", sans-serif; font-size: 1.5rem; font-weight: var(--w-max); color: var(--text); line-height: 1.2;
         margin: var(--sp-1) 0 0; -webkit-text-stroke: var(--stroke-max) currentColor; text-wrap: balance; }
.w .sub { font-size: var(--t-small); color: var(--text-muted); line-height: 1.45;
       margin: var(--sp-2) 0 0; font-variant-numeric: tabular-nums; }
.w .hint { font-size: var(--t-small); color: var(--text-muted); line-height: 1.45; margin: var(--sp-1) 0 0; }

/* Fields. No placeholder attribute anywhere in the new design — examples live
   outside the field, because a user test showed a placeholder read as content
   the learner had broken. Any capped field carries a live count, and the cap
   counts runes. */
.w .lbl { display: block; font-size: var(--t-label); font-weight: var(--w-strong); letter-spacing: .04em;
       text-transform: uppercase; color: var(--text-muted); margin: var(--sp-4) 0 var(--sp-1); }
.w .lbl .opt { font-weight: var(--w-strong); text-transform: none; letter-spacing: 0; }
.w .field { display: block; width: 100%; background: var(--surface); border: 1px solid var(--border);
         border-radius: var(--radius-sm); padding: var(--sp-3); font-family: inherit;
         font-size: var(--t-body); color: var(--text); line-height: 1.4; resize: vertical; }
.w .count { font-size: var(--t-label); color: var(--text-muted); text-align: right;
         font-variant-numeric: tabular-nums; margin: var(--sp-1) 0 0; }
.w .count.over { color: var(--text); font-weight: var(--w-strong); }

/* Something the screen must tell you before you can go on. Ground, weight and
   a bar — no hue, because --red is due-now and the app has no error colour. */
.w .msg { background: var(--surface-2); border-left: 3px solid var(--text-muted);
       border-radius: 0 var(--radius-sm) var(--radius-sm) 0; padding: var(--sp-3) var(--sp-4);
       margin-bottom: var(--sp-4);
       font-size: var(--t-small); font-weight: var(--w-strong); color: var(--text); line-height: 1.45; }

/* One primary shape. The .sub line inside carries a price or a consequence,
   which is why the button is always a column: with one child it renders
   identically, so the two former variants collapse to one rule. */
.w .btn { display: flex; flex-direction: column; align-items: center; justify-content: center;
       width: 100%; min-height: 44px; padding: 1.05rem; border: none; border-radius: var(--radius-pill);
       background: var(--accent); color: var(--accent-text); font-family: inherit;
       font-size: var(--t-action); font-weight: var(--w-max); text-decoration: none; cursor: pointer;
       text-transform: none; letter-spacing: normal; margin: 0; }
.w .btn .sub { display: block; font-size: var(--t-small); font-weight: var(--w-strong);
            opacity: .82; margin-top: .25rem; color: inherit; }
.w .btn.sec { background: var(--surface); color: var(--accent); border: 2px solid var(--accent); }
.w .btn.compact { width: auto; padding: .6rem 1.05rem; font-size: var(--t-small); }

/* The quiet alternative — the affordance for the other path, deliberately not
   a button, because a second button is a second front door. It was .filter on
   the deck screen, .to-edit on browse and .gen on the generate row: one object
   under three names, which is how a design language stops being one. */
.w .quiet { display: block; background: none; border: 0; padding: 0; font: inherit;
         font-size: var(--t-small); font-weight: var(--w-strong); color: var(--text-muted);
         text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 3px;
         cursor: pointer; min-height: 44px; }
.w .quiet.right { text-align: right; font-size: var(--t-label); line-height: 44px; }
.w .quiet.centre { text-align: center; }
/* An underline means "this leaves the screen" (§03). .quiet bakes one in,
   which is right for the three links that do leave and wrong for the two
   shipped buttons that act in place — the defect this modifier closes. */
.w .quiet.inplace { text-decoration: none; }

/* A <label> that wraps CONTENT rather than naming a field. talkuma.css's own
   bare-element rule below styles every label as an uppercase form label with
   its own size, weight, spacing and margin — correct for a form, and the
   reason every card row on the deck screen once rendered in capitals. */
.w .wraps { margin: 0; font-size: inherit; font-weight: inherit; color: inherit;
         text-transform: none; letter-spacing: normal; }

* { box-sizing: border-box; }

body {
  font-family: Manrope, "Segoe UI", system-ui, sans-serif;
  background: var(--bg); color: var(--text);
  max-width: 560px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem;
}
/* Live character counter for the free-text prompt fields (AI topic, Pace
   goal, generate-more context). Sits BELOW and OUTSIDE the field so it can
   never read as content inside it, right-aligned, tabular so the number
   doesn't jitter as it counts. Rendered server-side as "0 / 600" so the
   limit is named even with JS off; talkuma.js keeps it live. */
.charcount { font-size: var(--t-small); color: var(--text-muted); text-align: right;
             margin: .3rem 0 0; font-variant-numeric: tabular-nums; }
.charcount.near { color: var(--text); font-weight: var(--w-strong); }
a { color: var(--accent); text-decoration: none; }
a:hover { opacity: .8; }

/* Floating pill nav (was a plain text row) — sticky across scroll, rounded,
   shrink-to-content and centered rather than stretched full-width. */
nav {
  /* Gap/padding trimmed to make room for the /help icon without pushing
     common phone widths (iPhone SE 375px, 12-15 at 390px) into needing the
     scroll-to-reach-theme-toggle fallback below — measured via Playwright's
     scrollWidth/clientWidth before and after, not guessed. */
  display: flex; align-items: center; gap: .55rem; width: fit-content;
  max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: none; /* the overflow-x is a last-resort safety net for a
    truly cramped viewport, not a visible affordance — it shouldn't show a
    scrollbar track when the pill fits, which is the normal case */
  margin: 0 auto 1.75rem; padding: .5rem .85rem;
  background: var(--surface); border-radius: 100px; box-shadow: var(--nav-shadow);
  position: sticky; top: .75rem; z-index: 10;
}
nav::-webkit-scrollbar { display: none; }
/* On a real phone, the nav pill (wide with icons+labels on every item)
   can push logout off-screen with no visible hint. Tightens the page's
   own side padding (more usable width for nav to grow into before it has
   to scroll) and the pill's own internal padding, then drops the "Talkuma"
   wordmark entirely — the logo mark alone is still a clear home link, and
   it's the one nav item that costs width for zero added information once
   space is this tight. */
@media (max-width: 420px) {
  body { padding-left: .75rem; padding-right: .75rem; }
  nav { padding: .45rem .6rem; gap: .4rem; }
  .nav-brand .brand-text { display: none; }
}
/* Every nav item stays on one line — a wide pill can make flex children
   shrink and wrap "Log out" onto two lines instead of the row simply
   growing; nowrap here plus nav's own overflow-x above is the actual fix
   (scroll rather than wrap/break, on the rare viewport too narrow for the
   whole pill). */
nav a, nav button { white-space: nowrap; }
nav a { font-size: .85rem; text-decoration: none; color: var(--text-muted); font-weight: 700; }
.nav-brand { display: inline-flex; align-items: center; gap: .45rem; }
.nav-logo { border-radius: 6px; display: block; }

/* Beta-mode global rate limit: a heads-up that generation may get
   throttled soon, shown site-wide (nav is on every page) rather than only
   at the moment a request is actually rejected. */
.beta-banner { max-width: 640px; margin: -1.2rem auto 1.75rem; padding: .7rem 1rem; background: var(--amber-soft); color: var(--amber); border-radius: var(--radius-sm); font-size: .82rem; font-weight: 700; text-align: center; }
/* maintenance-banner reuses .beta-banner's shape (a same-styled, clickable
   variant — planned stays amber like the throttle warning; .active swaps
   to red since the app is actually gated right now, not just heads-up). */
.maintenance-banner { display: block; text-decoration: none; }
.maintenance-banner.active { background: var(--red-soft); color: var(--red); }
.maintenance-banner::after { content: " \2192"; } /* trailing arrow — the only visual cue this banner is a link, not just a notice */

/* Icon-labeled nav links — icons are fixed-width regardless of
   language/name length, which avoids the overflow failure mode a
   variable-width label would reintroduce. Icon+label rather than
   icon-only, for clarity over pure compactness. */
.nav-link { display: inline-flex; align-items: center; gap: .3rem; }
.nav-link svg { flex-shrink: 0; opacity: .8; }

/* "More" overflow menu: Help/Settings move here so the always-visible
   row is shorter. Dropdown
   toggled via the inline <script> in _partials.html's nav define block
   (deliberately not talkuma.js — study.html includes nav but not that
   file). Rendered as nav's SIBLING in the DOM, not its child, and
   position:fixed (JS-positioned against the button's own rect on open) —
   nav itself is `overflow-x: auto` for its own narrow-viewport scroll
   fallback, which would silently clip an absolutely-positioned child
   dropdown to invisible; a real bug caught via a Playwright screenshot of
   the OPEN state, not visible from the closed-state screenshot alone. */
.nav-dropdown {
  position: fixed; z-index: 20;
  display: flex; flex-direction: column; gap: .1rem; min-width: 160px;
  background: var(--surface); border-radius: var(--radius-sm);
  box-shadow: var(--nav-shadow); padding: .4rem;
}
/* An author-stylesheet `display` rule always beats the UA stylesheet's
   default `[hidden]{display:none}` regardless of specificity numbers (UA
   origin loses to author origin outright) — this class rule alone made the
   [hidden] attribute a no-op, so the dropdown rendered open by default.
   Caught via a real screenshot showing it open with no click, not from
   reading the CSS. Re-asserting display:none at equal-origin but higher
   specificity (class+attribute) fixes it. */
.nav-dropdown[hidden] { display: none; }
.nav-dropdown-item {
  display: flex; align-items: center; gap: .5rem; padding: .55rem .6rem;
  border-radius: 8px; font-size: .85rem; font-weight: 700; color: var(--text-muted);
}
.nav-dropdown-item:hover { background: var(--surface-2); color: var(--accent); }
.nav-dropdown-item svg { flex-shrink: 0; opacity: .8; }
.nav-dropdown-divider { height: 1px; background: var(--border); margin: .3rem .2rem; }

/* Settings page: shows the account's current plan next to their name. */
.tier-badge { font-size: .72rem; font-weight: 700; padding: .2rem .6rem; border-radius: 100px; background: var(--accent-soft); color: var(--accent); margin-left: .5rem; }
/* The plan, next to the account name wherever the name is shown (2026-09-20,
   principal: "the user should have the tier pill next to account name"). */
.tier-pill { display: inline-block; vertical-align: middle; font-size: .62rem; font-weight: var(--w-max); letter-spacing: .06em; text-transform: uppercase; padding: .15rem .5rem; border-radius: 100px; background: var(--surface-2); color: var(--text-muted); margin-left: .35rem; }
nav.app-sidebar .tier-pill { background: rgba(255,255,255,.12); color: rgba(255,255,255,.78); }
/* Plus reads coral wherever the pill appears (principal, 2026-09-21). */
.tier-pill.plus, nav.app-sidebar .tier-pill.plus { background: var(--accent); color: #fff; }

/* Price tag: the cost of an AI-priced control, beside it, at the moment of
   choosing. Same recipe as .tier-badge, its own name — a plan and a price
   are different objects even when they look alike. Scoped to .w like the
   rest of the layer. Named .price-tag, not .price: door.html and
   edit_set.html already use class="price" for plain muted notes, and a
   shared name would turn those into green pills unintentionally. */
.w .price-tag { display: inline-flex; align-items: center; font-size: .72rem; font-weight: 700; padding: .2rem .6rem;
                border-radius: 100px; background: var(--accent-soft); color: var(--accent); margin-left: .35rem; white-space: nowrap; }

/* Logout is icon-only (title/aria-label carry the word) — it's a single,
   unambiguous, universally-recognized glyph, so the label would be pure
   redundancy here specifically, unlike Report/Review/Settings above. */
.nav-logout { margin: 0; padding: 0; display: inline-flex; }
.nav-icon-btn {
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 50%;
  background: none; border: none; cursor: pointer; padding: 0;
  color: var(--text-muted);
}

/* Haptic-feeling nav: a real hover lift + a real press-down on tap, on
   every interactive nav element. Kept to transform+color (cheap, no
   layout thrash) and short (120ms) so it reads as responsive, not laggy.
   prefers-reduced-motion turns the motion off but keeps the color
   feedback, so it stays informative without being physically bothersome
   for anyone who's asked for less motion. */
.nav-link, .nav-icon-btn, .theme-toggle {
  transition: transform .12s ease, color .12s ease, background-color .12s ease;
}
.nav-link:hover { color: var(--accent); }
.nav-icon-btn:hover { color: var(--red); background: var(--red-soft); }
.theme-toggle:hover { color: var(--accent); }
.nav-link:active, .nav-icon-btn:active, .theme-toggle:active { transform: scale(.88); }
@media (prefers-reduced-motion: reduce) {
  .nav-link, .nav-icon-btn, .theme-toggle { transition: color .12s ease, background-color .12s ease; }
  .nav-link:active, .nav-icon-btn:active, .theme-toggle:active { transform: none; }
}

/* Shared "label above full-width input" rhythm — one rule, one rhythm,
   applied everywhere the eye needs to move from a label to its field the
   same way every time, instead of each page hand-rolling its own copy and
   drifting. admin.html's
   .stat-card/.rate-limit-form/.add-email-form rules are genuinely different
   (compact inline controls, not stacked label+field) and stay
   class-scoped — more specific than these tag selectors, so they keep
   overriding exactly as before. */
label { font-size: .78rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; display: block; margin: 1.4rem 0 .5rem; }
.pick-grid label { margin-top: 0px; }
input[type=text], input[type=email], input[type=password], input[type=number], input[type=file], textarea {
  width: 100%; box-sizing: border-box; padding: .85rem 1rem; border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: var(--radius-sm); font-family: inherit; font-size: .95rem;
}

/* One focus style for every typing field (2026-09-21, review item 1): a
   dark border and a soft coral halo. The old auth rule painted the border
   full coral, and with autofocus the first field on Log in / Sign up /
   Invite opened looking like a failed validation. Chips and radios keep
   their outline ring — they are choices, not fields. */
input[type=text]:focus-visible, input[type=email]:focus-visible, input[type=password]:focus-visible,
input[type=number]:focus-visible, textarea:focus-visible, .w .field:focus-visible {
  outline: none; border-color: var(--text); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

.theme-toggle {
  display: flex; align-items: center; justify-content: center;
  border: none; background: none; cursor: pointer; padding: 0; margin: 0 0 0 .2rem;
  color: var(--text-muted);
}

/* Section color-blocking, umano-style: a solid-accent band marks the top
   of a page as its own "chapter" before settling into the calm paper
   background for the actual content below — rather than one flat --bg
   for the whole page. Deliberately NOT applied to every element that
   touches --accent (buttons, links stay as they are) — just the page's
   own title/header area, one block per page. */
.hero-block {
  background: var(--accent); color: var(--accent-text);
  border-radius: calc(var(--radius) + 4px); padding: 1.85rem 1.6rem; margin: 0 0 2rem;
  box-shadow: var(--hero-shadow);
}
.hero-block h1, .hero-block p { color: var(--accent-text); }
/* Was opacity: .78 under the original forest-green accent, which had
   enough headroom (7.3:1 white-on-accent) to lose a fifth of its contrast
   and still read fine. The new coral only clears AA at full strength
   (4.8:1) — diluting it the same way lands at 3.45:1, short of the 4.5:1
   text bar this file holds everywhere else. Full opacity instead of a
   quieter fix that would need yet another token. */
.hero-block .subtitle { color: var(--accent-text); }

/* Display type: Bricolage Grotesque (2026-09-19 "bold fonts" direction),
   Manrope for everything else — a genuine second typeface now, not the
   same-family-different-weight trick the original umano palette used.
   Family/weight identical on every page; font-size and margin are
   deliberately not — each page sets its own in a local override. */
/* 800 is this face's heaviest shipped weight too — same ExtraBold ceiling
   as Manrope, no 900/Black instance exists to request. The stroke below
   still earns its keep on top of that ceiling. Chromium-based browsers
   support -webkit-text-stroke; the rule is a no-op fallback to plain 800
   anywhere it isn't. */
h1 { font-family: 'Bricolage Grotesque', Manrope, "Segoe UI", sans-serif; font-style: normal; font-weight: 800; letter-spacing: -.01em; -webkit-text-stroke: .5px currentColor; }

/* ---------------------------------------------------------------------
   Bottom tab bar
   ---------------------------------------------------------------------
   Three destinations, navigation only — no logout form, no theme toggle,
   no action buttons. See _partials.html's `tabs` comment for why (Apple's
   HIG rule, plus: an action here is a fifth CTA competing with the one on
   screen).

   Fixed to the bottom of the viewport: an in-flow bar trades reachability
   for immunity to mobile browser chrome collapsing under it, and that
   trade is the weirdness itself — the one navigation surface the app
   scrolls away the moment you pass the first screen, which is not how
   any native or PWA tab bar behaves.

   env(safe-area-inset-bottom) keeps it clear of the iPhone home indicator
   (paired with viewport-fit=cover in the page's viewport meta, without
   which the inset always resolves to 0). max() and not a bare addition so
   the offset never falls below the base value on hardware with no inset. */
/* The bare `nav` rule above styles the old floating pill — sticky, fully
   rounded, auto-margined, width:fit-content, overflow-x:auto. `nav.tabs` is
   a different object entirely and inherits none of that usefully, so every
   one of those properties is reset here rather than left to collide. */
nav.tabs {
  /* An island, not a full-bleed band: a band is the loudest object on an
     empty screen even when lightened, because its height is set by the
     44px tap target, not by its styling. A content-width pill spends the
     same ink on a third of the footprint. The shape reuses --radius-pill
     and --nav-shadow from the retired top nav — that object's shape was
     never what was wrong with it; it carried six controls, sat at the
     top, and floated over content. This carries three and sits at the
     bottom.

     Centred via left/transform rather than margin:auto, which only
     auto-centres a non-fixed box or a fixed one with left/right both
     already set to 0. */
  /* 2026-09-20: the island became the v2 mockups' full-width bar — four
     labelled tabs, active in coral, translucent ground with a hairline on
     top (principal: "i want the whole ux/ui that was shown there"). The
     comment above records why the island was chosen; the mockup overrode
     it with the principal's eyes open. */
  display: flex; width: 100%; max-width: none; gap: 0; justify-content: space-around;
  padding: .45rem .5rem calc(.4rem + env(safe-area-inset-bottom, 0px));
  margin: 0; /* the bare `nav` rule's `margin: 0 auto 1.75rem` otherwise lifts a fixed bar 31px off the bottom edge */
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  border: 0; border-top: 1px solid var(--border);
  border-radius: 0;
  box-shadow: none;
  /* top: auto is load-bearing, not decorative: the bare `nav` rule above
     (the retired top pill) sets `top: .75rem`, and nav.tabs still matches
     that selector too (it's a `<nav>` element with an extra class, and the
     bare rule doesn't stop applying just because a more specific one also
     matches). Under position:static that inherited value was harmless —
     but once nav.tabs is position:fixed, `top: .75rem` combined with this
     rule's own `bottom` pins BOTH edges, and a fixed box with both top and
     bottom set stretches to fill the gap between them instead of sizing
     to its content. Resetting top to auto here is what lets bottom alone
     anchor it and height go back to hugging the actual tab row. */
  position: fixed; top: auto; left: 0; right: 0; transform: none;
  bottom: 0;
  z-index: 10; overflow: visible;
}
.tab {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .25rem;
  /* WCAG 2.2 SC 2.5.8 sets the AA floor at 24x24 CSS px; platform guidance
     is stricter (44pt iOS / 48dp Android) and a primary nav target is
     exactly where to honour the stricter number. */
  min-height: 44px; min-width: 64px;
  padding: .1rem .5rem;
  border-radius: var(--radius-sm);
  font-size: .68rem; font-weight: var(--w-strong); letter-spacing: 0;
  color: var(--text-muted); text-decoration: none; white-space: nowrap;
}
.tab svg { display: block; width: 22px; height: 22px; stroke-width: 2.2; flex: none; }
/* The current place in coral, with weight and stroke as the second and
   third channel — the mockup's choice; the old island marked it without
   hue. */
.tab.on { background: none; color: var(--accent); font-weight: var(--w-max); }
.tab.on svg { stroke-width: 2.5; }

/* A width fight against the body gutter doesn't apply once the pill is
   position:fixed — a fixed box is placed against the viewport, not
   constrained by body's content box. left:50% + transform above is the
   whole centring story.

   position:fixed no longer needs a flex parent to reach the bottom, so
   body:has(nav.tabs) below is just about giving scrolling content room
   to clear the fixed bar, plus the fade below it — content now passes
   BEHIND a fixed bar instead of ending before an in-flow one, so an
   abrupt edge under the pill needs covering the same way a native app's
   blurred/translucent tab bar backdrop does. */
body:has(nav.tabs) { display: flex; flex-direction: column; min-height: 100vh; }
/* Taller than the fade below, not just taller than the pill — otherwise
   the last couple rem of every page sits inside the fade and behind the
   pill, which reads as a rendering bug rather than a design choice. */
body:has(nav.tabs) main { padding-bottom: calc(6.5rem + env(safe-area-inset-bottom, 0px)); }
body:has(nav.tabs)::after {
  content: ""; position: fixed; left: 0; right: 0; bottom: 0;
  height: calc(6rem + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to bottom, transparent, var(--bg) 65%);
  pointer-events: none; z-index: 9; /* one below the pill (z-index: 10), so the pill itself stays fully opaque and legible over it */
}
.tab-icon { padding: .1rem .5rem; }
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--radius-pill); }

/* ── Desktop sidebar (2026-09-20) ─────────────────────────────────────────
   CSS-only device split, principal-directed: "i prefer css only" over a
   separate template or a device-detect redirect. Same markup renders on
   every width — nav.tabs (mobile) and nav.app-sidebar (desktop) both exist
   on the page always; only one is ever visible, decided purely by this
   media query, so resizing a window (or rotating a tablet past the
   breakpoint) switches nav with no reload and no flash — the exact thing a
   redirect-based split cannot do for free. 960px, not a phone/tablet
   boundary: chosen so the sidebar plus a comfortable content column
   together still clear a 1024px iPad-landscape width with room to spare,
   while a real tablet in portrait (768-820px, the common iPad widths)
   keeps the mobile nav rather than getting a cramped sidebar. */
nav.app-sidebar { display: none; }
@media (min-width: 960px) {
  nav.tabs, body:has(nav.tabs)::after { display: none; }
  body:has(nav.tabs) { padding-bottom: 1.5rem; } /* the mobile fixed-bar clearance no longer applies */
  /* The desktop shell has one look, not two (principal, 2026-09-20: "for
     desktop there is no need for bright/dark just the bright one"). The
     mockup itself never defined a dark palette — its :root declares
     `color-scheme: light` outright. Pinning the light values here, on the
     highest-specificity selector in the cascade for these tokens (element
     + :has(), same weight as :root[data-theme="dark"] but declared later
     in the file so it wins the tie), means a desktop visitor sees the same
     bright content column regardless of OS or in-app theme setting — only
     the sidebar itself, already a fixed near-black in every mode, doesn't
     change. Dark mode remains fully intact below 960px. */
  /* html, not just body: several pages cap body's width, and past that
     edge the canvas showed the UA's white — a visible seam at 1180px. */
  html:has(nav.app-sidebar) { background: #fbf7f0; }
  body:has(nav.app-sidebar) {
    --bg: #fbf7f0; --surface: #ffffff; --surface-2: #f0e9dc;
    --text: #191714; --text-muted: #5f665b; --border: #e5dfd3;
    --accent: #b4533a; --accent-text: #ffffff; --accent-soft: #faf5f3; --accent-deep: #632e20;
    --moss: #0c6b54; --moss-soft: #e7f0ee; --red: #b91c1c; --red-soft: #fbe4e4;
  }
  nav.app-sidebar {
    display: flex; flex-direction: column; gap: var(--sp-2);
    position: fixed; top: 0; left: 0; bottom: 0; width: 268px; /* 248 + 20 (2026-09-21) so the account name has room */
    background: #141311; color: #fff;
    padding: var(--sp-4) var(--sp-3); box-sizing: border-box;
    z-index: 10;
    /* The generic `nav {}` pill rule above (the retired top nav's shape)
       still matches this element by tag — the first build rendered the
       sidebar with 100px rounded ends, a shadow and a sticky top offset.
       Every one of those is reset here explicitly, the same way nav.tabs
       has to. */
    border-radius: 0; box-shadow: none; border: 0; margin: 0;
    max-width: none; overflow: hidden auto; transform: none;
  }
  /* margin-left, not padding: box-sizing is border-box everywhere in this
     file, so padding here would eat into the existing max-width instead of
     shifting the box — a 560px-capped page would lose 248px of its own
     content width instead of moving. margin-left alone (leaving whatever
     margin-right each page already has, auto or 0) pins the box's left
     edge just past the sidebar and lets width resolve exactly as it did
     before on every page, sidebar or not. */
  body:has(nav.app-sidebar) { margin-left: 268px; }
  /* Forms and articles on desktop (2026-09-20 review item 7): the phone's
     560px column sat beside 700px of empty paper. 800px incl. padding is a
     comfortable measure for inputs, plan cards and a changelog; the screens
     with their own desktop layout (Today, Learn, Library, Settings, Report,
     the chat) keep theirs. */
  body:has(nav.app-sidebar):not(:has(main.today, main.learn, main.stuck, main.convo, main.report, main.admin, .lib-top, .set-grid)) {
    max-width: 800px; padding: 2.4rem 3rem 3rem;
  }
  /* Two desktop widths, not six (2026-09-21, review item 7): 1180 for the
     wide screens above, 800 for everything else — the learn card, the
     stuck screen and the editors included, so "Fix this card" no longer
     jumps the page sideways. */
  /* The learn loop is the one screen that stays centred on desktop — a
     stage, not a page — and the stuck screen it opens shares its column,
     so "Get help" does not shift the page. Every other screen is
     left-aligned at 800 or 1180. */
  body:has(nav.app-sidebar) main.learn { max-width: 660px; margin: 0 auto; }
  body:has(nav.app-sidebar):has(main.stuck) { max-width: none; padding: 2.4rem 3rem 3rem; }
  body:has(nav.app-sidebar) main.stuck { max-width: 660px; margin: 0 auto; padding-inline: var(--sp-4); }
  /* The card and deck editors cap themselves at the phone measure inside;
     on desktop they follow the body. */
  body:has(nav.app-sidebar) main.card-screen, body:has(nav.app-sidebar) main.deck, body:has(nav.app-sidebar) main.deck .slot-in { max-width: none; }
  .sidebar-brand {
    display: flex; align-items: center; gap: .6rem; color: #fff; text-decoration: none;
    font-weight: var(--w-max); font-size: 1.05rem; padding: 0 var(--sp-2); margin-bottom: var(--sp-4);
  }
  .sidebar-links { display: flex; flex-direction: column; gap: .15rem; }
  .sidebar-link {
    display: flex; align-items: center; gap: .7rem; padding: .65rem var(--sp-2);
    border-radius: var(--radius-sm); color: rgba(255,255,255,.68); text-decoration: none;
    font-size: var(--t-body); font-weight: var(--w-strong); min-height: 44px;
  }
  /* Text stays white on .on, never var(--accent) directly: #b4533a on this
     sidebar's near-black measures under AA for normal text (the mockup
     avoids this the same way — only the icon takes the accent, the label
     stays white). */
  .sidebar-link.on { background: rgba(180,83,58,.16); color: #fff; }
  .sidebar-link.on svg { color: var(--accent); }
  .sidebar-link:hover { color: #fff; }
  .sidebar-acct {
    margin-top: auto; display: flex; align-items: center; gap: .65rem; min-width: 0; max-width: 100%;
    padding: var(--sp-2); border-radius: var(--radius-sm); color: #fff; text-decoration: none;
  }
  .sidebar-acct:hover { background: rgba(255,255,255,.06); }
  .sidebar-avatar {
    flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%;
    background: #f3ede5; color: #191714; font-weight: var(--w-max); font-size: var(--t-small); /* paper on the dark sidebar; the palette there is pinned light, so var(--text) would vanish */
    display: flex; align-items: center; justify-content: center;
  }
  .sidebar-who { flex: 1 1 auto; min-width: 0; overflow: hidden; display: flex; flex-direction: column; }
  .sidebar-who b { font-size: var(--t-small); font-weight: var(--w-strong); display: flex; align-items: center; gap: .35rem; min-width: 0; }
  .sidebar-who b .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .sidebar-who b .tier-pill { flex: 0 0 auto; margin-left: 0; }
  .sidebar-who span { font-size: .72rem; color: rgba(255,255,255,.56); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* ── Desktop content reflow (2026-09-20) ──────────────────────────────────
   Per-screen desktop compositions ported from the app-desktop mockup onto
   the real templates. Same markup as mobile; only these rules change past
   the breakpoint. Every block is scoped to the screen's own root, and the
   Today block additionally to body:has(nav.app-sidebar) — the day-one
   picker renders no nav at all (see today.html) and keeps its centred
   one-question layout on any width.

   Deliberately NOT ported: the mockup's "This week" stat tiles on Report
   (cards reviewed / conversations / tripping-up count). The real handler
   computes none of those; the grid below holds the real per-language
   mastery only, and Tempo/Korpus follow it as a readable column. */
@media (min-width: 960px) {
  /* Shared desktop type: the page identity reads as a title, not an eyebrow. */
  body:has(nav.app-sidebar) .w .title,
  body:has(nav.app-sidebar) .lib-eyebrow,
  body:has(nav.app-sidebar) main.report > .report-title,
  body:has(nav.app-sidebar) .set-title,
  body:has(nav.app-sidebar) .lib-title,
  body:has(nav.app-sidebar) .ctx-lang {
    font-family: 'Bricolage Grotesque', Manrope, "Segoe UI", sans-serif;
    font-size: 1.7rem; font-weight: var(--w-max); letter-spacing: -.01em;
    text-transform: none; color: var(--text); line-height: 1.1;
  }

  /* ---- Today: hero | quick stack | this week ---- */
  body:has(nav.app-sidebar) main.today {
    max-width: 1180px; margin: 0; padding: 2.4rem 3rem 2rem;
    display: grid; column-gap: 22px; row-gap: 0;
    grid-template-columns: 1.5fr .85fr 1fr;
    /* Row 4 (the week list) is the one flexible track: hero and quick stack
       span rows 3-4, and with all-auto rows the grid split their height
       across both, pushing the list down to the second quick card. A
       flexible track absorbs the spanning items' height instead, so the
       label row stays label-high and the list starts beside the hero's top. */
    grid-template-rows: auto 1fr auto auto auto auto;
    /* The main grows to the viewport and the hero row is the 1fr track, so
       the hero, the quick stack and the week card run down to the closing
       line — the same rule as the learn card. */
    flex: 1 1 auto;
    grid-template-areas:
      "ctx    ctx    ctx"
      "hero   quick  week"
      "voice  voice  voice"
      "langsa langsa langsa"
      "langs  langs  langs"
      "alt    alt    alt";
    align-items: stretch; align-content: stretch;
  }
  body:has(nav.app-sidebar) main.today .ctx, body:has(nav.app-sidebar) main.today .greet-row { grid-area: ctx; align-items: center; margin-bottom: .9rem; }
  body:has(nav.app-sidebar) main.today .greet { font-size: 1.7rem; }
  body:has(nav.app-sidebar) main.today .ctx-stat { white-space: nowrap; }
  body:has(nav.app-sidebar) main.today .rec { grid-area: hero; margin-top: 0; }
  body:has(nav.app-sidebar) main.today .rec.scene {
    min-height: 420px; padding: 32px 30px 30px; justify-content: flex-end;
    background: linear-gradient(155deg, #1d1b17 0%, #141311 65%);
    border-color: transparent; color: #fff; position: relative; overflow: hidden;
  }
  body:has(nav.app-sidebar) main.today .rec.scene::after {
    content: ""; position: absolute; right: -50px; top: -50px; width: 260px; height: 260px;
    border-radius: 50%; pointer-events: none;
    background: radial-gradient(circle, rgba(180,83,58,.45), transparent 70%);
  }
  body:has(nav.app-sidebar) main.today .rec.scene > * { position: relative; }
  body:has(nav.app-sidebar) main.today .rec.scene .rec-eyebrow { color: #e3a18f; }
  /* padding-right, not max-width, to keep the measure short: max-width on
     this stretched flex item made Chrome size the box for one line and
     overflow the second straight through the rule below it. */
  body:has(nav.app-sidebar) main.today .rec.scene .utter-line { font-size: 2.1rem; padding-right: 14%; }
  body:has(nav.app-sidebar) main.today .rec.scene .utter-rule { opacity: .18; width: 100%; align-self: stretch; margin: .9rem 0; }
  body:has(nav.app-sidebar) main.today .rec.scene .utter-cue { color: rgba(255,255,255,.62); opacity: 1; }
  body:has(nav.app-sidebar) main.today .rec.scene .rec-btn {
    background: var(--accent); color: #fff; align-self: flex-start; width: auto; min-width: 220px; padding-left: 26px; padding-right: 26px;
  }
  /* The two quick cards keep their own height (equal to each other, not to
     the hero): the stack sits at the top of its track and the hero and the
     week card are the ones that stretch. */
  body:has(nav.app-sidebar) main.today .quick-row { grid-area: quick; grid-template-columns: 1fr; grid-auto-rows: 1fr; gap: 22px; margin-top: 0; align-self: start; }
  body:has(nav.app-sidebar) main.today .quick-card { padding: 24px 22px; gap: 14px; justify-content: center; }
  body:has(nav.app-sidebar) main.today .quick-ic { width: 42px; height: 42px; border-radius: 12px; }
  body:has(nav.app-sidebar) main.today .quick-card b { font-size: 1rem; }
  body:has(nav.app-sidebar) main.today .week-card { grid-area: week; margin-top: 0; padding: 24px 24px 12px; }
  body:has(nav.app-sidebar) main.today .voice-card { grid-area: voice; display: block; margin-top: 22px; }
  body:has(nav.app-sidebar) main.today .voice-card .voice-mod.together { margin: 0; gap: 28px; padding: 22px 28px; }
  body:has(nav.app-sidebar) main.today .voice-card .vessel { width: 124px; height: 131px; }
  body:has(nav.app-sidebar) main.today .voice-card .together-txt { flex-direction: row; align-items: center; gap: 28px; flex: 1 1 auto; }
  body:has(nav.app-sidebar) main.today .voice-card h2 { font-size: 1.15rem; flex: 0 0 200px; }
  body:has(nav.app-sidebar) main.today .voice-card .voice-reward { flex: 1.3 1 0; }
  body:has(nav.app-sidebar) main.today .lang-all { grid-area: langsa; margin-top: var(--sp-6); }
  body:has(nav.app-sidebar) main.today .lang-rows { grid-area: langs; }
  body:has(nav.app-sidebar) main.today .alt { grid-area: alt; }

  /* ---- Library: one table per language ---- */
  body:has(.lib-top) { max-width: 1180px; padding: 2.4rem 3rem 3rem; }
  body:has(main.admin) { max-width: 1180px; padding: 2.4rem 3rem 3rem; }
  body:has(nav.app-sidebar) .lib-top { padding: 0 0 22px; }
  body:has(nav.app-sidebar) .lang-label { margin: 18px 0 8px; font-size: var(--t-small); }
  body:has(nav.app-sidebar) .lang-total { white-space: nowrap; }
  body:has(nav.app-sidebar) .deck-list {
    background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius);
    overflow: hidden; gap: 0; padding: 6px 0;
  }
  body:has(nav.app-sidebar) .deck-head {
    display: grid; grid-template-columns: 44px 1fr 1.1fr 44px; gap: 14px;
    padding: 8px 0 12px 24px; align-items: center;
  }
  body:has(nav.app-sidebar) .deck-head span {
    font-size: .7rem; font-weight: var(--w-max); letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted);
  }
  body:has(nav.app-sidebar) .deck-row { background: none; border: 0; border-radius: 0; border-top: 1px solid var(--border); }
  body:has(nav.app-sidebar) .deck-row.lead { border-color: var(--border); box-shadow: inset 3px 0 0 var(--accent); }
  body:has(nav.app-sidebar) .deck-link {
    display: grid; grid-template-columns: 44px 1fr 1.1fr; gap: 14px;
    padding: 16px 0 16px 24px; align-items: center;
  }
  body:has(nav.app-sidebar) .deck-badge { width: 44px; height: 44px; border-radius: 13px; grid-column: 1; }
  /* One number per row (2026-09-20 review item 9): name, then today's state.
     Card and mastered counts live on the deck screen and on Progress. The
     name wrapper dissolves so name and state each take a column. */
  body:has(nav.app-sidebar) .deck-main { display: contents; }
  body:has(nav.app-sidebar) .deck-name { grid-column: 2; font-size: 1rem; }
  body:has(nav.app-sidebar) .deck-state { grid-column: 3; font-size: var(--t-small); }
  body:has(nav.app-sidebar) .deck-badge, body:has(nav.app-sidebar) .deck-name, body:has(nav.app-sidebar) .deck-state { grid-row: 1; }

  /* ---- Report: mastery three-up, then Tempo/Korpus as a reading column ---- */
  body:has(main.report) { max-width: 1180px; padding: 2.4rem 3rem 3rem; }
  /* Two columns: mastery + week on the left, pace + sentences on the right
     (2026-09-20 review item 7 — the right half was empty from Pace down). */
  main.report { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 32px; align-items: start; }
  main.report > .report-title { grid-column: 1 / -1; }
  main.report .rep-left, main.report .rep-right { min-width: 0; }
  main.report > .report-title { margin: 0 0 22px; }
  main.report .subjects { margin-bottom: 20px; }
  main.report .mastery-grid { display: grid; grid-template-columns: 1fr; gap: 20px; margin: 0 0 26px; } /* one per row inside the left column */
  main.report .mastery-grid:has(.mastery-card:nth-child(2)) { grid-template-columns: 1fr 1fr; }
  main.report .rep-right .subjects { margin: 0 0 22px; }
  main.report .mastery-card { padding: 24px; }
  main.report .week-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; background: none; border: 0; overflow: visible; max-width: none; }
  main.report .week-stat { flex-direction: column; align-items: flex-start; gap: 10px; padding: 22px 24px; background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius); }
  main.report .week-stat span { color: var(--text-muted); }
  main.report .week-stat b { font-size: 1.9rem; }
  main.report .mastery-card .num-row b { font-family: 'Bricolage Grotesque', Manrope, "Segoe UI", sans-serif; font-size: 2.2rem; }
  main.report > :not(.mastery-grid):not(.subjects):not(.report-title):not(.week-stats) { max-width: 640px; }

  /* ---- Settings: account card, then plan | language two-up ---- */
  body:has(.set-grid) { max-width: 1180px; padding: 2.4rem 3rem 3rem; }
  body:has(nav.app-sidebar) .acct { padding: 22px 24px; margin: 0 0 26px; }
  body:has(nav.app-sidebar) .acct-avatar { width: 58px; height: 58px; font-size: 1.3rem; }
  .set-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; align-items: start; }
  .set-grid .month .v, .set-grid .month .k { white-space: nowrap; }
  .set-col > .lbl:first-child { margin-top: 0; }
  /* The theme switch can't act on desktop — the shell is pinned light above —
     so the control isn't shown rather than shown doing nothing. */
  body:has(nav.app-sidebar) .lbl:has(+ .chips #theme-light),
  body:has(nav.app-sidebar) .set-row:has(#theme-light),
  body:has(nav.app-sidebar) .chips:has(#theme-light) { display: none; }


  /* ---- Learn: the card, centred at a comfortable reading width ---- */
  main.learn { padding-top: 2.4rem; }
  main.learn .card-line { font-size: 2.1rem; }
  main.learn .grades { gap: 16px; }

  /* ---- Rabbit hole: conversation column + sticky context panel ---- */
  body:has(main.convo) { max-width: none; padding: 2.4rem 3rem 3rem; }
  main.convo {
    display: grid; grid-template-columns: minmax(0, 560px) minmax(260px, 1fr);
    gap: 32px; align-items: start; max-width: 1180px;
  }
  main.convo .convo-main { grid-column: 1; grid-row: 1; min-width: 0; }
  main.convo .convo-side {
    grid-column: 2; grid-row: 1; position: sticky; top: 2.4rem;
    background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius); padding: 24px;
  }
  main.convo .convo-side .title { font-size: 1.35rem; }
  main.convo .convo-side .subline { margin-bottom: var(--sp-3); }
  main.convo .convo-side .hint { margin-bottom: 0 !important; }
}

/* ── Shared components lifted out of single pages (2026-09-20 audit) ─────
   One back link, one plan card, one pill button family, the auth shell and
   the stuck-card screen — so the pages the mockups never drew stop each
   inventing their own. */
.w .back { display: inline-flex; align-items: center; gap: .3rem; color: var(--text-muted); font-weight: var(--w-strong);
           font-size: var(--t-small); text-decoration: none; min-height: 44px; margin-left: -.35rem; }
.w .back svg { width: 20px; height: 20px; flex: 0 0 auto; }
.w .back:hover { color: var(--text); opacity: 1; }
.w .page-title { font-family: 'Bricolage Grotesque', Manrope, "Segoe UI", sans-serif; font-size: 1.45rem; font-weight: var(--w-max);
                 letter-spacing: -.02em; line-height: 1.1; margin: .2rem 0 var(--sp-4); -webkit-text-stroke: 0; color: var(--text); }

.s-btn { display: inline-flex; align-items: center; justify-content: center; gap: .4rem; min-height: 46px; padding: 0 1.3rem;
         border-radius: var(--radius-pill); font: inherit; font-weight: var(--w-max); font-size: var(--t-action); text-decoration: none;
         border: 1.5px solid transparent; white-space: nowrap; cursor: pointer; }
.s-btn.primary { background: var(--accent); color: var(--accent-text); }
.s-btn.ghost { background: transparent; color: var(--text); border-color: var(--border); }
.s-btn.line { background: transparent; color: var(--accent); border-color: var(--accent); }
.s-tag { display: inline-block; font-size: var(--t-label); font-weight: var(--w-max); letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); background: var(--surface-2); padding: .4rem .7rem; border-radius: var(--radius-pill); white-space: nowrap; }
.s-tag.coral { color: var(--accent); background: var(--accent-soft); }
.s-plans { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-4); max-width: 900px; margin-inline: auto; } /* centred in a wide column (2026-09-22) */
@media (max-width: 700px) { .s-plans { grid-template-columns: 1fr; } }
.s-plan { min-width: 0; display: flex; flex-direction: column; gap: var(--sp-3); background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius); padding: var(--sp-5); }
.s-plan.hot { border-color: var(--accent); }
.s-plan .head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2) var(--sp-3); flex-wrap: wrap; }
.s-price-big { font-family: 'Bricolage Grotesque', Manrope, sans-serif; font-size: clamp(2.2rem, 4vw, 3rem); font-weight: var(--w-max); line-height: 1; letter-spacing: -.02em; }
.s-price-cad { color: var(--text-muted); font-size: var(--t-small); font-weight: var(--w-strong); margin: 0; }
.s-plan ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .6rem; }
.s-plan li { display: flex; gap: .6rem; align-items: flex-start; font-size: var(--t-body); line-height: 1.45; }
.s-plan li::before { content: "✓"; color: var(--moss); font-weight: var(--w-max); flex: 0 0 auto; }
.s-plan li.no { color: var(--text-muted); }
.s-plan li.no::before { content: "✕"; color: var(--text-muted); }
.s-plan .s-btn, .s-plan form { margin: 0; }
.s-plan form:first-of-type { margin-top: auto; }
.s-plan form .s-btn { width: 100%; }
.s-plan > .s-btn { margin-top: auto; align-self: flex-start; }

/* Auth screens (login, sign up, forgot, reset): the concept's split shell. */
body.auth { max-width: none; padding: 0; margin: 0; }
.auth-shell { display: grid; grid-template-columns: 1.1fr 1fr; min-height: 100svh; }
.auth-visual { background: #141311; color: #fff; padding: clamp(2rem, 4vw, 3.5rem); display: flex; flex-direction: column; justify-content: space-between; position: relative; overflow: hidden; }
.auth-visual .drift { position: absolute; inset: 0; pointer-events: none; }
.auth-visual .drift span { position: absolute; font-family: 'Bricolage Grotesque', Manrope, sans-serif; font-weight: var(--w-max); font-size: 2.4rem; color: rgba(255,255,255,.07); white-space: nowrap; letter-spacing: -.02em; }
.auth-brand { display: inline-flex; align-items: center; gap: .6rem; color: #fff; text-decoration: none; font-family: 'Bricolage Grotesque', Manrope, sans-serif; font-weight: var(--w-max); font-size: 1.25rem; letter-spacing: -.02em; position: relative; }
.auth-brand img { border-radius: 8px; }
.auth-visual h2 { font-family: 'Bricolage Grotesque', Manrope, sans-serif; font-weight: var(--w-max); font-size: clamp(1.8rem, 3vw, 2.5rem); letter-spacing: -.02em; line-height: 1.1; margin: 0 0 .8rem; max-width: 18ch; position: relative; text-wrap: balance; }
.auth-visual p { color: rgba(255,255,255,.7); max-width: 40ch; margin: 0; position: relative; line-height: 1.55; font-size: var(--t-body); }
.auth-form { display: flex; align-items: center; justify-content: center; padding: clamp(1.5rem, 4vw, 3rem) clamp(1rem, 4vw, 2.5rem); }
.auth-inner { width: 100%; max-width: 420px; }
.auth-mobile-brand { display: none; align-items: center; gap: .6rem; color: var(--text); text-decoration: none; font-family: 'Bricolage Grotesque', Manrope, sans-serif; font-weight: var(--w-max); font-size: 1.2rem; letter-spacing: -.02em; margin: 0 0 var(--sp-4); }
.auth-mobile-brand img { border-radius: 8px; }
.auth-inner .title { margin: .3rem 0 .5rem; }
.auth-inner .sub { margin: 0 0 var(--sp-4); }
.auth-inner .sub a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; font-weight: var(--w-strong); }
.auth-inner form label { display: block; font-size: var(--t-label); font-weight: var(--w-max); letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted); margin: 0 0 var(--sp-2); }
.auth-inner form input { width: 100%; font: inherit; font-size: var(--t-action); color: var(--text); background: var(--surface); border: 1.5px solid var(--border);
                         border-radius: var(--radius-sm); padding: .85rem var(--sp-3); margin: 0 0 var(--sp-4); box-sizing: border-box; }

.auth-inner .google { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); width: 100%; min-height: 48px; font-size: var(--t-action); font-weight: var(--w-strong);
                      color: var(--text); background: transparent; border: 1.5px solid var(--text); border-radius: var(--radius-pill); text-decoration: none; }
.auth-inner .google:hover { background: var(--text); color: var(--bg); opacity: 1; }
.auth-divider { display: flex; align-items: center; gap: .8rem; margin: var(--sp-4) 0; color: var(--text-muted); font-size: var(--t-label); font-weight: var(--w-max); text-transform: uppercase; letter-spacing: .08em; }
.auth-divider::before, .auth-divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.auth-inner .instead { margin-top: var(--sp-4); }
.auth-foot { margin-top: var(--sp-5); display: flex; gap: var(--sp-4); flex-wrap: wrap; justify-content: center; font-size: var(--t-label); font-weight: var(--w-strong); }
.auth-foot a { color: var(--text-muted); text-decoration: none; min-height: 44px; display: inline-flex; align-items: center; }
@media (max-width: 860px) {
  .auth-shell { grid-template-columns: 1fr; min-height: auto; }
  .auth-visual { display: none; }
  .auth-mobile-brand { display: inline-flex; }
  .auth-form { align-items: flex-start; padding-top: var(--sp-5); }
}

/* Stuck-card help (ISC-157, mockup screen 7). */
.stuck { max-width: 560px; }
/* Public help/data pages wear the site shell (nav + footer, body.site
   resets the 560px body) — the article itself keeps the app's measure. */
/* Public surfaces — the marketing site, the auth screens and the signed-out
   Help/Data pages — are always the paper the mockups drew, whatever the
   visitor's OS theme. Only the signed-in app follows the theme setting
   (2026-09-20, principal: "it is important that all pages are correct.
   otherwise people lose trust"). Same token set the desktop shell pins. */
body.site, body.auth {
  color-scheme: light;
  --bg: #fbf7f0; --surface: #ffffff; --surface-2: #f0e9dc;
  --text: #191714; --text-muted: #5f665b; --border: #e5dfd3;
  --accent: #b4533a; --accent-text: #ffffff; --accent-soft: #faf5f3; --accent-deep: #632e20;
  --moss: #0c6b54; --moss-soft: #e7f0ee; --red: #b91c1c; --red-soft: #fbe4e4;
}
/* The public article pages take the site's header scale, so Help and Data
   read as pages of the site, not app screens dropped into it. */
body.site > main.article { max-width: 760px; margin: 0 auto; padding: clamp(2rem, 4vw, 3.5rem) clamp(16px, 4vw, 40px) 4rem; }
body.site > main.article .page-title { font-size: clamp(2rem, 4vw, 3rem); line-height: 1.05; letter-spacing: -.02em; margin: .2rem 0 .8rem; }
body.site > main.article .sub { font-size: 1.15rem; line-height: 1.55; max-width: 34rem; } /* .article: only the public Help/Data pages — an unscoped body.site > main squeezed the whole marketing site to 560px (2026-09-20, principal: "it is so small") */
body.site > main.article p { margin-block: 0 var(--sp-3); } /* site.css zeroes every p for the marketing sections; the article keeps its rhythm */
.stuck-flag { display: inline-flex; align-items: center; gap: .4rem; background: var(--accent-soft); color: var(--accent-deep); font-size: var(--t-small); font-weight: var(--w-max);
              padding: .45rem .9rem; border-radius: var(--radius-pill); margin: var(--sp-2) 0 var(--sp-3); }
.stuck-card { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius); padding: var(--sp-5) var(--sp-4); text-align: center; margin-bottom: var(--sp-4); }
.stuck-card .from { font-size: var(--t-label); font-weight: var(--w-strong); color: var(--text-muted); margin: 0 0 .6rem; }
.stuck-card .sentence { font-family: 'Bricolage Grotesque', Manrope, sans-serif; font-size: 1.45rem; font-weight: var(--w-max); line-height: 1.3; letter-spacing: -.015em; margin: 0 0 .5rem; }
.stuck-card .gloss { color: var(--text-muted); font-size: var(--t-body); margin: 0; }
.stuck-actions { display: flex; flex-direction: column; gap: .6rem; }
.stuck-action { display: flex; align-items: center; gap: var(--sp-3); padding: .95rem 1rem; background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius-sm); color: var(--text); text-decoration: none; }
.stuck-action .ic { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 11px; background: var(--surface-2); color: var(--text); display: flex; align-items: center; justify-content: center; }
.stuck-action b { display: block; font-size: var(--t-body); font-weight: var(--w-max); }
.stuck-action span { display: block; font-size: var(--t-small); color: var(--text-muted); }
.stuck-note { font-size: var(--t-small); color: var(--text-muted); text-align: center; margin: var(--sp-4) 0 0; }

/* Help page demo: the three steps, one per row, drawn with the real card and grade components. */
.demo { display: grid; grid-template-columns: 1fr; gap: var(--sp-4); margin: var(--sp-4) 0 var(--sp-5); }
.demo-step { display: flex; flex-direction: column; gap: .6rem; }
.demo-step .step { font-size: var(--t-label); font-weight: var(--w-max); letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.demo-card { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius-sm); padding: 2.4rem 1.2rem 1.4rem; min-height: 150px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: .45rem; position: relative; }
.demo-card .tag { position: absolute; top: .75rem; left: .9rem; font-size: .62rem; font-weight: var(--w-max); letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); }
.demo-card .line { font-family: 'Bricolage Grotesque', Manrope, sans-serif; font-weight: var(--w-max); font-size: 1.2rem; line-height: 1.25; }
.demo-card .hint { font-size: var(--t-small); color: var(--text-muted); }
.demo-grades { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; width: 100%; }
.demo-grade { border-radius: 18px; padding: .8rem .4rem; display: flex; flex-direction: column; align-items: center; gap: .1rem; font-weight: var(--w-max); font-size: var(--t-action); white-space: nowrap; min-width: 0; }
.demo-grade small { font-size: .7rem; font-weight: var(--w-strong); opacity: .75; }
.demo-grade.forgot { background: var(--accent-soft); color: var(--accent-deep); }
.demo-grade.knew { background: var(--moss); color: #fff; }

/* Today's opener-failure retry — matches the conversation view's own
   rate-limit retry button rather than inventing a third button shape. */
.utter-retry {
  margin-top: .7rem; padding: .5rem .9rem; border: none;
  border-radius: var(--radius-pill); background: var(--accent-text);
  color: var(--accent); font-weight: 700; font-size: .82rem;
  cursor: pointer; font-family: inherit;
}

/* ── promoted from per-page <style> blocks ──────────────────────────────
   These rules were byte-identical across two or more templates; each
   screen had its own copy of the same layout instead of drawing on the
   layer that exists for it, and the cost showed up as navigation that
   behaved differently on different pages. These are the ones that are
   genuinely shared. */

/* The full-height column an auth or single-purpose screen needs. */
.w main.sheet { min-height: calc(100vh - 5.5rem); display: flex; flex-direction: column; }
.w .head { display: flex; justify-content: flex-end; margin-bottom: var(--sp-4); }

/* The signed-out language switch. */
.lang-switch { text-align: center; margin-top: 2rem; font-size: .78rem; }
.lang-switch a { color: var(--text-muted); font-weight: 700; margin: 0 .4rem; }
.lang-switch a.active { color: var(--accent); }

/* Chip groups: a row of labels wrapping radios or checkboxes. */
.chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.chip input { position: absolute; opacity: 0; width: 0; height: 0; }

/* Together — the community voice goal, one partial on Plans, Home and
   desktop Today (2026-09-21): a glass that fills as everyone reads aloud,
   the count beside it. The surface drifts; still under reduced motion. */
.voice-mod.together { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius); padding: var(--sp-5) var(--sp-4); display: flex; align-items: center; gap: var(--sp-4); margin: var(--sp-5) 0 0; }
.voice-mod.together .vessel { flex: 0 0 auto; width: 150px; height: 159px; }
.voice-mod.together .vessel .glass-bg { fill: var(--bg); }
.voice-mod.together .vessel .glass { fill: none; stroke: var(--text); stroke-width: 3; }
/* Water: a deep back wave, a pale middle one, the coral front with a lit
   edge, a sheen down the glass. static/vessel.js moves them. */
.voice-mod.together .vessel .liq-top { stop-color: #c2725d; }
.voice-mod.together .vessel .liq-deep { stop-color: #944430; }
.voice-mod.together .vessel .w-back { fill: var(--accent-deep); opacity: .3; }
.voice-mod.together .vessel .w-mid { fill: #d6a093; opacity: .6; }
.voice-mod.together .vessel .w-front { fill: url(#vessel-liquid); }
.voice-mod.together .vessel .w-shine { fill: none; stroke: #fff; stroke-width: 1.4; stroke-linecap: round; opacity: .5; }
.voice-mod.together .vessel .bubbles circle { fill: #fff; }
.voice-mod.together .vessel .sheen { fill: url(#vessel-sheen); opacity: .3; }
.voice-mod.together .together-txt { min-width: 0; display: flex; flex-direction: column; gap: .35rem; }
.voice-mod.together h2 { margin: 0; font-family: 'Bricolage Grotesque', Manrope, sans-serif; font-size: 1.35rem; font-weight: var(--w-max); letter-spacing: -.015em; }
.voice-mod.together .voice-reward { margin: 0; font-size: var(--t-small); color: var(--text-muted); line-height: 1.5; max-width: 60ch; }
@media (max-width: 480px) { .voice-mod.together { flex-direction: column; align-items: flex-start; } }
/* On the site: big. */
.s-together .voice-mod.together { border: 0; background: var(--surface-2); padding: clamp(2rem, 5vw, 4rem); gap: clamp(1.5rem, 4vw, 3.5rem); margin: 0; }
.s-together .voice-mod.together .vessel { width: clamp(160px, 22vw, 260px); height: auto; }
.s-together .voice-mod.together h2 { font-size: clamp(1.6rem, 3.2vw, 2.4rem); }
.s-together .voice-mod.together .voice-reward { font-size: var(--t-body); }
@media (max-width: 700px) { .s-together .voice-mod.together { flex-direction: column; align-items: center; text-align: center; } }
/* The plan cards' asterisk: three Plus bullets carry it, one line below
   the cards explains it (2026-09-21). */
/* The star sits in the sentence, not as its own flex item — as a sibling it
   was pushed to the far edge of the row and read as missing. */
.s-star { color: var(--accent); font-weight: var(--w-max); margin-left: .1em; }
/* Cards and their footnote share one centred 900px block, so the note
   lines up with the cards' left edge on every page. */
.s-plans-wrap { max-width: 900px; margin-inline: auto; }
.s-plans-wrap .s-plans { max-width: none; margin-inline: 0; }
.s-plans-wrap .s-fair { font-size: var(--t-small); color: var(--text-muted); line-height: 1.5; margin: var(--sp-4) 0 0; max-width: none; }
