/* ---------------------------------------------------------------------------
   COMPONENTS.

   No literal colour, spacing, weight or radius appears below. Everything comes
   from tokens.css, so a re-skin is a token swap rather than a rewrite.
   Enforced by scripts/check-tokens.mjs.

   Adapted from VitalTrack's stylesheet — same token shape, same neutral base,
   restructured around this project's surfaces.

   NOTE: @import must be the FIRST statement in a stylesheet. Anything above it
   makes the browser ignore it silently, which drops the entire token layer and
   renders the app unstyled with no error anywhere. That happened once.
   --------------------------------------------------------------------------- */
@import url("/tokens.css");


* { box-sizing: border-box; }

/* The canvas is painted ONCE, on the root.
   With the same fixed-attachment vignette on both html and body, the two copies
   composited over the body's box and only there — so on any page shorter than
   the window the background changed tone at the exact pixel the content ended.
   Invisible on a phone, where the daily form is always taller than the screen,
   and unmissable on a laptop. */
html {
  /* The app is dark and only dark. Without this the browser draws its OWN
     widgets light — the date picker's calendar, the select's option list, the
     checkbox, the scrollbar, the caret — so tapping the date field on the notes
     form opened a white sheet at the one time of day this is used. It is not a
     theme switch: there is no light theme to switch to. */
  color-scheme: dark;
  /* And the browser's own accent, or the checkbox on every note renders in the
     UA's default blue — the only blue in the product, on the one control whose
     state has a consequence outside this machine. */
  accent-color: var(--calm);
  background: var(--canvas);
  background-image: var(--canvas-vignette);
  background-attachment: fixed;
}
body {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading);
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--calm); color: var(--canvas); }

/* Phone first. The wide layout is the adaptation, not the other way round. */
main { max-width: var(--measure); margin: 0 auto;
  padding: var(--space-5) var(--space-4)
           calc(var(--navbar-h) + var(--space-5) + env(safe-area-inset-bottom)); }
/* Type, following VitalTrack's three registers:
     page title   — serif, medium, large, full ink
     item prompt  — serif, regular, full ink (it is the question, so it is content)
     section head — serif ITALIC, quieter ink, small (it only names the group)

   h2 was uppercase tracked sans, which is the loudest thing typography offers
   and was being spent on the word "Sleep". Italic serif says the same thing
   without raising its voice. */
h1 { font-family: var(--font-display); font-size: var(--text-display); margin: 0;
  font-weight: var(--weight-medium); line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight); }
h2 { font-family: var(--font-display); font-style: italic; font-size: var(--text-md);
     margin: 0; color: var(--ink-heading);
     font-weight: var(--weight-normal); letter-spacing: normal; }
/* Below the h2 it groups, so the ladder runs the right way down. Without a rule
   the UA sheet renders h3 BOLD SANS at 1.17em — larger than the serif h2 above
   it, on /labs, where it was the last sans heading in the app. */
h3 { font-family: var(--font-display); font-size: var(--text-base);
     font-weight: var(--weight-medium); color: var(--ink);
     margin: 0 0 var(--space-2); letter-spacing: normal; }
/* Separation only when something precedes it. A card whose first child is an h3
   gets its space from the card's own padding. */
* + h3 { margin-top: var(--space-5); }
.eyebrow { font-family: var(--font-display); font-style: italic; font-size: var(--text-md);
  color: var(--ink-heading); margin: 0 0 var(--space-1); }
.page-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-4); }
.page-sub { color: var(--ink-2); font-size: var(--text-sm); margin: 0 0 var(--space-5);
  max-width: 38ch; }
a { color: var(--accent); }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

/* The date stacks above the count rather than sitting beside it. At display
   size "Wednesday 26 November" and a tally cannot share a phone row without one
   of them shrinking, and neither should. */
.day-header { display: flex; flex-direction: column; align-items: flex-start;
  margin-bottom: var(--space-3); gap: var(--space-1); }
.completion { color: var(--ink-2); font-size: var(--text-sm);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

.item { border: 0; padding: 0; margin: 0 0 var(--stack); }

/* ── The prompt above each field ─────────────────────────────────────────────
   Selected by NAME, not by position. This rule was `.item legend, .item label`,
   and because `.opt` and `.chip` are both <label> elements inside an item, it
   also matched every option row and every chip — `padding: 0` beating
   `.opt { padding: var(--pad-row) }` and `.chip { padding: var(--pad-pill) }`
   on specificity. Every option and every chip in the daily form rendered with
   its text flush against its own edge. The tokens were right the whole time and
   never reached the element.

   Narrowing it to `.item > label` did not fix it either: `.opt-skip` — the "not
   answered" row — is itself a direct child of the fieldset, so the child
   combinator still swept it up, zeroed its padding, blockified it and broke its
   flex row. Position is the wrong thing to select on when two unrelated controls
   in the same container are both labels.

   So the prompt carries a class and the rule matches that class only. A control
   added later cannot fall into it by accident, and a prompt that forgets the
   class renders plainly rather than breaking a layout.
   scripts/check-prompt-scope.mjs holds this line. */
/* The direction of the one anchor set that runs the other way. Same words the
   charts use ("higher is worse" / "higher is better"), and shown ONLY on the
   exception — printing it on all twenty items would be reading load spent to
   say nothing, and reading load is the constraint. It is not colour: tokens.css
   is explicit that polarity is declared and never painted. */
.prompt-dir { display: block; font-family: var(--font-body); font-style: normal;
  font-size: var(--text-xs); color: var(--ink-2); margin-top: var(--space-1); }

.prompt {
  display: block; padding: 0; margin-bottom: var(--space-3);
  font-family: var(--font-display); font-size: var(--text-lg);
  font-weight: var(--weight-normal); line-height: var(--leading-snug);
  color: var(--ink); }

/* Ordinal options.

   The anchor labels are frozen and LONG — "moderate, it interferes",
   "severe, it dictates my day". Truncating one to fit is an instrument change
   and would sever the pre-treatment baseline, so the layout is built around
   the label rather than the label around the layout: full width, one per row,
   wrapping freely. INSTRUMENT.md section 1, D-005. */
.opts { display: flex; flex-direction: column; gap: var(--space-2); }
.opt { display: flex; align-items: center; gap: var(--space-3);
  padding: var(--pad-row); border: 1px solid transparent;
  border-radius: var(--r-md); background: var(--surface);
  cursor: pointer; min-height: var(--tap-min);
  transition: background .15s ease, color .15s ease; }
.opt:hover { background: var(--surface-2); }
.opt input { accent-color: var(--accent); flex: none; }
/* A SCORED row that is selected paints itself with the magnitude colour, so the
   accent-green control lands on green at step 1 and fights the tan at step 2.
   It takes the row's own ink instead. Scoped to the scored values: on a
   yes/no/not-answered row the background does not change, and there the green
   is still the affirmative signal it was chosen to be. */
.opt:has(input:is([value="0"], [value="1"], [value="2"], [value="3"]):checked) input {
  accent-color: currentColor; }
/* The mono numeral is pinned to the body line height. Left to its own metrics
   it made every scored row two pixels taller than the skip row below it, which
   is exactly the kind of drift that reads as "slightly off" without ever being
   locatable. */
.opt-score { color: var(--ink-numeral); font-family: var(--font-mono);
  font-variant-numeric: tabular-nums; width: 1ch; flex: none;
  line-height: var(--leading); }
.opt-label { font-size: var(--text-base); }
/* Set apart from the four scored options: it is a different kind of answer, not
   a fifth level — and drawn as an ABSENCE rather than as a fifth filled row.
   Every ordinal carries exactly one checked row from load, so with all five
   looking alike a scan down six screens could not tell an answered item from an
   untouched one. Outlined means nothing is recorded here; filled means
   something is. Same vocabulary the month grid uses for a blank day. */
.opt-skip { margin-top: var(--space-3); background: transparent;
  box-shadow: inset 0 0 0 1px var(--edge); }
.opt-skip:hover { background: var(--surface); }
.opt-skip .opt-label { color: var(--ink-2); }
.opt-skip:has(input:checked) { box-shadow: inset 0 0 0 var(--ring-thin) var(--ink-2); }
.opts-bool { flex-direction: row; flex-wrap: wrap; }
.opts-bool .opt { flex: 1 1 auto; }

/* Magnitude, applied to the selected step.

   POLARITY IS DECLARED BUT NEVER RENDERED. Level 3 paints identically whether
   the item is burden or inverse. Inverting the ink for a reversed item would
   make the ramp mean BAD rather than MUCH, which is precisely the signal it
   must not become. There is deliberately no [data-polarity] rule below. */
.opt:has(input:checked) { border-color: transparent; }
/* Step 0 alone carries a ring. Its fill is --mag-0, which is 1.28:1 against the
   surrounding --surface — far under the 3:1 WCAG 1.4.11 floor for a state
   indicator, and it cannot be lifted without breaking the ramp's ink pairing.
   Steps 1-3 need no ring; their fills are 7.79, 7.64 and 5.97 against the page.
   "Absent" is her most frequent answer and this is the only place the product
   renders "I said none of that" as distinct from "I did not answer". */
.opt:has(input[value="0"]:checked) { background: var(--mag-0); color: var(--mag-0-ink);
  box-shadow: inset 0 0 0 var(--ring-thin) var(--ink-2); }
.opt:has(input[value="1"]:checked) { background: var(--mag-1); color: var(--mag-1-ink); }
.opt:has(input[value="2"]:checked) { background: var(--mag-2); color: var(--mag-2-ink); }
.opt:has(input[value="3"]:checked) { background: var(--mag-3); color: var(--mag-3-ink); }
/* No alpha. Compositing --mag-N-ink at 65% over its own step took four pairs
   that pass AA (5.11–7.79:1) down to 3.04–3.89:1. The numeral already recedes by
   being a narrow mono glyph beside a sentence; it does not need to fade too. */
.opt:has(input:checked) .opt-score { color: inherit; }
.opt:has(input:checked) { font-weight: var(--weight-medium); }

textarea, input[type=number], input[type=password], input[type=text],
input[type=date], input[type=search], select {
  width: 100%; padding: var(--pad-row); font: inherit;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--edge); border-radius: var(--r-md);
  transition: border-color .15s ease; }
textarea:focus, input:focus, select:focus { border-color: var(--calm); }
textarea { resize: vertical; min-height: calc(var(--tap-min) * 1.6); display: block; }
.item-int { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
/* Flex-item margins do not collapse into the container's gap, so the prompt's
   own margin-bottom stacked on top of it and every integer field sat 24px below
   its prompt where everything else sits 12px. The gap supplies it. */
.item-int > .prompt { flex: 1 1 100%; margin-bottom: 0; }
.item-int input { flex: 0 0 var(--field-narrow); }
.unit { color: var(--ink-2); font-size: var(--text-sm); }

/* Chips. Sized by the label, not by the tap target — a pill held at 44rem-tall
   by min-height while its side padding stays small reads as cramped however
   much dead space sits above and below the word. Generous sides, and the tap
   floor is met rather than aimed at. */
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip { display: inline-flex; align-items: center; justify-content: center;
  padding: var(--pad-pill); border-radius: var(--r-pill);
  border: 1px solid transparent;
  background: var(--surface); color: var(--ink-2); cursor: pointer;
  min-height: var(--tap-min); text-align: center;
  transition: background .15s ease, color .15s ease; }
.chip:hover { background: var(--surface-2); color: var(--ink); }
/* The checkbox is visually hidden, so BOTH its focus ring and its checked mark
   have to be rebuilt on the chip. Without the first, focusing a chip drew a
   2px outline around a 1×1px input (WCAG 2.4.7); without the second, a chosen
   chip differed from an unchosen one by background and weight alone, which is
   meaning carried by colour (WCAG 1.4.1). */
.chip input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.chip:has(input:focus-visible) { outline: var(--ring) solid var(--ink);
  outline-offset: var(--focus-offset); }
.chip::before { content: ""; flex: none; width: var(--swatch); height: var(--swatch);
  margin-right: var(--space-2); border-radius: var(--r-pill);
  box-shadow: inset 0 0 0 1px currentColor; }
.chip:has(input:checked)::before { background: currentColor; }
.chip:has(input:checked) { background: var(--calm); color: var(--accent-ink);
  font-weight: var(--weight-medium); }
.chip span { font-size: var(--text-sm); line-height: var(--leading-snug); }

.block { border-top: 1px solid var(--edge); padding-top: var(--stack);
  margin-bottom: var(--stack); }
/* ── One vertical rhythm, stated once ────────────────────────────────────────
   Three intervals, and every gap in the app is one of them:

     --space-2  (8px)   a thing and its own explanation. They are one unit.
     --space-3  (12px)  a rubric and the field it names.
     --space-5  (24px)  one unit and the next.

   These were previously scoped to `.block`, so the daily form obeyed them and
   the weekly form — whose sections are plain <section> — did not: the same
   heading-and-note pair sat 8px apart on one page and 16px on the other.
   The rules key off the RELATIONSHIP now, so a new page gets the rhythm without
   having to know the class name.

   Note that adjacent vertical margins COLLAPSE to the larger of the two, so
   these are set on one side of each pair rather than added to each other. */
.block-note, .skip-note { color: var(--ink-2); font-size: var(--text-sm); }
.block-note { margin: 0 0 var(--space-5); }
/* Two notes in a row are one paragraph pair, not two units. */
.block-note:has(+ .block-note) { margin-bottom: var(--space-2); }

h2 { margin-bottom: var(--space-5); }
/* One unit and the next, at section scale. h2 has no top margin of its own —
   it takes its separation from the section boundary, so /export's three blocks
   and /labs' two do not run together. (The daily form's `.block` sections are
   never adjacent siblings — an item always sits between them — so this leaves
   its own border-and-padding rhythm alone.) */
section + section { margin-top: var(--stack); }
/* h1 carries no bottom margin of its own, so on the pages that are a title
   followed by sections it sat flush against the first one — `section + section`
   only ever separated the second from the first. Scoped to a direct child of
   <main>, so it cannot reach the h1 inside `.day-header`, `.page-head` or
   `.panel`, none of which are. */
main > h1 { margin-bottom: var(--stack); }
/* ...unless a note explains the heading, in which case they close up and the
   gap moves below the pair. */
h2:has(+ .block-note) { margin-bottom: var(--space-2); }

/* Read before starting, not after saving. Below the button it looked like a
   footnote to the button, or to whatever section happened to end above it — so
   it sits under the date, and the header's own gap moves onto it. */
.skip-note { margin: 0 0 var(--stack); }

/* A note that follows the control it explains stays attached to it, against
   the item's own --stack. Otherwise the guidance under the bleeding tri-state
   floats between the question it explains and the one it does not. */
.item + .block-note { margin-top: calc(var(--space-3) - var(--stack)); }
.conditional[hidden], .bleeding-detail[hidden] { display: none; }

.optional { border-top: 1px solid var(--edge); padding-top: var(--space-4);
  margin-bottom: var(--stack); }

/* ── Disclosures ─────────────────────────────────────────────────────────────
   `display: flex` on a <summary> removes the browser's own triangle, so both
   "Other symptoms" and "Add a note" rendered as headings that happened to do
   nothing when read and something when tapped. They are drawn as rows now — a
   surface, a chevron that turns, and on the daily form a count of what is
   inside — so the affordance is visible without being tapped. */
.disclosure > summary { list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: var(--space-3);
  min-height: var(--tap-min); padding: var(--space-2) var(--space-4);
  background: var(--surface); border-radius: var(--r-md);
  color: var(--ink-2); transition: background .15s ease, color .15s ease; }
.disclosure > summary::-webkit-details-marker { display: none; }
.disclosure > summary:hover { background: var(--surface-2); color: var(--ink); }
.disclosure[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
/* --ink-heading, not --ink: this is the section register (D-045), and at full
   ink it outranked every h2 on the page it sits on. */
.disclosure-label { font-family: var(--font-display); font-style: italic;
  font-size: var(--text-md); color: var(--ink-heading); }
.disclosure-hint { font-size: var(--text-xs); color: var(--ink-2); }
.disclosure .chev { flex: none; margin-left: auto; transition: transform .18s ease; }
.disclosure[open] .chev { transform: rotate(180deg); }
/* The revealed panel continues the summary's surface rather than floating free
   of it, so open and closed read as one object. */
.disclosure[open] > .block-note { background: var(--surface);
  margin: 0 0 var(--space-5); padding: var(--space-3) var(--space-4) var(--space-4);
  border-bottom-left-radius: var(--r-md); border-bottom-right-radius: var(--r-md); }

/* A recovered draft is PENDING, not stale: it is real input that has not reached
   the database yet. It gets the pending signal and a way to throw it away. */
.restored { color: var(--signal-pending); font-size: var(--text-sm);
  border-left: var(--rule-emphasis) solid var(--signal-pending);
  padding-left: var(--space-2); margin: 0 0 var(--stack);
  display: flex; gap: var(--space-3); align-items: baseline; flex-wrap: wrap; }
/* An inline text control inside a sentence. It cannot be 44px tall without
   pushing the sentence apart, so the HIT AREA is 44px and the ink is not:
   ::after draws an invisible box centred on the text. This is the only
   sanctioned way to cancel the touch floor, and scripts/check-tap-targets.mjs
   knows the shape — position: relative here, height: var(--tap-min) there. */
.restored button { width: auto; min-height: auto; padding: 0; position: relative;
  background: none; color: var(--signal-pending); font-size: var(--text-sm);
  text-decoration: underline; font-weight: var(--weight-normal); }
.restored button::after { content: ''; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: var(--tap-min); }

/* Degraded values never render bare. D-021. */
.degraded, .notice { color: var(--signal-stale); font-size: var(--text-sm);
  border-left: var(--rule-emphasis) solid var(--signal-stale);
  padding-left: var(--space-2); margin: 0 0 var(--space-4); }

/* Unobserved FLATTENS rather than dims. Four bars at four heights inside a
   dimmed container still assert four magnitudes; reducing opacity does not
   withdraw the claim. The ramp collapses to a single step instead. */
[data-observed="false"] { color: var(--signal-missing); }
[data-observed="false"] .opt:has(input:checked),
[data-observed="false"] .bar { background: var(--mag-0) !important; }
[data-observed="false"] .bar { height: var(--space-1) !important; }

/* While STUCK, the button cleared the bar by 12.6px (bottom 53.6 + 12 padding,
   against a 53px bar), so on a long form — the weekly scale above all, where it
   is stuck for the whole scroll — the pill and the bar read as one slab pinned
   to the bottom of the screen. 24.6px now. At the document bottom it comes to
   rest 48.6px clear, because main's 77.6px padding-bottom is all that follows.
   The top padding stays at --space-4: the whole stuck bar costs 88px of a
   667px screen and there is no need to spend more. */
.actions { position: sticky; padding: var(--space-4) 0 var(--space-5);
  bottom: calc(var(--navbar-h) + env(safe-area-inset-bottom));
  background: linear-gradient(transparent, var(--canvas) 35%); }
button { width: 100%; padding: var(--space-3); font: inherit;
  font-weight: var(--weight-bold); color: var(--accent-ink);
  background: var(--accent); border: 0; border-radius: var(--r-pill);
  cursor: pointer; min-height: var(--tap-min);
  transition: filter .15s ease, transform .12s ease; }
button:hover { filter: brightness(1.07); }
button:active { transform: scale(0.985); }
:focus-visible { outline: var(--ring) solid var(--ink); outline-offset: var(--focus-offset); }

.panel { background: var(--surface); border: 0; box-shadow: var(--lift);
  border-radius: var(--r-lg); padding: var(--space-5); margin-top: var(--space-5); }
/* ── Sign in ────────────────────────────────────────────────────────────────
   The only surface here with no data on it, which is what makes it the one
   place this product can be warm without over-asserting. It gets the page to
   itself: no nav, no chrome, one field.

   Three facts do the work, and each is true rather than cheerful — the name at
   full size, the day the instrument is ready for (from loggingDay(), so at
   00:30 it still says yesterday), and the sentence explaining why there is no
   username field. See the comment on `signInPage` in src/server.ts. */
.login { margin-top: var(--space-7); padding: var(--space-6) var(--space-5) var(--space-5); }

/* Vertically composed rather than stacked from the top. She opens this on a
   phone, in the evening, holding it one-handed — the field wants to be near
   the middle of the screen, not under the fold of a masthead that isn't there. */
body:has(.login) main { display: flex; align-items: center;
  min-height: calc(100dvh - var(--space-8)); }
body:has(.login) main > .login { width: 100%; }

.login-mark { position: relative; padding-bottom: var(--space-4);
  display: inline-block; }
/* Stacked, deliberately. On one line at this size the name overflows a 375px
   card, and the fix is not a smaller name — it is the name given two lines and
   the room it was asking for.

   The whitespace between the two spans is load-bearing: without it the
   accessible name is "DelicateWeapon", one word, which is what a screen reader
   would announce. Two block-level children swallow it visually and keep it in
   the text. */
.login-mark h1 { font-size: var(--text-wordmark); line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight); margin: 0; }
.login-mark h1 span { display: block; }

/* A rule, ruled. The gesture belongs to an instrument — a measurement being
   taken — and it is drawn in the same hairline language as every other rule in
   the app rather than being a brand flourish. It runs once, it is 600ms, and it
   cannot delay anything: the field is autofocused and accepts typing from the
   first frame. */
.login-mark::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  /* Ruled to the width of the name, not the card: `.login-mark` is
     inline-block, so the measurement is of the thing being measured. */
  height: var(--rule-emphasis); background: var(--calm); border-radius: var(--r-pill);
  transform-origin: left center; animation: rule-in 600ms var(--ease-out) both; }

@keyframes rule-in { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.login-ready { margin: var(--space-4) 0 var(--space-6); color: var(--ink-2);
  font-size: var(--text-sm); line-height: var(--leading); }
.login-ready strong { color: var(--ink); font-weight: var(--weight-normal);
  font-family: var(--font-display); font-size: var(--text-md); }

/* Last on the page and quiet about it. It answers the question a sign-in screen
   with one field always raises, and the answer happens to be the warmest true
   thing this product can say. */
.login-alone { margin: var(--space-6) 0 0; color: var(--ink-2);
  font-size: var(--text-xs); line-height: var(--leading); }

.login .error { margin: 0 0 var(--space-4); }

@media (prefers-reduced-motion: reduce) {
  .login-mark::after { animation: none; }
}
/* Field labels on a utility form. Set in the section register rather than the
   prompt register: "Date" and "Type" name a field, they are not the instrument
   asking a question.

   ONE rhythm, the same everywhere in the app: a rubric sits --space-3 above the
   field it names, and --space-5 below whatever came before. These were 4px and
   16px, which binds a label to the field ABOVE it rather than its own. */
.panel label { display: block; margin: var(--space-5) 0 var(--space-3);
  font-family: var(--font-display); font-style: italic;
  font-size: var(--text-md); color: var(--ink-heading); }
.panel > :first-child { margin-top: 0; }
.panel label.inline { margin-bottom: 0; }
.panel .block-note { margin: var(--space-3) 0 var(--space-5); }
/* The panel's rhythm covered label-to-field and field-to-label but not
   field-to-submit, so on /login the password field and the Sign in button sat
   flush. display:block lets this collapse with the note above it on /notes
   rather than doubling. */
.panel button { display: block; margin-top: var(--space-5); }
.panel label.inline { display: flex; align-items: center; gap: var(--space-2); }
.panel label.inline input { width: auto; }
.error { color: var(--signal-stale); font-size: var(--text-sm); }

.days, .notes, .threads { list-style: none; padding: 0; }
.days li, .notes li, .threads li { padding: var(--pad-row);
  border-radius: var(--r-md); background: var(--surface);
  margin-bottom: var(--space-2); transition: background .15s ease; }
.days li:hover, .notes li:hover, .threads li:hover { background: var(--surface-2); }
.meta { color: var(--ink-2); font-size: var(--text-xs);
  margin-left: var(--space-2); }
/* A thread's span is one fact and wrapped as three ("28 Jul to 5 / Aug · open").
   The name is the row; everything about it goes underneath. */
.threads li { display: grid; gap: var(--space-1); }
.threads .meta { margin-left: 0; }

/* The scale is eleven `.item` blocks now, rendered by the same `ordinalField`
   as the daily form — so it needs no styling of its own beyond the numeral
   below. The table it used to be, with its own five classes sharing not one
   selector with `.opt`, is gone: it could not fit a phone. Five 44px targets
   plus four gutters is a 252px floor, which left under 106px for eleven items
   of text that may never be shortened, so it overflowed by 149px and reaching
   "severe" scrolled the item away. D-053. The official grid survives NOWHERE —
   `/paper` is the daily sheet and has never carried the scale, and the export's
   MRS table is dates against subscale totals. D-055 corrects that claim.

   `--mrs-min-width` and `--mrs-item-width` went with it. */
.item-mrs .prompt { display: flex; gap: var(--space-3); align-items: baseline; }
.prompt-n { flex: none; font-family: var(--font-mono); font-size: var(--text-sm);
  color: var(--ink-2); font-variant-numeric: tabular-nums; }

.attribution { color: var(--ink-2); font-size: var(--text-xs);
  margin-top: var(--space-3); }

/* Notes */
.search { display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-4); }
.search button { width: auto; padding: var(--space-2) var(--space-4); }
.search .clear { font-size: var(--text-sm); }
/* The panel continues the summary's surface: square where they meet, rounded
   where they do not, so an open disclosure is one card rather than a heading
   with a card underneath it. The panel's own inset and radius are overridden to
   the summary's, or the join shows as a step at both edges. */
.new-note { margin-bottom: var(--stack); }
.new-note[open] > .panel { margin-top: 0; box-shadow: none;
  padding: var(--space-4); border-radius: 0 0 var(--r-md) var(--r-md); }
.note-head { display: flex; gap: var(--space-2); align-items: baseline; flex-wrap: wrap;
  font-size: var(--text-xs); color: var(--ink-2); }
.note-body { margin: var(--space-2) 0 var(--space-3); font-size: var(--text-sm);
  color: var(--ink); line-height: var(--leading); }

/* The three status words are chips, so the row reads as date · kind · state
   rather than as five spans of the same weight running together. The border
   does the separating; colour only agrees with the word already there. */
.note-auto, .note-resolved, .note-shared { padding: var(--pad-chip);
  border: 1px solid var(--edge-strong); border-radius: var(--r-pill); }
/* --calm at 7.79:1 on the canvas. This is the one piece of state on the page
   with a second reader, and it is the only reason to read the row twice. */
.note-shared { color: var(--calm); }
.note-thread { padding: var(--pad-chip); border-radius: var(--r-pill);
  background: var(--surface-2); min-height: var(--tap-min);
  display: inline-flex; align-items: center; }

/* Two rows. `display: flex; flex-wrap: wrap` put a select, a checkbox and two
   buttons on one line, which at 375px became three lines with Resolve alone on
   the last — a write, orphaned, at the thumb edge of the card. */
.note-actions { display: grid; gap: var(--space-3); margin-top: var(--space-4); }
.note-controls { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
.note-buttons { display: flex; gap: var(--space-2); }
/* A grid item stretches to its column whatever its own width, so the thread
   form's single button — a secondary action — rendered full-bleed and read as
   the loudest thing on the page. */
.note-actions > button { justify-self: start; }
/* `min-height: auto` cancelled the global 44px floor, which is a decision this
   project does not get to make locally: PRODUCT.md's accessibility section says
   targets clear 44px throughout, and these are the controls used at the end of
   a bad evening. The buttons stay visually small — the padding is unchanged —
   and gain the height back. */
.note-actions button { width: auto; padding: var(--space-1) var(--space-4);
  font-size: var(--text-sm); min-height: var(--tap-min); }
.note-actions select { width: auto; }
/* The whole label is the target, not the 16px box inside it. */
.note-actions .inline { display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--text-xs); min-height: var(--tap-min);
  padding-right: var(--space-2); }

/* Markers the app wrote live in their own collapsed section (see notes.ts).
   `opacity: 0.75` used to say "less important" here and took the head row from
   6.15:1 to 3.75:1 to do it — under the 4.5:1 floor, on the only line that says
   what the marker is. Position carries that now; the rule carries the rest. */
.written-by-app, .shared-list { margin-top: var(--stack); }
.shared-list { margin-top: var(--space-4); }
/* Said on the row the write changed, because the redirect anchors there — a
   banner at the top of the page is scrolled past by the thing that puts her
   where the banner is about. */
.note-saved { margin: var(--space-2) 0; font-size: var(--text-sm);
  color: var(--calm); }
.note-system { border-left: var(--rule-emphasis) solid var(--edge-strong); }
li[data-resolved="true"] .note-body { color: var(--ink-2); }

/* Charts */
.chart { margin: 0 0 var(--space-5); }
/* The caption is the card's title, and it is the same item prompt the daily
   form asks in serif — so it is set in serif here too. The meta spans beside it
   are not titles and stay in the body face. */
.chart figcaption { font-family: var(--font-display); font-size: var(--text-md);
  display: flex; gap: var(--space-2);
  flex-wrap: wrap; align-items: baseline; margin-bottom: var(--space-2); }
.chart-dir, .chart-sparse, .chart-meta { color: var(--ink-2);
  font-family: var(--font-body); font-size: var(--text-xs); }
.chart svg { width: 100%; height: auto; display: block; }
/* `.axis` was the only stroke in this block without non-scaling-stroke, so it
   was the only line whose width tracked the viewport — 0.95px on a phone and
   1.53px on a laptop, while its three siblings held. */
.chart .axis { stroke: var(--chart-axis); stroke-width: 1;
  vector-effect: non-scaling-stroke; }
.chart .series { fill: none; stroke: var(--chart-line); stroke-width: 1.5;
  vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round; }
.chart .marks circle { fill: var(--chart-line); }
.chart .marker { stroke: var(--chart-marker); stroke-width: 1;
  stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
/* Drawn, not typed: ①–⑳ are not in the shipped font subset, so they fell
   through to the system face and changed shape entirely past twenty. */
.chart .marker-flag circle { fill: var(--surface); stroke: var(--chart-marker);
  stroke-width: 1; vector-effect: non-scaling-stroke; }
.chart .marker-flag text { fill: var(--chart-marker); font-family: var(--font-body);
  font-size: 5.6px; font-variant-numeric: tabular-nums; }
/* A px length inside a viewBox is a USER UNIT, not a CSS pixel — so this is
   geometry in the same coordinate space as PAD, not typography, and it cannot
   be a rem token. At 7 it rendered at 6.63 CSS px on a phone: the smallest type
   in the app, on the chart whose whole job is telling 1 from 2 from 3. At 11.5
   it renders at 10.9. The words underneath do the work this was doing badly —
   see .chart-levels — and they are real text that honours a text-size setting.
   check-tokens.mjs allows the literal on this selector and nowhere else. */
.chart .tick { fill: var(--chart-tick); font-size: 11.5px; }
/* The commit that raised the y-axis ticks to 11.5 added these at 10 and 9 —
   two NEW text elements smaller than the one it had just condemned. */
.chart .tick-date { font-size: 11.5px; }
.chart .grid { stroke: var(--chart-grid); stroke-width: 0.5;
  vector-effect: non-scaling-stroke; }
.chart .grid-month { stroke-dasharray: 2 4; }

/* The four frozen anchors, printed in full.
   PRODUCT.md: they "may never be truncated, abbreviated or clipped to fit a
   layout. The layout is built around the label." The y-axis said 0 1 2 3 and
   the page never said what they meant — while the clinician's copy carried a
   legend the app did not have. */
.chart-levels { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3);
  margin: var(--space-3) 0 0; padding: 0 var(--space-1);
  font-size: var(--text-xs); color: var(--ink-2); }
.chart-levels b { color: var(--ink); font-family: var(--font-mono);
  font-weight: var(--weight-normal); font-variant-numeric: tabular-nums; }

/* One key per page, below the charts whose flags refer to it. */
.chart-legend { margin-top: var(--space-6); }
.chart-legend .chart-key { margin-top: var(--space-3); }

/* The marker key. The rules used to carry their identity in an SVG <title>:
   no hover on the iPhone this targets, no hover on paper, and role="img"
   pruned all thirty from the accessibility tree. */
.chart-key { list-style: none; margin: var(--space-3) 0 0; padding: 0 var(--space-1);
  font-size: var(--text-xs); color: var(--ink-2); }
.chart-key li { display: flex; gap: var(--space-2); align-items: baseline;
  margin-bottom: var(--space-1); }
/* The ring is drawn here too, so the key's mark matches the chart's. */
.chart-flag { color: var(--chart-marker); flex: none; display: inline-flex;
  align-items: center; justify-content: center;
  min-width: var(--space-5); height: var(--space-5); border-radius: var(--r-pill);
  border: 1px solid var(--chart-marker); font-variant-numeric: tabular-nums; }
.chart-key-date { color: var(--ink); flex: none;
  font-variant-numeric: tabular-nums; }

/* Off-screen but in the accessibility tree. The chart's entire non-visual
   content used to be one aria-label naming the title, the direction and a
   count. */
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  border: 0; }
.chart { background: var(--surface); border-radius: var(--r-lg);
  padding: var(--pad-card); margin-bottom: var(--space-5); }
.chart figcaption { padding: 0 var(--space-1); }
.chart-meta { padding: 0 var(--space-1); }
/* The empty chart dimmed the item's OWN NAME to 2.70:1 — below AA — by
   inheriting a figure-level rule, and dropped its window and its polarity with
   it. "Absence is data" argues the other way: an absence with no stated window
   is not a statement about the record. */
.chart-none { color: var(--ink); font-size: var(--text-sm); margin: 0 0 var(--space-2); }
.chart-meta { margin: var(--space-1) 0 0; }

/* Rides the same bar token every other bottom-anchored element uses. At a flat
   --space-6 it sat entirely inside the 53px nav, which painted over it. */
.saved { position: fixed; left: 50%; transform: translateX(-50%); z-index: 7; margin: 0;
  bottom: calc(var(--navbar-h) + var(--space-4) + env(safe-area-inset-bottom));
  background: var(--surface-2); border: 1px solid var(--edge);
  border-radius: var(--r-sm); padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm); color: var(--ink-2); }


.panel-row { background: var(--surface); border-radius: var(--r-lg);
  padding: var(--pad-card); margin-bottom: var(--space-3); }
.panel-row ul { list-style: none; padding: 0; margin: 0; }
.panel-row li { padding: var(--space-1) 0; font-size: var(--text-sm); }

/* The one passive surfacing prompt. Deliberately quiet: a rule that shouts on a
   safety-adjacent surface trains her to dismiss it, and then it protects
   nothing. No colour, no icon, no badge. */
.surfacing { border: 0; border-radius: var(--r-lg); box-shadow: var(--lift);
  padding: var(--pad-card); margin-bottom: var(--stack); background: var(--surface); }
.surfacing p { margin: 0 0 var(--space-2); font-size: var(--text-sm); }
/* This is the dismiss on the one thing the app ever raises unprompted, so it is
   the one control she presses while not looking for it. It kept its small
   padding and dropped the touch floor with it. The padding stays; the floor
   comes back. */
.surfacing button { width: auto; padding: var(--space-1) var(--space-4);
  font-size: var(--text-sm); }


/* ── The month grid ───────────────────────────────────────────────────────
   A map of what the record holds, not a report card. No streak, no reward, and
   a blank day is drawn blank rather than flagged. SCIENCE.md section 8. */
/* Pushed right rather than spaced apart, so that when a long month name
   ("September 2026" at display size) forces the arrows onto a second row they
   stay under themselves instead of jumping to the left margin. */
.cal-nav { display: flex; gap: var(--space-2); margin-left: auto; }
.cal-nav a { display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap-min); height: var(--tap-min); border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink-2); text-decoration: none;
  font-size: var(--text-lg); }
.cal-nav a:hover { background: var(--surface-2); color: var(--ink); }

/* The card gives its horizontal padding to the grid on a phone.
   Seven columns at the 44px floor need 332px. A 375px iPhone has 343px inside
   main, so the card can afford 4px a side and no more; at --pad-card it was
   giving cells 39.9px and the product commits to 44. Vertical padding is
   unaffected, and the full inset returns as soon as there is room for it.

   Below about 360px the floor cannot be met by any padding: 360 offers 328px
   against the 332 required. Cells are 42.3px there and 36.6px at 320px, full
   height either way. Stated rather than hidden. */
.cal { background: var(--surface); border-radius: var(--r-lg);
  padding: var(--pad-card) var(--space-1); box-shadow: var(--lift); }
@media (min-width: 27rem) {
  .cal { padding: var(--pad-card); }
  /* Square again once the track is wider than the floor. Above 27rem a column is
     at least 48px, so deriving the height from the width can no longer inflate
     it past its track — which is the only reason `aspect-ratio` was a problem. */
  .cal-day { aspect-ratio: 1; height: auto; min-height: var(--tap-cell); }
}
/* No aspect-ratio, and the width comes from the track.
   `aspect-ratio: 1` derived the height from the width and `min-height` then
   floored the height — so the browser inflated the WIDTH back to preserve the
   ratio and every cell overflowed its track. The 4px gap did not exist on any
   phone: cells overlapped by 0.4px at 390, 4.7px at 360, 10.4px at 320, and the
   last column punched 14px into the card's padding, so a tap near a boundary
   landed on whichever cell came later in the DOM.

   Flooring the TRACK instead (`minmax(--tap-cell, 1fr)`) just moves the overflow
   to the row: seven 44px columns and six gaps need 332px, and a 390px phone has
   318px inside the card. The width has to come from somewhere real. */
.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--space-1); width: 100%; }
.cal-head { margin-bottom: var(--space-2); }
/* --ink-2 throughout: --ink-3 is 2.93:1 on the canvas and fails AA at any size,
   which tokens.css says in its own words. The weekend variant was worse still —
   --edge-strong is a BORDER token and reads at 1.69:1 as text. It was left
   behind when --ink-3 was lifted off the line above it, which is a fix applied
   from a checklist rather than a search. The weekend now differs by weight. */
.cal-dow { text-align: center; font-size: var(--text-xs); color: var(--ink-2);
  font-weight: var(--weight-bold); letter-spacing: var(--tracking-wide); }
.cal-dow[data-weekend="true"] { font-weight: var(--weight-normal); }

.cal-day { position: relative; height: var(--tap-cell); min-width: 0; display: flex;
  flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-1); border-radius: var(--r-sm); background: var(--surface-2);
  color: var(--ink-2); text-decoration: none; font-family: var(--font-mono);
  font-size: var(--text-sm); transition: transform .12s ease;
  min-height: var(--tap-cell); }
a.cal-day:hover { transform: scale(1.06); z-index: 1; }
.cal-day[data-step="1"] { background: var(--fill-1); color: var(--fill-ink-1); }
.cal-day[data-step="2"] { background: var(--fill-2); color: var(--fill-ink-2); }
.cal-out { visibility: hidden; }
/* A gap in the record is a HOLE punched through the card, not a lighter square
   on it. Two reasons, and the second one is arithmetic: a blank day and a
   not-yet-arrived day were byte-identical declarations, so two different kinds
   of absence looked the same while behaving differently — one is a link and one
   is inert. And against --surface the first fill step only reaches 2.99:1;
   against --canvas it clears 3.25:1, which is what lets the ramp exist at all. */
.cal-blank { background: var(--canvas); box-shadow: inset 0 0 0 1px var(--edge-strong);
  color: var(--ink-2); }
/* The future is not a gap. Nothing could have been recorded, so it is drawn as
   absence rather than as an outline waiting to be filled — and it is inert, so
   it must not look tappable. */
.cal-future { background: transparent; box-shadow: none; color: var(--ink-3); }

/* The day she just recorded, marked persistently. The arrival state used to be
   byte-identical to a cold visit — the only new element was a toast reading
   "Saved", with no date, measured 371px from the cell it was confirming, gone in
   six seconds. Not a celebration: a hairline under the numeral, which says
   "this one" and nothing else. */
.cal-saved .cal-n { box-shadow: 0 var(--dot-thin) 0 currentColor; }

/* D-021, on the grid. A provisional boundary and a retrospective entry never
   render bare. */
.cal-onset-provisional::before { border-left-style: dashed; }
.cal-retro::after { content: ""; position: absolute; right: var(--space-1);
  top: var(--space-1); width: var(--dot); height: var(--dot-thin);
  background: var(--signal-stale); }
/* --calm, not --ink. The onset rule is --ink on the cell's own left edge, so a
   today ring in the same colour erased it on the one day both occur — and that
   is the mark D-001 calls the product's reason for existing. */
.cal-today { box-shadow: 0 0 0 var(--ring) var(--calm); }
.cal-onset::before { content: ""; position: absolute; inset: 0;
  border-left: var(--rule-emphasis) solid var(--ink); border-radius: var(--r-sm); }
.cal-n { line-height: 1; }
.cal-marks { display: flex; gap: var(--space-1); height: var(--dot); align-items: center; }
.cal-bleed, .cal-note, .cal-mrs { width: var(--dot); height: var(--dot); border-radius: var(--r-pill);
  display: inline-block; }
.cal-bleed { background: currentColor; }
/* Full strength. At 55% it measured 2.18-2.65:1 on every background — under the
   3:1 floor for a non-text mark, on a 1.5px bar. */
.cal-note { background: currentColor; width: var(--dot); height: var(--dot-thin);
  border-radius: 0; }
.cal-mrs { background: transparent; box-shadow: inset 0 0 0 1px currentColor; }

.cal-key { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4);
  margin-top: var(--space-4); font-size: var(--text-xs); color: var(--ink-2); }
.key-item { display: inline-flex; align-items: center; gap: var(--space-1); }
.key-swatch { width: var(--swatch); height: var(--swatch); border-radius: var(--r-xs); display: inline-block; }
.key-swatch[data-step="1"] { background: var(--fill-1); }
.key-swatch[data-step="2"] { background: var(--fill-2); }
.key-blank { width: var(--swatch); height: var(--swatch); border-radius: var(--r-xs);
  display: inline-block; background: var(--canvas);
  box-shadow: inset 0 0 0 1px var(--edge-strong); }
.key-item .cal-bleed, .key-item .cal-note { color: var(--ink-2); }
.key-onset { width: var(--swatch); height: var(--swatch); border-radius: var(--r-xs);
  display: inline-block; background: var(--surface-2);
  border-left: var(--rule-emphasis) solid var(--ink); }
.key-onset-provisional { border-left-style: dashed; }
.key-retro { width: var(--swatch); height: var(--swatch); border-radius: var(--r-xs);
  display: inline-block; background: var(--surface-2); position: relative; }
.key-retro::after { content: ""; position: absolute; right: 0; top: 0;
  width: var(--dot); height: var(--dot-thin); background: var(--signal-stale); }


/* The strip's own heading and its arrows. The window is a viewport; the arrows
   move it without touching the form, which is what makes the strip navigation
   rather than twelve simultaneous edit links. Same shape as the month grid's
   header, because two time-navigation surfaces that behave differently are the
   thing that made this one feel arbitrary. */
.weeks-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-4); margin-bottom: var(--space-3); }
.weeks-head h2 { margin: 0; }
.weeks-move { display: flex; gap: var(--space-2); flex: none; }
.weeks-move a, .weeks-move span { display: inline-flex; align-items: center;
  justify-content: center; width: var(--tap-min); height: var(--tap-min);
  border-radius: var(--r-pill); background: var(--surface); color: var(--ink-2);
  text-decoration: none; font-size: var(--text-lg); }
.weeks-move a:hover { background: var(--surface-2); color: var(--ink); }
/* The end of the record. Dimmed rather than removed, so the control does not
   move under her thumb when she reaches the current week. */
.weeks-move span { color: var(--ink-3); background: transparent; }

/* ── The week strip ───────────────────────────────────────────────────────
   Twelve weeks, each answered or blank, each a link to its own form. Same
   vocabulary as the month grid: a blank week is drawn blank rather than
   flagged, there is no streak, and nothing is called missed. */
.weeks-nav { margin: var(--space-5) 0 var(--stack); }
/* Four across, so roughly a month a row and the label has room. At six the cell
   was 44.7px against a "18 May" that measures 45 — fitting exactly is not
   fitting, and the next font metric along would have wrapped or clipped it. */
.weeks { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-2); }
.week { display: flex; align-items: center; justify-content: center;
  min-height: var(--tap-min); border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: var(--text-xs);
  text-decoration: none; transition: transform .12s ease; }
.week:hover { transform: scale(1.06); z-index: 1; }
.week[data-state="answered"] { background: var(--fill-2); color: var(--fill-ink-2); }
/* --ink-2, not --ink-3. A blank week is the cell the strip exists to make
   visible, and it was drawn in the one ink tokens.css itself describes as
   failing AA at any size — 2.93:1. The outline moves to --edge-strong so the
   state clears the 3:1 floor rather than resting on a 1.85:1 fill. */
.week[data-state="blank"] { background: transparent; color: var(--ink-2);
  box-shadow: inset 0 0 0 1px var(--edge-strong); }
/* Both states set box-shadow, so the current week has to restate the outline it
   would otherwise overwrite — an outlined current week was losing its outline to
   a rule collision that happened to look right. */
.week[aria-current="page"] { box-shadow: 0 0 0 var(--ring) var(--ink); }
.week[data-state="blank"][aria-current="page"] {
  box-shadow: 0 0 0 var(--ring) var(--ink), inset 0 0 0 1px var(--edge-strong); }
.weeks-note { color: var(--ink-2); font-size: var(--text-xs);
  margin: var(--space-3) 0 0; }

/* Segmented control for the cycle axis. Two views of the same data, so the
   switch is a control rather than navigation. */
.seg { display: inline-flex; gap: var(--space-1); padding: var(--space-1);
  background: var(--surface); border-radius: var(--r-pill);
  margin-bottom: var(--space-4); }
.seg a { display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap-min); padding: 0 var(--space-4); border-radius: var(--r-pill);
  color: var(--ink-2); text-decoration: none; font-size: var(--text-sm);
  transition: background .15s ease, color .15s ease; }
.seg a:hover { color: var(--ink); }
.seg a[aria-selected="true"] { background: var(--surface-2); color: var(--ink);
  font-weight: var(--weight-medium); }

/* Cycle-relative chart. The band is the observed range, the line the mean, and
   the strip beneath is how much sits behind each day — a two-observation mean
   and a fourteen-observation mean must not look equally confident. */
.cy-cell { fill: var(--calm); }
.cy-mean { fill: none; stroke: var(--chart-line); stroke-width: 1.5;
  stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.cy-mark { fill: var(--chart-line); }
.cy-mark.cy-thin { fill: var(--canvas); stroke: var(--chart-line); stroke-width: 1;
  vector-effect: non-scaling-stroke; }
.cy-density { fill: var(--chart-line); opacity: 0.45; }
.cy-density.cy-thin { opacity: 0.18; }
.cy-onset { stroke: var(--calm); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.cy-edge { fill: var(--calm); }
.cy-note { color: var(--ink-2); font-size: var(--text-sm); margin: 0 0 var(--stack);
  padding-left: var(--space-3); border-left: var(--rule-emphasis) solid var(--edge-strong); }


@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}
/* ── Chrome ─────────────────────────────────────────────────────────────
   Eight destinations never fit one phone row, and a scrolling row hides what
   you want behind a gesture nobody finds. Split by frequency instead: the four
   daily surfaces sit in a fixed bar at the thumb end, the rest behind a
   disclosure in the header. Nothing overflows and nothing scrolls sideways. */
/* One sticky box carries both bars. Sticking them separately meant the lower
   one needed the upper one's height as an offset, and it was given a token that
   measures something else entirely. */
.chrome { position: sticky; top: 0; z-index: 6; background: var(--canvas); }
.masthead { background: var(--canvas);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4); padding: var(--space-3) var(--space-4);
  padding-top: calc(var(--space-3) + env(safe-area-inset-top));
  border-bottom: 1px solid var(--edge); }
/* min-height, because at 24px tall it was the only interactive element in the
   application under the 44px floor — and it is the link home on every page. */
.wordmark { font-family: var(--font-display); font-size: var(--text-base);
  font-weight: var(--weight-medium); letter-spacing: var(--tracking-tight);
  color: var(--ink); text-decoration: none; white-space: nowrap;
  display: inline-flex; align-items: center; min-height: var(--tap-min); }
.wordmark:hover { color: var(--calm); }

.more { position: relative; }
.more summary { list-style: none; cursor: pointer; display: inline-flex;
  align-items: center; gap: var(--space-2); min-height: var(--tap-min);
  padding: var(--space-2) var(--space-3); border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink-2); font-size: var(--text-sm);
  transition: background .15s ease, color .15s ease; }
.more summary::-webkit-details-marker { display: none; }
.more summary:hover { background: var(--surface-2); color: var(--ink); }
.more[open] summary { background: var(--surface-2); color: var(--ink); }
.more[data-current="true"] summary { color: var(--ink); font-weight: var(--weight-medium); }
.more[open] summary svg { transform: rotate(180deg); }
.more summary svg { transition: transform .18s ease; }
.more-nav { position: absolute; right: 0; top: calc(100% + var(--space-2));
  min-width: var(--menu-min); background: var(--surface-2); border-radius: var(--r-md);
  box-shadow: var(--lift); padding: var(--space-2); display: flex;
  flex-direction: column; z-index: 10; }
.more-nav a { display: flex; align-items: center; min-height: var(--tap-min);
  padding: 0 var(--space-3); border-radius: var(--r-sm); color: var(--ink-2);
  text-decoration: none; font-size: var(--text-sm);
  transition: background .15s ease, color .15s ease; }
.more-nav a:hover { background: var(--surface); color: var(--ink); }
.more-nav a[aria-current="page"] { color: var(--ink); font-weight: var(--weight-bold);
  background: var(--surface); }

/* The primary bar sits at the bottom on a phone — that is where a one-handed
   evening reach lands — and returns to the top on a wide screen. */
.nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 6;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  background: var(--surface); border-top: 1px solid var(--edge);
  padding: var(--space-1) var(--space-2);
  padding-bottom: calc(var(--space-1) + env(safe-area-inset-bottom)); }
/* Icon over label. Four words in a row is a legible bar and a forgettable one;
   the glyph is what makes a destination findable by shape at arm's length in a
   dim room, which is the actual usage scene. Line art only, currentColor only,
   no fill and no badge — the bar must not become somewhere that reports things. */
.nav a { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--space-1);
  min-height: var(--tap-min); border-radius: var(--r-md);
  /* --ink-3 is 2.70:1 on --surface. That failed AA at the old 0.88rem and the
     label is now 0.78rem, so the one thing compensating for it is gone. --ink-2
     is 5.66:1 and the icon, stroked in currentColor, clears the 3:1 floor for
     non-text with it. */
  color: var(--ink-2); text-decoration: none; font-size: var(--text-xs);
  transition: color .15s ease, background .15s ease; }
.nav a svg { width: var(--icon); height: var(--icon); display: block;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round; }
.nav a[aria-current="page"] svg { stroke-width: 1.75; }
.nav a:hover { color: var(--ink); }
.nav a[aria-current="page"] { color: var(--ink); background: var(--surface-2);
  font-weight: var(--weight-medium); }


/* ── The wide layout ─────────────────────────────────────────────────────────
   LAST IN THE FILE, and that is not tidiness. A media query adds no specificity,
   so `@media { .nav { position: static } }` and `.nav { position: fixed }` are
   both (0,1,0) and the later one simply wins. This block used to sit 54 lines
   ABOVE the .nav it overrides, so at 1024px the query matched, every rule in it
   lost, the bar stayed pinned to the bottom of a laptop window, and main's
   padding-bottom — which DID apply, being declared after its base rule — dropped
   to 40px against a 53px bar. Thirteen pixels of every page sat under it.
   Nothing errored, and the two halves of one block disagreed silently.

   Anything that overrides a base rule at the same specificity belongs after it. */
@media (min-width: 40rem) {
  main { padding: var(--space-5) var(--space-4) var(--space-6); }
  .mrs-scroll { margin: 0; padding: 0; }

  /* The bar returns to the top, under the masthead, where a pointer is already
     travelling. It is emitted before <main> so this needs no reordering. */
  /* Now that the two bars are one object, one rule closes it. On a phone the
     masthead's own border is the only one and it stays. */
  .masthead { border-bottom: 0; }
  /* In flow inside .chrome, which is what makes it stick — no offset of its own
     and nothing to keep in step with the masthead's height. */
  .nav { position: static; border-top: 0;
    border-bottom: 1px solid var(--edge); background: var(--canvas);
    grid-auto-columns: max-content; justify-content: start;
    gap: var(--space-2); padding: 0 var(--space-4) var(--space-2); }
  .nav a { flex-direction: row; gap: var(--space-2); padding: 0 var(--space-4);
    font-size: var(--text-sm); }
  .actions { bottom: 0; }
  /* Room for six across once there is room for six across. */
  .weeks { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/* ── The daily form on a laptop ───────────────────────────────────────────────
   At one column the form was 4,969px on a 1024px window with 46% of the width
   empty — a phone layout that had been centred. The constraint was never one
   question per row; it is that a frozen anchor label may not be shortened, and
   "3 severe, it dictates my day" fits a 440px column with room to spare.

   Items pair up; a block keeps its whole width, because sleep and bleeding are
   groups and splitting a group across a column boundary is worse than the
   scroll it saves. 64rem rather than 40rem: a tablet in portrait has the width
   for two columns and not the pointer precision to want them. */
@media (min-width: 64rem) {
  main:has(.daily) { max-width: var(--measure-wide); }
  .daily { display: grid; grid-template-columns: 1fr 1fr;
    column-gap: var(--space-7); align-items: start; }
  .daily > .day-header,
  .daily > .skip-note,
  .daily > .degraded,
  .daily > .restored,
  .daily > .block,
  .daily > .optional,
  .daily > .actions { grid-column: 1 / -1; }
}

/* ── What the record now holds ────────────────────────────────────────────────
   Three figures, once, after the answers are already written. Deliberately not
   a dashboard: no ring, no bar, no sparkline, nothing that invites a target. A
   figure that cannot be shown honestly is not shown at all. */
.reading { background: var(--surface); border-radius: var(--r-lg);
  box-shadow: var(--lift); padding: var(--pad-card); margin-bottom: var(--space-5); }
.reading h2 { margin-bottom: var(--space-2); }
.reading-figure { font-family: var(--font-mono); font-size: var(--text-display);
  font-variant-numeric: tabular-nums; color: var(--ink); margin: 0;
  line-height: var(--leading-tight); }
.reading-of { font-size: var(--text-md); color: var(--ink-2); }
.reading-move { color: var(--ink-2); font-size: var(--text-sm); margin: var(--space-2) 0 0; }
.reading .notice, .reading .degraded { margin: var(--space-4) 0 0; }
.button-link { display: flex; align-items: center; justify-content: center;
  min-height: var(--tap-min); padding: var(--space-3); border-radius: var(--r-pill);
  background: var(--accent); color: var(--accent-ink); text-decoration: none;
  font-weight: var(--weight-bold); }

/* The weekly form gets the laptop's two columns too — but the grid goes on the
   SECTION holding the eleven scale items, not on the form. The form's direct
   children are all full-width things (header, strip, sections, Save), so a grid
   there lays out nothing and the items inside the section stay in one column.
   Which is what the first attempt did. */
@media (min-width: 64rem) {
  main:has(.weekly) { max-width: var(--measure-wide); }
  .weekly section:has(.item-mrs) { display: grid; grid-template-columns: 1fr 1fr;
    column-gap: var(--space-7); align-items: start; }
  .weekly section:has(.item-mrs) > h2,
  .weekly section:has(.item-mrs) > .block-note,
  .weekly section:has(.item-mrs) > .attribution { grid-column: 1 / -1; }
}

/* The grid deliberately does NOT take --measure-wide.
   It was called out as the only content surface refusing the laptop, and widening
   it is the wrong reading: at 62rem a cell becomes 128px square, which is a
   calendar nobody needs. Seven columns have a natural size and 560px is close to
   it. The real opportunity width offers this surface is more than one month at
   once — a feature, not a max-width — and that is not built. */

/* ── Lab intake ──────────────────────────────────────────────────────────────
   The review task is a COMPARISON, not a form. Section 10 of the intake
   specification: one value at a time against the photograph, because a table
   of forty rows invites reading down the numbers, and reading down the numbers
   is the failure mode — the eye agrees with plausible digits.

   On a phone the photograph sits above the card and the card is what scrolls
   to. On a wide screen they sit side by side, because the whole task is looking
   at two things at once and a comparison that needs scrolling is not one. */

.intake-form fieldset { border: 0; padding: 0; margin: 0 0 var(--space-6); }
/* Reached by CLASS, never by element. `.intake-form legend` also matches the
   `.prompt` legend of a nested radio fieldset — and the check that caught this
   exists because `.item legend, .item label` once flattened every option row
   and every chip in the daily form. Same rule, same file, second time. */
.intake-legend { font: var(--text-md)/var(--leading-snug) var(--font-display);
  color: var(--ink-heading); padding: 0 0 var(--space-2); }
.item-check { display: flex; align-items: center; gap: var(--space-3);
  min-height: var(--tap-min); }
.item-check input { width: auto; }

.pending-list { list-style: none; padding: 0; }
.pending-list li { padding: var(--pad-row); border-radius: var(--r-md);
  background: var(--surface); margin-bottom: var(--space-2); }
.pending-list a { display: block; min-height: var(--tap-min); }
.flag { color: var(--signal-stale); }

/* The date and laboratory are CONTEXT on this screen, not the task. At display
   size they took a third of a phone viewport and pushed the value card down
   with them, so the sheet started below the fold and the comparison needed a
   long scroll. Compacted here only — the same heading on /labs, where it IS the
   content, keeps its size. */
main:has(.review-split) > h1 { font-size: var(--text-md);
  line-height: var(--leading-snug); margin-bottom: var(--space-2); }
.review-summary { color: var(--ink-2); font-size: var(--text-sm);
  margin-bottom: var(--space-4); }

/* On a phone the value comes FIRST and the sheet follows.
   Measured on a 375x812 viewport: a full-height page image pushed the card
   entirely below the fold, so the two things she has to compare were never on
   screen at the same time — which is the one thing this screen exists to do.
   The image is capped so the card and the top of the sheet share the view, and
   it stays pinch-zoomable for the row she is actually checking. */
.review-split { display: grid; gap: var(--space-5); }
.review-queue { order: -1; }
.review-page img { max-height: 55vh; object-fit: contain; object-position: top; }
@media (min-width: 60rem) {
  /* The reading measure is 35rem, which is right for prose and wrong for this.
     Measured rather than eyeballed: a 900x1200 photograph rendered 252px wide
     inside it, for a task whose entire content is reading digits off that
     photograph. The month grid refuses --measure-wide on purpose (A-31); this
     screen needs it for the opposite reason — it is not reading, it is
     comparing two things that must both be legible at once. */
  main:has(.review-split) { max-width: var(--measure-wide); }
  .review-split { grid-template-columns: 1fr 1fr; align-items: start; }
  /* Side by side, so the reading order that mattered on a phone stops
     mattering — and the sheet gets its full height back. */
  .review-queue { order: 0; }
  .review-page img { max-height: none; }
  /* The card stays put while the page image scrolls. The comparison is the
     task, so the thing being compared against must not leave the screen. */
  .review-queue { position: sticky; top: var(--space-5); }
}

.review-pages { display: grid; gap: var(--space-4); }
.review-page { margin: 0; }
.review-page img { width: 100%; height: auto; display: block;
  border-radius: var(--r-md); background: var(--surface-2); }
.review-page figcaption { color: var(--ink-2); font-size: var(--text-xs);
  padding-top: var(--space-2); }

.review-card { background: var(--surface); border-radius: var(--r-lg);
  padding: var(--pad-card); }
.review-progress { color: var(--ink-2); font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  margin-bottom: var(--space-3); }
.review-analyte { font: var(--text-lg)/var(--leading-snug) var(--font-display);
  color: var(--ink-heading); margin: 0 0 var(--space-2); }

/* Big enough to read at arm's length against a photograph held in the other
   hand. The numeral face is the one used everywhere a measured value appears. */
.review-value { display: flex; align-items: baseline; gap: var(--space-2);
  margin: 0 0 var(--space-2); }
.review-number { font: var(--text-display)/1 var(--font-display);
  color: var(--ink-numeral); font-variant-numeric: tabular-nums; }
.review-unit { color: var(--ink-2); font-size: var(--text-md); }
.review-range { color: var(--ink-2); font-size: var(--text-sm); margin: 0 0 var(--space-4); }

.review-why { font-size: var(--text-sm); color: var(--ink-2);
  border-top: var(--rule-emphasis) solid var(--edge); padding-top: var(--space-3);
  margin-bottom: var(--space-5); }
.review-why ul { margin: 0; padding-left: var(--space-4); }
.review-why li { margin-bottom: var(--space-2); }

/* "Matches" is the answer nine times in ten, so it is the only filled button
   here. Both were the accent fill, which gave the exception the same weight as
   the default and made the card ask a question it had already answered.

   The outline is --ink-2 (5.66:1 on --surface) rather than --edge-strong, which
   measures 1.69:1 — below the 3:1 floor WCAG 1.4.11 sets for the boundary of a
   control. --edge-strong is a rule between blocks, and a button's edge is the
   only thing saying where the button is. */
.review-actions { display: grid; gap: var(--space-3); }
.review-actions button[value="correct"] { background: transparent;
  color: var(--ink); border: var(--ring-thin) solid var(--ink-2); }
/* Sits INSIDE the filled accent button, so it takes the accent's own ink.
   It inherited --ink-2, which is #8B979C on a #6FB6A0 button: the letter was
   there and read as an empty box. Caught by looking at it, not by a check —
   the contrast script measures tokens against surfaces, and this is a token
   used on a surface the script does not pair it with. */
.key-hint { display: inline-block;
  border: var(--ring-thin) solid currentColor; border-radius: var(--r-xs);
  padding: 0 var(--space-2); margin-left: var(--space-2);
  font-size: var(--text-xs); color: var(--accent-ink); opacity: 0.7; }
/* Keyboard hints are noise on a device with no keyboard. */
@media (hover: none) { .key-hint { display: none; } }
