/* Shared tokens + base elements for every Warren page. Page-specific
   templates keep only their own unique component styles — see html/*.html.
   Extracted 2026-09-01 to kill cross-page drift (was six copies of this
   block, out of sync — study.html's body was 460px while every other page
   was 560px).

   Redesign 2026-09-01 (register: Playful, per the frontend-design doctrine's
   closed register list — "saturated color, chunky type" — pulled from
   https://umanodesign.studio/ at the principal's explicit direction: bold
   display type, saturated color energy, floating pill nav, full redesign).
   Font loading centralized here so a future type change is a one-line edit
   instead of eight — was a literal <link> duplicated in every template.

   Self-hosted, not a fonts.googleapis.com @import (2026-09-06 fix): the
   CSP (middleware.go's cspHeader) is `style-src 'self' 'unsafe-inline'` —
   deliberately same-origin-only, and its own comment claims "no external
   script/style/image/font is loaded anywhere in this app." The @import
   this replaced violated that claim from the day it was added (2026-09-01,
   a month before the CSP's 2026-09-03 audit — never caught because a
   blocked stylesheet fails silently, with the sans-serif fallback close
   enough to Manrope that it read as fine in every visual check since).
   Two files (fonts.gstatic.com serves Manrope as a variable font — every
   weight below resolves to the same two files, latin + latin-ext) cover
   every language this app's UI or study subjects actually render in
   (en/de chrome, French/Italian/Spanish/Romansh/Turkish/Bosnian/German
   subject text) — Cyrillic/Greek/Vietnamese subsets Google also serves
   aren't needed and are skipped. No CSP change needed: same-origin stays
   exactly as tight as the header already claims. */
@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;
}

/* Root type scale (2026-09-02, principal feedback: "font sizing too small,
   whitespacing too narrow"). 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 {
  /* Fourth color pass (2026-09-01) — principal unsure between the plum
     scheme (previous pass) and sage, so trying sage for real comparison
     rather than describing it. Cream stays close to the plum pass's warm
     paper; accent is a deep forest green, not a bright/minty one. */
  --bg: #f7f4ec; --surface: #fffdf8; --surface-2: #ece4d2;
  /* --text-muted was #71786d until 2026-09-09, which failed WCAG AA (4.5:1)
     for normal-size text on EVERY ground in this palette — measured 4.48 on
     --surface, 4.14 on --bg, 3.60 on --surface-2, 3.60 on --accent-soft. It
     missed by 0.02 on the lightest ground, so it read as fine and shipped.
     This is the token behind every hint, caption, deck state, gloss and
     eyebrow in the app, so the failure was app-wide, not local to one
     screen. #5f665b is the lightest value in the same desaturated-green
     hue family that clears 4.5:1 on all four: 5.84 / 5.40 / 4.69 / 4.69.
     Near-invisible as a change; it just stops being non-compliant.
     Dark mode needs no equivalent fix — its own --text-muted (#a3ab9c,
     below) already measures 7.00 / 6.23 / 5.43 / 4.64 on the dark grounds.
     Rule this replaces: de-emphasis is carried by weight, size and border,
     never by dropping text below a contrast minimum. */
  --text: #1c231d; --text-muted: #5f665b; --border: #ddd6c2;
  /* White text on this green: 7.3:1 (comfortably passes AAA). Green is also
     readable directly on --bg at 7.0:1 for links. */
  --accent: #2f5c3f; --accent-text: #fff8f2; --accent-soft: #dde8d8;
  /* Radius scale, not ad-hoc per-component px — umano leans hard into big,
     confident rounding (and full pills for anything button-shaped), not
     Warren'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 (2026-09-09, Phase 4)
     ------------------------------------------------------------------
     Colour, radius and shadow have been tokenised since 2026-09-01;
     type and spacing never were, so every page invented its own. An
     audit of the three day-one surfaces found 21 distinct font-size
     values, with five of them ( .70 / .72 / .74 / .76 / .78rem ) doing
     one single job, and `0.8rem` written next to `.8rem` for the same
     number.

     Seven steps, each collapsing one measured cluster. 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. The audit found 13 distinct padding/gap
     values across two pages; these six are the ones that recur, and
     they are applied only where the substitution is exact. The
     leftovers (.55 / .8 / .95 / 1.05 / 1.15 / 1.35 / 1.4rem) are NOT
     snapped to the nearest step here — the day-one card's geometry was
     measured to the pixel and signed off, and rounding its padding 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;
  --moss: #4f8158; --moss-soft: #e5eee2;
  --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 the principal hasn't chosen
   explicitly; the [data-theme="dark"] block lets the in-app toggle (added
   2026-09-01 after "dark with orange is absolute hardcore, don't like")
   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: #1a201a; --surface: #232a20; --surface-2: #2c342a;
    --text: #eaf0e6; --text-muted: #a3ab9c; --border: #3c4636;
    /* Brighter sage than light mode's deep #2f5c3f — needs more luminance
       against near-black. Text-on-accent flips to dark here (#15201a,
       6.55:1) since white only manages 2.4:1 on this lighter green. */
    --accent: #8faa7c; --accent-text: #15201a; --accent-soft: #33402c;
    --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: #7cb587; --moss-soft: #2e3a2a;
    --red: #e57373; --red-soft: #3a2424;
    --amber: #e0a458; --amber-soft: #3a2f1c;
    --green: #6fcf82; --green-soft: #2a3a2c; --green-text: #8fd39c;
  }
}
:root[data-theme="dark"] {
  --bg: #1a201a; --surface: #232a20; --surface-2: #2c342a;
  --text: #eaf0e6; --text-muted: #a3ab9c; --border: #3c4636;
  --accent: #8faa7c; --accent-text: #15201a; --accent-soft: #33402c;
  --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: #7cb587; --moss-soft: #2e3a2a;
  --red: #e57373; --red-soft: #3a2424;
  --amber: #e0a458; --amber-soft: #3a2f1c;
  --green: #6fcf82; --green-soft: #2a3a2c; --green-text: #8fd39c;
}

/* ══════════════════════════════════════════════════════════════════════
   THE COMPONENT LAYER (2026-09-09).

   One definition per object, for the screens of the new design: the Learn
   loop, the deck screen, the card editor, manual add, the generate question
   and browse. Every one of these was previously defined in each template's
   own <style> block, and after a single night the copies had already drifted
   — .question was 1.5rem on two screens and 2rem on a third, .btn was a
   column on two and a row on two, .top was flex-end on four and
   space-between on one.

   That drift is not cosmetic. Five defects in one session came from the same
   object being styled twice in different files, or from a template copying a
   neighbour instead of reading the rule. This block is the fix for that
   class of defect, not for tidiness.

   SCOPED TO .w, which the six redesigned screens carry on <body>. That is not
   tidiness: the first version of this block was global, and within the hour it
   was measurably restyling /rydon — a 34.2px headline number forced
   right-aligned and muted, because rydon.html uses class="count" and defines
   no text-align or colour of its own. The same trap was waiting on
   library.html and cheatsheet.html for .count, and on all three *_stapel.html
   files, where .exit is a CONTAINER rather than a link.

   The thirteen 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. The scope is what makes that impossible rather
   than merely unintended.
   ══════════════════════════════════════════════════════════════════════ */

/* 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 (2026-09-11, 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 the library, Heute, help and about screens have no .w body yet —
   the principal's first report after shipping was the reverse-deck action
   on the library sheet (2026-09-12). */
.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-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. warren.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; warren.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 2026-09-06 to make room for the new /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; }
/* 2026-09-02 principal report: on a real phone, the nav pill (now wider
   than ever with icons+labels on every item) pushed logout off-screen
   again with no visible hint, same failure class as the two earlier
   overflow regressions this session — except this time the fix is to
   give the pill more room to work with, not to remove content from it.
   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 "Warren" 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 — the root font-size bump (2026-09-02)
   made the pill wide enough that flex children were shrinking and wrapping
   "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; }

/* 2026-09-04, 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 (2026-09-02, principal feedback: "too cramed no
   place" — display name used to live here too and caused two real overflow
   regressions before it was moved to Settings' account row for good; icons
   are fixed-width regardless of language/name length, so they don't
   reintroduce that failure mode). Icon+label rather than icon-only — a
   bare icon row was rejected per the same pass's "text too small/cluttered"
   feedback, which is about clarity, not just size. */
.nav-link { display: inline-flex; align-items: center; gap: .3rem; }
.nav-link svg { flex-shrink: 0; opacity: .8; }

/* "More" overflow menu (2026-09-08, alternative nav — cramped-row fix):
   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 warren.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; }

/* Price tag: the cost of an AI-priced control, beside it, at the moment of
   choosing (ethos rule 4). 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 for
   one deploy this rule turned those into green pills (2026-09-11,
   principal-reported) — the same collision the .count incident taught. */
.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 — 2026-09-02 principal feedback ("feels
   not enough haptic... hover, active states in the navigation"). Kept to
   transform+color (cheap, no layout thrash) and short (120ms) so it reads
   as responsive, not laggy — same instinct as the card-flip fade fix
   earlier this session. 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 — promoted 2026-09-04 after
   seven pages (login/register/reset/forgot-password, new_set, settings,
   edit_set) each hand-rolled their own copy of this and drifted (1rem vs
   1.4rem top margin, some inline-patched per instance instead of baked into
   the rule). One rule, one rhythm, applied everywhere the eye needs to move
   from a label to its field the same way every time. 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;
}

.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); }
.hero-block .subtitle { color: var(--accent-text); opacity: .78; }

/* Third display-type pass (2026-09-01) — principal's actual ask, once
   clarified: not a second display typeface at all, just Manrope (the same
   font as body text) at real weight. Simpler than either Fraunces (too
   swashy) or Unbounded (still a separate face) — one typeface for the
   whole app, headlines differentiated by weight/size alone. Dropped the
   Unbounded @import entirely, no reason to load a font nothing uses.
   Family/weight identical on every page; font-size and margin are
   deliberately not — each page sets its own in a local override. */
/* 800 is Manrope's heaviest weight — Google Fonts only ships it up to
   ExtraBold, no 900/Black instance exists to request. To go bolder than
   that without switching typefaces (the whole point of the last pass), a
   hairline stroke on top of the fill genuinely thickens the glyphs — a
   standard technique, not a hack unique to this. Android Chrome (the
   principal's browser) supports -webkit-text-stroke; the rule is a no-op
   fallback to plain 800 anywhere it isn't. */
h1 { font-family: Manrope, "Segoe UI", sans-serif; font-style: normal; font-weight: 800; letter-spacing: -.01em; -webkit-text-stroke: .5px currentColor; }

/* ---------------------------------------------------------------------
   Bottom tab bar (2026-09-08, Phase 4 / ISC-98)
   ---------------------------------------------------------------------
   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, which is exactly the four-front-doors problem growing back).

   Fixed to the bottom of the viewport (2026-09-11, principal: "ensure
   navigation is fixed bottom because otherwise on mobile will be weird" —
   reverses the position:static call below). In-flow traded the bar's
   reachability for immunity to mobile browser chrome collapsing under it,
   and that trade is the weirdness itself: the one navigation surface the
   app has scrolled away the moment you passed the first screen of Library
   or Fortschritt, 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. This
   is the cascade collision that shipped in the first pass: the tab bar
   rendered as a rounded floating pill overlapping the footer. */
nav.tabs {
  /* An island, not a band (principal, 2026-09-10, after six forms were built
     and photographed). The full-bleed band was the loudest object on an empty
     screen — 94.6% of it a surface brighter than the page — and lightening it
     changed nothing, because its height is set by the 44px tap target and not
     by its styling. A content-width pill spends the same ink on a third of
     the footprint. The shape is not new: --radius-pill and --nav-shadow are
     the retired top nav's own, and 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.

     `position: static` used to be load-bearing here against data_footer,
     a six-link row repeated at the bottom of seventeen templates that a
     fixed pill would have sat on top of and sliced in half mid-scroll.
     data_footer was retired 2026-09-10 (its contents moved to Einstellungen)
     — the thing position:fixed was guarding against no longer exists, which
     is what makes the 2026-09-11 switch to fixed safe now. 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. */
  display: flex; width: fit-content; gap: 0;
  padding: var(--sp-1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  box-shadow: var(--nav-shadow);
  /* 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). nav.tabs never used to set `top` itself, which was harmless
     under position:static — but the instant it became position:fixed,
     that inherited `top: .75rem` combined with this rule's own `bottom`
     to pin 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. That's the giant pill (measured 1180px tall via
     getBoundingClientRect — principal: "look at the navgation what the
     fuck"). 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: 50%; transform: translateX(-50%);
  bottom: max(var(--sp-1), env(safe-area-inset-bottom));
  z-index: 10; overflow: visible;
}
.tab {
  display: flex; flex-direction: row; align-items: center; justify-content: center;
  gap: var(--sp-1);
  /* 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;
  padding: 0 var(--sp-2);   /* --sp-3 makes the object 350px, which does not fit */
  border-radius: var(--radius-pill);
  font-size: var(--t-label); font-weight: var(--w-strong); letter-spacing: 0;
  color: var(--text-muted); text-decoration: none; white-space: nowrap;
}
.tab svg { display: block; width: 18px; height: 18px; stroke-width: 2; flex: none; }
/* The current place, marked without hue: a recessed capsule, darker ink,
   maximum weight, a thicker stroke. Four redundant channels, none of them
   colour — greyscale the screenshot and nothing is lost. It also returns
   --accent to the grade position, where §03 says filled green belongs. */
.tab.on { background: var(--surface-2); color: var(--text); font-weight: var(--w-max); }
.tab.on svg { stroke-width: 2.4; }
/* Optical, not geometric: the left end opens with a small round icon that
   carries its own air, while the right end closes on the last letter of a
   long German word. Equal padding therefore looks unequal, and the pill
   reads as if it is leaning right (principal, 2026-09-10). One step on the
   space scale is enough. */
.tab:last-child { padding-right: var(--sp-3); }

/* The "353px wants more than the 348px body gutter allows" width fight
   (principal, 2026-09-10: "give more width to nav so it is not so
   cramped") doesn't apply once the pill is position:fixed — a fixed box
   is placed against the viewport, not constrained by body's content box,
   so it was never actually cramped by the gutter to begin with once this
   changed. The old margin-inline/align-self reclaim hack is gone; left:50%
   + transform above is the whole centring story now.

   The old `margin-top: auto` push (pinning the pill to the bottom of a
   flex-column body) is gone too — position:fixed no longer needs a flex
   parent to reach the bottom, so body:has(nav.tabs) below is now just
   about giving scrolling content room to clear the fixed bar, plus the
   fade below it (2026-09-11, principal: "the part behind the nav should
   blend out the content when scrolling" — 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. The reserve
   was 6rem while the ::after gradient is 8rem, so the last two rem of every
   page sat inside the fade and behind the pill — a Progress line came out
   sliced in half for three testers across two rounds (2026-09-13), and one
   of them read it as a rendering bug. */
body:has(nav.tabs) main { padding-bottom: calc(9rem + env(safe-area-inset-bottom, 0px)); }
body:has(nav.tabs)::after {
  content: ""; position: fixed; left: 0; right: 0; bottom: 0;
  height: calc(8rem + 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: 0 var(--sp-2); }
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--radius-pill); }

/* 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, 2026-09-10 ────────────────────
   Sixty-nine rules were byte-identical across two or more templates. Every
   screen rebuilt over the last two days added 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 were 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; }
