/* AUTO-GENERATED — do not edit this file directly.
   Edit the partials and run: python3 build-css.py */

/* ===========================================================
   00-settings.css
   SETTINGS — :root design tokens (palette, spacing, type scale, elevation, fonts). No selectors that paint; lowest specificity. Loads first.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   ── Version history lives in CHANGELOG.md, not here. ──────────
   =========================================================== */

/* ── Self-hosted Inter (variable, OFL-1.1) ──────────────────────────
   2026-06-06 — Inter is now the single app-wide face, self-hosted as a
   variable woff2 (replaced the Google Fonts CDN load + the proprietary
   Google Sans CDN face, both removed from index.html). Self-hosting makes
   the app fully offline on first run and leaks no user IP to Google
   (GDPR). One variable file per subset covers every weight 100–900, so
   the variable axis serves 300/400/500/600/700/800 natively (no synthetic
   weights, unlike the old Google-Sans-ships-only-400/500/700 situation).
   Files live in fonts/ and are precached in sw.js (verified by
   check-precache.py); license in fonts/OFL.txt. unicode-range values
   mirror Google Fonts' own Inter subsets so glyph coverage is unchanged
   (latin + latin-ext → English plus accented names / European copy). */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/InterVariable-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/InterVariable-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* PRANA DESIGN TOKENS — accent semantics are enforced:
   SURFACES (deep navy family)
   ACCENT (semantic only — never decorative)
   --sage      #7dd3e8  THE ONLY global accent.
   • active nav items
   • primary CTAs
   • current selection indicators
   • brand glows / brand mark
   • INHALE phase color
   --amber     #d4a574  LOCKED to retention/breath-hold state.
   • HOLD phase
   • Best Hold stat
   • active retention timers
   NEVER a button color.
   --rose      #e08a8a  LOCKED to warnings, medical disclaimers,
   critical errors. NEVER on ambient chrome.
   --lavender  #a89cd9  LOCKED to the EXHALE breathing phase
   ONLY. Nowhere else.
   --moon      #7d8eb0  Neutral informational tint (clinical
   TYPE
*/

  /* ── Surfaces ───────────────────────────────────────────────────
     PALETTE-UNIFICATION pass (2026-05) — PHASE 1 of 4 (define only).
     This phase is purely additive + one canonical-value reconciliation.
     No selectors change; no surface is recolored beyond the single
     reconciliation noted below. Phases 2–4 (redirect rogue literals →
     these tokens, rebuild) follow as separate approved steps.

     CANONICAL-DARK RECONCILIATION (structural seam fix, no recolor):
     the app had TWO near-identical canvas darks on adjacent layers —
     the <body> hardcoded #050a14 and --bg-0 #040912 (what .screen /
     page canvas actually painted). They differ by ~1–2 levels/channel,
     so any uncovered edge (transition gap, iOS overscroll, safe-area)
     showed a faint seam. Resolved by keeping --bg-0 at its original,
     cleanly-tokenized #040912 and making <body> READ var(--bg) (see
     html,body rule below) instead of its one-off #050a14 literal. Net
     effect: the body conforms DOWN to the canvas the screens already
     paint; the screens (incl. home) are visually unchanged. One token,
     no hardcoded backdrop, seam closed — and the canvas darks stay the
     original #040912 / #07101c / #0b1727 set.

       --bg-0  CANONICAL canvas. The backdrop everything floats on.
       --bg-1  raised canvas (sheets, raised page bg).
       --bg-2  third canvas step.
       --card-1 / --card-2  the navy card surfaces (unchanged). */
  /* 2026-07-20 — "bluer navy" palette (user-approved). Lifted the whole surface family
     off the near-black grayish set toward a warmer blue-navy. Previous set:
     bg-0 #040912 / bg-1 #07101c / bg-2 #0b1727 / card-1 #16181d / card-2 #1d2026. */
  --bg-0:     #0B111C;   /* screen background */
  --bg-1:     #0e1826;   /* one step lighter — sheet / raised canvas */
  --bg-2:     #111A28;   /* third navy step = card surface */
  /* Canonical premium sheet background — sage-tint glow + bg-1→bg-0 gradient.
     Shared by the Sound sheet and the technique tuning sheet so they match. */
  --grad-sheet: radial-gradient(120% 60% at 0% 0%, var(--sage-tint) 0%, transparent 60%),
                linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
  --card-1:   #111A28;   /* card / raised surface */
  --card-2:   #16223a;   /* raised card surface — a step lifted for depth */
  --icon-fill: #13202F;  /* avatar + row-icon tile fill */
  --chip-fill: rgba(148,180,220,0.09);  /* subtle translucent blue-grey chip fill */

  /* legacy aliases — --bg/--card still widely read; migration deferred.
     --bg2/--card2 retired (2026-05): consumers now read --bg-1/--card-2. */
  --bg:       var(--bg-0);
  --card:     var(--card-1);

  /* ── Unification tokens (Phase 1: defined as no-op aliases) ───────
     These name the surfaces that Phases 2–3 will redirect rogue
     hardcoded literals onto. Defined here pointing at the EXISTING
     values so adoption is lossless — nothing renders differently until
     a literal is repointed. Each maps a real color already in use:

       --surface         the card surface (= --card-1). Phase 2 repointed
                         the modals.css re-hardcoded card literal here.
       --surface-raised  lifted card surface (= --card-2).
       --on-sage         the DARK INK printed ON a sage fill (#0a1420).
                         NOT a surface — it's a foreground color (sage
                         CTA label, sage calendar-cell digit, timer
                         play glyph). Named so Phase 3 can repoint its
                         ~8 scattered literals to one source.
       --grad-ambient    the 3-stop page-glow gradient repeated as a
                         literal string in 3 files. Now built from the
                         canvas tokens so it tracks --bg automatically. */
  --surface:         var(--card-1);
  --surface-raised:  var(--card-2);
  --on-sage:         #0a1420;
  --grad-ambient:    linear-gradient(135deg, var(--bg-0) 0%, var(--bg-2) 60%, var(--card-1) 100%);

  /* Soft surface — the canonical "rest on canvas" fill.
     This is a 2%-white VEIL (transparent); its result depends on the
     layer beneath. Over the canvas (--bg-0 #040912) it lifts to a
     barely-there ~#0a0e17 — quiet by design. Over a --card it lands
     lighter. Unchanged. */
  --surface-soft:       rgba(255, 255, 255, 0.02);
  --surface-soft-hover: rgba(255, 255, 255, 0.03);

  /* ── Navy lift — raised navy surface fill ───────────────────────
     Collapses three near-identical hardcoded navy-to-navy ramps
     (tech-modal-sheet, live pill, avatar ring) into one tunable
     pair built from the navy family.

       --grad-lift    135° diagonal — small UI assets (pills, rings).
                      The diagonal mimics a top-left light source so
                      compact surfaces read as raised/organic.
       --grad-lift-v  180° vertical raised-navy ramp. NOTE: bottom
                      sheets no longer use this — .tech-modal-sheet now
                      defaults to the page canvas (var(--bg-0)) so sheets
                      read as the canvas family, not navy slabs. This
                      token is retained for any surface that explicitly
                      opts IN to a raised navy fill; none currently do. */
  --grad-lift:   linear-gradient(135deg, var(--card-2) 0%, var(--bg-2) 100%);
  --grad-lift-v: linear-gradient(180deg, var(--card-2) 0%, var(--bg-2) 100%);

  /* Borders / dividers (native iOS-style hairlines) */
  --border:        rgba(255,255,255,0.06);   /* canonical hairline */
  --border-strong: rgba(255,255,255,0.10);
  --border-warm:   rgba(125,211,232,0.22);   /* sage-tinted edge (was --border2) */

  /* Sage — the ONLY global accent. NOTE: token name is historical; the
     value is now a cyan-blue (#7dd3e8, rgb 125,211,232), re-themed 2026-05.
     The whole --sage-* ramp + raw rgb(125,211,232) usages move together. */
  --sage:       #7dd3e8;
  --sage-400:   #5ebac6;                         /* (was aliased --sage2) */
  --sage-soft:  rgba(125,211,232,0.08);          /* canonical tile bg */
  --sage-50:    var(--sage-soft);                /* alias matching design system */
  --sage-100:   rgba(125,211,232,0.16);
  --sage-200:   rgba(125,211,232,0.34);
  --sage-glow:  rgba(125,211,232,0.35);
  /* Sage opacity ramp — extends the four canonical steps above so every
     in-app sage rgba reads from a token. Lossless extraction (2026-05):
     the alphas below are the exact values that existed as literals across
     the partials; tokenizing them changed no pixels. Three "role" tokens
     at the top end (--sage-fill-3, --sage-phase-border, --sage-shimmer)
     are intentionally NOT on the linear ramp — they encode specific UI
     meaning (heatmap tier, reduced-motion phase cue, splash shimmer
     midpoint) and must not be snapped toward --sage-glow, or the calendar
     intensity steps, the accessibility phase border, and the success
     shimmer all visibly degrade. */
  --sage-wash:   rgba(125,211,232,0.02);  /* faintest radial/gradient wash */
  --sage-tint:   rgba(125,211,232,0.04);  /* faint surface tint */
  --sage-veil:   rgba(125,211,232,0.06);
  --sage-90:     rgba(125,211,232,0.10);
  --sage-110:    rgba(125,211,232,0.12);
  --sage-130:    rgba(125,211,232,0.14);
  --sage-170:    rgba(125,211,232,0.18);  /* most common edge/fill */
  --sage-210:    rgba(125,211,232,0.22);  /* matches --border-warm alpha */
  --sage-270:    rgba(125,211,232,0.28);
  --sage-290:    rgba(125,211,232,0.30);
  --sage-glow-40: rgba(125,211,232,0.40);
  --sage-glow-42: rgba(125,211,232,0.42);
  --sage-glow-45: rgba(125,211,232,0.45);
  --sage-glow-peak: rgba(125,211,232,0.55); /* canonical pulse/throb peak */
  --sage-fill-3:     rgba(125,211,232,0.62); /* calendar heatmap tier 3 */
  --sage-phase-border: rgba(125,211,232,0.90); /* reduced-motion phase cue */
  --sage-shimmer:    rgba(125,211,232,0.95); /* splash shimmer midpoint */

  /* Amber — retention status, never a button */
  --amber:      #d4a574;
  --amber-50:   rgba(212,165,116,0.08);
  --amber-100:  rgba(212,165,116,0.22);

  /* Rose — warnings, medical, errors */
  --rose:       #e08a8a;
  --rose-50:    rgba(224,138,138,0.10);
  --rose-100:   rgba(224,138,138,0.22);

  /* Lavender — EXHALE phase ONLY */
  --lavender:    #a89cd9;
  --lavender-50:  rgba(168,156,217,0.10);
  --lavender-100: rgba(168,156,217,0.22);  /* parity with sage/amber/rose ramp */

  /* Moon — clinical/informational tint, status only */
  --moon:       #7d8eb0;
  /* Focus — the per-category Focus tint (sky blue). Tint-only, not an accent. */
  --focus:      #7fb8d1;

  /* Pure white — opaque #fff for high-contrast glyphs/fills printed on a
     colored or dark surface (toggle knobs, hero numerals, sheet handles).
     Distinct from --text (#f4f1ec, the warm off-white READING ink): this is
     a deliberate true white reserved for non-text UI that must read as
     maximally bright. Collapses the ~6 scattered #fff / #ffffff literals
     onto one source. Adoption is visually inert. */
  --white:      #ffffff;

  /* White-alpha ramp — translucent white for surface washes and hairline
     strokes printed on the dark canvas. Mirrors the sage ramp's role split:
     the faint -wash/-tint/-veil end is for raised-surface backgrounds
     (glassmorphic tiles, sheet fills), the -line/-edge/-rule mid-band is for
     1px hairline borders, and the bright -bright/-grip end is for handles and
     active states. Collapses ~40 scattered rgba(255,255,255,a) literals (many
     near-duplicate .02/0.02/.025) onto a fixed scale, the same way the sage
     ramp ended that drift. Naming is semantic-then-functional, not numeric,
     because white-alpha reads by ROLE (wash vs line vs grip) rather than by an
     accent intensity step. Snapping is visually inert — each token equals the
     nearest existing literal cluster. */
  --white-wash:   rgba(255,255,255,0.02);  /* faintest surface tint */
  --white-wash-2: rgba(255,255,255,0.025); /* glassy card fill over a frosted scrim (.psd-card) */
  --white-tint:   rgba(255,255,255,0.03);
  --white-veil:   rgba(255,255,255,0.04);  /* canonical raised-tile bg */
  --white-soft:   rgba(255,255,255,0.05);
  --white-mist:   rgba(255,255,255,0.06);  /* most common surface wash (×36) */
  --white-haze:   rgba(255,255,255,0.07);
  --white-film:   rgba(255,255,255,0.08);
  --white-line:   rgba(255,255,255,0.10);  /* canonical hairline border */
  --white-line-2: rgba(255,255,255,0.12);
  --white-edge:   rgba(255,255,255,0.14);
  --white-rule:   rgba(255,255,255,0.16);  /* stronger divider */
  --white-rule-2: rgba(255,255,255,0.18);
  --white-grip:   rgba(255,255,255,0.22);  /* handle / drag affordance rest */
  --white-grip-2: rgba(255,255,255,0.28);  /* handle hover */
  --white-bright: rgba(255,255,255,0.32);  /* handle active / grabbing */
  --white-strong: rgba(255,255,255,0.40);
  --white-strong-2: rgba(255,255,255,0.45);
  --white-vivid:  rgba(255,255,255,0.60);

  /* Toggle OFF knob — a soft cool-gray so an inactive switch doesn't read as
     brightly as an active one on the dark canvas. The pip brightens to solid
     --white only when the toggle is ON (.ps-tog.on::after). Solid (not an
     rgba wash) so it stays opaque over the lightened off-track. */
  --toggle-off-knob: #99a1af;

  /* Medal ranks — leaderboard rank-1/2/3 ONLY (.lb-rank.gold/silver/bronze).
     Intentionally OFF the sage/amber/rose/lavender accent palette: these are
     literal metal tints (gold/silver/bronze), a universally-understood ranking
     convention that would lose meaning if mapped onto a brand accent. Named
     here (rather than left as raw literals) so they are discoverable and the
     hex lint can pass; they are an explicit, documented exemption from the
     one-accent rule, not drift. Used nowhere except the leaderboard. */
  --medal-gold:   #d4b454;
  --medal-silver: #c0c0c0;
  --medal-bronze: #cd9265;

  /* Type — 4-step text ramp on the navy canvas. Use the hyphenated
     names everywhere; they're the only canonical form. --text2 /
     --text3 alias passthroughs were retired 2026-05-29 after a
     sweep that migrated all consumers to the hyphenated names —
     matches the rest of the design-system scale tokens (--bg-0,
     --sage-100, --card-1, --s-1..--s-8, --r-sm/md/lg, --fs-h2-md
     etc.) which all use hyphens too.
     ──────────────────────────────────────────────────────────
     Contrast ratios — measured PER SURFACE (text-3 lands on cards far
     more than on the bare canvas, and cards are lighter, so the canvas
     number alone is misleading — that was the 2026-06-04 mistake):
                       canvas   card-1   card-2
       --text   #f4f1ec  ~14:1    ~13:1    ~12:1   AAA  body / primary
       --text-2 #8395ad   6.52     5.81     5.34   AA   secondary
       --text-3 #7a88ac   5.65     5.03     4.62   AA   sub-text/captions/eyebrows
       --text-4 #2d3a52   1.70      —        —     fail DECORATIVE ONLY
     ──────────────────────────────────────────────────────────
     2026-06-24 — --text-3 lifted #6878a0 → #7886aa. The 2026-06-04 pass
     (#4a5870 → #6878a0) only checked the CANVAS (4.54:1, a bare pass),
     but the same token on cards was 4.04 (card-1) / 3.72 (card-2) —
     BELOW WCAG AA 4.5 for normal text, which is why sub-text read faint
     on device. #7886aa is the minimal lift that clears 4.5:1 on the
     worst-case card (card-2) while staying visibly below --text-2 (5.34
     on card-2) so the secondary/tertiary hierarchy holds. Same blue-gray
     hue, ~+15 RGB. Used ~250× (.sub-text role class + .caption /
     page-subtitle / settings-row-sub composers + placeholders).
     2026-08-09 — re-lifted #7886aa → #7a88ac → #7c8aae. --card-2 drifted
     lighter over time (now #16223a), which dropped #7a88ac to 4.49 on it — a
     hair UNDER WCAG AA 4.5. #7c8aae clears it to 4.61 with margin while staying
     well below --text-2 (5.3 on card-2), so the secondary/tertiary hierarchy is
     unchanged; visually imperceptible (+2 per channel). Now PINNED by
     check-contrast.py so a future surface lift can't silently re-break it.
     --text-4 deliberately kept low-contrast — its consumers are
     decorative (stats calendar off-dots, disabled rows) where dimness
     is the point. */
  --text:    #f4f1ec;
  --text-2:  #8395ad;
  --text-3:  #7c8aae;
  --text-4:  #2d3a52;
  --slate:   #94A3B8;   /* medium slate — brighter secondary used on the auth /
                           paywall / wearable previews (7.37:1 on the navy). */

  /* Radius scale.
     CAT-7 drift-vs-anchor pass (2026-05): the radii flagged by lint-css.py
     split, like CAT-3, into mostly-inert drift (Δ0–1 from an existing step —
     snapped to a token, visually identical) and a few real decisions. The one
     real *scale* gap was 16px: four independent surfaces (modal icon wrap,
     stat/insight cards, the rose medical-note card) all sat at exactly 16px,
     stranded in the 14↔18 gap — Δ2 to either neighbour. Rather than distort
     four surfaces toward --r-lg or --r-xl, 16px is promoted to its own step,
     --r-lg2, exactly as the type scale added --fs-md for the orphaned 16–17px
     body↔h3 band. Adoption is visually inert. */
  --r-xs:    6px;
  --r-sm:    9px;
  --r-md:    12px;
  --r-lg:    14px;
  --r-lg2:   16px;   /* fills the 14↔18 gap (modal icon wrap, cards, rose note) */
  --r-xl:    18px;
  --r-2xl:   22px;
  --r-pill:  999px;
  /* Named non-scale radii — intentional, not "drift":
       --r-frame   the desktop device-mockup bezel (.phone / .phone-leaving).
                   A 390×844 rounded RECTANGLE — NOT a circle. Do not swap to
                   50%; that would render the whole app shell as an ellipse.
       --r-circle  true circular shapes (avatars, FAB). Use this (or 50%)
                   ONLY on square elements where a full circle is intended.
       --r-sheet   bottom-sheet TOP corners (.tech-modal-sheet), applied as
                   `var(--r-sheet) var(--r-sheet) 0 0`. 28px is a deliberate
                   large sheet radius above --r-2xl; snapping it to 22px would
                   visibly tighten every modal sheet. Anchor, not drift.
     Sub-scale functional radii (4px and below) are intentionally NOT tokens
     and are exempted in lint-css.py: focus-ring outline corners, the 4px-wide
     scrollbar thumb, and tiny status bars. The scale floor is --r-xs (6px);
     6px on a 4px-wide element reads visibly wrong, so these stay literal —
     the same reasoning that exempts the 2px hairline rule/track. */
  --r-frame:  44px;
  --r-circle: 50%;
  --r-sheet:  28px;
  /* legacy aliases used by older selectors */
  --radius:    18px;
  --radius-sm: 14px;
  --radius-lg: 24px;
  --radius-xl: 26px;
  --nav-h:     72px;

  /* ══════════════════════════════════════════════════════════════
     SPACING SCALE — 2-px rhythm (the rhythm the app is actually built on)
     ──────────────────────────────────────────────────────────────
     Governs every gap, margin, and padding in the app. Newly-authored
     components MUST pick from this scale; no more 7px, 13px, 21px one-offs.
     Existing component CSS keeps its inline px values for now — this token
     block exposes the canonical scale so the migration sprint has somewhere
     to reference from.

     RHYTHM RECONCILIATION (2026-05) — why half-steps exist:
     This block originally declared a "4-px rhythm" (8 coarse steps: 4/8/12/
     16/22/32/48/72). That was aspirational, not descriptive. An audit of the
     real padding/gap/margin declarations across all 16 partials found the
     four most-used spacing values in the entire app sit BETWEEN those coarse
     steps — they were never drift, they're load-bearing tiers:

       14px  72×   the single most common value app-wide (default comfy pad)
       18px  47×   = --gutter-sm; section/list inner padding
       10px  47×   the "loose" gap (looser than dense rows, tighter than s-3)
       6px   46×   the "tight" gap (chips, icon clusters)  — 212 uses combined

     Snapping these onto the coarse 4-px grid would have shifted ~212 real
     declarations by ±2px across every screen. Instead — following the exact
     precedent this file already set for the TYPE scale (--fs-h2-md at 26px
     sits between h2/h2-lg; --fs-md at 16px bridges body↔h3) and for spacing
     itself (--gutter-sm) — a real usage cluster earns a NAMED step rather
     than a visible snap. The four "half-steps" below (--s-1h … --s-4h) name
     the odd tiers so EVERY authored spacing value resolves to a token at its
     exact current pixel value. Adoption is therefore visually inert: nothing
     renders differently, the layout rhythm is preserved, and the scale now
     honestly documents the 2-px system the app was built on.

     The "h" suffix = "half-step above the integer step" (--s-3h = one tier
     above --s-3). Half-steps are first-class scale members, not exceptions.

     SUB-SCALE FLOOR (exempt, not migration debt): 1px / 2px / 3px stay raw.
     These are hairlines and optical nudges (borders, the 2px rule/track,
     -1px/-6px negative margin overlaps), the spacing analogue of the radius
     floor exemption documented above (--r-xs = 6px; sub-6px radii stay
     literal). The scale floor is --s-1 (4px); values below it are not tiers.

       s-1    4px   icon-to-label, hairline gap
       s-1h   6px   tight gap — chips, icon clusters
       s-2    8px   chip inner, dense list rows
       s-2h  10px   loose gap
       s-3   12px   row padding, default card inner
       s-3h  14px   default comfortable padding (most common value app-wide)
       s-4   16px   card inner, group separation
       s-4h  18px   section/list inner padding (matches --gutter-sm)
       s-5   22px   gutter (matches --gutter), card→card
       s-6   32px   section break
       s-7   48px   hero spacing
       s-8   72px   route-level breathing room
  ══════════════════════════════════════════════════════════════ */
  --s-1:   4px;
  --s-1h:  6px;
  --s-2:   8px;
  --s-2h: 10px;
  --s-3:  12px;
  --s-3h: 14px;
  --s-4:  16px;
  --s-4h: 18px;
  --s-5:  22px;
  --s-6:  32px;
  --s-7:  48px;
  --s-8:  72px;

  /* Type scale (Inter only — all weights 300–800 loaded in index.html).
     Migration pass 2026-05: the four supplementary steps below
     (--fs-micro / --fs-mini / --fs-md / --fs-h2-lg) were added to cover
     real usage clusters that the original 7-step scale left orphaned —
     the 9–10px micro-label band, the 16–17px body↔h3 gap, and the 28px
     section-title band. Every authored font-size now resolves to a token
     within ≤1px of its old literal value, so adoption is visually inert.

     Display numerals (revised): the two values ABOVE the text scale —
     72px and 60px — are the live breathing-timer readouts
     (.countdown-numeral, .hold-time-display). They are functional display
     glyphs, not text, and sit above --fs-display, so they get their own
     named tokens below (--fs-numeral-xl / -lg) rather than being silently
     left as literals. Everything AT or BELOW 56px (incl. the former
     "one-off" 48/36px) DOES belong to the scale and should reference a
     --fs-* token — those are migration debt, not exemptions.

     Migration pass 2 (2026-05): the 132 outstanding raw font-sizes were
     resolved. Most snapped to existing tokens (48→display, 36→h1, 22→h2,
     21/18→h3, plus the ≤1px Δ band). The sole ADDITION is --fs-h2-md (26px):
     a recurring hero/section-title value appearing 8× across home/session/
     profile-v2/80-screens that sat exactly between h2 (24) and h2-lg (28).
     Per the same precedent that created --fs-h2-lg and --fs-md, a real usage
     cluster earns a named step rather than a visible 2px snap. */
  --fs-numeral-xl: 72px;  --lh-numeral-xl: 1.0;  /* countdown numeral */
  --fs-numeral-lg: 60px;  --lh-numeral-lg: 1.0;  /* hold-time display   */
  --fs-display:   56px;  --lh-display: 1.0;   --ls-display: -1.5px;
  --fs-h1-lg:     44px;  --lh-h1-lg:   1.05;  --ls-h1-lg:   -1px;    /* hero-numeral step between display(56) and h1(32) — slider readouts */
  --fs-h1:        32px;  --lh-h1:      1.1;   --ls-h1:      -0.5px;
  --fs-h2-lg:     28px;  --lh-h2-lg:   1.15;  --ls-h2-lg:   -0.3px;  /* section titles */
  --fs-h2-md:     26px;  --lh-h2-md:   1.15;  --ls-h2-md:   -0.3px;  /* hero/section title mid-step (8× cluster: .pc-hero-title et al.) */
  --fs-h2:        24px;  --lh-h2:      1.15;  --ls-h2:      -0.3px;
  --fs-h3:        19px;  --lh-h3:      1.3;   --ls-h3:      -0.3px;
  --fs-md:        16px;  --lh-md:      1.4;   --ls-md:       0;       /* body↔h3 bridge */
  --fs-body:      14px;  --lh-body:    1.5;   --ls-body:     0;
  --fs-caption:   12px;  --lh-caption: 1.45;  --ls-caption: 0.2px;
  --fs-label:     11px;  --lh-label:   1.0;   --ls-label:   1.8px;
  --fs-mini:      10px;  --lh-mini:    1.2;   --ls-mini:    0.4px;    /* dense micro-labels */
  --fs-micro:      9px;  --lh-micro:   1.2;   --ls-micro:   0.6px;    /* smallest legible tier */

  /* ── Icon size tokens — REACH-FOR DEFAULTS for new icon contexts ──
     The existing 12-size constellation (.benefit-pill 11, .so-close
     15, .icon-btn 17, .goal-icon 22, .row-icon 24, etc.) stays as-is
     — each was sized to fit a specific container or visual weight
     (chip width, button chrome, card geometry). These four tokens are
     the DEFAULT picks any NEW icon site should reach for instead of
     hand-typing a fresh px value:

         --icon-sm  16px  inline glyphs, badges, list-row meta
         --icon-md  20px  default for a new icon when no container forces
                          a specific fit (e.g. card-pill leading glyph)
         --icon-lg  24px  canonical row-icon / nav-tab (de facto standard)
         --icon-xl  28px  hero, section-head, "Practise safely"-style

     Usage: width: var(--icon-md); height: var(--icon-md);
     No lint enforces these — they're a soft default. Reach for a token
     unless the surrounding container demands a contextual size; when
     in doubt, --icon-md is the right starter. Existing odd sizes do
     NOT need migration; the goal is to stop the 13th/14th/15th
     bespoke pixel value from accreting in new code. */
  --icon-sm: 16px;
  --icon-md: 20px;
  --icon-lg: 24px;
  --icon-xl: 28px;

  --elev-1:         0 1px 3px rgba(0,0,0,.25);
  --elev-2:         0 8px 28px rgba(0,0,0,.40);
  --elev-3:         0 24px 60px rgba(0,0,0,.55);
  --elev-glow-sage: 0 0 22px var(--sage-glow);

  /* ── State-layer system (Material 3 idea, Prana palette) ─────────
     One uniform overlay logic for interactive surfaces, so new
     components stop hand-rolling their own hover/press/focus/disabled
     look (the "re-polishing" tax). These are OPACITIES, not colors —
     a component composites them over a sage (or neutral) tint via the
     .state-target / .state-layer primitive in 03-primitives.css.

     Calibrated for the dark sage-on-navy palette: M3's default light-
     theme veils (8/12/16% on light surfaces) read too faint on the
     deep --card base, so the press/focus steps sit a touch higher.
     These are the FOUNDATION values; tune per-surface only via a
     component override, never by editing the token.

       --state-hover   resting → pointer hover
       --state-focus   keyboard focus veil (pairs with the sage focus
                       ring; the ring is the primary cue, veil is support)
       --state-press   active / pressed (deepest interaction)
       --state-drag    dragged surface

     Disabled is expressed as two separate opacities — CONTENT (text/
     icon legibility floor) and SURFACE (fill knock-back) — matching
     M3's split so disabled controls stay perceivable but clearly inert.
     38% content is the WCAG-informed floor M3 uses; keep it there. */
  --state-hover:            0.08;
  --state-focus:            0.12;
  --state-press:            0.16;
  --state-drag:             0.20;
  --state-disabled-content: 0.38;
  --state-disabled-surface: 0.04;

  /* The tint a state layer composites. Sage by default (matches the
     one-accent rule); a component may set --state-ink: 255,255,255 on
     a neutral surface where a sage veil would read as a color cast.
     Stored as an "R,G,B" triplet so the primitive can build rgba()
     with the opacity tokens above. */
  --state-ink: 125,211,232;   /* = --sage */


  /* ── Stacking layers ───────────────────────────────────────────
     One ordered scale so z-index values are intentional and
     conflicts are impossible to introduce by guessing. Author new
     stacked surfaces against these; never hand-pick a raw integer.
     Local 0/1 stacking inside a component is fine and not tokenized.
       behind  -1   sits BEHIND its own parent (glow halos, bg layers)
       base     1   in-flow raised element
       raised  10   hover-lifted card, dropdown trigger
       sticky  50   sticky headers / section bars
       overlay 100  scrims, ambient full-bleed overlays
       modal   200  bottom sheets, dialogs
       toast   300  transient toasts / snackbars
       top     999  boot splash, must-be-above-everything */
  --z-behind:  -1;
  --z-base:     1;
  --z-raised:  10;
  --z-sticky:  50;
  --z-overlay: 100;
  --z-modal:   200;
  --z-toast:   300;
  --z-top:     999;

  /* ── Safe-area system ──────────────────────────────────────────
     Four-edge model that honors iOS notch / Dynamic Island, Android
     gesture pills, and (in landscape) the punch-out on the side a
     notched phone is held with. Every chrome surface (top-bar,
     bottom-nav, sticky CTAs, sheets, scroll-content) reads from
     these so we stay consistent across pages.

     Fallbacks: the second argument to env() kicks in everywhere
     the browser doesn't expose the inset (desktop, older Android,
     non-notched phones). We deliberately keep the fallback small
     (0–4px) so the layout doesn't look padded on hardware that
     doesn't need it; pages add their own padding on top.

     Note: env() works without a unit fallback in modern browsers,
     but providing one keeps calc() chains safe everywhere.
  ─────────────────────────────────────────────────────────────── */
  --safe-top:    env(safe-area-inset-top,    0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left,   0px);
  --safe-right:  env(safe-area-inset-right,  0px);

  /* Minimum padded edge: never less than this regardless of inset.
     Lets us write `max(var(--gutter), var(--safe-left))` once and
     forget about the device.
     NOTE: --gutter (22px) and --gutter-sm (18px) coincide in value with
     --s-5 and --s-4h respectively, but are kept SEPARATE on purpose — these
     are DEVICE-EDGE insets (paired with safe-area math), a distinct role
     from interior spacing. Same number today, different meaning; don't
     collapse one into the other (cf. the type-scale role tokens that are
     deliberately kept off the linear ramp). */
  --gutter:    22px;
  --gutter-sm: 18px;
  /* List-row vertical padding — role wrapper around var(--s-4h) (18px).
     One source of truth for the rhythm shared by .tech-item (Library)
     and the Profile + Settings list-row classes (.prof-reminders-row,
     .row-flat-list .settings-row). Bumping or trimming this token
     re-tunes every consuming row site at once.
     History this session: started at var(--s-4h) (18px) → tightened
     to var(--s-2) (8px) when migrating to a shared token → reverted
     back to var(--s-4h) per user direction ('rows in library looked
     better… Library back to 18px, Profile + Settings rows with it').
     The morning 'icons read too big on Profile/Settings' problem
     stays solved because the icon dial-back commit (418f1da)
     trimmed those icons to 24px + no glow — so taller rows now
     surround a quieter glyph, not the 28px + halo treatment that
     felt heavy.
     Horizontal padding stays per-row because icon-column geometry
     (48px tile vs. bare 24px glyph vs. no icon) varies. This token
     is deliberately vertical-only. */
  --row-pad-y: var(--s-4h);

  --font-display: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-body:    'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  /* Inter (self-hosted variable, see @font-face above) is the single
     app-wide face. Google Sans was dropped 2026-06-06 (proprietary →
     un-self-hostable; the CDN dependency conflicted with the offline +
     privacy goals). The variable axis covers every used weight
     (300/600/800 included) natively. Numeric
     surfaces (times, stats, counts) use the same chain with
     tabular-nums — see the [data-tabular] rule below. There is
     intentionally no mono token: numerics render in the body face
     with tnum, so a separate monospace face is neither loaded nor
     used. Do not reintroduce one. */

  --ease-ios:       cubic-bezier(0.32, 0.72, 0, 1);
  --ease-out-soft:  cubic-bezier(0.22, 1,    0.36, 1);
  --ease-standard:  cubic-bezier(0.4,  0,    0.2,  1);  /* symmetric in/out;
                       the workhorse curve for width/left/height transitions
                       and revealUp. Anchors the 6 hand-rolled .4,0,.2,1
                       literals the CAT-15 ease pass found scattered across
                       primitives/screens/utilities/home (drift from no anchor,
                       not a distinct curve). Curve is duration-independent —
                       pairs with both in-band --dur-* and the slow .6s one-shot
                       progress fills, so it lives here, not beside a duration. */
  --ease-linear:    linear;

  --ease:           var(--ease-ios);

  /* NOT tokenized — single-use bespoke animation curves, each shaping one
     keyframe sequence and shared by nothing. Tokenizing a one-consumer curve
     just trades a readable literal for an indirection (same call made for the
     one-off navy in the onboarding check-circle gradient):
       • splash-draw    cubic-bezier(.6,.05,.3,1)  — modals.css
       • splash-shimmer cubic-bezier(.5,0,.5,1)    — modals.css
       • dateSheetUp    cubic-bezier(.2,.8,.2,1)   — reminders.css
     If a second consumer ever appears, promote it to an --ease-* token then. */

  /* Duration scale — the UI-INTERACTION band (CAT-6).
     These four steps cover every state change a tap can cause, and nothing
     slower. The CAT-6 drift-vs-anchor pass (2026-05) snapped the dense
     70–420ms cluster of hand-rolled .15s/.18s/.2s/.35s literals onto these
     tokens (Δ≤30ms, visually inert). Everything ABOVE this band is motion
     design, not interaction feedback, and is deliberately NOT tokenized — the
     linter exempts it (see lint-css.py › check_transition_dur):
       • ambient infinite loops — skeleton 1.6s, shimmer 3.5s, livePulse,
         heroPlayPulse, splash-breathe … (looping, not a transition)
       • breath physiology — orb-inhale/exhale AND the glow-rings that breathe with
         them (ring-inhale/ring-exhale, driven off --orb-*-dur). These ARE the
         session content (and the only motion kept under prefers-reduced-
         motion); they must never be pulled toward a UI duration.
       • multi-stage choreography — the splash sequence and toastIn/Out, whose
         staggered delays are timed against each other, not the token scale.
       • slow one-shots / crossfades — progress-bar width .6s, expand .45s,
         scrim fade .5s (all above the band). The .4s phase-COLOR crossfade on
         the orb/phase label is in-band and snaps to --dur-page (Δ20ms, inert).
       • the 0.001ms prefers-reduced-motion kill-switch sentinel. */
  --dur-tap:        120ms;
  --dur-state:      220ms;
  --dur-card:       350ms;       /* Card hover/lift/expand. Slower than
                                    state so a card "settles" rather than
                                    snaps. Pair with --ease-ios. */
  --dur-page:       420ms;       /* top of the interaction band; nothing
                                    slower belongs to a --dur-* token. */
}

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

html, body {
  height: 100%;
  background: var(--bg);   /* was hardcoded #050a14 — now THE canonical
                              canvas token (--bg → --bg-0 → #040912).
                              Body conforms to the screen canvas; seam closed. */
  font-family: var(--font-body);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: "kern" 1, "liga" 1;
  font-weight: 400;
  letter-spacing: -0.005em;
  text-rendering: optimizeLegibility;
  overflow: hidden;
}

.stat-val,
.stat-val-num,
.stat-val-unit,
.home-status-time,
[data-tabular] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "kern" 1;
}

/* ===========================================================
   01-base.css
   BASE — app shell, .screen transitions, desktop device-frame preview, page-fade keyframes, backdrop-filter fallback.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* ── Native-app feel: no text selection on the UI ───────────────
   This is an app, not a document — tapping or long-pressing a label
   (e.g. a "For you" row title) should do nothing, the way it can't
   in a native app. iOS otherwise fires a text selection (blue
   handles) on the row label. Suppress selection + the long-press
   callout globally, then re-enable on real text inputs so typing
   and editing still work. Selection behaviour only — no layout or
   paint change. Supersedes the scattered per-element
   `user-select:none` rules (streak pill, home hero, sheets); those
   are now redundant-but-harmless and can be removed in a tidy-up.
─────────────────────────────────────────────────────────────── */
body {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
input, textarea, [contenteditable] {
  -webkit-user-select: text;
  user-select: text;
}

/* ── App shell ──────────────────────────────────────────────────
   Mobile-first: #app fills the viewport edge-to-edge, .phone is
   the full screen (no frame, no shadow). Above 480px the frame
   reappears as a "device preview" for desktop / tablet viewing.

   Why 100dvh (with 100vh fallback): on iOS Safari, 100vh refers
   to the *largest* viewport (address bar hidden), so the bottom
   nav can sit below the chrome and get clipped. 100dvh tracks
   the *dynamic* viewport and shrinks/grows with the URL bar.
─────────────────────────────────────────────────────────────── */
#app {
  width: 100%;
  height: 100vh;        /* fallback for browsers without dvh */
  height: 100dvh;
  background:
    radial-gradient(ellipse at 30% 20%, var(--sage-tint) 0%, transparent 50%),
    radial-gradient(ellipse at 70% 80%, rgba(125,142,176,0.03) 0%, transparent 50%),
    linear-gradient(135deg, var(--bg-0) 0%, var(--bg-2) 60%, var(--card-1) 100%);
}

.phone {
  /* Mobile default: edge-to-edge, no chrome */
  width: 100%;
  height: 100vh;
  height: 100dvh;
  background: var(--bg);
  border-radius: 0;
  overflow: hidden;
  position: relative;
  box-shadow: none;
}

/* Desktop / tablet preview: restore the framed device look.
   480px is the breakpoint — anything larger than a typical phone
   in portrait gets the frame, anything smaller gets fullscreen. */
@media (min-width: 480px) {
  #app {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .phone {
    width: 390px;
    height: 844px;
    max-height: calc(100dvh - 32px);  /* never overflow short windows */
    border-radius: var(--r-frame);
    box-shadow:
      0 60px 150px rgba(0,0,0,0.9),
      0 0 0 1px var(--white-soft),
      inset 0 1px 0 var(--white-mist);
  }
}

.screen {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  overflow-y: auto; overflow-x: hidden;
  scrollbar-width: none;
  /* Page-swap motion — asymptotic ease-out. Settles softly without
     overshoot, matching the rest of the motion system. */
  animation: page-fade-in var(--dur-page) var(--ease-out-soft) both;
  will-change: opacity, transform;
}
.screen::-webkit-scrollbar { display: none; }

/* Same-page re-render: main.js stamps fresh .screen markup into the
   existing .phone and adds this class to suppress the page entrance
   animation — toggles / flashes / post-nav updates must not replay the
   blur+scale+lift that is meant only for navigation. */
.screen.no-page-anim { animation: none; }

/* Page-fade-in animation. The blur() was dropped (2026-07-22) — animating a
   filter on the whole screen is a per-frame repaint (not GPU-composited, and
   not covered by will-change), which stuttered the transition into heavy pages
   like the Library ('see all'). Opacity + translate + scale stay: all
   composited, so the entrance is smooth on every page. */
@keyframes page-fade-in {
  from { opacity: 0; transform: translateY(10px) scale(.992); }
  to   { opacity: 1; transform: translateY(0)    scale(1); }
}

/* Legacy alias — keep `fadeIn` working for any older callers. */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px) scale(.992); }
  to   { opacity: 1; transform: translateY(0)    scale(1); }
}

#app { position: relative; }

/* Only the OUTGOING page is lifted out of flow and overlaid; the incoming page
   stays in normal flow (static). This is the fix for the navigation "jump": the
   incoming page never flips absolute→static at cleanup, so it doesn't shift —
   the old page simply fades out on top to reveal it (same single-element motion
   as the smooth reminders dismissal). */
#app.is-transitioning .phone-leaving {
  position: absolute;
  inset: 0;
  margin: auto;
}

/* On the desktop preview, the framed-phone styling lives on `.phone`
   only. Mirror it for `.phone-leaving` so the outgoing screen retains
   its border-radius + shadow during the exit fade. */
@media (min-width: 480px) {
  .phone-leaving {
    width: 390px;
    height: 844px;
    max-height: calc(100dvh - 32px);
    border-radius: var(--r-frame);
    overflow: hidden;
    background: var(--bg);
    box-shadow:
      0 60px 150px rgba(0,0,0,0.9),
      0 0 0 1px var(--white-soft),
      inset 0 1px 0 var(--white-mist);
  }
}
.phone-leaving {
  width: 100%;
  height: 100dvh;
  background: var(--bg);
  overflow: hidden;
}

/* Outgoing screen — runs once, then the JS removes its host wrapper. */
.screen.is-leaving {
  animation: page-fade-out var(--dur-state) var(--ease-ios) both;
  pointer-events: none;     /* no double-clicks during the swap */
  will-change: opacity, transform;
}

/* The exit counterpart of page-fade-in — used by every leaving page during the
   navigation cross-fade AND by the reminders-page dismissal. blur() dropped
   (2026-07-22): animating a filter on the whole outgoing screen is a per-frame
   repaint that stuttered heavy exits (e.g. Home -> Library via 'see all');
   opacity + translate + scale stay, so every transition is composited + smooth
   and the exit matches the entrance. */
@keyframes page-fade-out {
  from { opacity: 1; transform: translateY(0)    scale(1); }
  to   { opacity: 0; transform: translateY(-6px) scale(.994); }
}

/* The OUTGOING screen sits on top and fades out to reveal the incoming page
   sitting still beneath it (which carries .no-page-anim, added in render()) —
   a single-element dissolve, so nothing shifts. */
#app.is-transitioning .phone-leaving > .screen.is-leaving {
  z-index: 2;
}

/* ===========================================================
   02-layout.css
   LAYOUT — bottom nav (.nav*) and the centered 'view status' pill.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* BOTTOM NAV */
/* The nav is position:absolute so it pins to the bottom of .screen
   (which is position:absolute; inset:0) regardless of content height.
   Pages that render a bottom-nav must add padding-bottom:var(--nav-h)
   to their scroll-content so content isn't hidden behind the nav.
   Horizontal insets cover landscape notched-side phones. */
.bottom-nav {
  /* Surface color is exposed as a custom property so the composite */
  --nav-bg: rgba(11,17,28,0.98);    /* matches --bg-0 (#0B111C) so the nav blends into the canvas, near-opaque so scrolling content doesn't bleed through the blur (was rgba(4,9,18,0.98) = the old #040912) */
  position: absolute; bottom: 0; left: 0; right: 0;
  background: var(--nav-bg);
  backdrop-filter: blur(28px);
  -webkit-backdrop-filter: blur(28px);
  border-top: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-around;
  padding:
    var(--s-2h)
    max(0px, var(--safe-right))
    calc(10px + max(var(--safe-bottom), var(--s-3)))
    max(0px, var(--safe-left));
  z-index: var(--z-overlay);
  /* Responsive: on wide viewports the nav spans the full phone width */
  width: 100%;
}
/* Soft top scrim — scrolling content dissolves into the bar instead of meeting
   a hard edge. Same canvas color (--bg-0) fading up to transparent. */
.bottom-nav::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  bottom: 100%;
  height: var(--s-6);
  background: linear-gradient(to top, var(--bg-0), transparent);
  pointer-events: none;
}
.nav-item {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center;
  /* Equal thirds so the middle tab (Library) sits at true viewport center.
     Without this, space-around distributes gaps around unequal-width labels
     ("Home" < "Profile"), nudging Library left of 50%. */
  flex: 1 1 0;
  gap: var(--s-1);
  cursor: pointer;
  padding: var(--s-1h) var(--s-4h) var(--s-1);
  /* Guaranteed 48 × 48 hit target (Material Design / WCAG 2.5.5 AAA).
     Without min-* the dimensions came from the icon + label + padding
     (~49px tall, label-width-dependent horizontally — solidly above
     iOS 44 but right on the Material 48 line). Explicit 48-floor
     removes the dependency on label length and stops the vertical
     from sitting on the standard. */
  min-width: 48px;
  min-height: 48px;
  border: none;
  background: none;
  color: var(--text-3);
  transition: color var(--dur-state) var(--ease-ios);
  position: relative;
}
.nav-item.active { color: var(--sage); }
.nav-item svg {
  /* Bumped 22 → 24 (2026-05-31) — aligns with the Material Design /
     iOS Tab Bar standard for bottom-nav glyphs. */
  width: 24px; height: 24px;
  stroke-width: 1.6;
  fill: none;
}
/* Escape hatch for icons that ARE solid-filled (e.g. Material Symbols
   'air' on the Library tab). The companion .ic-fill class on the <svg>
   beats the fill:none above so the glyph renders solid. Stroke still
   gets none by default since stroke-width is harmless on filled paths. */
.nav-item svg.ic-fill {
  fill: currentColor;
}
.nav-label {
  font-size: var(--fs-micro); font-weight: 600;
  letter-spacing: 1.2px; text-transform: uppercase;
}
/* Active tab is carried by the sage icon + label color shift alone — the
   underline indicator was dropped (2026-06-20) as redundant (less is more). */

.top-bar {
  display: flex; align-items: center; justify-content: space-between;
  padding:
    calc(18px + var(--safe-top))
    max(var(--gutter), var(--safe-right))
    var(--s-4h)
    max(var(--gutter), var(--safe-left));
  flex-shrink: 0;
  gap: var(--s-3);
}
.top-bar-title {
  font-weight: 600;
  color: var(--text);
  letter-spacing: -.1px;
  flex: 1;
  text-align: center;
}
/* ─── CANONICAL ICON BUTTON ──────────────────────────────────────
   The circular chrome icon button shared by the back chevron, gear,
   close/exit, favorite heart, and calendar nav. Default 40px; the
   .icon-btn--sm (32) / --lg (44) modifiers cover the other footprints.
   Color drives the glyph via currentColor, so the cyan hover glow
   recolors the icon for free. The reminders bell-master toggle
   (.rem-bell-master) IS based on this too — it adds only its on/off
   state coloring on top (sage glow when reminders are active, dimmed +
   struck when paused), so the on/off state stays obvious to the user. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  /* Bumped 40 → 44 (2026-05-31) to clear the iOS HIG 44 × 44 hit-target
     minimum (and the WCAG 2.5.5 AAA floor). Every back-chevron / close-X
     / settings-gear / edit pencil in the app rides this class, so the
     bump is the highest-leverage a11y win in one rule. */
  width: 44px; height: 44px;
  border-radius: 50%;
  /* Glassy raised disc (user 2026-08-05, superseding the 2026-05-29 flat
     transparent ring): a frosted top-lit white-alpha gradient gives volume,
     the single hairline border is the edge, and --elev-1 lifts it off the navy.
     (An inset top sheen was dropped — on a circle it rendered as a second ring
     just inside the border, reading as a double border. user 2026-08-05.)
     Light theme gets its own glass treatment in light.css. */
  background: linear-gradient(180deg, var(--white-mist), var(--white-tint));
  border: 1px solid var(--white-line);
  box-shadow: var(--elev-1);
  /* Glyph brightness bumped --text-2 → --text (2026-05-31) — the back
     chevron + close X read as muted secondary text at --text-2, but
     they ARE the primary topbar action; users were missing them. Now
     they sit at the same brightness as the topbar title beside them. */
  color: var(--text);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    background   var(--dur-state) var(--ease-ios),
    border-color var(--dur-state) var(--ease-ios),
    box-shadow   var(--dur-state) var(--ease-ios),
    color        var(--dur-state) var(--ease-ios),
    transform    var(--dur-tap)   var(--ease-ios);
}
/* position: relative so any --sm variant can anchor its 44×44
   invisible hit-extender via ::after (the base 44×44 button doesn't
   need one — its visible chrome IS the hit area). */
.icon-btn { position: relative; }
.icon-btn svg { width: 17px; height: 17px; stroke: currentColor; stroke-width: 1.6; fill: none; }
/* Invisible touch-target — bumps the --sm variant's hit area from
   32×32 to the WCAG 2.5.5 / iOS HIG 44×44 floor without changing the
   visible disc. Pseudo-element catches taps anywhere in the larger
   bounding box; the click bubbles to the parent <button>. */
.icon-btn--sm::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 44px; height: 44px;
}
/* Unified cyan hover glow (matches .rem-bell-master.is-on); currentColor
   carries the recolor down to the glyph. */
.icon-btn:hover { background: var(--sage-90); border-color: var(--sage-270); box-shadow: 0 0 12px var(--sage-170); color: var(--sage); }
.icon-btn:active { transform: scale(.94); }
/* Size modifiers */
.icon-btn--sm { width: 32px; height: 32px; }
.icon-btn--sm svg { width: 16px; height: 16px; }
/* .icon-btn--lg (44px) was used solely by .home-recent-gear, which was
   removed in the 2026-05-29 home flatten. Rule deleted with it. */
/* Invisible 40px placeholder that balances a top-bar against its icon-btn
   (same width) when one side has no button. Replaces inline width:40px. */
.topbar-spacer { width: 40px; }
/* .topbar-spacer--icon-btn retired 2026-06-03 — its only consumer
   (the reminders sheet) flipped the title to text-align:left so the
   balancing placeholder is no longer needed. */

/* Center "view status" pill — the small uppercase chip that names the
   current screen state (e.g. STATISTICS, PRACTICE). Flat surface,
   subtle border, tertiary text — never a CTA. */
.pill-label {
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: var(--s-2) var(--s-4);
  display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--fs-label); font-weight: 600;
  color: var(--text);
  letter-spacing: 1.2px;
  text-transform: uppercase;
  cursor: pointer;
}
.pill-label .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--sage);
  box-shadow: 0 0 8px var(--sage);
}

/* ===========================================================
   03-primitives.css
   PRIMITIVES — reusable UI atoms: .card (glass) .btn .toggle .slider progress bar .section-label .badge.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* The base .card primitive (heavy glass: white-veil fill + hairline +
   backdrop-blur + multi-stop shadow) was retired with the flat-design
   pass. Every surface that composed .card has been rewritten to live
   directly on the root background; the History page was the last
   consumer. Keep this comment as a forwarding note so future readers
   know NOT to reintroduce the class — use the flat row recipes
   (.row-flat-list, .tech-row, .settings-row) instead. */

/* ── Skip link (a11y) ────────────────────────────────────────────
   WCAG 2.4.1 'Bypass Blocks'. Visually hidden until focused, then
   it pops to the top-left so a keyboard user can hit Tab → Enter
   to jump past the status bar / top bar / page chrome straight to
   the main content. Target id 'main-content' is stamped on the
   active .screen by _finalizeRender in main.js. */
.skip-link {
  position: fixed;
  top: var(--s-2);
  left: var(--s-2);
  /* Off-screen by default, no visual presence. clip + clip-path is the
     belt-and-braces approach screen readers all support. */
  clip-path: inset(100%);
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
  overflow: hidden;
  white-space: nowrap;
  z-index: var(--z-overlay);
  background: var(--bg-1);
  color: var(--sage);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--sage);
  border-radius: var(--r-md);
  text-decoration: none;
  font-family: var(--font-body);
  font-weight: 600;
}
.skip-link:focus,
.skip-link:focus-visible {
  /* Restore presence — back to normal flow + click area. */
  clip-path: none;
  clip: auto;
  height: auto;
  width: auto;
  overflow: visible;
}

/* Sage icon-tile fill — the repeated inline background:var(--sage-soft);
   color:var(--sage) on row/option icon tiles. Added as a class so the tint
   is one source instead of N inline copies. Glyph color flows to the svg
   via currentColor. */
.tile-sage { background: var(--sage-soft); color: var(--sage); }
/* .tile-moon (rgba(125,142,176,.10) / --moon) removed 2026-06-04 — its only
   consumer was the standalone wisdom sheet's row-icon, which went out with
   the rest of that sheet (renderWisdomSheet, #wisdom-sheet, S.wisdomSheet).
   If another moon-tinted tile site appears later, reintroduce alongside
   tile-sage as a pair — the .row-icon comment block still documents this
   tile-* family pattern. */

/* .see-all-link — the small sage caption link in section headers ("See all").
   Shared type/color; each context keeps its own gap/align/hover alongside. */
.see-all-link {
  display: inline-flex;
  align-items: center;
  background: none; border: none; padding: 0;
  font-family: var(--font-body);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .see-all-link is a button — lh:1.45 would grow its height */
  font-weight: 600;
  color: var(--sage);
  letter-spacing: .2px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity var(--dur-tap) ease;
}
/* Unified hover — the opacity fade (the profile variant the user preferred). */
.see-all-link:hover { opacity: .8; }

/* .field-input — the canonical text input (Save-preset name, Edit-profile
   name/email, reminder name). 2% fill, hairline border, --r-md, sage focus.
   font-size is --fs-md (16px) so focusing never triggers iOS auto-zoom.
   (Search boxes are a separate pattern — not migrated.) */
.field-input {
  width: 100%;
  box-sizing: border-box;
  padding: var(--s-3) var(--s-3h);
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-md); /* lint-skip CAT-T4: .field-input — input element; lh:1.4 would change input field height */
  outline: none;
  -webkit-appearance: none; appearance: none;
  transition: border-color var(--dur-state) ease, background var(--dur-state) ease;
}
/* Single clean focus stroke: the field's sage border + a hugging 2px sage ring
   read as ONE crisp outline. Overrides the global input:focus-visible outline
   (99-utilities) that otherwise stacked a second, offset stroke on top. */
.field-input:focus,
.field-input:focus-visible { border-color: var(--sage); outline: none; box-shadow: 0 0 0 2px var(--sage); }
.field-input::placeholder { color: var(--text-3); }
/* Auth-preview inputs brighten the placeholder for outdoor / low-brightness
   legibility (user 2026-08-07). Preview-scoped via .pv-input. */
.pv-input::placeholder { color: #94A3B8; } /* lint-skip CAT-16: preview medium-slate, no matching token */

/* iOS Safari / Chrome paints autofill-eligible inputs with a yellow
   (and Chrome with a pale-blue) background that we can't override
   via `background:` directly — the engine uses an internal style.
   The standard workaround is an enormous inset box-shadow that
   visually covers the engine's fill. We also redirect the text color
   (Safari forces black-on-yellow otherwise) so the typed text stays
   readable on the navy canvas. The 5000s transition is a fixed-point
   hack: it pushes the automatic background-color animation so far
   into the future that the user never sees the unstyled flash.
   Applies to every .field-input in the app (onboarding name,
   reminders label, edit-profile name + email, …). */
.field-input:-webkit-autofill,
.field-input:-webkit-autofill:hover,
.field-input:-webkit-autofill:focus,
.field-input:-webkit-autofill:active {
  -webkit-box-shadow: 0 0 0 100px var(--surface-soft) inset;
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
  transition: background-color 5000s ease-in-out 0s;
}

/* .tab-underline — the underline tab-bar button: transparent chrome,
   --text-3 resting / --text-2 hover, with a 2px sage indicator bar (rounded +
   soft glow) under the .active tab. Shared skeleton for the two underline tab
   rows; each companion keeps ONLY its own padding, letter-spacing, active
   color, and indicator offset:
     .section-tab     — community feed tabs (Feed / Friends / Compete): roomier
        padding, active --text, indicator at bottom:-1px so it straddles the
        container's divider.
     .compete-sub-tab — Compete sub-nav (Leaderboard / Challenges): tighter
        padding, active --sage, indicator flush at bottom:0 (no divider).
   The container rows (.section-tabs / .compete-sub-tabs) stay bespoke — they
   share only display:flex, too thin to componentize. */
.tab-underline {
  position: relative;
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--text-3);
  font-family: var(--font-body);
  font-size: var(--fs-body); /* lint-skip CAT-T1: tab-button label — body-text lh:1.5 would grow button height */
  font-weight: 600;
  cursor: pointer;
  transition: color var(--dur-state) var(--ease-ios);
}
.tab-underline:hover:not(.active) { color: var(--text-2); }
.tab-underline.active::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  bottom: 0;
  height: 2px;
  background: var(--sage);
  border-radius: 2px;
  box-shadow: 0 0 6px var(--sage-glow);
}

/* .chip — the RESTING surface shared by every selectable chip/pill: 2% fill,
   hairline border, --text-2 caption type, plus the universal scale(.97) tap.
   Each companion keeps its own layout (row-fill vs inline rail), radius,
   weight, tracking, hover, and its selected-state rule — those genuinely
   differ. The selected look (sage-soft fill + sage border/text) is NOT folded
   here on purpose: the three chips toggle three different state classes
   (.modal-dur-pill.is-selected / .sound-v2-chip.is-on / .m-amb-chip.on, the last
   two driven by hot-path DOM patches + a flat filter-rail variant), so a
   shared selected rule would mean renaming classes across the JS. */
.chip {
  background: var(--chip-fill);   /* subtle blue-grey chip fill (was var(--surface-soft)) */
  border: 1px solid var(--border);
  color: var(--text-2);
  font-family: var(--font-body);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .chip is a button — lh:1.45 would change chip height */
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.chip:active { transform: scale(.97); }

/* BUTTONS */
/* .btn-reset — strips <button>'s native chrome so a button element
   composed with any non-button class (e.g. .settings-row, .goal-card,
   .tech-item) looks identical to the <div> version it replaces.
   Use this when you want native button semantics (Enter/Space, role,
   focus-visible) on a surface whose visual treatment lives in another
   class. Added 2026-06-05 with the Tier 3 button-vs-div conversion
   sweep — see pages.js for the 12 row-shaped surfaces that compose it.
   Companion class supplies the visible look. */
.btn-reset {
  /* Strip browser defaults */
  border: 0;
  background: transparent;
  /* Inherit text context — the companion class can override if needed */
  color: inherit;
  font: inherit;
  /* Native <button> text-aligns center; row-shaped surfaces flow LTR */
  text-align: left;
  /* Companion classes typically use display:flex which fills the parent;
     width:100% covers the case where they don't and still want a
     full-width row. */
  width: 100%;
  /* No padding/margin of our own — companion class controls them. */
  padding: 0;
  margin: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.btn-primary {
  width: 100%; padding: var(--s-4);
  background: var(--sage);
  color: var(--on-sage);
  /* Canonical primary-CTA type per design system §5.01:
     700 / 12px / uppercase / 1.5px tracking. Was 2px — pulled back to
     1.5px so every sage-filled CTA (Start session, Begin session,
     Continue, Done) tracks identically to the status-pill and
     section-label scale used everywhere else. */
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .btn-primary uppercase tracked button label, not body caption */ font-weight: 700;
  letter-spacing: 1.5px; text-transform: uppercase;
  line-height: 1;
  border: none; border-radius: var(--r-pill);
  cursor: pointer; transition: transform var(--dur-state) var(--ease-ios);
  font-family: var(--font-body);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
}
.btn-primary svg { width: 14px; height: 14px; flex-shrink: 0; }
.btn-primary:hover { transform: translateY(-1px); }
.btn-primary:active { transform: translateY(0); transition: all var(--dur-tap) var(--ease-ios); }
/* Disabled state — added 2026-06-05 with the Reminders sheet's
   schedule-required guard. Mutes the sage fill and removes the press
   affordance so the CTA visibly signals 'something upstream isn't
   ready yet'. The disabled attribute already blocks the click event in
   modern browsers; the rule here is purely visual. */
.btn-primary:disabled,
.btn-primary[aria-disabled="true"] {
  opacity: .42;
  cursor: not-allowed;
  transform: none;
}
.btn-primary:disabled:hover,
.btn-primary[aria-disabled="true"]:hover {
  transform: none;
}

/* Secondary / ghost CTA — outlined sage text on transparent.
   PILOT: migrated onto the state-layer system. The veil (::after, from
   the .state-target primitive below) now owns the hover/focus/press FILL,
   reading from --state-* tokens. .btn-ghost keeps the two cues the veil
   does NOT provide: the border brighten-to-sage on hover, and the subtle
   scale press. The old `background: var(--sage-tint)` hover fill is removed
   so the surface isn't tinted twice. --state-radius is set to the pill
   radius so the veil's corners match the button. */
.btn-ghost {
  width: 100%; padding: var(--s-4);
  background: transparent;
  color: var(--sage);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .btn-ghost uppercase tracked button label, not body caption */ font-weight: 700;
  letter-spacing: 1.5px; text-transform: uppercase;
  border: 1px solid var(--border-warm);
  border-radius: var(--r-pill);
  --state-radius: var(--r-pill);
  cursor: pointer; transition: border-color var(--dur-state) var(--ease-ios);
  font-family: var(--font-body);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
}
.btn-ghost:hover { border-color: var(--sage); }
.btn-ghost:active { transform: scale(.985); transition: transform var(--dur-tap) var(--ease-ios); }
.btn-ghost svg { width: 15px; height: 15px; stroke: currentColor; stroke-width: 1.7; fill: none; }

/* .session-finish-btn — the compact end-of-session pill (timer + hold
   finish): warm hairline outline, sage uppercase label. Composes
   .button-label--sm in markup for the full uppercase-tracked-button
   type role at 11px. Companion keeps only the chrome. */
.session-finish-btn {
  background: transparent;
  border: 1px solid var(--border-warm);
  color: var(--sage);
  padding: var(--s-2) var(--s-4h);
  border-radius: var(--r-pill);
  cursor: pointer;
}

/* TOGGLE · design-system §5.06
   2026-06-03 — converged with .ps-tog (modals.css) so the Settings
   page + Reminders sheet toggles read identically to the tune sheet
   and Wisdom sheet toggles. Before this commit:
     .toggle  was 44×26, pip 20×20, off-state border, harsh pip shadow
     .ps-tog  was 40×24, pip 18×18, no off-border, --elev-1 pip shadow
   The smaller / borderless ps-tog dimensions are the modern iOS-ish
   choice and the user converged on them across every other surface,
   so this rule now matches. Reminders + Settings auto-pick the
   unified look because they all source .toggle from the shared tog()
   / togVisual() helpers in ui.js. */
.toggle {
  width: 40px; height: 24px; border-radius: 999px;
  border: none; cursor: pointer;
  background: var(--white-line);
  transition: background var(--dur-state) var(--ease-ios);
  position: relative; flex-shrink: 0;
  padding: 0;
  display: inline-block;
}
.toggle.on  {
  background: var(--sage);
  /* 2026-06-02 — drop-shadow glow removed. The cyan halo around active
     toggles read as legacy/web-y; modern premium UI lets the solid
     fill carry the weight against the dark canvas. */
}
.toggle.off {
  background: var(--white-line);
  box-shadow: none;
  /* 2026-06-03 — off-state border removed during the .ps-tog
     convergence. The track-vs-canvas contrast carries enough weight
     without an extra hairline; the border also competed with the
     pip's shadow at the seam edges. */
}
.toggle::after {
  content: ''; position: absolute; top: 3px;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--white);
  transition: left var(--dur-state) var(--ease-ios),
              transform var(--dur-tap);
  box-shadow: var(--elev-1);
}
.toggle.on::after  { left: 19px; }   /* 40 - 18 - 3 = 19 */
.toggle.off::after { left: 3px; }
/* Pip squish on tap — same .92 scale the .ps-tog uses on press so the
   tactile-feedback affordance matches across surfaces. */
.toggle:active::after { transform: scale(.92); }

/* SLIDER */
.slider {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 4px; border-radius: 2px;
  background: var(--card-2);
  outline: none; cursor: pointer;
}
.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  /* DS §5.05 — 24px sage disc + translucent halo. The halo is a
     box-shadow ring (not a glow blur) so the knob keeps a generous
     tap target on small screens while reading as sage, not white. */
  width: 24px; height: 24px;
  border-radius: 50%; background: var(--sage);
  box-shadow: 0 0 0 5px var(--sage-130), 0 0 14px var(--sage-glow-45);
  cursor: pointer; transition: transform var(--dur-tap) var(--ease-ios);
}
.slider::-webkit-slider-thumb:active { transform: scale(1.18); }
/* Firefox parity — same 24px sage disc + halo. */
.slider::-moz-range-thumb {
  width: 24px; height: 24px; border: none;
  border-radius: 50%; background: var(--sage);
  box-shadow: 0 0 0 5px var(--sage-130), 0 0 14px var(--sage-glow-45);
  cursor: pointer; transition: transform var(--dur-tap) var(--ease-ios);
}
.slider::-moz-range-thumb:active { transform: scale(1.18); }

/* PROGRESS BAR */
.progress-bar-wrap {
  width: 100%; height: 5px;
  background: var(--card-2);
  border-radius: 999px; overflow: hidden;
}

/* UX POLISH: Page-nav progress bar.
   #page-progress is the routed-page bar (mounted by render()); the
   .page-progress-bar class carries the identical treatment for page-like
   overlays that aren't real routes (the reminders sheet's #reminders-progress),
   so both flash the same way via flashPageProgress(). */
#page-progress,
.page-progress-bar {
  position: absolute;
  top: 0; left: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--sage-400), var(--sage), var(--white-vivid), var(--sage));
  background-size: 200% auto;
  border-radius: 0 1px 1px 0;
  z-index: var(--z-top);
  width: 0%;
  opacity: 0;
  transition: width var(--dur-card) var(--ease-standard), opacity var(--dur-state);
  box-shadow: 0 0 8px var(--sage);
  animation: shimmer 1.2s linear infinite;
}
#page-progress.active,
.page-progress-bar.active {
  opacity: 1;
}
#page-progress.done,
.page-progress-bar.done {
  width: 100% !important;
  opacity: 0;
  transition: width var(--dur-state) ease, opacity var(--dur-page) ease .2s;
}

/* SECTION LABEL */
/* .section-label — the canonical uppercase eyebrow / section mini-label.
   Base is the --fs-mini (10px) tier; --lg (11px) and --xs (9px) are the only
   intentional size tiers. Weight is 600 and tracking is per-tier (1.5px base
   /--xs, 2px --lg) — this normalizes the old per-screen drift (4 sizes, 600/700
   mix, 1.2–2.5px trackings). Forks that adopted this keep ONLY their own margin
   /padding/position and any non-standard color (sage accent, --text2 lighter). */
.section-label {
  font-size: var(--fs-mini); font-weight: 600;
  letter-spacing: 1.5px;
  color: var(--text-3);
  text-transform: uppercase;
  margin-bottom: var(--s-3h);
}
.section-label--lg { font-size: var(--fs-label); letter-spacing: 2px; }   /* 11px — section headers */
.section-label--xs { font-size: var(--fs-micro); }                         /* 9px — dense captions */

/* SECTION HEAD · the header row that holds a section label on the left and a
   see-all link / count / metric on the right. Consolidates 7+ byte-similar
   copies (.home-v2-section-head, .modal-section-head-big, .shelf-head,
   .stats-mastery-head, .stats-mile-head, .comm-streak-head, .prof-ach-header,
   .live-rail-header). Companions keep ONLY their own margin (+ gap/line-height
   /position). --baseline aligns to the text baseline (titles with trailing
   metrics); the modal big-value head overrides to flex-end on its own class.
   .hist-week-head is excluded — it left-aligns (justify-content: flex-start). */
.section-head { display: flex; align-items: center; justify-content: space-between; }
.section-head--baseline { align-items: baseline; }

/* PAGE TITLE · canonical destination-screen title.
   Consolidates three previously byte-identical definitions
   (.stats-screen-title / .hist-screen-title / .comm-screen-title, formerly
   in home.css) into one primitive. The shared typography lives here; the
   two layout contexts are expressed as modifiers so rendering is unchanged:
     --flush  title sits flush in .scroll-content (Stats). Owns top+bottom
              margin so it spaces itself from the status bar and first card.
     --bar    title sits inside a .top-bar flex row (History, Community).
              Zero margin, flex:1, left-aligned — the bar owns the padding.
   This is the destination-title grammar ONLY. (The home greeting
   header that used to live here — .home-v2-title — was removed
   2026-05-30 with the "Ready to breathe" header.) */
.page-title {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 500;   /* 2026-07-22 (user): lighter, calmer page headers app-wide (was 700) */
  letter-spacing: -.4px;
  color: var(--text);
  line-height: 1;
  margin: 0;
}
/* .page-title--flush removed (2026-05-29) — the only consumer was
   the Statistics page's in-body title, which moved up into the
   topBar() and now uses .page-title--bar instead. */
.page-title--bar   { flex: 1; text-align: left; }

/* BODY TEXT · the canonical 14px paragraph / row-text tier.
   The most ad-hoc-duplicated type style in the codebase (96 sites before this
   class existed). Apply to inline text in cards, list rows, descriptions,
   subtitles — anywhere body-sized prose appears. Bundles ONLY the typography
   triplet (--fs-body / --lh-body / --ls-body) so the cascade leaves color,
   weight, family, and margins to the component class. font-family inherits
   from html,body (--font-body) and font-weight inherits from body (400);
   component classes routinely override color (--text vs --text-2) and weight
   (500/600/700 for emphasized rows). This intentional minimalism is what lets
   ~244 of the 276 ad-hoc font-size declarations cleanly drop their type-spec
   block and keep only their context-specific overrides. */
.body-text {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
}

/* CAPTION · the canonical 12px supporting-text tier.
   The single biggest type-spec cluster in the codebase (148 ad-hoc sites
   before this class existed). Apply to: helper text under inputs, sublabels
   under row titles (.modal-row-sub / .settings-row-sub / .post-action), meta
   text (post timestamps, friend meta, milestone units), small button labels.
   Same minimal contract as .body-text — only the typography triplet; color
   and weight stay on the component class. Guarded by lint-css.py CAT-T2:
   any bare `font-size: var(--fs-caption)` outside this rule body fails strict
   unless it carries an inline lint-skip CAT-T2 trailing-comment.

   IMPORTANT (2026-05-31): never write a literal slash-star…star-slash
   pair inside this doc block. CSS doesn't allow nested comments — the
   inner star-slash closes the outer comment early, leaving the
   following text (and the .caption rule that follows it) as syntax
   garbage. That had silently broken .caption for every consumer in the
   app until it was spotted via the .achieve-name labels rendering at
   the inherited 16px instead of 12px. */
.caption {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  letter-spacing: var(--ls-caption);
}

/* HERO TITLE · the section-hero text-title tier, with size variants for the
   three steps the design system uses for major in-screen titles:
     .hero-title          → --fs-h1     32px  (.pc-hero-title, .h1-display)
     .hero-title--lg      → --fs-h2-lg  28px  (.cust-hd-title)
     .hero-title--md      → --fs-h2-md  26px  (.prof-name--v3)
   Bigger than .page-title (24px route-title) — these are screen-anchoring
   hero/section titles, not nav titles. The numeric-display and
   avatar-initial uses of the same tokens stay on their component classes;
   .hero-title is reserved for actual prose titles. Guarded by CAT-T3. */
.hero-title {
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
  letter-spacing: var(--ls-h1);
}
.hero-title--lg {
  font-size: var(--fs-h2-lg);
  line-height: var(--lh-h2-lg);
  letter-spacing: var(--ls-h2-lg);
}
.hero-title--md {
  font-size: var(--fs-h2-md);
  line-height: var(--lh-h2-md);
  letter-spacing: var(--ls-h2-md);
}

/* HERO NUMERAL · the canonical big-numeric-display tier.
   Pairs with the .hero-title family on the same scale steps (h1 / h2-lg /
   h2-md) but for numbers, not prose. Adds font-variant-numeric: tabular-nums
   so columns of digits line up. Use for: stat values, wheel-picker numerals,
   timer/duration readouts, streak numbers, volume readouts. Same minimal
   contract — type triplet + tabular-nums; color/weight/family stay on the
   component class. Guarded by CAT-T3 (shared with .hero-title — both
   classes are canonical homes for the same heading tokens). */
.hero-numeral,
.hero-numeral--lg {
  font-variant-numeric: tabular-nums;
}
.hero-numeral {
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
  letter-spacing: var(--ls-h1);
}
.hero-numeral--lg {
  font-size: var(--fs-h2-lg);
  line-height: var(--lh-h2-lg);
  letter-spacing: var(--ls-h2-lg);
}
/* .hero-numeral--md size step removed (2026-05-29) — its only
   consumer was the Sound sheet volume display ("70%"), which was
   dropped with the slider. .hero-numeral (h1) and .hero-numeral--lg
   (h2-lg) remain for the day-streak / hold readouts. */

/* ════════════════════════════════════════════════════════════════════
   METRIC DISPLAY · the canonical "primary metric value" role.
   ────────────────────────────────────────────────────────────────────
   19px (--fs-h3) / display font / weight 600 / tabular-nums /
   tight ls / lh:1 / primary ivory text. Used for at-a-glance numeric
   readouts that anchor a row or a cell — durations, lifetime counts,
   prices — that need to read as confident headline values without
   reaching the h2 hero numeral tier.

   Consumers (2026-05-29 promotion):
     • .hist-rec-dur       (History row duration "12s")
     • .stats-life-val     (Personal Bests cells "0" / "0" / "0" / "—")
     • .sub-hero-price     (Subscription "$59.99" — keeps a sage color
                            companion since this metric reads as price,
                            not a neutral count)

   Color variant (.metric-display--sage) handles the cyan accent
   (used by .sub-hero-price on the Subscription page). The neutral
   base is --text ivory. An amber variant existed until 2026-05-31
   when the Stats Best-Hold cell was removed — re-add the rule if a
   new amber metric surface appears.

   NOT migrated (different tiers — keep for now):
     • .stat-val (Session Complete) — h1 32px, weight 500 (XL tier)
     • .stats-mile-now / .stats-chart-foot-val — 16px --fs-md (sm tier)
   A future commit could add .metric-display--lg / --sm if the
   constellation grows. */
.metric-display {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  letter-spacing: -.3px;
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.metric-display--sage  { color: var(--sage);  }

/* ════════════════════════════════════════════════════════════════════
   BUTTON LABEL · the canonical "uppercase tracked button text" tier.
   ────────────────────────────────────────────────────────────────────
   The most ad-hoc-duplicated type pattern in the codebase: every
   sage-CTA, every outline pill, every section-foot link respec'd
   the same 6-property combo (family / size / weight / tracking /
   uppercase / lh:1) inline, each carrying a CAT-T2 lint-skip.

   .button-label--sm = 11px (--fs-label) — compact uppercase pills (EDIT,
   Finish, Unlock, Add to Home Screen, session-finish-btn). --font-body /
   weight 600 / uppercase / tight tracking / lh:1 (no leading — the button's
   padding owns vertical rhythm). Color comes from the companion class.

   (The 12px .button-label base tier was removed 2026-07-19 — its only user,
   the "VIEW SESSION HISTORY" button, is gone. Re-add it if a mid-size uppercase
   CTA ever returns.)

   .btn-primary (filled CTA) stays separate — its weight 700 + 1.5px tracking
   is intentionally heavier so the primary action reads with more presence. */
.button-label--sm {
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  line-height: 1;
}

/* ════════════════════════════════════════════════════════════════════
   ROLE CLASSES · opinionated, color+weight+size baked together.
   Sit alongside the TIER classes above — different jobs:
     tier classes (.body-text/.caption/.lead-text/.hero-title) = size only
     role classes (.sub-text/.row-title/.page-subtitle)        = role-complete
   The role classes encode the proven recipes that have stabilized after
   real-app iteration — apply one and you get the whole correct treatment
   (size + line-height + letter-spacing + color + weight) without having
   to remember the four-decision combo per component.
   ════════════════════════════════════════════════════════════════════ */

/* SUB-TEXT · the canonical row-sublabel role.
   The recipe is the reminders pattern, now applied app-wide:
     font-size:       12px (--fs-caption)
     line-height:     1.45 (--lh-caption, no override — same as caption tier)
     letter-spacing:  .1px
     color:           --text-3
     font-weight:     400
   The only exception is tune/sound + sub-sheets which override size to 11px
   via .modal-row-sub (denser sheet-row context). */
.sub-text {
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  letter-spacing: .1px;
  /* --text-3 (~10% of title brightness) is intentionally below the
     contrast minimum: the dim color does the whole hierarchy job, so
     the title's optical lead is unmistakable even though title + sub
     share the same 400 weight. Lifting sub brightness exposes the
     weight tie and the two start competing. */
  color: var(--text-3);
  font-weight: 400;
}

/* ════════════════════════════════════════════════════════════════════
   16px DUO · picking between .row-title and .lead-text
   ────────────────────────────────────────────────────────────────────
   Both classes below set font-size: var(--fs-md) (16px) — they're at
   the same size. The distinction is BEHAVIORAL, not visual:

       .row-title    is a ROLE class (16/500/--text/lh:1.25/ls:-.1)
                     — color + weight baked in, tight line-height for
                     single-line emphasis in compact row contexts.
                     Pairs with .sub-text. Picks itself in markup.

       .lead-text    is a TIER class (size + lh + ls ONLY)
                     — color, weight, family deferred to the companion
                     class. Looser line-height (1.4) for wrapping
                     paragraphs and chart/modal headings. Composes in
                     markup; needs a companion to specify color/weight.

   Which to pick:
     ✓ List row titles, profile/settings/library rows → .row-title
     ✓ Chart titles, modal section heads, hero "emphasized body" → .lead-text
     ✓ Anything with prose that WRAPS to two lines → .lead-text (1.4 lh)
     ✓ Anything that stays one line tight against a sub → .row-title (1.25 lh)

   If you find yourself writing a companion that overrides .row-title's
   color/weight you probably wanted .lead-text instead. If you find
   yourself writing a companion that sets the color AND weight AND
   line-height for a row title you probably wanted .row-title.
   ════════════════════════════════════════════════════════════════════ */

/* ROW-TITLE · row-title ROLE class (16/500/--text/lh:1.25/ls:-.1).
   Pairs with .sub-text in standard list rows. Weight settled at 500
   (Medium) 2026-05-29 to unify line weights across all main views
   (Home, Library, Settings, Profile, Stats, Subscription, History).
   Sheet-internal rows still use .body-text / .modal-row-title at
   14px — not affected by this rule. */
.row-title {
  font-size: var(--fs-md);
  line-height: 1.25;
  letter-spacing: -.1px;
  color: var(--text);
  font-weight: 500;
}

/* PAGE-SUBTITLE · "wrapping body subtitle under a hero title" role
   (12px / text-3 / lh:1.5). Caps at the app-wide subtitle max (12px)
   and reads as a readable wrapping paragraph, not a tight inline meta
   string. */
.page-subtitle {
  font-size: var(--fs-caption);
  line-height: 1.5;
  letter-spacing: 0;
  color: var(--text-3);
  font-weight: 400;
}

/* LEAD TEXT · 16px TIER class — the "body↔h3 bridge."
   Sits between .body-text (14px) and .sheet-title (19px). Sized only;
   color/weight stay on the companion class. Apply to tile titles
   (.tech-name, .home-row-title, .goal-title), chart titles
   (.stats-chart-title), modal headings that aren't .sheet-title, and
   any "emphasized body" prose that needs the 1.4 line-height for
   wrapping. Guarded by CAT-T4. See the "16px DUO" block above for
   how this differs from .row-title. */
.lead-text {
  font-size: var(--fs-md);
  line-height: var(--lh-md);
  letter-spacing: var(--ls-md);
}

/* EMPTY STATE · the centered "no content yet" panel.
   Consolidates the byte-identical .comm-empty / .lib-empty recipes (and the
   centered base of .diag-empty). Compose .empty-state with a
   per-context class that supplies ONLY padding (it varies per screen). The
   optional children are the bare 32px glyph, the body-size message, and a sage
   text-action (was .comm-empty-clear / .lib-empty-reset). The richer card
   variant (.hist-empty: 48px circle tile + title/sub) is intentionally NOT
   folded here — it's a different structure. */
.empty-state { text-align: center; color: var(--text-3); }
.empty-state-icon {
  width: 32px; height: 32px;
  margin: 0 auto var(--s-2h);          /* 10px */
  color: var(--text-3);
  opacity: .7;
}
/* .empty-state-msg composes .body-text in markup for the size. */
.empty-state-action {
  margin-top: var(--s-3);              /* 12px */
  background: none; border: none;
  color: var(--sage);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .empty-state-action is a button — lh:1.45 would grow its height */
  cursor: pointer;
  font-family: var(--font-body);
}

/* BADGE */
.badge {
  display: inline-flex; align-items: center;
  padding: 3px var(--s-2h); border-radius: 999px;
  font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: .8px;
}
.badge-sage   { background: var(--sage-110); color: var(--sage); }

:root {
  --orb-inhale-dur: 1.8s;
  --orb-exhale-dur: 1.5s;
  --orb-hold-dur:   4s;
  --orb-color:      var(--sage);
  --orb-color-rgb:  125,211,232;
}

@keyframes orb-inhale  { from { transform: scale(0.80); } to { transform: scale(1.12); } }
@keyframes orb-exhale  { from { transform: scale(1.12); } to { transform: scale(0.80); } }

/* HOLD is intentionally static — anchor for the eyes during retention. */
.orb-inner.phase-INHALE { animation: orb-inhale var(--orb-inhale-dur, 4s) ease-in-out forwards; }
.orb-inner.phase-HOLD   { animation: none;       transform: scale(1.12); }
.orb-inner.phase-EXHALE { animation: orb-exhale var(--orb-exhale-dur, 8s) ease-in-out forwards; }

/* ═══════════════════════════════════════════════════════════════
   STATE-LAYER PRIMITIVE  ·  OPT-IN, additive, isolated
   ───────────────────────────────────────────────────────────────
   Reads the --state-* tokens from 00-settings.css. This is the
   canonical interaction-feedback surface for NEW components.

   DESIGN DECISION — opt-in, not automatic:
   A bare global `:disabled` / `:hover` rule would silently restyle
   every existing control (.cal-nav:disabled, .sound-v2-*.is-disabled,
   .reminder-row.disabled, the home .sound-v2-cue-row.is-disabled, every
   native button). That is the opposite of "isolated for safe review."
   So feedback is scoped to the .state-target opt-in class. Existing
   components are untouched until explicitly migrated in a later,
   approved pass — including everything on the home screen.

   USAGE:
     <button class="state-target"> … </button>
   The element must be position:relative-able (the layer is an
   ::after overlay). Set --state-radius to match the host's radius so
   the veil's corners align; defaults to inherit-friendly 0.

   INK (required decision, per the Wave-0 convention in 00-settings):
   NEUTRAL surfaces (navy rows, cards, the Sound row) MUST set
   --state-ink:255,255,255 — a sage veil on navy reads as a color cast.
   Leave the sage default ONLY on genuinely-sage/accent surfaces.

   PRESS SCALE (optional companion): the veil alone now presses at 0.20
   (Wave-0), enough for neutral rows. A host that also wants the M3-style
   depression adds, on its own :active:
     transform: scale(var(--state-press-scale));
     transition: transform var(--dur-tap) var(--ease-ios);
   The veil and the scale are independent — use either or both.
   ─────────────────────────────────────────────────────────────── */
.state-target {
  position: relative;
  /* host opts a corner radius in so the overlay matches; harmless at 0 */
  --state-radius: 0px;
}

/* The veil itself — a single compositing layer, opacity-animated.
   Pointer-events:none so it never eats the host's clicks. Uses the
   in-band --dur-state token + the workhorse ease, matching the rest
   of the interaction motion system. */
.state-target::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--state-radius, 0px);
  background: rgb(var(--state-ink));
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-state) var(--ease-standard);
  /* Sits BEHIND the host's own content (text/icons render at the default
     level 0) but in front of the parent canvas. This is why content stays
     fully legible while the veil tints the surface. Hosts with an opaque
     fill of their own should instead give content an explicit z-index;
     for transparent/translucent surfaces (the common case) --z-behind is
     correct and needs nothing from the host. */
  z-index: var(--z-behind);
}

/* Resting → hover → focus → press. Each step reads its own token, so
   re-tuning the whole app's feedback intensity is a one-line change. */
@media (hover: hover) {
  .state-target:hover::after { opacity: var(--state-hover); }
}
.state-target:focus-visible::after { opacity: var(--state-focus); }
.state-target:active::after        { opacity: var(--state-press); }
.state-target[data-dragging]::after { opacity: var(--state-drag); }

/* Keyboard focus ring — aligned with the existing form-field convention
   in 99-utilities.css (2px sage outline, 2px offset, 3px sage-soft ring)
   so focus looks identical everywhere. */
.state-target:focus-visible {
  outline: 2px solid var(--sage);
  outline-offset: 2px;
}

/* DISABLED — scoped to .state-target only. Two-opacity model:
   content (the element's own text/icon) knocks back to the legibility
   floor; an inert surface veil is suppressed. Covers both the native
   :disabled attribute and the .is-disabled class convention already
   used elsewhere in the app, so either works on an opted-in element. */
.state-target:disabled,
.state-target.is-disabled {
  opacity: var(--state-disabled-content);
  cursor: not-allowed;
  pointer-events: none;     /* no hover/press feedback while inert */
}
.state-target:disabled::after,
.state-target.is-disabled::after { opacity: 0; }

/* Respect reduced motion — the veil still appears, it just doesn't
   animate. Consistent with the app-wide reduced-motion policy. */
@media (prefers-reduced-motion: reduce) {
  .state-target::after { transition: none; }
}

/* ===========================================================
   breathing.css
   COMPONENT · BREATHING — orb stage, glow rings, phase label, timer chrome, cycle counter.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* Orb stage: positions the orb and its three concentric glow rings. */
.orb-stage {
  position: relative;
  width: 280px;
  height: 280px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Rings breathe WITH the phase — a peripheral breathing guide, not an ambient
   loop. Driven by the SAME --orb-*-dur the orb uses (mirrored onto .orb-stage so the
   rings, which are siblings of the orb, can read them; the phase class is patched onto
   .orb-stage alongside the orb in updateOrbAnimation). Inhale: bloom outward + brighten.
   Exhale: draw in + fade. HOLD / retention: FREEZE, held open + bright (mirrors the orb
   anchoring the eyes still). PUMP: hold steady (a calm anchor, not a frantic ripple over
   ~0.6s pumps). Reduced-motion is covered by the global 0.001ms animation kill-switch. */
@keyframes ring-inhale {
  from { transform: scale(.92);  opacity: calc(var(--ring-opacity, .35) * .28); }
  to   { transform: scale(1.05); opacity: var(--ring-opacity, .35); }
}
@keyframes ring-exhale {
  from { transform: scale(1.05); opacity: var(--ring-opacity, .35); }
  to   { transform: scale(.92);  opacity: calc(var(--ring-opacity, .35) * .28); }
}
.glow-ring {
  position: absolute;
  border-radius: 50%;
  border: 1px solid var(--orb-color);
  pointer-events: none;
  transition: border-color .5s ease;
  /* Settled baseline until a phase class drives it (first paint / between sessions). */
  transform: scale(1);
  opacity: var(--ring-opacity, .35);
  will-change: transform, opacity;
}
/* Innermost ring is brightest. --ring-opacity is each ring's baseline; the keyframes
   ramp toward/away from it so the whole field breathes at three depths. */
.glow-ring.r1 { width: 320px; height: 320px; --ring-opacity: .55; }
.glow-ring.r2 { width: 355px; height: 355px; --ring-opacity: .28; }
.glow-ring.r3 { width: 390px; height: 390px; --ring-opacity: .12; }

/* Phase-driven — class lives on .orb-stage (patched with the orb). */
.orb-stage.phase-INHALE .glow-ring { animation: ring-inhale var(--orb-inhale-dur, 4s) ease-in-out forwards; }
.orb-stage.phase-EXHALE .glow-ring { animation: ring-exhale var(--orb-exhale-dur, 8s) ease-in-out forwards; }
.orb-stage.phase-HOLD   .glow-ring { animation: none; transform: scale(1.05); opacity: var(--ring-opacity, .35); }
.orb-stage.phase-PUMP   .glow-ring { animation: none; transform: scale(1);    opacity: var(--ring-opacity, .35); }

/* PAUSE / PRE-START FREEZE — hold the orb + rings at their CURRENT frame while the
   timer isn't running. Two callers add .is-frozen: pauseTimer (freeze mid-breath so
   resume CONTINUES instead of restarting at scale(.80) — the "orb + numerals shrink"
   jump on play/pause) and pgTimer's pre-start markup (the orb carries phase-INHALE,
   so without this it auto-plays its expand on mount and, animation:forwards, sits
   big — then the first play restarts it small: the "first inhale" snap-down jump).
   Stripped by updateOrbAnimation on play, so the animation runs from there.
   The phase classes are included ON PURPOSE: the .orb-inner.phase-INHALE/EXHALE
   animation SHORTHANDS reset animation-play-state to running, so this longhand must
   out-specify them (0,3,0 > 0,2,0) to win regardless of cascade order. HOLD/PUMP
   are animation:none, so there's nothing to freeze there. */
.orb-inner.phase-INHALE.is-frozen,
.orb-inner.phase-EXHALE.is-frozen,
.orb-stage.phase-INHALE.is-frozen .glow-ring,
.orb-stage.phase-EXHALE.is-frozen .glow-ring { animation-play-state: paused; }

/* The orb body itself: dark glass center, soft inner + outer glow
   tinted by --orb-color. Sits on top of the three rings.
   2026-06-02 — children are now absolutely positioned so the BIG
   numeral (#t-num) always lands at the GEOMETRIC center of the orb,
   regardless of what sits below it. The previous flex layout centered
   the whole stack {numeral, unit, hint} as a group — which pushed the
   numeral above orb-center when the unit + hint were visible (most
   noticeable during RETENTION when all three render). Per user
   feedback "breath count — is it right in the center of the orb?" */
.orb-inner {
  position: relative;
  width: 280px;
  height: 280px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(14,24,36,.95) 60%, transparent 100%);
  box-shadow:
    0 0 60px rgba(var(--orb-color-rgb), .18) inset,
    0 0 40px rgba(var(--orb-color-rgb), .10);
  transition: box-shadow .5s ease;
}
.orb-inner > #t-num {
  position: absolute;
  top: 50%; left: 50%;
  /* translateZ(0) promotes the numeral to its OWN compositing layer so a
     digit swap (textContent change) repaints in isolation. Without it, iOS
     Safari repaints the numeral on the orb's shared layer WHILE the orb is
     mid scale-animation (orb-inhale/orb-exhale) and leaves the previous digit
     ghosted behind the new one — "two digits overlapping" as the count ticks.
     backface-visibility:hidden kills the residual sub-pixel halo. Non-visual:
     position/size/scale are unchanged; this only isolates the repaint. */
  transform: translate(-50%, -50%) translateZ(0);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  margin: 0;
  text-align: center;
  /* ONE numeral size for every technique + every mode — breath number, per-second
     countdown, and retention M:SS alike (2026-07-19). Moved off the old inline style
     (which set 60/88px dynamically) so it's token-based; the landscape / short-portrait
     media queries still resize it via their .countdown-numeral !important overrides. */
  font-size: var(--fs-numeral-xl);      /* 72px */
  /* Regular, not Thin — a weight-200 hairline washed out at arm's length / in
     sunlight during a session (user 2026-08-04). 400 keeps the clean look but
     the digit stays legible at a glance. */
  font-weight: 400;
  line-height: var(--lh-numeral-xl);
  color: var(--white);
  letter-spacing: -2px;
}
.orb-inner > #t-unit {
  /* Sits just below the numeral's optical baseline. 56px clears the
     88px non-retention numeral's lower half (44) + a small gap; for
     the 60px retention numeral the gap reads slightly larger and
     that's fine — RETENTION already has the hint line below to
     re-balance the stack visually. */
  position: absolute;
  top: calc(50% + 56px);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  text-align: center;
  white-space: nowrap;
}
.orb-inner > #t-hint {
  position: absolute;
  top: calc(50% + 84px);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  text-align: center;
  white-space: nowrap;
}

.orb-hint {
  font-size: var(--fs-micro);
  color: var(--orb-color);
  letter-spacing: 1.2px;
  min-height: 11px;
  transition: color var(--dur-page) ease;
}

/* 2026-06-02 — .orb-breath-line removed. Power Breath + Custom Session
   now render the BREATH NUMBER (just '4') in the big #t-num numeral
   itself (60px to match retention M:SS), so a separate small caption
   line above it would be redundant and visually noisy. The element
   used to live here; now there's nothing to style. */

/* Timer phase label (INHALE / EXHALE / HOLD / RETENTION) */
.timer-phase-label {
  margin: var(--s-5) 0 0;
  font-family: var(--font-body);
  font-size: var(--fs-h2);
  font-weight: 600;
  letter-spacing: 6px;
  /* text-indent == letter-spacing: uppercase tracking adds a trailing 6px after
     the last glyph, and text-align:center counts it, nudging the visible text
     LEFT of true center (~half the tracking). A matching leading indent balances
     it so the glyphs sit dead-center. (user spotted on RECOVERY HOLD / EXHALE) */
  text-indent: 6px;
  text-transform: uppercase;
  line-height: 1;
  text-align: center;
  color: var(--orb-color);
  text-shadow: 0 0 18px rgba(var(--orb-color-rgb), .35);
  transition: color var(--dur-page) ease, text-shadow var(--dur-page) ease;
}
/* 2026-06-02 — phase label was relocated to the top slot inside
   .timer-progress (was below the orb). In that context the var(--s-5)
   top margin would push it down by ~22px, leaving an unwanted gap;
   pull it tight to the top-bar instead. */
.timer-progress .timer-phase-label { margin-top: 0; }
/* The round counter moved INTO the orb area in place of the old
   phase label. Add the same top breathing room the phase label
   used to have so the counter sits where the eye expects. */
.timer-orb-area .timer-round { margin-top: var(--s-5); }
/* Long-label variant used for Power Breath protocol phases —
   'INHALE HOLD' (11), 'RECOVERY HOLD' (13), 'PREPARE FOR THE NEXT ROUND'
   (26). At the default h2 + 6px tracking these would overflow a phone
   viewport; the modifier shrinks the type and tightens the tracking
   while keeping the same amber color from --orb-color so the phase
   read stays consistent. JS toggles .is-protocol on #t-phase whenever
   S.protocolPhase is set. */
.timer-phase-label.is-protocol {
  font-size: var(--fs-md); /* lint-skip CAT-T4: per-state label size, not body */
  letter-spacing: 2px;
  text-indent: 2px;   /* balance the 2px trailing tracking (see base rule) */
}

/* Timer chrome: technique caption + cycle text + play button */
/* The caption is absolutely centred (below), so its top-bar needs to be
   the positioning context. Scoped to .timer-screen so no other page's
   top-bar layout is touched. */
.timer-screen .top-bar { position: relative; }

/* Technique caption in the session top-bar. The bar is a flex row with
   a 40px back button on the left and a wider "FINISH" pill on the right,
   so a caption left in normal flow gets shoved off-centre (it centres
   between the two unequal slots, not on the screen). We pin it to the
   true horizontal centre with absolute positioning so it sits on the
   SAME vertical axis as the breathing orb and the play button below —
   one strict centre line down the whole screen. pointer-events:none so
   it never intercepts taps meant for the buttons it overlaps. */
.timer-tech-caption {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  max-width: 60%;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  font-size: var(--fs-caption); /* lint-skip CAT-T2: uppercase tracked practice-screen title at 12px — not a .caption */
  font-weight: 600;
  color: var(--text-2);   /* cool muted grey — quiet chrome above the orb */
  letter-spacing: 1.2px;
  text-transform: uppercase;
}

/* ── Timer-screen chrome — extracted from inline styles (pgTimer) ──
   Token-based so the design-token lints stay clean. Off-scale snaps:
   Finish-btn pad 9px→8px (--s-2), orb area pad 20px→22px (--s-5). */
/* Finish pill now uses the shared .session-finish-btn (03-primitives.css). */
/* Phase label centered in the band between the top bar and the orb
   (2026-06-10). Everything below the bar is absolutely positioned (orb at
   screen 50%, round counter + play btn bottom-anchored), so .timer-progress
   and .timer-orb-area are pure flow spacers — splitting them 1:4 lands the
   label in the visual gap above the orb, responsively (no magic px). */
.timer-progress {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--s-1h) 0 0;
}
/* .timer-progress-label retired 2026-06-02 (SESSION PROGRESS eyebrow
   removed when the phase label took the top slot). */
.timer-round { font-family: var(--font-body); font-size: var(--fs-h2); font-weight: 600; }
/* .timer-round-unit removed 2026-08-05 — the "LEFT" suffix after the X:XX
   session-time readout was dropped (user); the class is now unused. */
/* Round-progress pips — replaced the 'Round N / M' text (2026-06-22, built by
   constants.roundIndicatorHTML). One capsule per round: completed dim, the
   current one bright amber (the retention/hold accent, matches the HOLD label),
   upcoming faint. The active pip widens slightly so progress reads at a glance
   without any number. */
.round-pips { display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2); }
.round-pip {
  width: 16px;
  height: 5px;
  border-radius: var(--r-pill);
  background: var(--text-3);
  opacity: .3;
  transition: opacity var(--dur-state) var(--ease-out-soft),
              width var(--dur-state) var(--ease-out-soft),
              background-color var(--dur-state) var(--ease-out-soft);
}
/* Completed rounds fill with the amber accent (recessive vs the current
   round); the active pip is full-strength + wider; upcoming stay faint grey. */
.round-pip--done   { background: var(--amber); opacity: .55; }
.round-pip--active { background: var(--amber); opacity: 1; width: 26px; }
.round-pip--todo   { background: var(--text-3); opacity: .3; }
/* 2026-06-02 — orb area restructured so the ORB itself sits at the
   vertical center of the viewport (was sitting ~50px above center
   because the flex column centered the whole stack of {orb, cycle
   text, round counter, play button}). New layout:
     • .orb-stage is absolutely centered on the area's mid-axis.
     • Cycle text + round counter cascade directly below the orb
       (no floating gap — they hug the orb's bottom edge).
     • Play button is pinned near the bottom of the screen, out of
       the orb's balance equation.
   This gives the user the "orb at screen center" they asked for
   without leaving an empty band of space above it.                */
/* .timer-orb-area stays flex: 1 (fills the column space) but is NOT
   set to position: relative — we want absolute children of it to
   resolve against the .screen wrapper (position: absolute from
   01-base.css), so 50% means viewport-center, not area-center. */
.timer-orb-area {
  flex: 4;   /* see .timer-progress — biases the flow split so the phase label lands in the gap above the orb, not at screen-center */
}
/* Orb anchored to the SCREEN center, not the area center. */
.timer-screen .orb-stage {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
/* Cycle text now stacks just ABOVE the round / time-left readout (both bottom-
   anchored) instead of orb-anchored. Orb-anchored (top:50% + 140px) overlapped
   the bottom-anchored .timer-round on shorter viewports — "cycle and time-left
   on one line". Bottom-stacking guarantees they never collide on any screen.
   bottom = .timer-round bottom + its line height + a gap. */
.timer-screen .timer-meta-row {
  position: absolute;
  /* One baseline in the gap between the orb's bottom and the play button —
     the round's old anchor. Cycle line + round/time now sit on this row. */
  bottom: calc(var(--s-6) + 72px + var(--s-5));
  left: 50%;
  top: auto;
  transform: translateX(-50%);
  margin: 0;
  /* Stacked (user 2026-08-14): the breath count sits ABOVE the round pips, both
     centred in one column. bottom-anchored, so the extra line grows UP into the
     orb gap rather than pushing the play button. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-1h);
}
/* Round counter is anchored ABOVE the play button (bottom-based
   positioning, not orb-based) so it always lands cleanly in the
   gap between orb-bottom and play-button-top, regardless of
   viewport height. Iterated 2026-06-02:
     • First pass used top: 50% + translate(140 + 180px) — measured
       from the orb's vertical center. Worked on tall phones, but on
       shorter viewports the offset could collide with the play button.
     • Second pass made it tiny ("more visible" / "stay in center"
       per user feedback). Reverted to a confident size and moved it
       INTO the gap between orb and play button.
   Sizing: --fs-h3 (18) + weight 600 + --text reads as supporting
   info that's actually legible — not a competing headline like the
   first --fs-h2 attempt, but not a near-invisible 14px body either. */
/* Cycle + round share one muted, secondary type inside .timer-meta-row —
   de-emphasized (was fs-h3 / 600 / near-black, which out-weighed the thin hero
   digit and inverted the hierarchy) so the orb numeral stays the clear hero. */
.timer-screen .timer-meta-row .timer-cycle-val,
.timer-screen .timer-meta-row .timer-round {
  position: static;
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-md); /* lint-skip CAT-T4: secondary meta-row value, not a .lead-text block */
  font-weight: 500;
  color: var(--text-2);
  letter-spacing: .2px;
}
/* The "·" inline divider was dropped 2026-08-14 — the cycle line + round pips now
   stack vertically (see .timer-meta-row), so no horizontal separator is needed. */
.timer-meta-row #t-breath:empty { display: none; }
/* Play button anchored near the bottom of the viewport, out of the
   orb-balance equation. */
.timer-screen #t-playbtn {
  position: absolute;
  bottom: var(--s-6);
  left: 50%;
  transform: translateX(-50%);
  /* Promote to a stable compositor layer so the border-radius:50% mask is baked
     into the layer's backing store. Without this, iOS Safari re-rasterizes the
     transformed button WITHOUT the rounded clip whenever its child icons toggle
     (running↔paused, e.g. when the session stops for the WISDOM outro), leaving
     a SQUARE. Desktop never drops the mask; this is iOS-only insurance. */
  will-change: transform;
}
.timer-unit-label {
  font-size: var(--fs-micro);
  color: var(--text-3);
  letter-spacing: 1.5px;
  margin-top: var(--s-1h);
}

/* Cycle counter — plain centered text under the orb (pill removed).
   Extra top margin clears the orb's outer glow rings (which extend to
   ~390px) so the text reads as sitting cleanly below the orb, strictly
   on the column's center axis. */
/* (.timer-cycle-text retired 2026-08-03 — the cycle line moved into the shared
   .timer-meta-row baseline; positioning now lives on .timer-screen .timer-meta-row.) */
.timer-cycle-lbl {
  font-size: var(--fs-micro);
  color: var(--text-3);
  font-weight: 600;
  letter-spacing: 1.2px;
  text-transform: uppercase;
}
/* .timer-cycle-val composes .body-text in markup for the size. */
.timer-cycle-val {
  font-weight: 500;
  color: var(--text);
  letter-spacing: .2px;
}

.timer-play-btn {
  width: 72px;
  height: 72px;
  /* Sits inside the centered column now; top margin gives the phase
     label and button their own breathing room and keeps the
     cycle-text → phase-label → button stack on one rhythm. */
  margin-top: var(--s-6);
  border-radius: 50%;
  background: var(--sage);
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 6px 20px rgba(0,0,0,.35);
  transition: transform var(--dur-tap) var(--ease-ios);
  /* 2026-06-02 — tap hygiene. Without these, iOS Safari produced two
     visible glitches the user reported as "messing up a bit":
       • The system grey tap-highlight flashed over the sage circle.
       • A 300ms double-tap-to-zoom delay made the press feel sluggish
         and could re-fire if the user tapped again before the delay.
       • Selecting the icon during the press changed text-cursor mid-tap.
     Together they kill the glitch entirely. */
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
}
/* 2026-06-02 — the original :active transform: scale(.96) overrode
   the .timer-screen #t-playbtn translateX(-50%) used to horizontally
   center the absolutely-positioned button, so on tap the button
   jumped right. Compose both transforms so the press shrinks IN
   PLACE. */
.timer-play-btn:active { transform: translateX(-50%) scale(.96); }
/* 'Skip' control during the post-session WISDOM reflection — an understated text
   button. Absolutely anchored JUST ABOVE the bottom-pinned play button (which is
   position:absolute; bottom var(--s-6); 72px tall) — in normal flow it landed
   mid-screen where the now-blank round line sits, so it read as missing. Hidden
   via [hidden] until the reflection opens (engine reveals it). Generous tap
   padding keeps it a comfortable touch target despite the small type. */
.wisdom-skip {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--s-6) + 72px + var(--s-4));   /* play-btn bottom + its height + gap */
  padding: var(--s-2) var(--s-4);
  background: transparent;
  border: 0;
  color: var(--text-3);
  letter-spacing: 1px;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--dur-state) var(--ease-ios);
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.wisdom-skip:hover,
.wisdom-skip:focus-visible { color: var(--text-2); }
.wisdom-skip:active { transform: translateX(-50%) scale(.96); opacity: .6; }
.wisdom-skip[hidden] { display: none; }
.timer-play-btn svg { fill: var(--on-sage); pointer-events: none; }
/* Both icons are rendered once; pause/play is toggled via the .is-paused class
   rather than swapping innerHTML. iOS Safari dropped the border-radius:50% mask
   on this transformed, layered button whenever its content was mutated, leaving
   a SQUARE after a pause/resume tap. A class toggle never touches the content,
   so the rounded layer is never invalidated. */
.timer-play-btn .t-ico { display: flex; align-items: center; justify-content: center; }
.timer-play-btn .t-ico-play { display: none; }            /* running → show pause */
.timer-play-btn.is-paused .t-ico-pause { display: none; }
.timer-play-btn.is-paused .t-ico-play  { display: flex; }

.seg-ctrl {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}
.seg-btn {
  flex: 1;
  padding: var(--s-1h) var(--s-3h);
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-3);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: .5px;
  cursor: pointer;
  transition: color var(--dur-state) var(--ease-ios), background var(--dur-state) var(--ease-ios), border-color var(--dur-state) var(--ease-ios);
}
.seg-btn:hover:not(.is-selected) { color: var(--text-2); }
.seg-btn:active:not(.is-selected) { transform: scale(.96); transition: transform var(--dur-tap) var(--ease-ios); }
.seg-btn.is-selected {
  /* 2026-06-02 — bumped active styling for redundant visibility:
     • background went from sage-50 (~8% opacity) to a higher
       opacity sage tint so the selected pill reads as a clear
       chip even without the border. The user reported the
       Power Breath protocol pill not appearing highlighted despite
       the underlying state being correct (headless verification
       confirmed the class WAS applied with proper computed style —
       cache must have been stale). Brighter fill + brighter border
       eliminates any ambiguity even on partial cache updates.
     • border-color stays at full-opacity var(--sage). */
  background: rgba(125, 211, 232, 0.18);
  color: var(--sage);
  border-color: var(--sage);
  font-weight: 700;
}

.section-tabs {
  display: flex;
  gap: var(--s-6);
  padding: 0 var(--s-1);
  margin-bottom: var(--s-4h);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
/* Underline-tab skeleton (transparent chrome, hover, sage indicator) from
   .tab-underline; below are only the bits unique to the feed tabs. */
.section-tab {
  padding: var(--s-2h) 0 var(--s-3);
  letter-spacing: .1px;
}
.section-tab.active { color: var(--text); }
.section-tab.active::after { bottom: -1px; }  /* straddle the container divider */

/* ===========================================================
   lists.css
   COMPONENT · LISTS — session-complete stat row, technique list item, row-list container, scroll content, session controls, hold phase, countdown numeral.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* STAT ROW (session complete) */
.stat-row {
  display: flex; align-items: stretch;
  gap: 1px; background: var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden; margin: var(--s-4h) 0;
}
.stat-col {
  flex: 1; background: var(--surface-soft);
  padding: var(--s-4) var(--s-2h);
  display: flex; flex-direction: column;
  align-items: center; gap: var(--s-1);
}
.stat-val {
  font-family: var(--font-display);
  font-size: var(--fs-h1); font-weight: 500; /* lint-skip CAT-T3: sizes the numeric stat-val display, not a body title */
  letter-spacing: -.5px; line-height: 1;
}
/* Time-segmented stat value (e.g. "1m 22s"). */
.stat-val-time {
  display: inline-flex;
  align-items: baseline;
  justify-content: center;
  white-space: nowrap;
  line-height: 1;
  letter-spacing: -.5px;
}
.stat-val-num {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: -.5px;
  line-height: 1;
  color: inherit;
}
.stat-val-unit {
  font-family: var(--font-display);
  font-size: var(--fs-md); /* lint-skip CAT-T4: .stat-val-unit base; the session.css lean compound overrides it for the lean variant */
  font-weight: 500;
  margin-left: 2px;
  color: var(--text-2);
  line-height: 1;
}
.stat-val-num-sec { margin-left: var(--s-1h); }
.stat-lbl {
  font-size: var(--fs-micro); color: var(--text-3);
  font-weight: 600; letter-spacing: 1.2px;
  text-transform: uppercase;
}

/* ROUND ROW */

/* TECH LIST ITEM */
/* Recent technique row — FLAT treatment. Was a hero card with
   surface fill, border, radius and a soft shadow; now sits
   directly on the root navy background like every other Library
   row. A bottom hairline keeps it separated from the chip rail /
   All Techniques list below. Hover recolors the icon; no chrome
   to swap any more. */
.tech-item {
  display: flex; align-items: center; gap: var(--s-4);
  /* Vertical padding from the shared --row-pad-y token (8px = var(--s-2)).
     The Library Recent row was the first consumer — Profile + Settings
     list-row classes migrate to the same token as they're touched, so
     the rhythm across list surfaces stays unified from one source. */
  padding: var(--row-pad-y) 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  margin-bottom: 0;
  cursor: pointer;
  transition: all var(--dur-state) var(--ease-ios);
}
/* — Icon chip — squircle, sage-tinted fill, sage-bordered outline. */
.tech-icon-wrap {
  /* Bare line icon — no tinted box (onboarding look). Category color +
     soft glow, matching the Home icons. */
  width: 48px; height: 48px;
  border-radius: var(--r-lg);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: var(--tint, var(--sage));
}
.tech-icon-wrap svg {
  width: 28px; height: 28px;
  stroke-width: 1.7; fill: none;
  color: var(--tint, var(--sage));
  filter: drop-shadow(0 0 8px rgba(var(--tint-rgb, 125,211,232), .5));
}
.tech-info { flex: 1; min-width: 0; }    /* min-width:0 lets long names truncate cleanly */
/* .tech-name composes .row-title in markup; only the bottom-gap to the
   sub stays. */
.tech-name { margin-bottom: var(--s-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* .tech-sub composes .sub-text in markup; companion brightens color
   --text-3 → --text-2 to match the main-view row-sub treatment. */
.tech-sub { color: var(--text-2); }

/* .tech-chevron + its hover variants removed (2026-05-29) — the
   trailing chevron on the Library Recent card and All Techniques
   rows was dropped at the user's direction. Tap affordance now
   relies on the row's hover veil. */

/* ROW LIST CONTAINER (Library · All Techniques) — flat.
   No surface fill, no border, no radius — rows sit directly on
   the root navy background. The .tech-row + .tech-row hairline
   below still gives each pair an edge, so the rows read as a
   list without a wrapping box. */
.tech-list {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
}
/* Library results header — extra vertical breathing room around the eyebrow
   (chip rail → RESULTS → first row), per the zen-rhythm pass. Scoped with the
   --lib modifier so Community's shared .section-label-row is untouched. */
.section-label-row--lib {
  margin-top: var(--s-3);       /* gap below the category chip rail */
  margin-bottom: var(--s-4h);   /* more room before the first result row */
}
.tech-row {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  /* +2px each side (--s-4 → --s-4h) so adjacent rows breathe a touch more. */
  padding: var(--s-4h) 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  cursor: pointer;
  transition:
    background var(--dur-state) var(--ease-ios),
    transform var(--dur-tap) var(--ease-ios);
}
/* Hairline divider between adjacent rows — every .tech-row inside
   .tech-list gets a top border EXCEPT when it's the first row of
   the list. The wrinkle that needed fixing 2026-06-02: user-saved
   custom presets render wrapped in a .swipe-wrap container (so the
   swipe-to-delete pad can sit behind them). That breaks the old
   `.tech-row + .tech-row` adjacent-sibling selector — the two rows
   on either side of a wrapped preset aren't adjacent siblings at
   the DOM level, just visual neighbours. The descendant variant
   below catches the wrapped case too. */
.tech-list .tech-row {
  border-top: 1px solid var(--border);
}
.tech-list > .tech-row:first-child,
.tech-list > .swipe-wrap:first-child .tech-row {
  border-top: 0;
}
.tech-row:hover {
  background: var(--white-mist);
}
.tech-row:active {
  transform: scale(.99);
}
.tech-row:hover .tech-icon-wrap {
  border-color: var(--border-warm);
}

/* Icon-tile per-category tint now comes from --tint-rgb (set inline from
   CAT_TINT), matching .home-row-icon — the old fixed [data-cat] tints were
   removed so the Library and Home tiles render identically. */

/* 8-TECHNIQUE LIBRARY GRID (Home page) */

/* — Fabulous-style play button — circular, glowing, sage-on-deep-navy
   The signature look: solid mint disc, dark play glyph, soft halo. */

/* 38px visual + 3px each side = 44px hit area. WCAG 2.5.5 / Apple HIG.     */

   /* nudge play glyph for optical center */
/* "Custom Session" card variant — amber accent, dashed-border feel */

/* SCROLL CONTENT */
.scroll-content {
  flex: 1; overflow-y: auto; overflow-x: hidden;
  padding:
    0
    max(var(--gutter), var(--safe-right))
    /* +--s-5 (was --s-4): a touch more clearance over the absolute bottom-nav
       (~82px real height vs --nav-h 72) so the last row never grazes it. */
    calc(var(--nav-h) + var(--s-5) + var(--safe-bottom))
    max(var(--gutter), var(--safe-left));
  scrollbar-width: none;
}
.scroll-content::-webkit-scrollbar { display: none; }

/* Top-padding modifiers — replace per-page inline `style="padding-top:Npx"`
   on .scroll-content with named, reusable classes. The base rule sets
   padding-top:0; these add a small breathing gap below a .top-bar when a
   screen wants it (Library, Community, Safety). Two steps cover every
   current use; add more only if a real new value appears.
     --pad-sm  4px  (Community feed, Safety guidelines)
     --pad-md  8px  (Library — slightly more air above the search field) */
.scroll-content--pad-sm { padding-top: var(--s-1); }
.scroll-content--pad-md { padding-top: var(--s-2); }
.scroll-content--pad-lg { padding-top: var(--s-5); }

/* Drop the reserved bottom space when the page intentionally omits the
   bottom-nav. Used on drill-down screens accessed via the gear icon
   (Settings + its children — Safety Guidelines, etc.) so the back-chevron
   is the only escape hatch and the content can sit closer to the edge.
   Without this, .scroll-content's base padding-bottom (--nav-h + s-4 +
   safe-bottom) would leave a tall empty band where the nav used to be. */
.scroll-content--no-nav {
  padding-bottom: calc(var(--s-4) + var(--safe-bottom));
}

/* SESSION CONTROLS */
.session-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-6);
  padding:
    0
    max(16px, var(--safe-right))
    calc(38px + var(--safe-bottom))
    max(16px, var(--safe-left));
  flex-shrink: 0;
}

/* HOLD PHASE — breath-retention timer display.
   font-family was pinned to 'Inter' !important in 2026-04 as a guard
   against the (now retired) Cormorant brand serif and italic-cursive
   variants leaking in via inline styles or parent rules. With
   Google Sans the only display face today, the threat surface is
   gone — moved to var(--font-body) (which still falls back to Inter
   at the requested weight 300, since Google Sans ships only 400/
   500/700). The !important stays as belt-and-suspenders for any
   future inline-style override. */
.hold-time-display {
  font-family: var(--font-body) !important;
  font-size: var(--fs-numeral-xl); font-weight: 300;
  letter-spacing: -1.5px; color: var(--white);
  font-variant-numeric: tabular-nums;
  font-style: normal !important;
  line-height: 1;
}

/* ── COUNTDOWN NUMERAL — global guard ─────────────────────────────
   Every actively-counting numeral in the app (the orb's t-num and the
   breath-hold h-time) must render in a clean tabular-nums sans face.
   font-family is var(--font-body) (now 'Inter' → system sans; the old
   Google Sans face was dropped 2026-06-06), pinned !important as belt-
   and-suspenders against future inline-style overrides. font-style:
   normal !important is the original italic guard. Tabular-nums keeps
   the digits in fixed columns so the count doesn't twitch as digits
   change width. (Weights live at each call site: t-num 400, h-time 300.) */
.countdown-numeral {
  font-family: var(--font-body) !important;
  font-style: normal !important;
  font-variant-numeric: tabular-nums !important;
  font-feature-settings: 'tnum' 1;
}

/* ════════════════════════════════════════════════════════════════
   LIBRARY screen (pgLibrary) — extracted from inline styles.
   Off-scale snaps: empty pad 28px→32px (--s-6), empty msg 13px→14px
   (--fs-body), Unlock pill pad 7/13→8/14, Unlock font 10.5px→11px
   (--fs-label), bottom spacer 20px→22px (--s-5).
   ════════════════════════════════════════════════════════════════ */
/* Canonical search bar (Library + Community). Was two parallel rule
   sets (.lib-search-wrap + .comm-search-wrap, .lib-search-clear +
   .comm-search-clear, .lib-search-clear-icon + .comm-search-clear-icon)
   that were byte-for-byte identical; unified 2026-05-31 alongside the
   pages.js searchBar() helper. The shared .search-input + .search-icon
   rules already live in modals.css and weren't duplicated. */
.search-bar { position: relative; margin-bottom: var(--s-4h); }  /* 18px */
.search-clear {
  position: absolute;
  right: 14px; top: 50%;
  transform: translateY(-50%);
  background: none; border: none;
  color: var(--text-3);
  cursor: pointer;
  padding: var(--s-2);                 /* 8px */
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
}
.search-clear-icon { width: 14px; height: 14px; display: inline-flex; }

/* RECENT one-card shelf */
.lib-recent-item { margin-bottom: var(--s-4h); }   /* 18px */

/* Empty-search / empty-filter state — composes .empty-state; keeps only padding. */
.lib-empty { padding: var(--s-6) 0; }

/* Locked catalog rows — dimmed, amber sub-line. */
.tech-row.is-locked { opacity: .5; }
.tech-row.is-locked .tech-sub { color: var(--amber); }

/* "Unlock" outline pill (locked rows) — composes .button-label--sm
   in markup for the full uppercase-tracked-button type role.
   Companion keeps only the chrome (padding, border, color, layout). */
.pill-outline {
  padding: var(--s-2) var(--s-3h);
  border-radius: var(--r-pill);
  cursor: pointer;
  white-space: nowrap;
  background: transparent;
  border: 1px solid var(--border-warm);
  color: var(--text);
}

/* Saved-preset rows now delete via swipe-to-reveal-trash (.swipe-wrap /
   .swipe-del-pad, modals.css) — the old always-visible ✕ (.lib-preset-*) is
   gone; the row shows just the chevron like any other technique row. */

/* 2026-06-03 — height collapsed to 0. Bumped to var(--nav-h) earlier
   in the same session to fix a 'Tummo bleeding under the nav' bug
   report, but stacked on top of .scroll-content's base
   --nav-h + --s-4 + --safe-bottom padding-bottom, the resulting
   ~190px dead space below the last row read as too generous on
   subsequent audit. The base padding alone handles nav clearance;
   the spacer element is kept as a markup hook in case a future
   tweak needs it, but no longer contributes height. */
.lib-bottom-spacer { height: 0; }

/* ===========================================================
   community.css
   COMPONENT · COMMUNITY — feed post, live feed rail, filter/chip rails, library section header, leaderboard, XP bar.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* COMMUNITY POST (Feed) */
.post-card {
  /* Surface (2% fill + hairline + 18px) from .card; hover lifts the wash. */
  padding: var(--s-3h) var(--s-4);
  margin-bottom: var(--s-2h);
  transition: background var(--dur-state), border-color var(--dur-state);
}
.post-card:hover {
  background: var(--white-mist);
  border-color: var(--white-line);
}
.post-header {
  display: flex; align-items: center; gap: var(--s-2h);
  margin-bottom: var(--s-2);
}
.avatar {
  width: 38px; height: 38px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  font-family: var(--font-display);
  font-size: var(--fs-body); font-weight: 700; /* lint-skip CAT-T1: sizes the initial glyph inside the avatar circle, not body text */
  color: var(--bg-0);
  background: linear-gradient(135deg, var(--sage), var(--sage-400));
  letter-spacing: .3px;
}
.post-meta { flex: 1; min-width: 0; }
/* Name + timestamp on the SAME line, separated by a middle-dot.
   Username can truncate; time stays right-aligned and never wraps. */
.post-byline {
  display: flex; align-items: baseline; gap: var(--s-1h);
  line-height: 1.3;
  /* .post-byline composes .caption in markup; lh:1.3 overrides .caption's 1.45. */
}
.post-username {
  font-weight: 600;
  color: var(--white);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.post-time {
  font-size: var(--fs-label);
  color: var(--text-3);
  flex-shrink: 0;
  font-weight: 400;
}
.post-time::before {
  content: '·';
  margin-right: var(--s-1h);
  color: var(--text-3);
  opacity: .6;
}
/* .post-body composes .body-text in markup; lh:1.55 overrides body-text's 1.5 (posts breathe a bit looser). */
.post-body { color: var(--text-2); line-height: 1.55; margin-bottom: var(--s-2h); }
.post-actions { display: flex; gap: var(--s-4h); }
.post-action {
  display: flex; align-items: center; gap: var(--s-1h);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .post-action is a button — lh:1.45 would change button height */ color: var(--text-3);
  cursor: pointer; transition: color var(--dur-state);
  border: none; background: none; padding: 0;
  font-family: var(--font-body);
}
.post-action:hover, .post-action.liked { color: var(--sage); }

/* PREVIEW BANNER — honest labeling.
   The feed posts and the "Practicing Now" rail render seeded sample
   data (see ensureFeedSeed in pages.js); the real community backend is
   not built yet. This muted note keeps that explicit so sample activity
   is never mistaken for live users — required for App Store review and
   basic trust. Remove this banner when the feed is backed by real data. */
.comm-preview-note {
  display: flex; align-items: center; gap: var(--s-2h);
  margin-bottom: var(--s-4);
  padding: var(--s-2h) var(--s-3h);
  border-radius: var(--r-lg);
  background: var(--white-wash);
  border: 1px dashed var(--white-edge);
  color: var(--text-3);
  line-height: 1.4;
  /* .comm-preview-note composes .caption in markup; lh:1.4 overrides .caption's 1.45. */
}
.comm-preview-note svg {
  width: 15px; height: 15px;
  flex-shrink: 0;
  color: var(--text-2);
}
.comm-preview-note span { flex: 1; min-width: 0; }
.comm-preview-note b { color: var(--text-2); font-weight: 600; }
.comm-preview-dismiss {
  flex-shrink: 0;
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent;
  color: var(--text-3);
  border-radius: var(--r-circle);
  cursor: pointer;
  transition: color var(--dur-state) var(--ease-ios);
}
.comm-preview-dismiss:hover { color: var(--text-2); }
.comm-preview-dismiss svg { width: 14px; height: 14px; color: inherit; flex-shrink: 0; }

/* LIVE FEED RAIL */
/* Composes .section-head; keeps only its margin. */
.live-rail-header { margin-bottom: var(--s-2h); }
/* Type from .section-label (base 10px tier); keeps inline layout + lighter --text2.
   Zero section-label's bottom margin — this title is a flex item, not a stacked header. */
.live-rail-title {
  display: inline-flex; align-items: center; gap: var(--s-1h);
  color: var(--text-2);
  margin: 0;
}
.live-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--sage);
  position: relative;
  flex-shrink: 0;
}
.live-dot::after {
  content: '';
  position: absolute; inset: -3px;
  border-radius: 50%;
  background: var(--sage);
  opacity: .35;
  animation: livePulse 1.8s ease-out infinite;
}
@keyframes livePulse {
  0%   { transform: scale(.7); opacity: .45; }
  70%  { transform: scale(1.9); opacity: 0; }
  100% { transform: scale(1.9); opacity: 0; }
}
.live-rail-count {
  font-size: var(--fs-mini); color: var(--text-3);
  font-weight: 500;
  letter-spacing: .3px;
}
.live-rail {
  display: flex; gap: var(--s-4h);
  overflow-x: auto;
  /* Left pad is --s-4h (not --s-5) so the first AVATAR — centered 4px
     inside its 64px chip — lines up with the content gutter and the
     post cards below. */
  padding: var(--s-1h) var(--s-5) var(--s-1h) var(--s-4h);
  scroll-padding: 0 var(--s-4h);
  margin: 0 calc(-1 * var(--gutter)) var(--s-4h);
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  /* Right-edge fade — signals the row scrolls (swipe to reveal more). */
  -webkit-mask-image: linear-gradient(to right, black calc(100% - var(--s-6)), transparent);
  mask-image: linear-gradient(to right, black calc(100% - var(--s-6)), transparent);
}
.live-rail::-webkit-scrollbar { display: none; }
.live-rail { scrollbar-width: none; }
.live-chip {
  flex-shrink: 0;
  width: 56px;                                    /* was 64 — lighter visual weight */
  display: flex; flex-direction: column; align-items: center;
  gap: var(--s-2);
  scroll-snap-align: start;
  background: none; border: none; padding: 0;
  cursor: pointer;
  font-family: var(--font-body);
}
.live-avatar-wrap {
  position: relative;
  width: 48px; height: 48px;                      /* was 56 — pulls the row up ~14% */
}
/* Decorative no-op — kept so any stale markup doesn't paint a
   stray box. The avatar's chrome lives on .live-chip .avatar. */

/* Pulsing sage halo — ONLY for chips with .is-active, i.e. live
   sessions started within the last 5 minutes (set by the renderer
   in pages.js based on elapsedMin). */
@keyframes liveHaloPulse {
  0%, 100% { box-shadow: 0 0 0 1px var(--sage-170), 0 0 6px var(--sage-130); }
  50%      { box-shadow: 0 0 0 1px var(--sage-glow), 0 0 10px var(--sage-170); }
}
.live-chip .avatar {
  width: 48px; height: 48px;                      /* was 56 — synced with -wrap */
  font-size: var(--fs-body);                      /* was --fs-h3 — initial scales with the smaller disc */ /* lint-skip CAT-T1: sizes the initial glyph inside a 48px circle, not body text */
  font-weight: 700;
  color: var(--bg-0);
  /* Solid sage fill stays — identity is the initials, not the */
  background: var(--sage);
  box-shadow: 0 0 0 1px var(--white-mist);
}
/* Recent-cognition state — the only entries that earn the bright
   pulsing Sage ring + shadow token effect. */
.live-chip.is-active .avatar {
  animation: liveHaloPulse 3s ease-in-out infinite;
}
/* .live-chip-name composes .caption in markup for the size. */
.live-chip-name {
  font-weight: 600;
  color: var(--text);
  max-width: 56px;                                /* was 64 — matches new chip width */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  letter-spacing: .1px;
}
.live-chip-tech {
  font-size: var(--fs-mini);
  font-weight: 500;
  color: var(--text-3);
  letter-spacing: .2px;
  max-width: 56px;                                /* was 64 — matches new chip width */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Honor reduced-motion: stop the active-state halo pulse. The
   live-dot still loses its scale animation; its opacity remains
   so the "live" semantic survives without motion. */
@media (prefers-reduced-motion: reduce) {
  .live-chip.is-active .avatar { animation: none; }
  .live-dot::after { animation: none; opacity: .3; }
}

/* M-FILTER-RAIL · canonical chip rail */
.m-filter-rail {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  scroll-padding-left: var(--s-5);
  /* Pull the strip to the live screen edges so chips can scroll */
  margin: 0 calc(-1 * var(--gutter)) var(--s-4h);
  padding: 0 var(--s-5) var(--s-1) var(--s-5);
  /* Defensive: prevents the strip from being mis-measured by a
     flex/grid ancestor and from forcing horizontal page scroll. */
  max-width: 100vw;
  min-width: 0;
  /* Fade BOTH edges symmetrically (was right-only) — chips dissolve into the
     gutter on the left and right as the rail scrolls. The fade width matches
     the s-5 side padding so the first/last chip sits just past the fade at rest. */
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--s-5),
    #000 calc(100% - var(--s-5)),
    transparent 100%
  );
          mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--s-5),
    #000 calc(100% - var(--s-5)),
    transparent 100%
  );
}
.m-filter-rail::-webkit-scrollbar { display: none; }
.m-filter-rail { scrollbar-width: none; }

/* ── Search-owns-filter rule (Library only, 2026-06-04) ──────────
   When the user is searching, the text input owns the entire search
   logic. Pills hide so the user can't accidentally narrow a query
   to a category and get a confusing '0 results' state.
   Trigger: the search-input is focused, OR it has content (i.e.
   not its placeholder). Either condition collapses the pills.

   Scoped via :has(> .search-bar > .search-input ...) > .m-filter-rail
   — direct-child constraints on BOTH sides mean only the Library
   structure matches (scroll-content > searchBar + scroll-content >
   m-filter-rail as siblings). Community's friend-search and post-
   filter pills live in different parents so this doesn't touch
   them.

   :has support: iOS Safari 15.4+, Chrome 105+, Firefox 121+. Older
   browsers fall through to the pre-fix behavior (pills visible,
   no functional break), which is the right graceful degradation. */
.scroll-content:has(> .search-bar .search-input:focus) > .m-filter-rail,
.scroll-content:has(> .search-bar .search-input:not(:placeholder-shown)) > .m-filter-rail {
  display: none;
}

/* M-AMB-CHIP · individual chip */
/* Resting surface + tap-scale from .chip; below are the ambient/filter chip's
   own bits (rounded-rect radius, scroll-snap, box-shadow on .on). */
.m-amb-chip {
  flex-shrink: 0;
  scroll-snap-align: start;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1h);
  padding: var(--s-2) var(--s-3h);
  border-radius: var(--r-md);
  font-weight: 500;
  letter-spacing: .2px;
  white-space: nowrap;
  transition:
    background var(--dur-state) var(--ease-ios),
    border-color var(--dur-state) var(--ease-ios),
    color var(--dur-state) var(--ease-ios),
    box-shadow var(--dur-state) var(--ease-ios),
    transform var(--dur-tap) var(--ease-ios);
}
.m-amb-chip:hover {
  border-color: var(--white-line-2);
  color: var(--text-2);
}
.m-amb-chip svg {
  width: 13px;
  height: 13px;
  stroke: currentColor;
  stroke-width: 1.6;
  fill: none;
  flex-shrink: 0;
}
.m-amb-chip.on {
  background: var(--sage-soft);
  border-color: var(--sage);
  color: var(--sage);
  font-weight: 600;
  box-shadow: 0 0 18px var(--sage-130);
}
.m-amb-chip.on:hover {
  border-color: var(--sage);
  background: var(--sage-110);
  color: var(--sage);
}

/* COMMUNITY FILTER VARIANT · pill chips (2026-06-01 redesign)
   Previously this rail was a flat text-link strip that read as
   "second row of underline tabs", indistinguishable at a glance
   from the Feed/Friends/Compete row above. The pill treatment
   restores clear visual differentiation — rounded chrome with a
   sage-soft fill when active vs the underline-only tabs above.

   The default .m-amb-chip styles already encode the pill look
   (border-radius, sage-soft .on state). This variant only adds:
     • A resting border so unselected pills don't disappear into
       the navy canvas.
     • Slightly tighter padding to keep the rail compact alongside
       the avatar row above and the post stack below. */
.community-filter-rail .m-amb-chip {
  border: 1px solid var(--border);
  color: var(--text-2);
  padding: var(--s-1h) var(--s-3);
}
.community-filter-rail .m-amb-chip.on {
  border-color: var(--sage);
}

/* Section header row used by Library: "All Techniques" label on
   the left, count badge on the right. Mirrors the visual rhythm
   of the iOS-style list section header in the target screen. */
.section-label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  margin-bottom: var(--s-3h);
}
/* The row owns the bottom spacing, so the label inside resets its own. */
.section-label-row .section-label { margin-bottom: 0; }
/* .section-label-count rule retired 2026-06-03 — the Library count
   badge ('ALL TECHNIQUES · 10') was the only consumer, dropped per
   user direction ('remove the number plese') in the same pass that
   shipped the Recent dedupe. No other surface ever used the class,
   so removing the markup made the rule orphan; dropping it here
   keeps the dead-CSS lint clean. Re-introduce if a future surface
   needs a small numeric tag next to a section eyebrow. */

.session-card {
  background: var(--sage-soft);
  border: 1px solid var(--border-warm);
  border-radius: var(--r-lg);
  padding: var(--s-3) var(--s-3h);
  margin-bottom: var(--s-2h);
}
.session-card-head {
  display: flex; align-items: center; gap: var(--s-2h);
  margin-bottom: var(--s-2h);
}
/* Extracted from inline styles (community feed). */
.comm-tabs-wrap { padding: var(--s-3h) var(--s-5) 0; flex-shrink: 0; }  /* was 14px 22px 0 */

/* COMM-COMPOSE-INLINE · always-visible "Share your practice…" prompt
   (2026-06-01 redesign). Replaces the top-right .comm-compose-btn icon,
   which Fitts's-law-wise lived in the hardest-to-reach corner of the
   screen and read as a generic edit button rather than a posting CTA.
   The inline card sits directly under the filter pills, in the natural
   reading path, with the user's own avatar making the affordance read
   as "this is yours to post into" (LinkedIn / Facebook pattern). */
.comm-compose-inline {
  display: flex;
  align-items: center;
  gap: var(--s-2h);
  width: 100%;
  padding: var(--s-2h) var(--s-3);
  margin-bottom: var(--s-4);
  background: var(--flat-card);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  font-family: var(--font-body);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-state) var(--ease-ios),
              background var(--dur-state) var(--ease-ios);
}
.comm-compose-inline:hover { border-color: var(--white-line-2); }
.comm-compose-inline-prompt {
  flex: 1;
  color: var(--text-3);
  font-weight: 500;
  letter-spacing: .1px;
  /* .comm-compose-inline-prompt composes .body-text in markup for size. */
}
.comm-compose-inline-icon {
  color: var(--text-3);
  display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px;
  flex-shrink: 0;
}
.session-card-titlecol { min-width: 0; flex: 1; }
.post-you-tag { font-size: var(--fs-mini); color: var(--sage); font-weight: 500; }
.post-action-ico { width: 15px; height: 15px; }
.session-card-icon {
  width: 32px; height: 32px;
  border-radius: var(--r-sm);
  background: var(--sage-soft);
  display: flex; align-items: center; justify-content: center;
  color: var(--sage);
  flex-shrink: 0;
}
.session-card-icon svg { width: 16px; height: 16px; }
/* .session-card-title composes .caption in markup; lh:1.2 overrides .caption's 1.45 (tighter title). */
.session-card-title {
  font-weight: 600;
  color: var(--white);
  line-height: 1.2;
}
.session-card-sub {
  font-size: var(--fs-mini);
  color: var(--text-3);
  margin-top: 2px;
  letter-spacing: .3px;
}
/* Metric row — Prana Design System v2.0. */
.session-card-stats {
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  padding-top: 2px;
}
.session-stat-flat {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-1);
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  min-width: 0;
}
/* .session-stat-flat-val composes .body-text in markup for the size. */
.session-stat-flat-val {
  font-weight: 600;
  color: var(--text);             /* Ivory — neutral primary text */
  letter-spacing: -.2px;
}
.session-stat-flat-lbl {
  font-size: var(--fs-mini);
  font-weight: 500;
  color: var(--text-3);            /* Quiet caption — meaning stays in the digit */
  letter-spacing: .3px;
  text-transform: lowercase;
}
/* Best Hold — pushed to the far right, amber-only.
   Label sits BEFORE the value here (small uppercase caption,
   then the duration) so the eye lands on the digit at the edge. */
.session-stat-flat-hold {
  margin-left: auto;
  gap: var(--s-1h);
}
.session-stat-flat-hold .session-stat-flat-lbl {
  font-size: var(--fs-micro);
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--text-3);
}
.session-stat-flat-hold .session-stat-flat-val {
  color: var(--amber);            /* #d4a574 — retention token */
  font-size: var(--fs-md); /* lint-skip CAT-T4: .session-stat-flat-hold .session-stat-flat-val — compound stat value, numeric not body title */
  font-weight: 600;
  letter-spacing: -.3px;
}

/* SHARE COMPOSER (bottom sheet) */

/* LEADERBOARD */

.lb-rank {
  font-size: var(--fs-caption); /* lint-skip CAT-T2: leaderboard rank per-context size override (12px here, 14px in .lb-item via body-text); not a generic caption */ font-weight: 600;
  color: var(--text-3);
  width: 24px; text-align: center;
}
.lb-rank.gold   { color: var(--medal-gold); }
.lb-rank.silver { color: var(--medal-silver); }
.lb-rank.bronze { color: var(--medal-bronze); }

/* ===========================================================
   COMMUNITY · FRIENDS TAB
   Extracted from inline style="" in pages.js communityFriends().
   Only DYNAMIC values stay inline (avatar gradient, follow-state
   colors). Three off-scale one-offs were snapped to the nearest
   token during extraction: empty-state pad 28px→32px (--s-6),
   empty message 13px→14px (--fs-body), pill pad 7px→8px (--s-2).
   =========================================================== */
/* .comm-search-wrap / .comm-search-clear / .comm-search-clear-icon
   removed 2026-05-31 — these were byte-for-byte duplicates of the
   Library equivalents; both call sites now use the unified .search-bar
   component (lists.css) via the searchBar() helper in pages.js. */

/* Invite banner — flat. Was a sage-tinted card; now a chrome-less
   row sitting on the page background. The sage tint survives only
   on the icon chip + CTA pill, which is plenty of accent for the
   small banner. */
.comm-invite-card {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: var(--s-4) 0;
  margin-bottom: var(--s-4h);          /* 18px */
  display: flex; align-items: center;
  gap: var(--s-3h);                    /* 14px */
}
.comm-invite-icon {
  width: 42px; height: 42px;
  border-radius: var(--r-circle);
  background: var(--sage-glow);
  display: flex; align-items: center; justify-content: center;
  color: var(--sage);
}
.comm-invite-text, .comm-friend-id { flex: 1; }
/* .comm-invite-title composes .body-text in markup for the size. */
.comm-invite-title {
  font-weight: 600;
  margin-bottom: 2px;
}
/* .comm-invite-sub composes .sub-text in markup — canonical row-sub
   pattern. Only the color override (--text-3 → --text-2) is bespoke. */
.comm-invite-sub {
  color: var(--text-2);
}
.comm-invite-btn {
  background: var(--sage);
  color: var(--on-sage);
  border: none;
  border-radius: var(--r-pill);
  padding: var(--s-2) var(--s-4);      /* 8px 16px */
  font-size: var(--fs-label);          /* 11px */
  font-weight: 700;
  cursor: pointer;
  letter-spacing: 1px;
  text-transform: uppercase;
  font-family: var(--font-body);
}

/* Connection-count badge inside the section label */
.comm-conn-count {
  color: var(--text-3);
  font-weight: 400;
  font-size: var(--fs-mini);           /* 10px */
  margin-left: var(--s-1);             /* 4px */
}

/* Empty-search state — composes .empty-state (primitives); keeps only padding. */
.comm-empty { padding: var(--s-6) 0; }

/* Friend + suggested rows */
.comm-friend-row {
  border-radius: var(--radius);
  padding: var(--s-3h) var(--s-4);     /* 14px 16px */
  margin-bottom: var(--s-2);           /* 8px */
  display: flex; align-items: center;
  gap: var(--s-3);                     /* 12px */
  transition: border-color var(--dur-state);
}
.comm-suggested-row {
  border-radius: var(--radius);
  padding: var(--s-3h) var(--s-4);     /* 14px 16px */
  margin-bottom: var(--s-2);           /* 8px */
  display: flex; align-items: center;
  gap: var(--s-3);                     /* 12px */
}
.comm-avatar { width: 42px; height: 42px; }
/* .comm-friend-name composes .body-text in markup for the size. */
.comm-friend-name {
  font-weight: 600;
}
.comm-friend-meta {
  font-size: var(--fs-label);          /* 11px */
  color: var(--text-3);
  display: flex; align-items: center;
  gap: var(--s-1h);                    /* 6px */
  margin-top: 2px;
}
.comm-friend-streak {
  display: inline-flex; align-items: center;
  gap: 3px;
  color: var(--amber);
}
.comm-flame-icon { width: 11px; height: 11px; display: flex; }
.comm-friend-xp {
  font-size: var(--fs-label);          /* 11px */
  color: var(--sage);
  font-weight: 500;
  margin-top: 2px;
}
.comm-suggested-meta {
  font-size: var(--fs-label);          /* 11px */
  display: inline-flex; align-items: center;
  gap: var(--s-1);                     /* 4px */
  margin-top: 2px;
  color: var(--amber);
}
.comm-subsection-label { margin-top: var(--s-3h); }   /* 14px */

/* Pill CTAs — Follow (dynamic state colors stay inline), suggested Follow */
/* .comm-pill — the small inline community pill (Follow / Suggested / Join).
   Base is the quiet outline-less pill (used by Follow). --fill adds the solid
   sage treatment (Suggested, Join). Folded the duplicated .comm-follow-btn /
   .comm-suggested-btn (.comm-join-btn keeps only its wider padding). */
.comm-pill {
  border-radius: var(--r-pill);
  padding: var(--s-2) var(--s-3h);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: .5px;
  cursor: pointer;
}
.comm-pill--fill {
  background: var(--sage);
  color: var(--on-sage);
  border: none;
  font-weight: 700;
}

/* ===========================================================
   COMMUNITY · COMPETE TAB
   Streak banner + challenges shelf extracted from inline styles
   in communityCompete(). Leaderboard (.lb-*) was already classes.
   Dynamic values stay inline: challenge progress-bar width, the
   joined-state footer margin, the streak-bar width (now derived
   from 14/30). Off-scale snaps during extraction: challenge title
   15px→16px (--fs-md), streak headline 18px→19px (--fs-h3), two
   gaps 5px→6px (--s-1h), Join pad 7px→8px (--s-2).
   =========================================================== */
/* Streak banner */
/* Compete streak banner — flat. Was a sage-tinted card; the kicker
   class .comm-streak-kicker keeps the sage accent on text instead. */
.comm-streak-card {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: var(--s-4) 0;
  margin-bottom: var(--s-4h);          /* 18px */
}
/* Composes .section-head; keeps only its margin. */
.comm-streak-head { margin-bottom: var(--s-1h); }   /* 6px */
/* Type from .section-label (base 10px tier); keeps only its sage accent color. */
.comm-streak-kicker { color: var(--sage); }
/* .comm-streak-days composes .caption in markup for the size. */
.comm-streak-days {
  color: var(--amber);
  display: inline-flex; align-items: center;
  gap: var(--s-1h);                    /* was 5px → 6px */
}
.comm-streak-headline {
  font-family: var(--font-display);
  font-size: var(--fs-h3);             /* was 18px → 19px */
  font-weight: 500;
  margin-bottom: var(--s-3);           /* 12px */
}
.comm-streak-caption {
  font-size: var(--fs-label);          /* 11px */
  color: var(--text-3);
  margin-top: var(--s-2);              /* 8px */
}

/* Shared 12px stat-icon wrapper (joined count, hourglass, streak flame) */
.comm-stat-icon { width: 12px; height: 12px; display: flex; }

/* Challenge cards */
.comm-challenge-card {
  margin-bottom: var(--s-2h);          /* 10px */
  padding: var(--s-4) var(--s-4h);     /* 16px 18px */
}
.comm-challenge-head {
  display: flex; align-items: flex-start;
  gap: var(--s-3h);                    /* 14px */
  margin-bottom: var(--s-3);           /* 12px */
}
.comm-challenge-icon {
  width: 44px; height: 44px;
  border-radius: var(--r-circle);
  background: var(--sage-soft);
  display: flex; align-items: center; justify-content: center;
  color: var(--sage);
  flex-shrink: 0;
}
.comm-challenge-body { flex: 1; }
.comm-challenge-titlerow {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--s-2);                     /* 8px */
}
.comm-challenge-title {
  /* was 15px → 16px */
  font-weight: 600;
}
/* .comm-challenge-desc composes .caption in markup; lh:1.5 overrides .caption's 1.45. */
.comm-challenge-desc {
  color: var(--text-2);
  margin-top: 3px;
  line-height: 1.5;
}
.comm-challenge-foot {
  display: flex; justify-content: space-between; align-items: center;
}
.comm-challenge-stat {
  font-size: var(--fs-label);          /* 11px */
  color: var(--text-3);
  display: inline-flex; align-items: center;
  gap: var(--s-1h);                    /* 6px */
}
.comm-stat-dot { margin: 0 var(--s-1); }   /* 4px */
/* Type/fill from .comm-pill.comm-pill--fill; keeps only its wider padding. */
.comm-join-btn { padding: var(--s-2) var(--s-4); }
.comm-challenge-progress-label {
  font-size: var(--fs-label);          /* 11px */
  color: var(--text-3);
  margin-top: var(--s-1h);             /* 6px */
}

/* ===========================================================
   onboarding.css
   COMPONENT · ONBOARDING — onboarding wizard + session-complete (v1) + profile/achievements intro block.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* .onboard-topbar / .onboard-back-btn / .onboard-wrap--top /
   .onboard-body--tight / .onboard-cta--name rules removed 2026-05-30 —
   the name-input step (step 1) was dropped from onboarding. Only the
   goal screen remains, and only its primitives stay below. */
.onboard-wrap {
  flex: 1; display: flex; flex-direction: column;
  /* 2026-06-05 — horizontal padding bumped --gutter (22px) → --s-6
     (32px) per user audit. The 22px gutter is the app-wide content
     default and reads correctly on dense list/data pages; an onboarding
     HERO with a 32px display title hugging the left edge at 22px reads
     as cramped. The extra 10px each side gives the hero air without
     touching the rest of the app's gutter rhythm — this rule is
     scoped to .onboard-wrap. */
  padding:
    0
    max(var(--s-6), var(--safe-right))
    calc(var(--s-5) + var(--safe-bottom))
    max(var(--s-6), var(--safe-left));
}
/* 2026-06-05 — padding-top --s-2 (8px) → --s-5 (22px) for the same
   audit. 8px from the status bar to the H1 was tight; 22px lets the
   title sit deliberately. */
.onboard-wrap--lead { justify-content: flex-start; padding-top: var(--s-5); }

/* ── Welcome step (2026-08-13) — a centered brand + value moment shown before
   the goal picker. Reuses .onboard-wrap; the two u-spacers centre the mark +
   copy block vertically with the CTA pinned to the thumb zone. ── */
.onboard-wrap--welcome { justify-content: center; align-items: center; text-align: center; }
.onboard-welcome-logo { margin-bottom: var(--s-4); }
.onboard-welcome-eyebrow { color: var(--sage); margin-bottom: var(--s-2h); }   /* composes .section-label */
.onboard-welcome-title { margin-bottom: var(--s-3); }
.onboard-welcome-sub { max-width: 340px; margin-left: auto; margin-right: auto; }

/* .onboard-body composes .page-subtitle in markup; only the bottom-gap
   to the goal cards stays. */
.onboard-body {
  margin-bottom: var(--s-5);
}

/* Primary CTA — .btn-primary supplies the mint pill; this handles
   width + the disabled state for the goal-gated Continue. */
.onboard-cta { width: 100%; margin-top: var(--s-3); }
.onboard-cta.is-disabled { opacity: .4; pointer-events: none; }

/* Skip — quiet text button below the name-step CTA */
.onboard-skip-btn {
  width: 100%;
  margin-top: var(--s-2);
  padding: var(--s-3);
  background: none; border: none;
  color: var(--text-3);
  font-family: var(--font-body);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: skip button — lh:1.45 would grow button height */
  font-weight: 500;
  letter-spacing: .2px;
  cursor: pointer;
}
.onboard-skip-btn:hover { color: var(--text-2); }

/* Goal card — FLAT (2026-05-29). The .flat-card composition was
   dropped from the markup; this rule no longer carries a surface.
   Hairlines between adjacent cards provide list rhythm. Selection
   affordance lives on .goal-check.checked (cyan square + check) — no
   surface tint or border glow on the card itself any more. */
.goal-card {
  display: flex; align-items: center; gap: var(--s-3h);
  padding: var(--s-4h) 0;
  margin: 0;
  cursor: pointer;
  transition: background var(--dur-state) var(--ease-ios);
}
.goal-card + .goal-card {
  border-top: 1px solid var(--border);
}
.goal-card:hover { background: var(--white-mist); }
.goal-card.selected .goal-title { color: var(--sage); }
.goal-info { flex: 1; }
/* .goal-title composes .lead-text in markup (16px). Color/weight/ls/lh
   from the component class keep the title bold + primary against the
   12px text-3 .goal-sub below — clean 16/12 hierarchy. */
.goal-title {
  line-height: 1.2;
  font-weight: 600;
  margin-bottom: 2px;
  letter-spacing: -.1px;
}
/* .goal-sub removed — onboarding cards now show only the goal title. */
/* Rounded-square CHECKBOX (was a circular radio) — goal selection is
   multi-select, and a square reads as "pick many" where a circle reads as
   "pick one". (2026-06-14.) */
.goal-check {
  width: 22px; height: 22px;
  border-radius: var(--r-xs);
  border: 1.5px solid var(--text-3);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  transition: all var(--dur-state) var(--ease-ios);
}
.goal-check.checked {
  border-color: var(--sage);
  background: var(--sage);
}
.goal-check.checked svg {
  width: 12px; height: 12px;
  stroke: var(--on-sage); stroke-width: 3; fill: none;
}

/* SESSION COMPLETE */
@keyframes checkPop {
  from { transform: scale(.7); opacity: 0; }
  to   { transform: scale(1);  opacity: 1; }
}
.check-circle {
  width: 92px; height: 92px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--sage-170), rgba(11,19,28,.85)); /* de-navy'd slate stop (was navy rgba(11,26,46)) */
  border: 1.5px solid var(--sage);
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto;
  animation: checkPop var(--dur-page) var(--ease-out-soft) both;
  box-shadow: 0 0 40px var(--sage-290);
}

/* ===========================================================
   profile.css
   COMPONENT · PROFILE (achievements) — achievement grid, categorized scroll shelves, ghost/locked badges, tier glow.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* PROFILE */

/* ACHIEVEMENTS */

/* Badge circle — shrunk from 56px → 44px (2026-05-29) at the user's
   direction; the dense uniform shelf was reading as visually
   crowded. SVG glyph proportionally trimmed 22 → 18. */
.achieve-icon {
  width: 44px; height: 44px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
}
.achieve-icon svg { width: 18px; height: 18px; stroke-width: 1.6; fill: none; }
.achieve-icon.earned {
  background: radial-gradient(circle at 50% 40%,
    var(--tier-glow, var(--sage-170)) 0%,
    rgba(20,32,46,.5) 70%);
  border: 1px solid var(--tier-c, var(--border-warm));
  color: var(--tier-c, var(--sage));
}
.achieve-icon.earned svg { stroke: currentColor; }
.achieve-icon.locked {
  background: rgba(20,32,46,.18);
  /* Dashed ring lifted from --white-edge (14% white ≈ 1.43:1 — nearly invisible
     on the dark canvas, user) to a medium slate that clears the 3:1 non-text
     contrast bar (#5E6C84 ≈ 3.56:1). The glyph opacity is also raised below.
     Light keeps its own values. */
  border: 1px dashed #5E6C84; /* lint-skip CAT-16: dark-only locked-state slate ring (3:1 AA non-text), no matching token */
}
/* Locked stroke fully drawn but dimmed so the badge reads as attainable (the
   user can see the target). Was opacity .14 — invisible on a dark phone; .5
   keeps it clearly locked vs the bright earned glyph but actually visible. */
.achieve-icon.locked svg { stroke: var(--text-2); opacity: .5; }
/* .achieve-name composes .caption in markup; lh:1.25 overrides .caption's 1.45 (tighter labels). */
.achieve-name {
  text-align: center;
  line-height: 1.25;
  font-weight: 500;
  color: var(--text-2);
}
/* Earned vs locked label color (was inline in the markup). Dark-theme values
   match the previous inline behaviour exactly; light.css darkens both for
   contrast (earned → primary ink, locked → readable-but-muted). */
.achieve-name.is-earned { color: var(--text-2); }
/* Locked label lifted --text-3 → medium slate so it clears AA comfortably on
   the dark canvas (7.37:1) while staying clearly inactive vs the teal earned
   labels. Light darkens it separately (light.css). (user 2026-08-06) */
.achieve-name.is-locked { color: #94A3B8; /* lint-skip CAT-16: dark-only AA locked-label slate, no matching token */ }
.achieve-name.is-ghost  { opacity: .8; }   /* overflow-preview cells (was inline) */

/* CATEGORIZED ACHIEVEMENT SHELVES */
.shelf {
  position: relative;
  /* Consistent 24px rhythm between every category shelf (8px grid). */
  margin-bottom: var(--s-5);
}
/* Composes .section-head--baseline; keeps only its margin. */
.shelf-head { margin-bottom: var(--s-4); }
/* Category shelf title — regular weight (2026-05-29). Was display
   font + 500 which read as a heavy heading against the rest of the
   page; dropped the family override and the heavier weight so the
   title composes its .lead-text size (16px) with body Google Sans
   at 400. */
.shelf-title {
  font-weight: 400;
  letter-spacing: .1px;
  color: var(--text);
}
/* .shelf-blurb + .shelf-meta + .shelf-meta b removed 2026-06-05 (later)
   with the markup that consumed them. Shelf header is now just the
   category title — the page-level 'All badges N/24' count above each
   shelf carries the same aggregate information without per-shelf
   noise. cat.blurb in CATEGORIES is back to dead-data status it had
   before the morning's pass; if it ever lights up again, restore
   .shelf-blurb here. */
.shelf-track-wrap {
  position: relative;
  /* Tuck the side arrows into the gutter so they sit just outside the
     visible badge area without stealing horizontal space. */
}
.shelf-track {
  /* The track is a CSS-only carousel: native horizontal overflow with
     scroll-snap keeps it native-smooth on touch, while the arrow
     buttons drive `scrollLeft` programmatically by one page-width. */
  display: flex;
  gap: var(--s-3);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* Hide the scrollbar — the arrows are the affordance. */
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding: 2px 0 var(--s-1);       /* room for the focus glow not to clip */
}
.shelf-track::-webkit-scrollbar { display: none; }
.shelf-cell {
  flex: 0 0 calc((100% - 36px) / 4);     /* 4 cells per page, 3×12px gaps */
  scroll-snap-align: start;
  display: flex; flex-direction: column;
  align-items: center; gap: var(--s-2);
  min-width: 0;
}
.shelf-arrow {
  position: absolute;
  /* Center on the icon row (track padding-top 2px + half of the 44px
     icon = 24px). Was 30px (when badges were 56px) — bumped down
     2026-05-29 when the badge circle shrank to 44px. */
  top: 24px; transform: translateY(-50%);
  width: 28px; height: 28px;
  border-radius: 50%;
  border: 1px solid var(--white-line);
  /* Background TRANSPARENT (2026-05-29) — matches the .icon-btn
     transparent-chrome treatment applied app-wide. The 1px ring +
     chevron glyph are the only visible chrome at rest; the backdrop
     blur went with the fill since there's nothing left to frost. */
  background: transparent;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  color: var(--text-2);
  z-index: 2;
  transition: opacity var(--dur-state), transform var(--dur-state), border-color var(--dur-state), color var(--dur-state);
}
.shelf-arrow:hover {
  border-color: var(--sage);
  color: var(--sage);
}
.shelf-arrow:active { transform: translateY(-50%) scale(.92); }
.shelf-arrow.disabled {
  opacity: 0;
  pointer-events: none;
}
/* Identical glyph sizing on both arrows so left/right read as a matched
   pair (both are chevrons now). */
.shelf-arrow svg { width: 16px; height: 16px; stroke-width: 2; fill: none; }
.shelf-arrow.left  { left:  -10px; }
.shelf-arrow.right { right: -10px; }
/* Invisible touch-target — the visible arrow disc stays 28×28 so the
   shelf layout (offset -10px outside the track) is unaffected, but a
   44×44 ::after extends the tap area to the WCAG 2.5.5 / iOS HIG
   floor. Important here because the small arrows on a 4-cell
   achievement shelf are easy to miss-tap with a thumb. */
.shelf-arrow::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 44px; height: 44px;
}

/* GHOST BADGES — locked variant inside a shelf cell */
/* The icon itself is dimmed via .achieve-icon.locked (low-opacity
   stroke). Here we only gently quiet the whole cell; no heavy grayscale
   that would crush the icon into "dead space". Hover lifts to hint
   interactivity. */
.ghost {
  opacity: .92;
  transition: opacity var(--dur-card) ease, transform var(--dur-state) ease;
}
.ghost:hover {
  opacity: 1;
  transform: translateY(-2px);
}
.ghost:hover .achieve-icon.locked svg { opacity: .3; }

/* shelf-toggle kept for backwards-compat but no longer rendered */

/* Empty-shelf hint (no earned badges yet) */
.shelf-empty {
  font-size: var(--fs-label); color: var(--text-3);
  padding: var(--s-4) 0;
  text-align: center;
}

/* ── UX POLISH: Focus rings ─────────────────────────────────────
   Accessibility + premium feel: every interactive surface MUST show
   a visible focus indicator when reached via keyboard, screen reader,
   or external pointer. We use :focus-visible (not :focus) so the ring
   never appears on mouse/touch taps — only when the user actually
   needs the cue. Inputs/textareas additionally get a bordered focus
   state via their own rules below (.search-input:focus, .pc-note:focus,
   .reminder-name-input:focus); those rules are kept narrow and
   bordered to coexist with this outline ring, which still applies as
   a backstop via :focus-visible.

   Color: sage at full strength, offset 2–3px from the element edge so
   it reads as a halo rather than a clipped border. The soft sage glow
   (box-shadow) doubles the visual weight on primary controls without
   adding harshness. */
:focus-visible {
  outline: 2px solid var(--sage);
  outline-offset: 2px;
  border-radius: 4px;
}
button:focus-visible,
[role="button"]:focus-visible,
a:focus-visible,
.icon-btn:focus-visible,
.nav-item:focus-visible {
  outline: 2px solid var(--sage);
  outline-offset: 3px;
  box-shadow: 0 0 0 4px var(--sage-soft);
}
.btn-primary:focus-visible {
  outline: 2px solid var(--sage);
  outline-offset: 3px;
  box-shadow: 0 0 0 5px var(--sage-soft), 0 0 30px var(--sage-210);
}

/* ===========================================================
   99-utilities.css
   UTILITIES — focus rings, tactile button-active, loading skeleton, haptic ripple, orb-glow layers, XP shimmer, .help-* helpers. Highest specificity; loads last among the early layers.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* Form fields: explicit :focus-visible backstop. The element-specific */
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--sage);
  outline-offset: 2px;
  box-shadow: 0 0 0 3px var(--sage-soft);
}

/* .u-sr-only — visually hide an element while keeping it announceable
   to screen readers. Used 2026-06-05 to mount a real <h1> page heading
   on surfaces whose visible design has none (Home, Library tabs whose
   page-titles were intentionally dropped per the iOS-style 'quieter
   chrome' direction). A screen-reader user gets a clear page anchor;
   sighted users see no change.
   Standard 1-px clip recipe — works in every modern browser, doesn't
   create a focus trap, doesn't break copy-paste. */
.u-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* UX POLISH: Button active states (tactile press) */
.tech-item:active      { transform: scale(.99); transition: transform var(--dur-tap) var(--ease-ios); }

.goal-card:active      { transform: scale(.99); transition: transform var(--dur-tap) var(--ease-ios); }
.icon-btn:active       { transform: scale(.92); transition: transform var(--dur-tap) var(--ease-ios); }

/* UX POLISH: Loading skeleton */
@keyframes skeleton-pulse {
  0%, 100% { opacity: .35; }
  50%       { opacity: .75; }
}

/* UX POLISH: Haptic feedback visual ripple */
@keyframes hapticRipple {
  0%   { transform: scale(1);    opacity: .6; }
  60%  { transform: scale(1.2);  opacity: .1; }
  100% { transform: scale(1.3);  opacity: 0; }
}
/* Phase-change flash on the breathing orb */
@keyframes phaseFlash {
  0%   { opacity: .7; }
  100% { opacity: 0; }
}
.phase-flash {
  position: absolute; inset: 0; border-radius: 50%;
  background: radial-gradient(circle, var(--sage-210), transparent 70%);
  pointer-events: none;
  animation: phaseFlash .55s ease-out forwards;
}

/* UX POLISH: Ghost badge tooltip */
.shelf-cell { position: relative; }
.shelf-cell.ghost::after {
  content: attr(data-hint);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%; transform: translateX(-50%);
  background: rgba(8,14,22,.96);   /* de-navy'd slate (was navy rgba(10,20,32)) */
  border: 1px solid var(--border-warm);
  border-radius: var(--r-sm);
  padding: var(--s-1h) var(--s-2h);
  font-size: var(--fs-mini); color: var(--text-2);
  white-space: normal;
  line-height: 1.4;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-state);
  z-index: var(--z-raised);
  text-align: center;
  width: 110px;
}
.shelf-cell.ghost:hover::after { opacity: 1; }

/* UX POLISH: Enhanced input focus */
.search-input:focus {
  border-color: var(--border-warm);
  box-shadow: 0 0 0 3px var(--sage-soft);
  background: var(--sage-wash);
}

/* UX POLISH: Nav tap ripple */
.nav-item { position: relative; overflow: hidden; }
.nav-item::after {
  content: '';
  position: absolute; inset: 0;
  background: radial-gradient(circle at center, var(--sage-170) 0%, transparent 70%);
  opacity: 0; border-radius: var(--r-md);
  transition: opacity var(--dur-tap);
}
.nav-item:active::after { opacity: 1; }

/* Earned badge — refined tier-aware glow */
/* Subtle, static glow that respects the badge's tier color (set via */
.achieve-icon.earned {
  box-shadow:
    0 0 0 1px var(--tier-c, var(--sage-glow-45)),
    0 2px 10px -2px var(--tier-glow, var(--sage-210));
  transition: box-shadow var(--dur-state) ease, transform var(--dur-state) ease;
}
.achieve-icon.earned:hover {
  transform: translateY(-1px);
  box-shadow:
    0 0 0 1px var(--tier-c, var(--sage-glow-peak)),
    0 4px 16px -2px var(--tier-glow, var(--sage-glow-40));
}

/* UX POLISH: Shelf reveal stagger */
@keyframes revealUp {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.shelf { animation: revealUp var(--dur-card) var(--ease-standard) both; }
.shelf:nth-child(1) { animation-delay: 0ms; }
.shelf:nth-child(2) { animation-delay: 70ms; }
.shelf:nth-child(3) { animation-delay: 140ms; }

/* UX POLISH: Orb glow layers synced to breathing phase */
@keyframes orbGlowInhale {
  from { filter: drop-shadow(0 0 8px rgba(var(--orb-color-rgb), .2));  }
  to   { filter: drop-shadow(0 0 24px rgba(var(--orb-color-rgb), .6)); }
}
@keyframes orbGlowExhale {
  from { filter: drop-shadow(0 0 24px rgba(var(--orb-color-rgb), .6)); }
  to   { filter: drop-shadow(0 0 8px rgba(var(--orb-color-rgb), .2));  }
}
/* Layer the scale + glow animations together. HOLD stays still by
   design — no glow ramp, just the steady inset on .orb-inner. */
.orb-inner.phase-INHALE { animation: orb-inhale var(--orb-inhale-dur, 4s) ease-in-out forwards, orbGlowInhale var(--orb-inhale-dur, 4s) ease-in-out forwards; }
.orb-inner.phase-EXHALE { animation: orb-exhale var(--orb-exhale-dur, 8s) ease-in-out forwards, orbGlowExhale var(--orb-exhale-dur, 8s) ease-in-out forwards; }

/* Kapalabhati rapid pumping — the orb holds completely STEADY: fixed size, one
   sage colour, and a STEADY glow (no pulse). A per-pump glow breath drifts out
   of sync with the count + reads as too much movement at this tempo, so the orb
   just sits calmly lit while the count ticks. Replaces the jarring
   expand/contract+flash "blinking" the user flagged. */
.orb-inner.phase-PUMP {
  transform: scale(1.04);
  filter: drop-shadow(0 0 18px rgba(var(--orb-color-rgb), .42));
}

/* UX POLISH: XP bar shimmer */
@keyframes shimmer {
  from { background-position: -200% center; }
  to   { background-position: 200% center; }
}
.progress-bar-fill {
  background: linear-gradient(90deg,
    var(--sage-400) 0%,
    var(--sage) 38%,
    var(--white-strong-2) 50%,
    var(--sage) 62%,
    var(--sage-400) 100%);
  background-size: 200% auto;
  animation: shimmer 3.5s linear infinite;
  transition: width .6s var(--ease-standard);
}

/* Locked badge styling is defined once above (line ~791).
   .ghost wrapper handles the dimming for locked-in-shelf context. */

/* ───────────────────────────────────────────────────────────
   ATOMIC UTILITIES
   Single-purpose helpers extracted from repeated inline styles in
   pages.js (style-extraction sprint). Each replaces a string that
   recurred 2+ times across screens. Add here only when a one-liner
   genuinely recurs — semantic blocks belong in their component partial.
   ─────────────────────────────────────────────────────────── */
/* Flex spacer: pushes following content to the bottom of a flex column.
   Was `<div style="flex:1"></div>` in onboarding and elsewhere. */
.u-spacer { flex: 1; }
/* Full-bleed screen: removes the default .screen gutter so a child
   (progress bar, hero) can manage its own edge padding. */
.u-screen-flush { padding: 0; }
/* Heading-to-subhead rhythm used by display headers. */
.u-mb-12 { margin-bottom: var(--s-3); }

/* ── Ring-fenced layout helpers (strict .u- namespace) ───────────────
   These exist ONLY to clear the last static inline style="" attrs from
   the JS templates (the prerequisite for tightening style-src in the
   CSP). Components stay semantic; these are isolated single-purpose
   helpers. Spacing values are tokenized; element dimensions are literal.
   Reuse .u-spacer (above) for flex:1. */
.u-pe-none     { pointer-events: none; }
.u-text-right  { text-align: right; }
.u-clickable   { cursor: pointer; }
.u-noshrink    { flex-shrink: 0; }
.u-amber       { color: var(--amber); }
.u-mb-22       { margin-bottom: var(--s-5); }
.u-ico-14      { width: 14px; height: 14px; display: inline-flex; }
.u-flex-center { display: flex; align-items: center; gap: var(--s-2); }
/* Vertical spacer divs (replace <div style="height:Npx">). */
.u-vspace-10 { height: var(--s-2h); }
.u-vspace-14 { height: var(--s-3h); }
.u-vspace-18 { height: var(--s-4h); }
.u-vspace-24 { height: 24px; }

/* ===========================================================
   modals.css
   COMPONENT · MODALS & SHEETS — skills, search, heart/info buttons, technique modal overlay, pre-session tuning, settings list, language picker, reminder sheet scaffolding, toasts, hover polish.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* SKILLS */

/* SEARCH */
.search-input {
  /* left pad = icon left (var(--s-4)=16) + icon width (var(--s-4)=16) + gap (var(--s-3h)=14) = 46px.
     Derived from .search-icon so text alignment tracks the icon if it ever resizes. */
  width: 100%; padding: var(--s-3) var(--s-4) var(--s-3) calc(var(--s-4) + var(--s-4) + var(--s-3h));
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text);
  outline: none; transition: var(--dur-state);
  /* 2026-06-04 — font-size pinned to --fs-md (16px). The markup still
     composes .body-text (14px) for font-family/line-height/letter-
     spacing, but iOS Safari auto-zooms into any input with
     font-size < 16px on focus — user audit: 'when I start tapping
     the name of technique the screen become closer ... zooming in'.
     Same rationale .field-input documented in 03-primitives.css
     ('font-size is --fs-md (16px) so focusing never triggers iOS
     auto-zoom. Search boxes are a separate pattern — not migrated.'
     — well, now they are). The override beats .body-text purely
     via source-cascade order: modals.css comes after 03-primitives.css
     in build-css.py's ORDER, so this rule wins. */
  font-size: var(--fs-md); /* lint-skip CAT-T4: search input — 16px so iOS doesn't auto-zoom on focus, same carve-out as .field-input */
}
.search-input:focus { border-color: var(--border-warm); }
.search-input::placeholder { color: var(--text-3); }
.search-icon {
  position: absolute; left: 16px; top: 50%;
  transform: translateY(-50%);
  width: 16px; height: 16px;
  stroke: var(--text-3); fill: none; stroke-width: 1.8;
}

/* TIME PICKER */

/* NOTIFICATION DOT */

/* SOUND CHIP */

/* HELPERS */
/* .h1-display composes .hero-title in markup; lh:1.1 and ls:-.5 match .hero-title's tokens. */
.h1-display {
  font-family: var(--font-body);
  font-weight: 700;
  font-style: normal;
}
.h1-display .accent {
  font-style: normal;
  color: var(--sage);
}

NEVER a button.
   Per the one-accent rule,
warm peach (--amber) carries status meaning
   only — it must never be tappable. This rule hardens that contract:
   no pointer cursor,
no hover lift,
no user-select shimmer. If a future
   surface needs a tappable streak control {
  display: flex; align-items: center; gap: var(--s-2);
  background: rgba(212,165,116,.1);
  border: 1px solid rgba(212,165,116,.22);
  border-radius: 999px;
  padding: var(--s-2) var(--s-3h);
  width: fit-content;
  cursor: default;
  user-select: none;
}

/* Status-dot pulse: opacity-only breath, no scale oscillation.
   Reads as a "live" indicator without violating the no-oscillation
   rule. The dot itself doesn't grow; its glow fades in and out. */
@keyframes pulse {
  0%, 100% { opacity: 1;   box-shadow: 0 0 6px var(--sage); }
  50%      { opacity: .55; box-shadow: 0 0 10px var(--sage); }
}

/* HEART / FAVORITE BUTTON */

/* heartPop: asymptotic settle — swells once and eases home with no
   bounce. Apple-style "the heart took notice" rather than a cartoon
   spring. Curve = iOS standard. */
@keyframes heartPop { from { transform: scale(.88); } to { transform: scale(1); } }

/* DIFFICULTY CHIP */

/* INFO BUTTON */

/* Batch A #5 — Hit-area extension: 22px button gets a ~44px tap zone. */

/* TECHNIQUE MODAL OVERLAY */
.tech-modal-overlay {
  /* Frosted-navy surface comes from the shared .sheet-scrim alongside. */
  position: absolute; inset: 0;
  z-index: var(--z-modal);
  display: flex; flex-direction: column;
  justify-content: flex-end;
  animation: overlayIn var(--dur-state) var(--ease-ios);
}
@keyframes overlayIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes sheetDown { from { transform: translateY(0); } to { transform: translateY(100%); } }

/* ─── REUSABLE BOTTOM-SHEET PRIMITIVES ───────────────────────────
   ONE canonical definition for sheet titles and rows, shared by every
   bottom sheet (sound, reminders sub-sheets, save-preset, tech/tuning
   modal). New sheets compose these instead of forking per-sheet classes.

   .sheet-title — typography matches the tech-modal title (the reference
     the rest of the app aligns to). Alignment/margin are deliberately
     NOT baked in: a sheet centers its title with .sheet-title--center
     (reminders sub-sheets) without forking the type spec.
   .sheet-row   — the standard row metric (= .settings-row): 14px gap,
     14px/16px padding, 56px min-height. Surface/border/hover stay on
     the per-context class that sits alongside it in the markup. */
.sheet-title {
  font-family: var(--font-body);
  font-size: var(--fs-h3);          /* 19px */
  font-weight: 700;
  letter-spacing: -.3px;
  line-height: 1.15;
  color: var(--text);
  margin: 0;
}
.sheet-title--center { flex: 1; text-align: center; }
.sheet-row {
  display: flex;
  align-items: center;
  gap: var(--s-3h);                 /* 14px */
  padding: var(--s-3h) var(--s-4);  /* 14px / 16px */
  min-height: 56px;
}
/* .row-icon — the canonical BARE row glyph (no tile box): a 24px line icon
   sitting inline with the row text. Shared by every list/row icon (settings,
   modal-row, reminder-opt, stats milestones + calendar). Color is NOT set
   here — the context provides it (own color, .amber, or a .tile-sage
   class for the glyph hue). No background — the companion class resets any
   tile-* fill. (.tile-moon was the moon-tinted partner; removed 2026-06-04
   when the wisdom sheet went; reintroduce as a pair if a moon site returns.) */
.row-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.row-icon svg { width: 24px; height: 24px; stroke: currentColor; stroke-width: 1.7; fill: none; }
/* .sheet-scrim — the canonical frosted-navy scrim behind every bottom sheet /
   modal / sub-sheet overlay. Reads as the blurred page canvas, NOT a black
   wash. The overlay class alongside owns position/inset/z-index/flex/animation;
   .sheet-scrim owns only the surface. */
.sheet-scrim {
  background: rgba(6, 14, 26, 0.55);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
}

.tech-modal-sheet {
  /* CANONICAL SHEET BACKGROUND — page canvas, not navy.
     Every bottom sheet in the app rises from the page, so it must
     read as the same surface family as the canvas behind it, never
     as a competing navy slab. This is the DEFAULT so new sheets are
     correct by birth — no per-sheet override required. A sheet that
     genuinely wants a raised navy fill must opt IN explicitly (none
     currently do). Previously this defaulted to var(--grad-lift-v),
     which made every unstyled sheet (Edit Profile, Composer) navy
     until someone remembered to override it — the root cause of the
     recurring navy-sheet bug. --grad-sheet is the same dark premium gradient
     the Sound sheet uses, so every sheet now matches it by default. */
  background: var(--grad-sheet);
  border-top: 1px solid var(--white-veil);
  border-radius: var(--r-sheet) var(--r-sheet) 0 0;
  padding: 0 0 calc(28px + var(--safe-bottom));
  /* Sheet entry — iOS asymptotic curve. Settles, no overshoot. */
  animation: sheetUp var(--dur-card) var(--ease-ios);
  position: relative;
  overflow: hidden;
  /* 2026-06-04 — will-change: transform per user audit ('there's a
     bit jumpy animation when I tap any breathing tech from library
     and home'). Same fix #reminders-sheet already had; the base
     .tech-modal-sheet was missing it. Without the hint, iOS Safari
     composites the sheet on the main thread for the first frame of
     the slide-up, then promotes it mid-animation — that compositor-
     layer handoff is exactly what reads as a 'jump' on the way up.
     Stamping will-change: transform here pre-promotes the sheet to
     its own GPU layer before the animation starts, so the slide is
     buttery from frame zero. */
  will-change: transform;
  /* Never extend past the safe-area-top — keeps the rounded top edge
     of the sheet clear of any notch / status-bar chrome. */
  max-height: calc(100dvh - var(--safe-top) - 8px);
}
.tech-modal-sheet::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--sage-290), transparent);
}
.modal-handle {
  width: 36px; height: 4px;
  border-radius: 2px;
  background: var(--white-edge);
  margin: var(--s-3h) auto 0;
}
.modal-header {
  display: flex; align-items: flex-start;
  gap: var(--s-3h);
  padding: var(--s-4h) var(--s-5) var(--s-4h);
}
.modal-icon-wrap {
  width: 52px; height: 52px;
  border-radius: var(--r-lg2);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  /* Bare line icon — no tile; category color + glow (matches Library/Home). */
  color: var(--tint, var(--sage));
}
.modal-icon-wrap svg {
  width: 38px; height: 38px;
  stroke-width: 1.6; fill: none;
  filter: drop-shadow(0 0 9px rgba(var(--tint-rgb, 125,211,232), .5));
}

/* PHASE BREAKDOWN STEPS */
/* Cycle-breakdown list — compact, hairline-separated rows. No own padding:
   the rows align to the modal-body gutter (like the eyebrows), and a 1px
   divider separates them. Vertical spacing comes from the tab-pane gap. */
.phase-steps { display: flex; flex-direction: column; }
.phase-step {
  display: flex; align-items: center; gap: var(--s-3h);
  padding: var(--s-2h) 0;
}
.phase-step:first-child { padding-top: 0; }
.phase-step + .phase-step { border-top: 1px solid var(--border); }
.phase-num {
  width: 32px; height: 32px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  font-size: var(--fs-caption); /* lint-skip CAT-T2: sizes the number glyph inside a 32px phase-step circle, not body caption */ font-weight: 700;
  font-family: var(--font-body);
}
.phase-label {
  font-weight: 500;
  letter-spacing: .1px;
  margin-bottom: 1px;
}
.phase-sub { font-size: var(--fs-label); color: var(--text-2); }
.phase-dur {
  margin-left: auto; flex-shrink: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h2); font-weight: 500;
  letter-spacing: -.3px;
  line-height: 1;
}
.phase-dur-unit { font-size: var(--fs-mini); color: var(--text-3); font-family: var(--font-body); font-weight: 500; letter-spacing: .5px; }
/* Duration column → right-aligned flex column with a stable min-width. The
   display-font numerals aren't tabular, so "5.5" sits wider than "4"/"8";
   anchoring the column to flex-end (+ a fixed min-width) keeps the right edge
   of the value and the SEC label below it aligned row-to-row, whatever the
   digit count. Scoped to .phase-step so the shared .u-text-right utility is
   untouched. (2026-06-13.) */
.phase-step .u-text-right {
  display: flex; flex-direction: column; align-items: flex-end;
  flex-shrink: 0; min-width: 3ch;
}
/* Loop indicator below the .phase-steps stack — used by Power Breath to
   signal that the 3 numbered stages repeat each round. Other techniques
   leave the slot empty (numbered cycles are obviously looped).         */
.phase-steps-loop {
  color: var(--text-3);
  margin-top: var(--s-2);
  padding-top: var(--s-2);
  border-top: 1px solid var(--border);
  text-align: center;
  letter-spacing: .2px;
}

/* BENEFIT PILLS */
/* No own padding — aligns to the modal-body gutter like the cycle list. */
/* 2-col grid (was flex-wrap): every technique carries exactly 4 benefits, so a
   greedy wrap left the 4th chip orphaned on its own row. A fixed 2×2 grid keeps
   the rows balanced and the block structurally even. Pills stretch to fill their
   column with centered content; min-width:0 + nowrap keep a long label (e.g.
   "Emotional regulation") from blowing out the column. (2026-06-13.) */
.benefit-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-1h); }
.benefit-pill {
  /* Checkmark removed 2026-08-06 (user) — label only; the pill chrome IS the
     "benefit" signal, so the tick was redundant. gap/svg rule dropped with it. */
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 0; white-space: nowrap;
  padding: var(--s-1) var(--s-2h); border-radius: 999px;
  background: var(--sage-soft);
  border: 1px solid var(--sage-130);
  font-size: var(--fs-mini); font-weight: 600;
  color: var(--text-2); letter-spacing: .5px;
}
/* The label truncates with an ellipsis inside its column (the pill's min-width:0
   above lets it shrink); without this a long benefit like "Emotional regulation"
   spilled past the pill on a narrow phone — the older comment claimed nowrap
   alone truncated, but it doesn't. */
.benefit-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* EXPANDED DESCRIPTION PANEL */

/* MODAL ACTION BUTTONS */

/* PRE-SESSION TUNING CARDS */

/* Duration chips */

/* Soundscape chips */

/* SVG inside a soundscape chip — pulls currentColor from .snd-chip */

/* Small rounded square tile holding an SVG glyph in the audio sheet rows.
   Used by Background music / Breath cues / Voice guidance rows. Color &
   background are set inline so each row keeps its accent. */

/* Binaural chips */

/* Binaural chip selection: mint per the one-accent rule — */

/* Pre-session toggle pill */
.ps-tog {
  width: 40px;
  height: 24px;
  border-radius: 999px;
  /* Off-track: lighter than --white-line (0.10 hairline) so the pill reads
     against the navy canvas even in glare. --white-grip (0.22) is the handle/
     affordance-rest token — right family for a switch track. */
  background: var(--white-grip);
  border: none;
  position: relative;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  transition: background var(--dur-state);
}
/* Invisible 48×48 hit area (centered) so the small 40×24 pill still meets the
   mobile minimum touch target without changing its painted size. */
.ps-tog::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 48px;
  height: 48px;
  transform: translate(-50%, -50%);
}
.ps-tog.on { background: var(--sage); }
.ps-tog::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  /* OFF knob: soft cool-gray, not pure white — a bright white pip on a dark
     canvas pulls the eye toward things that are switched OFF. The knob
     brightens to full white only in the ON state below. */
  background: var(--toggle-off-knob);
  transition: left var(--dur-state), transform var(--dur-tap), background var(--dur-state);
  box-shadow: var(--elev-1);
}
.ps-tog.on::after { left: 19px; background: var(--white); }
.ps-tog:active::after { transform: scale(.92); }
/* Disabled "coming soon" rows (Voice guidance, Haptic feedback). Dim the icon,
   sub-label and toggle to ~45% and mute the title so the whole control reads as
   non-interactive (prevents rage-clicks) — but keep the SOON badge at full
   strength. Per-element, NOT whole-row opacity: the badge lives inside the
   title, so a parent opacity would drag it down with the text. Title is dimmed
   via color (not opacity) for the same reason — the badge sets its own color. */
.modal-row.is-disabled .modal-row-icon,
.modal-row.is-disabled .modal-row-sub,
.modal-row.is-disabled .ps-tog { opacity: .32; }
.modal-row.is-disabled .modal-row-title { color: var(--text-3); }
.modal-row.is-disabled .ps-tog { cursor: not-allowed; }

/* SETTINGS LIST · in-row hairline cell separators */
.settings-row {
  display: flex; align-items: center; gap: var(--s-3h);
  padding: var(--s-3h) var(--s-4);
  min-height: 56px;
  transition: background var(--dur-state) var(--ease-ios);
}
/* Cell separator — every row after the first carries its own 1px */
.settings-row + .settings-row {
  border-top: 1px solid var(--white-mist);
}
/* MIGRATED → .state-target primitive (Wave 1, modals state-layer pass).
   The interactive (action) row now carries `state-target` from the JS
   builder; the veil below owns the hover fill. Neutral white ink (a sage
   veil on this navy panel would read as a color cast) and a per-surface
   --state-hover:.03 override so the hover stays PIXEL-IDENTICAL to the old
   var(--surface-soft-hover) (rgba 255,255,255,.03) — mechanism migrated,
   look unchanged. --state-radius:0 because the row is square; the parent
   .settings-list (border-radius + overflow:hidden) clips the veil to the
   rounded panel corners. The old `.settings-row[onclick]:hover,
   [data-action]:hover { background: var(--surface-soft-hover) }` pair is
   gone: grep-verified the builder (pages.js) emits data-action only, never
   onclick, so the [onclick] half was dead. */
.settings-row.state-target {
  --state-ink: 255,255,255;
  --state-hover: 0.06;   /* unified card/row hover = 6% white (was .03) */
  --state-radius: 0px;
  cursor: pointer;
}
.settings-row-icon {
  /* 2026-06-03 — dial-back from the morning 'borrow Library's icon
     treatment' commit (3a4d146). User direction: 'the weight of the
     icons at profile and settings too much' → drop the sage glow,
     pull the SVG back from 28→24px. The 48x48 wrapper stays as a
     transparent hit area so the icon column geometry is unchanged
     (rows still align to the same icon-left edge), but the visual
     presence comes back down toward the pre-today baseline. Library
     .tech-icon-wrap keeps its 28px + glow — the louder treatment
     fits tile-like primary actions, not Settings rows like 'Push
     notifications · On'.
     Safety row's inline rose color override still wins via
     currentColor cascade. */
  width: 48px;
  height: 48px;
  border-radius: var(--r-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--sage);
}
.settings-row-icon svg {
  width: 24px;
  height: 24px;
  stroke-width: 1.7;
  fill: none;
}
.settings-row-body { flex: 1; min-width: 0; }
.settings-row-sub { margin-top: 2px; color: var(--text-2); }
.settings-row-trail {
  display: flex; align-items: center;
  flex-shrink: 0;
  color: var(--text-3);
}
/* Legacy divider node — kept as a no-op so any stale markup in
   downstream branches doesn't open a 1px line break inside the
   panel. New code should rely on the .settings-row + .settings-row
   border-top above. */

/* EDIT-ACCOUNT BUTTON (under identity block) */

/* LANGUAGE PICKER (inline expand) */
.lang-list {
  border-top: 1px solid var(--border);
  max-height: 280px;
  overflow-y: auto;
  scrollbar-width: thin;
}
.lang-list::-webkit-scrollbar { width: 4px; }
.lang-list::-webkit-scrollbar-thumb { background: var(--border-warm); border-radius: 4px; }
.lang-option {
  width: 100%; display: flex; align-items: center;
  justify-content: space-between;
  padding: var(--s-3) var(--s-4) var(--s-3) var(--s-8);
  background: transparent; border: none;
  border-bottom: 1px solid var(--border);
  cursor: pointer; transition: background var(--dur-state);
  font-family: var(--font-body); text-align: left;
}
.lang-option:last-child { border-bottom: none; }
.lang-option:hover { background: var(--sage-tint); }
/* 2026-06-02 — active row's sage-soft background removed.
   The fill spanned from beneath the Language parent row down through
   the active English row, reading visually as 'English belongs to
   the header, Spanish is separate' — a false grouping (user audit).
   Active state now travels through the label color (sage) + the
   trailing checkmark instead of a background highlight, so every
   language option sits on the same surface and the divider between
   them is unbroken. */
.lang-option.active .lang-option-label { color: var(--sage); }
/* .lang-option-label composes .body-text in markup for the size. */
.lang-option-label {
  font-weight: 500; color: var(--text);
}
/* 2026-06-02 — native-language sub-text bumped --text-3 → --text-2.
   At --text-3 the Español / Français / Deutsch line was so dim
   against the dark navy canvas it failed WCAG AA contrast. --text-2
   keeps the visual hierarchy (still quieter than the primary label)
   while crossing the legibility line. */
.lang-option-native {
  font-size: var(--fs-label); color: var(--text-2);
}

/* — Reminder row + swipe-to-reveal-delete —
   The row sits in a wrapper that holds a hidden delete pad behind it.
   On swipe-left, the row translates to reveal the pad. Tap pad → delete. */
/* Flattened set-list panel */
.m-set-list {
  background: var(--surface-soft);
  border: 1px solid var(--white-mist);
  border-radius: var(--r-lg);
  overflow: hidden;       /* clip the inner rows to the panel radius */
}
/* Hairline separators between adjacent entries inside the panel. */
.m-set-list > * + * {
  border-top: 1px solid var(--white-mist);
}
/* Inner rows render flat — no per-row background tint, no per-row
   border-radius. The panel itself owns those properties. */
.m-set-list .reminder-row {
  background: transparent;
  border-radius: 0;
}
/* MIGRATED → .state-target primitive (Wave 1, modals state-layer pass).
   The reminder-row carries `state-target` from the JS builder (ui.js);
   the veil owns the hover fill. Same recipe as the settings row: neutral
   white ink, --state-hover:.03 to hold the exact old var(--surface-soft-
   hover) look, --state-radius:0 (the row is square; first/last-child
   rounding + the list-wrap overflow:hidden clip the veil to the rounded
   list corners). The veil's ::after{inset:0} rides the row's swipe
   transform correctly, and the swipe class is `.swipe-dragging` (not the
   primitive's `.is-dragging`), so no drag-state collision.
   NOTE — intentionally NOT touched here (deferred to the dead-code sweep,
   not silently deleted): the standalone `.reminder-row:hover` fallback and
   the `.reminders-active-list:not(.reminders-active-list-soft) .reminder-
   row[:hover]` pair below. The live builder always emits the `-soft`
   variant, so those :not(-soft) rules never fire today; each needs its own
   zero-reference verification before removal. */
.m-set-list .reminder-row.state-target {
  --state-ink: 255,255,255;
  --state-hover: 0.06;   /* unified card/row hover = 6% white (was .03) */
  --state-radius: 0px;
}
.m-set-list .reminder-row-wrap {
  border-radius: 0;
}
.reminders-active-list .reminder-row-wrap:first-child { border-top-left-radius: 14px; border-top-right-radius: 14px; }
.reminders-active-list .reminder-row-wrap:first-child .reminder-row { border-top-left-radius: 14px; border-top-right-radius: 14px; }
.reminders-active-list .reminder-row-wrap:last-child { border-bottom-left-radius: 14px; border-bottom-right-radius: 14px; }
.reminders-active-list .reminder-row-wrap:last-child .reminder-row { border-bottom-left-radius: 14px; border-bottom-right-radius: 14px; }
.reminder-row-wrap {
  position: relative;
  overflow: hidden;
  background: transparent;
}
.reminder-row-wrap + .reminder-row-wrap { border-top: 1px solid var(--white-mist); }
.reminder-row {
  /* Row metric (flex, 14px gap, 14/16 padding, 56px min-height) comes from
     the shared .sheet-row alongside this class in the markup. */
  cursor: pointer;
  transition: background var(--dur-state) ease, transform var(--dur-state) var(--ease-ios);
  position: relative;
  background: transparent;
  z-index: 1;
  touch-action: pan-y;
}
.reminders-active-list:not(.reminders-active-list-soft) .reminder-row {
  background: var(--surface);   /* was hardcoded #11203a — now the canonical card token */
}
.reminder-row:hover { background: var(--white-tint); }
.reminders-active-list:not(.reminders-active-list-soft) .reminder-row:hover {
  background: var(--white-tint);   /* subtle hover, matching the base
                                      .reminder-row:hover (was the heavier
                                      raised --surface-raised fill). */
}
/* ── Swipe-to-reveal-trash (reminder rows + library preset rows) ──────
   The row sits in a .swipe-wrap that holds a .swipe-del-pad (trash button)
   pinned to the right, behind the row. Swiping the .swipe-row left reveals
   the pad (which fades in); tapping it commits the delete. */
.swipe-wrap { position: relative; overflow: hidden; }
.swipe-del-pad {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 72px;
  display: flex; align-items: center; justify-content: center;
  background: var(--rose-50);
  color: var(--rose);
  border: none; cursor: pointer; padding: 0;
  opacity: 0; transition: opacity var(--dur-state) ease;
  -webkit-tap-highlight-color: transparent;
}
.swipe-del-pad svg { width: 20px; height: 20px; }
.swipe-wrap.swiping .swipe-del-pad,
.swipe-wrap:has(.swipe-row.swipe-open) .swipe-del-pad { opacity: 1; }
.swipe-row {
  position: relative; z-index: 1;
  transition: transform var(--dur-card) var(--ease-ios);
}
.swipe-row.swipe-dragging { transition: none; }
.swipe-row.swipe-open { transform: translateX(-72px); }
/* Opaque only while swiping/open so the pad doesn't bleed through the
   otherwise-translucent row; resting rows stay transparent (unchanged). */
.swipe-row.swipe-dragging,
.swipe-row.swipe-open { background: var(--bg-1); }

.reminder-body { flex: 1; min-width: 0; }
/* .reminder-meta composes .caption in markup for the size (12px). Color
   --text-3 matches the canonical subtitle pattern across the app. */
.reminder-meta {
  color: var(--text-3);
  display: flex; align-items: center; gap: var(--s-1h);
  flex-wrap: wrap;
}
.reminder-meta .dot-sep { opacity: .5; }
.reminder-meta .countdown {
  color: var(--sage); font-weight: 500;
}

/* Reminder row time-lead — the time is rendered large (≈22px,
   ivory) and tucks the count metadata directly under it
   as a quiet subline — matching the typographic rhythm used on
   Profile, Library, and Stats. The "All Reminders" master toggle
   banner is intentionally absent: a global kill-switch belongs in
   Profile → Settings, not as permanent chrome on the most-used
   surface. Master toggle is still wired (remindersToggleMaster) and
   can be re-surfaced via Profile when needed.
   ─────────────────────────────────────────────────────────────── */


/* Bell-master toggle (header right slot) */
/* The bell lives ONLY in .reminders-topbar and must read as the SAME glassy
   disc as the X (base .icon-btn) — identical size, fill, hairline and --elev-1
   lift — in BOTH themes. So the on/off states tint ONLY the GLYPH (sage when
   on, muted when off) and inherit the disc chrome from .icon-btn. Previously
   each state redefined the disc fill/border/shadow, which silently drifted from
   the .icon-btn base once it became a raised glass disc (2026-08-05) — the bell
   then read flat beside the X, and its hardcoded gradient couldn't match the
   light-theme disc either. Keeping only `color` here means the disc can never
   diverge again. (2026-08-11) */
.rem-bell-master {
  position: relative;
}
.rem-bell-master.is-on       { color: var(--sage); }
.rem-bell-master.is-off      { color: var(--text-3); }
.rem-bell-master.is-off:hover { color: var(--text-2); }
/* When master is off, ALL the in-page content reads as paused. Anchored on
   #reminders-sheet now that the bell lives in the top bar (a sibling of the
   composer), not inside .rem-composer. */
#reminders-sheet:has(.rem-bell-master.is-off) .rem-history-eyebrow,
#reminders-sheet:has(.rem-bell-master.is-off) .reminders-active-list-soft {
  opacity: 0.62;
  transition: opacity var(--dur-state) var(--ease-ios);
}

/* Inline chip next to the "Repeat" label — surfaces the current
   day-set label (e.g. "Mon-Fri") as a sage pill when something is
   picked, or a quiet ghost pill ("Select days") when empty. */

/* — Permission warning (when notifications denied/needed) — */
.reminder-perm-warn {
  margin: 0 var(--s-5) var(--s-3);
  padding: var(--s-2h) var(--s-3h);
  display: flex; align-items: flex-start; gap: var(--s-2h);
  border-radius: var(--r-md);
  background: rgba(212,165,116,.08);
  border: 1px solid rgba(212,165,116,.22);
  color: var(--text-2); line-height: 1.45;
  /* .reminder-perm-warn composes .caption in markup for the size; lh:1.45 matches .caption. */
}
.reminder-perm-warn svg { width: 14px; height: 14px; flex-shrink: 0; margin-top: 1px; color: var(--amber); fill: none; stroke: currentColor; stroke-width: 1.8; }
.reminder-perm-warn b { color: var(--amber); font-weight: 600; }
.reminder-perm-warn button {
  /* Per one-accent rule, the action CTA on a warning banner is sage, */
  background: var(--sage); color: var(--on-sage);
  border: none; padding: var(--s-1) var(--s-2h); border-radius: 999px;
  font-size: var(--fs-label); font-weight: 600; letter-spacing: .3px;
  cursor: pointer; margin-left: auto; flex-shrink: 0;
  font-family: var(--font-body);
}

/* — Edit-form view — */

/* Reminder name input now uses the shared .field-input (03-primitives). */

/* — Icon palette — */

/* — Time picker — */
/* Scroll-Wheel Drum Time Picker */
/* Wheel container — FLAT. The bordered rounded chrome (1px border,
   14px radius) was stripped; the wheel now sits directly on the
   sheet canvas like every other row group. The selection band
   below carries the only remaining affordance for which value is
   centered. */
.reminder-time-wheel-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0 var(--s-4h);
  position: relative;
  overflow: hidden;
  height: 140px;
}
/* Selection band — a subtle semi-transparent highlight box behind the centered
   row (native iOS/Android picker feel), instead of bare hairlines. Kept faint
   (--white-mist over the raised wheel surface) so it reads as a soft window,
   not the sage "gradient" pill that was removed earlier. Sits BEHIND the digits
   (z-index 0; the columns + colon are lifted to z-index 1 below). */
.reminder-time-wheel-wrap::before {
  content: '';
  position: absolute;
  left: var(--s-2); right: var(--s-2);
  top: 50%;
  transform: translateY(-50%);
  height: 48px;
  background: var(--white-mist);
  border: 0;
  border-radius: var(--r-md);
  pointer-events: none;
  z-index: 0;
}
/* Top & bottom fade masks — fade into page canvas, not card. */
/* No edge fade-mask: it painted var(--bg) (#040912) over the top/bottom,
   which read DARKER than the --grad-sheet page and made the wheel look like
   a gradient card. Edge numbers are already dimmed via --text4 on the items,
   so the wheel now blends flat into the sheet. (Override copy in home.css
   was also removed — the fade was defined in TWO places.) */
.reminder-wheel-col {
  flex: 1;
  height: 140px;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  position: relative;
  z-index: 3;
}
.reminder-wheel-col::-webkit-scrollbar { display: none; }
.reminder-wheel-item {
  height: 44px;
  display: flex; align-items: center; justify-content: center;
  scroll-snap-align: center;
  font-family: var(--font-display);
  font-weight: 300;
  color: var(--text-3);
  cursor: pointer;
  transition: color var(--dur-tap);
  user-select: none;
  letter-spacing: .5px;
}
/* Padding items (top & bottom) for centering edge values */
.reminder-wheel-item.pad {
  height: 48px;
  pointer-events: none;
  visibility: hidden;
}
.reminder-wheel-sep {
  font-family: var(--font-display);
  font-size: var(--fs-h2-lg); font-weight: 300; /* lint-skip CAT-T3: .reminder-wheel-sep — sizes the wheel separator glyph, not a title */
  color: var(--text-3);
  padding: 0 var(--s-1);
  z-index: 3;
  position: relative;
  pointer-events: none;
  line-height: 1;
}

/* — Day-of-week picker — */

/* — Haptic / channel toggles in edit form — */

.reminder-opt-icon {
  /* Layout + 24px glyph from the shared .row-icon; this companion only
     resets the tile-sage box fill. */
  background: transparent;
  border: none;
}
.reminder-opt-body { flex: 1; min-width: 0; }
/* .reminder-opt-label composes .row-title in markup (16px / 500) — same
   list-row title style as Profile / Settings, so the reminder rows read as
   harmoniously as the rest of the app. Companion only sets the colour. */
.reminder-opt-label { color: var(--text); }
/* .reminder-opt-sub composes .sub-text in markup — canonical row-sub
   pattern (12px / lh 1.45 / ls .1px / weight 400). The companion only
   overrides color (--text-3 → --text-2) since draft editor subs read
   as values the user is reviewing, not whisper-quiet helper text. */
.reminder-opt-sub { color: var(--text-2); margin-top: 1px; }

/* — Unified sheet: collapsed list above editor — */

/* — Fixed footer for Set Reminder — */

/* — Sheet body scroll container — NO SCROLL in edit view — */
.reminders-sheet-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  padding-bottom: calc(var(--s-5) + var(--safe-bottom));   /* full-page bottom clearance */
  flex: 1;
}
.reminders-active-list {
  margin: 0 var(--s-5);
}
.reminders-active-list::-webkit-scrollbar { width: 4px; }
.reminders-active-list::-webkit-scrollbar-thumb {
  background: var(--white-film); border-radius: 2px;
}
.reminders-sheet-scroll::-webkit-scrollbar { width: 4px; }
.reminders-sheet-scroll::-webkit-scrollbar-thumb {
  background: var(--white-film); border-radius: 2px;
}

/* Reminders as a FULL PAGE (2026-07-20) — was a bottom sheet. Fills the
   whole overlay with a flush top bar (X left, bell right), no rounded sheet
   corners / bottom padding; the composer + list scroll inside. */
#reminders-overlay { justify-content: stretch; animation: none; }   /* the sheet carries the page-fade-in */
.reminders-topbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--s-3);
  flex-shrink: 0;
  padding:
    calc(12px + var(--safe-top))
    max(var(--gutter), var(--safe-right))
    var(--s-2)
    max(var(--gutter), var(--safe-left));
}
/* 3-column grid: X pinned left, title dead-centre on the page, bell right. */
.reminders-topbar > :first-child { justify-self: start; }
.reminders-topbar > :last-child { justify-self: end; }
/* Composes .page-title (--fs-h2) so it matches every other page title
   (History, Statistics); centred, single line. */
.reminders-topbar-title {
  justify-self: center;
  text-align: center;
  white-space: nowrap;
}
#reminders-sheet {
  height: 100%;
  max-height: none;
  border-radius: 0;
  border-top: none;
  padding-bottom: 0;
  display: flex;
  flex-direction: column;
  will-change: opacity, transform;
  /* Same entrance as a routed page (was the bottom-sheet slide-up). */
  animation: page-fade-in var(--dur-page) var(--ease-out-soft) both;
  /* Flat --bg-0 page canvas (2026-07-21) — reminders is a full PAGE now, so it
     must match the other pages' background, NOT the --grad-sheet gradient every
     bottom SHEET uses. Overrides .tech-modal-sheet's --grad-sheet. */
  background: var(--bg-0);
}

/* Scope every surface inside the sheet so they can't inherit navy.
   Card fill matches the home-screen cards exactly — the faint 2%
   --surface-soft wash over the canvas (was --white-veil 4%, which read
   as visibly more opaque than every other page). Flat, no gradient. */
/* Flat list-panel surface (the old .settings-card recipe): 2% --surface-soft fill, 6% --white-mist edge,
   --r-lg radius. (Kept in sync with the inline !important block in index.html,
   which is what actually renders — see [[index-inline-style-override]].) */
/* New Reminder sheet — FLAT. The id-scoped overrides that used to
   revive the .rem-device-card and .reminder-time-wheel-wrap chrome
   (surface fill, 1px white-mist border, 14px radius) are gone so
   the partial-level transparent rules can finally read through. The
   wheel sits directly on the sheet canvas; the Date/Repeat/Push/
   Haptic rows sit on the canvas with their own hairlines between
   them; the "Your reminders" history list does the same. */
#reminders-sheet .rem-device-cell { background: transparent; }
#reminders-sheet .rem-device-cell-tap:hover { background: var(--white-mist); }
#reminders-sheet .reminders-sheet-scroll {
  flex: 1;
  max-height: none;
  overflow-y: auto;
  /* Flex column + composer flex:1 pushes the sticky Save CTA to the very
     bottom of the page even when the composer is short (full-page 2026-07-21). */
  display: flex;
  flex-direction: column;
}
#reminders-sheet .rem-composer { flex: 1; }
/* The handle becomes the swipe-grab affordance — applies to every */
.tech-modal-sheet .modal-handle {
  cursor: grab;
  width: 44px;
  height: 5px;
  margin-top: var(--s-2h);
  transition: background var(--dur-tap) ease, transform var(--dur-tap) ease;
  position: relative;
}
.tech-modal-sheet .modal-handle::before {
  content: '';
  position: absolute;
  /* Halo: invisible tappable region around the pill. */
  inset: -20px -40px;
  /* Ensure pointer events land on the pill, not the body behind. */
  pointer-events: auto;
}
.tech-modal-sheet .modal-handle:hover { background: var(--white-grip); }
.tech-modal-sheet .modal-handle:active { cursor: grabbing; background: var(--white-grip-2); }
.tech-modal-sheet.dragging { transition: none; animation: none !important; }
.tech-modal-sheet.dragging .modal-handle { background: var(--white-bright); }

/* Composer grab wrapper: invisible container that lets the JS */
.composer-grab { cursor: grab; user-select: none; }
.composer-grab:active { cursor: grabbing; }

/* Reminders-specific override kept for back-compat (no longer
   strictly necessary now that the rules above are universal, but
   preserved so any reminders-specific tuning can attach here). */
#reminders-sheet .modal-handle {
  cursor: grab;
}
#reminders-sheet.dragging { transition: none; }
#reminders-sheet .modal-handle:active { cursor: grabbing; }

/* — Fire/flash animation when a reminder fires (visual confirmation
     even before the OS push appears) — */
@keyframes reminderFire {
  0%   { box-shadow: 0 0 0 0 var(--sage-glow-peak); }
  60%  { box-shadow: 0 0 0 12px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* — Inline toast that confirms a reminder fired (sits under top nav) — */
.reminder-toast {
  position: absolute;
  top: 80px; left: 50%;
  transform: translateX(-50%);
  background: var(--grad-lift);
  border: 1px solid var(--sage);
  border-radius: 999px;
  padding: var(--s-2h) var(--s-4h) var(--s-2h) var(--s-3h);
  display: flex; align-items: center; gap: var(--s-2h);
  box-shadow: var(--elev-2), var(--elev-glow-sage);
  z-index: var(--z-toast);
  animation: toastIn var(--dur-card) var(--ease-out-soft), toastOut var(--dur-state) var(--ease-ios) 3.7s forwards;
  max-width: 86%;
}
@keyframes toastIn  { from { opacity: 0; transform: translate(-50%, -10px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; transform: translate(-50%, -10px); } }
.reminder-toast-icon {
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--sage-170);
  color: var(--sage);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.reminder-toast-icon svg { width: 12px; height: 12px; stroke-width: 2; fill: none; }
/* .reminder-toast-text composes .caption in markup; lh:1.3 overrides .caption's 1.45 (tighter toast). */
.reminder-toast-text {
  color: var(--text);
  line-height: 1.3;
}
.reminder-toast-text b { color: var(--sage); font-weight: 600; }

/* — Achievement-unlock toast — reader for S._newAchievements, shown on the
     session-complete screen (renderAchievementToast in achievements.js). A
     tappable pill routing to the achievements grid; --tier / --tier-glow are
     set inline per badge tier. Reuses toastIn/toastOut from the reminder toast
     above so entry/exit motion matches. Eyebrow/label compose .caption/.body-text
     in markup, so no font-size lives here. — */
.ach-toast {
  position: absolute;
  top: 80px; left: 50%;
  transform: translateX(-50%);
  background: var(--grad-lift);
  border: 1px solid var(--tier, var(--sage));
  border-radius: var(--r-pill);
  padding: var(--s-2h) var(--s-4h) var(--s-2h) var(--s-2h);
  display: flex; align-items: center; gap: var(--s-3);
  box-shadow: var(--elev-2), 0 0 24px var(--tier-glow, transparent);
  z-index: var(--z-toast);
  animation: toastIn var(--dur-card) var(--ease-out-soft), toastOut var(--dur-state) var(--ease-ios) 4.2s forwards;
  max-width: 86%;
  cursor: pointer;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.ach-toast-badge {
  width: var(--icon-xl); height: var(--icon-xl);
  border-radius: var(--r-circle);
  background: var(--tier-glow, var(--sage-170));
  color: var(--tier, var(--sage));
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.ach-toast-badge svg { width: var(--icon-sm); height: var(--icon-sm); stroke-width: 2; fill: none; }
.ach-toast-text { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.ach-toast-eyebrow {
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: .06em;
  font-weight: 600;
  white-space: nowrap;   /* keep the eyebrow on one line so the pill sizes to it, not the shorter label */
}
.ach-toast-label { color: var(--text); font-weight: 600; white-space: nowrap; }

/* — Share-results clipboard-confirm toast — fires on the desktop
   fallback path of shareSessionResults() (mobile gets the native
   OS share sheet, no toast needed). Single line of body text, no
   icon; reuses the toastIn/toastOut keyframes the reminder toast
   above defines so the entry/exit animation matches without
   duplicating motion. */
.share-toast {
  position: absolute;
  bottom: calc(120px + var(--safe-bottom));
  left: 50%;
  transform: translateX(-50%);
  background: var(--grad-lift);
  border: 1px solid var(--border-warm);
  border-radius: 999px;
  padding: var(--s-2h) var(--s-4);
  font-family: var(--font-body);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: toast button-style text, lh:1.45 would grow it */
  font-weight: 500;
  color: var(--text);
  letter-spacing: .1px;
  white-space: nowrap;
  box-shadow: var(--elev-2);
  z-index: var(--z-toast);
  animation: toastIn var(--dur-card) var(--ease-out-soft),
             toastOut var(--dur-state) var(--ease-ios) 1.8s forwards;
  pointer-events: none;
}

.boot-splash {
  position: fixed; inset: 0;
  background:
    radial-gradient(ellipse at 50% 38%, var(--sage-90) 0%, transparent 55%),
    radial-gradient(ellipse at 50% 80%, rgba(125,142,176,0.06) 0%, transparent 60%),
    linear-gradient(180deg, var(--bg-0) 0%, var(--bg-2) 60%, var(--card-1) 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: var(--z-top);
  /* main.js wipes us by replacing #app.innerHTML, but if anything ever
     needs to fade us out manually, this transition handles it. */
  transition: opacity .5s ease, visibility .5s ease;
}

/* The breathing aura behind the logo */
.boot-splash-aura {
  position: absolute;
  width: 280px; height: 280px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--sage-170) 0%, var(--sage-wash) 55%, transparent 75%);
  filter: blur(8px);
  animation: splash-breathe 3.6s ease-in-out infinite;
  pointer-events: none;
}
@keyframes splash-breathe {
  0%, 100% { transform: scale(0.92); opacity: .55; }
  50%      { transform: scale(1.08); opacity: 1;  }
}

/* The orb mark — a glowing filled circle that pops in, then gently breathes
   (scale + glow) in sync with the aura, echoing the app's breathing orb.
   Replaced the retired triquetra (three stroke-drawn circles). */
.boot-splash-mark {
  position: relative;
  width: 92px; height: 92px;
  margin-bottom: var(--s-6);
  animation: splash-pop 900ms var(--ease-out-soft) both;
}
/* overflow:visible so the breathing scale + glow aren't clipped by the box. */
.boot-splash-mark svg {
  width: 100%; height: 100%; display: block; overflow: visible;
  transform-origin: center;
  filter: drop-shadow(0 0 14px var(--sage-100));
  animation: splash-orb-breathe 3.6s ease-in-out infinite;
}
@keyframes splash-orb-breathe {
  0%, 100% { transform: scale(.84); opacity: .9; }
  50%      { transform: scale(1);   opacity: 1;  }
}
@keyframes splash-pop {
  from { opacity: 0; transform: scale(.85); }
  to   { opacity: 1; transform: scale(1);   }
}

.boot-splash-title {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: 6px;
  text-transform: uppercase;
  color: var(--text);
  opacity: 0;
  animation: splash-fade-up .8s ease .55s forwards;
}
.boot-splash-tagline {
  margin-top: var(--s-2h);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: var(--text-2);
  opacity: 0;
  animation: splash-fade-up .8s ease .75s forwards;
}
@keyframes splash-fade-up {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: .9; transform: translateY(0);   }
}

/* The shimmering progress bar — pure aesthetic, no progress data */
.boot-splash-bar {
  position: relative;
  margin-top: var(--s-6);
  width: 140px;
  height: 2px;
  border-radius: 999px;
  background: var(--sage-soft);
  overflow: hidden;
  opacity: 0;
  animation: splash-fade-up .8s ease .95s forwards;
}
.boot-splash-bar::after {
  content: '';
  position: absolute;
  top: 0; left: -40%;
  width: 40%; height: 100%;
  background: linear-gradient(90deg,
    transparent 0%,
    transparent 0%,
    var(--sage-shimmer) 50%,
    transparent 100%);
  border-radius: 999px;
  filter: blur(.2px);
  animation: splash-shimmer 1.6s cubic-bezier(.5,0,.5,1) 1.1s infinite;
  box-shadow: 0 0 12px var(--sage-glow-peak);
}
@keyframes splash-shimmer {
  0%   { left: -40%; }
  100% { left: 100%; }
}

/* ════════════════════════════════════════════════════════════════
   Batch A #6 — Reduced-motion support
   ────────────────────────────────────────────────────────────────
   When the user has set their OS to reduce motion (System Settings
   → Accessibility), disable purely decorative animations:
     – page fade-in on route changes
     – sheet/modal slide-in
     – pulse dots, shimmer, heart-pop, check-pop, reveal-up
     – live-feed ring pulse, haptic ripple, phase flash, skeleton pulse
     – glow rings around the orb (purely aesthetic)

   What we DO NOT disable:
     – orb-inhale / orb-exhale  (the breath cue itself — carries
       timing information; disabling would break the technique)
     – orbGlowInhale / orbGlowExhale  (paired with the breath cue,
       same justification)

   HOLD has no animation by design (the orb stays still during the
   hold phase — the stillness is the cue), so there is nothing to
   restore for HOLD under reduced-motion.

   Reference: WCAG 2.3.3 "Animation from Interactions" / Apple HIG.
   ════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  /* Nuclear catch-all: kill every animation by default. We then
     restore the orb-breathing animations below since those carry
     the breath cue and qualify as essential motion under WCAG 2.3.3. */
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  /* Restore the orb's breath-cue animations. These are the *content*
     of a breathwork session, not decoration. INHALE and EXHALE only
     — HOLD is static by design. */
  .orb-inner.phase-INHALE {
    animation-duration: var(--orb-inhale-dur, 4s),
                        var(--orb-inhale-dur, 4s) !important;
    animation-iteration-count: 1, 1 !important;
  }
  .orb-inner.phase-EXHALE {
    animation-duration: var(--orb-exhale-dur, 8s),
                        var(--orb-exhale-dur, 8s) !important;
    animation-iteration-count: 1, 1 !important;
  }

  /* Restore functional (non-decorative) animations: */
  .reminder-toast {
    animation-duration: 0.35s, 0.3s !important;
    animation-iteration-count: 1, 1 !important;
  }

  /* Static fallback for the orb during a session:
     When the orb can't animate its scale, add a colour-only border
     cue so the phase (inhale/hold/exhale) is still distinguishable
     without any motion. Colors must match PHASE_COLORS in constants.js:
     INHALE=sage, HOLD=amber, EXHALE=lavender. */
  .orb-inner.phase-INHALE { border-color: var(--sage-phase-border) !important; }
  .orb-inner.phase-HOLD   { border-color: rgba(212,165,116,0.9) !important; }
  .orb-inner.phase-EXHALE { border-color: rgba(168,156,217,0.9) !important; }

  /* Boot splash under reduced motion: orb static, no breathe/pop. */
  .boot-splash-aura     { animation: none !important; opacity: .8 !important; }
  .boot-splash-mark     { animation: none !important; }
  .boot-splash-mark svg { animation: none !important; transform: scale(1) !important; }
  .boot-splash-title,
  .boot-splash-tagline,
  .boot-splash-bar      { animation: none !important; opacity: .9 !important; }
  .boot-splash-bar::after { display: none !important; }
}

/* ════════════════════════════════════════════════════════════════
   HOME SCREEN v2  (redesign)
   ────────────────────────────────────────────────────────────────
   The redesigned Home is built around a clear visual hierarchy:
     1. Greeting header   — eyebrow + display title + streak strip
     2. Quick pill        — 1-Min Anxiety Relief shortcut
     3. FOR YOU section   — hero card + two mini cards in a grid
     4. Bottom nav        — handled by global .nav

   All new classes are prefixed `home-` to avoid colliding with the
   existing .lib-card / .streak-pill / .h2-display rules that other
   pages still use.
   ════════════════════════════════════════════════════════════════ */

/* The .screen container already provides flex + padding; we only
   tighten vertical rhythm specifically for the home route. */
.home-screen { padding-top: 0; }

/* ════════════════════════════════════════════════════════════════
   SAFETY OVERLAY — pre-session safety notice (pgSafetyOverlay).
   Extracted from inline styles; the overlay is fully static. Two raw
   rgba values have no token home and are intentionally kept literal:
   the scrim rgba(8,16,28,.92) and the moon clinical-note tint
   rgba(125,142,176,.08) (a --moon-50 token would be the clean
   follow-up). Off-scale snaps during extraction: overlay/card/header
   20px→22px (--s-5 / --r-2xl), card pad 28/24→32/22, header clearance
   36px→32px (--s-6), warning text 13px→14px (--fs-body), note corners
   8px→9px (--r-sm), three .2s→var(--dur-state) animations.
   ════════════════════════════════════════════════════════════════ */
.safety-overlay {
  /* Frosted-navy surface comes from the shared .sheet-scrim alongside —
     unified with every other overlay (was a heavier rgba(4,7,11,0.92)). */
  position: fixed; inset: 0;
  z-index: var(--z-top);
  display: flex; align-items: center; justify-content: center;
  padding: var(--s-5);                         /* was 20px */
  animation: fadeIn var(--dur-state) ease;     /* was .2s */
}
.so-card {
  background: var(--card);
  border: 1.5px solid var(--rose-100);
  border-radius: var(--r-2xl);                 /* was 20px */
  max-width: 320px; width: 100%;               /* compact popup, not a full card */
  max-height: 82vh; overflow-y: auto;
  padding: var(--s-5) var(--s-4h) var(--s-4h); /* tighter than the old 28/24 */
  box-shadow: 0 0 40px var(--rose-50);
  position: relative;
}
.so-close {
  /* top:27 vertically centres the 32px X on the 42px shield tile: the shield
     starts at the card's --s-5 (22px) top padding, so its centre is 22+21=43px;
     the X centre matches at 27+16=43px. right:14 keeps the corner inset. */
  position: absolute; top: 27px; right: 14px;
  width: 32px; height: 32px;
  border-radius: var(--r-circle);
  background: var(--white-veil);
  border: 1px solid var(--white-line);
  color: var(--text-2);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; padding: 0; z-index: 1;
  transition: background var(--dur-state), color var(--dur-state),
              border-color var(--dur-state);
}
.so-close:hover { background: var(--white-film); border-color: var(--white-rule-2); color: var(--text); }
.so-close:active { transform: scale(.94); }
.so-close svg { width: 15px; height: 15px; }
/* Invisible touch-target — bumps the close button's hit area from
   32×32 to the WCAG 2.5.5 / iOS HIG 44×44 floor without growing the
   visible disc. Pseudo catches taps in the larger bounding box; the
   click bubbles to the parent <button>. */
.so-close::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 44px; height: 44px;
}
.so-header {
  display: flex; align-items: flex-start;
  gap: var(--s-3);                             /* 12px */
  margin-bottom: var(--s-3h);                  /* tighter (was 20px) */
  padding-right: var(--s-6);                   /* clears the top-right X */
}
/* Title/eyebrow column — min-width:0 lets a long title wrap instead of overflowing. */
.so-header-text { flex: 1; min-width: 0; }
.so-icon {
  width: 42px; height: 42px; flex-shrink: 0;   /* compact (was 48) */
  border-radius: var(--r-lg);                  /* 14px */
  background: var(--rose-100);
  border: 1px solid var(--rose-100);
  display: flex; align-items: center; justify-content: center;
  color: var(--rose);
}
.so-icon-glyph { width: 22px; height: 22px; display: inline-flex; }
.so-title {
  font-family: var(--font-display);
  /* 16px */
  font-weight: 600;
  color: var(--rose);
  letter-spacing: .2px;
}
.so-eyebrow {
  font-size: var(--fs-caption); /* lint-skip CAT-T2: uppercase tracked eyebrow label, semantically a label tier (like .section-label), not body caption */
  color: var(--text-2);                        /* was --text-3 — slightly brighter for readability (user) */
  letter-spacing: .6px;
  text-transform: uppercase;
  margin-bottom: 3px;                          /* kicker sits ABOVE the title */
}
/* .so-intro composes .body-text in markup for the --fs-body / lh-body pair. */
.so-intro {
  color: var(--text-2);
  margin: 0 0 var(--s-3h);                      /* tighter (was 18px) */
}
/* Bolded risk words inside the intro — full-bright + heavy so they pop against
   the muted --text-2 body for fast skimmers. */
.so-risk { font-weight: 700; color: var(--text); }
.so-warnings {
  /* Callout, not a nested box: dropping the filled inner card + rounded border
     removes the "box-in-a-box" noise (user 2026-07-29). A 3px coral left rule
     frames the cautions against the card's own background instead. */
  border-left: 3px solid var(--rose);
  padding: var(--s-1) 0 var(--s-1) var(--s-4);
  margin-bottom: var(--s-4);                   /* 16px */
}
.so-warn-item {
  display: flex; align-items: flex-start;
  gap: var(--s-2h);                            /* 10px */
  margin-bottom: var(--s-2);                   /* tighter (was 10px) */
}
.so-warn-item:last-child { margin-bottom: 0; }
/* Subtle rose dot bullet — replaced three repeated X icons (they read punitive
   and repetitive). margin-top optically centres the 5px dot on the first line. */
.so-warn-dot {
  flex-shrink: 0;
  width: 5px; height: 5px;
  margin-top: var(--s-1h);                     /* ~centre the dot on the first text line */
  border-radius: 50%;
  background: var(--rose);
  opacity: .75;
}
/* .so-warn-text composes .caption in markup for the size. */
.so-warn-text {
  color: var(--text);
  line-height: 1.45;
}
.so-cta {
  margin-bottom: 0;
  border: 1.5px solid transparent;             /* reserve space so the :disabled outline never shifts height */
  /* Animate the disabled→active flip (checkbox tick removes [disabled]) from the
     low-contrast outline into the full vibrant sage fill — an instant, satisfying
     "you can proceed" pop instead of a hard snap. */
  transition: background var(--dur-state) var(--ease-ios),
              border-color var(--dur-state) var(--ease-ios),
              color var(--dur-state) var(--ease-ios);
}
/* Required acknowledgment checkbox — gates .so-cta (disabled until ticked). */
.so-ack {
  display: flex; align-items: flex-start; gap: var(--s-2h);
  width: 100%;
  background: none; border: none; padding: 0;
  margin: 0 0 var(--s-4);
  cursor: pointer; text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.so-ack-box {
  flex-shrink: 0;
  width: 20px; height: 20px;
  /* Optically centre the box on the first line of the label (line-height
     1.5 × 13px ≈ 20px) so the tick aligns with the text, not floating high. */
  margin-top: 1px;
  border-radius: var(--r-xs);
  /* Higher-contrast border so the empty box clearly reads as an interactive
     control against the dark card, not a static detail (was 1px --white-line 0.10). */
  border: 1.5px solid var(--white-grip);
  background: var(--white-veil);
  color: transparent;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--dur-state) var(--ease-ios),
              border-color var(--dur-state) var(--ease-ios),
              color var(--dur-state) var(--ease-ios);
}
.so-ack-box svg { width: 13px; height: 13px; }
.so-ack.is-checked .so-ack-box {
  background: var(--sage-soft);
  border-color: var(--sage);
  color: var(--sage);
}
/* .so-ack-label composes .caption in markup; lh:1.5 overrides .caption's 1.45. */
.so-ack-label {
  color: var(--text-2);
  line-height: 1.5;
}
/* Explicit disabled: a low-contrast OUTLINE, not a dimmed cyan fill (which read
   as a muddy "semi-active" button). Clearly reads as not-yet-tappable; ticking
   the ack box removes [disabled] and it snaps to the vibrant sage primary. */
.so-cta:disabled {
  background: transparent;
  border-color: var(--white-line);
  color: var(--text-3);
  cursor: not-allowed;
}
.so-link-wrap {
  text-align: center;
  margin-top: var(--s-3h);                     /* 14px */
}
.so-link {
  background: none; border: none;
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .so-link is a button (text-link button) — lh:1.45 would change link button height */
  color: var(--text-3);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ════════════════════════════════════════════════════════════════
   PRESET SAVE DIALOG — "Save as Preset" name prompt (pgCustom).
   Extracted from inline styles; fully static. The two scrim/shadow
   blacks (rgba(0,0,0,…)) have no token home and stay literal. Off-
   scale snaps: card radius 20px→22px (--r-2xl), card pad 22/20/18→
   22/22/18, titles 15px→16px (--fs-md), button text 13px→14px
   (--fs-body), input pad 11/13→12/14, button pad 11px→12px,
   .25s reveal→var(--dur-state), z-index 120→var(--z-modal).
   ════════════════════════════════════════════════════════════════ */
.psd-backdrop {
  /* Frosted-navy surface comes from the shared .sheet-scrim alongside. */
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  z-index: var(--z-modal);                      /* was 120 */
  padding: var(--s-5);                          /* was 24px → 22px */
  animation: fadeIn var(--dur-state) ease;      /* was .2s */
}
.psd-card {
  width: 100%; max-width: 340px;
  /* CANONICAL SHEET SURFACE — same --grad-sheet every bottom sheet uses
     (see .tech-modal-sheet). Was --white-wash-2 (a faint white-wash), which
     read as a different, washed-out surface vs the navy sheets — and the
     transparent .dialog-btn-cancel sitting on it looked wrong next to the same
     button on an Edit-Profile / reminders sheet. 2026-06-22. */
  background: var(--grad-sheet);
  border: 1px solid var(--border);
  border-radius: var(--r-2xl);                  /* was 20px */
  padding: var(--s-5) var(--s-5) var(--s-4h);   /* was 22px 20px 18px */
  box-shadow: 0 20px 60px rgba(0,0,0,.5);       /* bespoke — no elev token */
  animation: revealUp var(--dur-state) var(--ease-out-soft);  /* was .25s + raw curve */
}

/* Reminder discard-confirm — a lightweight alert, not a full bottom sheet.
   Anchor it to the TOP-LEFT corner (clear of the status bar) and drop it in
   from above instead of rising from the bottom edge. (user 2026-08-04) */
#reminder-discard-overlay {
  justify-content: flex-start;
  align-items: flex-start;
  padding: calc(env(safe-area-inset-top, 0px) + var(--s-6)) var(--s-4) 0;
}
#reminder-discard-overlay .psd-card {
  max-width: 300px;
  animation: revealDown var(--dur-state) var(--ease-out-soft);
}
@keyframes revealDown {
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.psd-head {
  display: flex; align-items: center;
  gap: var(--s-2h);                             /* 10px */
  margin-bottom: var(--s-3h);                   /* 14px */
}
.psd-head-icon {
  width: 36px; height: 36px;
  border-radius: var(--r-circle);
  background: var(--sage-soft);
  color: var(--sage);
  display: flex; align-items: center; justify-content: center;
}
.psd-head-icon-glyph { width: 18px; height: 18px; }
.psd-head-text { flex: 1; }
.psd-sub {
  font-size: var(--fs-label);                   /* 11px */
  color: var(--text-3);
  margin-top: 1px;
}
/* Save-preset name input now uses the shared .field-input (03-primitives). */
/* .dialog-btn — the compact side-by-side dialog action pair (Cancel + Save),
   shared by the Save-preset dialog and the Edit-profile sheet. Neutral-outline
   cancel (recessive, --border/--text-2) + solid-sage save (--on-sage; no
   gradient — gradients are a home-only treatment). Replaces the old forked
   .psd-btn-* and .edit-prof-* pairs. */
.dialog-actions {
  display: flex;
  gap: var(--s-2h);
  margin-top: var(--s-4);
}
.dialog-btn {
  flex: 1;
  padding: var(--s-3h) 0;
  border-radius: var(--r-pill);
  cursor: pointer;
  font-family: var(--font-body);
  font-size: var(--fs-body); /* lint-skip CAT-T1: dialog button — body-text lh:1.5 would grow the button ~5px */
  font-weight: 600;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur-state) var(--ease-ios),
              border-color var(--dur-state) var(--ease-ios),
              color var(--dur-state) var(--ease-ios),
              opacity var(--dur-state) var(--ease-ios),
              transform var(--dur-tap) var(--ease-ios);
}
.dialog-btn:active { transform: scale(.98); }
.dialog-btn-cancel {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-2);
}
.dialog-btn-cancel:hover { border-color: var(--text-2); color: var(--text); }
/* Dark mode — the secondary dialog copy (.psd-sub helper) and the ghost
   Cancel / "Keep going" label sit borderline on the deep card. Brighten to a
   medium slate (#94A3B8, ~6.5:1 on the navy) so they're effortless to read
   while staying clearly secondary to the white title; the ghost button keeps
   its outline-only container. Light keeps --text-3/--text-2 (already AA there,
   and #94A3B8 would read too light on the light ground). (user 2026-08-05) */
:root[data-theme="dark"] .psd-sub,
:root[data-theme="dark"] .dialog-btn-cancel { color: #94A3B8; /* lint-skip CAT-16: dark-only AA medium slate for secondary dialog copy — brighter than --text-2, no matching token */ }
.dialog-btn-save {
  background: var(--sage);
  border: 1px solid var(--sage);
  color: var(--on-sage);
}
.dialog-btn-save:hover { opacity: .92; }
/* Destructive variant — rose outline + rose label (e.g. "Discard reminder"). */
.dialog-btn-discard {
  background: transparent;
  border: 1px solid var(--rose-100);
  color: var(--rose);
}
.dialog-btn-discard:hover { background: var(--rose-50); }

/* ===========================================================
   80-screens.css
   SCREENS — viewport/standalone-scoped blocks: in-app status bar (live network+battery), @media standalone, tech-modal sheet sizing, duration & background-music big sections.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* In-app status bar */
/* Renders ONLY on the home screen so it blends into the premium
   header rather than feeling tacked on. Default state: HIDDEN.
   The real OS status bar already shows the time / signal / battery
   in a browser context, so drawing our own on top produces an
   embarrassing "double clock" effect. We only restore the in-app
   bar when the app is launched in PWA standalone mode (where the
   OS bar is translucent and our content sits under it).
   Height matches the iOS/Android status-bar feel (~40px).
   No interaction — aria-hidden on the entire bar. */
.home-status-bar {
  display: none;
  align-items: center;
  justify-content: space-between;
  /* Self-sufficient horizontal gutter so the bar can be placed at the
     .screen root (above top-bars) on every page — not only inside
     .scroll-content (which is where it used to live, home-only, and
     inherit its gutter from). Matches the .scroll-content gutter math
     so the icons line up with page content on all screens. */
  padding:
    calc(10px + var(--safe-top, 0px))
    max(var(--gutter), var(--safe-right))
    var(--s-1h)
    max(var(--gutter), var(--safe-left));
  pointer-events: none;
  user-select: none;
}
/* PWA / standalone only: real OS chrome is hidden, so our in-app
   bar takes over to keep the premium full-bleed feel. */
@media (display-mode: standalone) {
  .home-status-bar { display: flex; }
}
/* .home-status-time composes .caption in markup for the size + family. */
.home-status-time {
  font-weight: 600;
  color: var(--text);
  letter-spacing: -.1px;
}
.home-status-icons {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  color: var(--text);
  opacity: .9;
}

/* Live status-bar state: network + battery */
.status-network,
.status-battery {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  transition: opacity var(--dur-state) var(--ease-ios, ease);
}
/* Wifi slash is hidden until the .is-offline modifier is on the */
.status-wifi-slash {
  stroke-dasharray: 30;
  stroke-dashoffset: 30;
  transition: stroke-dashoffset var(--dur-state) var(--ease-ios, ease);
}
.status-network.is-offline .status-wifi-slash {
  stroke-dashoffset: 0;
}
/* When offline, fade the bars + wifi arcs so the slash reads as
   the dominant signal. We don't change colour (no red alarm) —
   premium wellness app, not a network monitor. The user just
   needs a quiet visual truth. */
.status-network.is-offline {
  opacity: .55;
}

/* Battery level transitions smoothly when the API delivers a */
.status-battery-level {
  transition: width var(--dur-card) var(--ease-ios, ease), fill var(--dur-state) ease;
}
/* Low battery: warm amber instead of full text colour, signalling */
.status-battery.is-low .status-battery-level {
  fill: var(--amber, #d4a574);
  opacity: 1;
}
/* Charging: tint to sage to mirror the app's "all is well" accent.
   Subtle pulse cues that the cable is in. Pulse uses opacity only
   so the geometry stays exact — no layout jitter in the status row. */
.status-battery.is-charging .status-battery-level {
  fill: var(--sage, #7dd3e8);
  animation: status-battery-pulse 2.4s ease-in-out infinite;
}
@keyframes status-battery-pulse {
  0%, 100% { opacity: .9; }
  50%      { opacity: .55; }
}
/* Reduced motion: kill the charging pulse but keep the colour cue. */
@media (prefers-reduced-motion: reduce) {
  .status-battery.is-charging .status-battery-level {
    animation: none;
  }
  .status-wifi-slash,
  .status-battery-level {
    transition: none;
  }
}

/* Greeting header */
/* When the in-app status bar is hidden (default / browser context),
   the greeting needs to drop down so it sits comfortably below the
   real OS status bar — handled by --safe-top in non-standalone
   browsers as a fallback. */

/* In standalone mode the in-app bar already absorbs the safe-top
   inset, so we use a tighter rhythm to avoid double-spacing. */

/* Two-column: text on the left, ring widget on the right */

/* Override the legacy padding-bottom on .tech-modal-sheet so the
   sticky footer can sit flush against the safe-area edge. */
.modal-sheet-v2 {
  padding: 0;
}

/* Sticky header (icon, title, subtitle, How it works, close) */
.modal-header-v2 {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3h);
  padding:
    var(--s-2)
    max(18px, var(--safe-right))
    var(--s-3h)
    max(18px, var(--safe-left));
  flex-shrink: 0;
}
.modal-header-v2 .modal-icon-wrap {
  /* Inherits size/shape from the legacy rule above; we override the
     stroke width slightly for visual consistency with v2 buttons. */
  width: 48px; height: 48px;
  border-radius: var(--r-lg);
}
.modal-header-text {
  flex: 1;
  min-width: 0;
  padding-top: 2px;
}
/* Tech-modal title spacing — the only modal-specific addition to the
   shared .sheet-title: a 4px gap down to the subtitle. */
.modal-header-text .sheet-title { margin-bottom: var(--s-1); }
/* LEARN tab header — [← back] · title · [♥] are a single row of peers with no
   subtitle, so center them to each other. (Tune keeps flex-start for its taller
   48px icon + two-line "See how it works" text.) Zero the tune-only top pad +
   title bottom-gap so the title's optical center lines up with the two circles. */
.modal-header-v2--learn { align-items: center; }
.modal-header-v2--learn .modal-header-text { padding-top: 0; }
.modal-header-v2--learn .modal-header-text .sheet-title { margin-bottom: 0; }
/* .modal-subtitle-v2 rule removed 2026-05-29 — the "Focus · 4-4-4-4"
   sub-line under the tune-sheet title was dropped from markup. */
.modal-howto {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  background: none;
  border: none;
  padding: 0;
  color: var(--sage);
  font-family: var(--font-body);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .modal-howto is a button — lh:1.45 would grow its height */
  font-weight: 500;
  letter-spacing: .1px;
  cursor: pointer;
  transition: color var(--dur-state);
}
.modal-howto:hover { color: var(--text); }
/* 2026-06-02 — trailing chevron RESTORED to the modal-howto link
   markup (was removed 2026-05-31 then asked back by user audit:
   'See how it works at modal sheets bring back chevron pointing
   right'). The chevron is inline SVG with stroke=currentColor so
   it inherits the sage label color + the hover-to-text shift via
   the parent rule. .modal-howto's gap:3px (inline-flex) already
   spaces it from the label — no extra rule needed for the SVG
   beyond what inheritance + the parent gap provide. */
/* Close button now uses the shared .icon-btn .icon-btn--sm (no bespoke rule). */

/* Header favorite heart — same 32px circle as the old close button. Neutral
   outline when not favorited; sage filled when it is. Tap saves to favorites. */
/* Skeleton + hover glow from the shared .icon-btn / .icon-btn--sm alongside
   in markup; this keeps only the favorited (filled-sage) toggle state. */
.modal-fav-btn.is-fav {
  background: var(--sage-soft);
  border-color: var(--sage);
  color: var(--sage);
}
.modal-fav-btn.is-fav svg { fill: var(--sage); }

/* Scrollable body */
.modal-body-v2 {
  flex: 1;
  overflow-y: auto;
  /* Bottom padding clears the sticky footer CTA (~80px) so the last content —
     the "Practice safely" safety section on the Learn tab — can scroll fully
     into view instead of hiding behind the button + its fade gradient.
     s-8 (72) + s-3 (12) ≈ footer height + a little margin. (2026-06-14.) */
  padding:
    0
    max(18px, var(--safe-right))
    calc(var(--s-8) + var(--s-3) + var(--safe-bottom))
    max(18px, var(--safe-left));
  scrollbar-width: none;
}
.modal-body-v2::-webkit-scrollbar { display: none; }
.modal-tab-pane {
  display: flex;
  flex-direction: column;
  gap: var(--s-3h);
  animation: page-fade-in var(--dur-state) var(--ease-out-soft) both;
}

/* Duration section (label + mode toggle + chip grid + summary) */
.modal-section { padding: var(--s-1h) 0 var(--s-1); }
/* The tuning sheet's section head is the big-value variant only (.modal-section
   -head-big), which composes .section-head — see home.css. */

/* min / cycles segmented toggle */

/* Duration chips grid: 4 fixed values + a "Set duration" tail chip */

/* The "Set duration" chip uses a smaller font so the two-word label fits. */

/* Summary line under the breaths slider — was right-aligned to
   sit beneath the value chip on the right; reverted to left
   2026-05-29 when the eyebrow/value collapsed into a single
   left-aligned headline above. */
.modal-dur-summary {
  margin: var(--s-2) 0 0;
  font-size: var(--fs-label);
  color: var(--text-3);
  letter-spacing: .2px;
  text-align: left;
}

/* Quick-pick duration pills (5 / 10 / 15 min) under the slider. Three
   equal-width chips; active = sage. Hover matches the unified 6% wash. */
.modal-dur-pills {
  display: flex;
  gap: var(--s-2);
  margin-top: var(--s-3h);
  /* Symmetric breathing room before the Minutes scroller below — the top pill
     row otherwise sat a fraction too tight against it. (2026-06-13.) */
  margin-bottom: var(--s-3h);
}
/* Resting surface + tap-scale from .chip; below are the row-fill pill's own bits. */
.modal-dur-pill {
  flex: 1;
  padding: var(--s-2) 0;
  border-radius: var(--r-pill);
  font-weight: 600;
  letter-spacing: .2px;
  transition:
    background var(--dur-state) var(--ease-ios),
    border-color var(--dur-state) var(--ease-ios),
    color var(--dur-state) var(--ease-ios),
    transform var(--dur-tap) var(--ease-ios);
}
.modal-dur-pill:hover { background: var(--white-mist); border-color: var(--white-rule-2); color: var(--text); }
.modal-dur-pill.is-selected {
  background: var(--sage-soft);
  border-color: var(--sage);
  color: var(--sage);
}

/* Row group (Sound / Voice / Cues / Save as default) — flat.
   Was a card-shaped surface holding 1+ rows; now no chrome at all
   so the group sits directly on the sheet background, consistent
   with the flat treatment on Profile/Settings/Library. The
   .modal-row + .modal-row hairline below still divides each pair,
   so multi-row groups read as a list without the wrapping box. */
.modal-row-group {
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;
}
/* 'Save as my preset' now renders as the LAST row INSIDE the main group
   (2026-07-15), so the .modal-row + .modal-row hairline divides it and it sits
   flush with no gap. It used to be its own .modal-row-group, which inherited
   the .modal-tab-pane flex gap (14px) above it — a band that made the previous
   row (Haptic feedback) look taller than the rest. The old
   .modal-row-group.modal-custom-save border-top rule was retired with that. */
.modal-row {
  /* Row metric (flex, 14px gap, 14/16 padding, 56px min-height) comes from
     the shared .sheet-row alongside this class in the markup. */
  background: none;
  border: none;
  width: 100%;
  color: inherit;
  text-align: left;
  font-family: var(--font-body);
}
.modal-row + .modal-row {
  border-top: 1px solid var(--white-soft);
}
/* 2026-06-03 — hairline survives the .modal-targets-rows wrapper.
   That wrapper is ALWAYS rendered (stable-id slot for the Power
   Breath per-round target rows so pickCustomProtocol can swap its
   contents without re-running renderModal). The wrapper sits
   between the Breaths wheel row and the Sound row. Adjacent-sibling
   `.modal-row + .modal-row` only matches siblings of the SAME
   parent, so the empty wrapper broke the chain — user reported
   'add a divider between these two rows' (Breaths and Sound).
   First selector restores the Breaths→Sound hairline (and Round 3
   → Sound when the wrapper has content). Second selector covers
   the wrapper-first-child case so Round 1's top hairline lands
   when targets are populated. */
.modal-targets-rows + .modal-row,
.modal-row + .modal-targets-rows > .modal-row:first-child {
  border-top: 1px solid var(--white-soft);
}
/* 2026-06-03 — same fix for the wisdom row's stable-id wrapper
   (#modal-wisdom-row). Same reason: the wrapper exists so
   pickCustomProtocol can patch the wisdom slot in-place when the
   user taps the Power Breath protocol pill; but the wrapper breaks
   the `.modal-row + .modal-row` adjacent-sibling chain, leaving
   Voice guidance → Wisdom undivided and the Wisdom row floating.
   User audit: 'should be in same height rows as phase bell... and
   divided'. Adds a hairline on the row inside the wrapper so it
   reads as another row in the group. */
#modal-wisdom-row > .modal-row,
/* ...and the row immediately AFTER the wrapper (Haptic feedback): the wrapper
   breaks the `.modal-row + .modal-row` chain the same way .modal-targets-rows
   does above, so the Wisdom→Haptic hairline was missing (user report). Same
   mirror-fix as line 313 for the targets wrapper. */
#modal-wisdom-row + .modal-row {
  border-top: 1px solid var(--white-soft);
}
/* Group eyebrow — a single section label injected BETWEEN rows in the
   tune sheet (e.g. "TARGET" above the per-round retention wheels).
   The hairline sits FLUSH against the previous row's bottom edge so
   the rows above (Rounds + Breaths) read as one connected group,
   then the eyebrow + its rows below read as a labeled second group.
   Padding lives INSIDE the border box (no external margins) so the
   eyebrow hugs both its hairline and the first row below it. */
.modal-group-eyebrow {
  margin: 0;
  padding: var(--s-3) var(--s-4) var(--s-1);
  border-top: 1px solid var(--white-soft);
  color: var(--text-3);
}
.modal-row-clickable {
  cursor: pointer;
  transition: background var(--dur-state);
}
.modal-row-clickable:hover { background: var(--white-mist); }
.modal-row-icon {
  /* Bare-icon LAYOUT + 24px glyph come from the shared .row-icon alongside
     in markup; this companion only resets any tile-* box fill (currently
     just .tile-sage; .tile-moon was retired 2026-06-04 with the wisdom sheet). */
  background: transparent;
}
.modal-row-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
/* Trailing chevron for ACTION rows that have no toggle (e.g. "Save as my
   preset") — gives the row a right-side anchor so it reads as an intentional,
   tappable action instead of floating text, matching the toggle rows' balance. */
.modal-row-chev {
  display: inline-flex;
  align-items: center;
  color: var(--text-3);
  opacity: .5;
  flex-shrink: 0;
}
.modal-row-chev svg { width: var(--icon-sm); height: var(--icon-sm); stroke: currentColor; stroke-width: 2; fill: none; }
/* .modal-row-title composes .body-text in markup for the size. The local
   line-height: 1.2 overrides .body-text's 1.5 (row titles are single-line). */
.modal-row-title {
  font-weight: 600;
  color: var(--text);
  letter-spacing: .1px;
  line-height: 1.2;
}
/* .modal-row-sub composes .sub-text in markup; only the truncation layout
   stays. */
/* .modal-row-sub composes .sub-text in markup but overrides the size to
   11px — tune/sound sheets are denser than other list rows, and the 11px
   sub keeps the 14/11 (~1.27×) ratio under .modal-row-title that
   originally felt right on these sheets. */
.modal-row-sub {
  font-size: var(--fs-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.modal-row-chevron {
  width: 16px; height: 16px;
  color: var(--text-3);
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.modal-row-chevron svg { width: 16px; height: 16px; }

/* LEARN tab content typography */
/* LEARN-tab vertical rhythm: the .modal-tab-pane flex gap is var(--s-3h)=14px.
   Each eyebrow adds 8px on top → 22px between sections and 14px from an eyebrow
   to its own content, uniformly. Content blocks carry no vertical margin; the
   gap (+ the next eyebrow's 8px) supplies their spacing. */
/* .modal-learn-tagline composes .body-text in markup for the size + lh.
   Italic was removed 2026-05-29 — the tagline reads as plain body. */
.modal-learn-tagline {
  color: var(--text-2);
  margin: 0;
}
/* Type from .section-label.section-label--xs; 8px top = the section gap. */
.modal-learn-eyebrow { margin: var(--s-2) 0 0; }
/* .modal-learn-body composes .caption in markup; lh:1.65 overrides .caption's 1.45 (looser body). */
.modal-learn-body {
  line-height: 1.65;
  color: var(--text-2);
  /* top margin restores the separation the removed "About this technique"
     eyebrow used to provide above the description paragraph (2026-08-03).
     --s-3h (was --s-5): the eyebrow-less sections were spacing out too far. */
  margin: var(--s-3h) 0 0;
}
/* Benefit pill grid leading a section without its "Benefits" eyebrow — carry
   the spacing the eyebrow used to add above the pills (2026-08-03). */
.benefit-row--lead { margin-top: var(--s-3h); }
/* Bulleted list inside the Learn tab — used for the Pace and Protocols
   sections on the Custom Session sheet. Modelled after .safe-list but
   neutral (no rose warn variant) and slightly tighter so the explanatory
   bullets sit close to their eyebrow above. */
.modal-learn-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.modal-learn-list li {
  position: relative;
  padding-left: var(--s-4h);
  line-height: 1.55;
  color: var(--text-2);
}
.modal-learn-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 8px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-3);
}
.modal-learn-list b {
  color: var(--text);
  font-weight: 600;
}
/* Sparkle-icon tip block ("Never practice while driving …") — FLAT.
   The sage-tinted bordered card was stripped; the amber sparkle
   glyph + muted body text are enough. Top margin keeps it spaced
   from the safety block above so the two blocks don't read as one. */
.modal-learn-tip {
  display: flex;
  gap: var(--s-2h);
  padding: var(--s-2) 0;
  margin-top: var(--s-3);
  background: transparent;
  border: 0;
  border-radius: 0;
}
.modal-learn-tip-ico {
  width: 16px; height: 16px;
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--amber);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.modal-learn-tip-ico svg { width: 100%; height: 100%; }
/* .modal-learn-tip p composes .caption in markup; lh:1.6 overrides .caption's 1.45. */
.modal-learn-tip p {
  color: var(--text-2);
  line-height: 1.6;
  margin: 0;
}
.modal-learn-bestfor {
  margin: var(--s-2) 0 0;
  font-size: var(--fs-label);
  /* 2026-06-04 — bumped --text-3 → --text-2 (4.7:1 → 5.5:1) per
     user audit ('barely see it in bright sun'). 'Practitioners
     who know what they need' is the meaningful content here — the
     label tells WHO each technique is for — so it earns more than
     the supplementary-line tier the original --text-3 implied. */
  color: var(--text-2);
  line-height: 1.5;
}
.modal-learn-bestfor strong {
  /* Prefix lifted one step too — --text-2 → --text — so the visual
     hierarchy (label brighter than body) is preserved at the new
     brighter base. Bold weight + color contrast both differentiate. */
  color: var(--text);
  font-weight: 600;
}

/* Sticky footer CTA */
.modal-footer-v2 {
  padding:
    var(--s-3)
    max(18px, var(--safe-right))
    calc(14px + var(--safe-bottom))
    max(18px, var(--safe-left));
  background: linear-gradient(0deg,
              rgba(11,19,28,1) 60%,
              rgba(11,19,28,.92) 85%,
              transparent 100%);
  flex-shrink: 0;
}
/* The tuning-sheet CTA now composes the shared .btn-primary (fill, type,
   icon support, hover/press all come from it). .modal-cta is kept only as a
   marker for the reduced-motion override below + the JS-targeted label. */
/* Decisive single-word CTA label ("START" / "CUSTOMIZE").
   Uppercase + letter-spacing make a single-syllable verb feel
   intentional rather than truncated; weight matches surrounding
   button weight so we don't end up with a typographic mismatch. */
.modal-cta-label {
  font-size: var(--fs-caption); /* lint-skip CAT-T2: uppercase tracked CTA button label, not body caption */
  font-weight: 700;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: inherit;
}
/* Legacy class retained as a no-op for back-compat with any cached
   render path that might still emit it; harmless if unused. */

/* Reduced motion */
/* Tab pane fade-in animation is decorative; honor the user's prefs. */
@media (prefers-reduced-motion: reduce) {
  .modal-tab-pane { animation: none; }
  .modal-cta { transition: none; }
}

/* ── Landscape phone / wide+short viewport ───────────────────────
   Triggers when orientation is landscape AND the viewport is short
   (≤ 560px tall). Pure `orientation: landscape` would also catch
   wide desktop monitors, which we don't want to restyle. */
@media (orientation: landscape) and (max-height: 560px) {
  /* Suspend the desktop frame — on a landscape phone we want the
     full-bleed treatment, not a 390×844 portrait frame inside a
     844×390 viewport. */
  #app {
    display: block;
    height: 100vh;
    height: 100dvh;
  }
  .phone {
    width: 100%;
    height: 100vh;
    height: 100dvh;
    max-height: none;
    border-radius: 0;
    box-shadow: none;
  }
  .phone-leaving {
    width: 100%;
    height: 100dvh;
    max-height: none;
    border-radius: 0;
    box-shadow: none;
  }

  /* Top-bar: tighten vertical, keep horizontal safe-area for the */
  .top-bar {
    padding:
      calc(10px + var(--safe-top))
      max(var(--gutter), var(--safe-right))
      var(--s-2)
      max(var(--gutter), var(--safe-left));
  }

  /* Hide the standalone "Session Progress" label row above the orb */
  .screen > div[style*="text-align:center"][style*="padding:var(--s-1h) 0 0"] {
    padding: 0 !important;
  }
  .screen > div[style*="text-align:center"][style*="padding:var(--s-1h) 0 0"] > p:first-child {
    display: none;        /* "Session Progress" label */
  }
  .screen > div[style*="text-align:center"][style*="padding:var(--s-1h) 0 0"] > p:nth-child(2) {
    font-size: var(--fs-body) !important; /* lint-skip CAT-T1: !important responsive override on inline-styled element; not refactorable to a class */
    margin-top: 2px;
  }

  /* Orb sizing — drop from 280px to 200px on landscape. Stage shrinks
     with it; the three concentric rings scale proportionally. */
  .orb-inner,
  .orb-stage {
    width: 200px !important;
    height: 200px !important;
  }
  .glow-ring.r1 { width: 230px !important; height: 230px !important; }
  .glow-ring.r2 { width: 252px !important; height: 252px !important; }
  .glow-ring.r3 { width: 274px !important; height: 274px !important; }

  /* Orb internals: the SVG viewBox stays 0 0 280 280 (the ring math
     is fixed), so we just rely on intrinsic sizing of the wrapper to
     scale the SVG down. The countdown numeral inside, however, was
     sized for the 280px orb — shrink it so it doesn't overflow. */
  .countdown-numeral {
    font-size: var(--fs-display) !important;
    letter-spacing: -1px !important;
  }
  .hold-time-display {
    font-size: var(--fs-display) !important;
  }

  /* Phase label + play button: pull closer to the orb (less vertical space
     available in landscape). The cycle/round meta-row is absolutely bottom-
     anchored now (.timer-meta-row), so it needs no margin override here. */
  .timer-phase-label { margin-top: var(--s-3h) !important; }
  .timer-play-btn { margin-top: var(--s-4) !important; }

  /* Session controls: tighter bottom padding (we're space-starved). */
  .session-controls {
    padding:
      0
      max(16px, var(--safe-right))
      calc(14px + var(--safe-bottom))
      max(16px, var(--safe-left));
    gap: var(--s-5);
  }

  /* Timer play button: the base rule pins it at --s-6 (32px) from the bottom
     with no safe-area term. On a short landscape viewport that can sit too high
     or graze the home indicator — match the tightened .session-controls bottom. */
  .timer-screen #t-playbtn {
    bottom: calc(14px + var(--safe-bottom));
  }

  /* Bottom-nav: tighten on landscape so it doesn't eat half the height.
     Nav-items get a horizontal row layout with smaller padding. */
  .bottom-nav {
    padding:
      var(--s-1h)
      max(0px, var(--safe-right))
      calc(6px + var(--safe-bottom))
      max(0px, var(--safe-left));
  }
  .nav-item {
    flex-direction: row;
    gap: var(--s-2);
    padding: var(--s-1h) var(--s-3h);
  }
  .nav-item svg { width: 18px; height: 18px; }
  .nav-label { font-size: var(--fs-mini); }

  /* Bottom-nav-aware scroll-content can drop the reserved space
     a bit since the nav itself is shorter now. */
  .scroll-content {
    padding-bottom: calc(56px + var(--safe-bottom));
  }

  /* Sheets (audio, reminders, tech modal): cap height at 96% of
     dvh and put scroll inside, otherwise the sheet eats the screen
     and pushes its CTA below the fold in landscape. */
  .tech-modal-sheet,
  #reminders-sheet,
  #audio-sheet {
    max-height: calc(96dvh - var(--safe-top));
  }
}

/* Tall-phone short viewport (portrait Pro Max / foldables) */
@media (orientation: portrait) and (max-height: 660px) {
  .orb-inner,
  .orb-stage {
    width: 240px !important;
    height: 240px !important;
  }
  .glow-ring.r1 { width: 278px !important; height: 278px !important; }
  .glow-ring.r2 { width: 304px !important; height: 304px !important; }
  .glow-ring.r3 { width: 334px !important; height: 334px !important; }
  .countdown-numeral { font-size: var(--fs-numeral-xl) !important; }
  .hold-time-display { font-size: var(--fs-numeral-lg) !important; }
}

/* ── Defensive: very narrow viewports (≤ 340px, e.g. iPhone SE 1st
   gen, Galaxy Fold cover screen). Reduce gutters so cards don't
   touch a 22px wall. */
@media (max-width: 340px) {
  :root {
    --gutter: 16px;
    --gutter-sm: 14px;
  }
  .top-bar-title { font-size: var(--fs-body); } /* lint-skip CAT-T1: @media downsize override — body-text would apply at all widths, defeating the responsive scale-down */
  .orb-inner,
  .orb-stage {
    width: 240px !important;
    height: 240px !important;
  }
  .glow-ring.r1 { width: 280px !important; height: 280px !important; }
  .glow-ring.r2 { width: 310px !important; height: 310px !important; }
  .glow-ring.r3 { width: 340px !important; height: 340px !important; }
}

/* ── Very tall viewports (iPad portrait, Galaxy Tab) ──────────
   When the phone frame appears at 480px+, content can feel lost
   in vertical whitespace on tablets. We don't fight that — the
   framed look is intentional for the desktop preview — but we do
   prevent the splash background gradients from looking banded by
   anchoring them further apart at very tall heights. */
@media (min-width: 480px) and (min-height: 900px) {
  #app {
    background:
      radial-gradient(ellipse at 30% 15%, var(--sage-tint) 0%, transparent 45%),
      radial-gradient(ellipse at 70% 85%, rgba(125,142,176,0.035) 0%, transparent 45%),
      linear-gradient(135deg, var(--bg-0) 0%, var(--bg-2) 60%, var(--card-1) 100%);
  }
}

/* PWA / standalone mode polish */
@media (display-mode: standalone) {
  .screen::before {
    content: '';
    position: absolute;
    top: 0; left: 0; right: 0;
    height: var(--safe-top);
    background: linear-gradient(180deg, rgba(5,10,20,.55), rgba(5,10,20,0));
    pointer-events: none;
    z-index: var(--z-sticky);
  }
  /* Boot splash needs the same treatment so the status-bar text
     is legible while the app is launching. */
  .boot-splash::before {
    content: '';
    position: absolute;
    top: 0; left: 0; right: 0;
    height: env(safe-area-inset-top, 0px);
    background: linear-gradient(180deg, rgba(5,10,20,.55), rgba(5,10,20,0));
    pointer-events: none;
  }
}

/* Sheet container — wider top padding for the handle, custom
   bottom padding for the safe-area spacer. */
.sound-v2-sheet {
  padding:
    var(--s-2)
    max(20px, var(--safe-right))
    max(22px, var(--safe-bottom, 22px))
    max(20px, var(--safe-left));
  max-height: 92vh;
  overflow-y: auto;
  scrollbar-width: none;
}
.sound-v2-sheet::-webkit-scrollbar { display: none; }

/* Header */
.sound-v2-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: var(--s-1h) 0 var(--s-4h);
}
.sound-v2-title-wrap {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.sound-v2-title-icon {
  width: 22px; height: 22px;
  color: var(--sage);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.sound-v2-title-icon svg { width: 100%; height: 100%; stroke-width: 1.7; }

/* Visualizer strip (slim dotted line) */

/* Type from .section-label.section-label--lg; keeps only its margin. */
.sound-v2-eyebrow { margin: 0 0 var(--s-2h); }
.sound-v2-eyebrow-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--s-2h);
}
.sound-v2-eyebrow-row .sound-v2-eyebrow { margin: 0; }
.sound-v2-hint-icon {
  width: 13px; height: 13px;
  color: var(--text-2);
  display: inline-flex;
  vertical-align: -2px;          /* sit on the text baseline inside the helper line */
}
.sound-v2-hint-icon svg { width: 100%; height: 100%; }

/* Toggle (iOS-style pill) */
/* Matches the canonical .toggle (settings/custom) exactly — same 44×26
   track + 20px knob with a balanced 3px inset — so every toggle in the
   app reads identically. (Was 50×30 with an oversized 24px knob that
   looked cramped/misaligned next to the rest.) */
.sound-v2-toggle {
  width: 44px; height: 26px;
  border-radius: 999px;
  border: none;
  cursor: pointer;
  padding: 0;
  position: relative;
  flex-shrink: 0;
  background: var(--white-haze);
  transition: background var(--dur-state);
  display: inline-block;
}
.sound-v2-toggle.is-on {
  background: var(--sage);
  /* 2026-06-02 — cyan halo dropped to match .toggle.on
     (03-primitives.css). Solid fill against the dark canvas carries
     the on-state without the legacy web glow. */
}
.sound-v2-toggle-knob {
  position: absolute;
  top: 3px; left: 3px;
  width: 20px; height: 20px;
  border-radius: 50%;
  background: var(--white);
  transition: left var(--dur-state) var(--ease-standard), transform var(--dur-tap);
  box-shadow: 0 1px 4px rgba(0,0,0,0.30);
}
.sound-v2-toggle.is-on .sound-v2-toggle-knob { left: 21px; }   /* 44 - 20 - 3 */
.sound-v2-toggle.is-disabled {
  opacity: .45;
  cursor: not-allowed;
}
.sound-v2-toggle:not(.is-disabled):active .sound-v2-toggle-knob { transform: scale(.92); }

/* Background music master card */

/* Soundscape tile row (4-up) */

/* Binaural beats row */
.sound-v2-bin-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--s-1h);
  padding: var(--s-1);
  background: var(--surface-soft);            /* was var(--card) navy slab — now the
                                                 canonical soft-canvas fill, matching
                                                 the duration preset chips */
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  margin-bottom: var(--s-3h);
}
.sound-v2-bin-seg {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: var(--s-2h) var(--s-1);
  min-height: 54px;
  border-radius: var(--r-md);
  background: transparent;
  border: 1px solid transparent;   /* thin stroke — matches the music chip weight (2026-06-12) */
  color: var(--text-2);
  cursor: pointer;
  font-family: var(--font-body);
  transition: background var(--dur-state), border-color var(--dur-state), color var(--dur-state), box-shadow var(--dur-state);
}
.sound-v2-bin-seg:hover:not(.is-on) { background: var(--surface-soft-hover); }
/* .sound-v2-bin-label composes .body-text in markup for the size. */
.sound-v2-bin-label {
  font-weight: 700;
  letter-spacing: .1px;
}
/* Two stacked sub-lines under the label: goal (e.g. "Meditation") then the
   frequency ("6 Hz") on its own line — deterministic, no bullet, never wraps. */
.sound-v2-bin-goal,
.sound-v2-bin-freq {
  font-size: var(--fs-mini);
  color: var(--text-3);
  letter-spacing: .2px;
  line-height: 1.25;
}
.sound-v2-bin-goal { margin-top: 2px; }
.sound-v2-bin-seg.is-on {
  /* Match the soundscape chip's active treatment EXACTLY: sage-soft glow fill +
     sage outline + sage text. The chip (.chip.sound-v2-chip.is-on) carries a
     sage border, so a fill-only segment read inconsistently beside it — the
     mismatch the user flagged 2026-07-29. Both now use fill + 1px sage outline. */
  background: var(--sage-soft);
  border-color: var(--sage);
  color: var(--sage);
}
.sound-v2-bin-seg.is-on .sound-v2-bin-label { color: var(--sage); }
.sound-v2-bin-seg.is-on .sound-v2-bin-goal,
.sound-v2-bin-seg.is-on .sound-v2-bin-freq { color: var(--sage); opacity: .8; }
.sound-v2-bin-seg.is-dim:not(.is-on) { opacity: .55; }

/* Background-music volume row — brought back 2026-06-06 (user
   direction: 'set the volume on my own'). Drives the music bus only
   (setMusicVol), independent of the device hardware master. The
   master slider stays gone; this one is scoped to the soundscape
   group, sitting just under the chip rail. */
.sound-v2-volume {
  margin: var(--s-3) 0 var(--s-2);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  transition: opacity var(--dur-tap) var(--ease-ios);
}
/* Dim — but stay interactive — when background music is muted, mirroring
   the chip rail's is-dim treatment so the group reads as one unit. */
.sound-v2-volume.is-dim { opacity: .55; }
/* Footer placement — the sheet-wide Volume now sits at the bottom (below the
   binaural helper), so give it a touch more top separation to stand alone. */
.sound-v2-volume--footer { margin-top: var(--s-4); }
.sound-v2-volume-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}
.sound-v2-volume-label {
  color: var(--text-2);
  letter-spacing: .2px;
}
.sound-v2-volume-value {
  color: var(--sage);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
/* Flush with the numeral (58%, not 58 %). The negative margin closes the
   trailing space the number's tabular-nums cell leaves after the last digit. */
.sound-v2-volume-unit { color: var(--text-3); margin-left: -0.1em; }
/* The thumb halo needs vertical room so it isn't clipped by the row. */
.sound-v2-volume-slider { margin: var(--s-1h) 0; }

/* Safe-area bottom spacer (the sheet already has padding-bottom,
   but inner content gets extra room for thumbs on mobile). */
.sound-v2-safe-spacer { height: 6px; }

/* (modal-tab pill override removed — see canonical .modal-tabs block) */

/* Safety panel inside LEARN tab — FLAT.
   The rose-tinted bordered box was stripped to match the rest of
   the app's flat treatment (Profile, Settings, Tune, History,
   Session Complete, Community). Hierarchy now comes from the rose
   glyph + rose section title + rose bullets, not a chrome box. The
   block uses top/bottom padding only so it breathes between
   neighbours, with no horizontal padding so the bullets align with
   the scroll gutter like every other row. */
.modal-learn-safety {
  margin: var(--s-3) 0 0;
  padding: var(--s-3) 0 var(--s-2);
  background: transparent;
  border: 0;
  border-radius: 0;
}
.modal-learn-safety-head {
  display: flex;
  /* center: the "Before you begin" eyebrow was removed (2026-08-03), so the
     titles block is now a single "Practice safely" line — center it against
     the shield icon instead of top-aligning to a two-line block. */
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}
/* Bare rose line glyph — the tinted square tile was dropped as
   part of the flat pass. The rose color on the icon + title is
   enough semantic signal. */
.modal-learn-safety-icon {
  width: 32px; height: 32px;
  background: transparent;
  border: 0;
  color: var(--rose);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
/* Bumped 18 → 28 (2026-05-30) so the shield reads at a comparable
   weight to the bold rose 'Practise safely' title + eyebrow block
   beside it. Was visually swamped by the title at 18px. */
.modal-learn-safety-icon svg { width: 28px; height: 28px; stroke-width: 1.7; }
.modal-learn-safety-titles {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-top: 2px;
}
/* .modal-learn-safety-title composes .body-text in markup for the size. */
.modal-learn-safety-title {
  font-weight: 700;
  color: var(--rose);
  letter-spacing: -.1px;
}
/* (eyebrow type now from .section-label.section-label--xs in markup) */
/* .modal-learn-safety-intro composes .caption in markup; lh:1.55 overrides .caption's 1.45. */
.modal-learn-safety-intro {
  color: var(--text-2);
  line-height: 1.55;
  margin: 0 0 var(--s-3);
}
.modal-learn-safety-list {
  list-style: none;
  margin: 0 0 var(--s-3);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
/* .modal-learn-safety-list li composes .caption in markup; lh:1.5 overrides .caption's 1.45. */
.modal-learn-safety-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2h);
  line-height: 1.5;
  color: var(--text);
}
.modal-learn-safety-bullet {
  width: 14px; height: 14px;
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--rose);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: .85;
}
.modal-learn-safety-bullet svg { width: 100%; height: 100%; stroke-width: 2.5; }
/* "Loss of consciousness…" note inside the safety panel — FLAT.
   The tinted background + left rule + partial radius were stripped;
   the moon-colored icon + muted body text carry the meaning now.
   Sits one indent in from the bullets above by virtue of the icon
   gap + the inherited block padding. */
.modal-learn-safety-note {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2h);
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  font-size: var(--fs-label);
  color: var(--text-2);
  line-height: 1.5;
  margin: var(--s-2) 0 var(--s-3);
}
.modal-learn-safety-note-icon {
  width: 14px; height: 14px;
  color: var(--moon);
  flex-shrink: 0;
  margin-top: 1px;
  display: inline-flex;
}
.modal-learn-safety-note-icon svg { width: 100%; height: 100%; }
.modal-learn-safety-link {
  background: none;
  border: none;
  padding: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-family: var(--font-body);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: .modal-learn-safety-link is a button — lh:1.45 would change button height */
  font-weight: 600;
  color: var(--rose);
  cursor: pointer;
  transition: color var(--dur-state);
  letter-spacing: .1px;
}
.modal-learn-safety-link:hover { color: var(--text); }
.modal-learn-safety-link svg { stroke: currentColor; }

/* BODY LAYOUT */
/* The body is a flex column that fills the screen below the top bar.
   Children stack from the top, but `.pc-actions` is pinned to the
   BOTTOM via `margin-top: auto` (see Zone 5) so the primary CTA
   always lands at thumb-reach — even when the session was so brief
   that hero + metrics together don't fill the viewport.
   When content IS long (achievements + reflection + many round
   stats), `margin-top: auto` collapses to zero and the whole stack
   scrolls naturally as before. */
.pc-screen .pc-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-3h) var(--s-5) calc(24px + var(--safe-bottom));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* ZONE 1 · HERO */
.pc-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--s-2) 0 var(--s-1);
  margin: 0;
}
.pc-hero-icon {
  margin: 0 0 var(--s-4h);
  width: 84px;
  height: 84px;
  /* Asymptotic settle on entrance, then a slow, soft mint-glow halo */
  animation: checkPop 420ms var(--ease-out-soft) both, pcHaloPulse 3.2s ease-in-out 0.42s infinite;
}
.pc-hero-icon .pc-check-svg {
  width: 28px;
  height: 28px;
}
/* .pc-hero-title composes .hero-title in markup; ls:-.4 and lh:1.15 override .hero-title's tokens for this title's specific cadence. */
.pc-hero-title {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -.4px;
  line-height: 1.15;
  color: var(--text);
  margin: 0 0 var(--s-2);
}
/* .pc-hero-sub composes .body-text in markup for the --fs-body type-spec. */
.pc-hero-sub {
  color: var(--text-2);
  max-width: 32ch;
  margin: 0;
}
/* Session-complete halo — toned down to a quiet, breath-paced glow. */
@keyframes pcHaloPulse {
  0%, 100% { box-shadow: 0 0 18px color-mix(in srgb, var(--pc-accent-hex, #7dd3e8) 18%, transparent); }
  50%      { box-shadow: 0 0 26px color-mix(in srgb, var(--pc-accent-hex, #7dd3e8) 26%, transparent); }
}

/* ════════════════════════════════════════════════════════════════
   BREATH HOLD screen (pgHold) — extracted from inline styles.
   Off-scale snaps: Finish pad 9px→8px (--s-2); instruction margin-top
   28px→32px (--s-6) + side pad 40px→32px (--s-6). Three raw values
   have no token home and stay literal: the inner-disc radial navy
   rgba(14,24,36,.97), and the amber play-button gradient tail #b8895c
   + its glow rgba(212,165,116,.3).
   ════════════════════════════════════════════════════════════════ */
/* Finish pill now uses the shared .session-finish-btn (03-primitives.css). */
.hold-progress-head { text-align: center; padding: var(--s-1h) 0 0; }
/* Type from .section-label.section-label--xs; keeps only its margin. */
.hold-progress-label { margin-bottom: var(--s-1); }
.hold-round {
  font-family: var(--font-body);
  font-size: var(--fs-h2);             /* 24px */
  font-weight: 600;
}
.hold-round-total { color: var(--text-3); }
.hold-ring-area {
  flex: 1;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
}
.hold-ring { position: relative; width: 280px; height: 280px; }
.hold-ring-svg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  transform: rotate(-90deg);
}
.hold-ring-arc { filter: drop-shadow(0 0 8px var(--amber)); }
.hold-ring-inner {
  position: absolute; inset: 18px;
  border-radius: var(--r-circle);
  background: radial-gradient(circle, rgba(14,24,36,.97) 60%, transparent);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
}
.hold-status {
  display: flex; align-items: center;
  gap: var(--s-2);                     /* 8px */
  margin-top: var(--s-3);              /* 12px */
}
.hold-status-dot {
  width: 7px; height: 7px;
  border-radius: var(--r-circle);
  background: var(--amber);
  box-shadow: 0 0 8px var(--amber);
  animation: pulse 1.5s infinite;
}
.hold-status-label {
  font-size: var(--fs-mini);           /* 10px */
  color: var(--amber);
  font-weight: 600;
  letter-spacing: 2px;
}
.hold-instruction {
  color: var(--text-2);
  margin-top: var(--s-6);              /* was 28px → 32px */
  text-align: center;
  padding: 0 var(--s-6);               /* was 0 40px → 0 32px */
  line-height: 1.7;                    /* overrides .body-text's 1.5: instruction breathes wider */
  /* font-size + family composed via .body-text in markup */
}
.hold-side-btn { width: 50px; height: 50px; }
/* Play/pause control — sage accent like .timer-play-btn (play is ALWAYS the
   accent color; amber stays on the retention ring + RETENTION/RELEASE labels). */
.hold-play-btn {
  width: 68px; height: 68px;
  border-radius: var(--r-circle);
  background: var(--sage);
  border: none;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 6px 20px rgba(0,0,0,.35);
}
.hold-check-ico { color: var(--sage); width: 16px; height: 16px; }

/* ===========================================================
   home.css
   COMPONENT · HOME + HISTORY/STATS — shared card shell, home-v2 list rows, big play button, custom-session sliders, profile hero, history/stats cards, calendar collapsible, disclaimer.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* SHARED CARD SHELL */
.pc-card {
  /* Surface (2% fill + hairline + 18px) from .card. */
  width: 100%;
  padding: var(--s-4) var(--s-4h);
}

/* ZONE 4 · REFLECTION */

/* ZONE 5 · ACTIONS */
/* margin-top:auto pushes the action stack to the BOTTOM of the */
.pc-actions {
  display: flex;
  flex-direction: column;
  gap: var(--s-2h);
  margin-top: auto;
  padding-top: var(--s-2);
}
.pc-primary {
  width: 100%;
  margin: 0 !important;
}
.pc-secondary {
  width: 100%;
  padding: var(--s-3) var(--s-4h);
  border-radius: 999px;
  border: 1px solid var(--border-warm);
  background: transparent;
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-body); /* lint-skip CAT-T1: secondary CTA button (.pc-secondary) — body-text lh:1.5 would grow the button height */
  font-weight: 600;
  letter-spacing: .15px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  transition: background var(--dur-state) ease, border-color var(--dur-state) ease, transform var(--dur-tap) ease;
  -webkit-tap-highlight-color: transparent;
}
.pc-secondary:hover { background: var(--sage-soft); }
.pc-secondary:active { transform: scale(.98); }
.pc-secondary-ico {
  display: inline-flex;
  width: 15px;
  height: 15px;
  color: var(--pc-accent, var(--sage));
}
.pc-secondary-ico svg { width: 100%; height: 100%; }

/* RESPONSIVE TIGHTENING */
@media (max-width: 380px) {
  .pc-screen .pc-body { padding: var(--s-3) var(--s-4h) var(--s-6); gap: var(--s-3h); }
  .pc-hero-title { font-size: var(--fs-h2-md); } /* lint-skip CAT-T3: @media downsize override on .pc-hero-title at narrow widths */
  .pc-card { padding: var(--s-3h) var(--s-4); }
}

/* REDUCED MOTION */
@media (prefers-reduced-motion: reduce) {
  .pc-hero-icon { animation: none; }
}

/* .home-v2-header / .home-v2-title / .home-v2-name rules removed
   2026-05-30 — the "Ready to breathe, <name>?" greeting was dropped.
   Home now opens straight on the hero card. */

/* Section wrapper — vertical rhythm only. Horizontal alignment is */
.home-v2-section {
  padding: var(--s-5) 0 0;
}
.home-v2-section-recent {
  /* The recent hero sits below the greeting. Breathing room
     settled 2026-05-29: 28px (var(--s-6)) above and 22px
     (var(--s-5)) below — was 16px / 0 originally, iterated 44 → 28. */
  padding-top: var(--s-6);
  padding-bottom: var(--s-5);
}

/* Composes .section-head (flex/center/space-between); keeps margin + gutter. */
.home-v2-section-head {
  margin-bottom: var(--s-3h);
  /* Horizontal alignment is delegated to .scroll-content's gutter, */
  padding: 0;
}
.home-v2-section-title {
  /* Type now from .section-label.section-label--lg; keeps only its margin. */
  margin: 0;
}
/* "See all" → Library link, placed BELOW the last technique row and left-aligned to
   the same gutter as the "For you" title (user direction 2026-07-19). Composes
   .section-label.section-label--lg so it's the SAME type as the "For you" heading
   (uppercase, tracked, 11px, weight 600); this rule only recolors it sage (the app's
   interactive accent) and handles the button reset + its own spacing. */
.home-v2-see-all {
  display: flex;
  align-items: center;
  gap: var(--s-1h);
  width: fit-content;       /* shrink to content so the tap target isn't full-width */
  background: transparent;
  border: none;
  margin: var(--s-3) 0 0;   /* top gap from the last row; overrides section-label's margin-bottom */
  padding: var(--s-2) 0;
  color: var(--sage);       /* overrides .section-label's --text-3 (home.css loads after primitives) */
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity var(--dur-state) var(--ease-ios);
}
.home-v2-see-all:active { opacity: 0.6; }
/* Trailing chevron (points right → Library). Inherits the sage currentColor; sized
   to sit with the 11px uppercase label. The label's 2px tracking adds a hair of extra
   space on its right edge, so a slight negative margin pulls the chevron back in. */
.home-v2-see-all-chev { display: inline-flex; margin-left: -2px; }
.home-v2-see-all-chev svg { width: 16px; height: 16px; }

/* Strip the universal top-padding from .scroll-content on home — */
.home-v2 .scroll-content { padding-top: 0; }

/* Trailing bottom padding on the last section. Replaces the */
.home-v2-section:last-child { padding-bottom: var(--s-5); }

/* RECENT card — FLAT (2026-05-29).
   The previous linear-gradient surface + bordered card chrome were
   stripped to match the rest of the app's flat treatment. What
   stays: the radial tint glow (.home-recent-glow), the animated
   breath wave (.home-recent-wave-path), and the cyan play-button
   glow + pulse (.home-recent-play). Those three are the Recent
   card's signature; the wrapping card chrome was not.

   overflow:hidden was REMOVED (2026-05-29) — with no border on
   the card any more, clipping the glow against an invisible box
   produced visible rectangular edges. The radial-gradient on
   .home-recent-glow already fades to transparent at 65% radius,
   so unclipped it diffuses naturally as a soft amber/category
   halo with no hard edges. */
.home-recent {
  position: relative;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: var(--s-3) 0 var(--s-2);
  isolation: isolate;
  box-shadow: none;
}
.home-recent-glow {
  position: absolute;
  top: -55%;
  right: -35%;
  /* Diffuse ambient haze — NO visible circle edge or line. A big blur() spreads the
     radial into a soft cloud with no discernible boundary; the low opacity keeps it a
     whisper. (Earlier passes just resized/faded the disc, which still showed an arc on
     the lighter #0B111C canvas — the blur is what removes the edge entirely.) */
  width: 360px;
  height: 360px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--tint-glow, var(--sage-210)) 0%, transparent 70%);
  opacity: 0.4;
  filter: blur(45px);
  pointer-events: none;
  z-index: 0;
}
/* .home-recent-head removed 2026-05-30; .home-recent-eyebrow (the
   'SUGGESTED' label on the empty-state hero) removed again 2026-06-15 —
   the hero opens directly on the suggested technique with no label above
   it. (It had been re-added between those dates; dropped for good now.) */
/* .home-recent-top removed 2026-05-31 — the previous side-by-side
   tap-zone + round-play layout was replaced by a stacked card
   (streak / title row / sub+pencil / wave / wide pill CTA). */

/* Streak eyebrow — flame icon + 'N-day streak' caption. Hidden when
   streak is 0 (the JS just omits the markup). */
.home-recent-streak {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  color: var(--text-3);
  margin-bottom: var(--s-3);
}
.home-recent-streak-ico {
  display: inline-flex;
  width: 16px; height: 16px;
  color: var(--amber);
}
.home-recent-streak-ico svg { width: 16px; height: 16px; }
.home-recent-streak-text {
  font-weight: 500;       /* was 600 — lighter so the streak recedes behind the title */
  letter-spacing: .2px;
}

/* Hero TAP zone — wraps icon + title + sub+pencil in a single
   tappable button that opens the tune modal. The whole upper area
   above the wave is the affordance for customizing. */
.home-recent-tap {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  background: transparent;
  border: none;
  padding: 0;
  margin: 0;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font: inherit;
  -webkit-tap-highlight-color: transparent;
  transition: opacity var(--dur-tap) var(--ease-ios);
}
.home-recent-tap:hover { opacity: .9; }
.home-recent-tap:active { opacity: .7; }
/* Hero icon — now a SPAN inside .home-recent-tap (no longer its own
   button — the parent .home-recent-tap owns the click). Size arc:
   44/32 → 48/36 → 52/40 → 56/44 → 52/40 → 60/48 (final, 2026-05-31).
   The last bump up addressed a short technique-name case ("Tummo")
   where the 52/40 felt visually light next to the round play
   button on the right; 60/48 balances both columns. The
   translateY(-12px) compensation stays — the offset between
   title-stack center and title center is independent of icon size. */
.home-recent-icon {
  width: 60px; height: 60px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--tint, var(--sage));
  /* Slight upward nudge from the default align-items: center stack
     midpoint, leaving the icon visually anchored just above the
     title+sub stack center. Arc on this offset: -12 → -6 → -2
     (settled 2026-05-31). The base align-items: center already
     puts the icon at the stack midpoint; this just bumps it a
     touch toward the title to feel less stack-bottom-heavy on
     2-line names. */
  transform: translateY(-2px);
}
.home-recent-icon svg {
  width: 48px; height: 48px;
  stroke-width: 1.7;
  fill: none;
  filter: drop-shadow(0 0 8px rgba(var(--tint-rgb, 125,211,232), .55));
}
.home-recent-titles {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
/* Technique title on the Recent / Suggested hero — 24px / weight
   700 (settled 2026-05-29 after iterating 24/700 → 20/600 → 20/500
   → 20/600 → 20/700 → 24/700). Back at the original --fs-h2 size
   with the same boldness; the page balance comes from the smaller
   hero icon (44/32 instead of 52/40) rather than a smaller title. */
/* Composes .hero-title--lg in markup for size + lh + ls (28px / 1.15 /
   -.3px). Size arc: 24 → 28 → 32 → 28 (reverted 2026-05-31 — the 32px
   pass felt too large; settled back on 28px). The tier class provides
   the typography triplet; the companion keeps only weight + color +
   margin. Weight 600 since the 700 trial paired with the 32px bump. */
.home-recent-title {
  font-family: var(--font-body);
  font-weight: 600;
  color: var(--text);
  margin: 0 0 var(--s-1h);   /* breathing room so the title anchors, metadata recedes */
}
/* Sub row — pencil edit icon LEADING the phase-summary sub, sitting in front of
   the breakdown as a quiet 'tap to customize' hint. nowrap keeps the icon and the
   (now compact "4 · 4 · 4 · 4") metadata on ONE line — the icon can no longer
   steal width that pushes the numbers to a second row. */
.home-recent-sub-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);          /* more breathing room between the pencil and the sub */
  flex-wrap: nowrap;
}
/* Plain-language "how you breathe" line ("Forceful exhales, passive inhales").
   Composes .caption (12px) in markup — a step down from the 24px title so it
   reads as a subordinate subtitle, not a competing headline. Lighter weight +
   roomier line-height + slight tracking. Fits one line on any real phone width;
   min-width:0 + ellipsis is a safety net so a long description on a very narrow
   (~320px) screen truncates cleanly instead of overflowing the card. */
.home-recent-tune {
  color: var(--text-3);
  font-weight: 400;       /* thinner than the title so it recedes */
  line-height: 1.6;
  letter-spacing: .4px;   /* slight tracking so the description reads as intentional */
  opacity: .9;            /* deepen the cool grey a touch — metadata fades back */
  white-space: nowrap;    /* keep the description on one line */
  min-width: 0;           /* allow the flex item to shrink below content width */
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Gear "tune" icon in the sub-row (was a pencil until 2026-07-15) — a secondary
   helper: smaller + dimmed so it doesn't catch the eye before the title. Signals
   the whole card is tap-to-customize. Click bubbles to .home-recent-tap. Class
   name kept as .home-recent-edit to avoid a churny rename. */
.home-recent-edit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Filled accent tile (user 2026-08-04) — pale-accent circle + accent glyph so
     the tune gear reads as a clear, tappable control in both themes (was a faint
     15px outline at --text-3 / .65 opacity). */
  width: 24px; height: 24px;
  background: var(--sage-soft);
  border-radius: 50%;
  color: var(--sage);
  opacity: 1;
  flex-shrink: 0;
  position: relative;
  top: -1px;   /* optical nudge: align with the sub text's center, not the tall line-box center */
  transition: background var(--dur-tap) var(--ease-ios),
              box-shadow  var(--dur-tap) var(--ease-ios),
              transform   var(--dur-tap) var(--ease-ios);
}
/* Tap feedback — the old icon-btn gears glowed on press; the filled tile lost it
   because a <span> has no :hover/:active glow. Restore the sage bloom + press-in
   so tapping the tune gear reads as a live control again. (user 2026-08-04) */
.home-recent-edit:active {
  background: var(--sage-90);
  box-shadow: 0 0 12px var(--sage-170);
  transform: scale(.9);
}
.home-recent-edit svg { width: 15px; height: 15px; display: block; }

/* Wide pill CTA — primary start-session affordance. Composes
   .btn-primary in markup, this companion only adds the relative
   position needed for the wave to sit just above it + the margin
   that pulls the pill flush with the card's bottom edge. */
.home-recent-cta {
  position: relative;
  z-index: 1;
  /* 2026-06-02 — restored to full-bleed primary pill per user
     direction ('play button should be the size of the card, make
     it wider how it was before, adjust height'). The 2026-06-01
     half-width compact spike (min-width: 50%, left-aligned) was
     rolled back; the card's primary action now matches the modal-
     sheet CTA grammar (full width + taller padding) so the play
     pill anchors the card visually.
     Vertical padding bumped back to var(--s-3h) (14px) — closer
     to the original 16px before the compact spike compressed it
     to 12px. Width 100% replaces the auto/min-width pair so the
     pill spans edge-to-edge of the card's inner padding box. */
  width: 100%;
  white-space: nowrap; /* never wrap — keeps icon + text on one line */
  margin: var(--s-2) 0 0;
  padding-top: var(--s-3h);
  padding-bottom: var(--s-3h);
  /* Faint left→right sage gradient (over btn-primary's flat fill) for a little
     'lit-from-within' depth — less sticker, more interface. Tokens only. */
  background: linear-gradient(100deg, var(--sage), var(--sage-400));
}

/* Animated wave — sits between the title block and the CTA row.
   Cheap (transform only) and disabled under prefers-reduced-motion
   via the global block in styles.css. */
.home-recent-wave {
  position: relative;
  z-index: 1;
  /* Margins: 14px top (gap from sub), 0 horizontal (extend slightly
     past card edges via negative side offset removed 2026-05-31),
     14px bottom (gap to the wide pill CTA below). Was --s-4h/--s-1/
     --s-5 before the layout restructure pulled the CTA into the card. */
  margin: var(--s-3h) calc(-1 * var(--s-1)) var(--s-3h);
  height: 40px;
  overflow: hidden;
}
.home-recent-wave-path {
  animation: recentWave 6.5s linear infinite;
  transform-origin: center;
  filter: drop-shadow(0 0 6px var(--tint, var(--sage)));
}
@keyframes recentWave {
  0%   { transform: translateX(0) scaleY(1); }
  50%  { transform: translateX(-6px) scaleY(1.08); }
  100% { transform: translateX(0) scaleY(1); }
}

/* .home-recent-tune-link rule removed 2026-05-30 — standalone tune
   link below the wave was dropped.
   .home-recent-play / .home-recent-play-zone / .home-recent-play-sub /
   .home-recent-play-pulse and the heroPlayPulse keyframes were
   removed 2026-05-31 — the round play button + its pulse halo were
   replaced by a wide .btn-primary pill (.home-recent-cta) at the
   bottom of the card. The visual focal point is now the pill CTA;
   the card's upper region is a quieter tap-to-customize zone. */

/* HOME LIST ROWS (FOR YOU, FAVORITES) — flat-treatment container.
   The 12px gap between rows was dropped when the rows stopped being
   cards: hairlines (.home-row + .home-row { border-top }) handle
   separation now, so the rows sit packed together as a single list. */
.home-v2-list {
  display: flex;
  flex-direction: column;
}
/* For You / Favorites tile row — FLAT (2026-05-29).
   Lost its --white-wash background, --white-film border and
   --r-xl radius. Hairlines now separate adjacent rows; the row
   itself is just icon + title/sub. Horizontal padding is zero
   so the icon column aligns with the page gutter; vertical
   padding + min-height match the Spotify-style 64px rhythm we
   adopted for Profile / Settings rows. */
.home-row {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  padding: var(--s-4h) 0;
  min-height: 64px;
  background: transparent;
  border: 0;
  border-radius: 0;
  color: inherit;
  cursor: pointer;
  font-family: var(--font-body);
  text-align: left;
  transition:
    background var(--dur-state) var(--ease-ios),
    transform  var(--dur-tap)   var(--ease-ios);
}
/* Hairline between adjacent rows — only between rows, never above
   the first, never below the last. Same recipe as .settings-row +
   .settings-row in the flat treatment. */
.home-row + .home-row {
  border-top: 1px solid var(--border);
}
.home-row:hover {
  background: var(--white-mist);
}
.home-row:active { transform: scale(.985); }
.home-row-icon {
  width: 48px; height: 48px;
  border-radius: var(--r-lg);
  background: transparent;   /* bare line icon — no tile, category color + glow */
  color: var(--tint, var(--sage));
  border: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.home-row-icon svg {
  width: 28px; height: 28px;
  stroke-width: 1.7;
  fill: none;
  filter: drop-shadow(0 0 8px rgba(var(--tint-rgb, 125,211,232), .5));
}
.home-row-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}
.home-row-sub {
  color: var(--text-2);
}
.home-row-fav {
  width: 22px; height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--sage);
  flex-shrink: 0;
  filter: drop-shadow(0 0 6px var(--sage-glow));
}
/* .home-row-chevron rule removed (2026-05-29) — the trailing
   chevron was dropped from the .home-row markup in the flatten
   pass. The favorited-heart indicator (.home-row-fav) is the only
   trailing affordance now. */

/* Big right-aligned readout: "DURATION ………… 10 min" */
/* .modal-section-head-big + .modal-section-value-big (+ .num / .unit
   variants) rules removed 2026-05-30 — the inline-headline grid that
   stamped "30 breaths before retention" above each slider is gone
   with the slider itself. The wheel picker (.modal-wheel-row above)
   carries label + value inline on one row, so this companion grammar
   is no longer needed. */

/* Custom Session pace control — full-width 3-up segmented control. */
.modal-pace-seg {
  display: flex;
  width: 100%;
}
.modal-pace-seg .seg-btn { flex: 1; }

/* Active duration chip — sharper teal glow matching screen 4 */

/* "Set duration" chip uses a dashed border at rest so it reads as
   "expandable / customise" rather than a value chip. */


/* The eyebrow + master toggle row uses tighter vertical alignment
   so the toggle pill sits flush with the eyebrow's baseline. */
.sound-v2-eyebrow-row-master {
  align-items: center;
}

/* 2026-06-12 — active binaural seg unified with the music chip (subtle sage-soft
   fill + thin sage border, no hard ring/glow). The previous heavy box-shadow
   (1.5px ring + 22px halo) made the active frequency read far more dominant than
   the active soundscape chip; the clean .sound-v2-bin-seg.is-on rule in
   80-screens.css now stands. */

/* Helper copy under the binaural row — stepped down 12px (--fs-caption,
   from the composed .caption tier) to 11px (--fs-label) on 2026-05-29
   so the headphones note reads as a quiet aside, not a body paragraph. */
.sound-v2-helper {
  font-size: var(--fs-label); /* lint-skip CAT-T2: per-component step-down from the composed .caption (12px) tier — Sound sheet helper aside */
  margin: 0 0 var(--s-4h);
  color: var(--text-2);
  letter-spacing: .1px;
  line-height: 1.45;
  padding: 0 2px;
}

/* Sound sheet + technique tuning sheet share the premium sheet gradient. */
.sound-v2-sheet,
.modal-sheet-v2 {
  background: var(--grad-sheet);
}

/* Reduced motion: disable the home-recent wave animation. */
@media (prefers-reduced-motion: reduce) {
  .home-recent-wave-path { animation: none; }
}

/* (Removed .btn-hover-rose — the saved-preset delete ✕ it styled is gone;
   presets now delete via swipe-to-reveal-trash.) */

/* (Removed .input-focus-sage — inputs now use the shared .field-input.) */

/* ════════════════════════════════════════════════════════════════
   DESIGN ENHANCEMENT v7 — single-accent discipline pass
   ────────────────────────────────────────────────────────────────
   Implements the seven design fixes from the design enhancement
   proposal. Every block below is scoped under a dedicated namespace
   so it is easy to grep and easy to walk back if needed:

     [DE-2] .modal-dur-section / .modal-dur-slider — section sheet
            DURATION slider replaces the old chip grid + stepper.
     [DE-3] .sound-v2-chip-rail / .sound-v2-cue-row — sound sheet
            ambient tiles → chip rail; cue cards → slim rows.
     [DE-4] .prof-* — profile hierarchy collapse.
     [DE-5] .composer-* — share sheet live preview + toggle.
     [DE-6] .reminder-wheel-v2 — wheel mechanics unchanged; visuals
            refined (centered selection band, sage highlight).
     [DE-7] .stats-* — single accent, lifetime row, 28-day strip.

   Color rules enforced everywhere below:
     • sage   — single accent, selection / active / brand glow
     • amber  — retention status only (Best Hold, streak status pill)
     • rose   — warnings only; never on chrome
     • moon   — clinical / informational; status-only
     • lavender — RESERVED for EXHALE breathing phase, nowhere else
     • ivory  — primary text; numbers default to ivory, not tinted
   ════════════════════════════════════════════════════════════════ */

/* ── MODAL WHEEL PICKER (horizontal, swipe-to-scroll) ────────────
   Replaces the Rounds / Breaths / Minutes sliders in the tune sheet
   (2026-05-30). Pattern mirrors the .reminder-wheel-col vertical
   picker rotated 90° — horizontal scroll-snap, pad items at the
   ends so the first/last value can snap to center, .is-current
   reads sage + bold, flanking items dim to text-3. Swipe to step;
   value commits on scroll-end (debounced).

   Layout: a flat row with the LABEL on the left and the wheel on
   the right. Adjacent rows separated by a hairline.

   Sizes are CSS custom properties (--wheel-w / --wheel-item-w) so
   the same component can be re-tuned per call site if needed; the
   defaults below match the user's reference design (3 items
   visible × 60px each). */
/* .modal-row-wheel — wheel-picker variant of .sheet-row.modal-row
   (2026-05-30). Inherits the sheet-row metric (14/16 padding,
   56px min, .modal-row + .modal-row hairline) so Minutes / Rounds /
   Breaths read as part of the same flat list as Sound / Haptic /
   Breath cues / Voice. The .modal-wheel sits on the right where
   a toggle would on the other rows.

   Legacy .modal-wheel-row rule removed with the markup that used
   it. */
.modal-row-wheel {
  /* The wheel is 64px tall (needed for the hero-tier picked glyph);
     bump the row past 56px so the wheel + row chrome don't overflow. */
  min-height: 72px;
  /* Vertical padding trimmed from 14/14 to 8/8 (2026-05-30) — the
     wheel widget already brings its own 64px of breathing room; the
     full .sheet-row padding around it made every wheel row read at
     ~92px, noticeably taller than the toggle/drill rows below them
     (~62px). 8/8 lands every wheel row at ~80px — closer to the
     toggle rows, consistent across Rounds / Breaths / per-round
     targets / Minutes on every tune sheet. */
  padding-top: var(--s-2);
  padding-bottom: var(--s-2);
}
/* .modal-wheel-label composes .lead-text in markup for the 16px
   size (per the CAT-T4 lint contract — size lives on the tier
   class, weight + color override stay here). */
.modal-wheel-label {
  flex: 1;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -.1px;
}
.modal-wheel {
  flex: 0 0 var(--wheel-w, 180px);
  /* Bumped 56 → 64 (2026-05-30) so the .is-current hero-tier 32px
     glyph renders at full size — at 56px the implicit line-height
     was clamping the centered numeral and killing the contrast jump. */
  height: 64px;
  overflow-x: scroll;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  position: relative;
  /* Subtle horizontal fade at the edges so flanking values feel
     like they sit under a viewport mask — no hard cutoff. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--s-3), #000 calc(100% - var(--s-3)), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 var(--s-3), #000 calc(100% - var(--s-3)), transparent 100%);
}
.modal-wheel::-webkit-scrollbar { display: none; }
.modal-wheel-track {
  display: flex;
  height: 100%;
}
.modal-wheel-item {
  flex: 0 0 var(--wheel-item-w, 60px);
  height: 100%;
  display: flex; align-items: center; justify-content: center;
  scroll-snap-align: center;
  /* scroll-snap-stop: always retired 2026-06-01 — was forcing the
     browser to land on every value during a flick, which made long
     scrolls (e.g. 0→30 on the Rounds wheel) feel sluggish. Dropping
     it lets momentum carry past many values per gesture, while
     scroll-snap-align: center still locks the final settle on a
     whole item. Matches the Reminders wheel's flick feel. */
  font-family: var(--font-display);
  font-weight: 400;
  /* Per-component step — neighbours stay small (16px) and faint,
     the centered .is-current item upsizes to hero-tier 32px so
     the picked value is unmistakable (~2× the neighbour size,
     matches the reference design). */
  font-size: var(--fs-md); /* lint-skip CAT-T4: .modal-wheel-item — per-component dim-neighbor type, not a body block */
  color: var(--text-4);
  opacity: .65;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.2px;
  transition: color var(--dur-tap), font-size var(--dur-tap), font-weight var(--dur-tap), opacity var(--dur-tap);
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
}
.modal-wheel-item.pad {
  pointer-events: none;
  visibility: hidden;
}
.modal-wheel-item.is-current {
  /* Centered item — full hero-tier size (32px), pure text colour,
     max weight, full opacity, tight line-height so the line-box
     doesn't truncate the glyph at the bottom of the 64px wheel. */
  font-size: var(--fs-h1); /* lint-skip CAT-T3: .modal-wheel-item.is-current — wheel's centered hero treatment */
  line-height: 1;
  color: var(--text);
  font-weight: 700;
  opacity: 1;
  letter-spacing: -.5px;
}

/* [DE-2] Duration slider */
/* The big "10 min" readout is now a button. Hover/active states are
   subtle so it reads as a value first and an affordance second —
   the slider sitting directly below is the loudest interaction. */
.modal-dur-section {
  padding: var(--s-1h) 0 var(--s-1);
}
/* The .modal-dur-readout / .modal-dur-slider(-wrap) / .modal-dur-ticks
   (+ .is-start / .is-end) rules — readout button, native range input
   with sage thumb + glow halo, sparse tick scale — were removed
   2026-05-30 with the slider markup itself. The wheel picker
   (.modal-wheel / .modal-wheel-row above) replaces all three. */

/* [DE-3] Sound sheet — chip rail + slim cue rows */
/* Replaces the four 100px tiles for River/Bowl/Birds/Ocean with a
   horizontally-scrollable chip rail that matches the Library category
   filter affordance already established elsewhere in the app. */
.sound-v2-chip-rail {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  overflow-y: hidden;
  padding: 2px var(--s-5) var(--s-1h);
  margin: 0 calc(-1 * var(--s-5));             /* bleed to sheet edges so the rail can scroll */
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.sound-v2-chip-rail::-webkit-scrollbar { display: none; }
/* Resting surface + tap-scale from .chip; below are the rail chip's own bits. */
.sound-v2-chip {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1h);
  padding: var(--s-2) var(--s-3h);
  border-radius: var(--r-pill);
  font-weight: 500;
  letter-spacing: .1px;
  white-space: nowrap;
  transition: background var(--dur-state), border-color var(--dur-state), color var(--dur-state), transform var(--dur-tap);
}
.sound-v2-chip:hover { background: var(--white-veil); border-color: var(--sage-170); }
.sound-v2-chip.is-on {
  background: var(--sage-soft);
  border-color: var(--sage);
  color: var(--sage);
  font-weight: 600;
}
.sound-v2-chip.is-dim { opacity: .42; }
.sound-v2-chip-ico {
  display: inline-flex;
  width: 14px;
  height: 14px;
}
.sound-v2-chip-ico svg { width: 100%; height: 100%; stroke: currentColor; stroke-width: 1.6; fill: none; }

/* SOON badge — used on the tune sheet's Voice-guidance row. (The sound-sheet
   cue rows it was named for moved to the tune sheet as standard .modal-rows.) */
.sound-v2-cue-row-badge {
  display: inline-block;
  margin-left: var(--s-2);
  padding: 2px var(--s-1h);
  border-radius: 999px;
  background: rgba(125,142,176,.13);
  color: var(--moon);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  vertical-align: 2px;
}

/* [DE-5] Share composer — live preview + mode toggle */
.composer-sheet {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-3h) var(--s-5) calc(20px + var(--safe-bottom));
  max-height: 90%;
  overflow-y: auto;
}
.composer-head {
  text-align: left;
}
.composer-head-title {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 700;
  letter-spacing: -.2px;
  color: var(--text);
  margin: 0 0 var(--s-1);
}
/* .composer-head-sub composes .caption in markup for the size. */
.composer-head-sub {
  color: var(--text-2);
  letter-spacing: .1px;
  margin: 0;
}

/* Attach toggle — a checkbox that opts the session card into the post.
   The card preview above is always visible; this controls whether it
   ships. Reads as a single tappable row: box + label. */
.composer-attach {
  display: flex;
  align-items: center;
  gap: var(--s-2h);
  width: 100%;
  padding: var(--s-1) 2px;
  background: transparent;
  border: none;
  cursor: pointer;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.composer-attach-box {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: var(--r-xs);
  background: var(--surface-soft);
  border: 1px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--bg, #07101c);
  transition: background var(--dur-state) var(--ease-ios), border-color var(--dur-state) var(--ease-ios);
}
.composer-attach-box svg {
  width: 14px;
  height: 14px;
  stroke: currentColor;
  stroke-width: 3;
  fill: none;
}
.composer-attach.is-on .composer-attach-box {
  background: var(--sage);
  border-color: var(--sage);
}
/* .composer-attach-label composes .body-text in markup for the size. */
.composer-attach-label {
  color: var(--text-2);
  letter-spacing: .1px;
  transition: color var(--dur-state) var(--ease-ios);
}
.composer-attach.is-on .composer-attach-label { color: var(--text); }
.composer-attach:active { transform: scale(.99); }

/* The preview area renders the actual session-card so users see what
   their friends will see. Wrapped in a card frame so it visually
   reads as a "preview" — distinct from the live feed below. */
.composer-preview-wrap {
  position: relative;
  padding: var(--s-3);
  border-radius: var(--r-lg);
  background:
    linear-gradient(155deg, var(--white-tint), var(--sage-wash));
  border: 1px dashed var(--white-line);
  transition: opacity var(--dur-state) var(--ease-ios);
}
/* When the card is unticked, dim the preview so it's clear it won't
   be posted — but keep it visible as the attainable/restorable state. */
.composer-preview-wrap.is-detached { opacity: .4; }
/* Type from .section-label.section-label--xs; keeps only its floating-pill chrome. */
.composer-preview-label {
  position: absolute;
  top: -8px;
  left: 16px;
  padding: 2px var(--s-2);
  border-radius: 999px;
  background: var(--bg, #07101c);
}

/* Optional note input — pill placeholder labels it as optional so
   the textarea doesn't read as a required prompt. */
.composer-note-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
/* type from .section-label (base 10px tier) in markup; .optional child below */
.composer-note-label .optional {
  color: var(--text-3);
  font-weight: 500;
  letter-spacing: .2px;
  text-transform: none;
  margin-left: var(--s-1);
  opacity: .8;
}
.composer-note {
  width: 100%;
  min-height: 64px;
  resize: none;
  padding: var(--s-3) var(--s-3h);
  border-radius: var(--r-md);
  background: var(--surface-soft);
  border: 1px solid var(--border);
  color: var(--text);
  letter-spacing: .1px;
  outline: none;
  transition: border-color var(--dur-state) ease, background var(--dur-state) ease;
  /* font-size + family + line-height composed via .body-text in markup */
}
.composer-note::placeholder { color: var(--text-3); }
.composer-note:focus {
  border-color: var(--sage-glow-45);
  background: var(--white-veil);
}

/* Share CTA — ghost outline per the design-system reference (the */
/* Composes the shared .btn-ghost (full-width sage outline pill, icon support);
   this only adds a faint hover background-tint on top of btn-ghost's border. */
.composer-cta {
  transition: border-color var(--dur-state) var(--ease-ios), background var(--dur-state) var(--ease-ios);
}
.composer-cta:hover { background: var(--sage-tint); }

/* [DE-6] Reminders — wheel visuals + day pills + "Once" preset */
/* Wheel MECHANICS preserved exactly: scroll-snap-y mandatory, momentum */

.reminder-time-wheel-wrap {
  height: 148px;
  /* Flat — no inset depth shadow, so the wheel reads as the sheet canvas
     (transparent fill + 6% edge only). */
  /* Scrollability affordance (2026-06-20): fade the top/bottom rows toward the
     canvas so the neighbour numbers (06/08, 29/31) read as "behind the window,
     waiting to be scrolled in". A mask (not a solid gradient overlay) fades to
     transparent → works regardless of the sheet's exact background, and the
     centre band (selected value) stays fully crisp. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 26%, #000 74%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 26%, #000 74%, transparent 100%);
}

/* (Stale sage selection-band removed 2026-08-01 — it was a leftover that
   overrode the intended white-mist hairlines in modals.css, painting a blue
   line across the reminders time wheel. modals.css .reminder-time-wheel-wrap::before
   is now the single source of truth for the band.) */

/* Fade masks — use the new soft surface color for the falloff so the
   fade reads as the wheel "breathing into" the page canvas, not
   into a navy card. */
/* Edge fade-mask removed (was the second copy of the wheel "gradient").
   The wheel blends flat into the sheet; edge numbers dim via --text4. */

.reminder-wheel-col { height: 148px; }

/* Wheel rows — refined type hierarchy. Item height stays at 44px to */
.reminder-wheel-item {
  height: 44px;
  font-size: var(--fs-h2-md); /* lint-skip CAT-T3: .reminder-wheel-item — sizes the wheel-picker numeral, not a title */
  font-weight: 300;
  /* 2026-06-02 — inactive numerals bumped to --text-3. Previous
     value (rgba 32% via the --text4 fallback) was so dim against the
     dark canvas that 07/09/01 read as decorative ghosts rather than
     legible neighbours of the selected value, failing contrast. The
     selected row still wins visually via the larger size + heavier
     weight + full --text colour. */
  color: var(--text-3);
  letter-spacing: 1px;
  font-variant-numeric: tabular-nums;
  transition: color var(--dur-state) var(--ease-ios), font-weight var(--dur-state) var(--ease-ios);
}
.reminder-wheel-item.is-selected {
  color: var(--text);
  font-weight: 500;
  font-size: var(--fs-h2-lg); /* lint-skip CAT-T3: .reminder-wheel-item.is-selected — sizes the wheel-picker numeral, not a title */
  letter-spacing: .5px;
}
.reminder-wheel-item.pad { height: 48px; }

/* Wheel separator — match the new selected-row weight so the time
   reads as one composed unit, not three loose elements. */
.reminder-wheel-sep {
  font-size: var(--fs-h2-md); /* lint-skip CAT-T3: .reminder-wheel-sep — sizes the wheel separator glyph, not a title */
  font-weight: 300;
  color: var(--text-2);
  padding: 0 var(--s-1h);
}

/* Day-pill row — tactile press, preserves existing sage fill on selected. */

/* "Once" preset — dashed at rest, solid when active. Reads as a
   one-off rather than a recurrence rule. */

/* Set Reminder button — enabled-by-default; commit-state polish. */

@media (prefers-reduced-motion: reduce) {
  .reminder-wheel-item { transition: none; }
  
}

/* Motion-reduce for the misc-component cluster (sound-sheet chips,
   composer mode/CTA buttons). Was joined by the modal duration
   slider's hover transition before the wheel picker replaced it. */
@media (prefers-reduced-motion: reduce) {
  .sound-v2-chip,
  .composer-attach,
  .composer-cta {
    transition: none;
  }
}

/* [DE-8] Statistics — reference-parity layout */

/* .stats-top-bar rule removed (2026-05-29) — pgStats switched to the
   canonical topBar() so the "Statistics" title now sits in the
   top-bar on the same line as the back chevron. The custom
   stats-top-bar surface is no longer needed. */
/* .stats-screen-title / .hist-screen-title / .comm-screen-title were three
   byte-identical definitions of the same destination-title typography. They
   are now consolidated into the canonical .page-title primitive (defined in
   03-primitives.css), with two layout modifiers for the in-bar vs flush
   contexts. These selectors are intentionally NOT moved here — see
   03-primitives.css › PAGE TITLE. Nothing references the old class names. */

/* Shared soft-card surface. Near-transparent fill (whisper of */
.stats-soft-card {
  /* FLAT (2026-07-22, user): no card chrome — sections sit directly on the page
     canvas, separated by hairline dividers. Vertical padding only so content
     aligns to the page gutter. */
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: var(--s-4h) 0;
  margin: 0;
}
/* Hairline dividers between the stacked, flat sections. */
.stats-pa-card { border-top: 1px solid var(--border); }

/* ══ STATISTICS · modular cards (2026-07-20 redesign) ═══════════════
   Total breaths · practice-activity bar chart · streak/hold duo. */
.stats-card-lbl {
  font-size: var(--fs-label);
  color: var(--text-2);          /* was --text-3 — brighter eyebrow, more readable (user) */
  letter-spacing: 1.2px;         /* more tracking (was .8px) for the uppercase micro-eyebrow */
  text-transform: uppercase;
  margin-bottom: var(--s-2);
  white-space: nowrap;
}
/* Total breaths */
.stats-breaths-row { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
.stats-breaths-num { line-height: 1; }   /* size via .hero-title (32px) — stepped down from 56 */
.stats-breaths-delta { color: var(--sage); font-weight: 600; white-space: nowrap; }
/* Streak · hold duo — flat, split by a vertical hairline, under a section divider. */
.stats-duo { display: flex; gap: 0; border-top: 1px solid var(--border); padding-top: var(--s-4h); }
.stats-duo > .stats-soft-card { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; padding: 0; }
.stats-duo > .stats-soft-card:first-child { padding-right: var(--s-4h); }
.stats-duo > .stats-soft-card:last-child { border-left: 1px solid var(--border); padding-left: var(--s-4h); }
/* translateX offsets the flame glyph's built-in left whitespace so the flame's
   visible edge lines up with the sub-line below (while staying flush to the number). */
/* baseline-align the flame + number + 'N to goal' so all three sit on one line;
   the flame gets a +2px drop because its glyph has empty space at the bottom of
   its viewBox, which otherwise floats it above the numeral's baseline. */
.stats-streak-top { display: flex; align-items: baseline; gap: 0; margin-bottom: 0; transform: translateX(-2px); }
.stats-streak-flame { display: inline-flex; color: var(--text); transform: translateY(2px); }
.stats-streak-flame svg { width: 22px; height: 22px; }
/* line-height:1 so the numeral's top aligns with the longest-hold value
   (.metric-display, also lh:1) in the sibling card — perfectly level. */
/* Both duo values share ONE large gold display size — a step up from
   .metric-display's 19px so the streak count + hold time read as matched hero
   numbers (user 2026-08-01). 2-class selector beats .metric-display cleanly.
   The "day" unit now lives in the sub-line, so the value is a clean number. */
.stats-streak-num.metric-display,
.stats-hold-val.metric-display {
  font-size: var(--fs-h2-md); /* lint-skip CAT-T3: matched hero stat numbers, not a .hero-title block */
}
.stats-streak-num { color: var(--text); margin: 0; }   /* crisp ink — matches the session-complete stat numbers; the warm accent now lives only in the pills (user 2026-08-05) */
/* Inline with the flame + number on one line (user 2026-08-01), sitting to the
   right of the count as a muted trailing note rather than a line below. */
.stats-streak-goal { color: var(--text-3); font-size: var(--fs-label); letter-spacing: .3px; margin-left: var(--s-2h); }
/* Longest hold — eyebrow (.stats-card-lbl) on top now, value below, matching the
   streak card's eyebrow→number structure (2026-08-01). The old bottom-caption
   (.stats-hold-cap) is retired: both cards now lead with the same uppercase
   eyebrow the rest of the page uses, so the duo folds into the page's one
   eyebrow style instead of a second sentence-case label style. */
.stats-hold-val { color: var(--text); }   /* crisp ink (see .stats-streak-num); size via .metric-display in markup */
.stats-hold-delta { color: var(--text-3); font-size: var(--fs-label); margin-top: var(--s-2); }
/* ── Practice activity — bar chart + W/M/Y toggle + foot stats ──────*/
.stats-pa-head {
  display: flex;
  align-items: center;   /* center the range pill against the title block */
  justify-content: space-between;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}
.stats-pa-title { font-size: var(--fs-h3); font-weight: 600; color: var(--text); line-height: 1.2; }
.stats-pa-sub { font-size: var(--fs-label); color: var(--text-2); margin-top: 2px; }  /* was --text-3 — lift to ≥4.5:1 (user) */

/* ══ STATISTICS · empty state (first-time user, 0 sessions) ═════════
   A ghosted preview of the live page: the "0" and headings are pushed
   back (muted), the chart is a hatched placeholder, and the "after your
   first session" list previews what the first session unlocks. */
/* Stacked: the '0' is the focus, the explainer sits neatly underneath in
   muted grey (was side-by-side, which left an awkward gap on the baseline). */
.stats-empty-num { color: var(--text-2); }
.stats-empty-hint { color: var(--text-2); margin-top: var(--s-1h); }  /* was --text-3 — lift secondary copy to ≥4.5:1 (user); size via .body-text */
/* Ghost chart — empty-state placeholder. UNIFORM baseline pills (was varying
   inline heights, which read as real data at 0 total breaths) + day labels so
   the week scope is legible. Decorative → aria-hidden in markup. */
.stats-ghost-bars {
  display: flex; align-items: flex-end; gap: var(--s-2);
  margin-top: var(--s-4);
}
.stats-ghost-col {
  flex: 1 1 0;
  display: flex; flex-direction: column; align-items: center; gap: var(--s-2);
}
.stats-ghost-bar {
  width: 100%;
  height: 6px;                        /* flat placeholder — no fake-data silhouette */
  border-radius: var(--r-pill);
  background: var(--white-rule);      /* faint, reads as "nothing here yet" */
}
.stats-ghost-day {
  font-size: var(--fs-micro);
  color: var(--text-3);
  letter-spacing: .5px;
}

/* "After your first session" list — icon tile + label per unlock. */
.stats-after-row {
  display: flex; align-items: center; gap: var(--s-3h);
  padding: var(--s-2h) 0;
}
.stats-after-row + .stats-after-row { margin-top: 2px; }
.stats-after-ico {
  /* Bare glyph, single sage accent — matches the canonical .row-icon used
     everywhere else (Subscription / Reminders / Statistics rows). Was a 44px
     coloured tile in sage/amber/violet, which broke the one-accent rule and
     read inconsistently against the rest of the app (user 2026-07-29). */
  width: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  color: var(--sage);
}
/* Soft cyan glow — the canonical sage drop-shadow the Home icons use, so these
   read as glowy sage glyphs consistent with the app (user 2026-07-29). */
.stats-after-ico svg { width: 24px; height: 24px; filter: drop-shadow(0 0 6px var(--sage-glow)); }
/* Body role in the page's 4-style scale (Title · Eyebrow · Display · Body).
   Was .lead-text + full --text: larger AND brighter than the hero "0" above,
   which inverted the visual weight and pulled the eye to the bottom of the
   screen (typographic-bloat audit, user 2026-08-01). Now .body-text size +
   --text-2 so the list reads as supporting body copy, level with the helper
   lines — the top statistics stay the primary anchor. */
.stats-after-text { color: var(--text-2); }
/* W/M/Y range toggle — flat text with an underline on the active one (mockup). */
/* Segmented-control pill (was underline tabs with a ~11px letter, near-impossible
   to tap). The subtle container + filled active segment expand each segment's hit
   box to a comfortable target AND give a defined box that centers cleanly against
   the title. (user 2026-08-05) */
.stats-range-seg {
  display: inline-flex;
  gap: 2px;
  flex-shrink: 0;
  padding: var(--s-1);
  background: var(--white-tint);
  border-radius: var(--r-pill);
}
.stats-range-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  min-height: 34px;
  padding: 0 var(--s-2h);
  background: transparent;
  border: 0;
  border-radius: var(--r-pill);
  color: var(--text-3);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: range-toggle button label, not caption prose */
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-state) ease, color var(--dur-state) ease;
}
.stats-range-btn:hover { color: var(--text-2); }
.stats-range-btn.is-on { background: var(--sage-soft); color: var(--sage); }
/* Bars */
.stats-bars {
  display: flex;
  align-items: flex-end;
  gap: var(--s-2h);
  height: 150px;
  margin-bottom: var(--s-3h);
}
.stats-bars.is-dense { gap: 3px; }
.stats-bar-col {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-1h);
  height: 100%;
}
.stats-bar-val {
  font-size: var(--fs-mini);
  font-weight: 600;
  color: var(--text-3);
  line-height: 1;
  min-height: 10px;
}
.stats-bar-val.is-empty { visibility: hidden; }
.stats-bar-track {
  flex: 1 1 0;
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
.stats-bar {
  width: 100%;
  max-width: 40px;
  border-radius: var(--r-sm);
  background: var(--sage-400);
  transition: height var(--dur-state) var(--ease-ios);
}
.stats-bars.is-dense .stats-bar { max-width: none; border-radius: 3px; }
/* Empty bucket — a faint fixed nub (fills the axis; no inline height). */
.stats-bar.is-empty {
  height: 4px;
  background: var(--white-line);
  box-shadow: none;
}
.stats-bar.has-val {
  background: linear-gradient(180deg, var(--sage) 0%, var(--sage-400) 100%);
}
.stats-bar.is-active.has-val {
  background: linear-gradient(180deg, var(--sage) 0%, var(--sage-300, var(--sage)) 100%);
  box-shadow: 0 0 14px 1px var(--sage-170);
}
.stats-bar-lbl {
  font-size: var(--fs-mini);
  font-weight: 500;
  color: var(--text-3);
  letter-spacing: 0.3px;
}
.stats-bar-lbl.is-active { color: var(--sage); font-weight: 700; }
/* Monthly view — replace the 30 per-column zero nubs (which read as a
   perforation line) with ONE continuous floor. Given the same visual weight as
   the yearly nub-dashes (3px, --white-line, rounded) so the bars read as
   connected to a solid baseline, not floating over a hairline. (user 2026-08-05) */
.stats-bars.is-month { position: relative; }
.stats-bars.is-month .stats-bar.is-empty { display: none; }
.stats-bars.is-month::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3px;
  border-radius: var(--r-sm);
  background: var(--white-line);
}
/* X-axis interval markers (start · mid · today) — muted slate, pulled up under
   the baseline. space-between flushes the ends to the chart edges. */
.stats-axis {
  display: flex;
  justify-content: space-between;
  margin: var(--s-1h) 0 var(--s-3h);
  font-size: var(--fs-mini);
  font-weight: 500;
  color: var(--text-3);
  letter-spacing: 0.3px;
}
/* Foot stats: total · avg · delta */
.stats-pa-foot {
  display: grid;
  grid-template-columns: repeat(3, 1fr);   /* perfectly even thirds */
  gap: var(--s-2);
  padding-top: var(--s-3h);
  border-top: 1px solid var(--border);
}
.stats-pa-stat { min-width: 0; }
/* Balanced distribution: left · centre · right across the three cells. */
.stats-pa-stat:nth-child(2) { text-align: center; }
.stats-pa-stat:nth-child(3) { text-align: right; }
.stats-pa-val { color: var(--text); }   /* size via .metric-display in markup */
.stats-pa-unit {
  font-size: var(--fs-label);
  font-weight: 500;
  color: var(--text-3);
  margin-left: 3px;
  font-family: var(--font-body);
}
.stats-pa-val--delta { color: var(--sage); }
.stats-pa-lbl { font-size: var(--fs-label); color: var(--text-3); margin-top: 2px; }

/* (.stats-history-btn — the "VIEW SESSION HISTORY" outline button at the bottom of
   the Stats page — was removed 2026-07-19 when Session History became a Profile row.
   The empty-state "Start First Session" CTA (.stats-empty-cta / .btn-primary) still
   docks at the bottom, so .stats-scroll's flex column is kept.) */
/* Container modifier — applied on pgStats's .scroll-content wrapper.
   Flex column lets the children stack with their natural heights
   (cards / lists) while the bottom CTA uses margin-top: auto to dock. */
.stats-scroll {
  display: flex;
  flex-direction: column;
  /* Gap below the "Statistics" title before the first card — matches the
     History page's title→content breathing room (its strip carries the
     same top margin); without it the TOTAL BREATHS card kissed the title. */
  padding-top: var(--s-3);
}

/* Motion-reduce — kill the practice-activity bar-height animation. */
@media (prefers-reduced-motion: reduce) {
  .stats-bar {
    transition: none;
  }
}

/* ≤380px phones */
@media (max-width: 380px) {
  /* (was .stats-screen-title — no-op, base size is already --fs-h2; kept
     targeting the new primitive so the rule's intent stays explicit) */
  .page-title { font-size: var(--fs-h2); }
  /* (.stats-life-val / .stats-mile-* narrow-width overrides removed 2026-07-20 with
     the modular-cards Stats redesign that retired those elements.) */
}

/* Top bar · left-aligned title */
.hist-top-bar {
  justify-content: flex-start;
  gap: var(--s-3);
  padding:
    calc(12px + var(--safe-top))
    max(var(--gutter), var(--safe-right))
    var(--s-2)
    max(var(--gutter), var(--safe-left));
}
/* Month picker in the History header — sage, inline (chevron beside the
   label), centered against the title. Without this the raw chevron svg
   rendered at its default box and shoved "July" up out of line. */
.hist-month {
  margin-left: auto;         /* push to the right edge (title takes the flex space) */
  align-self: center;
  position: relative;
  top: 1px;                  /* match the .page-title--bar 1px nudge so both sit level */
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex-shrink: 0;
  color: var(--sage);        /* size via .body-text — kept subordinate to the title */
  font-weight: 500;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.hist-month-chev { display: inline-flex; }
.hist-month-chev svg { width: 14px; height: 14px; stroke-width: 2.4; }

/* ── Calendar popover — pick any past day ───────────────────────────*/
.hist-cal-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
}
.hist-cal {
  position: fixed;
  z-index: calc(var(--z-overlay) + 1);
  /* Drops down from the 'July ▾' month picker (top-right) rather than sitting
     centred like a modal. Anchored just below the header, right-aligned. */
  top: calc(var(--safe-top, 0px) + 60px);
  right: max(var(--gutter), var(--safe-right));
  left: auto;
  width: min(330px, calc(100vw - 2 * var(--gutter)));
  transform-origin: top right;
  animation: hist-cal-pop var(--dur-state) var(--ease-ios) both;
  background: var(--bg-2);
  border: 1px solid var(--white-line);
  border-radius: var(--r-lg);
  padding: var(--s-4);
  box-shadow: var(--elev-3);
}
@keyframes hist-cal-pop {
  from { opacity: 0; transform: translateY(-6px) scale(.96); }
  to   { opacity: 1; transform: translateY(0)    scale(1); }
}
.hist-cal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--s-3h);
}
.hist-cal-title { font-size: var(--fs-h3); font-weight: 600; color: var(--text); }
.hist-cal-nav {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--white-line);
  background: transparent;
  color: var(--text-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color var(--dur-state) ease, color var(--dur-state) ease;
}
.hist-cal-nav:hover:not(:disabled) { border-color: var(--sage); color: var(--sage); }
.hist-cal-nav:disabled { opacity: .3; cursor: default; }
.hist-cal-nav svg { width: 16px; height: 16px; }
.hist-cal-dow {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  margin-bottom: var(--s-1h);
}
.hist-cal-dow span {
  text-align: center;
  font-size: var(--fs-mini);
  font-weight: 600;
  color: var(--text-3);
  letter-spacing: 0.3px;
}
.hist-cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--s-1);
}
.hist-cal-cell {
  position: relative;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  border-radius: var(--r-sm);
  color: var(--text-2);
  font-size: var(--fs-label);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background var(--dur-state) ease, color var(--dur-state) ease;
}
.hist-cal-cell.is-blank { visibility: hidden; }
.hist-cal-cell.is-future { color: var(--text-3); opacity: .35; cursor: default; }
.hist-cal-cell:hover:not(.is-future):not(.is-sel) { background: var(--white-mist); }
.hist-cal-cell.is-today { color: var(--sage); font-weight: 700; }
.hist-cal-cell.is-sel {
  background: var(--sage);
  color: var(--bg-0);
  font-weight: 700;
}
/* Session-day marker dot under the number. */
.hist-cal-cell.has-data::after {
  content: '';
  position: absolute;
  bottom: 4px;
  left: 50%;
  transform: translateX(-50%);
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--sage);
}
.hist-cal-cell.is-sel.has-data::after { background: var(--bg-0); }
/* Optical centering: the row is already align-items:center, but the display
   font (line-height:1) sits high in its line-box, so the title reads ~1px above
   the 40px back-button's centre. Nudge it down to true-centre (2026-06-20). */
.hist-top-bar .page-title { position: relative; top: 1px; }
/* .hist-screen-title → .page-title.page-title--bar (see 03-primitives.css) */

/* .comm-top-bar rule removed (2026-05-29) — pgCommunity now uses the
   canonical topBar() with back-to-profile. */
/* .comm-screen-title → .page-title.page-title--bar (see 03-primitives.css) */

/* Community Friends / Compete · soft-surface containers */

/* .comm-search-wrap .search-input selector removed 2026-05-31 — the
   surface (--surface-soft + border) it set is already the .search-input
   default in modals.css, so this clause was redundant even before the
   .search-bar componentization. */
.comm-friend-row,
.comm-suggested-row {
  background: var(--surface-soft);
  border: 1px solid var(--border);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
}
/* Leaderboard rows on the Compete tab also share the soft surface */

/* History page — FLAT. The week card, recent rows and empty state
   all sit directly on the root background. Recent rows divide with
   the same hairline pattern used on .row-flat-list / .tech-row, so
   the list reads without a wrapping box. The week card keeps its
   own padding (set below) so the bars don't kiss the page gutter. */
.hist-empty {
  background: transparent;
  border: 0;
  border-radius: 0;
}
/* Centered empty state (2026-06-24) — an icon-led block centered in the upper
   screen, matching the Library 'No favorites' / 'No results' empties. Kept as
   its own rule (not shared with the session cards) so only the first-run empty
   state gets the 52vh centered column. */
.hist-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  justify-content: center;
  min-height: 52vh;
  gap: var(--s-2);
}
.hist-empty-icon {
  width: 56px; height: 56px;
  margin-bottom: var(--s-2);
  border-radius: 50%;
  background: var(--sage-soft);
  color: var(--sage);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.hist-empty-icon svg { width: 24px; height: 24px; }
.hist-empty-cta { margin-top: var(--s-3h); min-width: 220px; }

/* ── Day filter — horizontal week strip ─────────────────────────────
   Seven day cells (Mon-start) that replace the old vertical bar-chart
   (2026-07-20). Today wears a cyan ring; the selected day fills cyan
   (dark ink); a day with no sessions dims its number. Tapping a day
   filters the list to it; tapping the selected day clears. */
.hist-daystrip {
  display: flex;
  gap: var(--s-2);
  margin: var(--s-3) 0 var(--s-4h);   /* breathe under the header + before the filter */
}
.hist-day {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-1h);
  padding: var(--s-2) 0 var(--s-2h);
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--dur-state) ease, border-color var(--dur-state) ease;
}
.hist-day:hover { background: var(--white-mist); }
.hist-day-dow {
  font-size: var(--fs-mini);
  font-weight: 500;
  color: var(--text-3);
  letter-spacing: 0.4px;
}
.hist-day-num {
  font-size: var(--fs-h3);
  font-weight: 600;
  color: var(--text);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.hist-day.is-blank .hist-day-num { color: var(--text-3); }
/* Exactly ONE solid active pill at a time. When a day is picked (strip
   .is-filtered) that selected day is the solid pill and today drops to a quiet
   teal outline marker; with no selection, today itself is the solid pill. This
   avoids two/three filled pills (today + selection). Future days (beyond today)
   dim to separate upcoming from past/logged days, so the strip reads
   past → today → future at a glance. (user 2026-08-05) */
.hist-day.is-sel,
.hist-day.is-sel:hover,
.hist-daystrip:not(.is-filtered) .hist-day.is-today {
  background: var(--sage);
  border-color: var(--sage);
}
.hist-day.is-sel .hist-day-dow,
.hist-day.is-sel .hist-day-num,
.hist-daystrip:not(.is-filtered) .hist-day.is-today .hist-day-dow,
.hist-daystrip:not(.is-filtered) .hist-day.is-today .hist-day-num { color: var(--bg-0); }
/* Today marker while a different day is selected — outline, not fill. */
.hist-daystrip.is-filtered .hist-day.is-today:not(.is-sel) { border-color: var(--sage); }
.hist-day.is-future { opacity: .45; }

/* ── Technique dropdown filter ──────────────────────────────────────
   Full-width trigger → listbox of the techniques the user has logged.
   A fixed scrim closes it on any outside tap. */
.hist-techfilter {
  position: relative;
  margin-bottom: var(--s-5);
}
.hist-techfilter-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-3h);
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  cursor: pointer;
  transition: background var(--dur-state) ease, border-color var(--dur-state) ease;
}
.hist-techfilter-btn:hover { background: var(--white-mist); }
.hist-techfilter-label { font-weight: 500; }   /* composes .body-text */
.hist-techfilter-chev {
  display: inline-flex;
  color: var(--text-3);
  transition: transform var(--dur-state) var(--ease-ios);
}
.hist-techfilter-chev svg { width: 16px; height: 16px; }
.hist-techfilter-chev.is-open { transform: rotate(180deg); }
/* Inline expanding list (not a floating card) — flush with the page, just
   hairline dividers, exactly like the Language settings picker. */
/* Bordered card panel: full-width 'All techniques' row (sage-highlighted
   when active) above a 2-column grid of techniques, each with a session
   count on the right + a sage check when selected (2026-07-21 mockup). */
.hist-techmenu {
  margin-top: var(--s-2);
  border: 1px solid var(--white-line);
  border-radius: var(--r-lg);
  background: var(--bg-2);
  padding: var(--s-1h);
  max-height: 48vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.hist-techmenu-item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--s-2h);
  min-height: 44px;   /* comfortable tap target for the single-column list */
  padding: var(--s-2h) var(--s-3);
  background: transparent;
  border: 0;
  border-radius: var(--r-md);
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-state) ease;
}
.hist-techmenu-item:hover { background: var(--white-mist); }
.hist-techmenu-item.active { background: var(--sage-soft); }
.hist-techmenu-item.active:hover { background: var(--sage-soft); }
.hist-techmenu-all { margin-bottom: var(--s-1); }
/* Single-column vertical list (was a 2-col grid, which forced a jagged Z-scan
   and left a lone item hanging — user 2026-08-05). Mobile users scan/scroll
   vertically; one column per row is instantly scannable. */
.hist-techmenu-grid { display: flex; flex-direction: column; }
.hist-techmenu-label {
  flex: 1;
  min-width: 0;
  font-weight: 500; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}   /* composes .body-text */
.hist-techmenu-item.active .hist-techmenu-label { color: var(--sage); }
.hist-techmenu-check { display: inline-flex; width: 16px; height: 16px; color: var(--sage); flex-shrink: 0; }
.hist-techmenu-check svg { width: 100%; height: 100%; }

/* Filters active but nothing matches (history isn't empty). */
.hist-nomatch {
  text-align: center;
  padding: var(--s-6) var(--s-4);
}
.hist-nomatch-title { color: var(--text-3); margin: 0 0 var(--s-3); }
.hist-nomatch-clear {
  background: transparent;
  border: 0;
  color: var(--sage);
  font-weight: 600;
  font-size: var(--fs-label);
  cursor: pointer;
  padding: var(--s-1) var(--s-2);
}

/* Date-group header inside the Recent list — quieter than the
   .section-label 'RECENT' eyebrow above so the dates read as a
   SUB-grouping, not a peer section. Mixed-case (date strings preserve
   their natural form: 'Today', 'Yesterday', 'Apr 23'), 600 weight,
   muted --text-3. The .caption companion in markup carries the size +
   tracking; this rule layers color/weight/margins only. Top margin
   spaces consecutive groups; :first-of-type collapses the top gap so
   the first group sits flush under the 'RECENT' label like the old
   ungrouped list did. Added 2026-06-05 with the date-grouping commit. */
/* Per-day header row: the date label (left) + that day's session/min summary (right). */
.hist-date-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  margin: var(--s-5) 0 var(--s-2h);
}
.hist-date-head:first-of-type { margin-top: var(--s-3); }
.hist-date-group {
  /* The per-day header is now the list's section structure (the "Recent" label was
     dropped), so it reads near-white to anchor each day section. */
  color: var(--text);
  font-weight: 600;
  margin: 0;   /* .hist-date-head owns the vertical rhythm now */
}
.hist-date-sum {
  /* Sage summary ("7 sessions · 15 min") — the day's total activity, mockup-cyan. */
  color: var(--sage);
  font-weight: 600;
  white-space: nowrap;
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

/* ── Session rows (2026-07-20) ──────────────────────────────────────
   Flat rows separated by hairlines — a navy circle with the technique's
   glyph, name + time (· personal best) on the left, duration + an amber
   HOLD pill on the right. No card chrome / progress bar. */
.hist-rec {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) 0;   /* tighter rows now the text is 16px (was --s-3h) */
}
.hist-rec + .hist-rec { border-top: 1px solid var(--border); }
.hist-rec-ico {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--icon-fill);
  color: var(--sage);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.hist-rec-ico svg { width: 20px; height: 20px; }
.hist-rec-body { flex: 1; min-width: 0; }
/* .hist-rec-name composes .row-title in markup — identical text style to
   the Profile / Settings list rows (16px / 500 / --text). Companion only
   keeps margin + truncation; no size/weight/colour overrides. */
.hist-rec-name {
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* .hist-rec-meta composes .sub-text in markup (12px); companion just
   brightens color + adds the small top gap under the name. */
.hist-rec-meta {
  margin: 2px 0 0;
  color: var(--text-3);
}
.hist-rec-pbtag { color: var(--sage); font-weight: 600; }
.hist-rec-right {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s-2);
}
/* .hist-rec-dur composes .lead-text (16px) — trailing value at the SAME
   size + face as the name, just a touch heavier so it reads as the value. */
.hist-rec-dur {
  margin: 0;
  font-weight: 600;
  color: var(--text);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* Unified with the sage .row-badge ("Upgrade") style — was an amber badge; now
   sage-soft fill + sage text so it reads as the app's canonical accent pill.
   Keeps the compact history-row size (mini / tabular). (user 2026-08-06) */
.hist-rec-hold {
  display: inline-block;
  font-size: var(--fs-mini);
  font-weight: 700;
  letter-spacing: 0.4px;
  color: var(--sage);
  background: var(--sage-soft);
  padding: var(--s-1) var(--s-2h);
  border-radius: var(--r-pill);
  font-variant-numeric: tabular-nums;
}

/* Empty state */
.hist-empty {
  text-align: center;
  padding: var(--s-6) var(--s-4h);
}
/* .hist-empty-ico rule removed 2026-05-30 — the wind glyph above the
   "No sessions yet" copy was dropped per user direction; the headline +
   sub + CTA pill carry the empty state without it. */
/* .hist-empty-title composes .body-text in markup for the size. */
.hist-empty-title {
  color: var(--text-2);
  font-weight: 500;
  margin: 0;
}
/* .hist-empty-sub composes .sub-text in markup — canonical row-sub
   pattern. Color text-3 already lives on .sub-text; companion only
   sets the top-margin off the title above. */
.hist-empty-sub {
  margin: var(--s-1) 0 0;
}
/* "Start First Session" pill — routes to Library so the user picks
   their first technique instead of being auto-launched into one. Sits
   centered below the sub with a comfortable gap; min-width lets the
   pill feel substantial without spanning the full text width. */
.hist-empty-cta {
  margin: var(--s-5) auto 0;
  display: inline-flex;
  min-width: 200px;
  justify-content: center;
}
/* Same primary pill on the Stats page when totalSessions === 0 —
   mirrors the History empty-state pattern. Sits centered above the
   Mastery card with a comfortable gap below so it doesn't crowd the
   page-of-zeros content. */
.stats-empty-cta {
  /* 2026-06-04 — dock to the bottom of the .stats-scroll flex column
     (margin-top: auto eats the slack between the last milestone row above
     and the button). Was sitting
     at the top of the page under the title; user moved it to the bottom
     'move the start button to the bottom of the screen' so the new
     user reads the whole zero-state above before reaching for the CTA,
     and so the bottom of the viewport always holds exactly one
     primary action regardless of session count. */
  margin-top: auto;
  display: flex;
  justify-content: center;
}

/* ── Legal pages (Privacy / Terms) ────────────────────────────
   In-app legal text, simple typographic treatment. Composes
   .body-text in markup for the size + family — this rule layers
   color, paragraph spacing, and the muted h2 sub-heading style.
   Added 2026-06-05 with pgPrivacy + pgTerms (replaces broken
   external URL links to a domain that didn't resolve). */
.legal-page {
  color: var(--text);
}
.legal-page p {
  margin: 0 0 var(--s-4);
}
.legal-page .legal-meta {
  color: var(--text-3);
  margin-bottom: var(--s-5);
}
.legal-h {
  /* Size + line-height come from .lead-text composed in markup
     (16px / 1.4). This rule layers family + weight + color + the
     vertical rhythm between sections. */
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--text);
  letter-spacing: -.1px;
  margin: var(--s-5) 0 var(--s-2h);
}

/* Disclaimer — single rose-tinted card at the top of the page */
.safe-disc {
  background: rgba(224, 138, 138, 0.06);
  border: 1px solid rgba(224, 138, 138, 0.22);
  border-radius: var(--r-lg2);
  padding: var(--s-4);
  margin-bottom: var(--s-5);
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
}
.safe-disc-ico {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: rgba(224, 138, 138, 0.14);
  color: var(--rose);
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.safe-disc-ico svg {
  width: 14px;
  height: 14px;
  stroke: currentColor;
  stroke-width: 1.8;
  fill: none;
}
/* .safe-disc-name composes .caption in markup for the size. */
.safe-disc-name {
  font-weight: 700;
  color: var(--rose);
  letter-spacing: -.1px;
  margin: 0 0 var(--s-1h);
}
/* .safe-disc-body composes .caption in markup; lh:1.55 overrides .caption's 1.45. */
.safe-disc-body {
  line-height: 1.55;
  color: var(--text-2);
  margin: 0;
}

/* Section eyebrow label (default neutral; .warn adds rose) */
.safe-sec {
  font-size: var(--fs-mini);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-3);
  font-weight: 600;
  margin: 0 0 var(--s-3);
}
.safe-sec.warn { color: var(--rose); }
.safe-sec-pre {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1h);
}
.safe-sec-ico {
  width: 11px;
  height: 11px;
  display: inline-flex;
  color: currentColor;
}
.safe-sec-ico svg {
  width: 11px;
  height: 11px;
  stroke: currentColor;
  stroke-width: 2;
  fill: none;
}

/* Lists — typographic bullets, no per-row card chrome */
.safe-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
/* .safe-list li composes .body-text in markup for the --fs-body size.
   The local line-height: 1.55 overrides .body-text's 1.5 (denser bullets). */
.safe-list li {
  position: relative;
  padding-left: var(--s-4h);
  line-height: 1.55;
  color: var(--text);
}
.safe-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 9px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  /* All safety bullets are coral (2026-06-16 user direction) — one consistent
     dot colour across every section, not gray for advisory + rose for danger. */
  background: var(--rose);
}
/* The STOP section keeps the extra glow halo as its "danger right now" accent;
   the dot colour is the same coral as every other section above. */
.safe-list.warn li::before {
  background: var(--rose);
  box-shadow: 0 0 6px rgba(224, 138, 138, 0.35);
}
.safe-list b {
  color: var(--text);
  font-weight: 600;
}

/* Sources footnote — clinical/informational tint (moon), not a card */
.safe-foot {
  font-size: var(--fs-label);
  line-height: 1.6;
  color: var(--text-3);
  padding: var(--s-3h) var(--s-4);
  margin: var(--s-1) 0 0;
  background: rgba(125, 142, 176, 0.06);
  border-left: 2px solid var(--moon);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
}

/* ════════════════════════════════════════════════════════════════
   DIAGNOSTICS page (pgDiagnostics) — local-only error breadcrumb log,
   reached from Settings → Diagnostics. Technical content; the detail
   block keeps newlines (pre-wrap) but stays in Inter per the type system.
   Nothing here ever leaves the device.
   ════════════════════════════════════════════════════════════════ */
/* .diag-note composes .caption in markup; lh:1.55 overrides .caption's 1.45. */
.diag-note {
  line-height: 1.55;
  color: var(--text-3);
  margin: 0 0 var(--s-4);
}
.diag-actions {
  display: flex;
  gap: var(--s-2h);
  margin-bottom: var(--s-4);
}
.diag-btn {
  flex: 1;
  padding: var(--s-2h) var(--s-3);
  border-radius: var(--r-lg);
  background: var(--white-veil);
  border: 1px solid var(--white-mist);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: .5px;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color var(--dur-state) var(--ease-ios);
}
.diag-btn:hover { border-color: var(--border-strong); }
.diag-btn-clear { color: var(--rose); }
.diag-entry {
  background: var(--white-wash);
  border: 1px solid var(--white-mist);
  border-radius: var(--r-lg);
  padding: var(--s-3);
  margin-bottom: var(--s-2h);
}
.diag-entry-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  margin-bottom: var(--s-2);
}
.diag-entry-kind {
  font-size: var(--fs-mini);
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--rose);
}
.diag-entry-ts {
  font-size: var(--fs-mini);
  color: var(--text-3);
}
.diag-entry-detail {
  margin: 0;
  font-size: var(--fs-label);
  line-height: 1.5;
  color: var(--text-2);
  white-space: pre-wrap;
  word-break: break-word;
}
/* Composes .empty-state (centered + text3) and .body-text in markup;
   keeps only the padding override here. */
.diag-empty {
  padding: var(--s-7) var(--s-4);
}

/* ── Add-to-Home-Screen nudge (data-durability) ──────────────────
   One-time Home card prompting install so the localStorage-only data
   isn't evicted by iOS ITP (see installNudgeHtml in pages.js). Quiet
   transparent-flat surface per the home visual direction — the sage
   glow is reserved for the PLAY button, so this stays a soft tint. */
.install-nudge {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: var(--s-4);
  padding-right: var(--s-6);            /* clearance for the close button */
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.install-nudge-icon {
  flex-shrink: 0;
  width: 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-md);
  background: var(--sage-soft);         /* canonical tile bg */
  color: var(--sage);
}
.install-nudge-icon svg { width: 22px; height: 22px; }
.install-nudge-body {
  min-width: 0;
  display: flex; flex-direction: column;
  gap: var(--s-1);
}
.install-nudge-sub { margin-bottom: var(--s-1); }
.install-nudge-close {
  position: absolute;
  top: var(--s-2); right: var(--s-2);
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none;
  color: var(--text-3); cursor: pointer;
  border-radius: var(--r-sm);
}
.install-nudge-close svg { width: 16px; height: 16px; }
.install-nudge-cta {
  display: inline-flex; align-items: center; gap: var(--s-1h);
  align-self: flex-start;
  margin-top: var(--s-1);
  padding: var(--s-1h) var(--s-3);
  background: var(--sage-soft);
  color: var(--sage);
  border: 1px solid var(--border-warm);
  border-radius: var(--r-pill);
  cursor: pointer;
}
.install-nudge-cta svg { width: 15px; height: 15px; }
.install-nudge-ios { margin-top: var(--s-1); }
.install-nudge-share { display: inline-flex; vertical-align: middle; }
.install-nudge-share svg { width: 14px; height: 14px; vertical-align: middle; }

/* ===========================================================
   session.css
   COMPONENT · SESSION COMPLETE (lean) — the lean session-complete variant: hero check ring, lean metrics card, new-PB amber pill, action stack.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* SESSION COMPLETE · lean cut */

/* The body for the lean variant overrides the looser `gap:var(--s-4)` of
   the base .pc-body — the screen now has only 3-4 blocks so the
   spacing reads as deliberate, breath-paced. */
.pc-screen .pc-body.pc-body-lean {
  gap: var(--s-4h);
  padding-top: var(--s-3);
}
/* Content group (zones 1–3) vertically centered in the space ABOVE the pinned
   actions, so a short (rhythm) session isn't top-jammed with a hollow middle.
   The auto-margins on the first/last child do the centering AND collapse to 0
   when the content is taller than the area (long retention session on a short
   phone) — so it scrolls from the top instead of clipping. Actions stay bottom-
   pinned (thumb reach) because this group is flex:1 and eats the free space. */
.pc-screen .pc-body.pc-body-lean .pc-content-lean {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-4h);
  overflow-y: auto;
  scrollbar-width: none;
}
.pc-screen .pc-body.pc-body-lean .pc-content-lean::-webkit-scrollbar { display: none; }
.pc-screen .pc-body.pc-body-lean .pc-content-lean > :first-child { margin-top: auto; }
.pc-screen .pc-body.pc-body-lean .pc-content-lean > :last-child  { margin-bottom: auto; }

/* Sage check ring — the existing .pc-hero-icon animation handles */
.pc-hero-icon {
  width: 92px;
  height: 92px;
  border-radius: 50%;
  border: 1.5px solid var(--sage);
  background: radial-gradient(circle, var(--sage-100) 0%, var(--bg-1) 70%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--sage);
}
.pc-hero-icon .pc-check-svg {
  width: 30px;
  height: 30px;
  color: var(--sage);
}
.pc-hero-icon .pc-check-svg svg {
  width: 100%;
  height: 100%;
  stroke: var(--sage);
}

/* Lean metrics card — FLAT. The surrounding chrome (white-wash fill,
   hairline border, radius) is stripped so the metrics row sits
   directly on the celebration screen's amber-tinted canvas. The
   per-cell hairlines below (.stat-col + .stat-col::before) still
   divide Duration · Best Hold · Rounds, so the row reads as three
   distinct stats without the wrapping box. */
.pc-card.pc-metrics-lean {
  padding: var(--s-4h) var(--s-3h);
  background: transparent;
  border: 0;
  border-radius: 0;
}
.pc-card.pc-metrics-lean .pc-stat-row {
  width: 100%;
  margin: 0;
  background: transparent;       /* override base .stat-row navy chrome */
  gap: 0;                        /* no gap — the hairline lives on the col */
  border-radius: 0;
  overflow: visible;
}
.pc-card.pc-metrics-lean .stat-col {
  background: transparent;       /* override base .stat-col --card fill */
  padding: var(--s-1) var(--s-2);
  gap: var(--s-2);
  position: relative;
}
/* Internal vertical hairlines — softer than the base --border divider.
   Sits on cols 2 + 3 as a left-edge 1px line, fading at the top/bottom
   so it reads as a whisper, not a stripe. */
.pc-card.pc-metrics-lean .stat-col + .stat-col::before {
  content: '';
  position: absolute;
  left: 0;
  top: 14%;
  bottom: 14%;
  width: 1px;
  background: var(--border);
  opacity: .6;
}
.pc-card.pc-metrics-lean .stat-val {
  font-family: var(--font-display);
  font-size: var(--fs-h2-md); /* lint-skip CAT-T3: .stat-val — sizes the numeric session-stat value, not a title */
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -.4px;
  color: var(--text);
  margin-bottom: var(--s-1h);
  font-variant-numeric: tabular-nums;
}
/* The fmtDuration split-numeral path uses .stat-val-num inside .stat-val */
.pc-card.pc-metrics-lean .stat-val-num {
  font-size: var(--fs-h2-md); /* lint-skip CAT-T3: .stat-val-num — sizes the numeric session-stat value, not a title */
  font-weight: 600;
  letter-spacing: -.4px;
}
/* .stat-val-unit in the lean variant is 14px (--fs-body), overriding the base
   .stat-val-unit (16px / --fs-md) in lists.css. The .body-text class on the
   markup loses to lists.css in the cascade (same specificity, later file), so
   the explicit override here is what actually renders. */
.pc-card.pc-metrics-lean .stat-val-unit {
  font-size: var(--fs-body); /* lint-skip CAT-T1: per-context override of the lists.css base size (lean variant) */
  font-weight: 500;
  color: var(--text-3);
}
.pc-card.pc-metrics-lean .stat-lbl {
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--text-3);
}

/* New-PB amber pill — a status row, never a button. Sits on a
   translucent amber field with a soft amber border, paired with
   a trophy/streak glyph on the left and the time value on the
   right. The pill is keyboard-inert (no focus ring) because it is
   purely informational. */
.pc-pb-pill {
  display: flex;
  align-items: center;
  gap: var(--s-2h);
  width: 100%;
  padding: var(--s-3) var(--s-3h);
  border-radius: var(--r-lg);
  background: var(--amber-50);
  border: 1px solid var(--amber-100);
  box-sizing: border-box;
  user-select: none;
}
.pc-pb-pill-ico {
  display: inline-flex;
  width: 18px;
  height: 18px;
  color: var(--amber);
  flex-shrink: 0;
}
.pc-pb-pill-ico svg { width: 100%; height: 100%; }
/* .pc-pb-pill-text composes .body-text in markup for the --fs-body size. */
.pc-pb-pill-text {
  flex: 1;
  min-width: 0;
  font-weight: 500;
  letter-spacing: .1px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* .pc-pb-pill-val composes .body-text in markup for the --fs-body size. */
.pc-pb-pill-val {
  font-family: var(--font-display);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--amber);
  letter-spacing: .2px;
  flex-shrink: 0;
}

/* Action stack in the lean variant — only two CTAs visible */
.pc-actions.pc-actions-lean {
  gap: var(--s-3);
  padding-top: var(--s-4);
}

/* The "Practice again" button in the lean variant takes a sage */
.pc-actions-lean .pc-secondary {
  border-color: var(--border-warm);
  color: var(--sage);
  text-transform: uppercase;
  letter-spacing: 1.4px;
  font-size: var(--fs-caption); /* lint-skip CAT-T2: button text (uppercase tracked), not body caption */
  padding: var(--s-3h) var(--s-4h);
}

/* ═══════════════════════════════════════════════════════════════
   PER-ROUND RETENTION BREAKDOWN  (.pc-rounds-card)
   ───────────────────────────────────────────────────────────────
   Surfaces each round's retention as a horizontal bar so the user
   can SEE the arc of the session — first round shorter, later
   rounds longer, or wherever the peak fell. Bars are scaled to the
   session's peak hold; the longest row gets a sage-tinted index
   number and an inline LONGEST chip pinned to the right end of
   its fill (overlapping the gradient end so it works at 100% fill).
   Added 2026-06-02 per user-attached reference; chrome thinned
   2026-06-02 to match the .flat-card transparency used by the
   community "Your Connections" card (2% white-wash, not 6%).
   ═══════════════════════════════════════════════════════════════ */
.pc-card.pc-rounds-card {
  /* Flat on the canvas (card chrome dropped). Horizontal padding
     matches .pc-card.pc-metrics-lean's --s-3h (14px) so the title
     'Breath hold length' and the row content start at the same
     vertical column as 'DURATION · BEST HOLD · ROUNDS' above. Was
     padding: var(--s-4) 0 — that pushed the title 14px to the LEFT
     of the metrics row's first column, breaking the page's left
     alignment. */
  padding: var(--s-4) var(--s-3h);
  background: transparent;
  border: 0;
  border-radius: 0;
  /* margin-top dropped 2026-06-24 — it compounded with the .pc-content-lean flex
     gap, opening an oversized void between the stat row and this breakdown. The
     gap alone reads cleaner. */
}
/* .pc-rounds-title composes .body-text in markup for the size; the
   role-specific styling (weight + ls + color) sits here. */
.pc-rounds-title {
  font-weight: 600;
  letter-spacing: .2px;
  color: var(--text);
  margin: 0 0 var(--s-3);
}
.pc-rounds-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.pc-round-row {
  display: grid;
  grid-template-columns: 32px 1fr auto;
  gap: var(--s-3h);
  align-items: center;
  padding: var(--s-3) 0;
  min-height: 44px;
}
.pc-round-row:first-child {
  padding-top: var(--s-2);
}
.pc-round-row:last-child {
  padding-bottom: var(--s-2);
}
/* 2026-06-02 — per-row hairline divider removed per user feedback.
   The vertical rhythm (gap from row padding + min-height) is enough
   to read each round as its own row; the divider added a stripe
   that competed with the bars. */
.pc-round-num {
  font-family: var(--font-display);
  font-size: var(--fs-body); /* lint-skip CAT-T1: list-index numeral, role-specific (.pc-round-num) */
  font-weight: 600;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  letter-spacing: .5px;
}
.pc-round-row.is-longest .pc-round-num {
  color: var(--sage);
}
.pc-round-bar {
  position: relative;
  height: 6px;
  background: var(--white-mist);
  border-radius: var(--r-pill);
  /* overflow visible so the LONGEST chip can sit half outside the
     fill, riding the bar's end like a flag on a pole. */
  overflow: visible;
}
.pc-round-bar-fill {
  position: relative;
  height: 100%;
  /* Sage gradient — lighter on the left, fuller on the right —
     reads as "more = stronger" without using a chrome color the
     rest of the screen doesn't share. The 'soft' anchor matches
     the muted left half in the reference. */
  background: linear-gradient(90deg,
    rgba(125,211,232,0.45) 0%,
    var(--sage) 100%);
  border-radius: var(--r-pill);
  /* 2026-06-02 — cyan box-shadow halo removed. The 12px glow bled
     above + below the 6px bar as a soft cyan horizontal stripe
     that read as 'extra divider lines' on adjacent rows ("blue
     lines" per user). The gradient itself is enough to carry the
     fill's visual weight; no halo needed for a 6px-tall bar. */
  transition: width var(--dur-page) ease;
}
/* .pc-round-badge rule retired 2026-06-02 alongside the LONGEST
   chip markup. The chip sat INSIDE the fill at the right edge so
   it always overlapped the gradient end (the longest row's fill is
   100% by definition, leaving no empty bar space to drop the chip
   into), and that overlap competed visually with the time value to
   its right. The .is-longest row-index sage color carries the
   'this row is the longest' signal on its own. */
.pc-round-time {
  font-family: var(--font-display);
  font-size: var(--fs-h3); /* lint-skip CAT-T3: per-round retention duration, role-specific value not a title */
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  letter-spacing: .3px;
  white-space: nowrap;
}

/* ===========================================================
   profile-v2.css
   COMPONENT · PROFILE v2/v3 + SUBSCRIPTION — identity masthead, XP block, stat row, reminders row, achievements rails, subscription page, edit-profile sheet, cleaned settings.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* PROFILE · v2 */

/* Top bar: left-aligned title + right-aligned settings gear,
   no center slot. Overrides the default .top-bar centering. */
/* ── PROFILE v3 — left-aligned identity masthead ──────────────
   No screen title. The header is a horizontal band: avatar on the
   left, name/email/level/edit stacked beside it, settings gear
   pinned top-right. Replaces the v2 centered-hero column. */

/* Profile scroll body. The gutter is bumped from the global 22px to
   24px here for a touch more breathing room against the device edge,
   while still respecting the safe-area inset on notched devices. */
.prof-scroll {
  padding-top: var(--s-1);
}
.prof-scroll-v3 {
  /* safe-top already clears the notch/clock; this is pure breathing
     room above the identity band. Trimmed 40px → 22px to lift the
     avatar + name higher in the band (the band still has air, just
     less). Horizontal padding inherits the canonical .scroll-content
     22px gutter (user direction 2026-06-03 — Home / Library / Settings
     / Profile all sit on the same left edge); the row group below
     now carries its OWN card chrome to address the 'too cramped'
     audit, so the page-edge gutter no longer has to compensate. */
  padding-top: calc(22px + var(--safe-top));
}

.prof-headbar {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  /* 2026-06-02 — bottom padding reduced from var(--s-6) (32px) to
     var(--s-3) (12px) per user feedback ('vertical gap feels
     disconnected from the profile it belongs to'). Gestalt anchor —
     the menu rows are an extension of the user's identity above, so
     they should read as ONE block, not two stacks separated by a
     band of dead space. Headbar's own padding-top kept at --s-1. */
  padding: var(--s-2) 0 var(--s-4h);
}

/* ── PROFILE · identity stat chips (sessions · min · streak) ──────
   A lifetime snapshot under the name. Translucent blue-grey pills; the
   streak pill is amber-tinted (the retention/streak accent). */
.prof-chips {
  display: flex;
  gap: var(--s-2h);
  margin: var(--s-1) 0 var(--s-5);   /* more air around the pills (user: 'more space') */
}
/* Equal-width pills that spread across the row, text centred — matches the
   tune-modal duration presets (5·10·15 min) the user referenced. */
.prof-chip {
  flex: 1 1 0;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-2) var(--s-3);   /* matches the tune-modal 5·10·15 preset pill height */
  border-radius: var(--r-pill);
  background: var(--chip-fill);
  color: var(--text-2);
  font-family: var(--font-body);
  font-size: var(--fs-caption); /* lint-skip CAT-T2: stat-chip pill label, not caption prose */
  font-weight: 600;
  letter-spacing: .2px;
  line-height: 1;
  white-space: nowrap;
}
/* .prof-chip--streak now inherits the neutral .prof-chip look — the streak pill
   is unified with its sibling stat chips ("13 sessions" / "22 min") instead of
   the old amber badge, so the whole profile chip row reads uniform. Kept as a
   (styleless) modifier hook in the markup. (user 2026-08-06) */

/* LG avatar — 84px identity focal point. The clear anchor of the
   identity band. A mint (sage) ring + faint glow gives it presence
   without competing with the breathing-orb language elsewhere. */
.prof-avatar.prof-avatar-lg {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  border: 2px solid var(--sage);
  background: var(--icon-fill);   /* icon-fill navy (was var(--bg-1)) */
  box-shadow: 0 0 18px var(--sage-100);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: var(--fs-h2); /* lint-skip CAT-T3: .prof-avatar-lg sizes the initial glyph inside the avatar circle, not a title */
  font-weight: 500;
  color: var(--sage);
  margin: 0;
  padding: 0;                 /* button reset */
  overflow: hidden;           /* clip the photo to the circle */
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-state) var(--ease-ios);
}
.prof-avatar.prof-avatar-lg:active { transform: scale(.96); }
.prof-avatar.prof-avatar-lg::after { content: none; }
/* Chosen photo fills the circle. */
.prof-avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Identity column — name, optional email, level line, edit pill. */
.prof-id {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
/* .prof-name-v3 composes .hero-title--md in markup for the H2-md
   tier marker; ls / lh override the composed tokens. font-size is
   ALSO overridden here (2026-05-29) — bumped 26px → 22px at the
   user's direction so the identity name reads as a calmer masthead
   against the cyan-ringed avatar. The composed tier class still
   semantically marks this as h2-md; this single-consumer step-down
   doesn't justify adding an --fs-h2-sm token. */
.prof-name.prof-name-v3 {
  font-family: var(--font-display);
  /* H1 tier (user 2026-08-01): unified with the START HERE card title (also
     --fs-h3) so identity + feature-card headline read as ONE headline level.
     Was a lone raw 22px — folding it onto the token both sets the hierarchy and
     retires the off-scale value. */
  font-size: var(--fs-h3);
  font-weight: 600;
  letter-spacing: -.4px;
  color: var(--text);
  margin: 0;
  line-height: 1.12;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.prof-meta {
  font-family: var(--font-body);
  font-weight: 400;
  /* Metadata tier — one muted tone with the row sub-lines (Free plan / No alerts,
     all --text-3 via .sub-text) so all secondary status reads as one level.
     Was --text-2, the lone brighter outlier. */
  color: var(--text-3);
  margin: 1px 0 0;
  letter-spacing: .1px;
  max-width: 100%;
  /* .prof-meta composes .caption in markup for the size. "Member since … · plan". */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Name + inline edit pencil on one row, beside the avatar (user 2026-07-24). */
.prof-name-row {
  display: flex;
  align-items: center;
  gap: var(--s-2h);
  min-width: 0;
}
.prof-name-row .prof-name { flex: 0 1 auto; }   /* shrink + ellipsis on long names */
/* Edit pencil — deliberately muted (this is a stats screen, not an edit-first
   one): moon glyph that recedes, sage on hover. The avatar is the primary,
   larger edit tap-target; this is the secondary affordance beside the name. */
.prof-edit-icon {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px;
  padding: 0; background: transparent; border: none;
  color: var(--moon); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-state) ease, transform var(--dur-tap) ease;
}
.prof-edit-icon svg { width: 17px; height: 17px; }
.prof-edit-icon:hover { color: var(--sage); }
.prof-edit-icon:active { transform: scale(.9); }
/* Settings gear pinned to the top-right of the masthead. The header
   band is center-aligned, so the gear self-aligns to the top to keep
   its conventional top-right corner position.
   ── It now reuses the neutral .icon-btn chrome (40×40 circle, soft
   surface, hairline border, muted --text-2 glyph) so it reads
   IDENTICALLY to the Library page's settings gear. This rule only adds
   the masthead positioning on top of .icon-btn; sizing, fill, stroke
   and glyph dimensions are all inherited so the two screens stay in
   lockstep. */
.prof-settings-gear {
  flex: 0 0 auto;
  /* Filled accent tile — pale-accent disc + a solid (filled) gear glyph, now
     with a sage ring (user 2026-08-06). (Light override in light.css.) */
  background: var(--sage-soft);
  border: 1px solid var(--sage-130);
  color: var(--sage);
  /* Vertically centred on the avatar's midline via the header's align-items:center
     (was align-self:flex-start + margin-top:2px, which pinned it too high — user). */
  /* Bumped 32→40 container, 16→20 svg on 2026-05-30 per user
     direction — gear now reads at a stronger weight next to the
     64px avatar without competing with the rows below. */
  width: 40px;
  height: 40px;
}
.prof-settings-gear svg { width: 20px; height: 20px; }
/* Tap glow — .icon-btn:active only scales; the sage bloom rode :hover, which is
   unreliable on touch. Pin the glow to :active so the filled tile lights up on
   every tap, matching the hero tune gear. (user 2026-08-04) */
.prof-settings-gear { transition: box-shadow var(--dur-tap) var(--ease-ios), transform var(--dur-tap) var(--ease-ios); }
.prof-settings-gear:active { box-shadow: 0 0 12px var(--sage-170); }
/* Streak hero / practice snapshot card REMOVED (2026-05-29) — the
   .prof-statrow / .prof-statrow-card / .prof-streak-hero / -num /
   -lbl / .prof-statval-amber rule cluster powered the day-streak
   block that briefly lived on the Profile tab, then moved to the
   top of the Statistics tab, then was dropped entirely at the
   user's direction. The streak engine + selectors are untouched;
   only this presentation layer is gone. */

/* Manage row (Subscription page) — FLAT (2026-05-29).
   The rounded bordered card chrome (2% white-wash bg, 1px border,
   radius) was stripped to match the flat-row treatment used on
   Profile / Settings / Library / Home. Adjacent rows separated by
   a hairline; hover stays as the 6% white-mist veil. The Reminders
   row name is historical — the class today serves only the
   pgSubscription Change-plan + Cancel rows; the Profile reminder
   entry moved to the shared .settings-row pattern. */
.prof-reminders-row {
  display: flex;
  align-items: center;
  gap: var(--s-3h);
  width: 100%;
  margin: 0;
  /* 2026-06-03 — migrated from var(--s-4h) (18px) to --row-pad-y
     (8px = var(--s-2)) so Profile + Subscription manage rows share
     the same rhythm as Library .tech-item. Same 48x48 icon now
     reads at the same visual weight on both pages. */
  padding: var(--row-pad-y) 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  text-align: left;
  cursor: pointer;
  font-family: var(--font-body);
  transition: background var(--dur-state) ease, transform var(--dur-tap) ease;
  -webkit-tap-highlight-color: transparent;
}
.prof-reminders-row + .prof-reminders-row {
  border-top: 1px solid var(--border);
}
.prof-reminders-row:hover {
  background: var(--white-mist);
}
.prof-reminders-row:active {
  transform: scale(.99);
}
/* (.prof-reminders-icon removed — the Profile rows it served now use
   the shared .settings-row / .settings-row-icon pattern. The remaining
   .prof-reminders-* classes are still used by pgSubscription's rows.) */
.prof-reminders-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
/* .prof-reminders-title composes .row-title in markup; keeps the display
   font-family (profile uses display family for hierarchical anchors). */
.prof-reminders-title { font-family: var(--font-display); }
/* .prof-reminders-sub rule retired 2026-06-03 — both consumers
   ('Change plan' subtitle and 'Cancel subscription' subtitle on
   the Subscription page) were dropped in the same pass that
   removed the sub-text from those rows. No live markup references
   the class. */
/* .prof-reminders-chev (the trailing > on Subscription manage rows)
   removed 2026-05-29 — chevrons were stripped from those rows in
   the flatten pass; the hover veil is the only tap affordance. */

/* Achievements header — eyebrow on the left, earned/total on the right. */
/* Composes .section-head--baseline; keeps margin + line-height. */
.prof-ach-header {
  /* ~32px above the section header (with the row's own padding-bottom) for a
     uniform section rhythm, matching the card→rows break above (user 2026-07-29). */
  margin: var(--s-5) 0 var(--s-2h);
  line-height: 1;
}
.prof-ach-eyebrow {
  margin: 0;
  line-height: 1;
}
/* Achievements screen title suffix — "Achievements · N/24" (2026-08-03).
   A quiet count folded into the topBar title; smaller + muted than the display
   title, with the earned count in the accent. Replaced the removed page-level
   "ALL BADGES · N/24" header. */
.ach-title-count {
  font-family: var(--font-body);
  font-size: var(--fs-body); /* lint-skip CAT-T1: inline title suffix, not a .body-text block */
  font-weight: 400;
  letter-spacing: .1px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.ach-title-count b { color: var(--sage); font-weight: 700; }

/* ── PROFILE · achievements "See all" link ─────────────────────
   Replaces the static count on the right of the eyebrow row. Sage
   text, with the earned/total count as a muted suffix. */
/* Type/color/hover from the shared .see-all-link; this keeps the baseline
   align (for the count) and line-height. */
.prof-ach-seeall {
  align-items: baseline;
  gap: var(--s-1h);
  line-height: 1;
}
.prof-ach-seeall:focus-visible {
  outline: 2px solid var(--sage);
  outline-offset: 3px;
  border-radius: 4px;
}
/* (.prof-ach-seeall-count removed 2026-07-20 — the "See all N/M" count suffix was
   replaced by the "N of M →" label + arrow.) */

/* Trailing "→" arrow on the "N of M" link — replaces the old count suffix. */
.prof-ach-seeall { align-items: center; }
.prof-ach-seeall-arrow { display: inline-flex; }
.prof-ach-seeall-arrow svg { width: 16px; height: 16px; stroke-width: 2; }
/* ── PROFILE · START HERE onboarding card (new users, 0 sessions) ──
   One tap into a 1-min 4-7-8 session that unlocks First Step. Markup-gated
   on _hasSessions, so it disappears once the user has practiced. */
.prof-starter {
  /* Matches the subscription plan card (.pw-plan): a subtle FLAT fill
     (--surface-soft) + a 1.5px --border hairline + --r-2xl radius. No gradient
     (user 2026-07-29). */
  background: var(--surface-soft);
  border: 1.5px solid var(--border);
  border-radius: var(--r-2xl);
  padding: var(--s-4) var(--s-5);   /* 16px v · 22px h — identical to the plan cards (.pw-plan) */
  margin-bottom: var(--s-4);
  display: flex; flex-direction: column;
}
/* Eyebrow tier: muted grey, NOT cyan (user 2026-08-01). Cyan is reserved for
   tappable actions (Begin / UPGRADE / see-all); a non-interactive eyebrow in
   cyan read as tappable. Now matches the ACHIEVEMENTS eyebrow (section-label
   default = --text-3), so both eyebrows are one quiet informational label. */
.prof-starter-eyebrow { color: var(--text-3); margin-bottom: var(--s-2); }    /* .section-label carries type */
/* Title stepped down to --fs-h3 (19px) so it stays ONE line (26px wrapped to
   two, making the card too tall). Not composing .hero-title--md any more. */
.prof-starter-title {
  margin: 0 0 var(--s-1h); color: var(--text);
  /* H1 tier — same --fs-h3 / 600 as the profile name (.prof-name-v3). */
  font-size: var(--fs-h3); font-weight: 600; line-height: var(--lh-h3); letter-spacing: var(--ls-h3);
}
.prof-starter-desc { color: var(--text-2); margin: 0 0 var(--s-3h); }       /* .body-text carries type */
/* Note on the left, Begin pill on the bottom-RIGHT — both sit INSIDE the card's
   symmetric --s-5 (22px) padding, so the Begin pill keeps a clean buffer from the
   card's right border that mirrors the left inset. align-items:center vertically
   centres the pill against the note. (Previously the foot broke out with a
   negative margin to reach the gutter, which pushed the pill into the rounded
   corner and read as overflowing the card — user 2026-08-01.) */
.prof-starter-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3h); }
.prof-starter-btn {
  flex-shrink: 0;
  /* Natural width, sitting against the card's right padding — a clean buffer to
     the border, like the UPGRADE pill in the row below. */
  padding: var(--s-2h) var(--s-4h);
  border: 0; border-radius: var(--r-pill);
  background: var(--sage); color: var(--on-sage);
  font-family: var(--font-body); font-weight: 700;
  font-size: var(--fs-md); /* lint-skip CAT-T4: pill label sizing, not a .lead-text block */
  letter-spacing: .2px; line-height: 1;
  cursor: pointer;
  transition: transform var(--dur-tap) var(--ease-ios);
}
.prof-starter-btn:active { transform: scale(.96); }
.prof-starter-note { color: var(--text-3); }   /* .sub-text carries size */
/* Dark — the helper note beside Begin ("1 min · unlocks your first badge") sat
   borderline on the card; lift to the medium slate (7.37:1) so it clears AA
   while staying secondary to the white title. Light --text-3 already passes. */
:root[data-theme="dark"] .prof-starter-note { color: #94A3B8; /* lint-skip CAT-16: dark-only AA secondary slate, no matching token */ }

/* ── PROFILE · Achievements empty-state · "next badges" shelf ──────
   New-user state (profile reference 2026-07-29): four ghost circles — the
   first three locked achievements + a "+N / to unlock" tail cell. The first
   is the "next up" (dashed cyan ring + cyan label). Replaced the single
   First Step row; the progress bar stays hidden until a badge is earned. */
/* Flush the profile row-icon glyph to the page gutter so it aligns with the
   avatar, card border and badges on ONE global left margin. The shared 48px
   icon wrapper centres the glyph ~12px in; the whole row is the tap target here,
   so shrink the wrapper to the glyph and left-align it. Scoped to the Profile. */
.prof-scroll-v3 .settings-row-icon {
  width: var(--icon-lg);
  justify-content: flex-start;
}

/* NOTE (2026-08-01): we deliberately do NOT indent the profile list actions to
   meet the START HERE card's Begin pill. Begin sits inside the card's --s-5
   padding (card content is expected to be inset); everything OUTSIDE the card —
   the identity stat chips (.prof-chips, flush to the gutter), the UPGRADE badge,
   the "N of 24 ›" see-all link — shares the page gutter as its right axis. The
   card is new-user-only and temporary; the stat chips are the persistent view,
   so the gutter is the one true right rail. Indenting the actions to Begin
   ('Solution A') would have misaligned UPGRADE from the stat chips the moment
   the card disappears. */

.prof-next-shelf {
  display: flex; align-items: flex-start;
  gap: var(--s-2);
  padding: var(--s-2) 0 var(--s-3);
}
/* EQUAL-width cells so the circles are evenly spaced — identical gap between every
   badge (user 2026-07-29). The circle centres on each equal column; labels centre
   under it (may overflow a hair, but the spacing stays uniform). */
.prof-next-cell {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: var(--s-2);
  text-align: center;
  background: none; border: 0; padding: 0;
  text-decoration: none; cursor: default;
}
/* Dashed ring on EVERY badge circle (reference) — uniform muted "not yet
   earned" look, no per-cell cyan highlight. */
.prof-next-ring {
  width: 52px; height: 52px; border-radius: 50%;
  border: 1.5px dashed var(--white-rule);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  color: var(--text-3);
}
/* inline-flex so the glyph centers by its box, not its text baseline — an inline
   span's line-box adds descender space that made the badge icon ride ~1px high. */
.prof-next-ico { display: inline-flex; align-items: center; justify-content: center; }
.prof-next-ico svg { width: 22px; height: 22px; }
/* Earned badge — SOLID cyan ring + cyan glyph (the earned First Step look the
   user liked), vs the dashed muted locked state above. */
.prof-next-ring.is-earned { border-style: solid; border-color: var(--sage); color: var(--sage); }
/* Label wraps (was nowrap) so "Finding Rhythm" breaks to two lines like the reference. */
.prof-next-name { color: var(--text-2); font-weight: 500; line-height: 1.25; }

/* ── SUBSCRIPTION PAGE ─────────────────────────────────────────
   Active-plan hero card, included-perks list, manage rows. Reuses
   the .prof-reminders-row chrome for the manage actions. */
/* Active-plan hero — FLAT (2026-05-29). The radial-gradient
   surface + 1px border + cyan halo box-shadow were stripped; the
   cyan badge + cyan price now carry the visual weight on their
   own against the canvas. */
.sub-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--s-1);
  /* 2026-06-03 — top padding + margin trimmed (s-5/s-2 → s-1/0) so
     the Premium badge + price block sits closer to the 'Subscription'
     title above. Per user audit: the section was bottom-heavy,
     reading as a band of dead space between the title and the
     YOUR ACTIVE BENEFITS eyebrow below. Bottom values kept so the
     gap to the eyebrow stays intact. */
  padding: var(--s-1) 0 var(--s-5);
  margin: 0 0 var(--s-5);
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.sub-hero-badge {
  width: 48px; height: 48px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--sage-soft);
  color: var(--sage);
  margin-bottom: var(--s-1h);
}
.sub-hero-badge svg { width: 22px; height: 22px; stroke: currentColor; stroke-width: 1.6; fill: none; }
.sub-hero-plan {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 700;
  letter-spacing: -.3px;
  color: var(--text);
}
.sub-hero-cycle {
  font-family: var(--font-body);
  color: var(--text-2);
  letter-spacing: .1px;
  /* .sub-hero-cycle composes .caption in markup for the size. */
}
/* .sub-hero-price composes .metric-display + .metric-display--sage
   in markup for the full numeric role with cyan price color.
   Companion keeps only margin-top so the price sits below the
   plan-name + cycle lines above it. */
.sub-hero-price {
  margin-top: var(--s-2);
}
.sub-hero-per {
  font-family: var(--font-body);
  font-weight: 500;
  color: var(--text-3);
  margin-left: 2px;
  /* .sub-hero-per composes .caption in markup for the size. */
}
/* .sub-eyebrow rule removed 2026-05-29 — both consumers ("Your plan
   includes" and "Manage") were dropped from the Subscription page;
   the perks list and manage rows read on their own. */
/* Eyebrow above the perks list — 'YOUR ACTIVE BENEFITS'. Sits flush
   to the gutter with a small breathing gap above (hero-card → label)
   and a tighter one below (label → first perk). The section-label
   token layer handles the typography (11px / 2px tracking / uppercase). */
.sub-perks-eyebrow {
  margin: var(--s-4) 0 var(--s-3);
  color: var(--text-3);
}
.sub-perks {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  margin: 0 0 var(--s-5);
}
.sub-perk {
  display: flex;
  /* flex-start (was center) so the check stays anchored to the FIRST line when a
     perk wraps to two lines (e.g. "Progressive difficulty & custom sessions") —
     prevents the centred-check "staircase" look. The 22px check ≈ one line box,
     so single-line perks still read centred. 2026-06-20. */
  align-items: flex-start;
  gap: var(--s-3);
}
.sub-perk-check {
  flex: 0 0 auto;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--sage-soft);
  color: var(--sage);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.sub-perk-check svg { width: 13px; height: 13px; stroke: currentColor; stroke-width: 2; fill: none; }
/* "Coming soon" perk — neutralise the check (was sage = active/included) so it
   reads as upcoming, not an active benefit; the SOON badge carries the meaning. */
.sub-perk--soon .sub-perk-check { background: var(--surface-soft); color: var(--text-3); }
.sub-perk--soon .sub-perk-text  { color: var(--text-2); }
/* .sub-perk-text composes .body-text in markup for the size + family. */
.sub-perk-text {
  color: var(--text);
  letter-spacing: .1px;
}
/* Manage rows have no leading icon tile — just title/sub + chevron. */
.sub-manage-row { gap: 0; }
.sub-cancel-title { color: var(--rose); }

/* Manage-actions group (Subscription page). Iteration 2 (2026-06-02):
   the previous pass wrapped the rows in a .flat-card-style box with
   border + rounded corners + fill. The user pointed at the Profile
   page as the reference: those rows are FLAT on the page canvas,
   grouped only by a single hairline between each row — no border,
   no fill, no rounded corners. Matching that grammar here:
     • container has NO chrome of its own (transparent)
     • each row carries its own padding and the only visible glue
       is the hairline between adjacent rows.
   Result: hero card sits on its own surface above, the manage rows
   read as a quiet grouped strip below — same rhythm as Profile's
   Subscription / Reminders / Statistics / Community stack. */
.sub-manage-card {
  margin: var(--s-3) 0 var(--s-4);
}
.sub-manage-card .prof-reminders-row {
  padding: var(--s-4) 0;
}
.sub-manage-card .prof-reminders-row + .prof-reminders-row {
  border-top: 1px solid var(--border);
}

/* ── PAYWALL — subscription upgrade offer (pgSubscription while billing off) ──
   Rebuilt ANIMA-native from the "Subscription & Auth" design comp. Reuses the
   .sub-hero-badge grammar, the .sub-perk benefit list, and the .btn-primary CTA;
   the .pw-* rules below add the trial eyebrow, glowing hero ring, tappable plan
   cards (with a selected state), and the footer note + legal links. Type sizes use
   only role-safe tokens (--fs-h2/h3/mini); caption/body/sub roles are composed in
   markup so the type-lint stays green. */
/* (The older .pw-* subscription-paywall styles were removed 2026-08-13 —
   the live paywall now uses the shared .pv-* preview design. .pw-link kept.) */
.pw-link {
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-body); color: var(--sage);
  padding: var(--s-1) 0;
}

/* ── EDIT-PROFILE SHEET ────────────────────────────────────────
   Lightweight bottom sheet. Mounts inside .tech-modal-overlay /
   .tech-modal-sheet so it inherits the slide-up animation, backdrop,
   and swipe-down dismissal with no extra chrome CSS. */
.edit-prof-body {
  padding: var(--s-1) var(--s-5) calc(22px + var(--safe-bottom));
}
.edit-prof-title {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 700;
  letter-spacing: -.3px;
  color: var(--text);
  margin: var(--s-1) 0 var(--s-4h);
}
/* ── Edit-profile · photo row (preview + change/remove) ─────────────*/
.edit-prof-avatar-row {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-bottom: var(--s-5);
}
.edit-prof-avatar {
  flex: 0 0 auto;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  border: 2px solid var(--sage);
  background: var(--icon-fill);
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.edit-prof-avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.edit-prof-avatar-initial {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 500;
  color: var(--sage);
}
.edit-prof-avatar-actions { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-2); }
.edit-prof-photo-btn {
  display: inline-block;
  background: var(--sage-soft);
  border: 1px solid var(--sage-270);
  color: var(--sage);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: .3px;
  padding: var(--s-2) var(--s-3h);
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: background var(--dur-state) ease;
}
.edit-prof-photo-btn:hover { background: var(--sage-90); }
.edit-prof-photo-remove {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1h);
  background: transparent;
  border: 0;
  color: var(--rose);
  opacity: .82;
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: .3px;
  /* Left-flush with the 'Change photo' pill above: the icon box starts exactly
     at the column origin (no left padding, no negative margin), so both controls
     share one left edge. Right + vertical padding keep a comfortable tap area. */
  padding: var(--s-1h) var(--s-2) var(--s-1h) 0;
  margin: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background var(--dur-state) ease, opacity var(--dur-state) ease;
}
.edit-prof-photo-remove-ico { display: inline-flex; }
.edit-prof-photo-remove-ico svg { width: 1.05em; height: 1.05em; }
.edit-prof-photo-remove:hover  { opacity: 1; background: var(--rose-50); }
.edit-prof-photo-remove:active { opacity: 1; background: var(--rose-100); }
.edit-prof-file { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* ── Interactive avatar crop overlay (above the edit sheet) ────────
   Picked photo fills a square stage with a circular guide (corners dimmed);
   drag to pan, slider to zoom, then bake a 256² JPEG on "Use photo". */
.avatar-crop-overlay {
  position: absolute; inset: 0; z-index: var(--z-top);
  display: flex; align-items: center; justify-content: center;
  padding: var(--s-4);
}
.avatar-crop-card {
  width: 100%; max-width: 320px;
  background: var(--card);
  border: 1px solid var(--white-line);
  border-radius: var(--r-2xl);
  padding: var(--s-5);
  display: flex; flex-direction: column; align-items: center; gap: var(--s-3);
}
.avatar-crop-title { margin: 0; }                 /* .sheet-title carries type */
.avatar-crop-hint { color: var(--text-3); margin: 0; text-align: center; }  /* .sub-text carries size */
.avatar-crop-stage {
  position: relative;
  width: 260px; max-width: 100%; aspect-ratio: 1;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--card-1);
  touch-action: none;
  cursor: grab;
}
.avatar-crop-stage:active { cursor: grabbing; }
.avatar-crop-img {
  position: absolute; top: 0; left: 0;
  user-select: none; -webkit-user-drag: none;
  will-change: transform;
}
/* Circular guide — dims the stage corners OUTSIDE the crop circle. */
.avatar-crop-ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  box-shadow: 0 0 0 999px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}
/* Buttons use the shared .dialog-btn pair (Cancel + Save) — same as the edit
   sheet this overlays. This only zeroes .dialog-actions' margin-top since the
   card's own flex gap already spaces the row. */
.avatar-crop-actions { margin-top: 0; width: 100%; }

.edit-prof-field {
  display: block;
  margin-bottom: var(--s-4);
}
/* Type from .section-label.section-label--lg; keeps only block + its margin. */
.edit-prof-label {
  display: block;
  margin-bottom: var(--s-2);
}
/* Edit-profile inputs now use the shared .field-input (03-primitives). */
.edit-prof-error {
  font-family: var(--font-body);
  color: var(--rose);
  margin: calc(-1 * var(--s-1h)) 2px var(--s-3h);
  letter-spacing: .1px;
  /* .edit-prof-error composes .caption in markup for the size. */
}
/* Edit-profile actions now use the shared .dialog-actions / .dialog-btn pair
   (see modals.css) — Cancel = .dialog-btn-cancel, Save = .dialog-btn-save. */

/* SETTINGS · cleaned hierarchy */

/* Google Sans is now the app-wide body face via --font-body
   (00-settings.css), so the page-level family override here is
   gone. Only the top padding is unique to settings. */
.settings-scroll {
  padding-top: var(--s-2);
  /* Horizontal padding inherits the canonical .scroll-content 22px
     gutter — same as Library, Home, Profile. User direction
     2026-06-03 ('library looks fabulous, clean, no clutter — let's
     standardize the padding'): the same value everywhere, no per-
     page overrides. Earlier 26px experiment reverted. */
}

/* Section eyebrow — matches the LABEL token from the design-system
   type scale (11px, +1.8px tracking, uppercase). Pads the section's
   first line up from the card edge by a consistent amount. */
/* Settings section eyebrow (PRACTICE / LOCALIZATION / LEGAL &
   SUPPORT). Hair larger and a touch tighter than the base
   .section-label--lg so semi-bold reads as semi-bold — the tiny
   11px / 2px-tracked default visually washed thinner than its
   600 weight. Bumped to 12px / 1.5px tracking; weight stays 600
   from the base rule. */
/* .settings-eyebrow rules removed 2026-05-29 — the PRACTICE /
   LOCALIZATION / LEGAL & SUPPORT section eyebrows were dropped
   from the Settings page; the row groups read as one continuous
   flat list. */

/* Chevron glyph on a settings row trail slot — token-driven size,
   tertiary color. Replaces the inline width/height/color triplet. */
.settings-row-chev {
  display: inline-flex;
  width: 16px;
  height: 16px;
  color: var(--text-3);
  flex-shrink: 0;
}
.settings-row-chev svg {
  width: 100%;
  height: 100%;
}

/* The .settings-row-trail-value + .settings-row-trail-text pair lived
   on the Language row's old "value text + chevron" trailing slot.
   The flat treatment moved that value to an inline .row-badge next
   to the title, so both rules are gone. .settings-row-chev stays
   alive — the legacy pgCustom page still emits it. */

/* Language picker option — wrap the label+native pair in a tidy
   text block, with the check icon as a fixed-size trail glyph. */
.lang-option-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
.lang-option-check {
  display: inline-flex;
  width: 16px;
  height: 16px;
  color: var(--sage);
  flex-shrink: 0;
}
.lang-option-check svg {
  width: 100%;
  height: 100%;
}

/* App version stamp at the bottom of the Settings screen. */
.settings-version {
  text-align: center;
  font-family: var(--font-body);
  font-size: var(--fs-mini);
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--text-3);
  padding: var(--s-4h) 0 var(--s-6);
  opacity: .6;
}

/* "You" post variant — the same .post-card chrome with a sage-tinted
   border so the author can spot their own posts in the feed without
   needing an explicit pill (the small "you" tag handles that). */
.post-card.post-card-me {
  border-color: var(--border-warm);
}

/* ═══════════════════════════════════════════════════════════════
   FLAT ROW LIST — chrome-less list container
   ═══════════════════════════════════════════════════════════════
   The flat-design treatment for Profile and Settings groups:
   rows extend edge-to-edge across the screen with NO card surface
   (no background fill, no border, no radius), only a hairline
   divider between rows. The chevron is also dropped — affordance
   is communicated by typography weight + the row's hover veil.
   Subtitles compress into inline pills (.row-badge) sitting next
   to the title, freeing the second line entirely.

   2026-06-02 — dropped the negative gutter margin. The rows used to
   bleed dividers + hover-veil edge-to-edge; per user feedback
   ("add padding to those pages, same as at home screen and library"
   on Profile + Settings) the rows now respect the screen gutter.
   Dividers stop at the gutter on both sides, matching the inset
   feel of Home + Library cards.
   ═══════════════════════════════════════════════════════════════ */
.row-flat-list {
  margin: 0;
}
/* Generous vertical rhythm — typography + breathing room do the
   visual work the card chrome used to. 18px/64px matches the
   Spotify account-drawer reference so adjacent rows feel like
   distinct decisions, not packed strips.
   Horizontal padding zeroed out (was var(--gutter) when the
   container had a negative gutter margin to cancel; now the
   container respects the page gutter directly via .scroll-content). */
.row-flat-list .settings-row {
  padding-left: 0;
  padding-right: 0;
  /* 2026-06-03 — vertical padding moved to --row-pad-y, joining
     Library .tech-item + Profile .prof-reminders-row on one rhythm.
     min-height kept at 64px so single-line rows still hit the
     Spotify-style touch target the Settings page was tuned for. */
  padding-top: var(--row-pad-y);
  padding-bottom: var(--row-pad-y);
  min-height: 64px;
}
.row-flat-list .settings-row + .settings-row {
  border-top: 1px solid var(--white-mist);
}
/* 2026-06-03 — divider across .row-flat-list group boundaries too.
   The Settings page splits PRACTICE / LOCALIZATION / LEGAL & SUPPORT
   into three separate .row-flat-list containers; the rule above
   only fires between sibling .settings-row elements inside one
   container, so the Push notifications → Language and Language →
   Privacy policy transitions read as a band of empty space while
   Privacy policy → Terms of service had the expected hairline.
   The user audit ('check dividers between row at the setting page')
   called out that inconsistency. The selector below catches the
   first row of every NON-first .row-flat-list sibling so dividers
   read continuously across the page. */
.row-flat-list + .row-flat-list > .settings-row:first-child {
  border-top: 1px solid var(--white-mist);
}

/* ═══════════════════════════════════════════════════════════════
   SETTINGS-PAGE OVERRIDES (2026-06-03)
   ────────────────────────────────────────────────────────────────
   The user audit ('make the rows height in the setting less')
   asked for tighter row padding specifically on Settings — Library
   and Profile keep the 18px --row-pad-y rhythm they liked yesterday,
   Settings shrinks. Scoped via .settings-scroll (the page-wrapper
   class on pgSettings) so the same .row-flat-list .settings-row
   rule on Profile is unaffected.
   ═══════════════════════════════════════════════════════════════ */
.settings-scroll .row-flat-list .settings-row {
  /* var(--row-pad-y) (18px) → var(--s-2h) (10px). Row height drops
     from ~84px to ~68px max; min-height floor reduced from 64 → 56
     so toggle-only rows (Push notifications, Language) sit tighter
     too. The 48px icon still fits with ~4px breathing room top +
     bottom. Token-driven so any future bump to --s-2h cascades. */
  padding-top: var(--s-2h);
  padding-bottom: var(--s-2h);
  min-height: 56px;
}
/* Group eyebrow above each .row-flat-list — section-label--lg is
   the canonical 11px / 2px-tracking eyebrow used app-wide. Tight
   bottom margin so the first row visually anchors to its label;
   generous top margin between groups so the cluster reads as a
   distinct section. */
.settings-scroll .settings-group-label {
  margin: var(--s-5) 0 var(--s-2);
  color: var(--text-3);
}
/* First label on the page hugs the top a little tighter — there's
   already breathing room under the topBar; no need to add s-5 on
   top of that. */
.settings-scroll > .settings-group-label:first-of-type {
  margin-top: var(--s-3);
}
/* Row-title weight is now Regular (400) at the base .row-title rule
   in 03-primitives.css, applied app-wide. The earlier .row-flat-list
   font-weight:500 override is gone — the spec changed to Google Sans
   400/16px across all row titles. Site-specific bolder weights still
   win where they exist (.home-row-title 700, .modal-row-title 600). */

/* ═══════════════════════════════════════════════════════════════
   ROW BADGE — inline status pill beside a row title
   ═══════════════════════════════════════════════════════════════
   Replaces the gray sub-line for short categorical labels (plan,
   count, language). Two variants on the same base shape:

     .row-badge            soft sage chip — neutral status, used
                           for counts ("2 active") and values
                           ("English"). Reads as ambient info.
     .row-badge--solid     SOLID sage fill, dark text, optional
                           leading icon — high-contrast plan/status
                           emphasis (the Premium pill). One per
                           screen at most.
     .row-badge--warn      rose tint — Safety guidelines warning.
     .row-badge--muted     low-contrast off/inactive state.
   ═══════════════════════════════════════════════════════════════ */
.row-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  /* margin-left: auto pushes the pill to the FAR RIGHT of the
     settings-row (which is a flex row). Pairs with the row-helper
     markup change 2026-05-31 that pulled the badge out of the title
     line into a row-level sibling — the pill now anchors to the row's
     right edge and vertical-centers against the FULL row, not just
     the title's first line. */
  margin-left: auto;
  flex-shrink: 0;
  /* Vertical padding bumped 2 → 4px (--s-1) alongside the size jump so
     the pill grows in proportion to the type. */
  padding: var(--s-1) var(--s-2h);
  border-radius: var(--r-pill);
  background: var(--sage-soft);
  color: var(--sage);
  font-family: var(--font-body);
  /* Bumped --fs-micro (9px) → --fs-caption (12px) on 2026-05-31 per
     user direction (~"4px bigger"; lands on the next real tier in the
     scale). Affects every .row-badge in the app — the PREMIUM pill on
     the Profile Subscription row, the ENGLISH pill on the Settings
     Language row, the IMPORTANT pill on the Safety guidelines row. */
  font-size: var(--fs-caption); /* lint-skip CAT-T2: row-badge is an uppercase tracked pill — composing .caption would inherit lh 1.45 and grow the pill height */
  font-weight: 700;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  white-space: nowrap;
}
.row-badge svg {
  width: 11px;
  height: 11px;
  stroke-width: 1.8;
}
/* (.row-badge--solid was the solid-sage Premium pill with a leading
   sparkle icon. Dropped — the soft-sage default reads quieter and
   matches the Language badge across the app. Re-add here if a more
   emphatic plan-status pill is needed later.) */
.row-badge--warn {
  background: var(--rose-100);
  color: var(--rose);
}
.row-badge--muted {
  background: var(--white-mist);
  color: var(--text-3);
}
/* When the title hosts a badge, switch to inline-flex so the pill
   centers on the title's optical center regardless of pill height
   (the .row-badge--solid variant is taller than the default).
   Replaces the brittle vertical-align nudge that broke as soon as
   the pill grew or the title font changed. */
.row-flat-list .row-title {
  display: inline-flex;
  align-items: center;
}

/* .prof-segmented rule removed (2026-05-29) — the PROFILE | STATISTICS
   | COMMUNITY segmented control on the Profile shell was retired.
   Statistics + Community now live as rows in the account list and
   navigate to their own pages on tap. */

/* Inline variant of .comm-tabs-wrap — used when the Community
   sub-tabs live INSIDE the Profile shell's scroll body (under the
   main segmented control) instead of as a fixed top band on the
   standalone Community page. Drops the fixed-band gutter, lays out
   the section-tabs + trailing compose icon as a single flex row, so
   the New Post affordance survives the move out of the old top-bar. */
/* .comm-tabs-wrap--inline + .comm-compose-btn retired 2026-06-01:
   the top-right [edit] compose button moved to an inline "Share your
   practice…" prompt card under the filter pills (see .comm-compose-inline
   in community.css). The tabs row no longer needs a flex wrapper to
   accommodate a sibling button — it's back to a plain .section-tabs strip. */

/* BREATH HOLD · ambient amber wash */

/* The breath-hold screen carries a faint amber radial wash behind */
.pg-hold-screen {
  background:
    radial-gradient(ellipse at 50% 38%, var(--amber-50) 0%, transparent 65%),
    var(--bg);
}

/* ===========================================================
   reminders.css
   COMPONENT · REMINDERS + CALENDAR — time-wheel typography, device hardware panel, history list, composer, day picker, date/repeat sheets, history calendar grid.
   ── Part of the Prana modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   =========================================================== */
/* Reminders is a full PAGE now (2026-07-20), not a bottom sheet — but it
   reuses .tech-modal-sheet for the slide/backdrop machinery, so it also
   inherited that sheet's decorative TOP edge: rounded top corners, a
   --white-veil top border, and a sage-290 gradient ::before hairline.
   On a full page those paint a faint sage/blue line right under the
   status bar (user-reported "thin blue line at the very top"). Flatten
   the top edge so the page reads as a page. Loads after modals.css in
   the build ORDER, so it wins on equal specificity. */
.reminders-page {
  border-top: none;
  border-radius: 0;
}
.reminders-page::before { content: none; }

/* REMINDERS · time-wheel typography pass */
/* GOLDEN-RULE NOTE: The wheel mechanics (.reminder-time-wheel-wrap,
   .reminder-wheel-col, .reminder-wheel-item, scroll-snap math) are
   NOT touched. The selectors below ONLY override color tokens,
   track opacity, and typography style — the geometric layout, scroll
   snap, item heights, and padding rows remain exactly as defined
   earlier in this file (lines ~3027 and ~6716). */

/* Unselected wheel items read as tertiary so the selected center
   row pops as the live value. */
.reminder-wheel-item {
  color: var(--text-3);
  font-family: var(--font-display);
  font-feature-settings: 'tnum' 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.5px;
  transition: color var(--dur-state) ease, opacity var(--dur-state) ease;
}

/* Selected center row — ivory at full opacity, slightly heavier
   weight. The size remains whatever the existing wheel rules set,
   so the scroll-snap math stays correct. */
.reminder-wheel-item.is-selected {
  color: var(--text);
  font-weight: 600;
  opacity: 1;
}

/* Top/bottom pad rows are invisible spacers — no glyph rendering. */
.reminder-wheel-item.pad {
  color: transparent;
  opacity: 0;
}

/* The wheel column's fade mask. We don't replace the mask
   geometry — only nudge the inner track to a slightly cooler
   navy so it reads as a calm pill on the deep card. */
.reminder-wheel-col {
  scrollbar-width: none;
  background: transparent;
}

/* AM/PM label and colon separator pick up the sage accent for the
   meridiem (per the New-Reminder screenshot) while the colon stays
   neutral tertiary so it doesn't compete with the digits. */
.reminder-wheel-sep {
  color: var(--text-3);
  font-family: var(--font-display);
  position: relative;   /* sit above the selection band (::before, z-index 0) */
  z-index: 3;
}

/* Wheel sits in a padded block so it never butts up against the */
.rem-wheel-block {
  /* 2026-08-11 — the wheel now sits in a dedicated container: top rhythm off
     the header + a subtle raised surface with soft corners, so it reads as an
     'interaction zone' instead of floating in the dark canvas. */
  margin: var(--s-6) var(--s-5) var(--s-4);
  padding: var(--s-2h) var(--s-3);
  background: var(--surface-soft);
  border-radius: var(--r-xl);
}

/* Device hardware panel — ONE unified card with TWO cells (Push + */
.rem-device-card {
  margin: 0 var(--s-5) var(--s-4);
  /* 2026-05 — the card is now visually transparent: it acts purely */
  background: transparent;
  border: none;
  border-radius: 0;
  overflow: hidden;
}
.rem-device-cell {
  /* Row metric (flex, 14px gap, 14/16 padding, 56px min-height) comes from
     the shared .sheet-row alongside this class in the markup. */
  background: transparent;
}
/* Single hairline separator between consecutive cells — same weight
   as design-system §5.07 dividers. */
.rem-device-cell + .rem-device-cell {
  border-top: 1px solid var(--border);
}

/* SOON-disabled cell (Haptic alert) — dim the icon, sub-line, and the inert
   toggle so the row reads as unavailable; the label + SOON badge stay readable
   (same treatment as the session sheet's SOON rows). 2026-06-24. */
.rem-device-cell.is-disabled .reminder-opt-icon,
.rem-device-cell.is-disabled .reminder-opt-sub {
  opacity: .5;
}
.rem-device-cell.is-disabled .toggle {
  opacity: .45;
}

/* History eyebrow — quiet section label above the active reminders */
/* Type from .section-label (base 10px tier); keeps only its padding. */
.rem-history-eyebrow { padding: var(--s-1) var(--s-5) var(--s-2); }

/* History list — fully flat. The earlier "soft card" surface is
   stripped (no fill, no border, no radius) to match the app-wide
   flat treatment. Rows still divide via .reminder-row-wrap +
   .reminder-row-wrap hairlines. */
.reminders-active-list-soft {
  background: transparent;
  border: 0;
  border-radius: 0;
  margin: 0 var(--s-5);
}
/* Individual rows inside the soft list: keep them transparent so the
   panel's near-transparent fill shows through. Divider hairline is
   already painted by .reminder-row-wrap + .reminder-row-wrap rules. */
.reminders-active-list-soft .reminder-row {
  background: transparent;
}
/* The reminder row carries .state-target — its hover fill is owned by the
   veil (--state-hover: 0.06, white) per the modals.css state-layer rules.
   Keep this transparent so the veil isn't doubled by an explicit bg. */
.reminders-active-list-soft .reminder-row:hover {
  background: transparent;
}
/* The row body now leads with the time itself (the reminder name
   field is gone in the simplified flow). Larger leading time + meta
   on a single line. */
.reminder-meta-lead {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  letter-spacing: .1px;
  /* .reminder-meta-lead composes .caption in markup for the size. */
}
.reminder-time-lead {
  font-family: var(--font-display);
  font-size: var(--fs-md); /* lint-skip CAT-T4: .reminder-time-lead — sizes the time-picker numeric lead, not body title */
  font-weight: 600;
  color: var(--text);
  letter-spacing: -.2px;
  font-variant-numeric: tabular-nums;
}
.reminder-meta-lead .dot-sep {
  opacity: .45;
}
.reminder-meta-lead .countdown {
  color: var(--sage);
  font-weight: 500;
}

/* ───────────────────────────────────────────────────────────────
   REMINDERS · DEAD-CODE FLAGS (refactor 2026-05)
   ───────────────────────────────────────────────────────────────
   The following selectors are no longer referenced by any JS or HTML
   after the unified-view refactor. They are left in place for one
   release so any third-party theme override that targeted them keeps
   compiling cleanly; the next CSS dead-code sweep should verify
   zero-reference status (grep across pages.js, ui.js, reminders.js)
   and then remove them.
     • .reminders-fixed-footer     (was the Set Reminder button bar)
     • .btn-set-reminder           (the Set Reminder button itself)
     • .reminders-list-collapsed   (list↔editor toggle wrapper)
     • .reminders-editor-panel     (editor panel wrapper)
     • .reminder-edit-header,
       .reminder-edit-header-titles,
       .reminder-edit-title,
       .reminder-edit-subtitle,
       .reminder-edit-cancel
     • .reminder-edit-chip-on, .reminder-edit-chip-off

   Newly flagged in the 2026-05-20 always-editing refactor:
     • .reminder-row-delete-pad   (was the swipe-revealed Delete pad;
       left-swipe now commits a delete instantly, no pad rendered)
     • .reminder-row-delete-pad .stack, .reminder-row-delete-pad .lbl
     • .reminder-row.swipe-open   (was the persistent open state for
       the now-deleted Delete pad)
     • .rem-cancel-iconbtn        (was the header X in CREATE mode;
       the header right slot is the bell-master now)
     • .rem-days-block, .rem-day-row, .rem-day-chip, .rem-day-presets,
       .rem-day-preset              (was the chip + preset row;
       the Repeat row + bottom-sheet replaces this UI)
     • .rem-section-eyebrow       (was the "Repeat" eyebrow above
       the chip strip; the Repeat row carries its own label now)
     • .reminder-edit-section, .reminder-edit-label
     • .reminder-days, .reminder-day
     • .reminder-day-presets, .reminder-day-preset
     • .reminder-edit-actions, .reminder-edit-actions .btn-delete
     • .reminder-opt-row           (replaced by .rem-device-cell)
     • .reminder-icon-opt          (icon picker gone)
     • .reminder-name              (per-row name field gone)
   ─────────────────────────────────────────────────────────────── */

/* REDUCED MOTION — keep the new screens accessible */
@media (prefers-reduced-motion: reduce) {
  .pc-pb-pill,
  .prof-avatar-lg,
  .prof-edit-icon,
  .prof-reminders-row,
  .dialog-btn {
    transition: none;
    animation: none;
  }
  .pc-hero-icon {
    animation: none;
    box-shadow: 0 0 28px var(--sage-glow);
  }
}

/* RESPONSIVE TIGHTENING · ≤380px phones */
@media (max-width: 380px) {
  .prof-avatar.prof-avatar-lg {
    width: 66px;
    height: 66px;
    font-size: var(--fs-h2-lg); /* lint-skip CAT-T3: .prof-avatar-lg sizes the initial glyph inside the avatar circle, not a title */
  }
  .prof-name.prof-name-v3 {
    font-size: var(--fs-h2);
  }
  .pc-card.pc-metrics-lean .stat-val,
  .pc-card.pc-metrics-lean .stat-val-num {
    font-size: var(--fs-h2);
  }
  .pc-pb-pill-text {
    font-size: var(--fs-caption); /* lint-skip CAT-T2: @media downshift override — body-text class at full width, caption at narrow */
  }
}

/* Sub-nav · plain text links */
.compete-sub-tabs {
  display: flex;
  gap: var(--s-5);
  padding: 0 2px;
  margin: var(--s-1) 0 var(--s-4h);
}
/* Underline-tab skeleton (transparent chrome, hover, sage indicator at
   bottom:0) from .tab-underline; below are only the bits unique to the sub-nav. */
.compete-sub-tab {
  padding: var(--s-1h) 0 var(--s-2h);
  letter-spacing: .2px;
}
.compete-sub-tab.active { color: var(--sage); }

/* ── Leaderboard collective wrapper ──────────────────────────────
   Inherits the soft-surface base from .stats-soft-card (near-
   transparent fill + hairline border + canonical radius). Side
   padding is 14px per spec; vertical padding is 0 because each
   .lb-item provides its own breathing room and the hairline
   divider needs to sit flush against the wrapper edge. Backdrop
   chrome is explicitly neutralised so no leftover glass blur or
   shadow from earlier overrides bleeds through. */
.stats-soft-card.lb-stack {
  padding: 0 var(--s-3h);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
  overflow: hidden; /* clip the .me row tint to the wrapper radius */
}

/* Internal row · seamless horizontal flex */
.lb-item {
  display: flex;
  align-items: center;
  gap: var(--s-3h);
  padding: var(--s-3) var(--s-1);
  border-bottom: 1px solid var(--white-mist);
}
.lb-item:last-child { border-bottom: none; }

/* "You" row · soft ambient backdrop tint */
.lb-item.me {
  background: var(--sage-tint);
  margin: 0 calc(-1 * var(--s-3h));
  padding-left: var(--s-4h);
  padding-right: var(--s-4h);
}

/* Rank numeral */
/* .lb-rank composes .body-text in markup for the size. */
.lb-item .lb-rank {
  flex: 0 0 24px;
  font-weight: 600;
  color: var(--text-3);
  text-align: center;
}

/* Avatar · unified sage gradient, identical across rows */
.lb-avatar {
  width: 36px;
  height: 36px;
  font-size: var(--fs-body); /* lint-skip CAT-T1: sizes the initial glyph inside the leaderboard avatar circle, not body text */
  flex-shrink: 0;
}

/* Identity block · name + streak sub-line */
.lb-id { flex: 1; min-width: 0; }
/* .lb-name composes .body-text in markup for the size. line-height: 1.25
   overrides .body-text's 1.5 (leaderboard rows are denser). */
.lb-name {
  font-weight: 500;
  color: var(--text);
  line-height: 1.25;
}
.lb-item.me .lb-name { font-weight: 700; }
.lb-you {
  color: var(--sage);
  font-size: var(--fs-label);
  font-weight: 500;
  margin-left: 2px;
}
.lb-streak {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  margin-top: 2px;
  font-size: var(--fs-label);
  color: var(--amber); /* AMBER token — flame + "X day streak" */
}
.lb-streak-flame {
  display: inline-flex;
  width: 11px;
  height: 11px;
  color: var(--amber); /* flame icon explicitly AMBER, even on .me row */
}

/* XP value · high-contrast ivory, ALWAYS */
.lb-xp {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 500;
  color: var(--text);
  letter-spacing: -.3px;
  flex-shrink: 0;
}
.lb-item.me .lb-xp { color: var(--text); } /* explicit override · no sage */
.lb-xp-unit {
  font-family: var(--font-body);
  font-size: var(--fs-mini);
  font-weight: 600;
  letter-spacing: 1px;
  color: var(--text-3);
  margin-left: var(--s-1);
}

/* The composer wrapper. Holds the .is-creating class when in
   creation mode so descendant selectors can branch their look
   without splitting markup across two trees. */
.rem-composer {
  position: relative;
  padding-bottom: var(--s-1);
}

/* Quiet section label above grouped controls (Repeat, etc.).
   Same eyebrow treatment as .rem-history-eyebrow — uppercase,
   tertiary text, wide tracking. */

/* Day picker block */

/* Round day chip · design-system §5.12 · 36px diameter ·
   11px/600 type. Off state is a deep card ring with dim text;
   on state inverts to sage fill + navy text with sage glow. */

/* Preset pill row · "Every day · Weekdays · Weekends · Once" */

/* ── Date row (tap to open calendar) ────────────────────────────
   Sits inside .rem-device-card alongside Push/Haptic cells. The
   trailing chevron signals the row opens a sub-sheet. */
.rem-device-cell-tap {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur-state) var(--ease-ios),
              transform var(--dur-tap) var(--ease-ios);
}
/* Match the canonical tappable-row hover (.tech-row): a visible surface
   wash plus a small chevron nudge so the row clearly reads as live. */
.rem-device-cell-tap:hover { background: var(--white-mist); }
.rem-device-cell-tap:hover .rem-chev { color: var(--text-2); transform: translateX(2px); }
.rem-device-cell-tap:active { transform: scale(.99); }
.rem-device-cell-trail {
  display: flex;
  align-items: center;
  gap: var(--s-1h);
  flex-shrink: 0;
  color: var(--text-3);
}
.rem-chev {
  display: inline-flex;
  width: 16px; height: 16px;
  color: var(--text-3);
  flex-shrink: 0;
  transition: color var(--dur-state) var(--ease-ios),
              transform var(--dur-tap) var(--ease-ios);
}
.rem-chev svg { width: 16px; height: 16px; }

/* ── Reminder-sheet toggle shape ────────────────────────────────
   The reminder sheet uses the shared .toggle primitive (44×26 with an
   off-state border + on-state glow). Scoped to the composer, we adopt
   the cleaner modal (.ps-tog) proportions instead — 40×24 pill, 18px
   knob, no off-border, no on-glow — so these read identically to the
   tune/sound sheet toggles. The global .toggle primitive is untouched. */
.rem-composer .toggle { width: 40px; height: 24px; }
.rem-composer .toggle.off { border: none; background: var(--white-line); }
.rem-composer .toggle.on  { box-shadow: none; }
.rem-composer .toggle::after { width: 18px; height: 18px; top: 3px; }
.rem-composer .toggle.on::after  { left: 19px; }
.rem-composer .toggle.off::after { left: 3px; }

.rem-date-clear {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--white-mist);
  border: none;
  color: var(--text-2);
  font-size: var(--fs-body); /* lint-skip CAT-T1: clear-date button (.rem-date-clear) — small button, lh:1.5 would resize the round 22px hit area */
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  transition: background var(--dur-state) var(--ease-ios), color var(--dur-state) var(--ease-ios);
}
.rem-date-clear:hover {
  background: rgba(224,138,138,.18);
  color: var(--rose);
}

/* ── Cancel button (header right slot in CREATING mode) ─────────
   Same circular footprint as the "+" button but with a neutral
   tint — this is a dismissal, not an action. */

/* Active-edit highlight */
.reminders-active-list-soft .reminder-row-wrap.is-active-edit .reminder-row {
  background: var(--sage-veil);
}
.reminders-active-list-soft .reminder-row-wrap.is-active-edit::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--sage);
  box-shadow: 0 0 8px var(--sage-glow);
  z-index: 2;
}

/* Dim countdown text on disabled reminders so the row's "off"
   state reads at a glance — per directive 4 of the brief. */
.reminder-row.disabled .reminder-meta-lead,
.reminder-row.disabled .countdown,
.reminder-row.disabled .reminder-time-lead {
  color: var(--text-3);
}

/* History list parent · overflow:hidden so swipe-revealed delete */
.reminders-active-list-soft {
  overflow: hidden;
}

/* Sticky "Save Reminder" CTA */
.rem-cta-spacer {
  /* Reserved vertical space so the sticky CTA never overlaps the
     last list row when the composer is short. 18px when idle,
     86px when creating (covers the CTA's height + safe-area). */
  height: 18px;
  transition: height var(--dur-state) var(--ease-ios);
}
.rem-composer.is-creating + .rem-cta-bar .rem-cta-btn,
.rem-composer.is-creating ~ .rem-cta-bar .rem-cta-btn,
.rem-composer.is-creating .rem-cta-spacer { /* expand spacer when creating */ }
.rem-composer.is-creating .rem-cta-spacer { height: 86px; }

.rem-cta-bar {
  /* sticky bottom strip — natural-flow so the document scroll */
  position: sticky;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 0 var(--s-5);
  max-height: 0;
  overflow: hidden;
  /* No gradient bleed — on the full page the Save button sits clean on the
     page canvas (the bleed was a bottom-sheet scroll-behind affordance). */
  background: transparent;
  pointer-events: none;
  z-index: var(--z-raised);
  transition: max-height var(--dur-card) var(--ease-out-soft),
              padding 320ms var(--ease-out-soft);
}
/* Sibling expansion — when the composer is in creating mode, the
   bar gains height + padding so the CTA can slide up into view. */
.rem-composer.is-creating ~ .rem-cta-bar,
.rem-composer.is-creating + .rem-cta-bar {
  /* 2026-06-05 — bumped 120px → 160px to fit the hint line above the
     button when no schedule is set. The hint adds ~20px (caption
     line-height + s-2 bottom margin); 120px was the bare-button cap. */
  max-height: 160px;
  padding: var(--s-3) var(--s-5) calc(14px + var(--safe-bottom, 0px));
}
.rem-cta-btn {
  pointer-events: none;
  opacity: 0;
  transform: translateY(110%);
  transition:
    opacity var(--dur-state) var(--ease-ios),
    transform 320ms var(--ease-out-soft);
  will-change: transform, opacity;
}
/* The composer and CTA bar are siblings inside the scroll body,
   so we use the sibling combinator to "lift" the CTA when the
   composer enters creation mode. */
.rem-composer.is-creating ~ .rem-cta-bar .rem-cta-btn,
.rem-composer.is-creating + .rem-cta-bar .rem-cta-btn {
  pointer-events: auto;
  opacity: 1;
  transform: translateY(0);
}
/* …but a not-yet-schedulable reminder's Save button must still read DISABLED:
   the slide-in opacity:1 above was overriding .btn-primary:disabled's dim, so
   the CTA looked fully active before a Date/Repeat was set (a user-error trap).
   Re-assert the dim at matching specificity (2026-06-20). */
.rem-composer.is-creating ~ .rem-cta-bar .rem-cta-btn:disabled,
.rem-composer.is-creating ~ .rem-cta-bar .rem-cta-btn[aria-disabled="true"],
.rem-composer.is-creating + .rem-cta-bar .rem-cta-btn:disabled,
.rem-composer.is-creating + .rem-cta-bar .rem-cta-btn[aria-disabled="true"] {
  opacity: .42;
}

.rem-date-overlay {
  /* Frosted-navy surface comes from the shared .sheet-scrim alongside. */
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: stretch;
  z-index: var(--z-sticky);
  animation: dateOverlayIn var(--dur-state) var(--ease-ios) forwards;
}
@keyframes dateOverlayIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.rem-date-sheet {
  width: 100%;
  /* Same canvas as #reminders-sheet / the Sound sheet — the canonical
     --grad-sheet gradient, so the calendar and Repeat sub-sheets read as
     the same surface family as every other bottom sheet (was flat var(--bg),
     left behind when the parent sheet moved to the gradient). */
  background: var(--grad-sheet);
  border-top-left-radius: var(--r-2xl);
  border-top-right-radius: var(--r-2xl);
  border-top: 1px solid var(--border);
  padding: var(--s-2) var(--s-4h) var(--s-5);
  animation: dateSheetUp var(--dur-state) cubic-bezier(.2,.8,.2,1) forwards;
  max-height: 86%;
  overflow-y: auto;
}
@keyframes dateSheetUp {
  from { transform: translateY(20%); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.rem-date-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-1) var(--s-4h);
}
/* Month prev/next nav now uses the shared .icon-btn .icon-btn--sm. */

/* Day-of-week header strip (S M T W T F S) + the day grid. */
.rem-cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--s-1);
}
.rem-cal-dow-row {
  margin-bottom: var(--s-1h);
}
.rem-cal-dow {
  font-family: var(--font-body);
  font-size: var(--fs-mini);
  font-weight: 700;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--text-3);
  text-align: center;
  padding: var(--s-1h) 0;
}
.rem-cal-cell {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-body); /* lint-skip CAT-T1: calendar cell button (.rem-cal-cell) — lh:1.5 would change cell height/grid alignment */
  font-weight: 500;
  cursor: pointer;
  padding: 0;
  transition:
    background var(--dur-state) var(--ease-ios),
    border-color var(--dur-state) var(--ease-ios),
    color var(--dur-state) var(--ease-ios);
  -webkit-tap-highlight-color: transparent;
}
.rem-cal-cell:hover:not(.is-empty):not(.is-past) {
  background: var(--sage-50);
  border-color: var(--sage-200);
  color: var(--sage);
}
.rem-cal-cell.is-empty {
  cursor: default;
  pointer-events: none;
}
.rem-cal-cell.is-past {
  color: var(--text-4);
  cursor: not-allowed;
}
.rem-cal-cell.is-today {
  color: var(--sage);
  font-weight: 700;
}
.rem-cal-cell.is-selected {
  background: var(--sage);
  border-color: var(--sage);
  color: var(--on-sage);
  font-weight: 700;
  /* 2026-06-02 — halo dropped to match the wider toggle/active-state
     sweep. Each calendar cell is a binary on/off pick, so it lives in
     the toggle family; the solid sage fill + on-sage text reads as
     selected without the cyan glow. */
}

/* Multi-select affordance line below the grid — tells the user the
   calendar accepts more than one day and echoes the running count. */
.rem-cal-hint {
  margin-top: var(--s-3);
  text-align: center;
  color: var(--text-3);
  /* .rem-cal-hint composes .caption in markup for the size. */
}

/* Bottom action row of the date sheet (just a Done button). */
.rem-date-actions {
  display: flex;
  justify-content: center;
  margin-top: var(--s-4h);
  padding-top: var(--s-3h);
  border-top: 1px solid var(--border);
}
.rem-date-actions .btn-ghost {
  width: auto;
  min-width: 140px;
  padding: var(--s-3) var(--s-6);
}

/* The repeat overlay shares .rem-date-overlay's animation + dim */
.rem-repeat-overlay {
  z-index: calc(var(--z-sticky) + 1);
}

/* The sheet body itself. Same .rem-date-sheet container so animation
   + rounded corners are inherited; this modifier just trims the
   bottom padding since the option rows already carry their own. */
.rem-repeat-sheet {
  padding-bottom: calc(18px + var(--safe-bottom, 0px));
}

/* Vertical stack of option rows. No internal borders — each row
   carries its own bottom hairline so the spacing reads as a list,
   not a card grid. */
.rem-repeat-list {
  display: flex;
  flex-direction: column;
  padding: var(--s-1) 0 0;
}

/* Single repeat option. Two-row text body (label + sub) + an optional */
.rem-repeat-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3h);
  padding: var(--s-4) var(--s-1);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur-state) var(--ease-ios);
  font-family: var(--font-body);
}
.rem-repeat-opt:last-child {
  border-bottom: none;
}
.rem-repeat-opt:hover {
  background: var(--sage-tint);
}
.rem-repeat-opt:active {
  background: var(--sage-soft);
}
.rem-repeat-opt-body {
  flex: 1;
  min-width: 0;
}
.rem-repeat-opt-label {
  font-weight: 600;
  letter-spacing: -.1px;
  color: var(--text);
  margin-bottom: 2px;
}
/* .rem-repeat-opt-sub composes .caption in markup for the size. */
.rem-repeat-opt-sub {
  color: var(--text-3);
  letter-spacing: .1px;
}
.rem-repeat-opt.is-selected .rem-repeat-opt-label {
  color: var(--sage);
}
.rem-repeat-opt-check {
  flex-shrink: 0;
  color: var(--sage);
  filter: drop-shadow(0 0 6px var(--sage-glow));
  display: inline-flex;
}
/* The inner SVG is now ${I.check} from the icon dictionary; size +
   stroke come from this companion rule so the markup stays glyph-
   free. Was a 18×18 standalone <svg class="rem-repeat-opt-check"> with
   stroke-width 2 hard-coded; the .check dictionary entry already
   carries fill:none + stroke-width 2, so we only set the dimensions. */
.rem-repeat-opt-check svg { width: 18px; height: 18px; }

/* Swipe-to-reveal-trash styling is now shared (.swipe-wrap / .swipe-del-pad /
   .swipe-row in modals.css). The old auto-delete tint (.will-delete) and the
   off-screen animation (.is-swipe-deleting) are gone with that flow. */

/* Reduce motion · honor the user's preference */
@media (prefers-reduced-motion: reduce) {
  
  .rem-cta-btn,
  .rem-bell-master,
  .rem-repeat-opt,
  .rem-cal-cell {
    transition: none;
  }
  .rem-date-overlay,
  .rem-date-sheet,
  .rem-repeat-overlay,
  .rem-repeat-sheet,
  .rem-cta-btn {
    animation: none;
  }
  .reminder-row {
    transition: none;
  }
}

/* PRACTICE CALENDAR SHEET — removed 2026-06-05.
   The .cal-* block (.cal-sheet / .cal-monthbar / .cal-nav / .cal-month-
   label / .cal-month-name / .cal-month-sum / .cal-weekdays / .cal-wd /
   .cal-grid / .cal-cell / .cal-cell-blank / .cal-cell-num / .cal-off /
   .cal-on / .l1 / .l2 / .l3 / .l4 / .is-today / .is-future / .cal-legend
   / .cal-legend-lbl / .cal-legend-swatch / .cal-safe-spacer) all went
   out with renderPracticeCalendarSheet in ui.js. See the deletion-note
   block there for the rationale (dead-code removal — entry-point was
   stripped from Stats 2026-05-31 and the orphaned sheet had no plan
   for re-enablement). */

/* ═══════════════════════════════════════════════════════════════
   PREVIEW SCREENS  (owner-only, ?debug=paywall)
   ─────────────────────────────────────────────────────────────
   Token classes for the debug-only preview screens (ANIMA+ paywall,
   Day-15 downgrade, Register / Log in / Log out, Connect-a-watch).
   Extracted from inline styles so the previews are lint-clean and
   committable. These are NOT final production screens — the real
   accounts / billing / wearable build will re-home them. Values are
   snapped to the design-token scale (sub-2px drift from the mocks).
   (2026-08-07)
   ═══════════════════════════════════════════════════════════════ */

/* ── Shared layout ─────────────────────────────────────────── */
.pv-scroll     { padding: 0 var(--s-5) var(--s-6); }
.pv-logo-row   { display: flex; justify-content: center; margin-top: var(--s-2); }
.pv-logo-row--top { margin-top: var(--s-6); }
/* ANIMA moon mark — fills with the app accent (cyan on dark / teal on light);
   the crescent is a transparent hole that shows the surface behind it. */
.orb-mark { color: var(--sage); }
.pv-note       { text-align: center; color: var(--text-4); font-size: var(--fs-mini); margin-top: var(--s-5); }
.pv-check      { width: 16px; height: 16px; flex-shrink: 0; }

/* Profile debug launcher */
.pv-launch       { margin: var(--s-6) var(--s-1) 0; padding-top: var(--s-4); border-top: 1px solid var(--border); }
.pv-launch-label { color: var(--text-3); }
.pv-launch-btn   { width: 100%; margin-top: var(--s-2); }   /* composes .btn-ghost */
.pv-ent-line     { color: var(--text); font-size: var(--fs-body); margin: 0 0 var(--s-1); }
.pv-ent-hint     { color: var(--text-3); margin: 0 0 var(--s-3); }

/* ── Paywall type discipline (2026-08-11) ──────────────────────
   The paywall previews follow a strict, low-friction type system:
   • TWO weights only — 700 (bold) for the hero title + primary numbers
     (stat values, prices); 400 (regular) for EVERYTHING else (eyebrow,
     sub, card labels, /mo, billing microcopy, badges, buttons).
   • Hierarchy comes from COLOR, not size: primary = --text, muted =
     --text-3, accent = --sage. Sizes are kept to a small set to stop
     the "5 sizes stacked together" clutter.
   • CTAs are sentence-case (no uppercase, no tracking) so the bottom of
     the screen reads calm and modern instead of shouty.
   The --ink-x and --teal-brand names in the design note map to these dark
   tokens (--ink-x are light-scope only). ──────────────────────── */
/* ── Eyebrow / title / sub (paywall + wearable) ────────────── */
.pv-eyebrow    { text-align: center; color: var(--sage); margin-top: var(--s-3); font-size: var(--fs-caption); font-weight: 400; letter-spacing: 2px; }   /* composes .section-label; 12px regular teal, editorial tracking */
.pv-title      { text-align: center; font-weight: 700; font-size: var(--fs-h2-md); color: var(--text); letter-spacing: -0.5px; margin: var(--s-1h) 0 0; text-wrap: balance; }
.pv-sub        { text-align: center; color: var(--text-3); font-size: var(--fs-body); margin-top: var(--s-2); }   /* muted — unified to --text-3 (single muted token across the paywall) */

/* ── Feature list ──────────────────────────────────────────── */
.pv-feats      { margin-top: var(--s-4h); }
.pv-feat       { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-2) 0; }
.pv-feat-label { color: var(--text); font-size: var(--fs-body); font-weight: 400; }

/* ── Plan cards ────────────────────────────────────────────── */
.pv-plans      { margin-top: var(--s-3h); }
.pv-plan       { border: 1px solid var(--border); border-radius: var(--r-lg2); padding: var(--s-3h) var(--s-4); display: flex; align-items: center; justify-content: space-between; margin-top: var(--s-2h); }
.pv-plan--on   { border-color: var(--sage); background: var(--sage-soft); }
.pv-plan-l     { display: flex; flex-direction: column; gap: var(--s-1h); }
.pv-plan-name  { color: var(--text); font-weight: 400; font-size: var(--fs-md); }   /* 16px regular, high-contrast — hierarchy via color, not weight */
.pv-plan-r     { text-align: right; }
.pv-plan-price { color: var(--text); font-weight: 700; font-size: var(--fs-h2); line-height: 1; }   /* bold — a primary number */
.pv-plan-permo { font-size: var(--fs-caption); font-weight: 400; color: var(--text-3); }
.pv-plan-detail{ color: var(--text-3); font-size: var(--fs-caption); margin-top: var(--s-1); }   /* 12px regular muted — billing microcopy */
.pv-save       { align-self: flex-start; background: var(--sage); color: var(--on-sage); font-weight: 400; font-size: var(--fs-mini); letter-spacing: 0.5px; padding: var(--s-1) var(--s-2); border-radius: var(--r-pill); }

/* ── Primary / ghost CTAs, footer links, cancel ───────────── */
.pv-cta        { width: 100%; margin-top: var(--s-4); font-weight: 700; font-size: var(--fs-body); letter-spacing: normal; text-transform: none; }   /* composes .btn-primary; sentence-case but BOLD (700) so the label reads clearly — stays within the 2-weight system (400/700) */
.pv-ghost-full { width: 100%; margin-top: var(--s-2h); font-weight: 700; font-size: var(--fs-body); letter-spacing: normal; text-transform: none; }   /* composes .btn-ghost; sentence-case, bold for legibility */
.pv-cancel     { text-align: center; color: var(--text-3); font-size: var(--fs-label); margin-top: var(--s-2h); }   /* muted — unified to --text-3 */
.pv-links      { display: flex; justify-content: center; gap: var(--s-4h); margin-top: var(--s-3); }

/* ── Downgrade stat cards ──────────────────────────────────── */
.pv-statrow    { display: flex; gap: var(--s-2h); margin-top: var(--s-4h); }
.pv-stat       { flex: 1; background: var(--white-mist); border-radius: var(--r-lg); padding: var(--s-3h) var(--s-2); text-align: center; }
.pv-stat-num   { color: var(--text); font-weight: 700; font-size: var(--fs-h2); letter-spacing: -0.5px; }
.pv-stat-label { color: var(--text-3); font-size: var(--fs-mini); letter-spacing: 0.5px; text-transform: uppercase; margin-top: var(--s-1); }   /* muted — unified to --text-3 */

/* ── Auth (register / login) ───────────────────────────────── */
.pv-authlogo   { display: flex; justify-content: center; margin-top: var(--s-1h); }
.pv-authtitle  { text-align: center; font-weight: 600; font-size: var(--fs-h2-lg); color: var(--text); margin: var(--s-4) 0 0; letter-spacing: -0.7px; text-wrap: balance; }
.pv-authsub    { text-align: center; color: var(--slate); font-size: var(--fs-md); margin-top: var(--s-2); }
.pv-field      { margin-top: var(--s-4h); }
.pv-label      { color: var(--slate); display: block; margin-bottom: var(--s-2); text-align: left; }   /* composes .section-label--lg */
.pv-input      { width: 100%; padding: var(--s-4); border-radius: var(--r-lg); }                       /* composes .field-input   */
.pv-input--pass{ padding-right: var(--s-7); }
.pv-pass-wrap  { position: relative; }
.pv-eye        { position: absolute; right: var(--s-2h); top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--slate); display: inline-flex; padding: var(--s-1h); cursor: pointer; }
.pv-eye svg    { width: 18px; height: 18px; }
.pv-terms      { text-align: center; color: var(--slate); font-size: var(--fs-caption); margin-top: var(--s-3); }
.pv-cross      { text-align: center; color: var(--slate); font-size: var(--fs-caption); margin-top: var(--s-5); }
.pv-forgot     { text-align: right; margin-top: var(--s-2h); }

/* ── Social sign-in row ────────────────────────────────────── */
.pv-divider      { display: flex; align-items: center; gap: var(--s-3); margin: var(--s-5) 0 var(--s-3h); }
.pv-divider-line { flex: 1; height: 1px; background: rgba(148,163,184,.28); }
.pv-divider-text { color: var(--slate); font-size: var(--fs-caption); }
.pv-social       { display: flex; gap: var(--s-3); }
/* Brand-standard social buttons — self-contained (NOT .btn-ghost, whose
   fill:none svg rule hollowed the marks). Apple: white button + black mark on
   the dark bg per Apple HIG, flips to a black button + white mark in light.
   Google: always a white container w/ the 4-color G, neutral-dark label + a
   hairline border, on both themes (Google brand guidance). */
.pv-social-btn   { flex: 1; padding: var(--s-4); border-radius: var(--r-pill); -webkit-appearance: none; appearance: none; font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body); letter-spacing: normal; text-transform: none; display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2); cursor: pointer; border: 1px solid transparent; transition: opacity var(--dur-state) var(--ease-ios); }
.pv-social-btn:active { transform: scale(.985); }
.pv-social-btn svg { width: 16px; height: 16px; }
.pv-social-btn--apple svg { width: 20px; height: 20px; transform: translateY(-1.5px); }  /* Apple mark: size up to match the Google G + a small upward nudge (its mass sits low, so it optically centers below the cap line) */
/* Base = DARK theme: elevated dark cards — a fill one step lighter than the
   canvas (--surface-soft, same lift as the input fields) with white marks/labels,
   not hollow outlines. Light theme flips both to the white-pill / hairline pair
   (see light.css). Apple mark + Google label follow currentColor; the Google G
   keeps its own 4-color fills in both themes. */
.pv-social-btn--apple  { background: var(--surface-soft); color: #fff; border-color: var(--border); } /* lint-skip CAT-16: pure-white mark/label on the dark card. Border matches the input fields. */
.pv-social-btn--google { background: var(--surface-soft); color: #fff; border-color: var(--border); } /* lint-skip CAT-16: pure-white label on the dark card */

/* ── Reset password ────────────────────────────────────────── */
.pv-authicon    { width: 68px; height: 68px; border-radius: var(--r-circle); border: 1.5px solid var(--sage); color: var(--sage); display: inline-flex; align-items: center; justify-content: center; }
.pv-authicon svg { width: 28px; height: 28px; }
.pv-reset-title { text-align: center; font-weight: 700; font-size: var(--fs-h2-lg); color: var(--text); letter-spacing: -0.7px; margin: var(--s-4) 0 0; text-wrap: balance; }
.pv-reset-sub   { text-align: center; color: var(--slate); font-size: var(--fs-md); margin: var(--s-2h) auto 0; max-width: 340px; line-height: 1.45; }
.pv-sent        { display: flex; align-items: flex-start; gap: var(--s-2h); background: var(--sage-soft); border: 1px solid var(--sage-270); border-radius: var(--r-lg2); padding: var(--s-3h) var(--s-4); margin-top: var(--s-4); }
.pv-sent-check  { color: var(--sage); flex-shrink: 0; margin-top: 1px; }
.pv-sent-check svg { width: 18px; height: 18px; }
.pv-sent-text   { color: var(--text); font-size: var(--fs-body); line-height: 1.5; }

/* ── Log-out bottom sheet ──────────────────────────────────── */
.pv-dim        { flex: 1; background: rgba(3,6,12,.62); }  /* proper dark modal scrim — a washed light-grey scrim made the sheet's rounded top corners read as stray grey 'angle' wedges */
.pv-sheet      { flex-shrink: 0; background: var(--grad-sheet); border-top: 1px solid var(--border); border-radius: var(--r-2xl) var(--r-2xl) 0 0; padding: 0 var(--s-5) calc(env(safe-area-inset-bottom, 0px) + var(--s-6)); box-shadow: none; }  /* dark scrim (.pv-dim) provides separation; the old upward shadow haloed the rounded corners into visible 'angle' wedges */
.pv-grip       { width: 40px; height: 4px; border-radius: var(--r-pill); background: var(--white-grip); margin: var(--s-3) auto var(--s-5); }
.pv-sheet-iconrow { display: flex; justify-content: center; }
.pv-sheet-icon { width: 60px; height: 60px; border-radius: var(--r-circle); border: 1.5px solid var(--rose); background: var(--rose-50); display: inline-flex; align-items: center; justify-content: center; color: var(--rose); }
.pv-sheet-icon svg { width: 26px; height: 26px; }
.pv-sheet-title{ text-align: center; font-weight: 600; font-size: var(--fs-h2); color: var(--text); margin: var(--s-4h) 0 0; letter-spacing: -0.3px; }
.pv-sheet-sub  { text-align: center; color: var(--slate); font-size: var(--fs-body); line-height: 1.5; margin: var(--s-2h) auto 0; max-width: 300px; }
.pv-sheet-btn  { width: 100%; padding: var(--s-4); border-radius: var(--r-pill); font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body); letter-spacing: normal; text-transform: none; cursor: pointer; }   /* sentence-case bold — matches the .pv-cta CTA treatment */
.pv-sheet-btn--primary { background: var(--sage); border: none; color: var(--on-sage); margin-top: var(--s-5); }
.pv-sheet-btn--ghost   { background: var(--rose-50); border: 1px solid rgba(224,138,138,.35); color: var(--rose); margin-top: var(--s-2h); }

/* ── Wearable / connect ────────────────────────────────────── */
.pv-wear-icon  { width: 64px; height: 64px; border-radius: var(--r-circle); background: var(--sage-soft); display: inline-flex; align-items: center; justify-content: center; color: var(--sage); }
.pv-wear-icon svg { width: 30px; height: 30px; }
.pv-devices-label { color: var(--text-3); margin: var(--s-5) 0 var(--s-1); }   /* composes .section-label--lg */
.pv-device     { display: flex; align-items: center; gap: var(--s-3h); padding: var(--s-3h) 0; border-top: 1px solid var(--border); }
.pv-device-body{ flex: 1; min-width: 0; }
.pv-device-tile{ width: 40px; height: 40px; border-radius: var(--r-md); background: var(--sage-soft); display: inline-flex; align-items: center; justify-content: center; color: var(--sage); }
.pv-device-tile svg { width: 22px; height: 22px; }
.pv-device-name{ color: var(--text); font-weight: 500; font-size: var(--fs-md); }
.pv-device-sub { color: var(--slate); font-size: var(--fs-caption); margin-top: var(--s-1); }
.pv-connect    { background: transparent; border: 1px solid var(--sage); color: var(--sage); font-family: var(--font-body); font-weight: 700; font-size: var(--fs-mini); letter-spacing: 1px; text-transform: uppercase; padding: var(--s-2) var(--s-3h); border-radius: var(--r-pill); cursor: pointer; }
.pv-connected  { display: flex; align-items: center; gap: var(--s-2); }
.pv-connected-badge { display: inline-flex; align-items: center; gap: var(--s-1h); color: var(--sage); font-family: var(--font-body); font-weight: 700; font-size: var(--fs-mini); letter-spacing: 1px; text-transform: uppercase; }
.pv-connected-badge svg { width: 14px; height: 14px; }
.pv-manage     { background: none; border: none; color: var(--slate); display: inline-flex; padding: var(--s-1h); cursor: pointer; }
.pv-manage svg { width: 18px; height: 18px; }

/* ===========================================================
   light.css
   LIGHT THEME · token overrides only (2026-08-03)
   ── Part of the ANIMA modular CSS. Edit partials, never the
      concatenated styles.css. Rebuild with: python3 build-css.py
   ── Loads LAST in the build ORDER. The whole app is driven by the
      design tokens in 00-settings.css; a light theme is therefore a
      TOKEN FLIP, not a per-screen repaint. Everything below only
      redefines those tokens under the [data-theme="light"] scope, so
      the attribute on <html> (set pre-paint by error-handler.js)
      switches the entire app.

   Palette (user hand-off):
     bg      #0B111C → #F0F4F9      ink     #E8EEF6 → #16232E
     muted   #8494AA → #66788A      accent  #7DD3E8 → #0E7A91
   Rules:
     · Accent darkens to #0E7A91 (4.9:1) for TEXT + ICONS; the pale
       cyan survives only as a FILL tint (so the low-alpha --sage-*
       fills below are intentionally NOT overridden — pale cyan over
       white reads as the same soft tint).
     · Filled accent buttons take WHITE labels (--on-sage → #fff).
     · Dividers become INK at 8–10% rather than white at 8% — same
       visual weight, opposite direction (the --white-* + --border
       ramp flips from white-alpha to ink-alpha).
     · Locked achievements dim AND lose their tint (rule at the tail).
   =========================================================== */
:root[data-theme="light"] {
  /* ── Canvas + surfaces ─────────────────────────────────────── */
  --bg-0:   #F0F4F9;    /* screen background */
  --bg-1:   #FFFFFF;    /* one step lifted — sheet / raised canvas */
  --bg-2:   #FFFFFF;    /* card surface */
  --card-1: #FFFFFF;
  --card-2: #FFFFFF;    /* raised card — lift comes from shadow, not a darker step */
  --chip-fill: rgba(14,122,145,0.08);   /* pale teal stat-chip fill (ref: profile chips) */
  --icon-fill: rgba(14,122,145,0.10);   /* avatar + row-icon tile — pale teal, was #13202F navy */

  --surface-soft:       rgba(22,35,46,0.03);
  --surface-soft-hover: rgba(22,35,46,0.05);

  --on-sage: #FFFFFF;   /* filled-accent labels = white, not near-black */

  /* Gradients → light. The dark navy sheet/lift/ambient gradients would
     otherwise punch a dark slab into the light UI. */
  --grad-sheet:   linear-gradient(180deg, #FFFFFF 0%, #F5F9FD 100%);
  --grad-lift:    linear-gradient(135deg, #FFFFFF 0%, #F0F4F9 100%);
  --grad-lift-v:  linear-gradient(180deg, #FFFFFF 0%, #F0F4F9 100%);
  --grad-ambient: linear-gradient(135deg, #F0F4F9 0%, #FFFFFF 60%, #FFFFFF 100%);

  /* ── Text / ink ────────────────────────────────────────────── */
  --text:   #16232E;   /* ink — 14:1 on --bg-0 */
  --text-2: #5e7082;   /* muted — nudged from the spec #66788A to clear 4.5:1 AA
                          for body/metadata (imperceptibly darker, still muted) */
  --text-3: #5f7184;   /* tertiary muted — clears 4.5:1 AA on the #F0F4F9 bg for
                          hero subtitles / sub-lines (was faint at #8494AA) */
  --text-4: #b4bcc7;   /* faintest (was a dark slate — flips to light grey) */
  --slate:  #475569;   /* medium-dark slate on the light bg (6.86:1) — comfortably
                          AA, still clearly secondary vs the near-black headers. */

  /* ── Semantic ink scale ────────────────────────────────────────
     Collapses the former ~8 ad-hoc per-selector slates into 4 tiers so
     light-theme text drift can't accrete. primary = headings/titles;
     secondary = body copy / chip labels; muted = captions / eyebrows /
     hints (AA-safe on the canvas); faint = disabled / decorative only. */
  --ink-primary:   #0F172A;   /* 16:1 on canvas */
  --ink-secondary: #334155;   /* 9.4:1 — body/subtitles */
  --ink-muted:     #5c6a80;   /* 4.97:1 on the #F0F4F9 canvas — AA for caption text */
  --ink-faint:     #94A3B8;   /* decorative / disabled only (sub-AA by design) */

  /* Destructive (light theme) — the canonical crimson, was hardcoded ~12×.
     Dark theme keeps --rose; crimson-on-navy would be too dark. */
  --crimson:     #C53030;
  --crimson-rgb: 197,48,48;

  /* ── Accent (cyan → teal) ──────────────────────────────────
     Only the SOLID accent (text / icons / filled-button bg) darkens.
     The low-alpha --sage-* fills stay pale cyan on purpose (see header). */
  --sage:      #0E7A91;    /* 4.9:1 on white */
  --sage-400:  #0c6a7e;
  --border-warm: rgba(14,122,145,0.24);

  /* Amber (retention / HOLD / streak accent) — the dark pastel #d4a574 never
     flipped, so every var(--amber) text/icon/pip (Best Hold value, PB pill,
     hold-ring chrome, round pips, leaderboard streak, reminder-permission warn,
     locked tech sub, learn tip) rendered at ~2:1 on the light canvas. Flip the
     SOLID token to a warm burnt orange (#C8632C, 3.6:1) — reads as fire, not the
     brown a yellow-amber gives — lifting all of them to ≥3:1 (large-text /
     non-text) at once. The low-alpha --amber-50/-100 FILLS stay pale on purpose
     (same rule as --sage), so amber badges read as dark-orange text on a pale
     wash. (user 2026-08-06 audit) */
  --amber:     #C8632C;

  /* Glows — keep the accent GLOWING on light like it does on dark (user).
     Teal-based (the light accent) at a real alpha so the halo actually reads
     on white around the play button, orb ring, and hero glow. */
  --sage-glow:      rgba(14,122,145,0.30);
  --sage-glow-40:   rgba(14,122,145,0.30);
  --sage-glow-42:   rgba(14,122,145,0.32);
  --sage-glow-45:   rgba(14,122,145,0.34);
  --sage-glow-peak: rgba(14,122,145,0.42);
  /* Row hover — the shared --white-mist (6% ink) reads as a gray smudge on the
     cool light ground. A faint teal-accent wash is a cleaner, on-brand highlight
     for tappable list rows in light theme. (user 2026-08-04) */
  --sage-wash:      rgba(14,122,145,0.07);

  /* ── Dividers / borders — ink-alpha, not white-alpha ───────── */
  --border:        rgba(22,35,46,0.14);   /* lifted 0.10 → 0.14 so dividers /
                                             card outlines read clearly on the
                                             light ground (user) */
  --border-strong: rgba(22,35,46,0.20);

  /* The full white-* ramp flips to ink-alpha so every hairline, wash,
     grip and raised-tile wash keeps its weight but points the opposite
     direction (subtle DARK over light, instead of subtle WHITE over dark). */
  --white-wash:     rgba(22,35,46,0.02);
  --white-wash-2:   rgba(22,35,46,0.03);
  --white-tint:     rgba(22,35,46,0.03);
  --white-veil:     rgba(22,35,46,0.04);
  --white-soft:     rgba(22,35,46,0.05);
  --white-mist:     rgba(22,35,46,0.06);
  --white-haze:     rgba(22,35,46,0.07);
  --white-film:     rgba(22,35,46,0.08);
  --white-line:     rgba(22,35,46,0.10);
  --white-line-2:   rgba(22,35,46,0.12);
  --white-edge:     rgba(22,35,46,0.13);
  --white-rule:     rgba(22,35,46,0.14);
  --white-rule-2:   rgba(22,35,46,0.16);
  --white-grip:     rgba(22,35,46,0.18);
  --white-grip-2:   rgba(22,35,46,0.24);
  --white-bright:   rgba(22,35,46,0.30);
  --white-strong:   rgba(22,35,46,0.38);
  --white-strong-2: rgba(22,35,46,0.42);
  --white-vivid:    rgba(22,35,46,0.55);

  /* ── Elevation — dark shadows softened for a light ground ──── */
  --elev-1:         0 1px 3px rgba(22,35,46,.08);
  --elev-2:         0 8px 28px rgba(22,35,46,.10);
  --elev-3:         0 24px 60px rgba(22,35,46,.14);
  --elev-glow-sage: 0 0 24px rgba(14,122,145,.32);
}

/* ── Component touch-ups for light ─────────────────────────────────
   Surfaces carrying a HARDCODED dark value or a too-faint accent fill —
   things a pure token flip can't reach. */

/* Bottom nav — --nav-bg is redefined locally on .bottom-nav, so the :root
   flip can't reach it; restate it as the light canvas (near-opaque so
   scrolling content doesn't bleed through the blur). */
:root[data-theme="light"] .bottom-nav { --nav-bg: rgba(240,244,249,0.98); }

/* Tune-sheet sticky CTA footer — a hardcoded navy gradient painted the dark
   slab behind the START button. Flip to a light-canvas fade so scrolling
   content still dissolves into it. */
:root[data-theme="light"] .modal-footer-v2 {
  background: linear-gradient(0deg, #F0F4F9 60%, rgba(240,244,249,0.92) 85%, transparent 100%);
}

/* Accent badges (UPGRADE): the ~8% fill nearly vanishes on white. Ground it
   with a stronger teal tint + a hairline so it reads as a clear pill. Scoped
   away from the muted/warn variants, which are meant to stay quiet. */
/* ALL badge pills get a defining border on light, tinted to their own fill so
   they read consistently (Upgrade=teal, Important=rose, SOON=neutral). */
:root[data-theme="light"] .row-badge {
  background: rgba(14,122,145,0.12);
  border: 1px solid rgba(14,122,145,0.24);
}
:root[data-theme="light"] .row-badge--warn {
  background: var(--rose-100);
  border-color: rgba(224,138,138,0.42);
}
:root[data-theme="light"] .row-badge--muted {
  background: var(--white-mist);
  border: 1px solid rgba(22,35,46,0.18);
}

/* Circular icon buttons (back / X / bell / gear) — glassy raised discs. A
   top-lit white→bg gradient gives them volume (a domed sheen), --elev-1 lifts
   them off the flat canvas so they clearly pop, and a soft ink ring finishes
   the edge. Was a flat 30%-ink outline + 2.5% fill, which read recessed. (user) */
:root[data-theme="light"] .icon-btn {
  background: linear-gradient(180deg, var(--bg-1), var(--bg-0));
  border-color: rgba(22,35,46,0.12);
  box-shadow: var(--elev-1);
}
/* Settings gear = filled accent tile (matches the hero gear), not the neutral
   icon-btn ring — out-specifies the rule above. */
:root[data-theme="light"] .prof-settings-gear {
  background: var(--sage-soft);
  border: 1px solid rgba(14,122,145,0.24);
  color: var(--sage);
}
/* Gear tap glow — the base :active bloom uses --sage-170 (pale sky-blue @18%),
   which is invisible on the light ground, so the user only saw it in dark. Swap
   to the deep-teal glow token so both gears light up on tap in light too. */
:root[data-theme="light"] .home-recent-edit:active,
:root[data-theme="light"] .prof-settings-gear:active {
  box-shadow: 0 0 12px var(--sage-glow-peak);
}

/* Tappable list-row hover → cool teal wash (see --sage-wash above). Covers the
   home/library, technique, profile, settings-modal, history-menu and reminder
   rows, which all default to --white-mist on hover. (user 2026-08-04) */
:root[data-theme="light"] .home-row:hover,
:root[data-theme="light"] .tech-row:hover,
:root[data-theme="light"] .prof-reminders-row:hover,
:root[data-theme="light"] .modal-row-clickable:hover,
:root[data-theme="light"] .hist-techmenu-item:hover,
:root[data-theme="light"] .reminder-row:hover,
:root[data-theme="light"] .lang-option:hover {
  background: var(--sage-wash);
}

/* Home hero mechanic subtitle — --text-3 @ .9 opacity reads too washed on the
   light card (user). Lift to a clearer slate at full opacity so it's a legible
   secondary line, not faded metadata. */
:root[data-theme="light"] .home-recent-tune { color: var(--ink-muted); opacity: 1; }

/* Home hero card title — pure ink reads too harsh on the light card; soften to
   the same slate as the sheet titles (user 2026-08-04). */
:root[data-theme="light"] .home-recent-title { color: var(--ink-primary); }

/* History screen metadata — on the light bg the session timestamps + the day
   summary read too faint, and the personal-best teal is light. Darken to
   accessible tones: slate #64748B for the timestamp + "N sessions · M min"
   summary (muted, so focus stays on the logs), teal #0F766E (4.96:1) for the
   personal-best tag. (user 2026-08-05) */
:root[data-theme="light"] .hist-rec-meta  { color: var(--ink-muted); }
:root[data-theme="light"] .hist-date-sum  { color: var(--ink-muted); }
:root[data-theme="light"] .hist-rec-pbtag { color: var(--sage); }
/* History HOLD pill — the --amber text (#d4a574) on the pale amber-50 fill reads
   washed out on the light bg. Darken the label to the warm accent orange, warm
   the fill, and add a defining border so the badge reads as a clear container. */
/* HOLD pill unified with the sage .row-badge light treatment (teal fill +
   border + teal text), matching the "Upgrade" pill. (user 2026-08-06) */
:root[data-theme="light"] .hist-rec-hold {
  color: var(--sage);
  background: rgba(14,122,145,0.10);
  border: 1px solid rgba(14,122,145,0.24);
}



/* Identity stat chips — give them a defining hairline on the light ground
   (like the UPGRADE pill), tinted per chip: a neutral ink edge for the
   grey chips, an amber edge for the streak chip. */
/* Stat pills — the neutral pill text (--text-2) on the pale chip fill measured
   4.17:1 (fails AA — the fill drags it below the page-bg figure). Darken to the
   #475569 slate (6.2:1 on the fill) + keep the hairline. (user 2026-08-05) */
:root[data-theme="light"] .prof-chip { border: 1px solid rgba(22,35,46,0.12); color: var(--ink-secondary); }
/* Streak pill — soft warm badge (NOT a solid fill, which read muddy). Matches
   the History "HOLD" pill exactly: the dark amber accent as the TEXT on a pale
   warm fill + warm border. Keeps the light/soft look while staying legible, and
   unifies the warm-accent badge language across screens. (user 2026-08-05) */
/* .prof-chip--streak inherits the neutral .prof-chip light treatment now
   (#475569 text + hairline) — unified with the sibling stat chips. (2026-08-06) */

/* Benefit pills (tech "how it works" sheet) already carry a border, but it's
   --sage-130 (14% cyan) — invisible on white. Lift it to a visible teal edge
   so the pills read as defined, matching the UPGRADE / chip treatment. */
:root[data-theme="light"] .benefit-pill { border-color: rgba(14,122,145,0.22); }

/* SOON pill on rows (.sound-v2-cue-row-badge — Haptic alert etc.) and the ghost
   "Done" button both read borderless/faint on light; give them a defining edge. */
:root[data-theme="light"] .sound-v2-cue-row-badge { border: 1px solid rgba(22,35,46,0.16); }
:root[data-theme="light"] .btn-ghost { border-color: rgba(14,122,145,0.42); }

/* ── Sound sheet — light-theme legibility (user 2026-08-04) ────────
   The inactive binaural card, unselected soundscape chips, and the headphones
   helper all render too faint on the cool light ground. Darken each to an
   explicit slate that reads clearly while still signalling secondary/inactive
   (the sage active states are untouched, so the selected item still stands out). */
/* Binaural card — inactive labels + goal + freq to a legible slate, and drop the
   .55 dim so #64748B renders true (it washed out to unreadable otherwise). */
:root[data-theme="light"] .sound-v2-bin-seg:not(.is-on) .sound-v2-bin-label,
:root[data-theme="light"] .sound-v2-bin-seg:not(.is-on) .sound-v2-bin-goal,
:root[data-theme="light"] .sound-v2-bin-seg:not(.is-on) .sound-v2-bin-freq { color: var(--ink-muted); }
:root[data-theme="light"] .sound-v2-bin-seg.is-dim:not(.is-on) { opacity: 1; }
/* Unselected soundscape chips — text + icon (currentColor). */
:root[data-theme="light"] .sound-v2-chip:not(.is-on) { color: var(--ink-secondary); }
/* Headphones helper note + its inline icon. */
:root[data-theme="light"] .sound-v2-helper,
:root[data-theme="light"] .sound-v2-hint-icon { color: var(--ink-muted); }

/* ── Learn / "how it works" sheet — 4-tier consolidation for light (user
      audit). The sheet had ~10 text styles; collapse the reading level to ONE
      body colour and the eyebrows to ONE label colour. Titles stay ink, the
      safety title stays coral, pills/CTA stay teal. */
/* BODY tier — every paragraph, step helper, loop line, safety intro, and
   safety bullet share one dark slate (#1F2937, ~12:1) so they read as one
   level and clear AA. Was a mid-gray --text-2/--text-3 (≈4.6:1, eyestrain). */
:root[data-theme="light"] .phase-sub,
:root[data-theme="light"] .phase-steps-loop,
:root[data-theme="light"] .modal-learn-body,
:root[data-theme="light"] .modal-learn-tagline,
:root[data-theme="light"] .modal-learn-safety-intro,
:root[data-theme="light"] .modal-learn-safety-list li,
:root[data-theme="light"] .modal-learn-safety-list li span { color: var(--ink-secondary); }
/* Loop instruction — drop the orphaned light-gray italic; it's body now. */
:root[data-theme="light"] .phase-steps-loop { font-style: normal; opacity: 1; }

/* Timer breathing orb — the fill is a HARDCODED rgba(14,24,36,.95) navy that
   doesn't flip, so on the light session screen it stayed a dark disc. Flip to
   a white orb (lifted off the cool bg by a soft shadow, with the phase-colour
   glow as a halo) and make the countdown numeral ink instead of white. */
:root[data-theme="light"] .orb-inner {
  background: radial-gradient(circle, #ffffff 58%, rgba(240,244,249,.55) 100%);
  box-shadow:
    0 0 50px rgba(var(--orb-color-rgb), .16) inset,
    0 0 40px rgba(var(--orb-color-rgb), .20),
    0 12px 36px rgba(22,35,46,.12);
}
:root[data-theme="light"] .orb-inner > #t-num { color: var(--text); }

/* Modal wheel — the SELECTED value was pure ink (--text), which reads as
   harshly black on the light sheet (user). Soften to a dark slate so it stays
   the clear focus without the heavy black. Neighbors keep their faint style. */
:root[data-theme="light"] .modal-wheel-item.is-current { color: var(--ink-primary); }

/* Sheet / dialog titles (.sheet-title — tune-modal header, Edit Profile, Save
   Preset, confirm dialogs) — pure ink reads too harsh on the bright surfaces
   (user 2026-08-04). Soften to one consistent dark slate. */
:root[data-theme="light"] .sheet-title { color: var(--ink-primary); }

/* Dialog cancel button ("Keep going") — the transparent fill + --border ring
   (10% ink) + muted --text-2 label read as faded on the light dialog.
   Strengthen the ring and darken the label so it's a clear secondary action. */
:root[data-theme="light"] .dialog-btn-cancel {
  border-color: rgba(22,35,46,0.22);
  color: var(--ink-secondary);
}
/* EYEBROW tier — one darker slate for the 11pt uppercase labels (BENEFITS /
   ABOUT THIS TECHNIQUE) so small text stays legible (#4A5568, ~6.9:1). */
:root[data-theme="light"] .modal-learn-eyebrow { color: var(--ink-muted); }

/* Safety title — soft --rose coral fails contrast on white at 16pt. Shift to a
   deeper crimson (still clearly a warning, but AA-legible). Light only; on the
   dark theme the coral reads fine on navy. */
:root[data-theme="light"] .modal-learn-safety-title { color: var(--crimson); }

/* Timer phase prompt (INHALE / HOLD / EXHALE) — in light theme --orb-color is
   already the DEEP per-phase hue (blue / lilac / orange, set theme-aware in
   pages.js + engine.js), so the label reads it directly and tracks the phase
   in an accessible tone. Drop the colored text-glow that only reads on the dark
   orb. (Was pinned to a stuck --orb-ink that never updated per phase.) */
:root[data-theme="light"] .timer-phase-label {
  color: var(--orb-color);
  text-shadow: none;
}

/* Locked achievements: on white, dim-opacity ALONE reads as a render bug,
   so they also lose their tint — muted grey glyph + dashed grey ring, at a
   deliberate 45% so "not yet earned" stays legible but clearly recessive. */
:root[data-theme="light"] .prof-next-ring:not(.is-earned) {
  opacity: .45;
  border-color: var(--text-3);
  color: var(--text-3);
}
:root[data-theme="light"] .prof-next-ring:not(.is-earned) .prof-next-ico { color: var(--text-3); }

/* Profile shelf earned badge — match the All-badges medallion: a soft cyan
   fill + white top-sheen inside the teal ring, instead of a hollow ring.
   Same recipe as .achieve-icon.earned below so the two surfaces agree. */
:root[data-theme="light"] .prof-next-ring.is-earned {
  background:
    radial-gradient(circle at 50% 32%, rgba(255,255,255,.55), rgba(255,255,255,0) 60%),
    linear-gradient(rgba(125,211,232,.20), rgba(125,211,232,.20)),
    linear-gradient(rgba(125,211,232,.20), rgba(125,211,232,.20));
}

/* Full "All badges" grid — locked tiles. The base fill is a HARDCODED
   rgba(20,32,46,.18) navy that doesn't flip, so on the light bg it rendered
   as a solid grey blob, and the glyph sat at opacity .14 (invisible on light).
   Give locked tiles a barely-there fill + a clearly muted glyph so they read
   as an empty dashed ring with a grey icon, not a grey disc. */
:root[data-theme="light"] .achieve-icon.locked {
  background: rgba(22,35,46,0.035);
  border-color: rgba(22,35,46,0.16);
}
:root[data-theme="light"] .achieve-icon.locked svg { stroke: var(--text-3); opacity: .5; }

/* Badge LABELS in light: the earned label goes to primary ink so it clearly
   reads as unlocked; locked labels darken to a readable muted grey (~5.9:1,
   comfortably past 4.5 AA) that still reads as inactive next to the ink one.
   (Dark theme keeps the softer text-2/text-3 from profile.css.) */
/* Earned badge labels read as the accent teal (matching the teal medallion +
   ring), so "unlocked" is instantly distinct from the muted-grey locked labels
   — and it drops the harsh near-black. Same on the profile shelf + full grid. */
:root[data-theme="light"] .achieve-name.is-earned,
:root[data-theme="light"] .prof-next-name.is-earned { color: var(--sage); }
:root[data-theme="light"] .achieve-name.is-locked { color: var(--ink-muted); }

/* Safety notes — the triangle warning note + the sparkle technique tip used the
   faded muted gray while the list above is dark slate ("two grays"). Unify all
   paragraph body copy to the same #1F2937 (icons keep their own tint). */
:root[data-theme="light"] .modal-learn-safety-note > span:not(.modal-learn-safety-note-icon),
:root[data-theme="light"] .modal-learn-tip > p { color: var(--ink-secondary); }

/* "Full safety guidelines" link — faded coral fails AA. Deep crimson + a heavier
   weight so the link stands out without relying on a light tint. */
:root[data-theme="light"] .modal-learn-safety-link { color: var(--crimson); font-weight: 600; }

/* Pre-session Safety Notice modal (.so-*) — same light fixes as the learn sheet:
   the soft-coral title fails AA (→ deep crimson), and the intro / ack-checkbox
   label / bottom link reverted to faded gray (→ the shared dark slate). The
   dark ack label also cures the "disabled" optical illusion next to the heavy
   teal checkbox. */
/* Full Safety Guidelines page (pgSafetyPage) — light contrast fixes (user):
   the disclaimer body was --text-2 (washed inside the pink box) → heavy slate;
   the coral section headers + disclaimer title (--rose) fail AA at small caps
   → deep crimson; the muted section eyebrows (--text-3) lift to a legible slate. */
:root[data-theme="light"] .safe-disc-body { color: var(--ink-secondary); }
:root[data-theme="light"] .safe-sec { color: var(--ink-muted); }
:root[data-theme="light"] .safe-sec.warn,
:root[data-theme="light"] .safe-disc-name { color: var(--crimson); }

/* Destructive dialog button ("Discard") — soft --rose fails AA on light → crimson. */
:root[data-theme="light"] .dialog-btn-discard { color: var(--crimson); border-color: rgba(var(--crimson-rgb),0.30); }

/* Log-out sheet (preview) destructive surfaces — soft --rose coral reads as a
   disabled/grayed state on white (2.3:1). Route the icon ring + ghost LOG OUT
   button through the same canonical crimson (4.95:1) so it carries intentional
   warning weight. Dark theme keeps the soft coral (reads fine on navy). */
:root[data-theme="light"] .pv-sheet-icon { color: var(--crimson); border-color: rgba(var(--crimson-rgb),0.35); background: rgba(var(--crimson-rgb),0.06); }
:root[data-theme="light"] .pv-sheet-btn--ghost { color: var(--crimson); border-color: rgba(var(--crimson-rgb),0.30); background: rgba(var(--crimson-rgb),0.06); }

/* Auth-preview placeholder — the brightened #94A3B8 is only 2.3:1 on the light
   field, so "you@email.com" washes out in bright sunlight. Darken to a clearly
   legible slate (light only; dark keeps the brighter value on navy). */
:root[data-theme="light"] .pv-input::placeholder { color: var(--ink-muted); }

/* Social-sign-in divider hairline — the translucent slate flanking "or sign up
   with" nearly disappears on the light bg. Pin it to a solid slate-200 stroke. */
:root[data-theme="light"] .pv-divider-line { background: #E2E8F0; }

/* Social sign-in buttons — light theme flips the dark ghost pills to the neutral
   white-pill / hairline pair (Apple mark+label black, Google 4-color G + neutral
   label). Dark theme keeps the ghost look from preview.css. */
:root[data-theme="light"] .pv-social-btn--apple,
:root[data-theme="light"] .pv-social-btn--google { background: #fff; border-color: #dadce0; }
:root[data-theme="light"] .pv-social-btn--apple  { color: #111; }
:root[data-theme="light"] .pv-social-btn--google { color: #3c4043; }

/* Edit-profile 'Remove' avatar action — the soft --rose coral (#e08a8a @ .82)
   washes out on white (~2.3:1). Route it through the canonical destructive
   crimson at full opacity, matching the logout sheet + Discard button. */
:root[data-theme="light"] .edit-prof-photo-remove { color: var(--crimson); opacity: 1; }

:root[data-theme="light"] .so-title { color: var(--crimson); }
:root[data-theme="light"] .so-intro,
:root[data-theme="light"] .so-ack-label,
:root[data-theme="light"] .so-link { color: var(--ink-secondary); }

/* Earned badge medallion in light: the base gradient fades to a HARDCODED
   rgba(20,32,46,.5) navy, which on white renders as that metallic grey. Replace
   the fill with a soft white top-sheen over the badge's own tier tint, so each
   earned badge is a clean, lit medallion in its tier colour (teal / amber /
   rose / …) — no grey. The tier-coloured border + glyph stay from the base. */
:root[data-theme="light"] .achieve-icon.earned {
  background:
    radial-gradient(circle at 50% 32%, rgba(255,255,255,.55), rgba(255,255,255,0) 60%),
    linear-gradient(var(--tier-glow, rgba(14,122,145,.2)), var(--tier-glow, rgba(14,122,145,.2))),
    linear-gradient(var(--tier-glow, rgba(14,122,145,.2)), var(--tier-glow, rgba(14,122,145,.2)));
}
