/* ── GENERATED by build.py from tokens.json — DO NOT EDIT BY HAND ─────────────
   Canonical theme palette for all eagleadams86 apps (source of truth:
   github.com/eagleadams86/claude-theme-pack, local: ~/claude-theme-pack).

   Themes: midnight (the base palette, in :root), dark, light, sepia — selected with
   data-theme="…" on <html>. Every pair below clears WCAG AA; check_contrast.py
   in the pack is the gate. --text-hint sits at the 4.5:1 floor — never fade it
   further. To change ANYTHING here: edit tokens.json in the pack, rebuild,
   re-verify, then copy the generated file into each app.
──────────────────────────────────────────────────────────────────────────── */
:root {
  /* Midnight — the base palette: what :root is filled with, and what an
     unknown saved theme id falls back to. NOT the same as the default an app picks
     when nothing is saved, which is "auto" — see the [data-theme="auto"] block below. */
  --bg: #0a0e1a;
  --surface: #121829;
  --surface-alt: #1b2238;
  --border: #262f4a;
  --border-strong: #374163;
  --text-primary: #e7eaf6;
  --text-secondary: #aab2d0;
  --text-muted: #838cab;
  --text-hint: #7f88a9;
  --accent: #818cf8;
  --on-accent: #0a0e1a;
  --accent-bg: #242b4a;
  --focus-border: #818cf8;
  --border-control: #5a6ba2;
  --ok: #72fe84;
  --warn: #efbe39;
  --serious: #f79b5c;
  --err: #fb7874;
  --ok-bg: #1e3635;
  --warn-bg: #322c1b;
  --serious-bg: #32241b;
  --err-bg: #321b1b;
  --btn-bg: #e7eaf6;
  --btn-text: #0a0e1a;
  --input-border: #5a6ba2;
  --spinner-track: #262f4a;
  --spinner-head: #7e87a8;
  --series-1: #999cea;
  --series-2: #ba8d12;
  --series-3: #54c693;
  --series-4: #cf5d9c;
  --series-5: #bd5a39;
  --radius: 10px;
  --radius-sm: 6px;
  --radius-pill: 999px;

  /* Type — web only. REM, off a 16px root, so browser zoom and a reader's own
     default text size both work with no help from the page (WCAG 1.4.4 Resize
     Text). A page that wants this has to leave the root alone: no `html {
     font-size: 14px }`, and no `maximum-scale=1` in the viewport meta, which
     takes pinch-zoom away from everyone and breaks the same criterion on a phone.

     The ramp is the 11/12/13/14/17/22/30px scale this pack shipped until
     2026-08-22, scaled by 16/13 and rounded to whole pixels at that root: same
     shape, base moved from 13px to 16px. The px equivalent is in each comment
     below, because that is the number an app author is picturing.

     Mobile deliberately does NOT get fixed sizes; see the role map in Theme.kt /
     Theme.swift, which maps these roles onto the platform's scaling text styles. */
  --font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  --fs-xs: 0.8125rem;        /*  13px · Chrome, labels, hints, captions, badges, footers, table headings */
  --fs-sm: 0.9375rem;        /*  15px · Table and list rows, toasts, secondary prose */
  --fs-base: 1rem;      /*  16px · Body text, prose, controls and buttons */
  --fs-md: 1.0625rem;        /*  17px · The app name, card headings, dialog headings */
  --fs-lg: 1.3125rem;        /*  21px · A heading that owns a whole panel — rare, most headings are md */
  --fs-xl: 1.6875rem;        /*  27px · Page titles, and the big figure on a summary tile */
  --fs-display: 2.3125rem;   /*  37px · The one hero figure on a page that has one */
  --fw-normal: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;
  --lh-tight: 1.25;
  --lh-base: 1.5;
  --lh-loose: 1.7;
  --ls-tight: -0.01em;
  --ls-label: 0.06em;

  /* Never set --text-muted, --text-hint below --fs-xs (13px), and never
     let that step itself fall under 12px. Both tiers sit at or near the 4.5:1
     contrast floor, and passing the contrast gate is not the same as being
     readable — check_contrast.py gates the second half of that. */

  /* Spacing — 2px steps to 8px, 4px to 16px, then 8px and up. */
  --sp-1: 2px;
  --sp-2: 4px;
  --sp-3: 6px;
  --sp-4: 8px;
  --sp-5: 12px;
  --sp-6: 16px;
  --sp-7: 24px;
  --sp-8: 32px;
  --sp-9: 48px;

  /* Back-compat aliases (old lottery/portfolio names) — defined once, track the
     active theme via var() */
  --bg-card: var(--surface);
  --bg-card-alt: var(--surface-alt);
  --surface2: var(--surface-alt);
  --border2: var(--border-strong);
  --text: var(--text-primary);
  --text2: var(--text-secondary);
  --text3: var(--text-muted);
  --green: var(--ok);
  --red: var(--err);
  --amber: var(--warn);
  --green-bg: var(--ok-bg);
  --red-bg: var(--err-bg);
  --amber-bg: var(--warn-bg);
  --unit-active-bg: var(--btn-bg);
  --unit-active-txt: var(--btn-text);
  --chart-tick: var(--text-muted);
  --chart-grid: var(--border);
}

[data-theme="dark"] {
  /* Dark */
  --bg: #13151b;
  --surface: #1c1f27;
  --surface-alt: #262a33;
  --border: #2d313b;
  --border-strong: #3d424e;
  --text-primary: #eef0f5;
  --text-secondary: #adb1bd;
  --text-muted: #9396a3;
  --text-hint: #8c909d;
  --accent: #eef0f5;
  --on-accent: #13151b;
  --accent-bg: #3c3e46;
  --focus-border: #eef0f5;
  --border-control: #6a7387;
  --ok: #72fe84;
  --warn: #efbe39;
  --serious: #f79b5c;
  --err: #fb7874;
  --ok-bg: #273c33;
  --warn-bg: #322c1b;
  --serious-bg: #32241b;
  --err-bg: #321b1b;
  --btn-bg: #eef0f5;
  --btn-text: #13151b;
  --input-border: #6a7387;
  --spinner-track: #2d313b;
  --spinner-head: #9396a3;
  --series-1: #999cea;
  --series-2: #ba8d12;
  --series-3: #54c693;
  --series-4: #cf5d9c;
  --series-5: #bd5a39;
}

[data-theme="light"] {
  /* Light */
  --bg: #f5f5f3;
  --surface: #ffffff;
  --surface-alt: #f0f0ee;
  --border: #e0e0dd;
  --border-strong: #c8c8c4;
  --text-primary: #111110;
  --text-secondary: #555551;
  --text-muted: #656562;
  --text-hint: #6d6d6d;
  --accent: #266dc3;
  --on-accent: #ffffff;
  --accent-bg: #e9f0f9;
  --focus-border: #266dc3;
  --border-control: #8a8a82;
  --ok: #0b7a67;
  --warn: #7e6a07;
  --serious: #995c33;
  --err: #8f1e2a;
  --ok-bg: #bcfee8;
  --warn-bg: #fff0b2;
  --serious-bg: #fff1e8;
  --err-bg: #ffc9bf;
  --btn-bg: #111110;
  --btn-text: #ffffff;
  --input-border: #8a8a82;
  --spinner-track: #e0e0dd;
  --spinner-head: #656562;
  --series-1: #276fa5;
  --series-2: #8d7803;
  --series-3: #009966;
  --series-4: #722460;
  --series-5: #8d3f24;
}

[data-theme="sepia"] {
  /* Sepia */
  --bg: #f3ead7;
  --surface: #fbf5e7;
  --surface-alt: #eee2c9;
  --border: #e0d3b4;
  --border-strong: #cbb992;
  --text-primary: #3a3020;
  --text-secondary: #5e5039;
  --text-muted: #6a5e46;
  --text-hint: #71644b;
  --accent: #3a3020;
  --on-accent: #fbf5e7;
  --accent-bg: #e2dbcd;
  --focus-border: #3a3020;
  --border-control: #977e49;
  --ok: #087256;
  --warn: #746206;
  --serious: #915430;
  --err: #8b232d;
  --ok-bg: #bcfee8;
  --warn-bg: #fff0b2;
  --serious-bg: #fee3d0;
  --err-bg: #ffc9bf;
  --btn-bg: #3a3020;
  --btn-text: #fbf5e7;
  --input-border: #977e49;
  --spinner-track: #e0d3b4;
  --spinner-head: #6a5e46;
  --series-1: #0f63ae;
  --series-2: #937b27;
  --series-3: #247e51;
  --series-4: #87366f;
  --series-5: #7b3015;
}

[data-theme="auto"] {
  /* Auto, resolved LIGHT — the reader's system is in light mode. Every value is
     the Light block above; nothing new is defined here. */
  --bg: #f5f5f3;
  --surface: #ffffff;
  --surface-alt: #f0f0ee;
  --border: #e0e0dd;
  --border-strong: #c8c8c4;
  --text-primary: #111110;
  --text-secondary: #555551;
  --text-muted: #656562;
  --text-hint: #6d6d6d;
  --accent: #266dc3;
  --on-accent: #ffffff;
  --accent-bg: #e9f0f9;
  --focus-border: #266dc3;
  --border-control: #8a8a82;
  --ok: #0b7a67;
  --warn: #7e6a07;
  --serious: #995c33;
  --err: #8f1e2a;
  --ok-bg: #bcfee8;
  --warn-bg: #fff0b2;
  --serious-bg: #fff1e8;
  --err-bg: #ffc9bf;
  --btn-bg: #111110;
  --btn-text: #ffffff;
  --input-border: #8a8a82;
  --spinner-track: #e0e0dd;
  --spinner-head: #656562;
  --series-1: #276fa5;
  --series-2: #8d7803;
  --series-3: #009966;
  --series-4: #722460;
  --series-5: #8d3f24;
}

@media (prefers-color-scheme: dark) {
  [data-theme="auto"] {
    /* Auto, resolved MIDNIGHT — the family default, which is what "the dark one"
       means here. Same specificity as the block above and later in the file, so
       it wins; no !important needed. */
    --bg: #0a0e1a;
    --surface: #121829;
    --surface-alt: #1b2238;
    --border: #262f4a;
    --border-strong: #374163;
    --text-primary: #e7eaf6;
    --text-secondary: #aab2d0;
    --text-muted: #838cab;
    --text-hint: #7f88a9;
    --accent: #818cf8;
    --on-accent: #0a0e1a;
    --accent-bg: #242b4a;
    --focus-border: #818cf8;
    --border-control: #5a6ba2;
    --ok: #72fe84;
    --warn: #efbe39;
    --serious: #f79b5c;
    --err: #fb7874;
    --ok-bg: #1e3635;
    --warn-bg: #322c1b;
    --serious-bg: #32241b;
    --err-bg: #321b1b;
    --btn-bg: #e7eaf6;
    --btn-text: #0a0e1a;
    --input-border: #5a6ba2;
    --spinner-track: #262f4a;
    --spinner-head: #7e87a8;
    --series-1: #999cea;
    --series-2: #ba8d12;
    --series-3: #54c693;
    --series-4: #cf5d9c;
    --series-5: #bd5a39;
  }
}

/* Explicit selection of midnight needs no block: with no matching [data-theme]
   the :root values above already apply. Apps should normalize removed saved
   theme ids before setting data-theme: "slate" -> "dark" (same palette,
   renamed), anything else unknown -> "auto". "auto" IS a valid saved id (see
   the block above) and must not be normalized away.

   THE DEFAULT IS "auto" (2026-08-22): a reader who has never touched the picker
   gets Light on a light system and Midnight on a dark one, so an app writes
   data-theme="auto" when it finds nothing saved — and writes it in the markup
   too, so the page is still right if the boot script never runs. Midnight stays
   the base palette in :root and stays what Auto means by "dark"; what changed is
   only which id an app reaches for when the reader has expressed no preference. */

/* ── The real RULES in this file (five, plus rule 7c's font-smoothing pair) ──
   Everything above is custom properties; these are declarations that take
   effect on their own, and they are here because every app needs them and no
   app should have to remember them.

   iOS Safari (and Chrome on Android) inflates the font of a block it judges to
   be a wide column of small text — "font boosting" — only in LANDSCAPE, only
   for some blocks, and never for the headings, tiles or table cells beside
   them. These are UI-dense screens whose body text sits in full-width cards,
   which is precisely the shape the heuristic fires on (it fired hardest while
   --fs-base was 13px, and a 16px base makes it less likely, not impossible):
   Financial Plan's card intros rendered two or three times the size of the
   figures they introduced while everything around them stayed put.

   `100%`, NOT `none`: `none` also disables the reader's own text scaling in
   some engines, which is an accessibility setting rather than a bug — the same
   distinction rule 8 draws for mobile Dynamic Type.

   WEB ONLY, like the type scale, and for the same reason: there is no such
   knob on Theme.kt / Theme.swift and there must not be one. Platform text
   scaling on mobile is the setting to respect, not to switch off. Every web
   app links this file as of 2026-08-18; if one ever inlines its tokens
   instead, it needs the same two lines in its own <style>. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* ── ...and rule 7c's font-smoothing pair, found the same way ────────────────
   TEXT RENDERED VISIBLY BOLDER IN FIVE APPS THAN IN THE OTHER THREE, at
   identical font-weights. Charles spotted it comparing PAPTrack's header with
   Sprint Predictability's on 2026-08-22 — "a couple of the apps still have
   bolder text" — and he was right, in a way no measurement taken up to that
   point could show: every control in both rows computed `font-weight: 400`.
   The difference was not the WEIGHT, it was the RASTERISATION.

   macOS renders text with subpixel antialiasing by default, which thickens
   glyph stems. On a dark background — which is what Midnight, the default
   theme, gives every one of these apps — that reads as noticeably heavier and
   slightly blurred. `antialiased` switches to grayscale, which is why it is the
   near-universal choice for dark UIs.

   Money Map, Sprint Predictability and Flow Metrics had carried it in their own
   body rules since long before this file existed; Golf Handicap, PAPTrack, the
   two lottery pages and the Task Dashboard never had it. Eight pages, two
   different renderings of the same 400-weight text, and nothing in any app's
   CSS that looked wrong on its own. It belongs HERE for the same reason the
   palette and the type ramp do: it is a property of the family, not of any one
   app, and no app should have to remember it.

   The Firefox/macOS counterpart comes with it. It does nothing anywhere else —
   both properties are no-ops off macOS, so this costs Windows and Android
   readers nothing.

   WEB ONLY. Theme.kt and Theme.swift must not gain an equivalent: both
   platforms already render text the way their own OS intends. */
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }

/* ── ...and the second one, for the same reason ──────────────────────────────
   iOS Safari ZOOMS THE PAGE IN whenever a focused form control is under 16px.
   Body text was 13px (--fs-base) and controls inherit it, so every tap into a
   box zoomed the app: the page then sits wider than the screen — dialogs cut
   off at the right, the wrap's padding gone — and pinching back out lasts only
   until the next field is tapped. Found in an INSTALLED web app on an iPhone,
   where there is no browser chrome to make the zoom legible as zoom, and then
   found in all five web apps at once (2026-08-19).

   SINCE 2026-08-22 --fs-base IS 16px, so a control that simply inherits body
   text is already at the floor and this rule is a no-op for it. It stays, and
   it must: the rule is not "make the base 16px", it is "nothing a coarse
   pointer can focus may be under 16px", and the ramp still has four steps below
   base that an app is free to put on a control. A base that happens to clear
   the floor is a fact about today's tokens, not a guarantee.

   The fix has to be the FONT, not the viewport: `maximum-scale=1` /
   `user-scalable=no` stop it by taking pinch-zoom away from everyone, which is
   a real accessibility loss, and modern iOS ignores them anyway.

   This does NOT contradict rule 7's scale. The scale still governs what text
   READS at; 16px here is a floor iOS imposes on one interaction, on touch only.
   Nothing else moves, and no `--fs-*` step changed.

   `!important`, AND `html` prefixes. The first version of this rule shipped
   with the prefixes alone, on the reasoning that they beat a bare `select` in
   the app's own <style> further down the cascade — which they do — while still
   leaving an app free to override "where it really means to", the app then
   owning the re-assertion. That contract failed completely and silently.
   `html select` is specificity 0-0-2: it out-ranks an element selector and
   NOTHING else. Every app sizes its pickers with a class or an id — `.theme-sel`
   (0-1-0), `#teamSel, #themeSel` (1-0-0), `#shareLink` (1-0-0) — so all seven
   web apps kept zooming on iOS with this rule present and believed to be
   working (measured 2026-08-20, when the base was 13px: 13px pickers in five
   apps, an 11px share-link field in four, 12px controls in the lottery pages). A rule every app defeats
   by accident is not a rule, and "no app should have to remember it" above is
   the whole point of this file.

   So the floor is now non-negotiable on touch. An app that wants a smaller
   control cannot have one where a coarse pointer can focus it — that is the
   correct answer, not a limitation: below 16px iOS zooms the page, and no
   design intent survives that. Sizes on a mouse are untouched, which is where
   the chrome height actually reads.

   THE HALF OF THIS AN APP STILL OWES, and every one of them owed it for four
   days. A <button> raises no keyboard, so it is deliberately not in the list
   below and keeps whatever size the app gave it. Put a 16px picker beside a
   13px button — which is exactly what a header row is — and this rule makes
   them different sizes, and different heights with it, on touch and only on
   touch. Nothing in a desktop check can see it.

   So: AN APP THAT PINS A CHROME ROW HEIGHT MUST LET THAT ROW GROW ON TOUCH.
   The answer all six web apps carry, from 2026-08-23:

       @media (pointer: coarse) {
         :root { --chrome-h: 34px; }
         <that app's chrome buttons> { font-size: var(--fs-base); }
       }

   34px is not a new number: it is what 16px text measures in the 4px/9px box
   those rows already use — 24px line + 8px padding + 2px border — so the
   buttons and the natively-drawn pickers land on the same height without
   fighting each other. `--chrome-h` is an app-local token (each app declares
   its own, and Golf Handicap and the starter also size their `.icon-btn` row
   actions from it), which is why the rule lives in the apps and only its
   reasoning lives here. The cost is a sticky header about 4px taller per
   control row on a phone, and nothing at all on a mouse.

   Checkbox and radio are absent on purpose: they raise no keyboard, and sizing
   them here would be a visual change with no reason behind it. `select` IS
   present even though it raises no keyboard either — iOS zooms for its picker
   just the same, which the "only controls you type into" wording used to miss.

   WEB ONLY, like the rule above and the type scale — Theme.kt and Theme.swift
   must not gain an equivalent. */
@media (pointer: coarse) {
  html input[type=text], html input[type=number], html input[type=date],
  html input[type=month], html input[type=search], html input[type=tel],
  html input[type=url], html input[type=email], html input[type=password],
  html select, html textarea { font-size: 16px !important; }
}

/* ── ...and a third, from the same afternoon ─────────────────────────────────
   A DATE FIELD RENDERED 20px WIDER THAN THE COLUMN IT WAS GIVEN, in Sprint
   Predictability, PAPTrack and Golf Handicap at once, and only on iOS. Every
   app declares `* { box-sizing: border-box }`, so `width: 100%` on a field is
   meant to mean "the column, padding and border included". WebKit ignores that
   on a form control it is drawing NATIVELY: `input[type=date]` comes back
   `content-box` whatever the author stylesheet says — even an inline
   `box-sizing: border-box` on the element loses — so 100% became the column
   PLUS 18px of padding and 2px of border, and the box hung out over its
   neighbour and past the edge of the panel. Measured on iOS 27: a field in a
   145px column laid out at 165px, and 145px the moment appearance was off.

   Turning the native appearance off is what hands box-sizing back. It costs
   nothing that was wanted: these fields are already given the family's
   background, border and radius, and the native chrome was being painted over
   anyway. The picker still opens on tap and the value still reads as a
   formatted date — appearance governs the drawing, not the control.

   Only the date-ish types. `select` keeps its appearance because the arrow IS
   the affordance, and text and number inputs were never drawn natively enough
   for WebKit to override them.

   The reason it surfaced when it did is the rule above: at the 13px base of
   the time the same 20px overflow was there and read as a snug box, and at the
   16px touch floor the date text grew into the room the overflow was
   stealing. A bug this file uncovered is
   this file's to fix.

   WEB ONLY. */
input[type=date], input[type=month], input[type=week],
input[type=time], input[type=datetime-local] {
  -webkit-appearance: none; appearance: none;
}


/* ── ...and a fourth, for the medium the others forget ───────────────────────
   PRINTING. Every app in this family defaults to a dark palette, and a dark
   palette on paper is either a wall of ink or — because browsers drop
   backgrounds by default — pale grey text on white, which is worse. Nothing in
   an app can fix that on its own without inventing a print palette, and a
   palette invented in an app is exactly what this repo exists to prevent.

   So the palette below is LIGHT, copied verbatim from the Light theme. No new
   colour is defined here and none can be: the contrast gate already covers
   every value, and rule 5's count of four themes is untouched, because this is
   a fifth MEDIUM rather than a fifth theme.

   It leaves Light and Sepia alone. Both are already light, and Sepia is a
   deliberate choice of a paper-coloured screen — overriding it would be this
   file telling a reader their own answer was wrong. Only the dark ones are
   changed, which is the only case that was actually broken. "auto" needs no
   special case: in light mode it already IS these values, and in dark mode it
   is caught by the same selector.

   `:root:not(...)` out-ranks any app's own `[data-theme=...]` block (0-3-0
   against 0-1-0), which matters because several apps re-declare a token or two
   in their own <style> AFTER linking this file.

   WEB ONLY, like the three rules above. */
@media print {
  :root:not([data-theme="light"]):not([data-theme="sepia"]) {
    /* Form controls, scrollbars and the like are drawn by the engine off this,
       not off the tokens — a dark one puts dark chrome on white paper. */
    color-scheme: light;
    --bg: #f5f5f3;
    --surface: #ffffff;
    --surface-alt: #f0f0ee;
    --border: #e0e0dd;
    --border-strong: #c8c8c4;
    --text-primary: #111110;
    --text-secondary: #555551;
    --text-muted: #656562;
    --text-hint: #6d6d6d;
    --accent: #266dc3;
    --on-accent: #ffffff;
    --accent-bg: #e9f0f9;
    --focus-border: #266dc3;
    --border-control: #8a8a82;
    --ok: #0b7a67;
    --warn: #7e6a07;
    --serious: #995c33;
    --err: #8f1e2a;
    --ok-bg: #bcfee8;
    --warn-bg: #fff0b2;
    --serious-bg: #fff1e8;
    --err-bg: #ffc9bf;
    --btn-bg: #111110;
    --btn-text: #ffffff;
    --input-border: #8a8a82;
    --spinner-track: #e0e0dd;
    --spinner-head: #656562;
    --series-1: #276fa5;
    --series-2: #8d7803;
    --series-3: #009966;
    --series-4: #722460;
    --series-5: #8d3f24;
  }

  /* A CHART'S COLOURS ARE ITS MEANING. Browsers drop background colour when
     printing, to save ink, which is right for a page whose backgrounds are
     decoration and wrong for a canvas: every bar, band and slice on it is
     painted, and dropped it prints as an empty box. Asked for by name rather
     than with `*`, so the ink is spent where it carries information.
     (An app's own status TINTS — a RAG pill, a threshold band drawn in CSS —
     are the same argument and the same fix, but they are the app's own class
     names; Sprint Velocity's block is the worked example.) */
  canvas { print-color-adjust: exact; -webkit-print-color-adjust: exact; }

  /* The family's own chrome, and nothing an app owns. `.headbar` is the header
     row every app-shaped consumer here uses; what goes is the BUTTONS in it and
     the two pickers that only exist to change how the page looks, while
     `.brand` stays — a printed page with no name on it is a page you cannot
     file.

     `.headbar select` in general does NOT go, and that is a correction worth
     recording: Sprint Velocity's team picker lives in the header, and its
     selected value is the one thing a printed report has to say. A select
     prints as its chosen option, which is exactly what is wanted. Only the
     pickers named here are furniture by definition — they change presentation
     and nothing else — and those names are already known to this file (see
     rule 11's account of `#themeSel` / `.theme-sel`). */
  .headbar button,
  .headbar .btn,
  #themeSel, .theme-sel, #zoomSel,
  [role="tablist"] { display: none !important; }

  /* The handle for everything else. An app tags its own furniture with this
     rather than this file guessing at class names it cannot see. */
  .no-print { display: none !important; }

  /* A sticky header prints once, over the top of whatever the first page
     happens to start with. */
  header { position: static !important; }

  /* What any page owes a sheet of paper. A long table repeats its headings, a
     row or a chart is not torn in half, and a heading does not print alone at
     the foot of a page. Card shadows are ink spent on a screen effect. */
  @page { margin: 12mm; }
  thead { display: table-header-group; }
  tr, img, svg, canvas { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }
  .card { box-shadow: none; }

  /* Some stylesheets print a link's href after it. On a page of financial
     figures that is noise; an app that wants it can say so itself. */
  a[href]::after { content: none !important; }
}


/* ── ...and a fifth, for the reader who asked their computer to settle down ───
   PREFERS-REDUCED-MOTION. Turned on by a reader, for their whole machine —
   usually because movement on a screen makes them ill. Motion sickness and
   vestibular disorders are the reason the setting exists; it is not a taste.

   `.01ms` AND NOT `none`, which is the one detail here worth getting right.
   A zero-duration transition still FIRES `transitionend`; `transition: none`
   never fires it at all, so any code waiting on that event waits forever. No
   app in this family listens for it today — measured, not assumed — but the
   Task Dashboard had reached for the `none` spelling on its own, and a rule
   that quietly breaks a listener somebody adds next year is not one to ship in
   ten places at once. Same reasoning for `animation-iteration-count: 1`: it
   lets an animation finish its first pass rather than vanishing mid-frame.

   `*::before, *::after` are named because pseudo-elements animate too, and the
   spelling that omitted them was a third of the copies.

   WHAT THIS CANNOT DO, and the reason an app may still need a line of its own:
   `scroll-behavior: auto` here overrides the CSS property, and nothing more. A
   scroll asked for from SCRIPT — `scrollTo({ behavior: 'smooth' })` — ignores
   CSS entirely, so a page that scrolls itself must read the query in JS as
   well. The Task Dashboard already does; Money Map never scrolls smoothly at
   all, and says so. That half stays the app's.

   WEB ONLY, like the four rules above. Both mobile platforms surface the same
   setting through their own APIs and Theme.kt / Theme.swift must not grow a
   knob for it. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ── ...and a sixth: the grey hint in an empty box ──────────────────────────
   PLACEHOLDER TEXT IS TEXT, so it answers to the 4.5:1 rule like any other.
   Left to the browser it is a fixed mid-grey at the UA's own opacity, which
   measured 3.84:1 on Midnight, 3.58 on Dark and 4.24 on Sepia against these
   field surfaces — a hint nobody with low vision could read, on the one
   control that says what the box is for. `--text-hint` is the token defined
   for exactly this job and it is gated at 4.5:1 on every surface a field can
   sit on; `opacity: 1` because the UA applies its own otherwise and would
   undo the colour. One rule, here, so no app has to remember it and no new
   field can arrive at the browser's grey again (2026-09-05).

   WEB ONLY, like the five rules above: both mobile platforms draw their own
   placeholders from their own text styles. */
::placeholder { color: var(--text-hint); opacity: 1; }
