/* =============================================================================
   LA VELITA — DESIGN SYSTEM  (candlelit dark)
   -----------------------------------------------------------------------------
   A night-indigo ground lit by candle gold, parchment-toned text. Reverent,
   tender, quiet. Mobile-first (~380px primary), one-handed: primary nav sits in
   the bottom thumb zone, touch targets are ≥48px. The liturgical color of the
   day arrives as --accent on <body> — a single quiet daily thread, never loud.

   This file IS the design library. It is organized as:
     1. TOKENS      — the only place to change the look (color, type, space…)
     2. BASE        — element defaults
     3. UTILITIES   — small layout/text helpers
     4. COMPONENTS  — the reusable pieces (buttons, cards, tiles, nav, charts…)
     5. PRINT       — paper is paper: the light palette

   A living catalog of every token + component renders at /styleguide/.
   Charts are drawn server-side (Python); their palette is mirrored in --chart-*
   here for reference, but examen/analytics.py is the source of truth for SVG.
   ============================================================================= */

/* ---- 1. TOKENS ------------------------------------------------------------ */
:root {
  /* device safe areas, resolved once so every rule spends the same value —
     and so a test can shadow them and see what a notched phone sees */
  --bar-pad: 10px;             /* the top bar's padding above the safe area */
  --header-h: calc(var(--safe-top) + var(--bar-pad) + var(--touch));   /* the bar's row: what a sticky thing sits under */
  --tabbar-h: 58px;            /* the tab row itself (border + padding + link), without the safe inset */
  --safe-top: env(safe-area-inset-top);
  --safe-bottom: env(safe-area-inset-bottom);
  --safe-left: env(safe-area-inset-left);
  --safe-right: env(safe-area-inset-right);

  /* night ladder (page ground, darkest last) */
  --night: #1c2541;
  --night-2: #141a30;
  --night-3: #0f1426;

  /* parchment text ladder (most→least emphasis) */
  --parch: #f4ecd8;
  --ink: rgba(244, 236, 216, .92);
  --ink-2: rgba(244, 236, 216, .80);
  --muted: rgba(244, 236, 216, .62);
  --faint: rgba(244, 236, 216, .45);

  /* brand hues */
  --gold: #c79a44;             /* candle gold — the brand */
  --gold-soft: #e3c47e;        /* the lit edge of the flame */
  --marian: #6a86c9;           /* Marian blue — the second voice (spouse) */
  --rose: #b07a86;             /* tender rose — used sparingly */

  /* surfaces & hairlines (translucent parchment over the night ground) */
  --bg: var(--night);
  --card: rgba(244, 236, 216, .055);
  --card-strong: rgba(244, 236, 216, .09);
  /* The third step, and the only one that is a SURFACE rather than a wash.
     .055 and .09 sit at 1.16:1 and 1.28:1 against the ground — below the point
     where an edge is perceived at all, which is why the chat read as one flat
     field. This is 1.59:1: present without shouting, in a room lit by one
     candle. Spend it where something must be seen to be a distinct object. */
  --card-raised: rgba(244, 236, 216, .16);
  /* Tinted surfaces — a wash of meaning over the ground, in exactly these
     steps. They had drifted to five gold values (.06 .07 .08 .09 .10, one of
     them on a different base) and two danger values, which is why pages read
     as different blues even though the ground beneath them is identical. */
  --tint-gold-soft: rgba(199, 154, 68, .08);   /* at rest: a glyph, a highlighted field */
  --tint-gold: rgba(199, 154, 68, .10);        /* chosen, active, hovered */
  --tint-danger: rgba(231, 161, 161, .10);
  --tint-cool: rgba(106, 134, 201, .10);       /* the solo tag — the one non-gold tint */
  /* THE TAB BAR'S GROUND — opaque on purpose. It was rgba(…, .92) under a
     backdrop-filter, and a blurred backdrop on a fixed element has to be
     re-sampled every frame the page moves beneath it, which is one of the ways
     iOS drops a fixed layer mid-scroll. The colour is --night-3 exactly, which
     is also the gradient's terminal stop, so at the foot of the screen the
     opaque bar and the 92% one paint the same pixels. */
  --bar-bg: var(--night-3);                    /* the tab bar over the ground */
  /* The bar's ground BEFORE that — kept as a token only so the scroll lab can
     put it back verbatim, and because a surface colour written raw in a rule
     is what the page-shell sweep exists to catch. It goes when the lab does.
     Deliberately absent from design/build.py's TOKENS map: it is not part of
     the design system, it is a held-open door. */
  --bar-bg-blurred: rgba(15, 20, 38, .92);
  --field-bg: rgba(15, 20, 38, .55);           /* an input sunk into the ground */
  /* The select's arrow, drawn by us in --muted's colour. The platform paints
     its own indicator otherwise — black on iOS — and it vanished against the
     night on every settings screen. The rgba is --muted written out: a data
     URI cannot read a custom property. */
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(244,236,216,.62)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  --scrim: rgba(8, 6, 12, .55);
  /* THE GROUND, defined once: the full-viewport layer and the status bar's
     strip both paint it, so the strip is indistinguishable from the page
     behind it at rest and hides what scrolls under it completely. */
  --ground: radial-gradient(125% 85% at 50% -12%, #35426f 0%,
    var(--night) 40%, var(--night-2) 74%, var(--night-3) 100%);
  --line: rgba(199, 154, 68, .28);      /* gold-tinted hairline */
  --line-soft: rgba(244, 236, 216, .14);

  /* status */
  --accent: #7fae8e;           /* overridden per-day by the liturgical color */
  --danger: #e7a1a1;
  --on-gold: #241a08;          /* dark ink for text sitting on gold */

  /* type families */
  --serif: "Spectral", "Iowan Old Style", Palatino, Georgia, serif;   /* body */
  --display: "Fraunces", "Iowan Old Style", Georgia, serif;           /* headings */
  --sans: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; /* UI/meta */

  /* THE PERSON'S TEXT SIZE. Every size in this file is in rem (a handful of
     px for the drawn heights of controls), so scaling the root scales the
     app: html's font-size and body's both multiply by this. base.html stamps
     the person's step on <html data-text="-2 … 2"> (User.text_size, Account
     settings → "Text size") and the rules below set the factor. The top step
     is the largest at which a pill, a tab label and a button still fit their
     drawn height at 402px — design-qa.spec.mjs renders the app at that step
     and fails on any overflow, so it is a measured ceiling, not a guess.
     Paper ignores it (@media print). */
  --text-scale: 1;

  /* type scale (rem) */
  --t-display: 2rem;
  --t-h1: 1.5rem;
  --t-h2: 1.2rem;
  --t-h3: 1.02rem;
  --t-body: 1rem;
  --t-small: .85rem;
  --t-eyebrow: .68rem;

  /* spacing scale (4pt base) */
  --s1: 4px;  --s2: 8px;  --s3: 12px;  --s4: 16px;  --s5: 24px;  --s6: 32px;

  /* layout & rhythm — the space BETWEEN components, standardized per device.
     A component owns its internal spacing; a screen composes with these and
     never invents a gap. --gutter widens on the ≥700px canvas (see media). */
  --gutter: var(--s4);         /* page side padding */
  --page-max: 640px;           /* the one comfortable reading column */
  --gap-section: var(--s5);    /* between sections of a screen (.child-section) */
  --gap-card: 10px;            /* between stacked block components (cards, lists, notices) */
  --gap-controls: var(--s2);   /* between paired controls in one row (.row.tight) */

  /* z scale — every stacked layer has a named altitude; no magic z-index */
  --z-nav: 40;                 /* bottom tab bar */
  --z-float: 50;               /* floating pills above content (pause, pager) */
  --z-overlay: 300;            /* full-screen overlays (pull-to-refresh) */

  /* radii */
  --r-sm: 10px;
  --r: 14px;                   /* default card radius */
  --r-lg: 18px;
  --r-pill: 999px;
  --radius: var(--r);          /* legacy alias */

  /* elevation & motion */
  --shadow-1: 0 10px 30px rgba(0, 0, 0, .45);
  --dur: .3s;
  --ease: cubic-bezier(.4, 0, .2, 1);
  --touch: 48px;               /* minimum TAP AREA — never a drawn size */
  /* Drawn sizes. A control hugs its text or glyph; the tap area stays --touch
     through an invisible halo (see "the halo" beside .pill). Draw at --ctl
     for a pill, chip, segment or text control; --ctl-btn for a button;
     --ctl-icon for a round icon control (menu dots, pager arrow, clear). */
  --ctl: 36px;
  --ctl-btn: 44px;
  --ctl-icon: 40px;

  /* chart palette (mirror of examen/analytics.py — Python is source of truth) */
  --chart-a: #b08a35;          /* partner A / the viewer — gold, dark-band */
  --chart-b: #6a86c9;          /* partner B / spouse — marian blue */
  --chart-grid: #2b3352;
  --chart-ink: #e2dbcb;
  --chart-muted: #9f9c98;
  /* The four product areas of Home's momentum card — the ONE categorical
     palette for "which area". The brand hues, stepped so all four pass the
     colour-blind and contrast checks on the card surface in this stack order
     (examen, Emmaus, Bible, homeschool). Never reuse one for a status. */
  --chart-examen: #b08a35;
  --chart-emmaus: #c96c8e;
  --chart-bible: #6f8ccf;
  --chart-homeschool: #4ba07a;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; font-size: calc(100% * var(--text-scale)); }
/* the five steps of Account settings → Text size; 0 is the default and
   carries no attribute (the factor above stays 1) */
html[data-text="-2"] { --text-scale: .86; }
html[data-text="-1"] { --text-scale: .93; }
html[data-text="1"] { --text-scale: 1.1; }
html[data-text="2"] { --text-scale: 1.22; }
body {
  margin: 0;
  /* THE GROUND. Its base colour is the gradient's own terminal stop, so any
     strip the gradient does not reach matches the bottom of the page instead
     of showing a lighter band. It was --night-2 (#141a30) under a gradient
     ending at --night-3 (#0f1426), and the home-indicator inset showed the
     difference as a lighter bar across the foot of the screen. */
  background: var(--night-3);
  color: var(--ink);
  font-family: var(--serif);
  font-size: calc(17px * var(--text-scale));
  line-height: 1.55;
  /* THE FLOOR THE DOCUMENT STANDS ON — and the whole of the crawl fix.

     100dvh is the layout viewport, which in a standalone iOS app is the thing
     that is wrong (the reason is written out on nav.tabbar). --screen-h is the
     height that viewport will have once WebKit resolves it, published once by
     base.html from window.screen — a physical measurement the bug cannot
     reach. While the viewport is short this floor is taller than it, the
     document overflows, and WebKit resolves the viewport exactly as it does
     for a page that is simply long. Once resolved the two are equal and the
     floor costs nothing: no residual scroll, no rubber band.

     It is never withdrawn. That is the difference from the version that
     crawled, which removed the extra height the moment the viewport caught up
     — and so removed the very thing holding it resolved.

     --screen-h is unset in a browser tab and on every other platform, where
     max() collapses to 100dvh and this is the plain rule it looks like. */
  min-height: max(100dvh, var(--screen-h, 0px));
  /* exactly the tab bar's height — derived from the same token, so the two
     cannot drift apart. The page shell sweep asserts they are equal. */
  padding-bottom: calc(var(--tabbar-h) + var(--safe-bottom));
}

/* The candlelit radial, laid over the whole VISUAL viewport — safe areas
   included — rather than attached to the document. `background-attachment:
   fixed` is unreliable on iOS under viewport-fit=cover: it leaves the area
   behind the home indicator to the base colour, which is how a band of another
   shade appeared at the bottom of a home-screen app and nowhere else. */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: var(--ground);
}

/* The first row of content sits one step under the chrome, not a whole gutter.
   The bar's corners are PINNED (page shell, clause 5), so the row it reserves
   is empty between them; a full gutter on top of it left 89px above the first
   word of every screen but Home — 151px on a phone, once the top inset counts
   (owner, 15 Sept 2026). */
main { max-width: var(--page-max); margin: 0 auto;
  padding: var(--s2) calc(var(--gutter) + var(--safe-right))
           var(--s6) calc(var(--gutter) + var(--safe-left)); }

/* Motion respects the reader: when the OS asks for reduced motion, everything
   settles instantly — the flame, the spinner, the caret, pull-to-refresh. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

h1, h2, h3, h4 { line-height: 1.2; margin: 0 0 .4em; font-weight: 500;
  font-family: var(--display); color: var(--parch); letter-spacing: -.01em; }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.2rem; }
h3 { font-size: 1.02rem; }
h4 { font-size: .95rem; }
a { color: inherit; }
p { margin: .4em 0; }

.muted { color: var(--muted); }
.small { font-size: var(--t-small); }
.serif-caps, .eyebrow {
  font-family: var(--display); font-size: var(--t-eyebrow); font-weight: 500;
  letter-spacing: .22em; text-transform: uppercase; color: var(--muted);
}
.eyebrow { display: block; }               /* section eyebrow (often centered) */

/* ---- 3. UTILITIES -------------------------------------------------------- */
/* Small, composable layout helpers so screens don't grow bespoke inline styles. */
[hidden] { display: none !important; }  /* JS toggles must beat display classes (.row etc.) */
.stack > * + * { margin-top: var(--s3); }  /* even vertical rhythm */
.row { display: flex; align-items: center; gap: var(--s3); }
.row.between { justify-content: space-between; }
/* .row.end — a DECISION ROW: the answers to one thing gather at the trailing
   edge, the affirmative last (Apple HIG, Alerts: "place the button people are
   most likely to choose on the trailing side… Cancel buttons are typically on
   the leading side"; Material dialogs: the confirming action last). Tool rows
   (filters, Copy/Share) stay leading; see DESIGN-SYSTEM.md → "Action
   alignment". Never put .row on a .card: .card is display:block and wins. */
.row.end { justify-content: flex-end; }
.row.wrap { flex-wrap: wrap; }
.row.baseline { align-items: baseline; }
.row.tight { gap: var(--gap-controls); }
/* An input row (a date field + "Use this day"): the button keeps one line and
   its own width, the field takes the slack. The button wrapped onto two lines
   at 402px — a secondary's text never wraps (DESIGN-SYSTEM.md, the ladder). */
.row.tight > .btn, .row.tight > form > .btn { white-space: nowrap; flex: 0 0 auto; }
.row.tight > input { flex: 1 1 auto; min-width: 0; }
.center { text-align: center; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); }
.italic { font-style: italic; }
/* Two layout primitives, deliberately the ONLY size/flex utilities. Everything
   else comes from components and .stack — this is not a utility framework. */
.grow { flex: 1; }               /* a flex child that should take the slack */
.min0 { min-width: 0; }          /* lets a flex child actually truncate */
.accent { color: var(--accent); }
.gold { color: var(--gold-soft); }
.danger { color: var(--danger); }

/* stat tiles — one number + a caption; the platform's headline-number unit */
.tiles { display: flex; gap: var(--s3); margin: var(--s4) 0; flex-wrap: wrap; }
.tile { flex: 1; min-width: 84px; text-align: center;          /* use with .card */
  /* A tile is narrow BY DESIGN (three across a 402px phone), and its caption is
     letter-spaced uppercase, so a compound word like "family/month" is wider
     than the tile and simply SPILLS — flex items do not clip. That shipped: the
     cost tiles' captions ran out over their neighbours. Breaking mid-word is
     ugly; a caption printed outside its own tile is a bug. */
  overflow-wrap: anywhere; }
/* A tile caption is the one place .serif-caps has to survive a ~74px column,
   and .22em of tracking is roughly a third of the word's width. Tightened here
   only: at full tracking "PRACTISING" cannot fit and the rule above breaks it
   as "PRACTISI / NG", which is not better than spilling. */
.tile .serif-caps { letter-spacing: .1em; }
.tile-num { font-family: var(--display); font-weight: 600; font-size: 1.7rem;
  line-height: 1; color: var(--parch); margin-bottom: var(--s1); }
.tile-num.lg { font-size: var(--t-display); }
/* The gold wash is the Analytics hero tile's alone. The momentum card on Home
   is an ordinary card (owner, 23 Sept 2026): one card leads Home, the evening. */
.tile.hero { background: linear-gradient(180deg, rgba(199, 154, 68, .16),
  rgba(199, 154, 68, .03)); border-color: var(--line); }

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

/* ---- view header — the standard screen header (title + optional actions) ---- */
.view-header, .day-header { padding: 8px 0 14px; border-bottom: 1px solid var(--line);
  margin-bottom: 16px; }
.view-header h2, .day-header h2 { font-size: 1.3rem; }
.view-header .feast, .day-header .feast { color: var(--accent); font-style: italic; }
.view-header .subline, .day-header .subline { display: flex; justify-content: space-between;
  align-items: center; gap: var(--s2); }
.view-header .subline.baseline, .day-header .subline.baseline { align-items: baseline; }

/* ---- date pager — a real 48px tap target for ‹ prev / next › (never a bare glyph) ---- */
.pager { display: inline-flex; align-items: center; gap: var(--s1); }
.nav-arrow { display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-icon); height: var(--ctl-icon); border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--card); color: var(--gold-soft); font-size: 1.2rem; line-height: 1;
  text-decoration: none; transition: background .16s ease, border-color .16s ease; }
.nav-arrow:hover { background: var(--card-strong); border-color: var(--gold-soft); }
.nav-arrow:active { transform: scale(.96); }
.pager .reset { font-family: var(--sans); font-size: .8rem; color: var(--muted);
  text-decoration: none; padding: 0 var(--s2); min-height: var(--ctl); display: inline-flex;
  align-items: center; }
.pager .reset:hover { color: var(--ink); }

/* the standalone domain eyebrow (outside a .card, the accent must be set) */
.serif-caps.domain, .eyebrow.domain { color: var(--accent); }

/* ---- pillbar — a horizontal row of filter / selector / nav pills ---- */
.pillbar { display: flex; flex-wrap: wrap; gap: var(--gap-controls); margin: 6px 0 10px; }
.pillbar.roomy { margin-top: var(--gap-section); }  /* a pillbar starting a new region */
.pillbar.center { justify-content: center; }

/* ---- empty state — eyebrow + lede + optional call to action ---- */
.empty { text-align: center; padding: 20px 8px; }
.empty .eyebrow { color: var(--gold); }
.empty p { color: var(--muted); max-width: 30em; margin: 6px auto; }

/* ---- dotstrip — a 7-night streak strip (S M T W T F S) ---- */
.dotstrip { display: flex; gap: var(--s2); justify-content: center; margin: var(--s2) 0; }
.dotstrip .d { display: flex; flex-direction: column; align-items: center; gap: 3px;
  font-family: var(--display); font-size: .6rem; letter-spacing: .06em; color: var(--muted); }
.dotstrip .pip { width: 20px; height: 20px; border-radius: 50%; border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center; font-size: .7rem; }
.dotstrip .d.on .pip { border-color: var(--gold);
  background: linear-gradient(180deg, var(--gold-soft), var(--gold)); color: var(--on-gold); }
.dotstrip .d.today .pip { box-shadow: 0 0 0 2px var(--line-soft); }

/* ---- option — an examen answer button (left-aligned, numbered) ---- */
.btn.option { text-align: left; margin-top: var(--s2); }
.btn.option .n { opacity: .5; margin-right: 4px; }

/* ---- meta-list — stacked "label · value" lines under a stat ---- */
.meta-list { line-height: 1.7; }

/* ---- stat table — small aggregate tables (Platform insights) ---- */
.stat-table { width: 100%; border-collapse: collapse; margin-top: 6px; }
.stat-table th {
  font-family: var(--sans); font-size: .68rem; font-weight: 600; color: var(--muted);
  letter-spacing: .08em; text-transform: uppercase; text-align: left;
  padding: 6px 8px 6px 0; border-bottom: 1px solid var(--line);
}
/* Top, not middle: when a label wraps to two lines and its value does not, a
   centred value floats between them and reads as though it belongs to the row
   above. The gate table shipped looking exactly that broken. */
.stat-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line-soft);
  vertical-align: top; }
.stat-table td + td, .stat-table th + th { text-align: right; padding-right: 0; padding-left: 8px; }
.stat-table tr:last-child td { border-bottom: 0; }

/* ---- cards ---- */
.card {
  background: var(--card);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin: var(--gap-card) 0;
  display: block;
  text-decoration: none;
  position: relative;
}
a.card:active { background: var(--card-strong); }
.card:has(.check) { padding-right: 56px; }   /* keep titles clear of the check circle */
.card .domain { color: var(--accent); }
.card h3 { margin: 2px 0; }
.card.done { opacity: .55; }
.card.done h3 { text-decoration: line-through; text-decoration-thickness: 1px; }
.card.done h3.no-strike { text-decoration: none; }  /* devotional cards: dim, don't cross out */
.card .check {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  width: 28px; height: 28px; border-radius: 50%;
  border: 2px solid var(--line); background: transparent;
}
.card.done .check { border-color: var(--gold); background: var(--gold); }
.card.done .check::after {
  content: "✓"; color: var(--on-gold); display: block; text-align: center; line-height: 26px;
  font-family: var(--sans); font-size: 16px;
}

.child-section { margin-top: var(--gap-section); }
.child-section > h2 { display: flex; align-items: baseline; gap: 8px; }
/* A GROUP LABEL IS NOT A HEADING (owner, 15 Sept 2026). "Old Testament" names
   the list under it; "Mateo" names a person, and a page's own name sits above
   both. Those three sat at 19.2, 19.2 and 22.4px — steps of 1.17 and 1.20 —
   so nothing read as bigger and every screen read as loud. A label now takes
   the eyebrow register the system already had, and the tiers separate.

   It is OPT-IN, and that direction is deliberate: a section that names a
   PERSON keeps the heading (the curriculum's Today, Rhythm and Growing put a
   child's name here, beside their band chip), so a forgotten class leaves a
   label looking as it always did — never a child's name shouted in capitals. */
.child-section > h2.section-label {
  font-family: var(--display); font-size: var(--t-eyebrow); font-weight: 500;
  letter-spacing: .22em; text-transform: uppercase; color: var(--muted);
  margin-bottom: var(--s2); }
.band-chip {
  font-family: var(--display); font-size: .62rem; font-weight: 500; color: var(--muted);
  letter-spacing: .14em; text-transform: uppercase;
  border: 1px solid var(--line); border-radius: 99px; padding: 3px 10px;
  background: transparent; white-space: nowrap;
}

.notice {
  background: var(--tint-gold); border: 1px solid var(--line);
  border-left: 3px solid var(--gold);
  border-radius: var(--radius); padding: 12px 14px; margin: var(--gap-card) 0;
}
.notice.danger { background: var(--tint-danger); border-color: rgba(231, 161, 161, .4);
  border-left-color: var(--danger); color: var(--ink); }

/* A NOTICE THAT SAYS MORE THAN ONE LINE IS A TWO-COLUMN GRID: a glyph that
   hangs, and ONE text edge for everything else.

   It used to be nested flex rows, which put the glyph inside the first line's
   row — so the parts BELOW that row (a steps line, the disclosure's body, the
   decision row) each started one glyph-width to the LEFT of the sentence they
   belong to, and a wrapped line landed under the glyph instead of under the
   first word. Three ragged edges in a 14px-padded box. The grid gives the
   glyph a column of its own on the first row only; every other child shares
   the second column, so there is one edge and a wrapped line keeps it. */
.notice.stack { display: grid; grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--s3); row-gap: var(--s2); align-items: start; }
.notice.stack > .notice-glyph { grid-column: 1; grid-row: 1; color: var(--gold-soft);
  display: flex; align-items: center; min-height: 1.5em; }
.notice.stack > .notice-glyph svg { display: block; }
.notice.stack > :not(.notice-glyph) { grid-column: 2; }
/* `.stack`'s `> * + *` margin is the vertical rhythm for a plain column; in
   this grid the row-gap is, and the two together pushed the disclosure 12px
   BELOW the glyph that introduces it — measured on the phone, not guessed.
   One owner per gap. */
.notice.stack > * + * { margin-top: 0; }

/* A notice's disclosure line is a LINE, not a centred block. `details.day-info
   > summary` centres its text inside a --touch-tall box, which puts the first
   line at 24px while it is one line and at 12px once it wraps — so a glyph
   beside it can only be right in one of the two cases, and the notice on Home
   is the one place a glyph sits beside a summary. Here the summary keeps its
   natural height and the tap area comes back as a halo, the same device every
   control in this file uses (see "THE HALO"): it reaches --touch without
   moving the first line by a pixel. */
.notice.stack > .notice-main > summary { min-height: 0; display: block; position: relative; }
.notice.stack > .notice-main > summary::before { content: ""; position: absolute;
  left: 0; right: 0; top: calc((100% - var(--touch)) / 2); height: var(--touch); }
.notice .notice-line { display: block; line-height: 1.5; }
/* ...and no line of a notice ends as a single short word, which is what
   strands the ⓘ alone on a line of its own. `text-wrap: pretty` pulls a word
   down where the browser has it, and costs nothing where it doesn't — .glue
   stays the contract for the ⓘ itself (see day-info below: a page that
   scrolls sideways is a worse fault than a glyph that wraps). */
.notice .notice-line, .notice .notice-steps { text-wrap: pretty; }

/* The steps under the home-screen invitation: what to tap, in order. The glyph
   leads the line rather than sitting inside the sentence, so the sentence stays
   ONE translatable string with no markup in it. */
.notice .notice-steps { margin: 0; font-size: var(--t-small); color: var(--ink-2); }
.notice .notice-steps svg { width: 15px; height: 15px; vertical-align: -2px;
  color: var(--gold-soft); }

/* ALREADY A HOME-SCREEN APP — then the invitation has been answered, and the
   server is the one place that cannot know it: iOS gives a standalone app the
   same user-agent as an in-app browser, so onefamily/device.py renders the
   notice for both and this withholds it from the one that is done. It lands
   before paint, so the notice never appears and then vanishes. (base.html's
   navigator.standalone is the iOS belt to this braces — see _notices.html.) */
@media (display-mode: standalone) { .notice-device { display: none; } }

/* ---- back link — the left slot of the top bar on every non-root screen ---- */
/* backlink — the ONE back control, and it lives in the bar, not in the content
   column. It used to be a bordered chip below the header, which made three rows
   of chrome (brand / back / page head) before the first real thing and made the
   loudest element on the page the way OUT of it. In the bar it is what every
   phone app does: a quiet chevron and the parent's name, weighted as chrome. */
.backlink { display: inline-flex; align-items: center; gap: 5px; min-height: var(--touch);
  margin-left: -8px; padding: 0 8px;
  font-family: var(--sans); font-size: .88rem; font-weight: 500; letter-spacing: .01em;
  color: var(--muted); text-decoration: none; -webkit-tap-highlight-color: transparent;
  max-width: 62%; }
.backlink::before { content: ""; flex: 0 0 auto; width: 9px; height: 9px;
  border-left: 2px solid currentColor; border-bottom: 2px solid currentColor;
  border-radius: 1px; transform: rotate(45deg); margin-right: 2px; }
.backlink > span, .backlink { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.backlink:hover { color: var(--parch); }

/* ---- form helpers — consistent labels, help text, checkbox rows ---- */
.help { font-family: var(--sans); font-size: .82rem; color: var(--muted); margin: 3px 0 0; }
.field + .field { margin-top: var(--s3); }
.field-hi { border: 1px solid var(--line); border-left: 3px solid var(--gold);
  background: var(--tint-gold-soft); border-radius: var(--radius); padding: 12px 14px; }
.field-hi select { margin-top: 6px; }
.check-row { display: flex; align-items: center; gap: 10px; min-height: var(--touch); margin: 4px 0;
  font-family: var(--serif); font-size: 1rem; color: var(--ink);
  text-transform: none; letter-spacing: 0; cursor: pointer; }
.check-row input[type=checkbox] { width: 22px; height: 22px; flex: 0 0 auto; accent-color: var(--gold); }

/* ---- activity detail ---- */
.activity h1 { margin-bottom: 2px; }
.activity .meta { margin-bottom: 14px; }
.activity section { margin: 18px 0; }
.activity ul, .activity ol { margin: .4em 0; padding-left: 1.3em; }
.activity li { margin: .35em 0; }
.script {
  background: var(--card); border: 1px solid var(--line-soft); border-radius: var(--radius);
  padding: 14px 16px; font-size: 1.05rem;
}
.song {
  background: var(--card-strong); border: 1px solid var(--line-soft); border-radius: var(--radius);
  padding: 14px 16px; white-space: pre-line;
}
.song .gloss { font-style: italic; color: var(--muted); white-space: normal;
  border-top: 1px dashed var(--line); margin-top: 10px; padding-top: 8px; }
.activity.condensed .depth { display: none; }

/* segmented Essentials / Full-guide control */
.detail-toggle { display: inline-flex; border: 1px solid var(--line); border-radius: 999px;
  overflow: hidden; margin: 10px 0 4px; }
.detail-toggle .seg { font-family: var(--display); font-size: .78rem; font-weight: 500;
  color: var(--muted); text-decoration: none; padding: 6px 14px; min-height: var(--ctl);
  display: flex; align-items: center; }
.detail-toggle .seg.on { background: linear-gradient(180deg, var(--gold-soft), var(--gold));
  color: var(--on-gold); }

/* lesson feedback */
.feedback { border-top: 1px solid var(--line-soft); margin-top: 24px; padding-top: 14px; }
.feedback .fb-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.feedback .fb-actions { display: flex; gap: 8px; }
.feedback .fb-actions form { margin: 0; }
.fb-btn { line-height: 1; padding: 8px; min-height: 48px; min-width: 48px;
  display: inline-flex; align-items: center; justify-content: center; color: var(--muted);
  border: 1px solid var(--line); border-radius: 12px; background: var(--card); cursor: pointer; }
.fb-btn svg { width: 22px; height: 22px; }
.fb-btn.on { border-color: var(--gold); background: var(--card-strong); color: var(--gold-soft); }
.fb-reasons { margin-top: 8px; }
.feedback .fb-thanks { margin: 6px 0 0; }
.feedback .fb-prompt { margin: 8px 0 6px; }
.feedback .fb-send { margin-top: 10px; }
.fb-reason { display: flex; align-items: center; gap: 10px; padding: 12px 0; min-height: var(--touch);
  font-size: .95rem; border-bottom: 1px solid var(--line-soft); cursor: pointer; }
.fb-reason input { width: 22px; height: 22px; flex: 0 0 auto; accent-color: var(--gold); }

details.why { border-top: 1px solid var(--line-soft); padding-top: 10px; }
details.why summary { cursor: pointer; font-weight: 600; padding: 8px 0;
  min-height: var(--touch); display: flex; align-items: center; }

/* ---- disclosure section — the ONE expandable section (About, long-form
   reading). A tappable title row with a rotating gold caret; the body stays
   in the reading measure. Use for optional depth, never to hide essentials. */
details.section { border-bottom: 1px solid var(--line-soft); }
details.section > summary { cursor: pointer; list-style: none; display: flex;
  align-items: center; gap: var(--s3); min-height: var(--touch); padding: 12px 0;
  font-family: var(--display); font-size: var(--t-h3); color: var(--parch); }
details.section > summary::-webkit-details-marker { display: none; }
details.section > summary svg { width: 20px; height: 20px; flex: 0 0 auto; color: var(--gold-soft); }
details.section > summary::after { content: ""; margin-left: auto; flex: 0 0 auto;
  width: 8px; height: 8px; border-right: 2px solid var(--gold);
  border-bottom: 2px solid var(--gold); transform: rotate(45deg);
  transition: transform var(--dur) var(--ease); }
details.section[open] > summary::after { transform: rotate(-135deg); }
/* A section heading may live INSIDE the summary, so the report keeps a real
   document outline (six <h2>s a screen reader can jump between) while the
   summary keeps the one disclosure treatment. The heading takes the summary's
   type rather than bringing its own — two type scales on one row is how a
   disclosure stops looking like the others. */
details.section > summary > h2 { margin: 0; font: inherit; color: inherit; }
details.section .section-body { padding: 0 0 var(--s4); }
/* Inside a card the card's own edge is the rule; a second one under the
   summary would draw the same edge twice. The direct-child selector missed
   the nested case — the platform read's fold sits inside the card's own body
   div, and drew a hairline a few pixels above the card's border. What the
   rule is actually about is a section that has nothing after it, so
   :last-child says it for both, and dividers BETWEEN stacked sections (the
   About page) are untouched. */
.card > details.section, .card details.section:last-child { border-bottom: 0; }
.card > details.section + form { margin-top: var(--s2); }
details.section .section-body > *:first-child { margin-top: 0; }
details.section .section-body .btn { margin-top: var(--s3); }
/* readings — the daily-readings disclosure; a small gold check rides the
   summary once today's Word has been received */
details.section.readings .readings-done-mark svg { width: 15px; height: 15px; }
/* the embedded readings text (sanitized USCCB feed HTML) set in our own
   typography: each reading wears its NAME as a title — the display face at
   --t-h2, gold — its citation on the line beneath, the passage in serif; and
   one reading is set apart from the next by a hairline and a section's worth
   of air (owner, 9 Sept 2026: the readings ran into one another, and the gold
   eyebrow was too small to read as the title of a reading). The h3 is the
   traditional calendar's Mass title — a label over the set, so it keeps the
   eyebrow. The citation arrives two ways from the feed — inside the heading
   (the live days) or as the paragraph after it (the fixtures) — and is set the
   same way from either: sans, small, on its own line. */
.readings-text { margin-top: var(--s2); }
.readings-text h3 { font-family: var(--display);
  font-size: var(--t-eyebrow); font-weight: 600; letter-spacing: .18em;
  text-transform: uppercase; color: var(--gold-soft); margin: var(--s4) 0 var(--s1); }
.readings-text h4 { font-family: var(--display); font-size: var(--t-h2);
  font-weight: 500; line-height: 1.25; letter-spacing: 0; text-transform: none;
  color: var(--gold-soft); margin: var(--s5) 0 var(--s2); }
.readings-text h3:first-child, .readings-text h4:first-child { margin-top: 0; }
/* the second reading onward: a hairline and the section gap above the title */
.readings-text h4 ~ h4 { margin-top: var(--gap-section); padding-top: var(--gap-section);
  border-top: 1px solid var(--line-soft); }
.readings-text h4 a, .readings-text h4 + p a:only-child { display: inline-block;
  font-family: var(--sans); font-size: var(--t-small); font-weight: 600;
  line-height: 1.4; letter-spacing: .02em; text-transform: none; }
.readings-text h4 a { display: block; width: max-content; max-width: 100%;
  margin-top: var(--s1); }
.readings-text h4 + p { margin-top: 0; }
/* The feed opens with a list holding the day's own title as a link
   ("Readings for the Memorial of Saint Peter Claver…") and, some days, its
   lectionary number. The page already names the day above, so this is a
   quiet lead line in the sans, never a bullet (owner's screenshot, 9 Sept
   2026: a stray bullet over the first reading). */
.readings-text ul, .readings-text ol { list-style: none; margin: 0 0 var(--s3); padding: 0; }
.readings-text li { font-family: var(--sans); font-size: var(--t-small); color: var(--muted);
  margin: 0 0 var(--s1); }
.readings-text li a { color: var(--muted); border-bottom-color: var(--line-soft); }
.readings-text li a:hover { color: var(--parch); }
.readings-text p, .readings-text div { margin: .35em 0; }
.readings-text a { color: var(--gold-soft); text-decoration: none;
  border-bottom: 1px solid var(--line); }
.readings-text a:hover { color: var(--parch); border-bottom-color: var(--gold); }
.readings-credit { margin-top: var(--s3); border-top: 1px solid var(--line-soft);
  padding-top: var(--s2); font-size: .72rem; }
.readings-credit a { color: var(--gold-soft); }
/* the quiet door from the disclosure to the ±7-day readings page */
.readings-days { margin-top: var(--s3); }
.readings-days a { display: inline-flex; align-items: center; gap: 4px;
  min-height: var(--ctl); color: var(--gold-soft); text-decoration: none;
  font-family: var(--sans); font-weight: 600; }
.readings-days a:hover { color: var(--parch); }
/* the "show in <reader language>" peek under a curriculum-language script */
details.peek { margin-top: 8px; }
details.peek summary { cursor: pointer; min-height: var(--touch); display: inline-flex;
  align-items: center; font-family: var(--sans); font-size: .82rem; color: var(--gold-soft); }
details.peek .script { margin-top: 6px; }
.pillar { margin: var(--gap-card) 0; }
.pillar .serif-caps { color: var(--accent); }

/* ---- buttons & forms ---- */
.btn {
  display: inline-block; font-family: var(--display); font-weight: 500;
  border-radius: 12px; border: 1px solid var(--line); background: var(--card);
  color: var(--ink); padding: 10px 16px; min-height: var(--ctl-btn); cursor: pointer;
  text-decoration: none; text-align: center; font-size: .95rem;
}
/* ONE selected treatment, two ways of being selected. Most answer rows are
   submit buttons and carry .primary when chosen; a rating that has to travel
   WITH a textarea has to be a radio instead, and it wears exactly the same
   thing rather than a second species. That case shipped as small wrapping
   chips beside full-width rows on every other rung. */
.btn.primary,
.btn.option:has(> input:checked) {
  background: linear-gradient(180deg, var(--gold-soft), var(--gold));
  border-color: transparent; color: var(--on-gold);
}
/* The radio itself is hidden — the row IS the control, as on .chip. */
.btn.option > input { position: absolute; opacity: 0; pointer-events: none; }
.btn.option:has(> input:focus-visible) { outline: 2px solid var(--gold-soft);
  outline-offset: 2px; }
.btn.option:has(> input) .n { opacity: .5; }
.btn.block { display: block; width: 100%; }
.btn.danger { color: var(--danger); border-color: rgba(231, 161, 161, .45); }
.btn.danger:hover { background: var(--tint-danger); }
.btn:active { filter: brightness(.92); }
.btn:disabled, .pill:disabled { opacity: .45; cursor: not-allowed; filter: none; }

/* Focus — ONE ring, everywhere. Pointer taps stay quiet (:focus-visible);
   keyboard and switch users always see where they are. Hidden chip inputs
   surface their ring on the chip label (see .chip-select). */
:focus-visible { outline: 2px solid var(--gold-soft); outline-offset: 2px; }
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline-offset: 0; }
input[type=text], input[type=email], input[type=tel], input[type=number],
input[type=password], input[type=date], select, textarea {
  width: 100%; font: inherit; color: var(--parch); background: var(--field-bg);
  border: 1px solid var(--line-soft); border-radius: 10px; padding: 12px; min-height: 48px;
}
input::placeholder, textarea::placeholder { color: var(--faint); }

/* ---- .search — a CONTROL, not a form field ----------------------------------
   A search box filters what you are already looking at; it does not capture
   data you submit. That is why it wears the control language — --r-pill, the
   height of a pill, a leading magnifier — rather than the 10px corners every
   other input in the app has. It shipped once as `input[type=search]` appended
   to the form-field selector, which put a 10px box directly above three 999px
   pill selects: peer controls in a row must be the same component, and they
   were not.

   The ground stays --field-bg, because that part of the input language is
   right: a thing you type into is sunk into the page, not raised off it. */
.search { position: relative; display: flex; align-items: center; }
.search > svg { position: absolute; left: 15px; width: 18px; height: 18px;
  color: var(--faint); pointer-events: none; }
.search input[type=search] {
  width: 100%; font: inherit; color: var(--parch); background: var(--field-bg);
  border: 1px solid var(--line-soft); border-radius: var(--r-pill);
  min-height: var(--touch); padding: 0 16px 0 43px;
  -webkit-appearance: none; appearance: none;
}
.search input[type=search]:focus { outline: none; border-color: var(--gold); }
/* :has(), not a sibling combinator — the magnifier PRECEDES the input, and `~`
   only ever matches forward. `:focus ~ svg` is valid CSS that can never match,
   which no test can see: the rule is simply never applied. */
.search:has(input[type=search]:focus) > svg { color: var(--gold-soft); }
.search input[type=search]::placeholder { color: var(--faint); }
/* Safari paints its own cancel button and inner shell on a search field. */
.search input[type=search]::-webkit-search-decoration,
.search input[type=search]::-webkit-search-cancel-button { -webkit-appearance: none; }
/* The clear, shown only when there is something to clear. It is a link, so it
   works with no JS: it points at the same screen with the query dropped. */
.search .search-clear { position: absolute; right: 4px; display: inline-grid;
  place-items: center; width: var(--ctl-icon); height: var(--ctl-icon);
  border-radius: var(--r-pill); color: var(--muted); text-decoration: none; }
.search .search-clear svg { width: 16px; height: 16px; }
.search .search-clear:hover { color: var(--parch); }
.search:has(.search-clear) input[type=search] { padding-right: var(--ctl-icon); }
/* Safari paints its own cancel button and inner shell on a search field. */
input[type=search] { -webkit-appearance: none; appearance: none; }
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-cancel-button { -webkit-appearance: none; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--gold); }
/* ---- every select wears the same arrow — ours, never the platform's ----------
   appearance: none takes the native indicator away (black on iOS, invisible
   on the night); --select-arrow paints one chevron in the app's own ink, at
   the right edge, with the padding to keep the text off it. The form field
   and the pill differ only in how far in from the edge it sits. */
select { -webkit-appearance: none; appearance: none;
  background-image: var(--select-arrow); background-repeat: no-repeat;
  background-position: right 12px center; background-size: 18px 18px;
  padding-right: 40px; }
select option { background: var(--night-2); color: var(--parch); }
/* A date field on iOS draws at WebKit's own intrinsic width — wider than its
   card, spilling past the right edge (owner's screenshot of a child's card,
   9 Sept 2026) — and centres its value. Headless Chromium shows neither, so
   the sweep never saw it. Taking the platform's appearance away lets the
   width rule above hold, as it does for every other field; the inner value
   box is set flush left like a text input, and given a height so an empty
   field does not collapse. Chromium parses the -webkit- pseudo-element and
   matches nothing, which the stylesheet guard allows. */
input[type=date] { color-scheme: dark; -webkit-appearance: none; appearance: none;
  display: block; min-width: 0; max-width: 100%; text-align: left; }
input[type=date]::-webkit-date-and-time-value { text-align: left; min-height: 1.4em; }
/* ---- range — the ONE slider (Account settings → Text size) ----------------
   A form field, not a control: it sits in a .field under a label like a
   select. `.range-row` holds it between its two ends — a small A and a large
   A in the display face, so the scale reads without a word. The platform's
   track and thumb are taken away (appearance: none — the iOS thumb is white
   and the track grey, neither of them ours) and drawn in tokens: a hairline
   track in --line-soft, a gold thumb with the night as its rim, 48px tall to
   tap. Steps are the input's own (min/max/step); the page never invents
   positions. WebKit/Blink pseudo-elements only: the app is a home-screen
   app on iOS and the design-QA sweep drops a selector Chromium cannot parse
   (a ::-moz-range-thumb rule is one), so Firefox keeps its own thumb.
   Registered in /styleguide/ → Forms. */
.range-row { display: flex; align-items: center; gap: var(--s3); min-height: var(--touch); }
.range-row .range-a { font-family: var(--display); font-weight: 500; color: var(--muted);
  line-height: 1; flex: 0 0 auto; }
.range-row .range-a.sm { font-size: .8rem; }
.range-row .range-a.lg { font-size: 1.45rem; }
input[type=range].range { -webkit-appearance: none; appearance: none; flex: 1 1 auto;
  min-width: 0; width: 100%; height: var(--touch); margin: 0; padding: 0;
  background: transparent; border: 0; cursor: pointer; }
input[type=range].range:focus { outline: none; }
input[type=range].range::-webkit-slider-runnable-track { height: 4px; border-radius: 2px;
  background: var(--line-soft); }
input[type=range].range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
  width: 24px; height: 24px; border-radius: 50%; margin-top: -10px;
  background: var(--gold); border: 2px solid var(--night-3);
  box-shadow: 0 0 0 1px var(--line); }
input[type=range].range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--tint-gold), 0 0 0 4px var(--gold); }
label { display: block; margin: 12px 0 4px; font-family: var(--display);
  font-size: .8rem; font-weight: 500; letter-spacing: .06em; color: var(--muted); }
form .btn { margin-top: 16px; }
.errorlist { color: var(--danger); font-family: var(--sans); font-size: .85rem;
  list-style: none; padding: 0; margin: 4px 0; }

/* ---- messages ---- */
/* The container never takes a tap — only a toast does. After the toast fades
   (visibility: hidden, below) the fixed container is still there, full-width
   above the tab bar, and it swallowed every tap on whatever control sat in
   that band: the Send-feedback button, once the controls were drawn smaller
   and it moved up into it. */
.messages { position: fixed; left: 12px; right: 12px; pointer-events: none;
  bottom: calc(76px + var(--safe-bottom)); z-index: var(--z-float); }
.messages .msg { pointer-events: auto;
  background: var(--parch); color: #241a08; font-family: var(--sans); font-size: .9rem;
  border-radius: 12px; padding: 12px 16px; margin-top: 8px; text-align: center;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  animation: msg-fade 4s forwards;
}
@keyframes msg-fade { 0%,80% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }

/* ---- bottom navigation: the app switcher (thumb zone) ---- */
nav.tabbar {
  /* Plainly at the bottom of the viewport — and the whole job is making sure
     the viewport is the right size when it gets here.

     In a standalone home-screen app WebKit lays the page out 62px short (the
     top inset) and only resolves it once the document can scroll. Measured on
     the device: innerHeight 812 against an 874px screen, and vh/dvh/svh/lvh
     all derive from that same box, so no CSS length escapes it.

     Pushing the bar DOWN to the real screen edge does not work either: WebKit
     clips a fixed element at the layout viewport, so the bar landed correctly
     and its labels — the part below 812 — simply were not painted, while the
     icons above the line survived. That is the version with icons and no
     words under them.

     So nothing here compensates. base.html makes the document scrollable by a
     hair while the viewport is short, which is what makes WebKit resolve it —
     the same thing a small scroll with a finger does. Once it resolves, this
     rule is simply correct. */
  position: fixed; left: 0; right: 0; z-index: var(--z-nav);
  box-sizing: border-box;
  height: calc(var(--tabbar-h) + var(--safe-bottom));
  top: auto; bottom: 0;
  background: var(--bar-bg);
  /* ITS OWN COMPOSITING LAYER, AND NO BACKDROP TO RE-SAMPLE.
     The bar came loose from the bottom edge while a page scrolled — measured
     on the device at 523pt on an 874pt screen, 259pt up, which is a scroll
     distance: it travelled with the document instead of staying anchored. A
     transform on the element ITSELF does not disturb its own fixed
     positioning (only an ancestor's would); it hands the bar to the scroll
     thread so the page can move under it without a repaint. */
  transform: translateZ(0);
  border-top: 1px solid var(--line);
  display: flex; justify-content: center;              /* centered on wide screens */
  /* The home indicator's inset is the only thing between the labels and the
     screen's edge. Adding 6px of our own on top of it left a 40px dead band
     below the labels and made the bar ~15px taller than a native tab bar. */
  padding: 6px 4px var(--safe-bottom);
}
nav.tabbar a {
  flex: 1 1 0; max-width: 128px;                        /* fill on mobile, cap on desktop */
  font-family: var(--display); font-size: .62rem; font-weight: 500;
  letter-spacing: .08em; color: var(--muted);
  text-decoration: none; text-align: center; padding: 6px 10px; min-width: 56px;
  min-height: var(--touch); display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px;
  border-radius: 10px;
}
nav.tabbar a .glyph { display: block; line-height: 1; }
nav.tabbar a .glyph svg { width: 22px; height: 22px; display: block; }
nav.tabbar a.active { color: var(--gold-soft); background: var(--tint-gold); }
nav.tabbar a.active .glyph svg { filter: drop-shadow(0 0 6px rgba(227, 196, 126, .35)); }

/* ---- SCROLL LAB — temporary, every switch default OFF ---------------------
   The tab bar came loose from the bottom edge while a page scrolled on a real
   iPhone and stayed pinned in headless Chromium throughout, so the suspects
   could not be told apart from a desktop. The fix removed all of them at once
   rather than bisecting a bug nobody could measure.

   These switches now run the other way: each puts ONE of those things back. If
   the bar still comes loose, turning them on one at a time says which change
   mattered — or that none did, which points at the ground, the only suspect
   left standing. Delete this block, the switch reader in base.html, the lab
   section of /viewport/ and scroll-lab.spec.mjs together once it is settled. */

/* Put the bar's backdrop-filter back. */
html[data-lab~="blur"] nav.tabbar {
  background: var(--bar-bg-blurred);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}
/* Take both fixed layers off their own compositing layer again. */
html[data-lab~="nolayer"] nav.tabbar,
html[data-lab~="nolayer"] .statusbar-scrim { transform: none; }
/* Leave the overlay composited on every screen, as it was. */
html[data-lab~="eagerptr"] #ptr { display: flex; }
html[data-lab~="eagerptr"] #ptr .ptr-core { will-change: opacity, transform; }
/* The one suspect the fix did NOT touch: the candlelit ground is a
   full-viewport fixed layer too, but a static one, which is what a compositor
   handles best — and it is the app's identity. If the bar is still loose with
   everything else back to new, this is what is left. */
html[data-lab~="noground"] body::before { display: none; }
html[data-lab~="noscrim"] .statusbar-scrim { display: none; }
/* And the floor itself, so it can be taken back out from the phone without a
   deploy if it turns out to cost more than the crawl did. base.html reads the
   same switch and publishes nothing. */
html[data-lab~="nofloor"] body { min-height: 100dvh; }
/* And the resume handling, in case a reload on return is worse than the freeze
   it is there to clear — a cold start is a real cost on an app that scales to
   zero. base.html reads the same switch. */
html[data-lab~="noresume"] body { }
/* nonudge and noplace are read by base.html too: the first skips the
   one-pixel scroll-and-back on a return to the app and the same one after the
   keyboard goes away, the second lands a reload at the top instead of
   restoring a saved place. Both are programmatic scrolls, and each is a
   suspect for stranding the fixed layers. */
html[data-lab~="nonudge"] body { }
html[data-lab~="noplace"] body { }

/* ---- top bar ---- */
header.topbar {
  display: flex; align-items: center; justify-content: space-between;
  max-width: 640px; margin: 0 auto; font-family: var(--sans);
  /* viewport-fit=cover + a black-translucent status bar means iOS draws the
     page BEHIND the clock in a home-screen app — deliberately, so the
     candlelit ground runs to the very top. The bottom inset was honoured in
     three places and the top in none, so the lockup sat under the time. */
  padding: calc(var(--safe-top) + var(--bar-pad)) 16px 0;
}
/* The bar is a row on every screen: the left slot (the menu, the lockup
   beside it on Home, the way back, or the ✕ on More) and the right slot (the
   Emmaus door; history + New on Emmaus's own page). It used to float onto the
   page's name row on tab roots, where it held nothing but the overflow — now
   it holds two controls, and a row of two controls IS a bar, the way every
   iOS nav bar carries a leading and a trailing item. */
.bar-lead { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
/* THE CORNERS ARE PINNED (owner, 14 Sept 2026, after Truthly): the menu / the
   way back on the left and the Emmaus door (or history + New) on the right stay
   where the thumb learned them while the page scrolls beneath — two more fixed
   layers, held to the same law as the tab bar and the status strip (clauses
   12–14): promoted, opaque, no backdrop to re-sample, no transformed ancestor.
   Each control wears an opaque disc from the bar's own ground and a hairline,
   so it reads as a button over whatever passes under it; the lockup on Home
   stands centred in the flow and scrolls away, as a name should. The bar
   keeps the height of the row, so a page starts below the corners at rest. */
header.topbar { min-height: calc(var(--safe-top) + var(--bar-pad) + var(--touch)); justify-content: center; }   /* border-box: the inset, the pad, the row */
.bar-lead, header.topbar .controls {
  position: fixed; top: calc(var(--safe-top) + var(--bar-pad)); z-index: var(--z-nav);
  transform: translateZ(0); }
.bar-lead { left: max(16px, calc(50% - 320px + 16px)); }
header.topbar .controls { right: max(16px, calc(50% - 320px + 16px)); }
.bar-lead .menu-btn, .bar-lead .backlink, header.topbar .controls .menu-btn, header.topbar .controls .pill {
  background: var(--bar-bg); border: 1px solid var(--line); }
/* Pinned, the way back sits in a shrink-to-fit box, where its percentage
   max-width has nothing to resolve against and clipped the name to three
   letters ("Exa"). A viewport length instead. */
.bar-lead .backlink { padding: 0 14px 0 10px; margin-left: 0; border-radius: var(--r-pill); max-width: 58vw; }

/* NOTHING LEGIBLE PASSES UNDER THE CLOCK.
   viewport-fit=cover + a black-translucent status bar means the page draws
   behind the status bar on purpose, so the candlelit ground runs to the very
   top. The top bar scrolls away with the page, though, and once it has, body
   text collides with the time and the signal bars.
   A fixed strip exactly the inset's height blurs whatever passes beneath it.
   It is NOT a solid fill: at rest the backdrop is the smooth ground, so a blur
   of it is indistinguishable from it — no band, which is the mistake the foot
   of the screen already taught. Zero-height where there is no inset. */
.statusbar-scrim {
  position: fixed; top: 0; left: 0; right: 0; z-index: var(--z-nav);
  height: var(--safe-top); pointer-events: none;
  transform: translateZ(0);   /* the other fixed layer, loose in the same frame */
  /* It paints THE GROUND, sized and positioned to the viewport exactly as the
     full-viewport layer is — so at rest it is the same pixels as the page
     behind it, and scrolled content simply disappears beneath it. A blur alone
     left readable smear under the clock; a flat fill would be a dark band at
     rest, which is the mistake the foot of the screen already taught. */
  background: var(--ground);
  background-size: 100vw 100dvh;
  background-position: 0 0;
}

/* ---- brand lockup ------------------------------------------------------
   The name means "the little candle", so the mark IS one: a votive whose
   wax is lit from above by the flame it carries and falls into shadow
   below. No wick — at 30px a wick reads as a gap between two shapes; the
   pool of light bridges them instead, which is also what a real candle
   does. The flame stays the brightest thing in the mark, because it is the
   motif the whole app is built on.
   The wordmark subordinates the article: "LA" is a small letterspaced
   particle in muted ink, "Velita" carries the name in display italic gold.
   Two words, one name — not two names. */
/* not scoped to the topbar: the styleguide renders the lockup too, and an
   underlined wordmark there is the same bug wherever it appears. */
.brand { display: inline-flex; align-items: center; gap: 10px;
  text-decoration: none; min-height: var(--touch); }
.brand-mark { flex: 0 0 auto; width: 30px; height: 30px; display: grid; place-items: center;
  border-radius: 50%; border: 1px solid rgba(199, 154, 68, .42);
  background: radial-gradient(circle at 50% 36%, rgba(227, 196, 126, .24), rgba(20, 26, 48, 0) 74%); }
.brand-mark svg { width: 30px; height: 30px; overflow: visible; }
.brand-mark .wax { fill: url(#waxgrad); }
.brand-mark .pool { fill: url(#poolgrad); }
.brand-mark .flame { fill: url(#flamegrad); stroke: none;
  filter: drop-shadow(0 0 4.5px rgba(240, 181, 61, .8)); }
.brand-word { font-family: var(--display); font-weight: 500; font-size: 1.2rem;
  font-style: italic; color: var(--gold-soft); letter-spacing: .005em;
  line-height: 1; white-space: nowrap; }
/* the article — a particle, not half the name */
/* In the bar the mark is chrome, not a feature: no ring, no pool of light, so
   it never competes with the one circular focal object a screen is allowed. The
   standalone lockup (sign-in, the styleguide, About) keeps the full medallion. */
header.topbar .brand-mark { border: 0; background: none; width: 26px; height: 26px; }
header.topbar .brand-mark svg { width: 26px; height: 26px; }
header.topbar .brand-word { font-size: 1.1rem; }

.brand-word i { font-style: normal; font-size: .52em; font-weight: 500;
  letter-spacing: .18em; color: rgba(244, 236, 216, .52);
  margin-right: .30em; vertical-align: .18em;
  text-transform: uppercase; }

/* the running build — a commit is not prose, so it wears the mono face */
.build-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82em; letter-spacing: .02em; }

/* ---- icons — one line-art set (see families/templatetags/icons.py) ---- */
.ico { width: 1.1em; height: 1.1em; display: inline-block; vertical-align: -0.16em;
  flex: 0 0 auto; }
.ico-lg { width: 1.5em; height: 1.5em; }
header.topbar .controls { display: flex; flex-wrap: wrap; justify-content: flex-end;
  gap: 8px; align-items: center;
  /* the left slot is empty on the tab roots that are not Home — keep the
     overflow on the right edge instead of letting it slide left */
  margin-left: auto; }
/* menu-btn — the overflow control. The five daily things own the thumb zone;
   everything else lives here, one tap from any screen. */
.menu-btn { display: inline-grid; place-items: center;
  min-width: var(--ctl-icon); min-height: var(--ctl-icon); border-radius: var(--r-pill);
  color: var(--muted); text-decoration: none; -webkit-tap-highlight-color: transparent; }
.menu-btn svg { width: 20px; height: 20px; }
.menu-btn:hover { color: var(--parch); }
.menu-btn.active { color: var(--gold-soft); background: var(--tint-gold); }
/* Open state: the ⋯ has become the ✕ that closes what it opened. */
.menu-btn.is-close { color: var(--gold-soft); background: var(--tint-gold); }
/* THE CORNERS ARE GLYPHS, on purpose (owner, 14 Sept 2026). The More control
   carried the word for a while — a ⋯ in a corner reads as "settings for this
   page" — and the hamburger is the one glyph that does not need one: it is
   read as "menu" by everyone, in every app. The Emmaus door and, on Emmaus's
   page, history and New are the same species: a bare glyph, its accessible
   name in aria-label, never a second label under it. The row-level ⋯ menus
   (a conversation's options) are the same control too; one treatment. */
.menu-btn.nav-menu svg, .menu-btn.nav-emmaus svg { width: 22px; height: 22px; }
/* The corner controls are the full tap area (--touch), not the drawn 40px: the
   lockup beside the menu on Home is 48px tall, and a 40px button centred in
   that row started 4px below the bar's padding — the first chrome on Home sat
   lower than on every other screen (page shell, rule 2). At 48px the button
   IS the row, on every screen, and the thumb gets the target it should. */
.menu-btn.nav-menu, .menu-btn.nav-emmaus, .chat-switch .menu-btn {
  min-width: var(--touch); min-height: var(--touch); }
/* The right corner READS AS A BUTTON (owner, 14 Sept 2026: a bare glyph there
   "doesn't really look clickable"): a tinted disc with a hairline, the same
   treatment the active tab wears, so the door to Emmaus — and, on its own
   page, history and New — are plainly things to press. The hamburger stays
   bare: it is the one glyph that needs no help. */
.menu-btn.nav-emmaus, .chat-switch .menu-btn {
  color: var(--gold-soft); background: var(--bar-bg); border: 1px solid var(--line); }
.menu-btn.nav-emmaus:hover, .chat-switch .menu-btn:hover { border-color: var(--gold); }
button.menu-btn { background: none; border: 0; padding: 0; cursor: pointer; font: inherit; }
.menu-btn:focus-visible { outline: 2px solid var(--gold-soft); outline-offset: 2px; }
.pill {
  font-family: var(--display); font-size: .72rem; font-weight: 500;
  border: 1px solid var(--line); background: transparent; color: var(--muted);
  border-radius: 99px; padding: 0 12px; min-height: var(--ctl);
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  text-decoration: none; cursor: pointer;
}
.pill.active { color: var(--gold-soft); border-color: var(--gold); }
.pill.danger { color: var(--danger); border-color: rgba(231, 161, 161, .5); }
.pill.danger:hover { background: var(--tint-danger); }
/* The arrow is declared again here: .pill's `background: transparent` shorthand
   above wiped it once (a select.pill shipped bare), and background-COLOR keeps
   the ground without touching the image. */
select.pill { width: auto; max-width: 44vw; background-color: var(--night-2); color: var(--muted);
  background-image: var(--select-arrow); background-repeat: no-repeat;
  background-position: right 9px center; background-size: 14px 14px; padding-right: 28px; }
/* icon-only pill (a single glyph/svg): keep it square */
.pill.icon { padding: 0; width: var(--ctl); }
.pill.icon svg { width: 18px; height: 18px; }
/* THE HALO. Every control is drawn smaller than the tap area (36 / 44 / 40
   against 48) — the difference is an invisible box around it, so nothing gets
   harder to hit while everything stops looking inflated. --h is the control's
   drawn height; the halo reaches --touch tall and --s1 past each side. Two
   controls in a .row.tight (8px apart) get halos that just meet, never
   overlap. design-qa.spec.mjs measures the halo, not the drawn box. */
.pill, .btn, .menu-btn, .nav-arrow, .ico-btn, .search .search-clear,
.chip-select .chip, .ask-starters a, .ask-starters button, .detail-toggle .seg,
.chat-switch a, .chat-switch button, .q-cancel, .pager .reset, .readings-days a,
.word-act, .feed-act {
  position: relative; --h: var(--ctl); }
.btn { --h: var(--ctl-btn); }
.menu-btn, .nav-arrow, .ico-btn, .search .search-clear, .feed-act { --h: var(--ctl-icon); }
.pill::before, .btn::before, .menu-btn::before, .nav-arrow::before, .ico-btn::before,
.search .search-clear::before, .chip-select .chip::before, .ask-starters a::before,
.ask-starters button::before,
.detail-toggle .seg::before, .chat-switch a::before, .chat-switch button::before,
.q-cancel::before, .pager .reset::before, .readings-days a::before, .word-act::before,
.feed-act::before {
  content: ""; position: absolute;
  inset: calc((var(--h) - var(--touch)) / 2) calc(-1 * var(--s1)); }

/* ---- "More" tab — the mobile overflow hub (a full page, not a dropdown) ---- */
/* ---- page head: the ONE way a page introduces itself ----
   A glyph (section landings only), the page's name, and a line saying what it
   is for. Generalised from the More tab's own header, which was this shape
   already; .more-head/.more-avatar are gone so there is one treatment, not two.
   Partial: templates/_page_head.html */
.page-head { margin: 0 0 var(--s4); }
/* The mark and the name share a row; the description sits under BOTH, at the
   page's own left edge, with the full width to run in — which the long ones
   (the Bible's translation note) need. An earlier version put the description
   beside the glyph too, in a narrow column that started 66px in while every
   card on the page started at 17px. */
/* The mark is drawn to the NAME's height, not above it: at 44px it stood 9px
   taller than the title beside it and pushed the whole head down a screen. It
   is decorative (aria-hidden) and never a target, so the 48px tap floor does
   not apply to it. */
.page-title { display: flex; align-items: center; gap: var(--s2); }
.page-glyph { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 50%;
  display: grid; place-items: center; border: 1px solid var(--line);
  color: var(--gold-soft); background: var(--tint-gold-soft); }
.page-glyph svg { width: 20px; height: 20px; }
.page-head p { margin: var(--s2) 0 0; }
.page-head h1 { font-size: 1.3rem; margin: 0; }
.menu-list { display: flex; flex-direction: column; margin: var(--gap-card) 0;
  border: 1px solid var(--line); border-radius: var(--r); overflow: hidden;
  background: var(--card); }
.menu-list a, .menu-list button {
  display: flex; align-items: center; gap: 12px; width: 100%; min-height: var(--touch);
  padding: 0 14px; font-family: var(--serif); font-size: 1rem; color: var(--ink);
  text-decoration: none; background: none; border: 0; cursor: pointer; text-align: left; }
.menu-list a + a, .menu-list button + button { border-top: 1px solid var(--line-soft); }
.menu-list a:hover, .menu-list button:hover { background: var(--card-strong); }
.menu-list a > span, .menu-list button > span { flex: 1; }
.menu-list svg { width: 20px; height: 20px; color: var(--gold-soft); flex: 0 0 auto; }
.menu-list .row-go { color: var(--muted); width: 16px; height: 16px; }
.menu-list-inline { }
.menu-list .signout { color: var(--danger); }
.menu-list .pill { flex: 0 0 auto; min-height: 0; padding: 2px 10px;
  font-size: .62rem; letter-spacing: .08em; text-transform: uppercase; }
.menu-list .badge { flex: 0 0 auto; }
.admin-eyebrow { margin: var(--s4) 0 0; }
.menu-list .signout svg { color: var(--danger); }

/* ---- bible text — Scripture set for long reading (the in-app Bible) ---- */
.bible-text { max-width: 34em; }
.bible-verse { margin: 0 0 var(--s2); line-height: 1.62; }
.vnum { font-family: var(--sans); font-size: .62rem; font-weight: 600;
  color: var(--gold-soft); vertical-align: .45em; margin-right: 6px; opacity: .8;
  display: inline-block; min-width: 1.5em; line-height: 1.5em; text-align: center;
  border-radius: 99px; }
/* A verse that has been READ wears its number in a small gold bubble — the
   same gold-on-ink the `.badge` count already owns, at the verse number's own
   size. One state on the existing component (as `.pill.read` is to `.pill`),
   never a second species; the text itself stays untouched so a chapter half
   read still reads as one page. */
.bible-verse.read .vnum, .vnum.read { background: var(--gold); color: var(--on-gold);
  opacity: 1; padding: 0 .35em; }
/* In a legend the number sits on the line, not raised — the same glyph the
   reader shows, explained once under the text. */
.legend .vnum { vertical-align: 0; margin-right: 0; }
/* A verse the reader was SENT to — by an #anchor, or by ?v= from a citation
   anywhere in the app. One treatment for one idea: the same gold wash serves
   both, and a cited RANGE adds the accent rule the system already owns
   (`.card.accent`) so several verses read as one block rather than as several
   separate highlights. */
/* Land BELOW the bar, not under it. An anchored verse scrolled flush to y=0
   sits behind the fixed top bar and reads as if the link missed — the same fix
   `.chat-msg#reply` already carries. */
/* An anchor target clears the fixed top bar. One rule owns it: a cited verse
   the Bible scrolls to, and any section heading a page jumps to (the platform
   report's spine). A second copy of this is how two anchors start to differ. */
.bible-verse, h2[id], details.section[id] {
  scroll-margin-top: calc(var(--safe-top) + var(--s6)); }
.bible-verse:target { background: var(--tint-gold); border-radius: var(--r-sm);
  padding: 2px 6px; margin-left: -6px; }
/* A cited RANGE is one passage, not a stack of nine highlighted cards: the
   verses abut into a single band under one gold rule, and only the ends of the
   run are rounded. */
.bible-verse.cited { background: var(--tint-gold); color: var(--ink);
  border-left: 3px solid var(--gold); border-radius: 0;
  margin: 0 0 0 -12px; padding: 5px 8px 5px 9px; }
.bible-verse.cited:not(.bible-verse.cited + .bible-verse.cited) {
  border-top-right-radius: var(--r-sm); padding-top: 8px; }
.bible-verse.cited:not(:has(+ .bible-verse.cited)) {
  border-bottom-right-radius: var(--r-sm); padding-bottom: 8px;
  margin-bottom: var(--s2); }

/* ---- the Word's family actions — .word-marks · .word-actions · the comments page ----
   ONE component wherever Scripture is read (docs/architecture/SCRIPTURE-CONTEXT.md
   §2.7, design/DESIGN-SYSTEM.md "The Word's row"). A tap on a verse opens its row
   IN THE FLOW under it — never a fixed layer, never a scroll (§3.5); the chapter
   otherwise carries only the family's traces, never a button per verse. */
[data-word] .bible-verse[data-ref] { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.bible-verse.sel { background: var(--tint-gold); border-radius: var(--r-sm);
  box-shadow: 0 0 0 6px var(--tint-gold); }
.word-marks { display: inline-flex; gap: 8px; margin-left: .4em; white-space: nowrap;
  font-family: var(--sans); font-size: .72rem; color: var(--gold-soft);
  vertical-align: .1em; }
.word-marks > span { display: inline-flex; align-items: center; gap: 2px; }
.word-marks svg { width: 13px; height: 13px; }
.word-actions { margin: calc(-1 * var(--s1)) 0 var(--s2); padding: var(--s1) 0;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.word-actions-row { display: flex; justify-content: space-around; align-items: flex-start; }
.word-actions form { display: contents; }
.word-act { display: flex; flex-direction: column; align-items: center; gap: 3px;
  min-width: 56px; padding: 4px 2px; background: none; border: 0; cursor: pointer;
  color: var(--ink-2); text-decoration: none; font-family: var(--sans);
  font-size: .7rem; line-height: 1.2; }
.word-act svg { width: 22px; height: 22px; }
.word-act.on { color: var(--gold-soft); }
.word-act.on svg { fill: currentColor; }
.word-act[data-act="like"].on { color: var(--rose); }
.word-act.gold { color: var(--gold-soft); }
.word-who { margin: 6px 0 0; text-align: center; font-family: var(--sans);
  font-size: .72rem; color: var(--muted); }
.word-tip { display: flex; align-items: center; gap: var(--s2); }
.word-tip .notice-line { flex: 1; margin: 0; }
.word-quote { margin: var(--s2) 0 var(--s3); padding: 2px 0 2px var(--s3);
  border-left: 2px solid var(--gold); font-style: italic; color: var(--ink);
  line-height: 1.55; }
.word-count { margin: 0 0 var(--s1); }
.word-comments { list-style: none; margin: 0 0 var(--s3); padding: 0; }
.word-comment { display: flex; gap: var(--s2); align-items: flex-start;
  padding: var(--s2) 0; border-bottom: 1px solid var(--line-soft); }
.word-avatar { flex: none; width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--card-strong); border: 1px solid var(--line); color: var(--gold-soft);
  font-family: var(--sans); font-size: .8rem; font-weight: 600; }
.word-comment-body { flex: 1; min-width: 0; }
.word-comment-who { margin: 0; font-family: var(--sans); font-size: .8rem; }
.word-comment-text { margin: 2px 0 0; color: var(--ink-2); overflow-wrap: anywhere; }
.word-compose { display: flex; gap: var(--s2); align-items: flex-end; margin: 0 0 var(--s1); }
.word-compose textarea { flex: 1; min-width: 0; resize: none; }

/* ---- The Word — the Bible tab's landing and the full-screen feed ----
   (SCRIPTURE-CONTEXT.md §2.2, §2.4; registered in /styleguide/ → "The Word").
   .why is the one reason a card was chosen; .word-read the readings once read;
   .word-foryou the lead card of what is ready; the feed's cards sit in the
   flow — no fixed layer, nothing a script scrolls (app/CLAUDE.md §3.5). */
.word-title h1 { flex: 1; min-width: 0; }
.word-read { display: flex; align-items: center; gap: var(--s3); min-height: var(--touch);
  text-decoration: none; color: var(--ink); }
.word-read-mark { flex: none; width: 28px; height: 28px; border-radius: 50%;
  display: inline-grid; place-items: center; background: var(--gold); color: var(--on-gold); }
.word-read-mark svg { width: 16px; height: 16px; }
.word-read-text { flex: 1; min-width: 0; font-family: var(--display); font-size: 1.05rem; }
.word-lines { margin: var(--s3) 0 0; }
.word-lines a { color: inherit; }
.word-foryou { margin-top: var(--gap-section); }
.word-foryou .card-head .eyebrow { margin: 0; }
.word-hook { font-family: var(--display); font-style: italic; font-size: 1.22rem;
  line-height: 1.4; color: var(--ink); margin: 0 0 var(--s1); display: -webkit-box;
  -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.word-meta { margin: 0 0 var(--s3); }
.word-meta .gold svg { width: 13px; height: 13px; vertical-align: -1px; }
.word-virtue { color: var(--gold-soft); font-weight: 600; }
/* Home's "For you today": the threads today's cards run along, as list rows. */
.word-home .list-rows { margin: var(--s2) 0 var(--s3); }
.word-lane { text-decoration: none; color: var(--ink); min-height: var(--touch); }
.word-lane-glyph { flex: none; width: 32px; height: 32px; border-radius: 50%;
  display: inline-grid; place-items: center; border: 1px solid var(--line); }
.word-lane-glyph svg { width: 14px; height: 14px; }
.word-deeper { margin: 0 0 var(--s3); }
.word-deeper a { color: var(--gold-soft); text-decoration: none; }
/* "Read more like this ›" — the one quiet line under a Go deeper pill, in the
   underlined-link style of "Watch one sin this week ›"; never a second button. */
.deeper-more { display: block; margin-top: var(--s2); color: var(--muted); }
.word-foryou .word-deeper { margin: var(--s3) 0 0; }
.word-chip { display: inline-block; font-family: var(--sans); font-size: .8rem;
  color: var(--ink-2); background: var(--card-strong); border-radius: var(--r-pill);
  padding: 4px 12px; margin: var(--s3) 0 var(--s1); }
.why { display: block; border: 1px solid var(--line); border-radius: var(--r);
  background: var(--tint-gold-soft); padding: var(--s2) var(--s3); }
.why > summary, div.why { display: flex; gap: var(--s2); align-items: flex-start;
  list-style: none; cursor: pointer; min-height: var(--touch); }
div.why { cursor: default; min-height: 0; }
.why > summary { font-weight: 400; }
.why > summary::-webkit-details-marker { display: none; }
.why-glyph { flex: none; padding-top: 2px; }
.why-glyph svg { width: 18px; height: 18px; }
.why-text { display: flex; flex-direction: column; min-width: 0; }
.why-line { font-family: var(--display); font-size: 1.02rem; color: var(--ink); }
.why-sub { font-family: var(--sans); font-size: .8rem; color: var(--muted); margin-top: 2px; }
.why-all { padding: var(--s2) 0 var(--s1) calc(18px + var(--s2)); }
.why-all ul { margin: var(--s1) 0 var(--s2); padding-left: 1.1em; color: var(--ink-2); }
/* ONE CARD, ONE SCREEN (owner, 25 Sept 2026: "it's supposed to scroll like
   TikTok"). The page snaps card to card; nothing is fixed and no script
   scrolls — the snap is the browser's, on the person's own swipe. A card is at
   least a screen tall and grows only when its reflection is opened. */
html:has(.feed) { scroll-snap-type: y mandatory; }
.feed-card { box-sizing: border-box; min-height: 100svh; display: flex; position: relative;
  flex-direction: column; scroll-snap-align: start; scroll-snap-stop: always;
  padding: calc(var(--safe-top) + var(--bar-pad)) 0 var(--s4); }
.feed-card > * { flex: none; }
/* The row beside the ✕ (the pinned corner, --touch wide): progress, then where
   you are. The Scripture gets the screen; the chrome gets one line. */
.feed-top { display: flex; align-items: center; gap: var(--s3); min-height: var(--touch);
  margin: 0 0 var(--s3) calc(var(--touch) + var(--s3)); }
.feed-count { font-family: var(--sans); font-size: .8rem; font-weight: 400; margin: 0;
  color: var(--muted); white-space: nowrap; }
.feed-segs { flex: 1; display: flex; gap: 4px; min-width: 0; }
.feed-segs i { flex: 1; height: 3px; border-radius: 2px; background: var(--line-soft); }
.feed-segs i.on { background: var(--gold); }
.feed-top .feed-progress { flex: 1; height: 3px; margin: 0; }
.feed-top .feed-change { flex: none; }
/* The reason and the tag share one row. */
.feed-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); flex-wrap: wrap; }
.feed-head > * { min-width: 0; }
/* The reason is one line, not a box: the sparkle and the words. */
.feed-card .why { border: 0; background: none; padding: 0; }
.feed-card .why > summary { min-height: 0; align-items: center; }
.feed-card .why-line { font-family: var(--sans); font-size: .85rem; color: var(--ink-2); }
.feed-card .why-sub { display: none; }
.feed-card .why-glyph { padding-top: 0; }
.feed-card .why-glyph svg { width: 15px; height: 15px; }
.feed-card .word-chip { margin: 0; }
.feed-title { font-size: 1.75rem; line-height: 1.15; margin: var(--s1) 0 0; }
.feed-ref { margin: var(--s1) 0 0; font-family: var(--sans); font-size: .85rem; }
.feed-ref a { color: var(--muted); text-decoration: none; }
.feed-ref a::after { content: " ›"; }
/* THREE BANDS, THE PASSAGE AT THE SCREEN'S CENTRE (owner, 25 Sept 2026: "the
   passage text should be on the center of the screen"). The rows above and
   below the passage are equal fractions of the card, so the passage sits at
   the middle of the screen however tall the lead or the caption is; a lead
   too tall for its half pushes the passage down rather than covering it. The
   rail spans the passage and the caption bands, at the foot. */
.feed-card:has(> .feed-lead) { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: 1fr auto 1fr; column-gap: var(--s3); height: 100svh; }
/* The equal bands need a definite height (with only a floor, a grid makes
   both halves as tall as the taller one); opened, the caption may grow it. */
.feed-card:has(> .feed-lead):has(.feed-refl[open]) { height: auto; }
/* One verse too long to split: the card grows past the screen instead of
   spilling into the next, the passage straight under the lead. */
.feed-card.feed-tall { height: auto; min-height: 100svh; grid-template-rows: auto auto 1fr; }
.feed-lead { grid-column: 1 / -1; grid-row: 1; align-self: start; display: flex;
  flex-direction: column; }
.feed-body { display: contents; }
.feed-card > .feed-body > .feed-text { grid-column: 1; grid-row: 2; padding: var(--s4) 0; }
.feed-card > .feed-body > .feed-rail { grid-column: 2; grid-row: 2 / 4; align-self: end; }
.feed-foot { grid-column: 1; grid-row: 3; align-self: end; padding-top: var(--s4); }
/* The whole passage, its heart lit: the verses that carry the reason in ink,
   the rest of the story in muted (owner, 25 Sept 2026). */
.feed-text { margin: 0; font-size: 1.3rem; line-height: 1.5; color: var(--muted); position: relative;
  touch-action: manipulation; }
.feed-text p { margin: 0; }
.feed-v.lit { color: var(--ink); }
.feed-vn { font-family: var(--sans); font-size: .62em; color: var(--faint);
  margin-right: .25em; }
.feed-v.lit .feed-vn { color: var(--gold-soft); }
/* The passage's pages, swiped sideways inside the card: the strip is its own
   scroller (the page never scrolls sideways), snapping page to page; the
   dots under it say there is more, as a photo carousel's do. */
.feed-pages { display: flex; gap: var(--s4); overflow-x: auto; scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain; scrollbar-width: none; }
.feed-pages::-webkit-scrollbar { display: none; }
/* Each page is exactly the strip's width, so a swipe moves a whole page and
   no line is ever seen cut at the edge; the passage sits in the middle of
   the card's open space, left-aligned (owner, 25 Sept 2026), every page the
   height of the tallest so the dots never jump. */
.feed-page { flex: 0 0 100%; scroll-snap-align: start; min-width: 0; box-sizing: border-box;
  display: flex; flex-direction: column; justify-content: center; }
/* The dots say where you are in the passage: centred under the card, with
   room to breathe, large enough to read at a glance. */
.feed-text .feed-dots { justify-content: center; gap: 10px; margin: var(--s5) 0 0; }
.feed-text .feed-dots i { width: 8px; height: 8px; }
/* The pager: the dots, and on a desktop (a mouse, no swipe) an arrow either
   side to turn the page. On touch the arrows stay hidden; the swipe is the way. */
.feed-pager { display: flex; align-items: center; justify-content: center; gap: var(--s3);
  margin: var(--s5) 0 0; }
.feed-pager .feed-dots { margin: 0; min-width: 0; flex-wrap: wrap; }
.feed-step { display: none; }
@media (hover: hover) and (pointer: fine) {
  .feed-step { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 36px;
    height: 36px; border-radius: 50%; border: 1px solid var(--line); background: var(--card);
    color: var(--ink); cursor: pointer; padding: 0; }
  .feed-step:hover:not(:disabled) { border-color: var(--gold); color: var(--gold); }
  .feed-step:disabled { opacity: .35; cursor: default; }
  .feed-step svg { width: 18px; height: 18px; }
}
.feed-dots { display: flex; gap: 6px; margin: var(--s3) 0 0; }
.feed-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--line); }
.feed-dots i.on { background: var(--gold); }
.feed-rail { display: flex; flex-direction: column; align-items: center; gap: var(--s3);
  align-self: end; }
.feed-rail form, .feed-rail > div { display: flex; flex-direction: column; align-items: center;
  gap: 2px; }
.feed-act { display: inline-grid; place-items: center; width: var(--ctl-icon);
  height: var(--ctl-icon); border-radius: 50%; border: 1px solid var(--line);
  background: var(--card); color: var(--ink); cursor: pointer; padding: 0;
  -webkit-tap-highlight-color: transparent; }
.feed-act svg { width: 20px; height: 20px; }
.feed-act.on { color: var(--gold-soft); border-color: var(--gold); }
.feed-act.on svg { fill: currentColor; }
.feed-act[data-act="like"].on { color: var(--rose); border-color: var(--rose); }
/* Read: the card's own mark — a gold ring, filled gold once read. */
.feed-act[data-act="read"] { color: var(--gold-soft); border-color: var(--gold); }
.feed-act[data-act="read"].on { background: var(--gold); color: var(--on-gold); }
.feed-act[data-act="read"].on svg { fill: none; }
.feed-act-n:empty { display: none; }
/* A double tap's heart: rises and fades over the Scripture. */
.feed-burst { position: absolute; left: 40%; top: 40%; color: var(--rose); pointer-events: none;
  animation: feed-burst .8s ease-out forwards; }
.feed-burst svg { width: 96px; height: 96px; fill: currentColor; }
@keyframes feed-burst {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(.6); }
  25% { opacity: .9; transform: translate(-50%, -50%) scale(1.1); }
  100% { opacity: 0; transform: translate(-50%, -80%) scale(1); } }
.feed-act-n { font-family: var(--sans); font-size: .72rem; color: var(--muted); }
/* The reflection is the card's caption: its first sentence under the lens,
   and "more" opens the rest (a feed caption's grammar). */
.feed-refl > summary { list-style: none; cursor: pointer; min-height: var(--touch);
  font-size: .95rem; line-height: 1.5; color: var(--ink-2); padding: var(--s1) 0; }
.feed-refl > summary::-webkit-details-marker { display: none; }
.feed-refl > summary .eyebrow { display: inline; margin: 0 var(--s1) 0 0; }
.feed-refl-more { font-family: var(--sans); font-size: .85rem; color: var(--muted); }
.feed-refl[open] .feed-refl-more, .feed-refl:not([open]) .feed-refl-less { display: none; }
.feed-refl-less { font-family: var(--sans); font-size: .85rem; color: var(--gold-soft); }
/* Opened, the reflection takes the story's place and the card stays one
   screen: the lit verses stay (what the reflection is about), the rest of
   the story steps aside until "less" (owner, 25 Sept 2026: opening it was
   hard to scroll to and unclear how to close). */
.feed-card:has(.feed-refl[open]) .feed-v:not(.lit),
.feed-card:has(.feed-refl[open]) .feed-page:not(:has(.lit)),
.feed-card:has(.feed-refl[open]) .feed-pager { display: none; }
.feed-reflection { font-size: .95rem; line-height: 1.5; color: var(--ink-2); margin: 0 0 var(--s2); }
.feed-more { text-align: center; margin: var(--s2) 0 0; }
.feed-more svg { width: 14px; height: 14px; vertical-align: -2px; }
.feed-pause, .feed-end { justify-content: center; }
.feed-star { display: inline-grid; place-items: center; width: 48px; height: 48px;
  border-radius: 50%; color: var(--gold-soft);
  background: radial-gradient(circle, var(--tint-gold) 0%, transparent 70%); }
.feed-star svg { width: 26px; height: 26px; }
.feed-pause h2, .feed-end h2, .feed-end-title { font-size: 1.8rem; margin: var(--s3) 0 var(--s2); }
.feed-pause .btn, .feed-end .stack { margin: var(--s4) 0 var(--s3); }
/* Explore — "Go deeper": groups of threads, each tile a card that opens a feed. */
.explore-group { margin: var(--gap-section) 0 0; }
.explore-group > h2.eyebrow { font-size: var(--t-eyebrow); margin: 0 0 var(--s2); }
.explore-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s3); }
.explore-tile { display: flex; flex-direction: column; gap: 4px; margin: 0;
  text-decoration: none; color: var(--ink); min-height: var(--touch); }
.explore-title { font-family: var(--display); font-size: 1.05rem; line-height: 1.25; }

/* ---- day-info — the liturgical day as a visible disclosure ---- */
/* The day's name wears a small info glyph so it READS tappable; open, it tells
   you who the saint was or why the day matters (content/liturgical/celebrations). */
details.day-info > summary { list-style: none; cursor: pointer;
  min-height: var(--touch); display: flex; flex-direction: column; justify-content: center; }
details.day-info > summary::-webkit-details-marker { display: none; }
/* .glue — binds a name to its trailing glyph (the day-info ⓘ). An svg is an
   atomic inline: browsers may break the line right before it even with no
   space (and Chromium ignores a word-joiner there — measured, not assumed).
   It used to be `nowrap`, which held the pair together but made a long feast
   name — "Wednesday of the twenty-first week in Ordinary Time" — one
   unbreakable box wider than the phone, scrolling the whole page sideways.
   A page that scrolls sideways is a worse fault than a glyph that wraps, so
   the name wraps and only the glyph may be orphaned, rarely, on the last
   line. Caught by design-qa, which is exactly what it's for. */
.glue { white-space: normal; }
details.day-info .info-ic { width: 15px; height: 15px; color: var(--gold-soft);
  vertical-align: -2px; margin-left: 5px; opacity: .75; }
details.day-info[open] .info-ic { opacity: 1; }
details.day-info .day-info-body { max-width: 34em; }
details.day-info .day-info-body p { margin: var(--s2) 0 0; }
.center details.day-info .day-info-body, .examen-home details.day-info .day-info-body,
.home-hero details.day-info .day-info-body { margin-left: auto; margin-right: auto; }

/* ---- about page ---- */
.about { max-width: 34em; margin: 0 auto; }
.about h1 { font-size: 2.2rem; margin: 6px 0 2px; }
.about h1 em { font-style: italic; color: var(--gold-soft); }
.about .lede { max-width: 30em; margin: 0 auto var(--s4); }
.about h2 { margin: var(--gap-section) 0 var(--s2); }  /* section rhythm, on the tokens */
.about-row { display: flex; align-items: flex-start; gap: var(--s3); }
.about-row + .about-row { margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--line-soft); }
.about-ic { flex: 0 0 auto; width: 42px; height: 42px; border-radius: 12px; display: grid;
  place-items: center; border: 1px solid var(--line); color: var(--gold-soft);
  background: var(--tint-gold-soft); }
.about-ic svg { width: 22px; height: 22px; }
.about-row h3 { margin: 2px 0 2px; }

/* ---- home dashboard ---- */
/* THE EVENING ON THE FIRST SCREEN (owner, 22 Sept 2026). The hero is the
   family, the greeting and the day on three short lines — the time-of-day
   emblem (64px of medallion) left with the redesign — so the Word and the
   evening's card stand above the fold on a phone. */
.home-hero { text-align: center; margin: 6px 0 14px; }
.home-hero .examen-title { margin: 2px 0 4px; }
/* THIS EVENING — one card for the night's three parts: the prayer (the one
   primary), the examination, the particular examen — each part ruled off
   from the last, the head carrying the days since the sacrament. */
.card.evening .part { padding-top: var(--s3); margin-top: var(--s3); border-top: 1px solid var(--line-soft); }
.card.evening .part:first-of-type { padding-top: 0; margin-top: 0; border-top: 0; }
.card.evening .part h3 { margin: 2px 0 6px; font-size: 1.15rem; }
.card.evening .part > p { margin: 4px 0; }
.card.evening .part .stack, .card.evening .part > .btn { margin-top: var(--s2); }
.card.evening .particular { padding-bottom: 0; margin-bottom: 0; border-bottom: 0; }
/* MOMENTUM, COMPACT (22 Sept 2026): the same chart at two-thirds the height
   (the view draws PLOT_H = 42), the figures smaller, and the area totals on
   ONE line with the door to the week beside them. The chart grammar holds:
   ≤ 24px bars, a 2px gap, a 4px cap, a stub for a quiet day. */
.card.momentum.compact .figure .tile-num.lg { font-size: 1.7rem; }
.card.momentum.compact .momentum-bars .mbar { height: 42px; }
.card.momentum.compact .momentum-bars .momentum-empty { min-height: 42px; }
.card.momentum.compact .momentum-areas.line { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s2) var(--s3); margin-top: var(--s2); padding-top: var(--s2); }
.card.momentum.compact .momentum-areas.line .pill { margin-left: auto; }
.tiles-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); }
.tiles-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); }  /* four meters, 2×2 on the phone */
.tiles-3 .tile-num.lg { font-size: 1.7rem; }

/* ---- momentum card — Home's one glance at the week ----
   Two figures (the streak, this week's total — the Friends number, one tally
   in onefamily.social), seven stacked columns by product area, and the week's
   total by area beneath. Chart grammar: bars ≤ 24px, a 2px surface gap
   between segments, a 4px cap on the top one, a muted 2px stub for a quiet
   day, one recessive baseline, text in ink tokens — never the series colour. */
.card.momentum { margin: var(--s4) 0; }
.momentum-figures { display: flex; justify-content: space-between; align-items: flex-end;
  gap: var(--s3); }
.momentum-figures .figure { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.momentum-figures .figure.end { align-items: flex-end; text-align: right; }
.momentum-figures .tile-num { margin: 0; }
.momentum-figures .serif-caps { letter-spacing: .1em; }
.momentum-bars { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  margin-top: var(--s2); border-bottom: 1px solid var(--line-soft); }
.momentum-bars .mday { display: flex; justify-content: center; align-items: flex-end; }
.momentum-bars .mbar { width: 22px; height: 60px; display: flex; flex-direction: column-reverse;
  gap: 2px; }
.momentum-bars .seg { display: block; width: 100%; flex: 0 0 auto; }
.momentum-bars .seg:last-child { border-radius: 4px 4px 0 0; }   /* column-reverse: the top */
.momentum-bars .seg.quiet { height: 2px; border-radius: 1px; background: var(--line-soft); }
/* The zero state: the seven stubs stay as the week's skeleton and the plot's
   air holds one line saying what fills it. The stubs collapse to their own
   2px so the line owns the plot's height rather than sitting above it. */
.momentum-bars .momentum-empty { grid-column: 1 / -1; min-height: 60px; margin: 0;
  padding: 0 var(--s2) var(--s2); display: flex; align-items: center; justify-content: center;
  text-align: center; line-height: 1.4; }
.momentum-bars.is-empty .mbar { height: auto; }
.seg.examen, .sw.examen { background: var(--chart-examen); }
.seg.emmaus, .sw.emmaus { background: var(--chart-emmaus); }
.seg.bible, .sw.bible { background: var(--chart-bible); }
.seg.homeschool, .sw.homeschool { background: var(--chart-homeschool); }
.momentum-days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); margin-top: var(--s1); }
.momentum-days span { text-align: center; font-family: var(--display); font-size: .6rem;
  letter-spacing: .06em; color: var(--muted); }
.momentum-days span.today { color: var(--parch); }
.momentum-areas { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s2) var(--s4); margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--line-soft); }
.momentum-areas .area { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.momentum-areas .sw { width: 8px; height: 8px; border-radius: 50%; align-self: center; flex: 0 0 auto; }
.momentum-areas .n { font-family: var(--display); font-weight: 600; color: var(--parch);
  font-variant-numeric: tabular-nums; }
.momentum-areas .l { font-family: var(--sans); font-size: .8rem; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* activation checklist — the PLG ladder */
/* accent card — an emphasis card marked by a 3px gold left rule (the ONE accent
   treatment; used by .activation, .tonight, and a chooser row for a prayer
   left open, so every emphasised left edge in the app matches) */
.card.accent, .examen-card.accent, .activation, .tonight {
  border-left: 3px solid var(--gold); }

/* card-head — the standard title row inside a card (title + optional action).
   Center-aligned, no stray heading margin, even gap below. Use this instead of
   ad-hoc .row.between.baseline so every card header spaces identically. */
.card-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); margin-bottom: 12px; }
.card-head > h2, .card-head > div > h2 { margin: 0; font-size: 1.05rem; }

/* invite-code — an invite/redemption code shown with letter-spacing for legibility;
   on an <input> it also centers and uppercases the code as it's typed */
.invite-code { letter-spacing: .15em; }
input.invite-code { text-transform: uppercase; text-align: center; font-size: 1.3rem; }

/* invite card — an open invitation with its delivery tools (copy / share / email) */
.invite-card .invite-actions { margin-top: 10px; padding-top: 10px;
  border-top: 1px solid var(--line-soft); }
/* who the invitation is FOR, on its own line — the first thing an admin looks
   for, and it used to sit buried mid-way through the middot meta run. The mail
   glyph reads as a label; a long address wraps instead of pushing the card
   sideways. .invite-to.none is the link-only state, said plainly. */
.invite-card .invite-to { display: flex; align-items: flex-start;
  gap: var(--gap-controls); margin: var(--s2) 0 0; overflow-wrap: anywhere; }
.invite-card .invite-to svg { width: 16px; height: 16px; flex: 0 0 auto;
  margin-top: 2px; color: var(--gold-soft); }
.invite-card .invite-to.none svg { color: var(--muted); }

/* ---- Bible coverage — the map of how much Scripture a person has read ----
   One idea, three registered pieces. Coverage is a magnitude, so the fill is
   the ONE sequential treatment (.progress-fill's gold) — never a second hue.
   .cov-row     a book as a link: name · gold bar · n/N (tabular digits)
   .cov-grid    two columns of .cov-rows ≥700px, one column on the phone
   .cov-hero    the overall meter card; .cov-of mutes the "/total" in a tile */
.cov-grid { display: grid; grid-template-columns: 1fr; gap: 0 var(--s4); }
@media (min-width: 700px) { .cov-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.cov-row { display: flex; align-items: center; gap: var(--s3); min-height: var(--touch);
  padding: 6px 0; color: var(--ink); text-decoration: none;
  border-bottom: 1px solid var(--line-soft); }
.cov-row .progress { flex: 1; margin: 0; }
.cov-name { flex: 0 0 auto; width: 9.5em; min-width: 0; font-family: var(--display);
  font-size: .92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cov-count { flex: 0 0 auto; min-width: 3.4em; text-align: right;
  font-variant-numeric: tabular-nums; }
.cov-row.done .cov-name { color: var(--gold-soft); }
.cov-hero .progress { margin: 10px 0 8px; }
a.cov-hero { display: block; color: var(--ink); text-decoration: none; }
a.cov-hero:hover { border-color: var(--gold-soft); }
.cov-of { font-size: .55em; color: var(--muted); font-weight: 400; }
.tile .tile-num { overflow: hidden; text-overflow: ellipsis; }
/* a chapter pill that has been read — same pill, worn state, never a new species */
.pill.read { border-color: var(--gold); color: var(--gold-soft); }
/* …and one STARTED — some verses read, not yet all: the same gold edge, dashed,
   the way an unfinished thing is drawn everywhere else */
.pill.part { border-color: var(--gold); border-style: dashed; }

/* badge — a small gold count: "something here waits for you" (menu rows) */
.badge { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 99px;
  background: var(--gold); color: var(--on-gold); font-family: var(--sans);
  font-size: .72rem; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center; }

.activation h2 { margin: 0; }
.activation-count { font-family: var(--display); color: var(--gold-soft); font-size: 1.1rem; }
.progress { height: 8px; border-radius: 99px; background: var(--card-strong);
  overflow: hidden; margin: 12px 0 14px; }
.progress-fill { display: block; height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, var(--gold), var(--gold-soft)); transition: width .4s ease; }
.checklist { list-style: none; margin: 0 0 14px; }
.checklist li { display: flex; align-items: center; gap: 10px; min-height: var(--touch);
  border-bottom: 1px solid var(--line-soft); }
.checklist li:last-child { border-bottom: 0; }
.checklist a { color: var(--ink); text-decoration: none; flex: 1; }
.checklist li.done { color: var(--muted); }
.checklist li.done span:last-child { text-decoration: line-through; }
.check-mark { flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%;
  border: 1px solid var(--line); display: grid; place-items: center; }
.checklist li.done .check-mark { border-color: var(--gold); color: var(--gold-soft); }
.check-mark svg { width: 14px; height: 14px; }
/* go — the ONE "this row leads somewhere" affordance. Always the svg chevron
   (never a typographic ›), so every forward mark in the app is the same shape. */
.go { color: var(--gold-soft); display: inline-flex; align-items: center; flex: 0 0 auto; }
.go svg { width: 16px; height: 16px; }

/* tonight — the primary action card */
.tonight.is-done { border-left-color: var(--accent); }
.tonight h2 { margin: 4px 0 4px; }
.tonight h2 svg { width: 20px; height: 20px; color: var(--accent); vertical-align: -.15em; }

/* today's plan rows */
.plan-row { display: flex; align-items: center; gap: 12px; padding: 12px 0;
  min-height: 48px; text-decoration: none; color: var(--ink);
  border-top: 1px solid var(--line-soft); }
.plan-name { flex: 0 0 auto; min-width: 5em; font-family: var(--display); }
.plan-bar { flex: 1; height: 8px; border-radius: 99px; background: var(--card-strong); overflow: hidden; }
.plan-bar span { display: block; height: 100%; background: var(--accent); border-radius: 99px; }
.plan-count { flex: 0 0 auto; }

/* milestones */
.milestones .milestone { display: flex; align-items: center; gap: 12px; text-decoration: none;
  color: var(--ink); padding: 12px 0; }
.milestones .milestone + .milestone { border-top: 1px solid var(--line-soft); }
.milestones .milestone h3 { margin: 0; font-size: 1.02rem; }
.milestone .go { margin-left: auto; }

/* COMING UP: the milestones card with a head and, under the feast and the
   stages, today's homeschool as one row — the plan's own row shape. */
.milestones .plan-row { border-top: 1px solid var(--line-soft); }
.milestones .plan-name { display: inline-flex; align-items: center; gap: 6px; }
.milestones .plan-name svg { width: 18px; height: 18px; color: var(--gold-soft); }

/* ---- examen home: the candlelit hero ---- */
.couple-name { color: var(--gold-soft); letter-spacing: .24em; margin: 0 0 4px; }
.examen-title { font-size: 2rem; margin: 6px 0 8px; }
.examen-title em { font-style: italic; color: var(--gold-soft); }
.greeting { max-width: 22em; margin: 0 auto; }

/* ---- "Which examen?" chooser (legacy renderExamenChooser) ---- */
.q-top { display: flex; justify-content: flex-end; padding: 4px 0 2px; }
/* The flow's quiet way out. It is a LINK on most screens and a <button> on the
   repair ones, where skipping is a POST and cannot be an anchor — so it must
   own its own background and border. Without that a bare button keeps the
   browser's default chrome and the control shipped as a near-white box with
   muted text inside it, floating over the top of the repair intro and every
   step of the Aftermath. */
.q-cancel { font-family: var(--sans); font-size: .82rem; color: var(--muted);
  text-decoration: none; padding: 6px 4px; min-height: var(--ctl); display: inline-flex;
  /* Skipping is a POST, so this is a <button> on the repair flow and an <a>
     everywhere else. A bare button keeps the UA's buttonface and border — it
     shipped as a near-white box — so the component owns both. */
  align-items: center; background: none; border: 0; cursor: pointer; }
.q-cancel:hover { color: var(--ink); }
.chooser { max-width: 34em; margin: 6px auto 0; }
.chooser h1 { font-size: 2.1rem; margin: 2px 0 6px; }
.chooser h1 em { font-style: italic; color: var(--gold-soft); }
.chooser .lede { max-width: 26em; margin: 0 auto; }
.examen-list { display: grid; gap: 12px; margin-top: 22px; }
.examen-card { display: flex; align-items: center; gap: var(--s3); text-align: left;
  width: 100%; border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 16px; background: var(--card); box-shadow: var(--shadow-1);
  text-decoration: none; transition: border-color .2s, background .2s, transform .12s ease; }
.examen-card:hover { border-color: var(--gold-soft); background: var(--tint-gold); }
.examen-card:active { transform: scale(.99); }
.examen-ic { flex: 0 0 auto; width: 42px; height: 42px; border-radius: 12px;
  display: grid; place-items: center; border: 1px solid var(--line);
  color: var(--gold-soft); background: var(--tint-gold-soft); }
.examen-ic svg { width: 22px; height: 22px; }
.examen-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.examen-nm { font-family: var(--display); font-weight: 500; font-size: 1.12rem; color: var(--parch); }
.examen-tl { color: var(--muted); font-size: .84rem; line-height: 1.35; }
.examen-go { flex: 0 0 auto; color: var(--gold-soft); font-size: 1.4rem; opacity: .7; }
/* The row is usually one link. Where it carries a second control — the ✕ on an
   open prayer — the card becomes the container and the tappable region an
   anchor inside it, because an anchor may not contain another one. */
.examen-card > a { text-decoration: none; color: inherit; }
/* …and the control must not shrink. NO min-width:0 above: that selector also
   matches the .menu-btn, which IS an anchor, and zeroing its min-width left
   the ✕ 22px wide against a 44px floor. The tappable region carries .min0
   itself. */
.examen-card > .menu-btn { flex: 0 0 auto; }
/* A card that carries its own deeper overview STACKS: the tappable prayer on
   top, the tagline beneath it as the summary of a day-info ⓘ. The tagline is
   the summary rather than a lone icon because that is how this app has always
   written a disclosure — the feast name on Home, the notice in the tray, text
   with the ⓘ glued to its last word — and a bare ⓘ under a line of muted text
   reads as a stray mark. The indent lines the tagline up under the name, where
   it sat when the whole row was one anchor. */
.examen-card:has(> .examen-info) { flex-direction: column; align-items: stretch;
  gap: 0; }
.examen-info { margin-top: 3px; padding-left: calc(42px + var(--s3)); }
.examen-info summary { cursor: pointer; }
/* The disclosure's body clears the indent — at 380px a 58px gutter on a
   paragraph of four sentences is a column of two words. */
.examen-info .day-info-body { margin-left: calc(-42px - var(--s3)); padding-top: 2px; }
.examen-tag { align-self: flex-start; font-family: var(--display); font-size: .6rem;
  letter-spacing: .14em; text-transform: uppercase; padding: .18em .6em;
  border-radius: var(--r-pill); border: 1px solid var(--line); color: var(--muted); }
.examen-tag.personal { color: var(--marian); border-color: rgba(106, 134, 201, .5);
  background: var(--tint-cool); }
.examen-tag.couple { color: var(--gold-soft); border-color: rgba(199, 154, 68, .5);
  background: var(--tint-gold); }
/* A prayer left half-finished is the most actionable thing on this screen, so
   its tag wears the app's FILLED gold — the same treatment .btn.primary and
   the streak dots use — rather than the outline every other tag has.
   NOT named .progress: that is the app's progress BAR (height: 8px), and the
   first version of this tag inherited it and shipped as a crushed 8px sliver
   with its own text cut in half. */
.examen-tag.open { color: var(--on-gold); border-color: transparent;
  background: linear-gradient(180deg, var(--gold-soft), var(--gold)); }

/* ---- pull-to-refresh — a candlelit loading state (our own, for standalone/PWA) ---- */
/* DISPLAY:NONE UNTIL IT IS WANTED. This is a full-viewport fixed overlay
   carrying a backdrop-filter, and it was in the DOM and composited on every
   touch device on every screen, all the time, to be invisible. A layer that
   costs that much to show nothing is a layer the scroll thread should not be
   carrying. `.armed` arrives with the pull itself. */
#ptr { position: fixed; inset: 0; z-index: var(--z-overlay); display: none; align-items: center;
  justify-content: center; pointer-events: none; }
#ptr.armed, #ptr.load { display: flex; }
#ptr .ptr-scrim { position: absolute; inset: 0; background: var(--scrim);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); opacity: 0;
  transition: opacity .3s ease; }
#ptr .ptr-core { position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 14px; opacity: 0; transform: translateY(-10px) scale(.6); }
/* will-change is a standing request for a layer, so it is spent only while
   there is something to animate. */
#ptr.armed .ptr-core, #ptr.load .ptr-core { will-change: opacity, transform; }
#ptr .ptr-orb { position: relative; width: 60px; height: 60px; display: flex;
  align-items: center; justify-content: center; }
#ptr .ptr-ring { position: absolute; inset: 0; width: 60px; height: 60px; color: var(--gold);
  opacity: 0; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round;
  stroke-dasharray: 36 120; transition: opacity .25s ease;
  filter: drop-shadow(0 0 6px rgba(199, 154, 68, .4)); }
#ptr .ptr-flame { width: 26px; height: 26px; color: var(--gold-soft);
  filter: drop-shadow(0 0 9px rgba(199, 154, 68, .6)); animation: ptrflicker 2.4s ease-in-out infinite; }
#ptr .ptr-label { font-family: var(--display); font-size: .72rem; letter-spacing: .26em;
  text-transform: uppercase; color: var(--gold-soft); opacity: 0; transition: opacity .3s ease; }
#ptr.load .ptr-scrim { opacity: 1; }
#ptr.load .ptr-core { opacity: 1; transform: none; transition: opacity .25s ease, transform .25s ease; }
#ptr.load .ptr-ring { opacity: 1; animation: ptrspin 1.05s linear infinite; }
#ptr.load .ptr-label { opacity: .92; }
@keyframes ptrspin { to { transform: rotate(360deg); } }
@keyframes ptrflicker { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .82; transform: scale(.94); } }

/* ---- spinner (async insight loading) ---- */
.spinner { width: 16px; height: 16px; flex: 0 0 auto; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--gold-soft);
  animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---- "Which evening?" date picker ---- */
.datepick { margin-top: 22px; display: flex; flex-direction: column; gap: 10px; }
/* Each choice names the evening it actually means. "Tonight" and "Last night"
   are ambiguous in exactly the window this app invented for itself: before 3am
   the examen day is still yesterday's, so at 1:23am on a Thursday "Tonight" IS
   Wednesday — while the phone says Thursday and the person reads a day later.
   That files a reflection on the wrong night, and nothing here contradicted it.
   The date is the second line of the button rather than a caption beside it,
   so it cannot be read as belonging to the other choice. */
/* The two-line offers only (Tonight / Last night). "Use this day" shares the
   name and inherited the column: its chevron dropped to a second line. */
.datepick .btn.block[name="when"] { display: flex; flex-direction: column; gap: 2px;
  padding-top: 11px; padding-bottom: 11px; line-height: 1.25; }
.datepick .btn.block[name="when"] .when { font-family: var(--sans); font-size: .76rem;
  font-weight: 400; letter-spacing: 0; opacity: .72; }
.datepick-other { margin-top: 8px; padding-top: 14px; border-top: 1px solid var(--line-soft); }
.datepick-other label { font-family: var(--display); font-size: .68rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--muted); }
.datepick-other .row { margin-top: 6px; }
.datepick-other input[type=date] { flex: 1; }
.datepick-other .btn { white-space: nowrap; }

/* couple-status card: Tonight / Last night with C — G marks */
.status-card { padding: 4px 16px; }
.status-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; }
.status-row + .status-row { border-top: 1px solid var(--line-soft); }
.status-label { flex: 0 0 auto; width: 84px; color: var(--muted); }
.status-marks { display: flex; align-items: center; }
.status-marks .mark { width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid var(--line); display: flex; align-items: center; justify-content: center;
  font-family: var(--display); font-size: .85rem; color: var(--muted); }
.status-marks .mark.on { border-color: var(--gold);
  background: linear-gradient(180deg, var(--gold-soft), var(--gold)); color: var(--on-gold); }
.status-marks .mark-link { width: 16px; height: 1px; background: var(--line); }
.status-text { flex: 1; text-align: right; font-size: .95rem; }

/* ---- week grid ---- */
/* ---- print packet (print_week) — day rows, checkbox squares, page discipline.
   Screen shows a faithful preview; @media print does the paper-specific work. */
.print-section-title { margin-top: var(--s4); }
.print-day { display: flex; gap: var(--s4); align-items: flex-start;
  border-top: 1px solid var(--line-soft); padding: 10px 0; }
.print-day.rest { opacity: .55; }
.print-day-label { flex: 0 0 58px; text-align: center; }
.print-day-label .d-name { display: block; font-family: var(--display);
  font-size: var(--t-eyebrow); font-weight: 600; letter-spacing: .2em;
  text-transform: uppercase; color: var(--gold); }
.print-day-label .d-date { display: block; font-family: var(--display);
  font-size: 1.5rem; font-weight: 500; line-height: 1.1; color: var(--parch); }
.print-day-blocks { list-style: none; margin: 0; padding: 0; flex: 1; }
.print-day-blocks li { padding: 3px 0; }
.print-day-blocks .rest-note { font-style: italic; }
.pbox { display: inline-block; width: .85em; height: .85em; margin-right: 8px;
  border: 1.5px solid var(--muted); border-radius: 3px; vertical-align: -.08em; }
.print-cols li { list-style: none; padding: 2px 0; }
.print-cols { padding-left: 0; }

.week-day { margin: var(--gap-card) 0; }
.week-day h3 { border-bottom: 1px solid var(--line-soft); padding-bottom: 4px; }
.week-day .mini {
  display: block; padding: 7px 0;
  text-decoration: none;
}
.week-day .mini .domain { display: block; margin-bottom: 1px; }

/* ---- verse / quote ---- */
.verse { font-style: italic; margin: var(--s5) 0 var(--s1); }
.verse.framed { border-left: 3px solid var(--gold); padding-left: var(--s3);
  color: var(--muted); margin: 0; }

/* verse-actions — the consistent affordance under every surfaced verse: ONE
   pill per citation, the door to the chapter, worn .pill.read once every verse
   it names is read. The mark lives on the chapter the door opens. */
.verse-actions { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s2); margin: var(--s2) 0 0; }
.verse-actions form { display: contents; }
.verse-actions .pill { font-style: normal; }
.closing .verse-actions { justify-content: center; }

/* ---- Emmaus chat ---- */
/* Two voices, one conversation: the person's bubble sits right on the strong
   card surface; Emmaus answers left, wearing the accent treatment (the one
   gold left-rule) — the app's own voice speaking. */
.chat { display: flex; flex-direction: column; gap: var(--s3); margin: var(--s4) 0; }
.chat-msg { max-width: 88%; padding: 10px 14px; border-radius: 14px;
  overflow-wrap: break-word; }
.chat-msg p { margin: 0 0 8px; }
.chat-msg p:last-child { margin-bottom: 0; }
/* TWO VOICES, TOLD APART BY STRUCTURE — NOT BY TWO SHADES OF THE SAME NOTHING.
   Both bubbles used to be washes over the ground: 1.16:1 and 1.28:1, which is
   below the threshold where an edge is seen at all. The whole screen — head,
   cards, bubbles, composer — was one value, and the conversation had no shape.
   Raising both alphas would only have made two slightly louder rumours.
   So the contrast is structural instead. What you said is a discrete object,
   filled and bounded. What Emmaus answered is open text under the gold rule,
   taking the page's own column: it is the long thing to read, and long prose
   reads better unboxed. The rule alone carries the voice — it always did. */
.chat-msg.user { align-self: flex-end; background: var(--card-raised);
  border: 1px solid var(--line); border-bottom-right-radius: 4px; }
.chat-msg.emmaus { align-self: flex-start; max-width: 100%; background: none;
  border-left: 3px solid var(--gold); border-radius: 0; }
.chat-msg .verse-actions { margin-top: var(--s2); }
/* UNDER EVERY ANSWER — copy · helpful · not helpful, as the bar's own glyph
   buttons: chrome, quiet, a row that reads as "things you can do with this"
   and never as part of the answer. A pressed verdict wears the gold tint the
   active states already use; the copied state is the same tint for a moment. */
.answer-actions { display: flex; align-items: center; gap: var(--s1); margin-top: var(--s2); }
.answer-actions form { display: contents; }
.answer-actions .menu-btn svg { width: 18px; height: 18px; }
.answer-actions .menu-btn[aria-pressed="true"], .answer-actions .menu-btn.done {
  color: var(--gold-soft); background: var(--tint-gold); }
.chat-form { display: flex; gap: var(--s2); align-items: stretch; }
.chat-form textarea { flex: 1; min-width: 0; resize: none; }
.chat-form .btn { align-self: flex-end; }

/* THE ANSWER ARRIVING (EMMAUS-UX.md). Three states of Emmaus's own bubble
   before it is an answer, all wearing the same gold rule so the eye already
   knows whose turn it is:
   - .thinking  — a status line naming the REAL step the server is on
                  (listening, remembering the family, opening the Scriptures,
                  walking, checking citations), never an invented progress bar;
   - .streaming — words as they come, with a caret at the living edge;
   - .failed    — the one calm notice and its one control, Try again.
   The person's own bubble appears the instant they send; this is the second
   thing that arrives. */
.chat-status { display: flex; align-items: center; gap: var(--s2); margin: 0;
  color: var(--muted); font-family: var(--sans); font-size: .9rem;
  min-height: 24px; }
.chat-status .spinner { width: 14px; height: 14px; }
.chat-msg.streaming p:last-child::after { content: ""; display: inline-block;
  width: 2px; height: .95em; margin-left: 3px; vertical-align: -.12em;
  background: var(--gold-soft); animation: chat-caret 1s steps(2, start) infinite; }
@keyframes chat-caret { 50% { opacity: 0; } }
/* The notice carries its own rule, so the bubble's gold one steps aside:
   two accent rules side by side would be one idea drawn twice. */
.chat-msg.failed { border-left: 0; padding-left: 0; }
.chat-msg.failed .notice { margin: 0; }
.chat-msg.failed .notice p { margin: 0 0 var(--s2); }
.chat-msg.failed .notice .btn { margin-top: 0; }
/* Sent, and not yet answered: the composer waits with the person. */
.chat-form textarea[readonly] { color: var(--muted); }

/* A BUSY BUTTON — the tap answered back. One spinner beside a verb
   ("Sending…", "Walking…"), the button held disabled so a second tap cannot
   ask twice. `aria-busy` is the state, so assistive tech hears it too. */
.btn[aria-busy="true"] { display: inline-flex; align-items: center;
  justify-content: center; gap: var(--s2); cursor: progress; }
.btn[aria-busy="true"]:disabled { opacity: .85; }
.btn .spinner { width: 14px; height: 14px; }
/* On the filled gold button the spinner's gold arc would vanish into it:
   it turns in the button's own ink instead. */
.btn.primary .spinner { border-color: rgba(36, 26, 8, .22); border-top-color: var(--on-gold); }

/* ---- .list-row — a thing, said in one line, with a control at the end -------
   The shape a list of records takes everywhere in this app: a name, a quieter
   line under it, and one control at the right. It was written twice — once as
   .convo for the conversation tray, once as .source-row for the canon registry
   — two components doing one job, which is the thing "one treatment per idea"
   exists to prevent. This is that one job; each screen adds only its own
   interaction on top.

   Separators belong to the LIST, not the row: a tray of conversations reads as
   discrete cards with air between them, a registry of 135 sources reads as a
   ruled ledger, and the row itself should not care which. */
.list-rows { display: flex; flex-direction: column; }
.list-rows.ruled > * { border-top: 1px solid var(--line-soft); padding: var(--s3) 0; }
.list-rows.ruled > *:last-child { border-bottom: 1px solid var(--line-soft); }
.list-row { display: flex; align-items: center; gap: var(--s3); }
.list-row-main { flex: 1; min-width: 0; }
.list-row-t { display: block; margin: 0; font-family: var(--sans);
  font-size: .87rem; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The second line wraps where the title truncates: a title is an identifier and
   must stay one line, but its details are worth reading in full. */
.list-row-m { display: flex; align-items: flex-start; gap: 6px; margin: 3px 0 0;
  font-family: var(--sans); font-size: .74rem; color: var(--muted); }
/* Switched off, retired, spent — stated by the row, not left to the control. */
.list-row.is-quiet .list-row-t { color: var(--muted); }
.list-row.is-quiet .list-row-m { color: var(--faint); }

/* ---- examination rows (confession/) ----
   The registered .list-row with ONE variant, .sin: the whole line is the
   control that marks (a form whose button fills the row, exactly as the
   conversation tray's .convo-open does), and the trailing slot holds the
   count — a .pill.active that takes one back — or nothing, the same width
   either way so the text edge never moves. A sin is a sentence, so the title
   WRAPS here where a conversation's truncates; the gravity word sits under
   it in the row's own meta line, gold for grave matter, muted for "can be".
   Registered at /styleguide/ → "Examination rows". */
.sin-list { margin-top: var(--s2); }
.sin-open { display: contents; }
.sin-open button { flex: 1; min-width: 0; text-align: left; background: none;
  border: 0; color: inherit; cursor: pointer; padding: 10px 6px 10px 2px;
  min-height: var(--touch); font: inherit; border-radius: var(--r-sm); }
.sin-open button:focus-visible { outline: 2px solid var(--gold-soft); outline-offset: 2px; }
.sin .list-row-t, .list-row-t.wrap { white-space: normal; overflow: visible;
  text-overflow: clip; font-family: var(--serif); font-size: 1rem; color: var(--ink); }
.sin .list-row-m { color: var(--gold-soft); }
.sin .list-row-m.weighs { color: var(--muted); }
.sin-slot { flex: 0 0 auto; width: 56px; display: flex; justify-content: flex-end; }
.sin-slot form { display: contents; }
/* THE STEPPER (owner, 23 Sept 2026): − count + in the row's trailing slot,
   minus left, plus right (NN/g's input steppers). QUIET, by the owner's word
   ("massive … less in your face"): at nought a bare + glyph; once marked, one
   hairline capsule holding − n +. Glyphs drawn 14px in a 30px cell, each
   cell's tap area 44px by an invisible halo. The slot is one width on every
   row, so the text edge never moves. */
.sin-slot.stepper { width: 88px; align-items: center; }
.sin-slot.stepper .step { position: relative; width: 30px; height: 30px; padding: 0; border: 0;
  background: none; color: var(--gold-soft); display: inline-grid; place-items: center;
  border-radius: 99px; cursor: pointer; }
.sin-slot.stepper .step::before { content: ""; position: absolute; inset: -7px; }
.sin-slot.stepper .step svg { width: 14px; height: 14px; }
.sin-slot.stepper .step:focus-visible { outline: 2px solid var(--gold-soft); outline-offset: 1px; }
.sin:not(.is-marked) .sin-slot.stepper .step { color: var(--muted); }
.sin.is-marked .sin-slot.stepper { border: 1px solid var(--line); border-radius: 99px;
  width: auto; min-width: 88px; justify-content: space-between; }
.sin-slot.stepper .sin-n { min-width: 16px; text-align: center; font-family: var(--display);
  font-weight: 600; font-size: .95rem; color: var(--parch); font-variant-numeric: tabular-nums; }
.sin .list-row-main { padding: 10px 6px 10px 2px; }
.sin.is-marked { background: var(--tint-gold); border-radius: var(--r-sm); }
.sin.is-marked .list-row-t { color: var(--parch); }
.tiles a.tile { text-decoration: none; color: inherit; }
/* A part of the examination, folded: the registered disclosure carrying its
   count as the same .pill.active the rows wear — one mark, one meaning. The
   pill sits before the chevron, which keeps its place at the end. */
details.sin-part > summary > .sin-count { margin-left: var(--s2); }
details.sin-part > summary > h2 { flex: 1; min-width: 0; }
details.sin-part .section-body { padding-bottom: var(--s3); }
/* THE STRIP AND THE FOLD (owner, 22 Sept 2026: "the search box should be
   frozen at the top and never disappear"). .sin-nav — the search and the
   parts' chips — is STICKY under the top bar's row, on the bar's own ground,
   bled to the page edges so nothing scrolls past beside it; the open part's
   summary is sticky under the strip, so the person always knows where they
   are and the tap that folds it is under the thumb; and an opened part lands
   under the strip (scroll-margin-top). The strip's height is measured by the
   page's script into --sin-nav-h; 112px is the no-script guess. Sticky,
   never fixed: nothing here joins the fixed layers (app/CLAUDE.md §3). The
   chips scroll sideways on one line — five parts would be three lines of
   chips on a phone — with 8px above and below so a pill's tap halo is not
   clipped by the scroller. */
.sin-nav { position: sticky; top: var(--header-h); z-index: 2; background: var(--bar-bg);
  margin: 0 calc(-1 * (var(--gutter) + var(--safe-right))) var(--s2) calc(-1 * (var(--gutter) + var(--safe-left)));
  padding: var(--s2) calc(var(--gutter) + var(--safe-right)) 0 calc(var(--gutter) + var(--safe-left));
  border-bottom: 1px solid var(--line-soft); }
.sin-nav.static { position: static; margin: 0; padding: 0; border: 0; }   /* the styleguide, at rest */
.sin-parts { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin: 0; padding: 8px 0; }
.sin-parts::-webkit-scrollbar { display: none; }
.sin-parts .pill { flex: 0 0 auto; white-space: nowrap; }
.sin-parts .sin-count { color: var(--gold-soft); }
/* Only where the strip is (its siblings): the particular examen's chooser
   borrows the parts without it, and a heading stuck under a strip that is
   not there parked mid-page (owner, 22 Sept 2026, with the screenshot). */
.sin-nav ~ details.sin-part { scroll-margin-top: calc(var(--header-h) + var(--sin-nav-h, 112px)); }
/* THE PARTICULAR EXAMEN'S DOOR on the examination (a card, not a pill —
   owner, 22 Sept 2026): the head holds the eyebrow, the sin or the
   invitation, and the chevron every navigational row carries. */
.card.particular-door { display: block; text-decoration: none; color: inherit; }
.card.particular-door .card-head { align-items: flex-start; }
.card.particular-door .card-head > div { min-width: 0; }
.card.particular-door .card-head .go { flex: 0 0 auto; margin-top: 4px; }
.card.particular-door h2 { font-size: 1.15rem; }
/* THE PARTICULAR EXAMEN on Home's examination card: one sin watched this
   week, its day-of-seven eyebrow, and today's two answers in a row. It sits
   above the card's usual line and rules itself off from it. */
.card.conscience .particular, .card.evening .particular { padding-bottom: var(--s3); margin-bottom: var(--s3);
  border-bottom: 1px solid var(--line-soft); }
.card.conscience .particular > p, .card.evening .particular > p { margin: 4px 0; }
.card.conscience .particular .row, .card.evening .particular .row { margin-top: var(--s2); }
.sin-nav ~ details.sin-part[open] > summary { position: sticky; top: calc(var(--header-h) + var(--sin-nav-h, 112px));
  z-index: 1; background: var(--bar-bg); }

/* ---- conversation rows (the tray, and its page) ----
   A .list-row that can be opened. THE RULE: at most two controls in a row at
   any moment, and delete never shares a row with a non-destructive action.
   Rename is an edit; delete is destruction; making them peers invites a misfire
   on the one action that cannot be undone. Four states, each one row, nothing
   wrapping at 314px. */
.convo-list { gap: 2px; }
.convo { gap: var(--s2); border-radius: var(--r-sm);
  border: 1px solid transparent; padding: 2px; }
.convo.current { background: var(--tint-gold); border-color: var(--line); }
.convo-open { display: contents; }
/* The pinned row (the examination of conscience): a LINK that fills the row
   the way .convo-open's button does — same padding, same height, same two
   lines — because it opens a road rather than posting an id. */
.convo-pin { flex: 1; min-width: 0; display: flex; flex-direction: column;
  justify-content: center; text-align: left; padding: 10px; min-height: var(--touch);
  text-decoration: none; color: inherit; }
.convo-pin .list-row-t svg { width: 16px; height: 16px; vertical-align: -3px; }
.convo-open button { flex: 1; min-width: 0; text-align: left; background: none;
  border: 0; color: inherit; cursor: pointer; padding: 10px;
  min-height: var(--touch); }
.ico-btn { flex: 0 0 auto; width: var(--ctl-icon); height: var(--ctl-icon);
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm); border: 1px solid var(--line-soft);
  background: var(--card); color: var(--muted); cursor: pointer; }
.ico-btn svg { width: 18px; height: 18px; }
.ico-btn.danger { color: var(--danger); border-color: rgba(231, 161, 161, .35); }
.ico-btn.gold { background: var(--gold); border-color: var(--gold);
  color: var(--on-gold); }
/* THE EMMAUS PAGE'S CORNER — history and New, as two of the bar's own icon
   buttons. Chrome, not content: no fill, no border, nothing gold (gold is for
   the thing you press to DO something). The form around New contributes no
   box of its own. */
.chat-switch { display: flex; align-items: center; gap: var(--s1); }
.chat-switch form { display: contents; }

/* No new species here: the page title's explanation control IS .menu-btn, the
   same borderless icon button the top bar uses, placed at the end of the name's
   row. A page that needs "what is this" gets the bar's own affordance rather
   than a second kind of icon button. */
.page-title .menu-btn { flex: 0 0 auto; }
.page-title .menu-btn svg { width: 20px; height: 20px; }

/* A row's overflow, opened INLINE — the conversation tray's own shape, where
   the row is replaced by its actions rather than covered by a popover, because
   the tray is a list of destinations and the menu names the one you are in.
   A row that keeps its content while acting uses details.row-menu instead (see
   "THE HALO" and friends.html); this is not a second row-menu, it is the tray.
   .convo-acts is composition, not a component: .menu-list supplies the rows,
   this gives it the row's full width and its own title line. */
.convo-acts { width: 100%; }
.convo-acts .menu-list { margin: 0; }
.convo-acts .menu-list .danger, .convo-acts .menu-list .danger svg { color: var(--danger); }
.convo-acts-t { margin: 0 0 var(--s2); padding: 0 4px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }

/* THE INLINE EDITOR — the one thing the tray and the examen history genuinely
   share, so it is named for the ROW rather than for a conversation. The row
   becomes its own single-field form instead of sending anyone to a screen:
   a title in the tray, a date in history. Drawn field, no halo — an input is
   not a control in the halo's sense and keeps the form-field height. */
.row-edit { display: flex; align-items: center; gap: var(--s2); width: 100%; }
.row-edit input { flex: 1; min-width: 0; }

/* The confirm gives the title its own line, so the two buttons get the full
   width and nothing wraps — and it names what is about to be destroyed, which
   "Are you sure?" never does. */
.convo-confirm { width: 100%; background: var(--tint-danger);
  border: 1px solid rgba(231, 161, 161, .4); border-radius: var(--r-sm);
  padding: var(--s3); }
.convo-confirm p { margin: 0 0 var(--s3); }

/* ---- sources — what an answer actually stood on ----
   Built on the app's existing disclosure idiom (`details.peek` /
   `details.section`) so it inherits the caret, the 48px summary target and the
   reduced-motion behaviour. Closed by default: the answer is the screen, the
   receipt sits under it.
   The tier badge is the point of the panel — gold where the Church speaks,
   marian where a person does, quiet where it is research. A row appears ONLY
   if its citation validated, so no panel is an honest answer. */
.sources { margin-top: var(--s3); border-top: 1px solid var(--line-soft); }
.sources > summary { list-style: none; cursor: pointer; display: flex;
  align-items: center; gap: var(--s2); min-height: var(--touch);
  font-family: var(--sans); font-size: .79rem; color: var(--muted); }
.sources > summary::-webkit-details-marker { display: none; }
.sources > summary::after { content: ""; margin-left: auto; flex: 0 0 auto;
  width: 7px; height: 7px; border-right: 1.5px solid var(--faint);
  border-bottom: 1.5px solid var(--faint); transform: rotate(45deg);
  transition: transform var(--dur) var(--ease); }
.sources[open] > summary { color: var(--gold-soft); }
.sources[open] > summary::after { transform: rotate(-135deg); }
.source-list { display: flex; flex-direction: column; gap: var(--s3);
  padding: var(--s1) 0 var(--s3); }
.source { display: flex; gap: var(--s3); align-items: flex-start; }
.source-tier { flex: 0 0 auto; margin-top: 3px; width: 22px; height: 22px;
  border-radius: 6px; display: inline-flex; align-items: center;
  justify-content: center; font-family: var(--sans); font-size: .6rem;
  font-weight: 700; background: var(--card); border: 1px solid var(--line-soft);
  color: var(--faint); }
.source-tier.magisterium, .source-tier.ecclesial {
  background: var(--tint-gold); border-color: var(--line); color: var(--gold-soft); }
.source-tier.saints, .source-tier.devotional {
  background: var(--tint-cool); border-color: rgba(106, 134, 201, .35);
  color: var(--marian); }
.source-main { min-width: 0; flex: 1; }
.source-locus { font-family: var(--sans); font-size: .85rem; color: var(--ink);
  display: block; }
a.source-locus { color: var(--gold-soft); }
.source-from { font-family: var(--sans); font-size: .72rem; color: var(--faint);
  display: block; margin-top: 1px; }

/* ---- ask-card — Emmaus, answerable from Home ----
   The input itself is on the page, not a link to a page with an input: the
   question is the call to action. Starters are real buttons that submit a
   prepared question, so a first conversation costs one tap. */
.ask-card .ask-form { display: flex; gap: var(--s2); align-items: stretch;
  margin-top: var(--s2); }
.ask-card .ask-form input[type="text"] { flex: 1; min-width: 0; }
.ask-starters { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s3); }
.chat-intro .ask-starters { margin: var(--gap-card) 0 0; }
/* CHOOSING IS NOT ASKING. A starter FILLS the box and marks itself chosen;
   Ask is the one thing that asks. Each is an anchor whose href pre-fills the
   composer on the road (`/companion/?q=`), so the same chip works with no
   script at all — it just never submits anything by itself. */
.ask-starters a, .ask-starters button { display: inline-flex; align-items: center;
  min-height: var(--ctl); padding: 6px 12px;
  border: 1px solid var(--line-soft); border-radius: var(--r-pill);
  background: var(--card); color: var(--ink); cursor: pointer; text-decoration: none;
  font-family: var(--sans); font-size: .86rem; text-align: left;
  -webkit-tap-highlight-color: transparent; }
.ask-starters a:hover, .ask-starters button:hover { border-color: var(--gold);
  color: var(--gold-soft); }
.ask-starters a:focus-visible, .ask-starters button:focus-visible {
  outline: 2px solid var(--gold-soft); outline-offset: 2px; }
/* The chosen one: the gold border and ink of `.pill.read`, plus the gold
   tint so "chosen" reads stronger than "hovered" — one selected treatment
   across both control species. */
.ask-starters [aria-pressed="true"], .pill[aria-pressed="true"] {
  border-color: var(--gold); color: var(--gold-soft); background: var(--tint-gold); }
/* GO DEEPER (docs/architecture/GO-DEEPER.md): the last chip is the person's own
   question — a beginning to finish, or the box cleared. Dashed, like .pill.part,
   because it is an opening rather than a whole question; chosen it lights the
   same gold as the others. */
.ask-starters a.free { border-style: dashed; color: var(--muted); }
/* "ASK ABOUT …" — one word at a time, turning over; a tap on the word is a
   tap on that starter. A change, not a motion: the swap is a short fade and
   none at all under reduced-motion. Without the script the first word stands. */
.ask-words { display: inline-grid; }
.ask-word { grid-area: 1 / 1; background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: var(--gold-soft); font-weight: 600; text-align: left; cursor: pointer;
  opacity: 0; pointer-events: none; transition: opacity .35s ease; }
.ask-word.on { opacity: 1; pointer-events: auto; }
a.ask-word { text-decoration: none; }   /* a link since 22 Sept 2026: the road is its no-script target */
.ask-word:focus-visible { outline: 2px solid var(--gold-soft); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .ask-word { transition: none; } }
/* WHERE NEXT — under a finished answer. The chips are forms (they leave the
   thread); the forms contribute no box of their own. */
.followups { margin-top: var(--s3); }
.followups .eyebrow { display: block; margin-bottom: var(--s1); }
.followups form { display: contents; }
.followups .ask-starters { margin-top: 0; }

/* THE FIRST-RUN INTRODUCTION (intro.html) — three screens, one promise each,
   the component itself in a frame with one line under it. No new species:
   the frame holds the app's own cards and bubbles, quiet and untouchable
   (aria-hidden, no pointer), and what the tour IS is said in the registered
   notice. The dots and the frame's caption are the only new marks. */
.intro { max-width: 560px; margin: 0 auto; padding: var(--s3) 0 var(--s4);
  display: flex; flex-direction: column; gap: var(--s3); }
.intro h1 { font-family: var(--display); font-size: 2rem; line-height: 1.1; margin: 0;
  text-wrap: balance; }
.intro-frame { border: 1px solid var(--line); border-radius: 18px; padding: var(--s3);
  background: var(--card-sunk, transparent); pointer-events: none; }
.intro-frame > .card { margin: 0; }
.intro-frame.chat { display: flex; flex-direction: column; gap: var(--s2); }
.intro-frame.chat .answer-actions { display: none; }
.intro-day { font-family: var(--display); color: var(--gold-soft); margin: 0 0 var(--s2); }
/* WHAT THIS IS, AT THE TOP OF EVERY SCREEN — the registered notice, carrying
   the product's name and the fact that this is a tour. The column owns the
   gap between blocks here, so the notice's own card margin comes off (one
   owner per gap, as the notice's grid already learned). */
.intro > .notice { margin: 0; }
/* THE CAPTION ON A FRAME — "Example", tight under the box it belongs to
   rather than floating between two blocks, so a demo conversation is never
   read as somebody's real one (owner, 15 Sept 2026). The column's gap is
   neutralized for this one pair; the caption keeps --s1 of its own. */
.intro-example { margin: 0; text-align: right; font-family: var(--display);
  font-size: .68rem; font-weight: 500; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted); }
.intro-frame + .intro-example { margin-top: calc(var(--s1) - var(--s3)); }
.intro-line { font-size: 1.05rem; color: var(--parch); margin: 0; }
.intro-dots { display: flex; gap: 8px; justify-content: center; }
.intro-dots span { width: 8px; height: 8px; border-radius: 50%; background: var(--line); }
.intro-dots span.on { background: var(--gold-soft); }
.intro-back { text-align: center; text-decoration: none; min-height: var(--touch);
  display: inline-flex; align-items: center; justify-content: center; }
.checklist .why { display: block; font-size: .8rem; margin-top: 2px; }
.intro-frame .examen-list { margin-top: 0; }
.intro-frame .examen-card { pointer-events: none; }
.intro-frame .card h3 { margin: var(--s1) 0; font-family: var(--display); font-weight: 500; font-size: 1.15rem; }
/* A chip shown, not offered: the starter's shape as a picture of the two
   languages side by side. */
.intro-frame .chip-static { display: inline-flex; align-items: center; min-height: var(--ctl);
  padding: 0 14px; border: 1px solid var(--line); border-radius: var(--r-pill);
  font-family: var(--sans); font-size: .88rem; color: var(--parch); }
.intro h1 { font-size: 1.75rem; }
.intro-line { font-size: .98rem; line-height: 1.5; }
/* WHEN · WHERE — two rows under the paragraph, the icon and the word as the
   term, one plain sentence as the description. */
.intro-meta { margin: 0; display: grid; gap: var(--s2); }
.intro-meta > div { display: grid; grid-template-columns: 82px 1fr; gap: var(--s2); align-items: start; }
.intro-meta dt { display: flex; align-items: center; gap: 6px; color: var(--gold-soft);
  font-family: var(--display); font-size: .68rem; letter-spacing: .1em; text-transform: uppercase;
  padding-top: 2px; }
.intro-meta dt svg { width: 16px; height: 16px; flex: 0 0 auto; }
.intro-meta dd { margin: 0; font-size: .92rem; color: var(--parch); }
/* THE SHELL IN MINIATURE — the corners and the tab bar as a new person will
   meet them on Home, the relevant piece lit with the tab bar's own active
   treatment. Static: a picture, not a bar (so the fixed-layer law is not in
   play), and never a control. */
.intro-shell { border: 1px solid var(--line); border-radius: 14px; background: var(--bar-bg);
  padding: var(--s2) var(--s2) 6px; display: grid; gap: var(--s2); }
.intro-corners { display: flex; justify-content: space-between; }
.intro-corners .glyph { display: inline-grid; place-items: center; width: 34px; height: 34px;
  border-radius: var(--r-pill); border: 1px solid var(--line); background: var(--bar-bg); color: var(--muted); }
.intro-corners .glyph svg { width: 18px; height: 18px; }
.intro-corners .glyph.corner { color: var(--gold-soft); }
.intro-corners .glyph.on { color: var(--gold-soft); background: var(--tint-gold); border-color: var(--gold); }
.intro-tabs { display: flex; justify-content: space-between; border-top: 1px solid var(--line); padding-top: 6px; }
.intro-tabs span { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 5px 2px; border-radius: 10px; font-family: var(--display); font-size: .56rem;
  letter-spacing: .08em; color: var(--muted); }
.intro-tabs span i { display: block; line-height: 1; }
.intro-tabs span svg { width: 19px; height: 19px; display: block; }
.intro-tabs span.on { color: var(--gold-soft); background: var(--tint-gold); }

/* ---- closing screen ---- */
/* chip-select — multi-select word chips (the Aftermath feelings step). A label
   wraps a hidden checkbox; the pill lights gold when chosen. Drawn at --ctl,
   tapped at 48 through the halo.

   The height came from the halo, which is the system's answer and arrived
   while this was being written by hand; what the halo does not fix is the
   FACE. Twelve feeling words in the sans at .9rem read as twelve buttons —
   "way too much real estate" on a phone. A chip is a selectable TOKEN, so it
   wears the token language like .pill, a shade larger (.82 against .72) only
   because these are words a person reads and chooses rather than a secondary
   action's label. */
.chip-select { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s3) 0; }
.chip-select .chip { display: inline-flex; align-items: center; min-height: var(--ctl);
  padding: 6px 14px; border: 1px solid var(--line-soft); border-radius: var(--r-pill);
  background: var(--card); cursor: pointer;
  font-family: var(--display); font-weight: 500; font-size: .82rem;
  -webkit-tap-highlight-color: transparent; }
.chip-select .chip input { position: absolute; opacity: 0; pointer-events: none; }
.chip-select .chip:has(input:checked) { border-color: var(--gold);
  background: var(--card-strong); color: var(--gold-soft); }
.chip-select .chip:has(input:focus-visible) { outline: 2px solid var(--gold-soft);
  outline-offset: 2px; }

/* the blank-kind follow-up prompt (the one-thing effort scale) */
.qtext-followup { font-family: var(--display); font-size: 1.1rem; color: var(--parch);
  margin: var(--s4) 0 0; }

/* rphrases — the repair phrases said out loud at turn-toward */
.rphrases { display: flex; flex-wrap: wrap; gap: var(--s2); justify-content: center;
  margin: var(--s4) 0; }
.rphrase { border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 8px 14px; background: var(--tint-gold-soft); font-style: italic;
  color: var(--gold-soft); }

/* q-form — the answer form on an examen question: an even breath above the
   options, the opt-out, and the back/pause row. Owned here, never inline. */
.q-form { margin-top: var(--s4); }
.q-form .btn.option.italic { margin-top: var(--s3); }
.q-form .q-nav { margin-top: var(--s4); }

/* closing — the ceremonial end of an examen. The component owns its own
   vertical crescendo (quiet between the verse lines, a breath before the
   blessing, the fullest pause before "Amen") so no screen carries it inline. */
.closing { text-align: center; margin-top: 10vh; }
.closing .glyph { font-size: var(--t-display); }
.closing .blessing { margin-top: var(--s5); }
/* A component owns its INTERNAL spacing; the page owns only where it sits.
   All four of these were scoped to .closing, so the first screen to reuse the
   card inherited the page's spacing instead of the component's and opened a
   hole under its own title. The closing is unchanged — it keeps the placement
   rule and nothing else moved. */
.insight-card .row { margin-top: var(--s2); }
.insight-card .verse { margin: var(--s2) 0 2px; }
.insight-card .note { margin-top: var(--s2); }
.insight-card .note p { margin: 0; }
.insight-card .note p + p { margin-top: var(--s3); }
.closing .insight-card { margin-top: var(--s5); text-align: left; }
.closing .closing-end { margin-top: var(--s6); }
.closing .closing-ctas { margin-top: var(--s4); justify-content: center; flex-wrap: wrap; }

/* ---- friends leaderboard ----
   ONE line per person: rank · who (name, streak) · score · the menu slot.
   Every row ends in the same --ctl-wide slot — empty on your own row — so the
   numbers sit on one vertical line down the board. A control never grows
   the row it sits in, and here a control never widens it either: the row's
   actions live behind three dots, not beside the score. A name truncates;
   the streak line never wraps. */
.leader { display: flex; align-items: center; gap: var(--s3); }
.leader .rank { font-family: var(--display); font-size: 1.2rem; font-weight: 600;
  width: 24px; text-align: center; color: var(--parch); }
.leader .who { flex: 1; min-width: 0; }
.leader .who h3 { margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.leader .who > .small { display: block; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }   /* never wraps; at a large text size it truncates like the name */
.leader .score { text-align: center; white-space: nowrap; }
.leader .score .n { font-family: var(--display); font-size: 1.3rem; font-weight: 600;
  color: var(--parch); line-height: 1; }
/* THE SLOT. Every row in a list owns the same one at its end, whether or not
   it has anything to do, so the text and numbers beside it stay on one
   vertical line and a row with a menu is not narrower than a row without.
   It was scoped to .leader; the examen history is the second list to need it,
   and an unstyled slot there collapsed when empty and took intrinsic width
   when full — which wrapped the meta line and made that card taller. That is
   the same fault the rule was written for ("the friends board grew every
   friend's card by 46px for the word Encourage"), so the slot is general. */
.menu-slot { flex: 0 0 var(--ctl); width: var(--ctl); }
.card.me { border-color: var(--gold); }
/* Row menu — a row's actions behind three dots. A <details>, so it works
   with no script; the popover is a .menu-list hung under the button, right
   edge to right edge, above the cards below it. Open = the dots go gold,
   exactly as an active pill does. The danger item is the one destructive
   action, red only in the menu — the row itself never looks alarmed. */
.row-menu { position: relative; }
.row-menu > summary { list-style: none; }
.row-menu > summary::-webkit-details-marker { display: none; }
.row-menu[open] > summary { color: var(--gold-soft); border-color: var(--gold); }
/* The hung panel is ONE surface, whether it holds actions or options — the
   same night-2 ground, the same shadow, the same stacking order. It was
   written as `.row-menu-pop.menu-list`, which tied the surface to the kind of
   rows inside it; the multi-select below hangs checkboxes instead, and two
   copies of the same panel would be the "two things doing one job" bug. */
.row-menu-pop { position: absolute; top: calc(100% + var(--s1));
  z-index: 20; margin: 0; min-width: 232px; background: var(--night-2);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .45); }
/* An actions menu hangs from the trailing edge (the dots sit at the row's
   right); a filter hangs from the leading edge, under its own label. */
.row-menu-pop.menu-list { right: 0; }
.row-menu-pop form + form { border-top: 1px solid var(--line-soft); }
.row-menu-pop button { white-space: nowrap; }
.row-menu-pop button.danger { color: var(--danger); }
.row-menu-pop button.danger svg { color: var(--danger); }

/* ---- .multiselect — a multi-select DROPDOWN --------------------------------
   Composition, not a new species: `details.row-menu` supplies the open/close
   (and `_row_menu_js.html` the one-at-a-time behaviour), `.row-menu-pop` the
   hung surface, `.check-row` the options. Nothing here is a new idea; this
   rule only makes the panel a list you can scroll and gives the summary the
   caret that says it opens.

   WHY NOT `<select multiple>`: iOS draws it as an inline listbox several rows
   tall that cannot be dismissed, on a canvas 402px wide — it is unusable, and
   its indicator is the platform's own black arrow on the night ground, which
   §4's "never dark ink on the night" forbids outright. WHY NOT CHIPS: chips
   are TOKENS you pick from a short closed set (the Aftermath feelings, which
   is `.chip-select`'s one home); a roster grows, and twenty wrapping chips
   push the table they filter off the screen. A dropdown costs one line
   whatever the roster does.

   The panel is capped and scrolls, so a roster of three and a roster of two
   hundred draw the same control. */
.multiselect { position: relative; }
.multiselect > summary { cursor: pointer; }
.multiselect > summary::after { content: ""; width: 7px; height: 7px;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur) var(--ease); }
.multiselect[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
.multiselect-pop { left: 0; max-width: min(320px, calc(100vw - 2 * var(--gutter)));
  max-height: 280px; overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid var(--line-soft); border-radius: var(--r-sm);
  padding: var(--s2) var(--s3); }
/* The options are .check-row; only their truncation is this panel's business,
   because a name is an unbounded string (§4, "Overflow & responsiveness"). */
.multiselect-pop .check-row { margin: 0; }
.multiselect-pop .check-row span { min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* The actions sit under the options, on the panel's own ground, so they stay
   reachable when the list scrolls. */
.multiselect-acts { position: sticky; bottom: calc(-1 * var(--s2));
  display: flex; gap: var(--s2); padding: var(--s2) 0 0;
  margin-top: var(--s2); border-top: 1px solid var(--line-soft);
  background: var(--night-2); }

/* ---- covenant analytics ---- */
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.person-name { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }
.num { font-family: var(--display); font-size: 1.8rem; color: var(--parch); line-height: 1; }
.insight { line-height: 1.6; }
.insight + .insight, .card .insight { margin-top: var(--s2); }
.insight.divided { border-top: 1px solid var(--line-soft);
  padding-top: var(--gap-card); margin-top: var(--gap-card); }
.card h4 { margin-top: var(--s4); }
/* the quiet line that closes an analytics page */
.chart-footnote { margin-top: var(--s4); }
/* a small label introducing a list inside a card */
.card .subhead { margin-top: var(--s3); }
.insight b { color: var(--gold-soft); font-weight: 500; }
.ot { margin: 6px 0; }
.ot .muted { font-size: .78rem; }

/* ---- month calendar ---- */
.cal { display: flex; flex-direction: column; gap: 4px; }
.cal-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
.cal-cell { min-width: 0; overflow: hidden; }
.cal-cell { aspect-ratio: 1 / 1; }
.cal-wd { aspect-ratio: auto; text-align: center; font-family: var(--display);
  font-size: .68rem; letter-spacing: .1em; color: var(--muted); padding-bottom: 2px; }
.cal-day { display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  padding: 6px 2px 2px; border: 1px solid var(--line-soft); border-radius: 10px;
  background: var(--card); text-decoration: none; color: var(--ink); }
.cal-day:active { background: var(--card-strong); }
.cal-day.today { border-color: var(--gold); }
.cal-day.future { opacity: .5; }
.cal-num { font-family: var(--display); font-size: .9rem; }
.cal-marks { display: flex; align-items: center; gap: 2px; margin-top: auto; padding-bottom: 2px;
  font-size: .7rem; min-height: 1.1em; }
.cal-blocks { color: var(--gold-soft); font-family: var(--display); font-weight: 500; }
.cal-marks svg { width: 11px; height: 11px; }
.cal-candle { color: var(--gold-soft); }
.cal-reading { color: var(--gold-soft); }   /* the day's readings, received */
.cal-plan { color: var(--muted); opacity: .55; }   /* a planned Bible reading, not yet read */
.cal-empty { border: none; }
/* week strip — the month's day cell, laid as a tappable Mon–Sun ribbon on the
   Day view (the week's story at a glance; each cell jumps to its day). */
.week-strip { display: flex; align-items: stretch; gap: 4px; margin: 0 0 var(--s3); }
.week-strip .ws-days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px; flex: 1; min-width: 0; }
.week-strip .cal-day { min-height: var(--touch); }
.week-strip .cal-wd { font-size: .56rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); }
.week-strip .nav-arrow { align-self: center; }
.cal-day.selected { border-color: var(--gold); background: var(--card-strong); }
.cal-day.selected .cal-num { color: var(--gold-soft); }
/* a feast / holy day — a dot in the day's liturgical accent, plus a tinted top edge */
.cal-day.has-feast { border-top: 2px solid var(--feast, var(--gold)); }
.cal-feast { width: 6px; height: 6px; border-radius: 50%; background: var(--feast, var(--gold));
  position: absolute; top: 6px; right: 6px; }
.cal-day { position: relative; }
/* legend — the ONE key row (chart series, month marks): centered, wrapping,
   each entry a swatch/icon + label. `.legend` and `.cal-legend` are the same
   component; pair entries with `.dot`/`.cal-swatch` swatches. */
.legend, .cal-legend { display: flex; flex-wrap: wrap; gap: var(--s3); justify-content: center;
  margin: var(--s3) 0; font-family: var(--sans); }
.legend span, .cal-legend span { display: inline-flex; align-items: center; gap: 5px; }
.cal-swatch { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.cal-swatch.feast {   /* "varies": a holy-day dot wears its day's liturgical color */
  background: conic-gradient(#7fae8e 0 25%, #e3c47e 0 50%, #cf7a7c 0 75%, #a58bc9 0); }
/* the chapter pill's two worn states, as legend swatches: read in full, started */
.cal-swatch.read { background: var(--gold); }
.cal-swatch.started { background: transparent; border: 1.5px dashed var(--gold); }
.cal-swatch.today-ring { background: transparent; border: 1.5px solid var(--gold);
  width: 10px; height: 10px; border-radius: 3px; }
.milestone.is-past { opacity: .5; }

/* ---- side-by-side review ---- */
.review-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.review-grid .col h4 { position: sticky; top: 0; background: var(--night); margin: 0;
  padding: 6px 0; font-family: var(--sans); font-size: .8rem; color: var(--muted); }
.review-block { border-bottom: 1px solid var(--line-soft); padding: 8px 0; font-size: .92rem; }
.review-flag { background: var(--tint-danger); outline: 2px solid var(--danger); border-radius: 4px; }

/* ---- print: paper is paper — force the light palette ---- */
@media print {
  /* paper is paper — the ground and the status bar never print */
  body::before, .statusbar-scrim { display: none !important; }
  /* and the person's text size is for the screen: paper keeps its own scale */
  html[data-text] { --text-scale: 1; }
  :root {
    --bg: #fff; --card: #fff; --card-strong: #f6f1e3;
    --parch: #2d2a24; --ink: #2d2a24; --ink-2: #3d3930; --muted: #7c7462;
    --faint: #a39a86; --line: #ccc; --line-soft: #ddd;
    --gold: #a8822f; --gold-soft: #a8822f; --on-gold: #fff;
    --accent: #444; --danger: #a63d40;
  }
  /* Paper economy: rem units track the ROOT size, which print must reset or
     every heading keeps its screen scale. One root cut shrinks the whole
     type ladder proportionally. Sized like a dense professional document:
     9.5pt body, headings barely above it. */
  html { font-size: 9.5pt; }
  body { background: #fff !important; background-image: none !important;
    color: #2d2a24; padding: 0; font-size: 9.5pt; line-height: 1.35; }
  h1, h2, h3 { color: #2d2a24; }
  h1 { font-size: 13pt; }
  h2 { font-size: 11pt; }
  h3 { font-size: 10pt; }
  .print-day-label { flex-basis: 44px; }
  .print-day-label .d-date { font-size: 11.5pt; }
  .print-day { padding: 6px 0; }
  .print-day-blocks li { padding: 1.5px 0; }
  nav.tabbar, header.topbar, .no-print, .messages { display: none !important; }
  main { max-width: none; padding: 0; }
  .card, .script, .song { border-color: #bbb; break-inside: avoid; background: #fff; }
  /* Day-by-day binder order: each DAY of full guides starts a fresh page;
     activities inside a day flow continuously, separated by a hairline. */
  .print-day-group { break-before: page; }
  .print-group-day { border-bottom: 1.5pt solid #444; padding-bottom: 3pt;
    margin-bottom: 8pt; color: #a8822f; }
  .print-activity + .print-activity { border-top: 1px solid #ddd;
    margin-top: 10pt; padding-top: 8pt; }
  .print-header { border-bottom: 2pt solid #444; margin-bottom: 12pt; padding-bottom: 6pt; }
  .print-header .eyebrow { color: #7c7462; }
  a { text-decoration: none; color: inherit; }

  /* page discipline: a heading never strands at a page's foot, a day row and
     a list item never split across the fold, text keeps its widows/orphans */
  h2, h3, .print-section-title { break-after: avoid; }
  .print-day, li { break-inside: avoid; }
  p { orphans: 3; widows: 3; }
  .print-day { border-top-color: #ddd; }
  .print-day.rest { opacity: 1; }                 /* toner-friendly: no washout */
  .print-day.rest .rest-note { color: #a39a86; }
  .print-day-label .d-name { color: #a8822f; }
  .print-day-label .d-date { color: #2d2a24; }
  .pbox { border-color: #777; }
  .print-cols { columns: 2; column-gap: 24pt; }
  .print-cols li { break-inside: avoid; }
  /* Binder margin: extra room on the LEFT for three-hole punching. */
  @page { margin: 12mm 10mm 12mm 24mm; }
}

/* screen-only: print must never inherit the big-canvas bump (this block sits
   after @media print, so an unscoped query would override paper sizes). */
@media screen and (min-width: 700px) {
  :root { --gutter: var(--s5); }   /* the page breathes wider on big canvases */
  body { font-size: calc(18px * var(--text-scale)); }
}

/* Emmaus daily allowance — a usage meter: the count in words, the bar for its
   shape, the reset time underneath. Reuses .progress (the ONE sequential
   treatment); adds only the row that labels it. */
.allowance { margin: var(--s3) 0 var(--s2); }
.allowance-line { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s2); margin: 0; font-size: .9rem; color: var(--ink-2); }
.allowance-of { font-family: var(--display); color: var(--muted); font-variant-numeric: tabular-nums; }
.allowance .progress { margin: 8px 0 6px; }
.allowance .small { margin: 0; }

/* ── The entrance ────────────────────────────────────────────────────────
   The sign-in doorway (allauth's account/* pages). Not the app shell: no top
   bar, no tabs, nothing to navigate — one candle and one door. Centred on the
   page rather than top-aligned, because this screen has one job. */
.entrance { display: grid; place-items: center; min-height: 100svh;
  padding: var(--s6) var(--gutter); }
.entrance-card { width: 100%; max-width: 22rem; text-align: center; }
/* the same .brand-mark component, sized for a doorway rather than a top bar
   (the gradient fills live on .brand-mark — a lookalike class would render a
   black candle, which is exactly what happened the first time) */
.entrance-mark, .entrance-mark svg { width: 64px; height: 64px; }
.entrance-mark { margin: 0 auto var(--s3); }
.entrance-word { font-size: 1.6rem; margin: 0 0 var(--s5); }
.entrance-line { color: var(--ink-2); margin: 0 0 var(--s5); }
.entrance-google { display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s2); }
.entrance-google .g-mark { width: 18px; height: 18px; flex: 0 0 18px; }
.entrance-note { color: var(--muted); font-size: .85rem; margin: var(--s4) 0 0; }
/* the address an invitation went to — the one thing on the door to read */
.entrance-invite b { color: var(--parch); font-weight: 500; overflow-wrap: anywhere; }
.entrance-messages { list-style: none; margin: 0 0 var(--s4); padding: 0;
  color: var(--ink-2); font-size: .9rem; }

/* The newest Emmaus reply is an anchor (#reply): after the POST → redirect →
   GET round trip the browser lands on the answer instead of the top of the
   page. A little air above it so it isn't flush against the viewport edge. */
.chat-msg#reply { scroll-margin-top: var(--s5); }
