/* styles/variables.css */
/* Sacred Press ships three self-hosted OFL families and no fourth.
 *
 * Every face below is same-origin, WOFF2, and vendored from its official
 * upstream by `scripts/vendor-sacred-press-fonts.py`; `public/fonts/README.md`
 * carries the provenance and SHA-256 manifest. The product makes no font
 * request to Google Fonts or any other third-party origin.
 *
 * Unbounded is one variable file spanning 400-900, so every display weight the
 * interface names is a real interpolated instance rather than a synthesised
 * one. IBM Plex ships static 400 and 600 faces, which are the only two weights
 * any Plex role is allowed to ask for — see `font-synthesis: none` in globals.
 */

@font-face {
  font-family: "Unbounded";
  src: url("/fonts/unbounded/Unbounded-Variable.woff2") format("woff2");
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans Condensed";
  src: url("/fonts/ibm-plex-sans-condensed/IBMPlexSansCondensed-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans Condensed";
  src: url("/fonts/ibm-plex-sans-condensed/IBMPlexSansCondensed-SemiBold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("/fonts/ibm-plex-sans/IBMPlexSans-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("/fonts/ibm-plex-mono/IBMPlexMono-SemiBold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

:root {

  /* Two schemes, one set of names.
   *
   * Every colour that changes between the light sheet and the night sheet is a
   * `light-dark(light, dark)` pair on its own token, so components never know
   * which scheme they are in. `color-scheme` picks the half: "light dark"
   * follows the operating system, and the reader's saved choice stamps
   * `data-theme` on <html> to pin one (rules below this block).
   *
   * The dark half is not an inversion. The page is a soft #121212 rather than
   * black, so the black header and battle field still read as a darker field
   * on it; body ink is #ededed rather than white, to take the glare off long
   * reading; green and red are lifted until they clear AA on the dark ground.
   * `test/ink-on-paper.test.js` computes both halves' ratios from these values.
   */
  color-scheme: light dark;

  --white: #ffffff;

  /* Achimari Monochrome Press paper.
   *
   * The sheet is white. Not off-white, not mineral, not warm: a printed page
   * whose whole identity is black ink, white space and the rules between them.
   * Every ink role below is measured against the two real grounds the product
   * sets text on — the white page and the recessed step — and every one of them
   * clears WCAG 2.2 AA on both; `test/ink-on-paper.test.js` computes the ratios
   * from these values rather than trusting the comments.
   *
   * Separation is made by space and by rules, never by elevation, which is why
   * there is no lighter paper above white to raise a surface onto.
   */
  --paper: light-dark(#ffffff, #121212);

  /* Kept as a name because components still ask to be "raised". It resolves to
     the same sheet: a region separates itself with spacing or a rule, and a
     tint step above white does not exist. */
  --paper-raised: var(--paper);

  /* The one recessed step — meter tracks, insets, the deselected half of a
     pair, a table's zebra. The only fill that is neither paper nor field. */
  --paper-muted: light-dark(#f2f2f2, #1f1f1f);

  /* The whole document is one sheet, so the page ground is the paper. */
  --canvas: var(--paper);

  /* The black field. Navigation, primary actions, image plates, the battle
     stage and the rare inverse region. Reserved: a black block is a rhythm
     reset, not decoration, and the working page stays predominantly white. */
  --field: #000000;
  --field-raised: #1a1a1a;

  --ink: light-dark(#000000, #ededed);
  --ink-raised: light-dark(#1a1a1a, #d6d6d6);

  /* Secondary copy. 7.82:1 on white and 6.98:1 on the recessed step — AA on
     every surface the product actually sets text on, so a muted label never
     has to be audited against which ground it happens to sit over. */
  --ink-muted: light-dark(#525252, #a3a3a3);

  /* 12.63:1 on white. For small, muted or plate-adjacent copy that has to clear
     roughly 7:1 where --ink-muted's 7.82:1 is only just comfortable. */
  --ink-secondary-strong: light-dark(#333333, #cccccc);

  /* 3.45:1 on white — quiet decoration only. It clears the 3:1 non-text floor,
     so it may draw a graphic or a mark, and it is never essential small text. */
  --ink-quiet: light-dark(#8a8a8a, #707070);

  /* Structural rules, drawn from the ink itself so they read as printing rather
     than as a UI border.

     --rule is decoration: it organises content and is never the only thing
     marking a control's edge. --rule-strong is the control-boundary colour and
     is a solid #767676, which clears the 3:1 component floor on white (4.54:1)
     and on the recessed step (4.06:1) — a translucent rule would have drifted
     below the floor over the darker of the two. */
  --rule: light-dark(rgb(0 0 0 / 18%), rgb(255 255 255 / 18%));
  --rule-strong: light-dark(#767676, #8a8a8a);

  --surface: var(--paper);
  --surface-subtle: var(--paper-muted);
  --ink-secondary: var(--ink-muted);
  --separator: var(--rule);

  /* Filled actions use the black field. Green and red are reserved for
     positive and negative state, so a button never competes with feedback. */
  --accent: var(--ink);
  --accent-hover: var(--ink-raised);
  --accent-ink: var(--ink);
  /* Text on an ink fill is the paper, so it reverses with the fill. */
  --on-accent: var(--paper);

  /* Green and red reinforce the labels, checks, and warning marks already
     used for state. The pale fills keep notices quiet and text readable. */
  --success: light-dark(#1d704e, #5cc794);
  --success-soft: light-dark(#edf7f0, #10261b);
  --danger: light-dark(#b73535, #ff8f85);
  --danger-soft: light-dark(#fff0ee, #2c1512);

  /* Navigation and ordinary links retain the neutral ink. */
  --brand: var(--ink);
  --brand-hover: var(--ink-raised);

  /* Four roles, three families. The role a piece of text has decides its
     family; nothing picks a font by taste.

     display — the wordmark, h1, a selected h2, and a small number of major
       editorial numerals. Never a label, a button, a form or a table.
     ui      — navigation, buttons, inputs, menus, filters, tables and compact
       labels. Condensed, because the interface is dense and the page is not.
     body    — prose and descriptions, where the wider face is easier to read.
     data    — dates, timers, chart values and metadata, where a fixed advance
       makes columns line up and a changing number stop jumping.

     Each stack ends in a real system fallback, so a failed font request leaves
     a usable page rather than a default serif. */
  --font-display: "Unbounded", system-ui, sans-serif;
  --font-ui: "IBM Plex Sans Condensed", "Arial Narrow", system-ui, sans-serif;
  --font-body: "IBM Plex Sans", system-ui, sans-serif;
  --font-data: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Long-form reading is the body role. Kept as its own name because the
     reading surfaces ask for it by that name. */
  --font-reading: var(--font-body);

  /* IBM Plex ships 400 and 600 here, and those are the only two weights a Plex
     role may request: `font-synthesis: none` is set globally, so a request for
     500 or 700 would quietly resolve to one of these anyway, and naming a
     weight the repository does not ship is how that drift starts.

     The display face is variable across 400-900, so its two steps are real
     interpolated instances. */
  --weight-body: 400;
  --weight-strong: 600;
  --weight-display: 700;
  --weight-display-heavy: 800;

  /* The page opening. Editorial scale, so it is fluid — but bounded: the cap is
     88px, under the 96px ceiling, and the floor is high enough that the word
     still reads as display type on a phone. */
  --text-display: clamp(2.75rem, 7.5vw, 5.5rem);

  /* A section opening, one step below the page opening. The 28px floor keeps
     the longest route name on one line inside the 320px shared shell. */
  --text-page-title: clamp(1.75rem, 4vw, 3rem);

  --text-title: 2rem;
  --text-subtitle: 1.5rem;

  /* Card and panel headings. Named because the sheets carried a dozen one-off
     sizes that all meant the same thing. */
  --text-component: 1.25rem;

  /* A statistic read at a glance: a streak count, a stat total, a timer. Set in
     the display face, so it carries at this size. */
  --text-figure: 2.25rem;

  /* Long-form reading. IBM Plex Sans is drawn for screen text and carries more
     information per pixel than the handwritten face this replaced, so the body
     step moves from 20px to 17px without losing legibility. */
  --text-body: 1.0625rem;

  /* Ordinary interface copy, inputs and primary controls. */
  --text-control: 1rem;

  /* Labels, and the critical HUD and statistics values. */
  --text-label: 0.9375rem;

  /* Dates, timers, counts and compact metadata, set in the mono role. This is
     the smallest step anything a reader has to read actually uses. */
  --text-meta: 0.875rem;

  /* The floor, and a narrow one: the mono margin marks that run beside a
     composition. Short, uppercase, tracked, and never the only place a fact
     appears — a reader can ignore every one of them and lose nothing. */
  --text-mark: 0.8125rem;

  /* Display type is set tight, because the letterforms are large enough to
     carry the line on their own. Prose keeps a generous rhythm. */
  --leading-display: 0.92;
  --leading-title: 1.12;
  --leading-body: 1.6;
  --leading-meta: 1.45;
  --leading-control: 1.35;

  /* The one tracking the system uses, on short mono labels set in capitals,
     where default spacing collides the letterforms. Display type never goes
     tighter than -0.02em. */
  --tracking-mark: 0.08em;
  --tracking-display: -0.02em;

  /* Corners scale with the surface: compact rows, comfortable controls,
     and gently rounded panels. Circular geometry keeps its own token. */
  --radius-row: 6px;
  --radius-control: 8px;
  --radius-panel: 12px;
  --radius-feature: 16px;
  --radius-capsule: 999px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;
  --space-9: 96px;

  --padding: clamp(var(--space-4), 2.2vw, var(--space-6));
  --section-padding: clamp(var(--space-6), 5.2vw, var(--space-8));
  --section-padding-compact: clamp(var(--space-4), 3.2vw, var(--space-6));

  --section-gap: clamp(var(--space-6), 4.5vw, var(--space-8));

  --gap-related: var(--space-3);
  --gap-sibling: var(--space-6);
  --gap-region: var(--space-7);

  --measure-wide: 1160px;
  --measure-focus: 820px;
  --measure-reading: 680px;

  /* Long-form prose, capped at a real 65-75 character measure. `ch` is the
     advance of "0", and IBM Plex Sans draws a normal-width zero, so the unit
     now means what it says — unlike the handwritten face this replaced, whose
     very wide zero made 68ch run to ~91 characters. */
  --measure-prose: 68ch;

  --tap-min: 44px;

  /* Nothing on the page floats. The only surface allowed a shadow is one that
     genuinely sits above the document — a modal over its backdrop — and even
     that is a single soft drop, not a lifted card.
     --shadow-float is kept as a name because components still ask for it, and
     is deliberately `none`: a region groups with space, a rule, a tonal fill or
     a black field, never by being lifted off the sheet. */
  --shadow-float: none;
  --shadow-dialog: 0 24px 64px light-dark(rgb(0 0 0 / 26%), rgb(0 0 0 / 64%));

  --chrome: var(--field);
  --chrome-raised: var(--field-raised);
  /* The masthead's one size at every width: a 44px control row plus its
     breathing room. Scroll offsets and sheet insets all read this token. */
  --header-height: 56px;

  --header-active: var(--white);
  --header-active-line: var(--white);
  --footer-bg: var(--chrome);
  --footer-text: rgb(255 255 255 / 78%);
  --footer-icon-hover: var(--white);

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
  --duration-press: 120ms;
  --duration-fast: 140ms;
  --duration-ui: 160ms;
  --duration-panel: 220ms;

  /* The page opening — the band a route's display title and its plate live in.
     A layout measure, not a backdrop: the shared ambient sky it used to hold is
     retired (CONSTRAINTS.md, 2026-09-11). */
  --opening-height: clamp(176px, 21vh, 236px);

  /* The paper fibre. One decorative layer, owned by material.css and nowhere
     else. Generated by scripts/build-paper-texture.py and served locally; never
     fetched from another origin.

     There is no ruling token here and no ruling layer anywhere. The full-page
     repeating horizontal rule was the loudest childish signal in the old
     system, and it is superseded (CONSTRAINTS.md, 2026-09-11). Paper is read
     through fibre and print edges; rules appear only where they organise real
     content. */
  --paper-texture: url("/textures/paper-fiber.webp");
  --paper-texture-size: 320px;

  /* How much of the fibre is allowed through. On a mineral ground this was 0.5;
     on white, the same layer would turn the sheet grey, and white has to read
     as white at ordinary zoom. Measured: the lightest pixel of the composited
     page stays at #ffffff and the mean sits above 253/255. */
  --paper-texture-opacity: 0.22;

  /* The focus ring is ink on the page and white on the field. Both are named,
     because a black ring on a black field is not a ring — every inverse surface
     in the system sets --focus-ring-color to --focus-ring-on-deep locally, and
     `test/interaction-states.test.js` proves each one does. */
  --focus-ring-color: var(--ink);
  --focus-ring-on-deep: var(--white);

  --z-background: 0;

  /* Decorative paper fibre: below every child of the working field, above the
     field's own paper fill. Negative, and therefore confined to main's own
     stacking context. */
  --z-page-texture: -1;
  --z-content: 1;
  --z-sticky: 80;
  --z-dropdown: 90;
  --z-menu: 100;
  --z-modal: 300;
  --z-page-transition: 400;
  --z-skip-link: 500;

  --ink-92: light-dark(rgb(0 0 0 / 92%), rgb(237 237 237 / 92%));
  --ink-72: light-dark(rgb(0 0 0 / 72%), rgb(237 237 237 / 72%));
  --ink-18: var(--separator);
  --ink-10: light-dark(rgb(0 0 0 / 10%), rgb(255 255 255 / 10%));
  --ink-06: light-dark(rgb(0 0 0 / 6%), rgb(255 255 255 / 6%));

  --white-92: rgba(255, 255, 255, 0.92);
  --white-72: rgba(255, 255, 255, 0.78);
  --white-54: rgba(255, 255, 255, 0.58);
  --white-18: rgba(255, 255, 255, 0.22);
  --white-10: rgba(255, 255, 255, 0.13);
  --white-06: rgba(255, 255, 255, 0.08);

  /* ---------------------------------------------------- identity patterns */

  /* The profile accent used to be a hue: Green, Amber, Blue, Rose, Neutral.
   * A monochrome press cannot show a hue, and five identical grey swatches
   * would be a lie told five times, so the choice survives as what a press
   * would actually have used — five printed fills.
   *
   * Every pattern is drawn in `currentColor`, so the same five read as ink on
   * the white sheet and as white on the black chrome without a second set.
   * The stored key is untouched: `green` still means `green` in the database
   * and in the API, and only its presentation and its label changed.
   * See CONSTRAINTS.md, 2026-09-12. */
  --accent-pattern-solid: linear-gradient(currentColor, currentColor);
  --accent-pattern-hatch: repeating-linear-gradient(45deg, currentColor 0 1.5px, transparent 1.5px 4px);
  --accent-pattern-cross:
    repeating-linear-gradient(45deg, currentColor 0 1px, transparent 1px 4px),
    repeating-linear-gradient(-45deg, currentColor 0 1px, transparent 1px 4px);
  --accent-pattern-stipple: radial-gradient(currentColor 0.8px, transparent 0.9px);
  --accent-pattern-stipple-size: 3.5px 3.5px;
  --accent-pattern-bar: repeating-linear-gradient(0deg, currentColor 0 1.5px, transparent 1.5px 4.5px);

  --paper-quiet: var(--surface-subtle);

  /* Primary actions use the black field and its dark-grey hover step. */
  --action: var(--ink);
  --action-hover: var(--ink-raised);
  --on-action: var(--paper);

  /* State borders follow their colors. Notices also retain different rule
     weights and explicit labels so their meaning does not depend on hue. */
  --success-tint: var(--success-soft);
  --success-line: var(--success);
  --danger-tint: var(--danger-soft);
  --danger-line: var(--danger);

  --bg: var(--canvas);
  --bg-page: var(--canvas);
  --surface-1: var(--surface);
  --surface-2: var(--canvas);
  --surface-elevated: var(--paper-raised);

  --text: var(--ink);
  --text-primary: var(--ink);
  --text-secondary: var(--ink-muted);
  --text-muted: var(--ink-muted);

  --border: var(--separator);
  --border-subtle: var(--separator);
  --border-strong: var(--rule-strong);
  --border-control: var(--rule-strong);
  --list-border: var(--ink-10);

  --link: var(--brand);
  --link-card-hover-border: var(--ink);

  --shadow-soft: none;
  --shadow-card-hover: none;
  --shadow-panel: var(--shadow-float);
  --shadow-pop: var(--shadow-dialog);
  --edge-light: none;

  --glass-1: var(--surface);

  --radius: var(--radius-panel);
  --radius-tag: var(--radius-row);
}

/* A saved choice pins the scheme; "system" (or no choice) leaves it to the OS. */
:root[data-theme="light"] {
  color-scheme: light;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}

/* The fibre is a light texture drawn for a white sheet; on the night sheet it
   would only grey the ground, so it drops out. This is the one scheme-specific
   value that is not a colour, which is why it cannot be a light-dark() pair. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper-texture-opacity: 0;
  }
}

:root[data-theme="dark"] {
  --paper-texture-opacity: 0;
}

/* ------------------------------------------------------- mobile token layer
 *
 * One documented block for the phone range, at the same 760px boundary the
 * header already switches on, so navigation and spacing change together rather
 * than at two different widths.
 *
 * Nothing new is introduced here: every entry re-points a token the desktop
 * sheet already defines, so each consumer follows without inventing a second
 * rule of its own. The three relationship bands keep their meaning — related,
 * sibling, region — and only the sibling step tightens, from 32px to 24px,
 * which is still inside the band `test/grouping-contract.test.js` holds.
 *
 * The page opening loses its reserved height rather than its content: on a
 * 844px-tall phone a 177px band spends a fifth of the screen before the first
 * word, and a route's title and dateline size themselves perfectly well.
 */
@media (max-width: 760px) {
  :root {

    /* The band is now exactly as tall as the title and dateline it holds. */
    --opening-height: 0px;

    --section-gap: var(--space-5);
    --gap-sibling: var(--space-5);
  }
}

/* styles/globals.css */
html,
body,
#root {
  min-height: 100%;
}

html {
  scroll-padding-top: calc(var(--header-height) + var(--space-3));
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

img,
svg {
  -webkit-user-drag: none;
  user-select: none;
}

/* Form controls do not inherit the document's font: left alone, a button or a
   select falls back to the UA's own family and size, which put Arial at 13.3px
   inside an otherwise handwritten interface. Zero specificity, so any component
   rule still wins — this only catches what nothing else styles. */
:where(button, input, select, textarea, optgroup) {
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  letter-spacing: inherit;
}

.sr-only,
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

html {
  background: var(--bg);
}

body {

  background: transparent;
  color: var(--text);
  font-family: var(--font-ui);

  /* The product ships one real 400 face. A synthesised bold or italic would be
     a smeared outline of it, so the browser is told never to invent either. */
  font-synthesis: none;
  font-size: var(--text-body);
  font-weight: var(--weight-body);
  line-height: var(--leading-body);
  letter-spacing: 0;

  /* No font-smoothing override. `-webkit-font-smoothing: antialiased` drops
     macOS from subpixel to grayscale rendering, which thins every stroke — the
     last thing a monoline handwritten face can afford. `optimizeLegibility` is
     also gone: it buys nothing here (the ligature/kerning work already comes
     from the font's own GPOS) and has a history of rendering bugs. Each
     platform now rasterises the text its own normal way.
     Stroke weight is solved in the font outline, not by CSS. */
}

:where(h1, h2, h3, h4, h5, h6) {
  font-weight: var(--weight-body);
  line-height: var(--leading-title);
  text-wrap: balance;

  /* A shared reflow correction, at zero specificity so any component rule
     still wins. A heading is the one place the product reliably sets a long
     unbreakable word at a large size, and under 200% text enlargement at
     320px that word is what pushes a document sideways. Last-resort breaking
     only: ordinary headings wrap between words exactly as before. */
  overflow-wrap: break-word;
}

main {
  min-height: calc(100vh - var(--header-height));
  display: flex;
  flex-direction: column;
  background: transparent;
  color: var(--text);
}

:where(a, button, [role="button"], input, select, textarea, summary):focus-visible {
  outline: 2px solid var(--focus-ring-color);
  outline-offset: -1px;
}

:root[data-input-modality="pointer"] :where(input, select, textarea):focus-visible {
  outline: none;
}

:where(a, button, [role="button"], input, select, textarea, summary, label) {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.skip-link {
  position: fixed;
  top: var(--space-2);
  left: var(--space-2);
  z-index: var(--z-skip-link);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-control);

  background: var(--white);
  color: var(--ink);
  box-shadow: var(--shadow-pop);
  font-weight: var(--weight-body);
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-4)));
  transition: transform var(--duration-ui) var(--ease-out);
}

.skip-link:focus-visible {
  transform: translateY(0);
}

main:focus {
  outline: none;
}

.battle-active {

  font-family: var(--font-ui);
  font-size: var(--text-control);
  line-height: 1.6;
}

.container {
  width: 100%;
  max-width: var(--measure-wide);
  margin-inline: auto;
  padding-inline: var(--padding);
  padding-block: var(--padding);
}

.card {
  background: var(--surface-elevated);
  border-radius: var(--radius);
  padding: var(--space-5);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-soft);
}

.flex {
  display: flex;
  align-items: center;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {

    animation-iteration-count: 1 !important;
  }

  *,
  *::before,
  *::after {
    transition-property: opacity, background-color, border-color, color, fill, stroke, outline-color !important;
    transition-duration: 200ms !important;
    transition-timing-function: ease !important;
  }
}

/* styles/shell.css */
.page-shell {
  width: 100%;
  max-width: var(--measure-wide);
  margin-inline: auto;
  padding-inline: var(--padding);

  padding-block: var(--section-gap);
}

.page-shell--focus {
  max-width: var(--measure-focus);
}

.page-shell--reading {
  max-width: var(--measure-reading);
}

.page-shell > * {
  min-width: 0;
}

.t-display,
.t-page-title,
.t-section-title,
.t-component-title,
.t-body,
.t-secondary,
.t-data,
.t-reading,
.t-eyebrow {
  font-family: var(--font-ui);
  font-weight: var(--weight-body);
}

.t-display {
  font-size: var(--text-display);
  line-height: var(--leading-display);
  letter-spacing: 0;
  text-wrap: balance;
}

.t-page-title {
  font-family: var(--font-display);
  font-size: var(--text-page-title);
  font-weight: var(--weight-body);
  line-height: var(--leading-display);
  letter-spacing: 0;
  text-wrap: balance;
}

.t-section-title {
  font-size: var(--text-title);
  line-height: var(--leading-title);
  letter-spacing: 0;
  text-wrap: balance;
}

.t-component-title {
  font-size: var(--text-subtitle);
  line-height: var(--leading-title);
  letter-spacing: 0;
}

.t-body {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

.t-secondary {
  color: var(--ink-muted);
  font-size: var(--text-meta);
  line-height: var(--leading-meta);
  text-wrap: pretty;
}

.t-data {
  font-size: var(--text-meta);
  line-height: 1.4;
  letter-spacing: 0.01em;
}

.t-reading {
  font-size: var(--text-body);
  line-height: 1.62;
  text-wrap: pretty;
}

.t-eyebrow {
  color: var(--ink-secondary-strong);
  font-size: var(--text-meta);
  line-height: 1.4;
  letter-spacing: 0.01em;
}

/* The page opening: a typographic band aligned to the same grid as every
   operational region below it, so the title and content share one left edge. */
.page-opening {
  margin-inline: calc(50% - 50vw);
  padding-inline: max(calc(50vw - 50%), env(safe-area-inset-left));
  padding-block: var(--section-gap) var(--space-5);
  min-block-size: var(--opening-height);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.page-opening-inner {
  width: 100%;
  max-width: var(--measure-wide);
  margin-inline: auto;
  padding-inline: var(--padding);
}

.page-head {
  display: grid;

  /* `minmax(0, 1fr)` rather than the implicit `auto`: an auto column grows to
     the widest unbreakable word, so at 200% text a route name in the display
     face pushed the document sideways and `overflow-wrap` never got the chance
     to break it. Measured at 390px with a 32px root: Community overflowed by
     88px before this, and by nothing after. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2);

  margin-block-end: var(--gap-region);
}

/* And the items inside it have to be allowed to be narrower than their longest
   word. A grid item's `min-width: auto` is its min-content width, and a browser
   does not count `overflow-wrap: break-word` when computing that — so without
   this the title sizes itself to "COMMUNITY" at 56px and overflows the track it
   was just given. Measured at 390px with a 32px root: 88px of overflow before,
   none after. */
.page-head > * {
  min-width: 0;
}

.page-head-eyebrow {
  color: var(--ink-muted);
  font-family: var(--font-data);
  font-weight: var(--weight-body);
  font-size: var(--text-mark);
  line-height: var(--leading-meta);
  letter-spacing: var(--tracking-mark);
  text-transform: uppercase;
}

/* A route names itself once, at display scale, in the one expressive face the
   system has. Capitals here are a typographic treatment: the markup writes the
   title in sentence case, which is what a screen reader announces. */
.page-head-title {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-page-title);
  font-weight: var(--weight-display-heavy);
  line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
  text-transform: uppercase;
  text-wrap: balance;
  overflow-wrap: break-word;
}

/* The line under the title is the page's own sentence, set in the body face at
   a real reading measure. */
.page-head-lede {
  max-width: var(--measure-prose);
  margin: 0;
  color: var(--ink-muted);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

.page-head-copy {
  display: grid;

  /* The same cap and the same escape as `.page-head` above: this inner grid is
     where most routes actually put their title, so without it the title is
     sized by its longest word one level further in. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2);
  min-width: 0;
}

.page-head-copy > * {
  min-width: 0;
}

.page-head--with-action {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  column-gap: var(--space-5);
}

.page-head--with-action > .page-head-eyebrow,
.page-head--with-action > .page-head-title,
.page-head--with-action > .page-head-lede {
  grid-column: 1;
}

.page-head-actions {
  grid-column: 2;
  grid-row: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

@media (max-width: 640px) {
  .page-head--with-action {
    grid-template-columns: minmax(0, 1fr);
  }

  .page-head-actions {
    grid-column: 1;
    grid-row: auto;
    margin-block-start: var(--space-2);
  }
}

.page-section {
  margin-block-end: var(--gap-sibling);
}

.page-section--region {
  margin-block-end: var(--gap-region);
}

.page-section:last-child {
  margin-block-end: 0;
}

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  margin-block-end: var(--space-4);
}

/* A section opening, one step below the page opening. Fluid, because a long
   heading in the display face is the one thing that reliably breaks a 320px
   screen, and `overflow-wrap` is the floor under that rather than the plan. */
.section-head-title {
  margin: 0;
  min-width: 0;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3.2vw, 2rem);
  overflow-wrap: break-word;
  font-weight: var(--weight-display);
  line-height: var(--leading-title);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}

.section-head-note {
  flex: 0 1 auto;
  min-width: 0;
  color: var(--ink-muted);
  font-family: var(--font-data);
  font-size: var(--text-meta);
  line-height: var(--leading-meta);
  letter-spacing: var(--tracking-mark);
  text-transform: uppercase;
}

.section-lede {
  max-width: var(--measure-reading);
  margin-block: calc(var(--space-2) * -1) var(--space-4);
  color: var(--ink-muted);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

.section-head-title + .section-lede {
  margin-block-start: var(--space-2);
}

.section-frame {
  display: block;
}

.section-frame > * + * {
  margin-block-start: var(--gap-related);
}

.section-frame > .section-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-block-start: var(--space-4);
}

.section-frame--ruled {
  position: relative;
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--ink);
}

.section-frame--ruled::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--ink);
  transform: translateY(-50%);
}

.section-frame--ruled[data-waypoint="open"]::before {
  background: var(--paper);
  box-shadow: inset 0 0 0 2px var(--ink);
}

.section-frame--ruled.surface-panel {
  border-block-start-color: var(--ink);
}

.surface-panel {
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  background: var(--surface);
}

.surface-panel--subtle {
  border-color: transparent;
  background: var(--surface-subtle);
}

.surface-panel--feature {
  border-radius: var(--radius-feature);
}

.surface-pad {
  padding: clamp(var(--space-4), 2.4vw, var(--space-5));
}

.rule-top {
  border-block-start: 1px solid var(--separator);
}

/* ------------------------------------------------------ phone-folded detail
 *
 * One disclosure primitive for optional detail that is worth folding on a
 * phone and not worth folding anywhere else: a correction backlog, a password
 * form, a composer, a filter row.
 *
 * The server renders every one of these <details> open, so the content is in
 * the document and visible with no script at all; `app.js` folds the ones it
 * is told to fold, and only while the window is inside the phone range. Above
 * that range the expander has nothing to do, so it is removed and the page is
 * exactly what it was.
 */
.fold-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding-block: var(--space-2);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--text-control);
  font-weight: var(--weight-body);
  cursor: pointer;

  /* The browser's own triangle would be a second marker beside the caret the
     component draws, and it cannot be positioned. */
  list-style: none;
}

.fold-summary::-webkit-details-marker {
  display: none;
}

/* The expander's mark: a printed caret turned by the state the element already
   owns, rather than by a second attribute that could disagree with it. */
.fold-summary-label {
  display: inline-flex;
  align-items: center;
  min-width: 0;
}

.fold-summary-label::after {
  content: "";
  flex: none;
  width: 0.45em;
  height: 0.45em;
  margin-inline-start: 0.55em;
  border-inline-end: 2px solid currentColor;
  border-block-end: 2px solid currentColor;
  transform: translateY(-0.15em) rotate(45deg);
  transition: transform var(--duration-ui) var(--ease-out);
}

details[open] > .fold-summary .fold-summary-label::after {
  transform: translateY(0.1em) rotate(225deg);
}

/* The folded content itself.
 *
 * A component that gives this element a `display` of its own — and most of
 * them do, because they are grids — overrides the browser's own hiding and
 * leaves a closed disclosure showing everything it was supposed to fold. One
 * explicit rule closes that hole for every fold in the product, rather than
 * each page remembering to. Measured: without it, a closed <details> whose
 * child declares `display: grid` renders open in both Chromium and WebKit. */
details:not([open]) > .fold-body {
  display: none;
}

/* A count that stands in for the folded content while it is closed. */
.fold-summary-count {
  flex: none;
  min-width: 1.75rem;
  padding: 2px var(--space-2);
  border-radius: var(--radius-row);
  background: var(--ink);
  color: var(--on-action);
  font-family: var(--font-data);
  font-size: var(--text-meta);
  text-align: center;
}

@media (min-width: 761px) {

  .fold-summary {
    display: none;
  }
}

.page-shell a:not([class]),
.link {
  color: var(--brand);
  text-decoration-color: color-mix(in srgb, var(--brand) 38%, transparent);
  text-underline-offset: 0.18em;
  transition: color var(--duration-ui) var(--ease-out);
}

.page-shell a:not([class]):hover,
.link:hover,
.page-shell a:not([class]):focus-visible,
.link:focus-visible {
  color: var(--brand-hover);
  text-decoration-color: currentColor;
}

@keyframes page-enter {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

html.page-entering main {
  animation: page-enter var(--duration-panel) var(--ease-out) both;
}

@media (prefers-reduced-motion: reduce) {

  html.page-entering main {
    animation: none;
    transition: opacity 200ms ease;
  }
}

@keyframes row-reveal {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

.is-revealing {
  animation: row-reveal var(--duration-ui) var(--ease-out) both;
  animation-delay: calc(var(--reveal-index, 0) * 30ms);
}

@keyframes status-settle {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

.is-settling {
  animation: status-settle 180ms var(--ease-out) both;
}

@keyframes unlock-settle {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
}

.is-unlocking {
  animation: unlock-settle var(--duration-panel) var(--ease-out) both;
}

@media (prefers-reduced-motion: reduce) {

  .is-revealing,
  .is-settling,
  .is-unlocking {
    animation: none;
    transition: opacity 200ms ease;
  }
}

/* styles/components/brand.css */
.brand-lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  color: inherit;
}

.brand-symbol {
  flex: none;
  width: 1.45em;
  height: 1.45em;
  transform: translateY(-0.03em);
}

/* The wordmark is the one place the display face carries the product's name.
   It is drawn in Unbounded at the heavy display step — the project's own
   choice, not lettering traced from any reference. */
.brand-wordmark {
  font-family: var(--font-display);
  /* Capped against the viewport as well as the root: the wordmark is one
     unbreakable word, so under 200% text enlargement on a 320px screen it was
     259px wide and pushed the document sideways. `min()` is inert at ordinary
     text sizes — at 320px and a 16px root the rem value still wins — and only
     bites when the text is enlarged far enough to break reflow. */
  font-size: min(var(--text-component), 7vw);
  font-weight: var(--weight-display);
  line-height: 1.2;
  letter-spacing: var(--tracking-display);
  white-space: nowrap;
}

.brand-lockup--chrome {
  color: var(--white);
}

.brand-lockup--paper {
  color: var(--ink);
}

/* An opening lockup, one step up from the header's. */
.brand-lockup--lead .brand-wordmark {
  font-size: var(--text-title);
}

/* The auth lockup sits above the form, with a readable symbol and wordmark. */
.brand-lockup--masthead {
  display: flex;
  align-items: center;
  gap: clamp(var(--space-2), 1.4vw, var(--space-4));
  color: var(--ink);
}

.brand-lockup--masthead .brand-wordmark {
  font-family: var(--font-display);

  font-size: min(clamp(1.25rem, 3vw, 1.75rem), 8vw);
  font-weight: var(--weight-display-heavy);
  line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
  text-transform: uppercase;
}

.brand-lockup--masthead .brand-symbol {
  width: 2rem;
  height: 2rem;
  transform: none;
}

@media (max-width: 480px) {
  .brand-wordmark {
    font-size: min(var(--text-control), 7vw);
  }

  .brand-lockup--lead .brand-wordmark {
    font-size: min(var(--text-component), 8vw);
  }
}

@media (max-width: 360px) {
  .brand-wordmark {
    letter-spacing: 0;
  }
}

/* styles/components/material.css */
/* Sacred Press material.
 *
 * The page is one printed sheet: a flat mineral ground, one fibre layer, and
 * ink. There is no full-page ruling, no sticker, no rotation, no contact shadow
 * and no floating card — all five are recorded as superseded in CONSTRAINTS.md
 * (2026-09-11).
 *
 * A group is made with exactly one boundary: spacing, one thin rule, a tonal
 * fill, or a black field. Never two at once, and never one region inside
 * another.
 */

/* ---------------------------------------------------------------- the sheet */

/* The working field is the paper itself. Flat: a gradient here is what used to
   wash a photograph up behind the content. */
main {
  background: var(--paper);
}

/* The fibre. One layer, owned here and nowhere else.
 *
 * Fixed rather than absolute, so scrolling never repaints it: the element stays
 * put in the viewport and the browser can keep it on its own layer. It is
 * pointer-events: none and aria-invisible, sits under every child of the
 * document, and paints a single flat tile: no filter, no blend mode, no moving
 * layer. Nothing here can make a scrolling container recomposite.
 */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-page-texture);
  pointer-events: none;
  opacity: var(--paper-texture-opacity);
  background-image: var(--paper-texture);
  background-repeat: repeat;
  background-size: var(--paper-texture-size) var(--paper-texture-size);
}

/* ------------------------------------------------------------------ regions */

/* One boundary per group. The default is a hairline rule drawn from the ink, so
   it reads as printing rather than as a UI border, and the sheet shows straight
   through — a region is an area of the page, not an object laid on it. */
.press-region {
  position: relative;
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  background: transparent;
}

/* Never nest. An inner boundary inside an outer one loses its own edge and
   keeps only its spacing, so a nesting mistake degrades to the right answer
   instead of drawing a box inside a box.
   `.surface-panel` and `.data-table-wrap` are the other two primitives that
   draw a boundary, so they are absorbed the same way. */
.press-region .press-region,
.press-region .surface-panel,
.press-region .data-table-wrap,
.surface-panel .surface-panel,
.surface-panel .press-region {
  border-color: transparent;
  background: transparent;
}

/* ------------------------------------------------------------------- plates */

/* A printed plate: an image with a cut edge, sitting on the page like a pasted
   photograph. The ratio is declared by the consumer so the box is reserved
   before the file arrives and nothing shifts when it does.
   The ground is the muted paper, so a plate whose image fails to load is still
   a deliberate tonal block rather than a hole. */
.press-plate {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: 0;
  background: var(--paper-muted);
  aspect-ratio: var(--plate-ratio, 3 / 2);
}

.press-plate > img,
.press-plate > picture,
.press-plate > picture > img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: var(--plate-position, center);
}

/* A reserved plate: the slot exists, the box is held, and no file has arrived
   yet. It reads as a deliberate tint block with a printed edge — the sheet's
   own fibre showing through a ruled rectangle — rather than as a grey void or
   a stock placeholder. The moment an image is placed inside it, `:empty` stops
   matching and it becomes an ordinary plate. */
.press-plate:empty {
  border: 1px solid var(--rule);
  background: transparent;
}

/* A plate whose file did not arrive. The box was reserved before the request
   was made, so nothing moves; the recessed ground is already painted, so the
   region is still a deliberate tonal block on the sheet. All that is removed
   here is the browser's own broken-image glyph, which the page did not draw.
   The attribute is set by app.js; with no script the plate keeps the glyph and
   everything else about the page is unchanged. */
.press-plate[data-plate-failed="true"] > picture,
.press-plate[data-plate-failed="true"] > img {
  visibility: hidden;
}

/* -------------------------------------------------------------------- icons */

/* One family, one geometry. Every icon is a 24-unit square drawn in Phosphor's
   regular weight — a 1.5px stroke on a 24px grid — so nothing has to be sized
   or re-weighted per component. The box scales with the text it sits beside and
   takes its colour from `currentColor`, which is what keeps an icon legible on
   paper, on a tonal region and on the black field without a single override. */
.ui-icon {
  display: inline-block;
  flex: none;
  inline-size: 1.25em;
  block-size: 1.25em;
  vertical-align: -0.22em;
  fill: currentColor;
}

/* Beside a line of running text, where a full 1.25em reads as a bullet. */
.ui-icon--sm {
  inline-size: 1em;
  block-size: 1em;
  vertical-align: -0.14em;
}

/* The mark that opens a titled module. It sits on the title's own baseline and
   takes the muted ink, so it reads as a printer's mark beside the heading
   rather than as a second, competing heading. */
.practice-mark {
  inline-size: 1em;
  block-size: 1em;
  margin-inline-end: 0.45em;
  color: var(--ink-muted);
  vertical-align: -0.1em;
}

/* --------------------------------------------------------- dashboard shell */

.dashboard-head {
  margin-bottom: var(--space-5);
}

.dashboard-kicker {
  margin: 0 0 var(--space-1);
  color: var(--ink-secondary-strong);
  font-family: var(--font-data);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  letter-spacing: var(--tracking-mark);
  text-transform: uppercase;
}

.dashboard-title {
  margin: 0;
  color: var(--text-primary);
  font-size: var(--text-page-title);
  font-weight: var(--weight-strong);
  line-height: var(--leading-title);
  letter-spacing: 0;
}

/* ------------------------------------------------------- reader preferences */

/* The fibre is the one decorative layer on the page, so it is also the first
   thing to go when a reader asks for a plainer surface. The paper underneath is
   a solid colour and loses nothing. */
@media (prefers-reduced-transparency: reduce) {
  body::before {
    display: none;
  }
}

@media (prefers-contrast: more) {
  body::before {
    display: none;
  }

  .press-region {
    border-color: var(--ink);
  }

  .press-plate {
    border: 1px solid var(--ink);
  }
}

@media (forced-colors: active) {
  body::before {
    display: none;
  }

  .ui-icon {
    forced-color-adjust: auto;
    fill: CanvasText;
  }

  .press-region {
    border: 1px solid CanvasText;
    background: Canvas;
    color: CanvasText;
  }

  .press-plate {
    forced-color-adjust: none;
  }
}

/* Save-Data readers get the flat ground with no decorative fibre request. */
@media (prefers-reduced-data: reduce) {
  body::before {
    display: none;
  }
}

@media print {
  body::before {
    display: none;
  }

  main {
    background: #fff;
  }

}

/* styles/components/buttons.css */
.ui-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  padding-inline: var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  background: none;
  color: var(--ink);
  font: inherit;

  font-family: var(--font-ui);
  font-size: var(--text-control);
  font-weight: var(--weight-body);
  line-height: 1.4;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
  -webkit-user-drag: none;
  transition:
    background-color var(--duration-ui) var(--ease-out),
    border-color var(--duration-ui) var(--ease-out),
    color var(--duration-ui) var(--ease-out),
    transform var(--duration-press) var(--ease-out);
}

.ui-button[hidden] {
  display: none;
}

.ui-button:active:not(:disabled) {
  transform: scale(0.98);
}

.ui-button:disabled,
.ui-button[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.5;
  transform: none;
}

/* A focus ring has to be visible against the thing it rings. The two filled
   controls are black fields, so their ring is white; the destructive control is
   paper now, so it keeps the ordinary ink ring. */
.ui-button--primary:focus-visible,
.ui-button--accent:focus-visible {
  outline-color: var(--focus-ring-on-deep);
}

.ui-button--primary {
  background: var(--action);
  color: var(--on-action);
}

/* Accent and primary actions share the same black field. */
.ui-button--accent {
  background: var(--accent);
  color: var(--on-accent);
  border-color: transparent;
}

@media (hover: hover) and (pointer: fine) {
  .ui-button--accent:hover:not(:disabled) {
    background: var(--accent-hover);
  }

  .ui-button--danger:hover:not(:disabled) {
    background: var(--danger-soft);
    border-inline-start-color: var(--danger-line);
  }

  .ui-button--primary:hover:not(:disabled) {
    background: var(--action-hover);
  }
}

.ui-button--primary:focus-visible {
  background: var(--action-hover);
}

.ui-button--secondary {
  border-color: var(--border-control);
  background: var(--surface);
  color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .ui-button--secondary:hover:not(:disabled) {
    background: var(--surface-subtle);
    border-color: var(--ink-muted);
  }
}

.ui-button--secondary:focus-visible {
  background: var(--surface-subtle);
  border-color: var(--ink-muted);
}

.ui-button--quiet {
  padding-inline: var(--space-3);
  color: var(--ink-secondary-strong);
  font-weight: var(--weight-body);
}

@media (hover: hover) and (pointer: fine) {
  .ui-button--quiet:hover:not(:disabled) {
    background: var(--ink-06);
    color: var(--ink);
  }
}

.ui-button--quiet:focus-visible {
  background: var(--ink-06);
  color: var(--ink);
}

/* Destructive actions use red text and a heavy leading rule. */
.ui-button--danger {
  border-color: var(--rule-strong);
  border-inline-start: 3px solid var(--danger-line);
  background: var(--paper);
  color: var(--danger);
  font-weight: var(--weight-strong);
}

.ui-button--danger:focus-visible {
  background: var(--danger-soft);
  border-inline-start-color: var(--danger-line);
}

.ui-button--icon {
  padding-inline: 0;
  width: var(--tap-min);
}

.ui-button--block {
  width: 100%;
}

.ui-button--large {
  min-height: 48px;
  padding-inline: var(--space-5);
  font-size: var(--text-subtitle);
}

.ui-button[data-loading="true"] {
  position: relative;
  color: transparent;
  pointer-events: none;
}

.ui-button[data-loading="true"]::after {
  content: "";
  position: absolute;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-radius: 50%;
  border-block-start-color: transparent;
  color: var(--ink);
  animation: ui-button-spin 620ms linear infinite;
}

/* The spinner rides whatever the control's own ground is: the reversed paper on
   the filled primary, ink on the paper-ground destructive control. */
.ui-button--primary[data-loading="true"]::after,
.ui-button--accent[data-loading="true"]::after {
  color: var(--on-action);
}

@keyframes ui-button-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ui-button[data-loading="true"]::after {
    animation: ui-button-wait 900ms ease-in-out infinite;
  }

  @keyframes ui-button-wait {
    0%,
    100% {
      opacity: 0.35;
    }

    50% {
      opacity: 1;
    }
  }
}

.ui-button-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

@media (prefers-contrast: more) {
  .ui-button--secondary,
  .ui-button--quiet {
    border-color: var(--ink);
    color: var(--ink);
  }
}

/* styles/components/fields.css */
.field {
  display: grid;
  gap: var(--space-2);
  min-width: 0;
}

.field-label {
  color: var(--ink);
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  line-height: 1.4;
}

.field-hint {
  color: var(--ink-secondary-strong);
  font-size: var(--text-label);
  line-height: 1.45;
  text-wrap: pretty;
}

.field-input,
.field-textarea,
.field-select {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-3);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-control);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-control);
  line-height: 1.4;
  transition:
    border-color var(--duration-ui) var(--ease-out),
    box-shadow var(--duration-ui) var(--ease-out);
}

.field-textarea {
  min-height: calc(var(--tap-min) * 2.5);
  resize: vertical;

  overflow-wrap: anywhere;
}

.field-input::placeholder,
.field-textarea::placeholder {
  color: var(--ink-muted);
}

.field-input:disabled,
.field-textarea:disabled,
.field-select:disabled {
  background: var(--surface-subtle);
  color: var(--ink-muted);
  cursor: not-allowed;
}

/* An invalid field used to turn red. The press has one ink, so it thickens
   instead: 2px of solid ink where a valid field carries a 1px #767676 rule. The
   weight is the signal, and the message below names the problem in words. */
.field-input[aria-invalid="true"],
.field-textarea[aria-invalid="true"],
.field-select[aria-invalid="true"] {
  border-color: var(--danger);
  border-width: 2px;
}

/* The message is set in the ink and opened with a warning mark from the shared
   icon family, so a failure is recognised by its shape before it is read — and
   it stays recognisable in greyscale, in forced colours, and to a reader who
   cannot distinguish the two states by colour at all. */
.field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  color: var(--danger);
  font-size: var(--text-label);
  font-weight: var(--weight-strong);
  line-height: 1.45;
  text-wrap: pretty;
}

.field-error-mark {
  flex: none;
  margin-block-start: 0.18em;
}

.field-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.field-row:focus-within .field-label {
  color: var(--brand);
}

.field-textarea--writing {
  max-width: var(--measure-reading);
  min-height: calc(var(--tap-min) * 4);
  font-family: var(--font-reading);
  font-size: var(--text-body);
  line-height: var(--leading-body);
}

@media (prefers-contrast: more) {
  .field-input,
  .field-textarea,
  .field-select {
    border-color: var(--ink);
  }
}

/* styles/components/lists.css */
.settings-group {
  margin-block-end: var(--gap-sibling);
}

.settings-group:last-child {
  margin-block-end: 0;
}

.settings-group-title {
  margin: 0 0 var(--space-3);
  color: var(--ink);
  font-size: var(--text-subtitle);
  font-weight: var(--weight-body);
  line-height: 1.4;
  letter-spacing: 0;
}

.settings-group-note {
  max-width: var(--measure-reading);
  margin: calc(var(--space-2) * -1) 0 var(--space-3);
  color: var(--ink-secondary-strong);
  font-size: var(--text-label);
  line-height: 1.5;
  text-wrap: pretty;
}

.settings-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-4);
  min-width: 0;
}

.settings-row + .settings-row {
  border-block-start: 1px solid var(--separator);
}

.settings-row-label {
  flex: 1 1 14rem;
  min-width: 0;
  display: grid;
  gap: 2px;
}

.settings-row-control {
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.stat-line {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  margin: 0;
  padding: 0;
  border-block: 1px solid var(--separator);

  background: var(--paper);
}

.stat-line-item {
  flex: 1 1 9rem;
  min-width: 0;
  padding: var(--space-4);
  display: grid;
  gap: 2px;
}

.stat-line-item + .stat-line-item {
  border-inline-start: 1px solid var(--separator);
}

.stat-line-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--ink-secondary-strong);
  font-family: var(--font-data);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  letter-spacing: var(--tracking-mark);
  text-transform: uppercase;
}

/* A headline figure. Set in the display face, because a streak count is one of
   the few numerals in the product the page is actually about — everything
   smaller stays in the mono data role. */
.stat-line-value {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-figure);
  font-weight: var(--weight-display);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
}

.stat-line-note {
  color: var(--ink-secondary-strong);
  font-family: var(--font-data);
  font-size: var(--text-meta);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 560px) {
  .stat-line-item {
    flex-basis: 100%;
  }

  .stat-line-item + .stat-line-item {
    border-inline-start: 0;
    border-block-start: 1px solid var(--separator);
  }
}

/* styles/components/tables.css */
.data-table-wrap {
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  background: var(--surface);

  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-data);

  /* The data role is IBM Plex Mono, whose digits already share one advance, so
     a column of figures lines up without help. The declaration stays for the
     fallback stack, where a proportional face would otherwise let the rank and
     figure columns shift under each other. Alignment and the reserved widths
     below remain the load-bearing mechanism either way. */
  font-variant-numeric: tabular-nums;
}

.data-table caption {
  padding: var(--space-3) var(--space-4);
  color: var(--ink-secondary-strong);
  font-size: var(--text-label);
  text-align: start;
}

.data-table th,
.data-table td {
  padding: var(--space-4) var(--space-4);
  text-align: start;
  vertical-align: middle;
}

.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  border-block-end: 1px solid var(--separator);
  background: var(--surface);
  color: var(--ink-secondary-strong);
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.data-table tbody tr + tr th,
.data-table tbody tr + tr td {
  border-block-start: 1px solid var(--separator);
}

.data-table tbody td {
  color: var(--ink);
  font-size: var(--text-control);
}

@media (hover: hover) and (pointer: fine) {
  .data-table tbody tr:hover td,
  .data-table tbody tr:hover th {
    background: var(--ink-06);
  }
}

.data-table tbody tr:focus-within td,
.data-table tbody tr:focus-within th {
  background: var(--ink-06);
}

.data-table a {
  color: var(--brand);
  font-weight: var(--weight-body);
  text-decoration: none;
  overflow-wrap: anywhere;
}

@media (hover: hover) and (pointer: fine) {
  .data-table a:hover {
    color: var(--brand-hover);
    text-decoration: underline;
    text-underline-offset: 0.18em;
  }
}

.data-table a:focus-visible {
  color: var(--brand-hover);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

.data-table tr[data-current-user] td,
.data-table tr[data-current-user] th {
  background: var(--surface-subtle);
}

.data-table-you {
  margin-inline-start: var(--space-2);
  color: var(--ink-secondary-strong);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  letter-spacing: 0.02em;
}

.data-table-name {
  display: block;
  min-width: 0;
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.data-table .data-table-rank {
  width: 3.5rem;
  color: var(--ink-secondary-strong);
  font-variant-numeric: tabular-nums;

  /* Outranks the `.data-table th, .data-table td { text-align: start }` base,
     which previously beat these column classes on specificity and left every
     figure ragged. Header and body cells share the class, so both move. */
  text-align: end;
  padding-inline-end: var(--space-3);
}

.data-table .data-table-figure {
  text-align: end;
  font-weight: var(--weight-body);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;

  /* A reserved column keeps "1 day" and "128 days" on the same right edge. */
  min-width: 7rem;
}

@media (max-width: 560px) {
  .data-table--stack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .data-table--stack tbody tr {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-2) var(--space-3);
    padding: var(--space-4);
  }

  .data-table--stack tbody tr + tr {
    border-block-start: 1px solid var(--separator);
  }

  /* The desktop separator selector includes `tr + tr`, so the mobile reset
     needs the same structural specificity. Otherwise every grid cell keeps a
     short top rule and the intended row separator is drawn in fragments. */
  .data-table--stack tbody tr + tr th,
  .data-table--stack tbody tr + tr td {
    border: 0;
  }

  .data-table--stack tbody th,
  .data-table--stack tbody td {
    padding: 0;
  }

  /* Cell backgrounds stop at the grid gaps and padding. Carrying the selected
     state on the row turns the current member into one continuous surface. */
  .data-table--stack tbody tr[data-current-user] {
    background: var(--surface-subtle);
  }

  .data-table--stack .data-table-name {
    max-width: none;
  }

  /* Stacked rows are their own grid; the reserved desktop column widths would
     only squeeze the name. Alignment still comes from the grid tracks. */
  .data-table--stack .data-table-rank {
    width: auto;
    padding-inline-end: 0;
    text-align: start;
  }

  .data-table--stack .data-table-figure {
    min-width: 0;
  }
}

@media (prefers-contrast: more) {
  .data-table-wrap {
    border-color: var(--ink);
  }

  .data-table thead th,
  .data-table tbody tr + tr td,
  .data-table tbody tr + tr th {
    border-color: var(--ink-muted);
  }
}

/* styles/components/status.css */
.segmented {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

.segmented::-webkit-scrollbar {
  display: none;
}

/* A tab in a segmented control is a real target in both axes, not just in
   height: "Bible" is a short word and would otherwise sit at 35px wide. */
.segmented-option {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  padding-inline: var(--space-2);
  border-block-end: 2px solid transparent;
  color: var(--ink-muted);
  font-size: var(--text-body);
  text-decoration: none;
}

@media (hover: hover) and (pointer: fine) {
  .segmented-option:hover {
    color: var(--ink);
  }
}

.segmented-option[aria-selected="true"],
.segmented-option[aria-current="true"],

.segmented-option[aria-current="page"] {
  color: var(--ink);
  border-block-end-color: var(--ink);
}

.inline-meter {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}

.inline-meter-track {
  flex: 1;
  min-width: 0;
  height: 4px;
  border-radius: var(--radius-capsule);
  background: var(--surface-subtle);
  overflow: hidden;
}

.inline-meter-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--ink);

  transform-origin: left center;
  transition: transform var(--duration-panel) var(--ease-out);
}

.inline-meter-value {
  flex: none;
  color: var(--ink-secondary-strong);
  font-size: var(--text-meta);
}

.status-text {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--text-label);
  font-weight: var(--weight-body);
}

/* ------------------------------------------------------- monochrome states */

/* Success and failure are the same ink now, so neither may be recognised by
   its colour. Each carries its own mark instead — a check for a confirmation,
   a struck bar for a failure — and the text beside it always says which, so a
   state survives greyscale, a monochrome display and any colour vision
   deficiency identically. `test/monochrome-states.test.js` holds this. */
.status-text--success {
  color: var(--success);
  font-weight: var(--weight-strong);
}

.status-text--danger {
  color: var(--danger);
  font-weight: var(--weight-strong);
  padding-inline-start: var(--space-3);
  border-inline-start: 3px solid var(--danger-line);
}

.status-text--quiet {
  color: var(--ink-muted);
  font-weight: var(--weight-body);
}

/* The mark that opens a state message. It is drawn, not coloured: the check is
   two strokes and the failure is a bar with a gap, so the two are different
   shapes at any size and in forced colours. */
.status-mark {
  flex: none;
  align-self: center;
  inline-size: 1em;
  block-size: 1em;
}

.status-dot {
  width: 8px;
  height: 8px;
  flex: none;
  align-self: center;
  border-radius: 50%;
  background: currentColor;
}

/* An open ring is "not yet"; a filled disc is "now". Two shapes, one ink. */
.status-dot--pending {
  background: transparent;
  box-shadow: inset 0 0 0 2px currentColor;
}

.availability-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding-block: var(--space-3);
  color: var(--ink-secondary-strong);
  font-size: var(--text-label);
}

.availability-label {
  min-width: 0;
}

.availability-value {
  font-weight: var(--weight-body);
  color: var(--ink);
}

.empty-state {
  display: grid;
  justify-items: start;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4);
  color: var(--ink-muted);
}

.empty-state-title {
  margin: 0;
  color: var(--ink);
  font-size: var(--text-body);
  font-weight: var(--weight-body);
}

.empty-state-note {
  max-width: var(--measure-reading);
  margin: 0;
  font-size: var(--text-label);
  line-height: 1.5;
  text-wrap: pretty;
}

.notice {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  background: var(--surface);
}

.notice--attention {
  border-color: color-mix(in srgb, var(--ink) 24%, transparent);
  background: var(--surface-subtle);
}

/* A failure notice is closed with the ink itself on its leading edge, where an
   attention notice is closed with a hairline. The weight of that rule is the
   difference, and it is measured rather than decorative. */
.notice--danger {
  border-color: var(--rule);
  border-inline-start: 3px solid var(--danger-line);
  background: var(--danger-soft);
}

.notice--success {
  border-color: var(--rule);
  border-inline-start: 1px solid var(--success-line);
  background: var(--success-soft);
}

.notice-title {
  margin: 0;
  color: var(--ink);
  font-size: var(--text-control);
  font-weight: var(--weight-body);
}

.notice-body {
  margin: 0;
  color: var(--ink-secondary-strong);
  font-size: var(--text-label);
  line-height: 1.5;
  text-wrap: pretty;
}

/* styles/components/preferences.css */
/* Language and colour scheme: rows of segments, the chosen one reversed.
 *
 * Two tones, because the same form sits on the page (sign-in, Settings) and
 * inside the black header (account panel, phone menu). Each tone only re-points
 * the five local colours; the geometry and every state are shared.
 */

.preferences {
  --preferences-line: var(--rule-strong);
  --preferences-text: var(--ink-muted);
  --preferences-on: var(--ink);
  --preferences-on-text: var(--on-action);
  --preferences-hover: var(--ink-06);

  display: grid;
  gap: var(--space-3);
  margin: 0;
}

.preferences--chrome {
  --preferences-line: var(--white-18);
  --preferences-text: var(--white-72);
  --preferences-on: var(--white);
  --preferences-on-text: var(--field);
  --preferences-hover: var(--white-10);
  --focus-ring-color: var(--focus-ring-on-deep);
}

.preferences-group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.preferences-legend {
  margin-bottom: var(--space-2);
  padding: 0;
  color: var(--preferences-text);
  font-family: var(--font-data);
  font-size: var(--text-mark);
  letter-spacing: var(--tracking-mark);
  text-transform: uppercase;
}

/* Wraps rather than truncates: "Системная" is half as long again as "System". */
.preferences-options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.preferences-option {
  flex: 1 1 auto;
  min-height: var(--tap-min);
  padding-inline: var(--space-3);
  border: 1px solid var(--preferences-line);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--preferences-text);
  font-family: var(--font-ui);
  font-size: var(--text-label);
  cursor: pointer;
  transition:
    background-color var(--duration-ui) var(--ease-out),
    color var(--duration-ui) var(--ease-out),
    transform var(--duration-press) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .preferences-option:hover {
    background: var(--preferences-hover);
    color: var(--preferences-on);
  }
}

.preferences-option:active {
  transform: scale(0.97);
}

.preferences-option[aria-pressed="true"] {
  border-color: var(--preferences-on);
  background: var(--preferences-on);
  color: var(--preferences-on-text);
  font-weight: var(--weight-strong);
}

@media (prefers-reduced-motion: reduce) {
  .preferences-option:active {
    transform: none;
  }
}

@media (forced-colors: active) {
  .preferences-option[aria-pressed="true"] {
    border-color: Highlight;
    background: Highlight;
    color: HighlightText;
  }
}

/* styles/header/base.css */
.header {

  --surface: var(--chrome);
  --text: var(--white);

  --text-secondary: var(--white-72);
  --border: var(--white-18);
  --link: var(--white);
  --link-card-hover-border: var(--white);
  --shadow-soft: none;
  --shadow-card-hover: none;
  --focus-ring-color: var(--focus-ring-on-deep);
  width: 100%;
  background: var(--footer-bg);
  border-bottom: 1px solid var(--ink);

  padding: 0;
  font-family: var(--font-ui);
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  box-shadow: none;
}

@media (prefers-contrast: more) {
  .header {
    border-bottom-color: var(--ink);
  }
}

.header-container {
  max-width: var(--measure-wide);
  margin: auto;
  min-height: var(--header-height);

  padding-inline: max(var(--padding), env(safe-area-inset-left), env(safe-area-inset-right));
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-4);
}

.header-left {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  min-width: 0;
}

.logo-block {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.logo-link {
  display: inline-flex;
  align-items: center;

  min-height: var(--tap-min);
  text-decoration: none;
  color: inherit;
}

.logo-link:visited {
  color: inherit;
}

.header-nav {
  display: flex;
  gap: var(--space-5);
  align-items: center;
  justify-content: center;
  min-width: 0;
  grid-column: 2;
}

.header-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  letter-spacing: var(--tracking-mark);
  text-transform: uppercase;
  color: var(--text-secondary);
  text-decoration: none;
  position: relative;
  padding: 0 var(--space-1);
  transition: color var(--duration-ui) var(--ease-out);
}

/* The current route is underlined, not just lit. The rule is drawn on a
   pseudo-element and scaled from nothing, so the transition is a transform
   rather than an animated width, and an inactive link reserves no space for it. */
.header-nav .header-link::after {
  content: "";
  position: absolute;
  inset-inline: var(--space-1);
  inset-block-end: 12px;
  block-size: 1px;
  background: var(--header-active-line);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--duration-ui) var(--ease-out);
}

.header-nav .header-link.active::after {
  transform: scaleX(1);
}

@media (hover: hover) and (pointer: fine) {
  .header-nav .header-link:hover::after {
    transform: scaleX(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .header-nav .header-link::after {
    transition: none;
  }
}

.header .logo-link:link,
.header .logo-link:visited {
  color: inherit;
}

.header-nav .header-link,
.header-nav .header-link:link,
.header-nav .header-link:visited {
  color: var(--text-secondary);
}

.header-nav .header-link:hover,
.header-nav .header-link:focus-visible {
  color: var(--text);
}

.header-nav .header-link.active,
.header-nav .header-link.active:link,
.header-nav .header-link.active:visited {
  color: var(--header-active);
  font-weight: var(--weight-body);
}

.header-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  min-width: 0;
}

/* The phone Menu exists only inside the phone range (header/responsive.css). */
.site-menu {
  display: none;
}

.header-auth-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}

.header-timezone {
  position: relative;
}

.header-timezone-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid var(--white-18);
  border-radius: var(--radius-control);
  background: var(--white-06);
  color: var(--white-72);
  font: inherit;
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  line-height: 1.4;
  cursor: pointer;
  transition:
    background-color var(--duration-ui) var(--ease-out),
    border-color var(--duration-ui) var(--ease-out),
    color var(--duration-ui) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

.header-timezone-button:hover,
.header-timezone-button:focus-visible,
.header-timezone-button[aria-expanded="true"] {
  background: var(--white-10);
  border-color: var(--border-strong);
  color: var(--white);
}

.header-timezone-button:active {
  transform: scale(0.97);
}

.header-timezone-menu {
  transform-origin: top right;
  opacity: 1;
  transform: none;
  transition:
    opacity var(--duration-ui) var(--ease-out),
    transform var(--duration-ui) var(--ease-out),
    overlay var(--duration-ui) allow-discrete,
    display var(--duration-ui) allow-discrete;
}

.header-timezone-menu[hidden] {
  display: none;
  opacity: 0;
  transform: translateY(-4px) scale(0.98);
}

@starting-style {
  .header-timezone-menu:not([hidden]) {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
}

.header-timezone-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: var(--z-menu);
  display: grid;
  width: min(260px, calc(100vw - 24px));
  max-height: min(360px, 70vh);
  overflow: auto;
  padding: 8px;
  border: 1px solid var(--white-18);
  border-radius: var(--radius-control);
  background: var(--chrome-raised);
  box-shadow: var(--shadow-pop);
}

.header-timezone-menu[hidden] {
  display: none;
}

.header-timezone-search {
  width: 100%;
  min-height: 38px;
  margin-bottom: 8px;
  padding: 0 10px;
  border: 1px solid var(--white-10);
  border-radius: var(--radius-control);
  background: var(--white-06);
  color: var(--white);
  font: inherit;
  font-size: var(--text-control);
  font-weight: var(--weight-body);
}

.header-timezone-search:focus-visible {
  outline-color: var(--focus-ring-on-deep);
}

.header-timezone-search::placeholder {
  color: var(--white-54);
}

.header-timezone-search:focus {
  border-color: var(--white-54);
}

.header-timezone-list {
  display: grid;
  gap: 2px;
}

.header-timezone-option {
  content-visibility: auto;
  contain-intrinsic-size: auto var(--tap-min);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--tap-min);
  padding: 0 10px;
  border: 0;
  border-radius: var(--radius-tag);
  background: transparent;
  color: var(--white-72);
  font: inherit;
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  text-align: left;
  cursor: pointer;
  transition:
    background-color var(--duration-ui) var(--ease-out),
    color var(--duration-ui) var(--ease-out),
    transform var(--duration-press) var(--ease-out);
}

.header-timezone-option:active {
  transform: scale(0.97);
}

.header-timezone-empty {
  padding: 8px 10px;
  color: var(--white-72);
  font-size: var(--text-label);
  font-weight: var(--weight-body);
}

.header-timezone-empty[hidden] {
  display: none;
}

.header-timezone-option[aria-current="true"] {
  background: var(--white-10);
  color: var(--white);
  font-weight: var(--weight-body);
}

.header-timezone-option[aria-current="true"]::after {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  margin-inline-start: auto;
  border-radius: 50%;
  background: currentColor;
}

.header-timezone-option:focus-visible {
  outline-color: var(--focus-ring-on-deep);
  color: var(--white);
}

@media (hover: hover) and (pointer: fine) {
  .header-timezone-option:hover:not([aria-current="true"]) {
    background: var(--white-06);
    color: var(--white);
  }
}

.header-auth-link {
  position: relative;

  /* The one navigation control a signed-out reader has in the masthead. It was
     33x33, under this project's 44px target floor; it is an aimed control, not
     a link in running prose, so the exception does not apply to it. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  font: inherit;
  font-size: var(--text-control);
  font-weight: var(--weight-body);
  line-height: 1.4;
  text-decoration: none;
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
  transition: transform var(--duration-fast) var(--ease-out), color var(--duration-ui) var(--ease-out);
}

.header-auth-link:active {
  transform: scale(0.97);
}

.header-auth-link:visited {
  color: var(--text-secondary);
}

.header-auth-link:hover,
.header-auth-link:focus-visible {
  color: var(--text);
}

.header-auth-link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: var(--header-active-line);
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left center;
  transition:
    transform var(--duration-panel) var(--ease-out),
    opacity var(--duration-ui) var(--ease-out);
}

.header-auth-link:hover::after,
.header-auth-link:focus-visible::after {
  transform: scaleX(1);
  opacity: 0.72;
}

.header-link {
  user-select: none;
  -webkit-user-drag: none;
}

.header-link::after {
  content: "";
  position: absolute;
  left: var(--space-1);
  right: var(--space-1);
  bottom: 6px;
  width: auto;
  height: 1px;
  border-radius: 1px;
  background: var(--header-active-line);
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left center;
  transition:
    transform var(--duration-panel) var(--ease-out),
    opacity var(--duration-ui) var(--ease-out);
}

.header-link:hover::after,
.header-link:focus-visible::after {
  transform: scaleX(1);
  opacity: 0.72;
}

.header-link.active::after {
  transform: scaleX(1);
  opacity: 1;
}

.header-account {
  position: relative;
  display: flex;
  align-items: center;
}

.header-help-link,
.header-account-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0;
  border: 1px solid var(--white-18);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--white-72);
  cursor: pointer;
  transition:
    transform var(--duration-fast) var(--ease-out),
    background-color var(--duration-ui) var(--ease-out),
    color var(--duration-ui) var(--ease-out);
}

.header-help-link {
  flex-shrink: 0;
  border: 0;
  text-decoration: none;
}

.header-help-link:link,
.header-help-link:visited {
  color: var(--white-72);
}

.header-help-link:active,
.header-account-button:active { transform: scale(0.97); }

.header-help-link[aria-current="page"],
.header-account-button[aria-expanded="true"] {
  background: var(--white-10);
  color: var(--white);
}

/* Forced colours discard the tint above, which is the only thing marking Help
   as the open page; the system highlight on its edge survives. */
@media (forced-colors: active) {
  .header-help-link[aria-current="page"] {
    border: 2px solid Highlight;
  }
}

@media (hover: hover) and (pointer: fine) {
  .header-help-link:hover,
  .header-account-button:hover { background: var(--white-06); color: var(--white); }
}

/* The identity mark rides the black chrome, so it prints in white: the same
   five fills as the profile page, inverted by `currentColor` alone. The stored
   accent key is unchanged — only its presentation is a pattern now. */
.header-account-identity {
  --identity-pattern: var(--accent-pattern-solid);
  --identity-pattern-size: auto;
  position: relative;
  display: block;
  width: 30px;
  height: 30px;
  overflow: hidden;
  border: 1px solid var(--white-54);
  border-radius: var(--radius-control);
  background: var(--white-06);
  color: var(--white);
}

.header-account-identity[data-accent="green"] { --identity-pattern: var(--accent-pattern-hatch); }
.header-account-identity[data-accent="amber"] { --identity-pattern: var(--accent-pattern-cross); }
.header-account-identity[data-accent="blue"] {
  --identity-pattern: var(--accent-pattern-stipple);
  --identity-pattern-size: var(--accent-pattern-stipple-size);
}
.header-account-identity[data-accent="rose"] { --identity-pattern: var(--accent-pattern-bar); }

.header-account-identity::after {
  content: "";
  position: absolute;
  inset: 26% 24%;
  border: 1px solid currentColor;
  background-image: var(--identity-pattern);
  background-size: var(--identity-pattern-size);
}

.header-account-identity[data-emblem="dawn"]::after {
  inset: 34% 22% 30%;
  border-radius: 50% 50% 0 0;
}

.header-account-identity[data-emblem="lantern"]::after {
  inset: 24% 32%;
  border-radius: var(--radius-tag);
}

.header-account-identity[data-emblem="still-water"]::after {
  inset: 44% 20%;
  border-radius: 999px;
}

.header-account-identity[data-emblem="north-star"]::after {
  inset: 30%;
  border-radius: 0;
  transform: rotate(45deg);
}

.header-account-identity[data-emblem="open-gate"]::after {
  inset: 28% 26% 24%;
  border-width: 2px;
  border-radius: 999px 999px 0 0;
  background: none;
}

.header-account-identity[data-emblem="quiet-hill"]::after {
  inset: 40% 18% 26%;
  border-radius: 100% 100% 0 0;
}

.header-account-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: var(--z-dropdown);
  width: min(280px, calc(100vw - 32px));
  padding: var(--space-2);
  border: 1px solid var(--white-18);
  border-radius: var(--radius-panel);
  background: var(--chrome-raised);
  box-shadow: var(--shadow-pop);
  display: grid;
  gap: var(--space-1);

  transform-origin: top right;
  opacity: 1;
  transform: none;
  transition:
    opacity var(--duration-ui) var(--ease-out),
    transform var(--duration-ui) var(--ease-out),
    overlay var(--duration-ui) allow-discrete,
    display var(--duration-ui) allow-discrete;
}

.header-account-panel[hidden] {
  display: none;
  opacity: 0;
  transform: scale(0.97) translateY(-4px);
}

@starting-style {
  .header-account-panel:not([hidden]) {
    opacity: 0;
    transform: scale(0.97) translateY(-4px);
  }
}

.header-account-nav { display: grid; }

.header-account-link {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--white-72);
  font: inherit;
  font-size: var(--text-control);
  font-weight: var(--weight-body);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-out), background-color var(--duration-ui) var(--ease-out), color var(--duration-ui) var(--ease-out);
}

.header-account-link:active {
  transform: scale(0.98);
}

.header-account-link.active { color: var(--white); }

@media (hover: hover) and (pointer: fine) {
  .header-account-link:hover { background: var(--white-06); color: var(--white); }
}

.header-account-link:focus-visible {
  outline-color: var(--white-54);
}

.header-account-section {
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--white-10);
  border-bottom: 1px solid var(--white-10);
}

.header-account-heading {
  margin: 0 0 var(--space-2);
  color: var(--white-72);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  letter-spacing: 0.02em;
}

.header-account-logout { margin: 0; }
.header-account-logout-button { color: var(--white-72); }

@media (prefers-reduced-motion: reduce) {
  .header-account-panel { animation: none; }
  .header-help-link,
  .header-account-button { transition: none; }
}

@media (min-width: 960px) {
  .header-container:has(.header-account) .header-nav {
    gap: var(--space-5);
  }
}

@media (min-width: 960px) and (max-width: 1240px) {
  .header-container:has(.header-account) .header-nav {
    gap: var(--space-4);
  }

}

@media (prefers-reduced-motion: reduce) {

  .header-timezone-menu[hidden],
  .header-account-panel[hidden] {
    transform: none;
  }
}

/* styles/header/responsive.css */
@media (max-width: 1120px) {
  .header-nav {
    gap: var(--space-5);
  }

  .header-link {
    font-size: var(--text-control);
  }

}

/* ------------------------------------------------------ the Menu range
 *
 * One compact masthead — brand, the direct Help link, a labelled Menu — at the
 * shared --header-height. The desktop row and the account control hand their
 * destinations to the Menu panel, which opens directly under the masthead.
 * There is no bottom navigation of any kind.
 *
 * 959px, not the 760px phone range: measured in Chromium, the five-link row
 * wraps "My Prayers" below 800px at normal text and clips below 960px at 200%
 * text. account-menu.js reads the same number.
 */
@media (max-width: 959px) {
  .logo-block {
    gap: var(--space-2);
  }

  /* Brand left, Help and Menu right, on one 56px row at normal text size. When
     enlarged text makes the wordmark and the controls wider than the screen
     (measured: 320px from 150% text, 390px at 200%), the controls wrap to a
     second row, still right-aligned, instead of sliding over the brand. */
  .header-container {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding-inline: max(var(--space-3), env(safe-area-inset-left)) max(var(--space-3), env(safe-area-inset-right));
  }

  .header-actions {
    gap: var(--space-2);
    margin-inline-start: auto;
  }

  .header-nav,
  .header-account {
    display: none;
  }

  .site-menu {
    display: block;
  }

  /* The two-line control the header used before the rail: a 44px target named
     "Menu" for assistive technology, whose lines cross into an X while open, so
     the state is a shape. Transform only. */
  .site-menu-toggle {
    position: relative;
    display: block;
    width: var(--tap-min);
    height: var(--tap-min);
    border-radius: var(--radius-control);
    color: var(--white);
    list-style: none;
    cursor: pointer;
    user-select: none;
  }

  .site-menu-toggle::-webkit-details-marker {
    display: none;
  }

  .site-menu-toggle:focus-visible {
    background: var(--white-06);
  }

  .site-menu-line {
    position: absolute;
    inset-inline: 11px;
    top: 21px;
    height: 1.5px;
    background: currentColor;
    transform: translateY(-4px);
    transition: transform var(--duration-ui) var(--ease-out);
  }

  .site-menu-line + .site-menu-line {
    transform: translateY(4px);
  }

  .site-menu[open] .site-menu-line {
    transform: rotate(45deg);
  }

  .site-menu[open] .site-menu-line + .site-menu-line {
    transform: rotate(-45deg);
  }

  @media (prefers-reduced-motion: reduce) {
    .site-menu-line {
      transition: none;
    }
  }

  /* Forced colours replace backgrounds with Canvas, which would erase lines
     drawn as fills; draw them in the system text colour instead. */
  @media (forced-colors: active) {
    .site-menu-line {
      background: CanvasText;
      forced-color-adjust: none;
    }
  }

  /* Opened the way apple.com opens its menu: the black field takes the whole
     screen under the masthead, the page stops scrolling behind it, and the
     destinations arrive in sequence. The panel's top is the masthead's real
     lower edge (account-menu.js measures it on press; 56px without script),
     and the panel is its own single scroll area. */
  html:has(.site-menu[open]) {
    overflow: hidden;
  }

  .site-menu-panel {
    position: fixed;
    inset: var(--site-menu-top, var(--header-height)) 0 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-block: var(--space-4) var(--space-6);
    padding-inline: max(var(--space-5), env(safe-area-inset-left)) max(var(--space-5), env(safe-area-inset-right));
    background: var(--chrome);
    text-align: start;
    animation: site-menu-reveal var(--duration-panel) var(--ease-out) both;
  }

  /* The destinations are the page's headline while the Menu is open. */
  .site-menu-list--primary .site-menu-link {
    font-size: 1.55rem;
    font-weight: var(--weight-strong);
    letter-spacing: -0.01em;
    line-height: 1.2;
  }

  .site-menu[open] .site-menu-list--primary > li,
  .site-menu[open] .site-menu-group {
    animation: site-menu-item-in var(--duration-panel) var(--ease-out) both;
  }

  .site-menu[open] .site-menu-list--primary > li:nth-child(1) { animation-delay: 40ms; }
  .site-menu[open] .site-menu-list--primary > li:nth-child(2) { animation-delay: 70ms; }
  .site-menu[open] .site-menu-list--primary > li:nth-child(3) { animation-delay: 100ms; }
  .site-menu[open] .site-menu-list--primary > li:nth-child(4) { animation-delay: 130ms; }
  .site-menu[open] .site-menu-list--primary > li:nth-child(5) { animation-delay: 160ms; }
  .site-menu[open] .site-menu-group { animation-delay: 190ms; }

  @media (prefers-reduced-motion: reduce) {
    .site-menu-panel {
      animation-name: site-menu-fade;
    }

    .site-menu[open] .site-menu-list--primary > li,
    .site-menu[open] .site-menu-group {
      animation: none;
    }
  }

  .site-menu-list {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .site-menu-group {
    margin-block-start: var(--space-2);
    padding-block-start: var(--space-2);
    border-block-start: 1px solid var(--white-10);
  }

  .site-menu-mark {
    margin: 0 0 var(--space-1);
    color: var(--white-72);
    font-family: var(--font-data);
    font-size: var(--text-mark);
    letter-spacing: var(--tracking-mark);
    text-transform: uppercase;
  }

  /* Every row reserves a 2px leading rule; the current page draws it, so the
     mark is a shape plus weight, and marking a row shifts nothing. */
  .site-menu-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    width: 100%;
    min-height: var(--tap-min);
    padding-block: var(--space-2);
    padding-inline: var(--space-3) 0;
    border: 0;
    border-inline-start: 2px solid transparent;
    background: transparent;
    color: var(--white-92);
    font: inherit;
    font-size: var(--text-control);
    line-height: 1.35;
    text-align: start;
    text-decoration: none;
    cursor: pointer;
  }

  .site-menu-link[aria-current="page"] {
    border-inline-start-color: var(--white);
    color: var(--white);
    font-weight: var(--weight-strong);
  }

  @media (hover: hover) and (pointer: fine) {
    .site-menu-link:hover {
      color: var(--white);
    }

    .site-menu-toggle:hover {
      background: var(--white-06);
    }
  }

  @media (forced-colors: active) {
    .site-menu-link[aria-current="page"] {
      border-inline-start-color: Highlight;
      text-decoration: underline;
    }
  }

  .site-menu-logout {
    margin: var(--space-2) 0 0;
  }

  .site-menu-timezone-current {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    color: var(--white-72);
    font-size: var(--text-meta);
    text-align: end;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .site-menu-timezone-panel {
    margin-block-start: var(--space-2);
  }

  .site-menu-timezone-panel[hidden] {
    display: none;
  }

  .site-menu-timezone-panel .header-timezone-list {
    max-height: 40vh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* The field grows down from the masthead; the rows follow it in. Opacity,
   transform and clip only — nothing that moves layout. */
@keyframes site-menu-reveal {
  from {
    clip-path: inset(0 0 100% 0);
  }
}

@keyframes site-menu-item-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
}

@keyframes site-menu-fade {
  from {
    opacity: 0;
  }
}

/* styles/home/dashboard.css */
.dashboard-page {
  --table-body-size: var(--text-control);
  --table-head-size: var(--text-label);
  --table-row-padding-y: 10px;
  --table-row-padding-x: var(--space-4);
  --table-row-min-height: 58px;

  --red-soft: var(--danger);
  --row-hover: var(--ink-06);
  --row-current: var(--ink-10);

  background: transparent;
  color: var(--text-primary);
}

.app-toast {
  position: fixed;
  left: var(--space-4);
  bottom: var(--space-4);
  z-index: var(--z-menu);
  max-width: min(360px, calc(100vw - 2 * var(--space-4)));
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-control);
  background: var(--surface-2);
  color: var(--text-primary);
  box-shadow: var(--shadow-panel);
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  line-height: 1.4;
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
  transition:
    opacity var(--duration-ui) var(--ease-out),
    transform var(--duration-ui) var(--ease-out);
}

.app-toast.visible {
  opacity: 1;
  transform: translateY(0);
}

.app-toast[data-variant="error"] {
  border-color: var(--red-soft);
}

.dashboard-card {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-panel);
  background: var(--surface-1);
  margin-bottom: var(--space-4);
}

.dashboard-label {
  margin: 0 0 var(--space-1);
  color: var(--ink-secondary-strong);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  line-height: 1.4;
}

.dashboard-card {
  padding: var(--space-5);
}

.dashboard-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.dashboard-card-head h2 {
  margin: 0;
  color: var(--text-primary);
  font-size: var(--text-component);
  font-weight: var(--weight-body);
  line-height: 1.4;
  letter-spacing: 0;
}

.dashboard-card-head span {
  color: var(--ink-secondary-strong);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  white-space: nowrap;
}

.home-page .dashboard-card .dashboard-card-head {
  margin-bottom: var(--space-3);
}

.leaderboard-table-wrap {
  overflow-x: auto;
}

.leaderboard-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  color: var(--text-primary);
  font-size: var(--table-body-size);
}

.leaderboard-table th,
.leaderboard-table td {
  padding: var(--table-row-padding-y) var(--table-row-padding-x);
  text-align: left;
  vertical-align: middle;
}

.leaderboard-table thead th {
  padding-top: 4px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border-subtle);
  color: var(--text-muted);
  font-size: var(--table-head-size);
  font-weight: var(--weight-body);
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.leaderboard-table td {
  height: var(--table-row-min-height);
  font-weight: var(--weight-body);
  border-top: 1px solid var(--border-subtle);
  transition: background var(--duration-fast) var(--ease-out);
}

.leaderboard-table tbody tr:first-child td {
  border-top: 0;
}

.leaderboard-table th:first-child,
.leaderboard-table td:first-child {
  width: 56px;
  color: var(--text-muted);
}

.leaderboard-table th:last-child,
.leaderboard-table td:last-child {
  width: 1%;
  text-align: right;
  white-space: nowrap;
}

.leaderboard-table th:nth-child(2),
.leaderboard-table td:nth-child(2) {
  width: auto;
}

@media (hover: hover) and (pointer: fine) {
  .leaderboard-table:not(.prayer-table) tbody tr:hover td {
    background: var(--row-hover);
  }
}

.leaderboard-table:not(.prayer-table) tbody tr:focus-within td {
  background: var(--row-hover);
}

.leaderboard-table tbody tr.is-current td {
  background: var(--row-current);
}

.leaderboard-table tbody tr.is-current td:first-child {
  box-shadow: inset 2px 0 0 var(--action);
}

.leaderboard-table tbody tr.is-current .leaderboard-rank {
  color: var(--text-primary);
}

/* A name in a leaderboard is a link to that member's profile, so it is a real
   target in both axes — a short name would otherwise be under 44px wide. */
.leaderboard-user,
.leaderboard-streak {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  line-height: 1.4;
}

a.leaderboard-user {
  min-height: var(--tap-min);
}

.leaderboard-streak {
  justify-content: flex-end;
}

.leaderboard-user-cell {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px var(--space-2);
}

.leaderboard-inactive-tag {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding: 0 var(--space-2);
  border: 1px solid var(--danger-line);
  border-radius: var(--radius-tag);
  background: var(--danger-tint);
  color: var(--danger);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  line-height: 1.4;
  white-space: nowrap;
}

.leaderboard-rank {
  color: var(--text-muted);
  font-weight: var(--weight-body);
}

.leaderboard-streak {
  width: 100%;
  gap: var(--space-3);
  font-size: var(--table-body-size);
  font-weight: var(--weight-body);
}

.leaderboard-streak-value {
  white-space: nowrap;
}

.leaderboard-streak-separator {
  display: none;
  color: var(--text-muted);
  font-weight: var(--weight-body);
}

.leaderboard-missed-tag {

  order: -1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 30px;
  padding: 0 var(--space-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-tag);
  background: var(--ink-06);
  color: var(--ink-secondary-strong);
  font: inherit;
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

.leaderboard-missed-tag:hover,
.leaderboard-missed-tag:focus-visible {
  border-color: var(--border-strong);
  background: var(--ink-10);
  color: var(--text-primary);
}

.leaderboard-missed-tag:active {
  transform: scale(0.97);
}

.leaderboard-missed-tag.is-static {
  cursor: default;
  pointer-events: none;
}

.leaderboard-row-icon {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  stroke-width: 2;
}

.leaderboard-row-icon {
  color: var(--text-muted);
  transition: color var(--duration-fast) var(--ease-out);
}

/* A member's name is a link to their profile, so it is a real target in both
   axes. `min-width: 0` was here to let a long name wrap inside a table cell;
   `max-width: 100%` plus `overflow-wrap` already does that, and the floor is
   what keeps a two-letter name aimable. */
.user-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: var(--tap-min);
  max-width: 100%;
  width: fit-content;
  overflow-wrap: anywhere;
  border-radius: var(--radius-control);
}

.leaderboard-table td.leaderboard-empty {
  height: auto;

  width: auto;
  min-width: 0;
  white-space: normal;
  padding: var(--space-7) var(--space-4);
  text-align: center;
  color: var(--text-muted);
}

p.leaderboard-empty {
  margin: 0;
  padding: var(--space-7) var(--space-4);
  text-align: center;
  color: var(--text-muted);
}

.table-empty-title {
  display: block;
  color: var(--text-primary);
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  line-height: 1.4;
}

.table-empty-hint {
  display: block;
  margin-top: 4px;
  color: var(--ink-secondary-strong);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  line-height: 1.45;
}

.telegram-connect-button {
  width: 100%;
  min-height: 52px;
  margin-top: auto;
  border-radius: var(--radius-control);
  font-size: var(--text-control);
  font-weight: var(--weight-body);
}

.action-menu-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

.action-menu-icon {
  display: block;
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
}

.action-menu-trigger:hover,
.action-menu-trigger:focus-visible {
  background: var(--ink-06);
  border-color: var(--border-subtle);
  color: var(--text-primary);
}

.action-menu-trigger[aria-expanded="true"] {
  background: var(--ink-10);
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.action-menu-trigger:active {
  transform: scale(0.97);
}

/* The row-actions trigger used to shrink to 40px on a coarse pointer — that is,
   it got smaller on exactly the devices where a finger is the pointer, and it
   dropped under this project's 44px target floor to do it. It keeps `--tap-min`
   everywhere now. */

.action-menu {
  position: fixed;
  z-index: var(--z-dropdown);
  display: grid;
  width: max-content;
  min-width: 188px;
  padding: 6px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-control);
  background: var(--surface-2);
  box-shadow: var(--shadow-card-hover);
  transform-origin: top right;
  animation: action-menu-in var(--duration-ui) var(--ease-out);
}

.action-menu[data-placement="top"] {
  transform-origin: bottom right;
  animation-name: action-menu-in-up;
}

.action-menu[hidden] {
  display: none;
}

.action-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;

  /* An item in a popup is an aimed control like any other, and 40px was under
     this project's 44px floor. */
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-tag);
  background: transparent;
  color: var(--ink-secondary-strong);
  font: inherit;
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  text-align: left;
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

/* These two client-rendered menu glyphs are open stroke paths. The shared icon
   family is filled by default, which otherwise closes the check into a wedge. */
.action-menu-item-icon {
  fill: none;
}

.action-menu-item[hidden] {
  display: none;
}

.action-menu-item:hover,
.action-menu-item:focus-visible {
  background: var(--ink-06);
  color: var(--text-primary);
}

.action-menu-item:disabled {
  cursor: wait;
  opacity: 0.55;
  background: transparent;
  color: var(--text-muted);
}

@keyframes action-menu-in {
  from {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes action-menu-in-up {
  from {
    opacity: 0;
    transform: translateY(4px) scale(0.98);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.prayer-reaction-modal[hidden],
.missed-answer-modal[hidden],
.settings-confirm-modal[hidden] {
  display: none;
  opacity: 0;
  transition-duration: var(--duration-ui);
}

.prayer-reaction-modal,
.missed-answer-modal,
.settings-confirm-modal {
  position: fixed;
  inset: 0;

  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: var(--space-4);
}

.prayer-reaction-modal,
.missed-answer-modal,
.settings-confirm-modal,
.reading-modal {
  opacity: 1;
  transition:
    opacity var(--duration-panel) var(--ease-out),
    display var(--duration-panel) allow-discrete;
}

@starting-style {
  .prayer-reaction-modal,
  .missed-answer-modal,
  .settings-confirm-modal,
  .reading-modal {
    opacity: 0;
  }
}

@starting-style {
  .prayer-reaction-dialog,
  .missed-answer-dialog,
  .settings-confirm-dialog,
  .reading-modal-dialog {
    opacity: 0;
    transform: scale(0.96);
  }
}

.prayer-reaction-modal[hidden] .prayer-reaction-dialog,
.missed-answer-modal[hidden] .missed-answer-dialog,
.settings-confirm-modal[hidden] .settings-confirm-dialog,
.reading-modal[hidden] .reading-modal-dialog {
  opacity: 0;
  transform: scale(0.96);
  transition-duration: var(--duration-ui);
}

.prayer-reaction-backdrop,
.missed-answer-backdrop,
.settings-confirm-backdrop {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 55%);
  backdrop-filter: blur(4px);
  transition: opacity var(--duration-panel) var(--ease-out);
}

.prayer-reaction-dialog,
.missed-answer-dialog,
.settings-confirm-dialog {
  position: relative;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-panel);
  background: var(--surface-2);
  box-shadow: var(--shadow-pop);
  transition:
    transform var(--duration-panel) var(--ease-out),
    opacity var(--duration-panel) var(--ease-out);
}

.prayer-reaction-dialog {
  display: flex;
  gap: 6px;
  padding: 8px;
  border-radius: var(--radius-panel);
}

.missed-answer-dialog,
.settings-confirm-dialog {
  display: grid;
  gap: var(--space-4);
  width: min(100%, 420px);
  padding: var(--space-5);
}

.missed-answer-head,
.settings-confirm-head {
  display: grid;
  gap: var(--space-1);
}

.missed-answer-head h2,
.settings-confirm-head h2 {
  margin: 0;
  color: var(--text-primary);
  font-size: var(--text-component);
  font-weight: var(--weight-body);
  line-height: 1.4;
}

.settings-confirm-head p:last-child {
  margin: 0;
  color: var(--ink-secondary-strong);
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  line-height: 1.45;
}

.missed-answer-actions,
.settings-confirm-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}

.missed-answer-actions .dashboard-action {
  justify-content: center;
  min-height: 52px;
  padding: 0 var(--space-3);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-control);
  font-size: var(--text-control);
  font-weight: var(--weight-body);
  text-align: center;
}

.missed-answer-actions .dashboard-action-yes {
  background: var(--action);
  color: var(--on-action);
}

.missed-answer-actions .dashboard-action-no {
  background: var(--ink-06);
  color: var(--text-primary);
}

.missed-answer-actions .dashboard-action:hover,
.missed-answer-actions .dashboard-action:focus-visible {
  background: var(--ink-10);
}

.missed-answer-actions .dashboard-action-yes:hover,
.missed-answer-actions .dashboard-action-yes:focus-visible {
  background: var(--action-hover);
  color: var(--on-action);
}

.prayer-reaction-choice {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-primary);
  font-size: var(--text-component);
  cursor: pointer;
  transition:
    border-color var(--duration-fast) var(--ease-out),
    background var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .prayer-reaction-choice:hover:not(:disabled):not([aria-pressed="true"]) {
    border-color: var(--border-subtle);
    background: var(--ink-10);
  }
}

.prayer-reaction-choice:focus-visible {
  border-color: var(--border-subtle);
  background: var(--ink-10);
}

.prayer-reaction-choice:active:not(:disabled) {
  transform: scale(0.97);
}

/* Hover on this control is a 10% ink wash. A selection therefore cannot also
   be a wash — one step darker is not a state, it is a guess — so a chosen
   reaction is ringed in solid ink and keeps a filled corner mark. Hover can
   never produce either. */
.prayer-reaction-choice[aria-pressed="true"] {
  position: relative;
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
  background: var(--paper);
  font-weight: var(--weight-strong);
}

.prayer-reaction-choice[aria-pressed="true"]::after {
  content: "";
  position: absolute;
  inset-block-start: 3px;
  inset-inline-end: 3px;
  inline-size: 6px;
  block-size: 6px;
  background: var(--ink);
  pointer-events: none;
}

.prayer-reaction-choice:disabled {
  cursor: progress;
  opacity: 0.65;
  transform: none;
}

@media (max-width: 900px) {

  .prayer-form {
    height: auto;
  }

  .prayer-form {
    grid-template-rows: auto minmax(180px, auto) auto;
  }

}

@media (max-width: 680px) {
  .dashboard-page {
    --table-row-padding-y: 10px;
    --table-row-padding-x: var(--space-2);
    --table-row-min-height: 54px;
  }

  .dashboard-actions {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .dashboard-action {
    min-height: 128px;
    padding: var(--space-4);
  }

  .dashboard-timer-card {
    padding: var(--space-3);
  }

  .dashboard-timer-copy {
    align-items: flex-start;
    text-align: left;
  }

  .dashboard-timer-value {
    flex: 0 0 auto;
    font-size: var(--text-figure);
  }

  .leaderboard-table th:first-child,
  .leaderboard-table td:first-child {
    width: 36px;
  }

  .leaderboard-user {
    gap: 6px;
  }

  .leaderboard-streak {
    justify-content: flex-end;
    gap: 6px;
    line-height: 1.4;
  }

  .leaderboard-row-icon {
    width: 15px;
    height: 15px;
  }

  .leaderboard-streak-separator {
    display: inline;
  }

  .leaderboard-missed-tag {
    order: 0;

    min-height: var(--tap-min);
    padding: 0;
    border: 0;
    background: transparent;
    font-size: var(--text-meta);
    font-weight: var(--weight-body);
  }

  .leaderboard-inactive-tag {
    min-height: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    font-size: 0;
    font-weight: var(--weight-body);
  }

  .leaderboard-inactive-tag::after {
    content: attr(data-mobile-label);
    font-size: var(--text-meta);
    line-height: 1.4;
  }

  .leaderboard-missed-tag:hover,
  .leaderboard-missed-tag:focus-visible {
    border-color: transparent;
    background: transparent;
    color: var(--text-primary);
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  button.leaderboard-missed-tag {
    position: relative;
  }

  button.leaderboard-missed-tag::before {
    content: "";
    position: absolute;
    inset: -13px -8px;
  }

  .prayer-reaction-button {
    min-height: 30px;
  }

  /* The reading dialog's small-screen geometry moved to its own sheet
     (components/reading-modal.css), which is its one owner: a `vh`-bounded
     dialog cannot follow the virtual keyboard, and two sheets disagreeing
     about its height is how that kind of bug survives. Its Remove control
     keeps its target here because the target floor is a shared rule. */
  .reading-passage-remove {
    min-height: 44px;
    padding: 0 var(--space-3);
  }

  .daily-goal-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    column-gap: var(--space-3);
    row-gap: var(--space-1);
  }

  .daily-goal-text {
    grid-column: 2;
    min-height: 0;
    padding-top: 2px;
  }

  .daily-goal-checkbox {
    grid-column: 1;
    grid-row: 1;
    margin-top: 3px;
  }

  .daily-goal-actions {
    grid-column: 2;
    justify-content: flex-end;
  }

  .daily-goals-add {
    justify-self: stretch;
  }

  .streak-board-grid {
    grid-template-columns: minmax(0, 1fr);
  }

}

@media (max-width: 420px) {
  .dashboard-page {
    --table-row-min-height: 52px;
  }

  .dashboard-action-label {
    font-size: 2.6rem;
    letter-spacing: 0;
  }
}

@media (max-width: 360px) {

  .leaderboard-table:not(.prayer-table) th:last-child,
  .leaderboard-table:not(.prayer-table) td:last-child {
    width: auto;
    max-width: 56%;
    white-space: normal;
  }

  .leaderboard-streak {
    flex-wrap: wrap;
    row-gap: 4px;
  }
}

/* styles/home/sections.css */
.section {
  padding: var(--section-padding) 0;
}

.section-compact {
  padding: var(--section-padding-compact) 0;
}

/* The shared opening for the simple pages — 404, About, Customer. It is the
   same printed opening every other route uses: a display title set on the
   shell's own left edge, with its sentence under it. Centring it was the
   generic composition the overhaul replaced. */
.section-title {
  font-family: var(--font-display);
  font-size: var(--text-page-title);
  font-weight: var(--weight-display-heavy);
  line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
  text-transform: uppercase;
  text-wrap: balance;
  overflow-wrap: break-word;
  margin-bottom: var(--space-3);
}

.section-text {
  max-width: var(--measure-prose);
  margin: 0;
  color: var(--ink-muted);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: 1.7;
}

.page-header {
  max-width: var(--measure-wide);
  margin: 0 0 var(--gap-region);
}

.page-title {
  margin-bottom: var(--space-2);
}

.page-subtitle {
  margin-bottom: 0;
}

/* The way back sits at the leading edge, under the sentence that explains why
   the reader is here — not floated in the middle of an empty page. */
.not-found-action {
  display: flex;
  justify-content: flex-start;
}

.not-found-action:has(.ui-button:empty) {
  display: none;
}

/* styles/home/home-layout.css */
.home-page .section {
  padding: clamp(var(--space-5), 4.2vw, 46px) 0;
}

.home-page .section.section-compact {
  padding: clamp(var(--space-3), 2.4vw, var(--space-5)) 0;
}

.home-page {
  overflow-x: clip;
}

/* styles/home/roadmap-and-links.css */
.about-roadmap {
  position: relative;
}

.about-roadmap-head {
  max-width: 820px;
  margin: 0 auto;
}

.about-roadmap-grid {
  margin-top: var(--space-5);
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-4);
  --roadmap-line: var(--accent);
  --roadmap-line-opacity: 0.5;
}

.about-roadmap-grid:not(.visit-roadmap-grid)::before,
.about-roadmap-grid:not(.visit-roadmap-grid)::after {
  content: "";
  position: absolute;
  left: 5%;
  right: 5%;
  height: 2px;
  border-radius: 999px;
  background: var(--roadmap-line);
  opacity: var(--roadmap-line-opacity);
  z-index: 0;
}

.about-roadmap-grid:not(.visit-roadmap-grid)::before {
  top: 38px;
}

.about-roadmap-grid:not(.visit-roadmap-grid)::after {
  top: calc(50% + 38px);
}

.roadmap-step {
  position: relative;
  z-index: 1;
  padding: var(--space-5);
  min-height: 220px;
  background: var(--surface-elevated);
}

.roadmap-step-index {
  position: absolute;
  top: var(--space-2);
  right: var(--space-3);
  color: var(--text-secondary);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
}

.card-title {
  font-size: var(--text-component);
  font-weight: var(--weight-body);
  line-height: 1.3;
  margin-bottom: var(--space-2);
}

.card-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.card-text {
  font-size: var(--text-control);
  line-height: 1.65;
  color: var(--text-secondary);
}

.hover-lift-card {
  --lift-offset: 0px;
  transform: translate3d(0, var(--lift-offset), 0);
  transition:
    transform var(--duration-panel) var(--ease-out),
    box-shadow var(--duration-panel) var(--ease-out),
    border-color var(--duration-panel) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .hover-lift-card:hover {
    transform: translate3d(0, calc(var(--lift-offset) - 3px), 0);
    border-color: var(--link-card-hover-border);
    box-shadow: var(--shadow-card-hover);
  }
}

/* styles/home/footer.css */
.section.footer-info {
  padding: clamp(14px, 2.1vw, 20px) 0;
  margin-top: auto;
  background: var(--footer-bg);
  border-top: 1px solid var(--ink);
  color: var(--footer-text);
}

.footer-info .container {
  padding-top: 0;
  padding-bottom: 0;
}

.footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: 44px;
}

.footer-tagline {
  flex: 1 1 auto;
  margin: 0;

  color: var(--footer-text);
  font-size: var(--text-meta);
  font-weight: var(--weight-body);
  line-height: 1.35;
  text-align: center;
}

.footer-socials {
  display: flex;
  align-items: center;
  gap: 10px;
}

.footer-social {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--footer-text);
  text-decoration: none;
  font-size: 26px;
  border-radius: var(--radius-control);
  transition:
    color var(--duration-ui) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .footer-social:hover {
    color: var(--footer-icon-hover);
    transform: translateY(-1px);
  }
}

.footer-social:focus-visible {

  outline-color: var(--focus-ring-on-deep);
}

.footer-text {
  font-size: var(--text-label);
  font-weight: var(--weight-body);
  line-height: 1.35;
  color: var(--footer-text);
  margin: 0;
}

@media (max-width: 680px) {
  .footer-row {
    flex-direction: column;
    justify-content: center;
    gap: var(--space-2);
    text-align: center;
  }

  .footer-tagline {
    max-width: 34ch;
    font-size: var(--text-meta);
  }
}

/* styles/home/responsive.css */
@media (max-width: 1024px) {
  /* `auto-fit` with a real minimum rather than a fixed pair: at 320px with
     200% text a two-column track is 136px wide, which no sentence fits into,
     and the cards pushed the page sideways. `min(…, 100%)` keeps the minimum
     from becoming a minimum wider than the screen itself. */
  .about-roadmap-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  }

  .about-roadmap-grid:not(.visit-roadmap-grid)::before,
  .about-roadmap-grid:not(.visit-roadmap-grid)::after {
    display: none;
  }
}