/* ═══════════════════════════════════════════════════════════════════════════
   EXTERNAL MIND — DESIGN SYSTEM 0.3
   Rewritten from scratch. Nothing was carried over from 0.2.

   WHY THIS FILE EXISTS IN THIS FORM
   0.2 had a coherent stylesheet and still looked like a 2015 admin panel,
   because the views never called it: 526 inline style attributes carrying 636
   hardcoded values, 36 font sizes, 14 weights. A token system nothing calls is
   decoration. So the rule that governs this file is not in this file — it is:
   **views contain zero inline style attributes.** If a view needs something,
   it gets a class here, derived from the tokens below.

   ARCHITECTURE — three layers, per ui-ux-pro-max:design-system
     1. PRIMITIVE  raw scales and raw palettes. No component may use these.
     2. SEMANTIC   purpose aliases (--bg, --text, --accent). Theming happens
                   entirely at this layer, which is why six themes need no
                   component rules of their own.
     3. COMPONENT  per-component values, all resolving to semantic tokens.

   HARD RULES this file holds itself to
     · 7 type sizes, 4 weights. Hierarchy from scale contrast, not variety.
     · 4pt spacing, expressed in rem so a larger system text size scales the
       layout with it (apple-design §15) instead of bursting it.
     · No element carries both a border and a shadow. Each surface picks one.
     · No negative margins.
     · No !important outside the reduced-motion block.
     · Animate transform and opacity only.
     · ≥16px body and inputs (below that iOS zooms on focus and never returns).
     · ≥44px on the smallest axis of anything tappable.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   LAYER 1 — PRIMITIVE
   Raw values. Never referenced by a component rule; only by layer 2.
   ═══════════════════════════════════════════════════════════════════════════ */
:root{
  /* ── Typeface ───────────────────────────────────────────────────────────
     System font first. On the target device (iPhone 11) this resolves to
     SF Pro, which ships optical sizing, tracking tables and legibility tuning
     for free and costs zero bytes — apple-design §15 is explicit that a custom
     face needs a reason to beat it. Inter, already self-hosted, is the
     cross-platform fallback. See SKILLS.md for the full conflict resolution. */
  --p-font-ui:-apple-system,BlinkMacSystemFont,'SF Pro Text','Inter',system-ui,'Segoe UI',sans-serif;
  --p-font-ar:'Noto Sans Arabic','Inter',system-ui,sans-serif;

  /* ── Type scale — 7 steps, in rem so Dynamic Type scales layout with text ──
     Steps 1–2 are labels and metadata, never body copy. Step 3 is the body and
     input floor at exactly 16px. */
  --p-fs-1:0.6875rem;  /* 11px  uppercase micro-label            */
  --p-fs-2:0.8125rem;  /* 13px  caption, metadata                */
  --p-fs-3:1rem;       /* 16px  body, row title, every input     */
  --p-fs-4:1.125rem;   /* 18px  emphasised body, the answer      */
  --p-fs-5:1.375rem;   /* 22px  screen + section title           */
  --p-fs-6:2rem;       /* 32px  secondary numeral                */
  --p-fs-7:3.5rem;     /* 56px  hero numeral                     */

  /* ── Weight — 4 steps. Every one exists in both shipped faces; asking for
     a weight the font lacks makes the browser snap or synthesise it. */
  --p-fw-1:400; --p-fw-2:500; --p-fw-3:600; --p-fw-4:700;

  /* ── Tracking — size-specific. A single letter-spacing value is wrong
     somewhere: large text reads loose, small uppercase reads cramped. */
  --p-tr-tight:-0.03em;   /* ≥32px            */
  --p-tr-snug:-0.015em;   /* 18–22px          */
  --p-tr-flat:0em;        /* body             */
  --p-tr-wide:0.08em;     /* uppercase micro  */

  /* ── Leading — inversely tracks size. */
  --p-lh-tight:1.05; --p-lh-snug:1.25; --p-lh-body:1.5; --p-lh-loose:1.65;

  /* ── Space — 4pt, in rem. --p-sp-1 is 4px at the default root size. */
  --p-sp-0:0rem;    --p-sp-1:0.25rem;  --p-sp-2:0.5rem;   --p-sp-3:0.75rem;
  --p-sp-4:1rem;    --p-sp-5:1.25rem;  --p-sp-6:1.5rem;   --p-sp-8:2rem;
  --p-sp-9:2.25rem; --p-sp-10:2.5rem;  --p-sp-12:3rem;    --p-sp-16:4rem;

  /* ── Radius — concentric: an inner radius equals its parent's minus the
     padding between them, so nested curves stay parallel. */
  --p-r-1:0.5rem; --p-r-2:0.75rem; --p-r-3:1rem; --p-r-4:1.375rem;
  --p-r-5:1.75rem; --p-r-full:999px;

  /* ── Duration ── */
  --p-d-press:120ms;   /* pointer-down feedback   */
  --p-d-fast:160ms;    /* chips, tags             */
  --p-d-base:220ms;    /* menus, colour changes   */
  --p-d-enter:320ms;   /* content entering        */
  --p-d-sheet:380ms;   /* bottom sheet            */
  --p-d-glad:520ms;    /* completion celebration  */

  /* ── Easing ──
     apple-design models UI motion as springs: damping 1.0 (no overshoot) by
     default, damping ~0.8 only where the gesture itself carried momentum.
     Plain CSS has no springs, so these curves approximate that ruling:
     --p-e-out is critically damped; --p-e-spring is the only curve permitted
     to overshoot, and only on momentum-driven moments. ease-in is absent by
     design — it delays the first frame, which is when the eye is on it. */
  --p-e-out:cubic-bezier(.23,1,.32,1);
  --p-e-inout:cubic-bezier(.77,0,.175,1);
  --p-e-sheet:cubic-bezier(.32,.72,0,1);   /* iOS drawer                     */
  --p-e-spring:cubic-bezier(.34,1.4,.64,1);/* momentum only — never on enter */

  /* ── Fixed dimensions. Intrinsic sizes, not spacing. Named so clearances
     can be computed instead of guessed. */
  --p-tap:44px;      /* HIG minimum on the smallest axis  */
  --p-tab-h:50px;    /* tab bar body, excluding safe area */
  --p-fab:56px;
  --p-check:22px;
  --p-hair:1px;

  /* ── Safe areas, resolved once. */
  --p-safe-t:env(safe-area-inset-top,0px);
  --p-safe-b:env(safe-area-inset-bottom,0px);
  --p-safe-l:env(safe-area-inset-left,0px);
  --p-safe-r:env(safe-area-inset-right,0px);

  /* ── Layer scale. Nothing invents its own z-index. */
  --p-z-base:0; --p-z-sticky:20; --p-z-tab:30; --p-z-fab:40;
  --p-z-scrim:50; --p-z-sheet:60; --p-z-toast:70;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAYER 2 — SEMANTIC
   Purpose aliases. Theming happens here and only here: six themes × two modes
   redefine these names, and every component below keeps working untouched.

   Naming contract, identical in all twelve variants:
     --bg          the page beneath everything
     --surface     a grouped container sitting on --bg
     --surface-2   an inset well inside a surface (inputs, tracks, segments)
     --text        primary copy
     --text-2      secondary copy — still real text, still needs 4.5:1
     --text-3      metadata and micro-labels — still real text, still 4.5:1
     --accent      the single accent. One per theme, never two.
     --on-accent   copy placed on top of --accent
     --ok/--warn/--bad   status only, never decoration
     --hairline    separator between rows inside a surface
     --ring        inset outline that gives a surface its edge
     --scrim       modal dimming
     --amb         accent as bare "r,g,b" for building translucent washes
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Basalt — cold neutral stone. The default: nothing competes with data. ── */
[data-theme="basalt"][data-mode="dark"]{
  --bg:#0b0d10; --surface:#14171c; --surface-2:#1b1f26; --sunken:#0e1114;
  --text:#eef1f5; --text-2:#a4adba; --text-3:#8b95a4;
  --accent:#7d9bc4; --on-accent:#0b0d10; --amb:125,155,196;
  --ok:#6fb894; --warn:#c9a468; --bad:#d18a8a;
  --hairline:rgba(255,255,255,.075); --ring:rgba(255,255,255,.09);
  --press:rgba(255,255,255,.055); --scrim:rgba(5,7,9,.62);
  --chrome:rgba(11,13,16,.72); --track:rgba(255,255,255,.085);
  --shade:6,7,8;
}
[data-theme="basalt"][data-mode="light"]{
  --bg:#f4f5f7; --surface:#ffffff; --surface-2:#eef0f4; --sunken:#e8eaef;
  --text:#0d1014; --text-2:#4f5866; --text-3:#5c6675;
  --accent:#3c5a80; --on-accent:#ffffff; --amb:60,90,128;
  --ok:#2e7d5b; --warn:#8a6420; --bad:#a44a4a;
  --hairline:rgba(13,16,20,.085); --ring:rgba(13,16,20,.10);
  --press:rgba(13,16,20,.045); --scrim:rgba(13,16,20,.42);
  --chrome:rgba(244,245,247,.78); --track:rgba(13,16,20,.075);
  --shade:7,8,11;
}

/* ── Linen — warm paper. Editorial, low-glare, easy at night. ── */
[data-theme="linen"][data-mode="dark"]{
  --bg:#100e0b; --surface:#1a1713; --surface-2:#221e19; --sunken:#141210;
  --text:#f4f0e9; --text-2:#b3a894; --text-3:#9c917e;
  --accent:#c89a72; --on-accent:#100e0b; --amb:200,154,114;
  --ok:#8aab7a; --warn:#c9a25f; --bad:#cf8a78;
  --hairline:rgba(255,248,235,.075); --ring:rgba(255,248,235,.09);
  --press:rgba(255,248,235,.055); --scrim:rgba(10,8,6,.62);
  --chrome:rgba(16,14,11,.72); --track:rgba(255,248,235,.085);
  --shade:8,7,6;
}
[data-theme="linen"][data-mode="light"]{
  --bg:#f7f4ee; --surface:#fffdf9; --surface-2:#f0ece3; --sunken:#eae5db;
  --text:#171310; --text-2:#5b5145; --text-3:#665c4f;
  --accent:#8a5a34; --on-accent:#fffdf9; --amb:138,90,52;
  --ok:#456b3c; --warn:#7d5c17; --bad:#9c4a3c;
  --hairline:rgba(23,19,16,.085); --ring:rgba(23,19,16,.10);
  --press:rgba(23,19,16,.045); --scrim:rgba(23,19,16,.42);
  --chrome:rgba(247,244,238,.78); --track:rgba(23,19,16,.075);
  --shade:12,10,8;
}

/* ── Moss — cool green-grey. Restful without being a "wellness" cliché. ── */
[data-theme="moss"][data-mode="dark"]{
  --bg:#0a0e0b; --surface:#131813; --surface-2:#1a201a; --sunken:#0d120d;
  --text:#ecf1ec; --text-2:#a0b0a3; --text-3:#8b9a8e;
  --accent:#8ab493; --on-accent:#0a0e0b; --amb:138,180,147;
  --ok:#78b98d; --warn:#c2a86a; --bad:#cf8d8d;
  --hairline:rgba(238,255,242,.07); --ring:rgba(238,255,242,.09);
  --press:rgba(238,255,242,.05); --scrim:rgba(5,8,6,.62);
  --chrome:rgba(10,14,11,.72); --track:rgba(238,255,242,.08);
  --shade:5,7,6;
}
[data-theme="moss"][data-mode="light"]{
  --bg:#f2f5f2; --surface:#ffffff; --surface-2:#eaefea; --sunken:#e3e9e4;
  --text:#0c110d; --text-2:#4c5850; --text-3:#586459;
  --accent:#3a6b4d; --on-accent:#ffffff; --amb:58,107,77;
  --ok:#2f7350; --warn:#836018; --bad:#9e4a45;
  --hairline:rgba(12,17,13,.085); --ring:rgba(12,17,13,.10);
  --press:rgba(12,17,13,.045); --scrim:rgba(12,17,13,.42);
  --chrome:rgba(242,245,242,.78); --track:rgba(12,17,13,.075);
  --shade:6,9,7;
}

/* ── Harbor — deep blue-grey. Steady, maritime, the coolest of the six. ── */
[data-theme="harbor"][data-mode="dark"]{
  --bg:#080c11; --surface:#111820; --surface-2:#182029; --sunken:#0b1016;
  --text:#e9f0f6; --text-2:#9dadbc; --text-3:#8797a7;
  --accent:#6fa8b6; --on-accent:#080c11; --amb:111,168,182;
  --ok:#6bb8a4; --warn:#c3a469; --bad:#cf8b93;
  --hairline:rgba(233,244,255,.07); --ring:rgba(233,244,255,.09);
  --press:rgba(233,244,255,.05); --scrim:rgba(4,6,9,.62);
  --chrome:rgba(8,12,17,.72); --track:rgba(233,244,255,.08);
  --shade:4,6,9;
}
[data-theme="harbor"][data-mode="light"]{
  --bg:#f1f4f7; --surface:#ffffff; --surface-2:#e9eef3; --sunken:#e2e8ee;
  --text:#0a0f14; --text-2:#4b5763; --text-3:#56626f;
  --accent:#2f6373; --on-accent:#ffffff; --amb:47,99,115;
  --ok:#2c7263; --warn:#7f5f1a; --bad:#9c4a52;
  --hairline:rgba(10,15,20,.085); --ring:rgba(10,15,20,.10);
  --press:rgba(10,15,20,.045); --scrim:rgba(10,15,20,.42);
  --chrome:rgba(241,244,247,.78); --track:rgba(10,15,20,.075);
  --shade:5,8,11;
}

/* ── Clay — warm brown-grey with a dusty rose accent. The softest. ── */
[data-theme="clay"][data-mode="dark"]{
  --bg:#0f0c0d; --surface:#191415; --surface-2:#211b1c; --sunken:#130f10;
  --text:#f3eeee; --text-2:#b0a3a4; --text-3:#9a8d8e;
  --accent:#c08d92; --on-accent:#0f0c0d; --amb:192,141,146;
  --ok:#84b391; --warn:#c6a367; --bad:#d08a8a;
  --hairline:rgba(255,244,244,.075); --ring:rgba(255,244,244,.09);
  --press:rgba(255,244,244,.055); --scrim:rgba(8,6,6,.62);
  --chrome:rgba(15,12,13,.72); --track:rgba(255,244,244,.085);
  --shade:8,6,7;
}
[data-theme="clay"][data-mode="light"]{
  --bg:#f6f3f3; --surface:#ffffff; --surface-2:#efeaea; --sunken:#e8e2e2;
  --text:#140f10; --text-2:#584e4f; --text-3:#635858;
  --accent:#8a4a55; --on-accent:#ffffff; --amb:138,74,85;
  --ok:#37704f; --warn:#805e1c; --bad:#9d4646;
  --hairline:rgba(20,15,16,.085); --ring:rgba(20,15,16,.10);
  --press:rgba(20,15,16,.045); --scrim:rgba(20,15,16,.42);
  --chrome:rgba(246,243,243,.78); --track:rgba(20,15,16,.075);
  --shade:11,8,8;
}

/* ── Ink — near-monochrome. Maximum restraint; the accent is light itself. ── */
[data-theme="ink"][data-mode="dark"]{
  --bg:#0a0a0b; --surface:#141415; --surface-2:#1c1c1e; --sunken:#0d0d0e;
  --text:#f2f2f3; --text-2:#a6a6a9; --text-3:#8e8e92;
  --accent:#d8d8dc; --on-accent:#0a0a0b; --amb:216,216,220;
  --ok:#79b393; --warn:#c4a56c; --bad:#cf8c8c;
  --hairline:rgba(255,255,255,.075); --ring:rgba(255,255,255,.09);
  --press:rgba(255,255,255,.055); --scrim:rgba(0,0,0,.64);
  --chrome:rgba(10,10,11,.72); --track:rgba(255,255,255,.085);
  --shade:5,5,6;
}
[data-theme="ink"][data-mode="light"]{
  --bg:#f5f5f6; --surface:#ffffff; --surface-2:#ededef; --sunken:#e6e6e8;
  --text:#0b0b0c; --text-2:#4e4e52; --text-3:#5a5a5f;
  --accent:#26262a; --on-accent:#ffffff; --amb:38,38,42;
  --ok:#2d7355; --warn:#7d5d18; --bad:#9c4646;
  --hairline:rgba(11,11,12,.085); --ring:rgba(11,11,12,.10);
  --press:rgba(11,11,12,.045); --scrim:rgba(11,11,12,.44);
  --chrome:rgba(245,245,246,.78); --track:rgba(11,11,12,.075);
  --shade:6,6,6;
}

/* Pre-resolution fallback. The layout renders dark-first before the inline
   head script settles data-mode, so a cold start never flashes unstyled. */
:root{
  --bg:#0b0d10; --surface:#14171c; --surface-2:#1b1f26; --sunken:#0e1114;
  --text:#eef1f5; --text-2:#a4adba; --text-3:#8b95a4;
  --accent:#7d9bc4; --on-accent:#0b0d10; --amb:125,155,196;
  --ok:#6fb894; --warn:#c9a468; --bad:#d18a8a;
  --hairline:rgba(255,255,255,.075); --ring:rgba(255,255,255,.09);
  --press:rgba(255,255,255,.055); --scrim:rgba(5,7,9,.62);
  --chrome:rgba(11,13,16,.72); --track:rgba(255,255,255,.085);
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAYER 3 — COMPONENT TOKENS
   Every value resolves to a semantic token. Changing a component's feel
   happens here, not scattered through the rules below.
   ═══════════════════════════════════════════════════════════════════════════ */
:root{
  --c-screen-pad:var(--p-sp-4);
  --c-group-r:var(--p-r-3);
  --c-row-h:52px;
  --c-row-pad-x:var(--p-sp-4);
  --c-row-lead:var(--p-sp-8);       /* leading control column width */
  --c-sep-inset:calc(var(--c-row-pad-x) + var(--c-row-lead) + var(--p-sp-3));
  --c-sheet-r:var(--p-r-5);
  --c-hero-r:var(--p-r-4);
  --c-btn-h:var(--p-tap);
  --c-btn-r:var(--p-r-full);
  --c-inp-r:var(--p-r-2);

  /* The two permitted elevation treatments. A surface picks exactly one —
     never a border and a shadow together, which is what made every 0.2
     container read at the same depth. */
  --c-elev-flat:inset 0 0 0 var(--p-hair) var(--ring);
  /* Tinted, not pure black. --shade is the theme's own darkest neutral as an
     r,g,b triple, so a shadow on the warm linen theme is a warm shadow and one
     on harbor is cool. Pure black over a tinted surface reads as a hole. */
  --c-elev-lift:0 1px 2px rgba(var(--shade),.18), 0 12px 32px -14px rgba(var(--shade),.55);
  --c-elev-sheet:0 -1px 0 var(--ring), 0 -24px 56px -20px rgba(var(--shade),.6);

  /* How a surface's own background is painted. Flat (default) is a plain
     opaque fill — exactly 0.4's look, unchanged. A family below can swap
     this to a translucent, blurred fill without any component rule knowing;
     every consumer just uses --c-surface-bg / --c-surface-blur instead of
     --surface / none directly. */
  --c-surface-bg:var(--surface);
  --c-surface-blur:none;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAYER 3 VARIANT — VISUAL FAMILY
   data-theme decides what colour a surface is; data-family decides how it is
   built. Four families, one rule each still obeys the file's own hard rule
   above (a surface picks exactly one of {ring, shadow} — never both):

     flat    (default) — inset hairline ring, opaque fill. Today's 0.4 look.
     glass   — translucent blurred fill, ring only, no shadow — glass is a
               material change, not a second elevation system stacked on top.
     soft    — "Frosted Clay" as of v0.8 (D21, D26; was 0.5's plain dual-
               shadow neumorphism-adjacent family before this). No ring —
               translucent blurred fill (glass) PLUS a dual soft shadow
               (light+dark, both derived from --shade, never a literal
               colour) so the translucency still reads as raised rather than
               flat. Sits above a fixed backdrop of blurred colour blobs
               (below, near the family's own block) drawn from each colour
               theme's own --accent/--ok/--warn — without real colour behind
               it the blur is just grey haze, which is what sank the pure-
               glass round of comparisons. Larger radii, literal px not the
               shared scale. Kept inside the app's own 4.5:1 contrast rules
               the same way the old soft did: only shadow/blur shifts, text
               and fill opacity never do.
     classic — outer shadow only (no ring), opaque fill, tuned softer/deeper
               than flat's --c-elev-lift for a more editorial "card" read.

   Every block below declares EVERY token this axis owns, including the ones
   equal to flat's — never only the ones that differ. --c-surface-bg etc. are
   custom properties, and an element that does not specify one inherits it
   from its nearest ancestor that does; a probe (or any nested element) under
   a page whose <html> carries a different data-family would otherwise leak
   that ancestor's values through instead of falling back to the true flat
   default. Declaring the full set on every family closes that gap.

   Every family reassigns only Layer 3 tokens, so every component below
   — .group, .sheet, .inp, .chip, .tag, .segctl, .scale, all of them — themes
   itself with zero changes, exactly like the six colour themes already do.
   ═══════════════════════════════════════════════════════════════════════════ */
[data-family="flat"],[data-family="glass"],[data-family="soft"],[data-family="classic"]{
  --c-group-r:var(--p-r-3);
  --c-sheet-r:var(--p-r-5);
  --c-surface-bg:var(--surface);
  --c-surface-blur:none;
  --c-elev-flat:inset 0 0 0 var(--p-hair) var(--ring);
  --c-elev-lift:0 1px 2px rgba(var(--shade),.18), 0 12px 32px -14px rgba(var(--shade),.55);
  --c-elev-sheet:0 -1px 0 var(--ring), 0 -24px 56px -20px rgba(var(--shade),.6);
}

[data-family="glass"]{
  --c-surface-bg:rgba(var(--shade),.42);
  --c-surface-blur:saturate(180%) blur(20px);
  --c-elev-flat:inset 0 0 0 var(--p-hair) rgba(255,255,255,.14);
  --c-elev-lift:inset 0 0 0 var(--p-hair) rgba(255,255,255,.18);
  --c-elev-sheet:inset 0 1px 0 rgba(255,255,255,.14);
}
[data-family="glass"][data-mode="light"]{
  --c-surface-bg:rgba(255,255,255,.5);
  --c-elev-flat:inset 0 0 0 var(--p-hair) rgba(255,255,255,.6);
  --c-elev-lift:inset 0 0 0 var(--p-hair) rgba(255,255,255,.7);
}

/* v0.8: "soft" is now Frosted Clay (docs/11_VISUAL_IDENTITY_V08.md, D21/D26)
   — the value stays "soft" (no settings migration for anyone already on it;
   this is a replacement, not a new fifth family) but the technique is now
   glass's translucent blur PLUS clay's dual soft shadow, not shadow alone.
   Surface-bg/blur were never set here before — every other component below
   already reads --c-surface-bg/--c-surface-blur generically (same tokens
   "glass" uses), so giving this family real values for them is the entire
   glass half of the technique; nothing downstream needed to change.
   Radii are literal px, not the shared --p-r-* scale — deliberately puffier
   than every other family, per the reference doc's own radius table. */
[data-family="soft"]{
  --c-group-r:26px;
  --c-sheet-r:32px;
  --c-surface-bg:rgba(255,255,255,.08);
  --c-surface-blur:saturate(150%) blur(15px);
  --c-elev-flat:-7px -7px 16px rgba(255,255,255,.10), 7px 7px 18px rgba(var(--shade),.5);
  --c-elev-lift:-8px -8px 20px rgba(255,255,255,.12), 9px 9px 24px rgba(var(--shade),.6);
  --c-elev-sheet:0 -1px 0 rgba(255,255,255,.10), 0 -24px 56px -20px rgba(var(--shade),.6);
}
[data-family="soft"][data-mode="light"]{
  --c-surface-bg:rgba(255,255,255,.55);
  --c-elev-flat:-7px -7px 16px rgba(255,255,255,.7), 7px 7px 18px rgba(var(--shade),.16);
  --c-elev-lift:-8px -8px 20px rgba(255,255,255,.8), 9px 9px 24px rgba(var(--shade),.2);
}

/* The colour blobs behind every frosted panel — without them the blur has
   nothing to reveal and just reads as grey haze, the specific defect that
   sank the pure-glass round of comparisons (D21). Each of the app's six
   colour themes supplies its OWN three hues here (--accent/--ok/--warn,
   already tuned per theme for both modes) rather than one fixed palette —
   reusing tokens that already exist rather than inventing a seventh palette
   (D26), and it means switching colour theme also restyles this backdrop
   instead of leaving it the one un-themed thing on the page.
   html carries the flat --bg fallback so nothing flashes unstyled; body
   turns transparent so the fixed, blurred pseudo-element behind it — sitting
   above html's own painted background, below every real element in body via
   z-index — is what actually shows through the gaps between panels. */
html{background:var(--bg)}
[data-family="soft"] body{background:transparent}
[data-family="soft"] body::before{
  content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(220px 220px at 88% 6%, var(--accent), transparent 70%),
    radial-gradient(200px 200px at 4% 32%, var(--ok), transparent 70%),
    radial-gradient(240px 240px at 78% 94%, var(--warn), transparent 70%);
  filter:blur(46px);
  opacity:.85;
}
[data-family="soft"][data-mode="light"] body::before{opacity:.55}

[data-family="classic"]{
  --c-elev-flat:0 1px 3px rgba(var(--shade),.14), 0 8px 20px -12px rgba(var(--shade),.4);
  --c-elev-lift:0 2px 4px rgba(var(--shade),.2), 0 20px 44px -18px rgba(var(--shade),.5);
}

/* ═══════════════════════════════════════════════════════════════════════════
   BASE
   ═══════════════════════════════════════════════════════════════════════════ */
*,*::before,*::after{box-sizing:border-box}

html{
  -webkit-text-size-adjust:100%;
  /* Removes the ~300ms synthetic click delay without disabling pinch-zoom. */
  touch-action:manipulation;
}

/* The grey flash iOS paints over every tap is the single loudest signal that
   an installed PWA is really a web page. */
*{-webkit-tap-highlight-color:transparent}

body{
  margin:0;
  background:var(--bg);
  color:var(--text);
  font-family:var(--p-font-ui);
  font-size:var(--p-fs-3);
  font-weight:var(--p-fw-1);
  line-height:var(--p-lh-body);
  letter-spacing:var(--p-tr-flat);
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  /* Data columns must not jitter as digits change width. */
  font-variant-numeric:tabular-nums;
  /* Stops a scroll at the end of a list chaining into the document and
     triggering pull-to-refresh mid-interaction. */
  overscroll-behavior-y:none;
  transition:background-color var(--p-d-base) var(--p-e-out),
             color var(--p-d-base) var(--p-e-out);
}
[dir="rtl"] body{font-family:var(--p-font-ar)}

h1,h2,h3,p,figure,dl,dd{margin:0}
ul,ol{margin:0;padding:0;list-style:none}
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
a{color:inherit;text-decoration:none}
svg{display:block;flex:none}
img{max-width:100%;display:block}
input,select,textarea{font:inherit;color:inherit}
table{border-collapse:collapse;width:100%}

::selection{background:var(--accent);color:var(--on-accent)}

/* Keyboard only. A focus ring that appears on touch reads as a rendering bug. */
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--p-r-1)}
:focus:not(:focus-visible){outline:none}

.u-sr{position:absolute;width:1px;height:1px;padding:0;margin:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}

/* ═══════════════════════════════════════════════════════════════════════════
   ICON SIZING — the base every sprite glyph gets, and why it exists
   An <svg> with no width/height and no viewBox of its own is a replaced
   element: the CSS default sizing algorithm gives it 300x150. The sprite's
   geometry lives in the <symbol>, so an icon() call renders at 300x150 unless
   something sizes it.
   0.3 relied entirely on per-container rules (.btn svg, .tile svg, .tab svg …)
   for that. Thirteen containers had one; .row, .row-x, .segctl and .srow did
   not, and 21 icons across Settings, the More sheet, the appearance sheet,
   Dashboard, Wallet, Notes, Search, Journal, Planner, Debts and Achievements
   rendered at 300x150. The audit for 0.4 has the full list.
   So the size is a BASE now, carried by every icon() call through .ico, and
   the per-container rules stay as overrides — they are all `.x svg` (0,1,1)
   and still beat this (0,1,0). Nothing that was correct changes; the ones
   with no container rule stop being 300x150. */
.ico{width:18px;height:18px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;flex:none}

/* Keyboard users get a way past the fixed chrome. */
.u-skip{position:absolute;inset-inline-start:var(--p-sp-2);top:var(--p-sp-2);
  z-index:var(--p-z-toast);padding:var(--p-sp-3) var(--p-sp-4);
  background:var(--surface);color:var(--text);border-radius:var(--p-r-2);
  box-shadow:var(--c-elev-lift);transform:translateY(-200%)}
.u-skip:focus{transform:none}

/* ═══════════════════════════════════════════════════════════════════════════
   TYPE ROLES
   ═══════════════════════════════════════════════════════════════════════════ */
.t-micro{font-size:var(--p-fs-1);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-wide);line-height:1;text-transform:uppercase;
  color:var(--text-3)}
/* Arabic has no case, and letterspacing breaks its joins. */
[dir="rtl"] .t-micro{letter-spacing:0;text-transform:none}
.t-cap{font-size:var(--p-fs-2);line-height:var(--p-lh-snug);color:var(--text-2)}
.t-body{font-size:var(--p-fs-3);line-height:var(--p-lh-body)}
.t-lead{font-size:var(--p-fs-4);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-snug);line-height:var(--p-lh-snug);text-wrap:balance}
[dir="rtl"] .t-lead{letter-spacing:0}
.t-title{font-size:var(--p-fs-5);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-snug);line-height:var(--p-lh-snug);text-wrap:balance}
[dir="rtl"] .t-title{letter-spacing:0}
.t-num{font-size:var(--p-fs-6);font-weight:var(--p-fw-4);
  letter-spacing:var(--p-tr-tight);line-height:var(--p-lh-tight);
  font-variant-numeric:tabular-nums}
[dir="rtl"] .t-num{letter-spacing:0}
.t-hero{font-size:var(--p-fs-7);font-weight:var(--p-fw-4);
  letter-spacing:var(--p-tr-tight);line-height:0.9;
  font-variant-numeric:tabular-nums}
[dir="rtl"] .t-hero{letter-spacing:0}
.t-dim{color:var(--text-2)}
.t-mute{color:var(--text-3)}
.t-ok{color:var(--ok)} .t-bad{color:var(--bad)} .t-warn{color:var(--warn)}
.t-accent{color:var(--accent)}
.t-clip{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ═══════════════════════════════════════════════════════════════════════════
   SHELL — fixed chrome, scrolling content
   The header and tab bar never scroll with content. Both are translucent
   layers with content passing underneath, which is how iOS conveys that
   chrome floats above the document rather than occupying a fixed strip.
   ═══════════════════════════════════════════════════════════════════════════ */
.app{min-height:100vh;min-height:100dvh;display:flex;flex-direction:column}

.topbar{
  position:sticky;top:0;z-index:var(--p-z-sticky);
  padding-top:var(--p-safe-t);
  background:var(--chrome);
  -webkit-backdrop-filter:saturate(180%) blur(20px);
  backdrop-filter:saturate(180%) blur(20px);
}
.topbar-in{
  max-width:560px;margin:0 auto;
  min-height:var(--p-tap);
  padding:var(--p-sp-2) max(var(--c-screen-pad),var(--p-safe-r))
          var(--p-sp-2) max(var(--c-screen-pad),var(--p-safe-l));
  display:flex;align-items:center;gap:var(--p-sp-3)}
.topbar-l{min-width:0;flex:1;display:flex;flex-direction:column;gap:var(--p-sp-1)}
.topbar-r{display:flex;align-items:center;gap:var(--p-sp-1);flex:none}
/* Back affordance for detail screens. iOS puts the chevron and the parent's
   name in the leading slot of the nav bar; the label is what makes it a
   destination rather than a generic "undo". It flips with the writing
   direction because the chevron is drawn from an inline-start icon. */
.topbar-b{flex:none;display:inline-flex;align-items:center;gap:var(--p-sp-1);
  min-height:var(--p-tap);padding-inline-end:var(--p-sp-3);
  font-size:var(--p-fs-3);color:var(--accent);
  transition:opacity var(--p-d-press) var(--p-e-out)}
.topbar-b:active{opacity:.5}
.topbar-b svg{width:20px;height:20px;stroke:currentColor;fill:none;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* Directional glyphs — disclosure chevrons and back arrows — are the only
   icons in the sprite that mean something about direction, so they are the
   only ones that mirror. A trash or pin glyph must not flip; tagging the
   chevrons explicitly is what keeps those two cases apart.

   0.3 shipped ONLY the mirror rule below and no base rule, so every chevron
   that was not inside a container with its own `svg` size rule rendered at the
   SVG replaced-element default of 300x150 — 21 call sites, Settings included.
   See ICON SIZING above (.ico) for the class-wide fix; this sets the chevron's
   own size. Both are base rules now, and the mirror is the modifier it always
   should have been. */
.chev{width:16px;height:16px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;flex:none}
[dir="rtl"] .chev{transform:scaleX(-1)}
/* A hairline under floating chrome reads as a hard divider; apple-design §12
   asks for a scroll edge effect instead — the chrome fades into content. */
.topbar::after{content:"";position:absolute;top:100%;inset-inline:0;
  height:var(--p-sp-4);pointer-events:none;
  background:linear-gradient(var(--bg),transparent)}

.screen{
  flex:1;
  max-width:560px;width:100%;margin:0 auto;
  padding:var(--p-sp-4) max(var(--c-screen-pad),var(--p-safe-r))
          calc(var(--p-safe-b) + var(--p-tab-h) + var(--p-fab) + var(--p-sp-6))
          max(var(--c-screen-pad),var(--p-safe-l));
  -webkit-overflow-scrolling:touch;
}
/* Screens with no tab bar (auth, install, overwhelmed) reclaim the clearance. */
.screen.is-bare{padding-bottom:calc(var(--p-safe-b) + var(--p-sp-10))}

/* ── Overwhelmed Mode ──────────────────────────────────────────────────
   Vertically centred, generously spaced, and holding exactly one decision.
   The card has no surface of its own: a bordered box inside an empty screen
   would just be another frame to parse. */
.bare-in{min-height:calc(100dvh - var(--p-safe-t) - var(--p-safe-b) - var(--p-sp-10));
  display:flex;flex-direction:column;justify-content:center;gap:var(--p-sp-8)}
.bare-exit{align-self:flex-start;margin-top:var(--p-safe-t)}
.bare-card{display:flex;flex-direction:column;align-items:center;
  gap:var(--p-sp-3);text-align:center;padding:var(--p-sp-6) var(--p-sp-2)}
.bare-card .btn{width:100%}
.bare-card form{width:100%}
.bare-note{margin-top:auto;text-align:center;
  font-size:var(--p-fs-2);color:var(--text-3);line-height:var(--p-lh-snug);
  max-width:34ch;margin-inline:auto}

@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .topbar,.tabbar{background:var(--surface)}
}
/* apple-design §14: a reduced-transparency request means frostier and solid,
   not merely less blurred. */
@media (prefers-reduced-transparency:reduce){
  .topbar,.tabbar{background:var(--surface);
    -webkit-backdrop-filter:none;backdrop-filter:none}
}

/* ═══════════════════════════════════════════════════════════════════════════
   TAB BAR — one navigation model. 0.2 shipped a desktop sidebar and a tab bar
   simultaneously; this replaces both.
   ═══════════════════════════════════════════════════════════════════════════ */
.tabbar{
  position:fixed;inset-inline:0;bottom:0;z-index:var(--p-z-tab);
  padding-bottom:var(--p-safe-b);
  background:var(--chrome);
  -webkit-backdrop-filter:saturate(180%) blur(20px);
  backdrop-filter:saturate(180%) blur(20px);
}
.tabbar::before{content:"";position:absolute;top:0;inset-inline:0;
  height:var(--p-hair);background:var(--hairline)}
.tabbar-in{max-width:560px;margin:0 auto;display:flex;
  padding-inline:max(0px,var(--p-safe-l)) max(0px,var(--p-safe-r))}
.tab{
  position:relative;flex:1;min-height:var(--p-tab-h);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--p-sp-1);
  font-size:var(--p-fs-1);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-wide);line-height:1;text-transform:uppercase;
  color:var(--text-3);
  transition:color var(--p-d-base) var(--p-e-out)}
[dir="rtl"] .tab{letter-spacing:0;text-transform:none}
.tab svg{width:22px;height:22px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;
  transition:transform var(--p-d-press) var(--p-e-out)}
.tab:active svg{transform:scale(.86)}
.tab.is-on{color:var(--accent)}
.tab-dot{position:absolute;top:var(--p-sp-2);inset-inline-start:calc(50% + 8px);
  width:6px;height:6px;border-radius:var(--p-r-full);background:var(--bad)}

/* ═══════════════════════════════════════════════════════════════════════════
   THE ANSWER — the hero of the home screen
   This is the only surface in the app that uses the nested construction:
   an ambient wash, an inset ring, and a lifted shadow. It earns it because it
   carries the one question the product exists to answer. Everything else stays
   flat so this reads as the single most important thing on screen.
   ═══════════════════════════════════════════════════════════════════════════ */
.answer{
  position:relative;overflow:hidden;
  border-radius:var(--c-hero-r);
  background:
    radial-gradient(120% 90% at 85% -20%, rgba(var(--amb),.18), transparent 62%),
    var(--c-surface-bg);
  -webkit-backdrop-filter:var(--c-surface-blur);backdrop-filter:var(--c-surface-blur);
  box-shadow:inset 0 0 0 var(--p-hair) rgba(var(--amb),.22),
             0 16px 40px -20px rgba(var(--amb),.42);
  padding:var(--p-sp-5);
  display:flex;align-items:center;gap:var(--p-sp-4);
  margin-bottom:var(--p-sp-6);
}
.answer-b{min-width:0;flex:1;display:flex;flex-direction:column;gap:var(--p-sp-2)}
.answer-b .t-micro{color:var(--accent)}
.answer-t{font-size:var(--p-fs-4);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-snug);line-height:var(--p-lh-snug);
  text-wrap:balance;overflow-wrap:anywhere}
[dir="rtl"] .answer-t{letter-spacing:0}
.answer-go{
  flex:none;width:var(--p-tap);height:var(--p-tap);border-radius:var(--p-r-full);
  background:var(--accent);display:grid;place-items:center;
  transition:transform var(--p-d-press) var(--p-e-spring)}
.answer-go svg{width:17px;height:17px;stroke:var(--on-accent);fill:none;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* A chevron is a directional glyph, not a layout box — it must mirror. */
.answer-go:active{transform:scale(.9)}

/* ═══════════════════════════════════════════════════════════════════════════
   STATS STRIP — what replaced the four-card grid
   Demoted to one flat row of figures. Context, not headline.
   ═══════════════════════════════════════════════════════════════════════════ */
.strip{display:flex;background:var(--c-surface-bg);border-radius:var(--c-group-r);
  -webkit-backdrop-filter:var(--c-surface-blur);backdrop-filter:var(--c-surface-blur);
  box-shadow:var(--c-elev-flat);overflow:hidden;margin-bottom:var(--p-sp-6)}
.strip-i{position:relative;flex:1;min-width:0;
  padding:var(--p-sp-4) var(--p-sp-3);
  display:flex;flex-direction:column;gap:var(--p-sp-2);align-items:flex-start}
.strip-i + .strip-i::before{content:"";position:absolute;
  inset-inline-start:0;top:var(--p-sp-3);bottom:var(--p-sp-3);
  width:var(--p-hair);background:var(--hairline)}
/* .strip-i packs to the start, which would collapse a width:100% .bar to zero.
   A bar in a strip item spans the item. */
.strip-i > .bar{align-self:stretch}
.strip-v{font-size:var(--p-fs-4);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-snug);line-height:1;font-variant-numeric:tabular-nums;
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
[dir="rtl"] .strip-v{letter-spacing:0}

/* ═══════════════════════════════════════════════════════════════════════════
   GROUPED LIST — the app's primary container
   One surface, hairline separators inset past the leading control. This is
   what replaced 0.2's stacks of individually bordered-and-shadowed cards.
   ═══════════════════════════════════════════════════════════════════════════ */
.sec{margin-bottom:var(--p-sp-6)}
.sec-h{display:flex;align-items:center;justify-content:space-between;
  gap:var(--p-sp-3);margin-bottom:var(--p-sp-2);padding-inline:var(--p-sp-1)}
.sec-h > a,.sec-h > button{position:relative;
  font-size:var(--p-fs-2);font-weight:var(--p-fw-2);
  color:var(--text-3);min-height:var(--p-tap);display:inline-flex;align-items:center;
  transition:color var(--p-d-press) var(--p-e-out)}
/* These carry short labels — "Add", "Edit", "View all" — so min-height alone
   left a 24x44 target: tall enough, far too narrow. Widening the box would
   pull the label off the section's edge, so the hit area is expanded behind
   it instead, the same way .check and .btn-ico.btn-sm do it. */
.sec-h > a::after,.sec-h > button::after{content:"";position:absolute;
  inset-block:0;inset-inline:calc(var(--p-sp-3) * -1)}
.sec-h > a:active,.sec-h > button:active{color:var(--text)}

.group{background:var(--c-surface-bg);border-radius:var(--c-group-r);
  -webkit-backdrop-filter:var(--c-surface-blur);backdrop-filter:var(--c-surface-blur);
  box-shadow:var(--c-elev-flat);overflow:hidden}

.row{position:relative;display:flex;align-items:center;gap:var(--p-sp-3);
  width:100%;text-align:start;
  min-height:var(--c-row-h);padding:var(--p-sp-2) var(--c-row-pad-x);
  transition:background-color var(--p-d-press) var(--p-e-out)}
.row:active{background:var(--press)}
/* Separator starts past the leading control, so the control column reads as a
   column rather than every row being boxed. */
.row + .row::before{content:"";position:absolute;top:0;
  inset-inline-start:var(--c-sep-inset);inset-inline-end:0;
  height:var(--p-hair);background:var(--hairline)}
/* Rows with no leading control get a plain inset instead. */
.row.is-flush + .row.is-flush::before,
.row + .row.is-flush::before{inset-inline-start:var(--c-row-pad-x)}
.row-b{min-width:0;flex:1;display:flex;flex-direction:column;gap:var(--p-sp-1)}
.row-t{font-size:var(--p-fs-3);font-weight:var(--p-fw-2);
  line-height:var(--p-lh-snug);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row-s{font-size:var(--p-fs-1);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-wide);line-height:1;text-transform:uppercase;
  color:var(--text-3)}
[dir="rtl"] .row-s{letter-spacing:0;text-transform:none}
/* Sentence-case subtitle, for note previews and descriptions. */
.row-p{font-size:var(--p-fs-2);color:var(--text-2);line-height:var(--p-lh-snug);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row-x{flex:none;display:flex;align-items:center;gap:var(--p-sp-1)}
.row.is-done .row-t{color:var(--text-3)}
/* Trailing figure column. Right-aligned so digits line up down the page —
   the reason money and counts never sit inside .row-b. Tabular numerals keep
   the columns from shifting as the digits change. */
.amt{flex:none;display:flex;flex-direction:column;align-items:flex-end;
  gap:var(--p-sp-1);text-align:end;font-variant-numeric:tabular-nums}
/* Stretched link. A row that both navigates and owns a control (pin, delete)
   cannot be a single <a> — a <form> inside an anchor is invalid HTML and the
   nested button stops working. The anchor instead covers the row from behind
   and the controls are lifted above it, so the whole row is one tap target
   without breaking the markup. */
.row-link::after{content:"";position:absolute;inset:0}
.row > .row-x{position:relative}

/* v0.8: Tasks manual reorder. touch-action:none on the handle stops the
   browser's own scroll gesture from fighting a drag started with a finger —
   the rest of the page still scrolls normally, only this one small control
   opts out. The dragged row lifts (transform/opacity only, per this file's
   own animation rule) rather than changing layout, so nothing else in the
   list has to reflow while a finger is still moving. */
[data-reorder-handle]{touch-action:none;cursor:grab}
.row.is-dragging{position:relative;z-index:1;opacity:.85;
  transform:scale(1.02);box-shadow:var(--c-elev-lift);
  transition:none}

/* ═══════════════════════════════════════════════════════════════════════════
   CHECK — completion, the product's most important positive moment
   The strike-through is a clip wipe rather than an instant text-decoration,
   so finishing something reads as an event.
   ═══════════════════════════════════════════════════════════════════════════ */
.check{
  position:relative;flex:none;
  width:var(--p-check);height:var(--p-check);border-radius:var(--p-r-full);
  box-shadow:inset 0 0 0 1.5px var(--text-3);
  display:grid;place-items:center;
  transition:background-color var(--p-d-press) var(--p-e-out),
             box-shadow var(--p-d-press) var(--p-e-out),
             transform var(--p-d-press) var(--p-e-spring)}
.check svg{width:12px;height:12px;stroke:var(--on-accent);fill:none;
  stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;
  opacity:0;transform:scale(.6);
  transition:opacity var(--p-d-press) var(--p-e-out),
             transform var(--p-d-press) var(--p-e-spring)}
/* The visible ring is 22px; this lifts the real hit area to 44 without
   changing the row's rhythm. */
.check::after{content:"";position:absolute;
  width:var(--p-tap);height:var(--p-tap);
  top:50%;inset-inline-start:50%;transform:translate(-50%,-50%)}
[dir="rtl"] .check::after{transform:translate(50%,-50%)}
.check:active{transform:scale(.86)}
.check.is-on{background:var(--ok);box-shadow:inset 0 0 0 1.5px var(--ok)}
.check.is-on svg{opacity:1;transform:none}

/* The wipe. Applied to the title when a row completes. */
.strike{position:relative}
.strike::after{content:"";position:absolute;inset-inline-start:0;top:50%;
  width:100%;height:1.5px;background:currentColor;border-radius:var(--p-r-full);
  transform:scaleX(0);transform-origin:left center;
  transition:transform var(--p-d-enter) var(--p-e-out)}
[dir="rtl"] .strike::after{transform-origin:right center}
.row.is-done .strike::after{transform:scaleX(1)}

/* Celebration ring — fires once on completion, then removes itself. */
@keyframes glad{
  0%{transform:scale(1);opacity:.55}
  100%{transform:scale(2.1);opacity:0}
}
.check.is-glad::before{content:"";position:absolute;inset:0;
  border-radius:var(--p-r-full);box-shadow:inset 0 0 0 2px var(--ok);
  animation:glad var(--p-d-glad) var(--p-e-out) forwards;pointer-events:none}

/* ═══════════════════════════════════════════════════════════════════════════
   TAG — status stamps. Colour is never the only carrier; each has a word.
   ═══════════════════════════════════════════════════════════════════════════ */
.tag{flex:none;display:inline-flex;align-items:center;gap:var(--p-sp-1);
  padding:var(--p-sp-1) var(--p-sp-2);border-radius:var(--p-r-1);
  font-size:var(--p-fs-1);font-weight:var(--p-fw-4);
  letter-spacing:var(--p-tr-wide);line-height:1;text-transform:uppercase;
  white-space:nowrap;box-shadow:var(--c-elev-flat);
  color:var(--text-2)}
[dir="rtl"] .tag{letter-spacing:0;text-transform:none}
.tag.is-late{color:var(--bad)} .tag.is-ok{color:var(--ok)}
.tag.is-warn{color:var(--warn)} .tag.is-accent{color:var(--accent)}

/* ═══════════════════════════════════════════════════════════════════════════
   METER — segmented, instrument-like. Discrete segments read as a measurement
   rather than a marketing progress bar.
   ═══════════════════════════════════════════════════════════════════════════ */
.meters{display:flex;flex-direction:column;gap:var(--p-sp-4)}
.meter{display:flex;align-items:center;gap:var(--p-sp-3)}
.meter-l{flex:none;width:4.5rem;min-width:0;
  display:flex;flex-direction:column;gap:var(--p-sp-1)}
.meter-v{font-size:var(--p-fs-2);font-weight:var(--p-fw-3);line-height:1;
  font-variant-numeric:tabular-nums}
.seg{flex:1;display:flex;gap:var(--p-sp-1);height:20px;align-items:stretch;min-width:0}
.seg i{flex:1;border-radius:2px;background:var(--track);
  transform:scaleY(.36);transform-origin:center;
  transition:transform var(--p-d-base) var(--p-e-out),
             background-color var(--p-d-base) var(--p-e-out)}
.seg i.is-on{background:var(--seg-c,var(--accent));transform:scaleY(1)}
/* Under RTL, .seg is a flex row inside an rtl container, so its segments fill
   from the right with no extra rule. */

/* Continuous fill, for proportions rather than counts. scaleX, never width —
   width would relayout the track every frame. */
.bar{height:6px;border-radius:var(--p-r-full);background:var(--track);overflow:hidden}
.bar i{display:block;height:100%;width:100%;border-radius:var(--p-r-full);
  background:var(--bar-c,var(--accent));
  transform:scaleX(var(--v,0));transform-origin:left center;
  transition:transform var(--p-d-enter) var(--p-e-out)}
[dir="rtl"] .bar i{transform-origin:right center}

/* v0.8 Phase 2: the ring counterpart to .bar — same --v (from vclass(), the
   identical class .bar reads) and the identical --bar-c colour hook the k-a/
   k-ok/k-warn/k-bad classes already set, so a ring and a bar showing the same
   number can never disagree. Rendered correct at paint time from a
   server-computed percentage, not animated in — stroke-dashoffset is not a
   compositor-only property, and this file's own hard rule is transform/
   opacity only, so there is nothing here to animate rather than something
   that quietly breaks that rule. rotate(-90deg) only moves the fill's start
   point to 12 o'clock; it does not itself animate. */
.ring{flex:none;transform:rotate(-90deg)}
.ring circle{fill:none;stroke-width:3}
.ring-track{stroke:var(--track)}
.ring-fill{stroke:var(--bar-c,var(--accent));stroke-linecap:round;
  stroke-dasharray:100;stroke-dashoffset:calc(100 * (1 - var(--v,0)))}
.ring-xs{width:20px;height:20px}
.ring-xs circle{stroke-width:4}
.ring-sm{width:32px;height:32px}

/* ═══════════════════════════════════════════════════════════════════════════
   CHARTS — bars and sparkline. Bars grow; the numbers never animate.
   ═══════════════════════════════════════════════════════════════════════════ */
@keyframes grow{from{transform:scaleY(.02)}to{transform:scaleY(1)}}
.chart{display:flex;align-items:flex-end;gap:var(--p-sp-2);height:120px;
  padding:var(--p-sp-4) var(--p-sp-4) 0}
.chart-c{flex:1;min-width:0;height:100%;display:flex;flex-direction:column;
  align-items:center;gap:var(--p-sp-2)}
.chart-g{flex:1;width:100%;display:flex;align-items:flex-end;justify-content:center;gap:var(--p-sp-1)}
.chart-g i{flex:1;max-width:16px;border-radius:2px 2px 0 0;
  background:var(--bar-c,var(--accent));
  height:var(--h,0%);min-height:3px;transform-origin:bottom;
  animation:grow var(--p-d-enter) var(--p-e-out) both}
.chart-l{font-size:var(--p-fs-1);font-weight:var(--p-fw-3);color:var(--text-3);
  letter-spacing:var(--p-tr-wide);text-transform:uppercase;white-space:nowrap}
[dir="rtl"] .chart-l{letter-spacing:0;text-transform:none}
.legend{display:flex;justify-content:center;gap:var(--p-sp-4);
  padding:var(--p-sp-3) var(--p-sp-4);
  font-size:var(--p-fs-1);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-wide);text-transform:uppercase;color:var(--text-3)}
[dir="rtl"] .legend{letter-spacing:0;text-transform:none}
.legend i{display:inline-block;width:8px;height:8px;border-radius:2px;
  margin-inline-end:var(--p-sp-1)}
.spark{display:flex;align-items:flex-end;gap:var(--p-sp-1);height:44px;
  padding:var(--p-sp-3) var(--p-sp-4) 0}
.spark i{flex:1;border-radius:2px 2px 0 0;background:var(--accent);opacity:.85;
  height:var(--h,0%);min-height:2px;transform-origin:bottom;
  animation:grow var(--p-d-enter) var(--p-e-out) both}

/* Habit heatmap — the same segment idea wrapped into a grid. */
.heat{display:flex;flex-wrap:wrap;gap:var(--p-sp-1)}
.heat i{width:12px;height:12px;border-radius:2px;background:var(--track)}
.heat i.is-on{background:var(--ok)}
.heat i.is-miss{background:var(--bad);opacity:.32}

/* ═══════════════════════════════════════════════════════════════════════════
   RECOVERY COUNTER — negative habits (v0.5)
   Not a checkbox row: the streak is a hero numeral, matching the "days since"
   framing of the reference apps (Quit Smoking / NoFap style) rather than a
   daily to-do. One card per habit — this is deliberately heavier than a list
   row, the same way .answer earns its own weight on Today.
   ═══════════════════════════════════════════════════════════════════════════ */
.recovery{padding:var(--p-sp-6) var(--p-sp-5);display:flex;flex-direction:column;
  align-items:center;gap:var(--p-sp-1);text-align:center}
.recovery-meta{display:flex;flex-direction:column;gap:var(--p-sp-1);
  margin:var(--p-sp-2) 0 var(--p-sp-4)}
.recovery-actions{display:flex;flex-direction:column;gap:var(--p-sp-2);width:100%}

/* ═══════════════════════════════════════════════════════════════════════════
   EMPTY STATE — heading, guidance, action. Never a bare line of grey text.
   ═══════════════════════════════════════════════════════════════════════════ */
.empty{padding:var(--p-sp-10) var(--p-sp-5);display:flex;flex-direction:column;
  align-items:center;gap:var(--p-sp-2);text-align:center}
.empty-i{width:40px;height:40px;display:grid;place-items:center;
  border-radius:var(--p-r-full);background:var(--surface-2);color:var(--text-3);
  margin-bottom:var(--p-sp-2)}
.empty-i svg{width:20px;height:20px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
.empty-t{font-size:var(--p-fs-3);font-weight:var(--p-fw-3);color:var(--text)}
.empty-p{font-size:var(--p-fs-2);color:var(--text-3);max-width:30ch;
  line-height:var(--p-lh-snug)}

/* A skeleton-shimmer set lived here and was removed in review: nothing
   referenced it, and it looped `background-position` forever — a paint
   property, not a compositor one, burning battery on a phone for a component
   that never rendered. CatchUpService runs server-side before the response is
   written, so there is no loading gap for a skeleton to fill in the first
   place. If one is ever needed, animate opacity between two static fills. */

/* ═══════════════════════════════════════════════════════════════════════════
   BUTTONS
   ═══════════════════════════════════════════════════════════════════════════ */
.btn{position:relative;display:inline-flex;align-items:center;justify-content:center;
  gap:var(--p-sp-2);
  min-height:var(--c-btn-h);padding:0 var(--p-sp-5);border-radius:var(--c-btn-r);
  font-size:var(--p-fs-3);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-snug);white-space:nowrap;
  transition:transform var(--p-d-press) var(--p-e-out),
             background-color var(--p-d-base) var(--p-e-out),
             box-shadow var(--p-d-base) var(--p-e-out),
             color var(--p-d-base) var(--p-e-out)}
[dir="rtl"] .btn{letter-spacing:0}
.btn:active{transform:scale(.97)}
.btn svg{width:18px;height:18px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
.btn[disabled],.btn[aria-disabled="true"]{opacity:.4;pointer-events:none}
.btn-fill{background:var(--accent);color:var(--on-accent)}
.btn-soft{background:var(--surface-2);color:var(--text);box-shadow:var(--c-elev-flat)}
.btn-quiet{background:transparent;color:var(--accent)}
.btn-bad{background:transparent;color:var(--bad);box-shadow:var(--c-elev-flat)}
.btn-sm{min-height:36px;min-width:36px;padding:0 var(--p-sp-4);font-size:var(--p-fs-2)}
.btn-wide{width:100%}
.btn-ico{width:var(--p-tap);min-width:var(--p-tap);padding:0}
/* A 36px button keeps a 44px hit area via an invisible expander.
   0.3 scoped this to .btn-ico.btn-sm only, so every TEXT small button — Save
   name, Update PIN, Save rate, Start/Pause on the study timer, and the action
   in all 27 empty states — was 36px on its short axis with nothing to make up
   the difference. The text case only ever needs the vertical 8px, and taking
   it vertically means the expander cannot reach a neighbour in a .u-wrap row;
   the icon case still needs both axes and keeps its own rule below. */
.btn-sm::after{content:"";position:absolute;inset-inline:0;
  top:50%;height:var(--p-tap);transform:translateY(-50%)}
.btn-ico.btn-sm::after{content:"";position:absolute;
  width:var(--p-tap);height:var(--p-tap);inset-inline:auto;
  top:50%;inset-inline-start:50%;transform:translate(-50%,-50%)}
[dir="rtl"] .btn-ico.btn-sm::after{transform:translate(50%,-50%)}

.fab{position:fixed;z-index:var(--p-z-fab);
  inset-inline-end:max(var(--p-sp-5),var(--p-safe-r));
  bottom:calc(var(--p-safe-b) + var(--p-tab-h) + var(--p-sp-5));
  width:var(--p-fab);height:var(--p-fab);border-radius:var(--p-r-full);
  background:var(--accent);color:var(--on-accent);
  display:grid;place-items:center;
  box-shadow:0 8px 24px -6px rgba(var(--amb),.5), 0 2px 6px rgba(0,0,0,.24);
  transition:transform var(--p-d-press) var(--p-e-spring)}
.fab svg{width:24px;height:24px;stroke:currentColor;fill:none;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.fab:active{transform:scale(.9)}

/* ═══════════════════════════════════════════════════════════════════════════
   FORM CONTROLS
   16px is not a style choice: below it iOS zooms the viewport on focus and
   never zooms back, which reads as the page breaking.
   ═══════════════════════════════════════════════════════════════════════════ */
.field{display:flex;flex-direction:column;gap:var(--p-sp-2);margin-bottom:var(--p-sp-4)}
.field:last-child{margin-bottom:0}
.inp{width:100%;min-height:var(--p-tap);
  padding:var(--p-sp-3) var(--p-sp-4);
  background:var(--surface-2);color:var(--text);
  border:0;border-radius:var(--c-inp-r);
  box-shadow:var(--c-elev-flat);
  font-size:var(--p-fs-3);font-weight:var(--p-fw-1);line-height:var(--p-lh-snug);
  -webkit-appearance:none;appearance:none;
  transition:box-shadow var(--p-d-base) var(--p-e-out),
             background-color var(--p-d-base) var(--p-e-out)}
.inp::placeholder{color:var(--text-3);opacity:1}
.inp:focus{outline:none;background:var(--surface);
  box-shadow:inset 0 0 0 1.5px var(--accent)}
textarea.inp{min-height:6rem;resize:vertical;line-height:var(--p-lh-body)}
/* The disclosure chevron on a <select>.
   A data: URI cannot read a custom property — var() does not substitute inside
   a url() token — so this glyph is the one place a literal colour has to be
   written. 0.3 wrote one: #8b95a4, which is basalt DARK's --text-3, applied to
   all twelve variants. Measured against --surface-2 it scores 2.54–2.65:1 in
   the six light variants, under the 3:1 that a non-text UI glyph needs.
   Keyed on data-mode instead, which is the smallest split that can be correct:
   the arrow is a neutral, and every theme's --surface-2 sits in the same band
   within a mode. Both values are measured — see the 0.4 audit. */
select.inp{padding-inline-end:var(--p-sp-10);
  background-repeat:no-repeat;background-position:right var(--p-sp-4) center;
  background-size:16px}
[data-mode="dark"] select.inp{
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238b95a4' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6.5 8 10.5 12 6.5'/%3E%3C/svg%3E")}
[data-mode="light"] select.inp{
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%235c6675' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6.5 8 10.5 12 6.5'/%3E%3C/svg%3E")}
[dir="rtl"] select.inp{background-position:left var(--p-sp-4) center;
  padding-inline-end:var(--p-sp-4);padding-inline-start:var(--p-sp-10)}
.inp[type="color"]{padding:var(--p-sp-1)}
.inp[type="file"]{padding:var(--p-sp-3) var(--p-sp-4);line-height:1.2}
.inp[type="date"],.inp[type="datetime-local"],.inp[type="month"],.inp[type="time"]{
  text-align:start}
.inp.is-bad{box-shadow:inset 0 0 0 1.5px var(--bad)}
.hint{font-size:var(--p-fs-2);color:var(--text-3);line-height:var(--p-lh-snug)}
.err{font-size:var(--p-fs-2);color:var(--bad);line-height:var(--p-lh-snug)}
.pair{display:grid;grid-template-columns:1fr 1fr;gap:var(--p-sp-3)}
/* Borderless field for the note editor, where chrome would fight the writing. */
.inp-bare{background:transparent;box-shadow:none;padding:0;min-height:0;border-radius:0}
.inp-bare:focus{background:transparent;box-shadow:none}

/* Search field. Fully-round rather than the standard field radius, because
   that shape is how iOS signals "this filters, it does not store". The glyph
   is a sibling, not a background-image, so it inherits colour on focus and
   flips side under RTL without a second rule. */
.srch{position:relative;display:flex;align-items:center}
.srch svg{position:absolute;inset-inline-start:var(--p-sp-3);width:17px;height:17px;
  stroke:var(--text-3);fill:none;stroke-width:1.75;
  stroke-linecap:round;stroke-linejoin:round;pointer-events:none;
  transition:stroke var(--p-d-base) var(--p-e-out)}
.srch .inp{border-radius:999px;padding-inline-start:var(--p-sp-9)}
.srch:focus-within svg{stroke:var(--accent)}

/* Segmented control — light/dark/system, filters, tab switches. */
.segctl{display:flex;gap:var(--p-sp-1);padding:var(--p-sp-1);background:var(--surface-2);
  border-radius:var(--p-r-2);box-shadow:var(--c-elev-flat)}
.segctl > *{position:relative;flex:1;min-height:38px;
  border-radius:calc(var(--p-r-2) - var(--p-sp-1));
  display:inline-flex;align-items:center;justify-content:center;gap:var(--p-sp-2);
  font-size:var(--p-fs-2);font-weight:var(--p-fw-3);color:var(--text-2);
  white-space:nowrap;
  transition:background-color var(--p-d-base) var(--p-e-out),
             color var(--p-d-base) var(--p-e-out)}
/* 38px is the right visual height for a segmented control — it is the third
   place 0.3 sized the box correctly and then never made up the difference to
   the 44px floor. Vertical-only again: the segments sit 4px apart, so an
   expander with any inline growth would steal its neighbour's taps. */
.segctl > *::after{content:"";position:absolute;inset-inline:0;
  top:50%;height:var(--p-tap);transform:translateY(-50%)}
.segctl input{position:absolute;opacity:0;width:0;height:0}
.segctl .is-on,.segctl label:has(input:checked){background:var(--surface);color:var(--text)}
.segctl label:has(input:focus-visible){outline:2px solid var(--accent);outline-offset:2px}

/* Choice chips — filters and pickers. Checked state is driven by :has() so a
   stored value renders selected on load with no JS. */
.chips{display:flex;flex-wrap:wrap;gap:var(--p-sp-2)}
.chip{position:relative;display:inline-flex;align-items:center;gap:var(--p-sp-2);
  min-height:var(--p-tap);padding:0 var(--p-sp-4);border-radius:var(--p-r-full);
  background:var(--surface-2);box-shadow:var(--c-elev-flat);
  font-size:var(--p-fs-2);font-weight:var(--p-fw-2);color:var(--text-2);
  white-space:nowrap;
  transition:background-color var(--p-d-base) var(--p-e-out),
             color var(--p-d-base) var(--p-e-out),
             transform var(--p-d-press) var(--p-e-out)}
.chip:active{transform:scale(.96)}
.chip input{position:absolute;opacity:0;width:0;height:0}
.chip.is-on,.chip:has(input:checked){background:var(--accent);color:var(--on-accent);box-shadow:none}
.chip:has(input:focus-visible){outline:2px solid var(--accent);outline-offset:2px}
/* A chip can carry a glyph — the quick-add type picker, the habit icon picker.
   Same well as .btn svg, on the sprite's 1.75 stroke. */
.chip svg{width:18px;height:18px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
.chip-row{display:flex;gap:var(--p-sp-2);overflow-x:auto;scrollbar-width:none;
  -webkit-overflow-scrolling:touch;scroll-snap-type:x proximity;min-width:0}
.chip-row::-webkit-scrollbar{display:none}
.chip-row > .chip{flex:none;scroll-snap-align:start}

/* 1–5 rating scale, for the check-in metrics. */
.scale{display:flex;gap:var(--p-sp-2)}
.scale label{flex:1;position:relative;cursor:pointer}
.scale input{position:absolute;opacity:0;width:0;height:0}
.scale span{display:grid;place-items:center;min-height:var(--p-tap);
  border-radius:var(--p-r-2);background:var(--surface-2);box-shadow:var(--c-elev-flat);
  font-size:var(--p-fs-3);font-weight:var(--p-fw-3);color:var(--text-2);
  font-variant-numeric:tabular-nums;
  transition:background-color var(--p-d-base) var(--p-e-out),
             color var(--p-d-base) var(--p-e-out),
             transform var(--p-d-press) var(--p-e-out)}
.scale label:active span{transform:scale(.94)}
.scale label:has(input:checked) span{background:var(--accent);color:var(--on-accent);box-shadow:none}
.scale label:has(input:focus-visible) span{outline:2px solid var(--accent);outline-offset:2px}
.scale-ends{display:flex;justify-content:space-between;margin-top:var(--p-sp-2);
  font-size:var(--p-fs-1);font-weight:var(--p-fw-3);letter-spacing:var(--p-tr-wide);
  text-transform:uppercase;color:var(--text-3)}
[dir="rtl"] .scale-ends{letter-spacing:0;text-transform:none}

/* Mood picker. The face is the content being chosen, not an interface icon —
   this is the one place a pictograph is the data itself. */
.mood{display:flex;justify-content:center;gap:var(--p-sp-3)}
.mood label{position:relative;cursor:pointer}
.mood input{position:absolute;opacity:0;width:0;height:0}
.mood span{display:grid;place-items:center;
  width:var(--p-tap);height:var(--p-tap);border-radius:var(--p-r-full);
  font-size:var(--p-fs-5);line-height:1;opacity:.4;
  transition:opacity var(--p-d-base) var(--p-e-out),
             background-color var(--p-d-base) var(--p-e-out),
             transform var(--p-d-press) var(--p-e-spring)}
.mood label:has(input:checked) span{opacity:1;background:var(--surface-2);transform:scale(1.08)}
.mood label:active span{transform:scale(.94)}
.mood label:has(input:focus-visible) span{outline:2px solid var(--accent);outline-offset:2px}

/* ═══════════════════════════════════════════════════════════════════════════
   SHEET — every modal in the app is a bottom sheet
   Rises from the bottom edge on the iOS drawer curve and exits the same way.
   Scrolling lives on the body, not the sheet, so a select opened inside is
   never clipped by the sheet's own overflow.
   ═══════════════════════════════════════════════════════════════════════════ */
.scrim{position:fixed;inset:0;z-index:var(--p-z-scrim);background:var(--scrim);
  opacity:0;pointer-events:none;
  -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);
  transition:opacity var(--p-d-sheet) var(--p-e-out)}
.scrim.is-open{opacity:1;pointer-events:auto}
.sheet{position:fixed;inset-inline:0;bottom:0;z-index:var(--p-z-sheet);
  max-width:560px;margin:0 auto;
  max-height:88vh;max-height:88dvh;
  display:flex;flex-direction:column;
  background:var(--c-surface-bg);
  -webkit-backdrop-filter:var(--c-surface-blur);backdrop-filter:var(--c-surface-blur);
  border-radius:var(--c-sheet-r) var(--c-sheet-r) 0 0;
  box-shadow:var(--c-elev-sheet);
  padding-bottom:var(--p-safe-b);
  transform:translate3d(0,100%,0);
  transition:transform var(--p-d-sheet) var(--p-e-sheet);
  will-change:transform}
.sheet.is-open{transform:translate3d(0,0,0)}
/* v0.5 fix: any sheet whose body is wrapped in a <form> (so .sheet-f's
   submit button can reach the fields — most sheets in the app) puts that
   <form> between .sheet and .sheet-b in the DOM. .sheet-b's flex:1 1 auto +
   min-height:0 only make it shrink-to-scroll when its OWN parent is the flex
   column — with an unstyled <form> in between, the form has no flex-shrink
   floor of its own, refuses to shrink below its content's natural height
   (min-height:auto is the default), and just overflows .sheet's max-height
   with nothing to scroll to reach the rest. Invisible on short forms;
   confirmed as a real cut-off bug on Study's "New subject" sheet (7 stacked
   day fields, tall enough to be the first sheet in the app to hit it) while
   investigating reported mobile scrolling problems there. Fixing at the
   component level, not just on that one sheet, since every form-wrapped
   sheet in the app has the same latent structural issue. */
.sheet > form{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
.grab{flex:none;width:36px;height:5px;border-radius:var(--p-r-full);
  background:var(--ring);margin:var(--p-sp-2) auto var(--p-sp-1)}
.sheet-h{flex:none;display:flex;align-items:center;justify-content:space-between;
  gap:var(--p-sp-2);padding:var(--p-sp-3) var(--p-sp-5)}
.sheet-t{font-size:var(--p-fs-4);font-weight:var(--p-fw-3);letter-spacing:var(--p-tr-snug)}
[dir="rtl"] .sheet-t{letter-spacing:0}
.sheet-b{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;padding:0 var(--p-sp-5) var(--p-sp-5)}
.sheet-f{flex:none;display:flex;flex-direction:column;gap:var(--p-sp-2);
  padding:var(--p-sp-3) var(--p-sp-5) var(--p-sp-5)}
.blk{margin-bottom:var(--p-sp-6)}
.blk:last-child{margin-bottom:0}

/* Settings rows inside a sheet. */
.srow{position:relative;display:flex;align-items:center;justify-content:space-between;
  gap:var(--p-sp-3);min-height:var(--p-tap);width:100%;text-align:start}
.srow + .srow::before{content:"";position:absolute;top:0;inset-inline:0;
  height:var(--p-hair);background:var(--hairline)}
.srow-t{font-size:var(--p-fs-3);font-weight:var(--p-fw-2)}
.srow-v{font-size:var(--p-fs-2);color:var(--text-3);font-weight:var(--p-fw-2)}

/* Theme swatches. Each reads its own theme's real colours at runtime, so the
   preview can never drift from the theme it applies. */
.themes{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--p-sp-3)}
.sw{position:relative;display:flex;flex-direction:column;gap:var(--p-sp-3);
  align-items:flex-start;min-height:var(--p-tap);
  padding:var(--p-sp-3);border-radius:var(--p-r-2);
  background:var(--surface-2);box-shadow:var(--c-elev-flat);
  transition:box-shadow var(--p-d-base) var(--p-e-out),
             transform var(--p-d-press) var(--p-e-out)}
.sw:active{transform:scale(.96)}
.sw.is-on{box-shadow:inset 0 0 0 1.5px var(--accent)}
.sw-d{display:flex;gap:var(--p-sp-1)}
.sw-d i{width:11px;height:11px;border-radius:var(--p-r-full)}
.sw-n{font-size:var(--p-fs-1);font-weight:var(--p-fw-3);letter-spacing:var(--p-tr-wide);
  text-transform:uppercase;color:var(--text-2);line-height:1}
[dir="rtl"] .sw-n{letter-spacing:0;text-transform:none}
.sw.is-on .sw-n{color:var(--text)}

/* ═══════════════════════════════════════════════════════════════════════════
   MENU — anchored dropdown, scaling from its trigger edge
   ═══════════════════════════════════════════════════════════════════════════ */
.menu{position:absolute;top:calc(100% + var(--p-sp-2));inset-inline-end:0;
  min-width:12.5rem;max-width:min(20rem,calc(100vw - var(--p-sp-8)));
  max-height:min(60dvh,26rem);overflow-y:auto;overscroll-behavior:contain;
  background:var(--c-surface-bg);border-radius:var(--p-r-2);
  -webkit-backdrop-filter:var(--c-surface-blur);backdrop-filter:var(--c-surface-blur);
  box-shadow:var(--c-elev-lift);padding:var(--p-sp-1);z-index:var(--p-z-tab);
  opacity:0;pointer-events:none;
  transform:translateY(-6px) scale(.97);transform-origin:top center;
  transition:opacity var(--p-d-base) var(--p-e-out),
             transform var(--p-d-base) var(--p-e-out)}
.menu.is-open{opacity:1;pointer-events:auto;transform:none}
.menu-i{display:flex;align-items:center;gap:var(--p-sp-3);width:100%;text-align:start;
  min-height:var(--p-tap);padding:0 var(--p-sp-3);border-radius:var(--p-r-1);
  font-size:var(--p-fs-3);font-weight:var(--p-fw-2);color:var(--text-2);
  transition:background-color var(--p-d-press) var(--p-e-out)}
.menu-i:active{background:var(--press);color:var(--text)}
.menu-i svg{width:18px;height:18px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}

/* ═══════════════════════════════════════════════════════════════════════════
   TOAST — enters and exits through the same edge
   A transition rather than a keyframe: toasts stack quickly, and a transition
   retargets mid-flight where a keyframe restarts from zero.
   ═══════════════════════════════════════════════════════════════════════════ */
.toasts{position:fixed;z-index:var(--p-z-toast);
  inset-inline:var(--p-sp-4);
  bottom:calc(var(--p-safe-b) + var(--p-tab-h) + var(--p-sp-3));
  max-width:560px;margin:0 auto;
  display:flex;flex-direction:column;gap:var(--p-sp-2);pointer-events:none}
.toast{display:flex;align-items:center;gap:var(--p-sp-3);
  padding:var(--p-sp-3) var(--p-sp-4);border-radius:var(--p-r-2);
  background:var(--c-surface-bg);-webkit-backdrop-filter:var(--c-surface-blur);backdrop-filter:var(--c-surface-blur);
  box-shadow:var(--c-elev-lift);
  font-size:var(--p-fs-3);font-weight:var(--p-fw-2);pointer-events:auto;
  opacity:0;transform:translateY(14px) scale(.97);
  transition:opacity var(--p-d-base) var(--p-e-out),
             transform var(--p-d-base) var(--p-e-out)}
.toast.is-in{opacity:1;transform:none}
.toast.is-out{opacity:0;transform:translateY(8px) scale(.98);
  transition-duration:var(--p-d-press)}
.toast svg{width:20px;height:20px;flex:none;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
.toast.is-ok svg{color:var(--ok)}
.toast.is-bad svg{color:var(--bad)}
.toast.is-info svg{color:var(--accent)}

/* ═══════════════════════════════════════════════════════════════════════════
   CALENDAR
   ═══════════════════════════════════════════════════════════════════════════ */
.cal{display:grid;grid-template-columns:repeat(7,1fr);gap:var(--p-sp-1)}
.cal-h{text-align:center;font-size:var(--p-fs-1);font-weight:var(--p-fw-3);
  letter-spacing:var(--p-tr-wide);text-transform:uppercase;color:var(--text-3);
  padding:var(--p-sp-1) 0}
[dir="rtl"] .cal-h{letter-spacing:0;text-transform:none}
.cal-d{position:relative;aspect-ratio:1;min-height:2.5rem;
  display:grid;place-items:center;border-radius:var(--p-r-1);
  font-size:var(--p-fs-3);color:var(--text-2);font-variant-numeric:tabular-nums;
  transition:background-color var(--p-d-press) var(--p-e-out),
             color var(--p-d-press) var(--p-e-out),
             transform var(--p-d-press) var(--p-e-out)}
.cal-d:active{background:var(--press);transform:scale(.9)}
.cal-d.is-today{background:var(--accent);color:var(--on-accent);font-weight:var(--p-fw-3)}
.cal-d.has{font-weight:var(--p-fw-3);color:var(--text)}
.cal-dot{position:absolute;bottom:5px;inset-inline-start:50%;
  transform:translateX(-50%);width:4px;height:4px;border-radius:var(--p-r-full);
  background:var(--accent)}
[dir="rtl"] .cal-dot{transform:translateX(50%)}
.cal-d.is-today .cal-dot{background:var(--on-accent)}

/* ═══════════════════════════════════════════════════════════════════════════
   TIMER
   ═══════════════════════════════════════════════════════════════════════════ */
.timer{font-size:var(--p-fs-7);font-weight:var(--p-fw-4);
  letter-spacing:var(--p-tr-tight);line-height:1;text-align:center;
  font-variant-numeric:tabular-nums}
[dir="rtl"] .timer{letter-spacing:0}

/* ═══════════════════════════════════════════════════════════════════════════
   ICON TILE — a glyph well at row scale
   ═══════════════════════════════════════════════════════════════════════════ */
.tile{flex:none;width:var(--p-sp-8);height:var(--p-sp-8);border-radius:var(--p-r-2);
  display:grid;place-items:center;background:var(--surface-2);color:var(--text-2)}
.tile svg{width:17px;height:17px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
.tile.is-accent{background:rgba(var(--amb),.14);color:var(--accent)}
.tile.is-ok{color:var(--ok)} .tile.is-bad{color:var(--bad)}
/* Same colour-only treatment as .tile.is-ok, but keyed to the item's own data
   colour — the one kclass() picked (--bar-c) rather than a stored hex. */
.tile.is-k{color:var(--bar-c,var(--accent))}
/* A tile holding a mood face rather than a glyph. The face is data, so it gets
   no surface behind it — otherwise it reads as a button. */
.tile.is-face{background:none;font-size:var(--p-fs-4);line-height:1}

/* ═══════════════════════════════════════════════════════════════════════════
   PROSE — the only place long-form copy lives
   ═══════════════════════════════════════════════════════════════════════════ */
.prose{font-size:var(--p-fs-3);line-height:var(--p-lh-loose);color:var(--text-2);
  white-space:pre-wrap;overflow-wrap:anywhere;max-width:68ch}
.prose-h{font-size:var(--p-fs-4);font-weight:var(--p-fw-3);color:var(--text);
  letter-spacing:var(--p-tr-snug);line-height:var(--p-lh-snug);margin-bottom:var(--p-sp-3)}
[dir="rtl"] .prose-h{letter-spacing:0}

/* ═══════════════════════════════════════════════════════════════════════════
   AUTH / INSTALL — pre-shell screens
   ═══════════════════════════════════════════════════════════════════════════ */
.auth{min-height:100vh;min-height:100dvh;display:grid;place-items:center;
  padding:calc(var(--p-safe-t) + var(--p-sp-5)) var(--p-sp-5)
          calc(var(--p-safe-b) + var(--p-sp-5))}
.auth-c{width:100%;max-width:25rem;background:var(--c-surface-bg);
  -webkit-backdrop-filter:var(--c-surface-blur);backdrop-filter:var(--c-surface-blur);
  border-radius:var(--p-r-4);box-shadow:var(--c-elev-lift);
  padding:var(--p-sp-8) var(--p-sp-6)}
.auth-h{display:flex;flex-direction:column;align-items:center;gap:var(--p-sp-3);
  text-align:center;margin-bottom:var(--p-sp-8)}
.auth-mark{width:52px;height:52px;border-radius:var(--p-r-3);background:var(--accent);
  color:var(--on-accent);display:grid;place-items:center}
.auth-mark svg{width:26px;height:26px;stroke:currentColor;fill:none;
  stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
.pin{font-size:var(--p-fs-5);letter-spacing:.5em;text-align:center;
  text-indent:.5em;font-variant-numeric:tabular-nums}
.note{padding:var(--p-sp-4);border-radius:var(--p-r-2);background:var(--surface-2);
  box-shadow:var(--c-elev-flat);font-size:var(--p-fs-2);color:var(--text-2);
  line-height:var(--p-lh-body)}
.note.is-bad{color:var(--bad)}

/* ═══════════════════════════════════════════════════════════════════════════
   ENTRY CHOREOGRAPHY
   Sections rise on first paint. Decorative only — it never gates interaction.

   This is deliberately short, and the reason is the architecture: the app is
   server-rendered, so every tab tap is a full page load and this runs again.
   Tapping between tabs is a hundred-times-a-day action, and DESIGN-AUDIT.md
   rejected animating tab switches for exactly that reason — a 7-step, 50ms
   stagger took 620ms to settle and quietly reintroduced it.

   The stagger caps at three slots: past the third element nobody perceives
   ordering, they only perceive lag. Worst case is now 200ms + 240ms = 440ms,
   and the element that matters most — the answer card — is always slot one
   with no delay at all.
   ═══════════════════════════════════════════════════════════════════════════ */
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.in{animation:rise 240ms var(--p-e-out) both}
.in:nth-child(2){animation-delay:40ms}
.in:nth-child(3){animation-delay:80ms}
.in:nth-child(n+4){animation-delay:120ms}

/* ═══════════════════════════════════════════════════════════════════════════
   DATA VALUES WITHOUT INLINE STYLES
   Bars and meters carry values only PHP knows. Views must not set them with a
   style attribute, so the 0–100 range is quantised into classes: the view
   rounds to the nearest 5 and emits `.v-65`. Twenty-one static classes that,
   unlike an inline style, also survive a strict Content-Security-Policy.
   --v drives scaleX (0–1); --h drives chart bar height (%).
   ═══════════════════════════════════════════════════════════════════════════ */
.v-0{--v:0;--h:0%}      .v-5{--v:.05;--h:5%}   .v-10{--v:.10;--h:10%}
.v-15{--v:.15;--h:15%}  .v-20{--v:.20;--h:20%} .v-25{--v:.25;--h:25%}
.v-30{--v:.30;--h:30%}  .v-35{--v:.35;--h:35%} .v-40{--v:.40;--h:40%}
.v-45{--v:.45;--h:45%}  .v-50{--v:.50;--h:50%} .v-55{--v:.55;--h:55%}
.v-60{--v:.60;--h:60%}  .v-65{--v:.65;--h:65%} .v-70{--v:.70;--h:70%}
.v-75{--v:.75;--h:75%}  .v-80{--v:.80;--h:80%} .v-85{--v:.85;--h:85%}
.v-90{--v:.90;--h:90%}  .v-95{--v:.95;--h:95%} .v-100{--v:1;--h:100%}

/* Per-item colour is an index into the theme's status palette, never a stored
   hex — a hex cannot stay on-theme across twelve variants. */
.k-a{--seg-c:var(--accent);--bar-c:var(--accent)}
.k-ok{--seg-c:var(--ok);--bar-c:var(--ok)}
.k-warn{--seg-c:var(--warn);--bar-c:var(--warn)}
.k-bad{--seg-c:var(--bad);--bar-c:var(--bad)}
/* Paint a glyph in whichever palette slot its .k-* ancestor selected. */
.t-k{color:var(--bar-c,var(--accent))}

/* ═══════════════════════════════════════════════════════════════════════════
   UTILITIES — deliberately few. Anything recurring becomes a component.
   ═══════════════════════════════════════════════════════════════════════════ */
.u-stack{display:flex;flex-direction:column;gap:var(--p-sp-3)}
.u-stack-sm{display:flex;flex-direction:column;gap:var(--p-sp-2)}
.u-row{display:flex;align-items:center;gap:var(--p-sp-3)}
.u-wrap{display:flex;align-items:center;gap:var(--p-sp-2);flex-wrap:wrap}
.u-spread{display:flex;align-items:center;justify-content:space-between;gap:var(--p-sp-3)}
.u-grow{flex:1;min-width:0}
.u-pad{padding:var(--p-sp-4)}
.u-center{text-align:center}
.u-scroll-x{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.u-scroll-x::-webkit-scrollbar{display:none}
.u-mt{margin-top:var(--p-sp-4)}
.u-mb{margin-bottom:var(--p-sp-4)}

/* Declared last so it beats a component's own display on equal specificity —
   which is why hiding never needs !important. */
[hidden]{display:none}

/* ═══════════════════════════════════════════════════════════════════════════
   REDUCED MOTION — the only !important in this file
   Reduced motion means gentler, not absent: colour and opacity still carry
   meaning, so they survive. Everything that moves is removed.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion:reduce){
  /* Kill the keyframes outright — those are all movement. */
  *,*::before,*::after{
    animation:none!important;
    scroll-behavior:auto!important}

  /* But NOT `transition-duration: .01ms` on everything, which is what the
     usual boilerplate does and what 0.3 shipped for a while. That also
     flattens the colour and opacity transitions, and those are comprehension
     aids, not decoration: the checkbox filling in, the chip turning accent,
     the toast fading up. Reduced motion means gentler, not absent.

     So: transitions keep their duration, and the things that would MOVE are
     neutralised individually instead. */
  .btn:active,.chip:active,.check:active,.fab:active,.tab:active svg,
  .row:active,.sw:active,.menu-i:active,.answer-go:active,
  .scale label:active span,.mood label:active span,.cal-d:active{
    transform:none!important}
  .sheet{transition:opacity var(--p-d-base) var(--p-e-out)!important;
    transform:none!important;opacity:0;pointer-events:none}
  .sheet.is-open{opacity:1;pointer-events:auto}
  .toast{transform:none!important}
  /* The completion wipe stays: it is how you know the tap registered. */
}

/* apple-design §14: a request for more contrast means near-solid surfaces with
   a defined edge, not merely darker text. */
@media (prefers-contrast:more){
  :root{--hairline:var(--text-3);--ring:var(--text-3)}
  .topbar,.tabbar{-webkit-backdrop-filter:none;backdrop-filter:none;background:var(--surface)}
}
