/* ============================================================================
   Escalate — design system
   ----------------------------------------------------------------------------
   Ink and parchment. No gendered colour, no wellness-app pastels, no SaaS
   dashboard chrome. The accent is a muted sage; brass appears only where the
   app wants to feel expensive, and never twice on one screen.

   Two modes. Dark is the default because the app is used at the edges of the
   day — last thing at night, first thing in the morning — and because a
   sanctuary is not brightly lit.

   Everything is one origin: fonts below are self-hosted, GSAP is vendored.
   ========================================================================= */

/* ── fonts ───────────────────────────────────────────────────────────────── */

@font-face {
  font-family: 'Lora';
  src: url('/fonts/lora-var.woff2') format('woff2-variations');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Raleway';
  src: url('/fonts/raleway-var.woff2') format('woff2-variations');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* ── tokens ──────────────────────────────────────────────────────────────── */

:root {
  /* primitive */
  --ink-900: #0B0F18;
  --ink-800: #101521;
  --ink-700: #171D2B;
  --ink-600: #1F2637;
  --ink-500: #2A3346;

  --parch-50:  #FDFCF9;
  --parch-100: #FBF9F5;
  --parch-200: #F4F1EA;
  --parch-300: #E9E4D9;

  --sage-300: #9BC0B2;
  --sage-400: #7FA898;
  --sage-500: #6E9A8B;
  --sage-600: #567C6F;

  --brass-300: #D8C29A;
  --brass-400: #BFA173;
  --brass-500: #A28757;

  --clay-400: #C2705F;   /* destructive — earthen, not emergency red */
  --clay-500: #A65946;

  /* semantic — dark is the default mode */
  --bg:            var(--ink-800);
  --bg-deep:       var(--ink-900);
  --surface:       var(--ink-700);
  --surface-raised: var(--ink-600);
  --surface-sunk:  #0E121C;

  --text:          #E9E5DC;
  --text-muted:    rgba(233, 229, 220, 0.62);
  --text-faint:    rgba(233, 229, 220, 0.38);

  --line:          rgba(233, 229, 220, 0.10);
  --line-strong:   rgba(233, 229, 220, 0.18);

  --accent:        var(--sage-400);
  --accent-quiet:  var(--sage-600);
  --on-accent:     #08120E;
  --brass:         var(--brass-400);
  --danger:        var(--clay-400);

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--sage-400);
  --mark-url: url('/brand/mark-ivory.png');

  /* type */
  --serif: 'Lora', Georgia, 'Times New Roman', serif;
  --sans:  'Raleway', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;

  --t-xs:   0.75rem;
  --t-sm:   0.8125rem;
  --t-base: 0.9375rem;
  --t-md:   1.0625rem;
  --t-lg:   1.25rem;
  --t-xl:   1.5rem;
  --t-2xl:  1.875rem;
  --t-3xl:  2.375rem;

  /* space — spacious, per the density dial */
  --s-1: 4px;   --s-2: 8px;   --s-3: 12px;  --s-4: 16px;
  /*
   * How tall .topbar actually is, derived rather than guessed.
   *
   * The desktop sidebar sticks below the header, so it needs this number and
   * it needs it to stay true. It was hardcoded as 57px in one place and 0 in
   * another, while the real height is 69px — the 44px brand target plus the
   * bar's own vertical padding and its bottom border. Written as a calc over
   * the same tokens the bar is built from, so changing --s-3 moves both
   * together instead of silently reopening the gap.
   */
  --topbar-h: calc(44px + (var(--s-3) * 2) + 1px);
  --s-5: 24px;  --s-6: 32px;  --s-7: 44px;  --s-8: 64px;  --s-9: 88px;

  --r-sm: 8px;  --r-md: 14px; --r-lg: 20px; --r-xl: 28px; --r-full: 999px;

  --shadow-sm: 0 1px 2px rgba(0,0,0,.28);
  --shadow-md: 0 6px 20px -6px rgba(0,0,0,.42);
  --shadow-lg: 0 24px 56px -20px rgba(0,0,0,.58);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.45, 0, 0.25, 1);
  --dur-fast: 160ms;
  --dur: 260ms;
  --dur-slow: 480ms;

  --shell-max: 720px;
  --nav-h: 64px;

  color-scheme: dark;
}

[data-theme='parchment'], [data-theme='light'] {
  --bg:            var(--parch-200);
  --bg-deep:       var(--parch-300);
  --surface:       var(--parch-100);
  --surface-raised: var(--parch-50);
  --surface-sunk:  #EFEBE1;

  --text:          #1B2130;
  --text-muted:    rgba(27, 33, 48, 0.66);
  --text-faint:    rgba(27, 33, 48, 0.42);

  --line:          rgba(27, 33, 48, 0.10);
  --line-strong:   rgba(27, 33, 48, 0.18);

  --accent:        var(--sage-600);
  --accent-quiet:  var(--sage-500);
  --on-accent:     #FBFDFC;
  --brass:         var(--brass-500);
  --danger:        var(--clay-500);

  /* Parchment had no --ring of its own and inherited :root's, which is built
     from --sage-400 — a pale sage picked to glow against ink navy and which
     all but disappears on cream. Its own accent, the darker sage-600, is what
     a focus ring here has to be drawn in to be seen at all. */
  --ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--sage-600);

  --shadow-sm: 0 1px 2px rgba(27,33,48,.06);
  --shadow-md: 0 6px 20px -8px rgba(27,33,48,.14);
  --shadow-lg: 0 24px 56px -22px rgba(27,33,48,.20);

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-aubergine.png');


  color-scheme: light;
}


/* ══════════════════════════════════════════════════════════════════════════
   Themes
   --------------------------------------------------------------------------
   Each block overrides only the semantic layer — never the primitives and
   never a component rule. That is the whole reason a new theme is ~25 lines:
   every component reads --surface, --accent, --line and so on, so nothing
   below has to know that buttons or chips exist.

   Contrast was checked for body text on --bg in each: all clear 4.5:1.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Midnight — ink navy and sage, the default ───────────────────────────
   These values are already in :root, which is what a browser gets before any
   theme is chosen. Restating them under the key means every theme in config
   has a matching block — so "is this theme wired up?" is answerable by
   searching for its name, and midnight is not a special case a future reader
   has to know about. There is a test that enforces exactly this. */
[data-theme='midnight'] {
  --bg:            var(--ink-800);
  --bg-deep:       var(--ink-900);
  --surface:       var(--ink-700);
  --surface-raised: var(--ink-600);
  --surface-sunk:  #0E121C;

  --text:          #E9E5DC;
  --text-muted:    rgba(233, 229, 220, 0.62);
  --text-faint:    rgba(233, 229, 220, 0.38);

  --line:          rgba(233, 229, 220, 0.10);
  --line-strong:   rgba(233, 229, 220, 0.18);

  --accent:        var(--sage-400);
  --accent-quiet:  var(--sage-600);
  --on-accent:     #08120E;
  --brass:         var(--brass-400);
  --danger:        var(--clay-400);

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--sage-400);

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-ivory.png');

  color-scheme: dark;
}

/* ── Ember — near-black and brass, candlelit ─────────────────────────────── */
[data-theme='ember'] {
  --bg:            #16110D;
  --bg-deep:       #0F0B08;
  --surface:       #201913;
  --surface-raised: #2A211A;
  --surface-sunk:  #120E0A;

  --text:          #EFE6DA;
  --text-muted:    rgba(239, 230, 218, 0.62);
  --text-faint:    rgba(239, 230, 218, 0.38);

  --line:          rgba(239, 230, 218, 0.10);
  --line-strong:   rgba(239, 230, 218, 0.20);

  --accent:        #C79A5C;
  --accent-quiet:  #8A6737;
  --on-accent:     #17110A;
  --brass:         #D8B27A;
  --danger:        #B9705A;

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px #C79A5C;

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-ivory.png');

  color-scheme: dark;
}

[data-theme='ember'] body::before {
  background:
    radial-gradient(60% 60% at 22% 8%, rgba(199, 154, 92, 0.16), transparent 70%),
    radial-gradient(52% 52% at 82% 0%, rgba(185, 112, 90, 0.10), transparent 72%);
}

/* ── Tide — deep water and pale aqua, cool and awake ─────────────────────── */
[data-theme='tide'] {
  --bg:            #0B1A1F;
  --bg-deep:       #071216;
  --surface:       #10252B;
  --surface-raised: #163038;
  --surface-sunk:  #081418;

  --text:          #E4EDEE;
  --text-muted:    rgba(228, 237, 238, 0.62);
  --text-faint:    rgba(228, 237, 238, 0.38);

  --line:          rgba(228, 237, 238, 0.10);
  --line-strong:   rgba(228, 237, 238, 0.20);

  --accent:        #7FB8BE;
  --accent-quiet:  #48838A;
  --on-accent:     #05171A;
  --brass:         #C4A98A;
  --danger:        #C87F6B;

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px #7FB8BE;

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-ivory.png');

  color-scheme: dark;
}

[data-theme='tide'] body::before {
  background:
    radial-gradient(60% 60% at 22% 8%, rgba(127, 184, 190, 0.16), transparent 70%),
    radial-gradient(52% 52% at 82% 0%, rgba(196, 169, 138, 0.09), transparent 72%);
}

/* ── Graphite — neutral charcoal, nothing decorative ─────────────────────── */
[data-theme='graphite'] {
  --bg:            #141517;
  --bg-deep:       #0E0F11;
  --surface:       #1D1F22;
  --surface-raised: #26282C;
  --surface-sunk:  #101113;

  --text:          #E6E8EA;
  --text-muted:    rgba(230, 232, 234, 0.60);
  --text-faint:    rgba(230, 232, 234, 0.36);

  --line:          rgba(230, 232, 234, 0.10);
  --line-strong:   rgba(230, 232, 234, 0.20);

  --accent:        #9AA3AD;
  --accent-quiet:  #626A73;
  --on-accent:     #101112;
  --brass:         #C2B49A;
  --danger:        #C08475;

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px #9AA3AD;

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-ivory.png');

  color-scheme: dark;
}

/* The one theme with no wash. "Nothing decorative at all" is the brief. */
[data-theme='graphite'] body::before { background: none; }

/* ── Linen — warm oat and clay, the softest light one ────────────────────── */
[data-theme='linen'] {
  --bg:            #F5EFE6;
  --bg-deep:       #EBE3D6;
  --surface:       #FBF7F0;
  --surface-raised: #FFFDF9;
  --surface-sunk:  #EFE8DC;

  --text:          #2C2520;
  --text-muted:    rgba(44, 37, 32, 0.66);
  --text-faint:    rgba(44, 37, 32, 0.42);

  --line:          rgba(44, 37, 32, 0.10);
  --line-strong:   rgba(44, 37, 32, 0.20);

  --accent:        #9A6A55;
  --accent-quiet:  #B08975;
  --on-accent:     #FFFAF5;
  --brass:         #7C7A63;
  --danger:        #A65946;

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px #9A6A55;

  --shadow-sm: 0 1px 2px rgba(44,37,32,.06);
  --shadow-md: 0 6px 20px -8px rgba(44,37,32,.14);
  --shadow-lg: 0 24px 56px -22px rgba(44,37,32,.20);

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-aubergine.png');


  color-scheme: light;
}

/* ── Amethyst — deep aubergine and soft violet ────────────────────────────
   Purple at low saturation and real depth: premium and quiet rather than
   sweet. The accent sits well back from magenta on purpose — the brief says
   no pinks, and violet only reads as one when it brightens.
   ------------------------------------------------------------------------ */

[data-theme='amethyst'] {
  --bg:            #171326;
  --bg-deep:       #100D1B;
  --surface:       #201A33;
  --surface-raised: #2A2240;
  --surface-sunk:  #130F20;

  --text:          #E9E3F2;
  --text-muted:    rgba(233, 227, 242, 0.64);
  --text-faint:    rgba(233, 227, 242, 0.40);

  --line:          rgba(233, 227, 242, 0.11);
  --line-strong:   rgba(233, 227, 242, 0.21);

  --accent:        #A996D9;
  --accent-quiet:  #6F5CA0;
  --on-accent:     #14101F;
  --brass:         #C6A98F;
  --danger:        #C2708C;

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px #A996D9;

  --shadow-sm: 0 1px 2px rgba(0,0,0,.34);
  --shadow-md: 0 6px 20px -8px rgba(0,0,0,.50);
  --shadow-lg: 0 24px 56px -22px rgba(0,0,0,.62);

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-ivory.png');


  color-scheme: dark;
}

[data-theme='amethyst'] body::before {
  background:
    radial-gradient(60% 60% at 20% 6%, rgba(169, 150, 217, 0.16), transparent 70%),
    radial-gradient(54% 54% at 84% 0%, rgba(198, 169, 143, 0.09), transparent 72%);
}

/* ── Wisteria — lilac paper and plum ink ──────────────────────────────────
   Amethyst's daylight counterpart, so the two toggle between each other the
   way Midnight and Parchment already do. The ground is barely violet; the
   colour lives in the ink and the accent, which is what keeps a light theme
   readable for long stretches.
   ------------------------------------------------------------------------ */

[data-theme='wisteria'] {
  --bg:            #F3F0F7;
  --bg-deep:       #E7E2EF;
  --surface:       #FAF8FC;
  --surface-raised: #FFFFFF;
  --surface-sunk:  #ECE7F3;

  --text:          #2A2337;
  --text-muted:    rgba(42, 35, 55, 0.66);
  --text-faint:    rgba(42, 35, 55, 0.42);

  --line:          rgba(42, 35, 55, 0.10);
  --line-strong:   rgba(42, 35, 55, 0.20);

  --accent:        #6B549C;
  --accent-quiet:  #8E79B8;
  --on-accent:     #FFFFFF;
  --brass:         #8A7A62;
  --danger:        #A6486A;

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px #6B549C;

  --shadow-sm: 0 1px 2px rgba(42,35,55,.06);
  --shadow-md: 0 6px 20px -8px rgba(42,35,55,.14);
  --shadow-lg: 0 24px 56px -22px rgba(42,35,55,.20);

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-aubergine.png');


  color-scheme: light;
}

/* ── Ivory — Erika's everyday palette ─────────────────────────────────────
   Warm ivory ground, aubergine ink, mushroom and cocoa carrying the lines and
   the quieter text. Royal violet is the accent; champagne is NOT wired in here
   as a decorative colour, because the brief is explicit that it should mean
   something — achievement, completion, Founding Tester — rather than sit on
   every card edge. It lives in --champagne, used only where that is true.

   The neutrals are warm on purpose. A cool grey ivory reads as a spreadsheet;
   the warmth is what stops "light" turning into "clinical", which is the whole
   risk in lightening an app that was designed dark.
   ------------------------------------------------------------------------ */

[data-theme='ivory'] {
  --bg:            #F4F0E8;
  --bg-deep:       #E8E2D6;
  --surface:       #FBF9F4;
  --surface-raised: #FFFFFF;
  --surface-sunk:  #EDE7DC;

  --text:          #241D2B;
  --text-muted:    rgba(36, 29, 43, 0.68);
  --text-faint:    rgba(116, 103, 95, 0.85);

  --line:          rgba(116, 103, 95, 0.20);
  --line-strong:   rgba(116, 103, 95, 0.34);

  --accent:        #6946A2;
  --accent-quiet:  #8B6FE8;
  --on-accent:     #FFFFFF;
  /* Not champagne. --brass paints eyebrows, the tab indicator, today's dot
     and the end of a progress bar — none of which is an achievement. Painting
     it gold would put the reserved colour on every screen in the app and empty
     it of meaning. Cocoa taupe carries the same warmth and claims nothing. */
  --brass:         #74675F;
  --danger:        #A6486A;

  /* Reserved: Founding Tester, completion, a manifested state. Nothing else. */
  --champagne:     #C7A86B;
  --iridescent:    linear-gradient(120deg, #9A7CF0 0%, #7456C7 52%, #4966B3 100%);

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px #6946A2;

  --shadow-sm: 0 1px 2px rgba(36,29,43,.06);
  --shadow-md: 0 6px 20px -8px rgba(36,29,43,.14);
  --shadow-lg: 0 24px 56px -22px rgba(36,29,43,.20);

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-aubergine.png');


  color-scheme: light;
}

/* ── Aubergine — the immersive counterpart ────────────────────────────────
   For the screens meant to be sat inside rather than worked through: audio,
   bedtime listening, Rewind. Defined now because Ivory needs a counterpart to
   toggle to, and because the per-screen work later needs it to exist.
   ------------------------------------------------------------------------ */

[data-theme='aubergine'] {
  --bg:            #241D2B;
  --bg-deep:       #191320;
  --surface:       #2E2537;
  --surface-raised: #392E44;
  --surface-sunk:  #1D1725;

  --text:          #F4F0E8;
  --text-muted:    rgba(244, 240, 232, 0.66);
  --text-faint:    rgba(244, 240, 232, 0.42);

  --line:          rgba(244, 240, 232, 0.12);
  --line-strong:   rgba(244, 240, 232, 0.22);

  --accent:        #8B6FE8;
  --accent-quiet:  #6946A2;
  --on-accent:     #1A1422;
  --brass:         #C9BFB2;   /* mushroom taupe — see the note under Ivory */
  --danger:        #C2708C;

  --champagne:     #C7A86B;
  --iridescent:    linear-gradient(120deg, #9A7CF0 0%, #7456C7 52%, #4966B3 100%);

  --ring: 0 0 0 2px var(--bg), 0 0 0 4px #8B6FE8;

  --shadow-sm: 0 1px 2px rgba(0,0,0,.34);
  --shadow-md: 0 6px 20px -8px rgba(0,0,0,.50);
  --shadow-lg: 0 24px 56px -22px rgba(0,0,0,.62);

  /* Which colourway of the doorway E this palette needs. Erika's artwork is a
     raster, so it cannot take its colour from the surface the way the drawn
     mark did — a light theme has to name the dark file and a dark theme the
     light one. Declared here rather than branched in Blade, so the
     token-parity test makes a new theme that forgets it fail the suite instead
     of shipping a mark nobody can see. */
  --mark-url: url('/brand/mark-ivory.png');


  color-scheme: dark;
}

[data-theme='aubergine'] body::before {
  background:
    radial-gradient(58% 58% at 20% 6%, rgba(139, 111, 232, 0.18), transparent 70%),
    radial-gradient(52% 52% at 84% 0%, rgba(64, 88, 166, 0.12), transparent 72%);
}

/* ── Iridescence, used only at transformation moments ─────────────────────
   Violet → iris → indigo. The brief asks for this "selectively … not rainbow,
   glittery or overly feminine", so it is a named utility rather than a default
   on anything: a thing has to ask for it.
   ------------------------------------------------------------------------ */

/* ── champagne, the earned colour ─────────────────────────────────────────
   The counterpart rule to iridescence: iridescence marks a moment of change,
   champagne marks something reached. Both are utilities a thing has to ask
   for, because a reserved colour that anything may use is just a colour.

   Falls back to --brass on the themes that predate the brand, so a Founding
   pill is still a warm pill on Midnight rather than an unstyled one.
   ------------------------------------------------------------------------ */

/* Doubled selectors — .btn.btn-iridescent rather than .btn-iridescent — because
   these two utilities are declared here, beside the tokens they explain, while
   .btn and .pill are declared several hundred lines below in the component
   layer. At equal specificity the later rule wins, so a single class here loses
   silently: the button renders flat accent and looks deliberate. */

.pill.pill-founding {
  color: var(--champagne, var(--brass));
  border-color: color-mix(in srgb, var(--champagne, var(--brass)) 40%, transparent);
}

.btn.btn-iridescent {
  background: var(--iridescent, linear-gradient(120deg, #9A7CF0, #7456C7 52%, #4966B3));
  border-color: transparent;
  color: #FFFFFF;
}

.btn.btn-iridescent:hover { filter: brightness(1.06); }

/* A hairline of the same gradient. Used to mark a threshold — the top of the
   landing hero — and nowhere that is merely a divider. */
.rule-iridescent::after,
.rule-iridescent::before { background: var(--iridescent); height: 2px; opacity: .8; }

/* ── theme picker swatches ───────────────────────────────────────────────── */

.swatch {
  display: inline-flex;
  flex: 0 0 auto;
  gap: 0;
  border-radius: var(--r-full);
  overflow: hidden;
  border: 1px solid var(--line-strong);
}

.swatch i {
  display: block;
  width: 16px;
  height: 28px;
}

/* ══════════════════════════════════════════════════════════════════════════ */

/* ── reset ───────────────────────────────────────────────────────────────── */

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

/* The `hidden` attribute is only a default `display: none`, so any element
   carrying an inline or class-based `display` silently ignores it. That bites
   hardest where it matters most — a hidden loading state stacked above the
   finished content it was meant to be replaced by. Make hidden mean hidden. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
  font: 400 var(--t-base)/1.65 var(--sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-wrap: break-word;
}

/* A slow atmospheric wash — the one gradient in the whole app. 14s, barely
   perceptible, and gone entirely under reduced-motion. */
body::before {
  content: '';
  position: fixed;
  inset: -20% -20% auto -20%;
  height: 70vh;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(60% 60% at 22% 8%, rgba(110, 154, 139, 0.16), transparent 70%),
    radial-gradient(52% 52% at 82% 0%, rgba(191, 161, 115, 0.10), transparent 72%);
  animation: drift 22s var(--ease-in-out) infinite alternate;
}

[data-theme='parchment'] body::before, [data-theme='linen'] body::before,
[data-theme='wisteria'] body::before, [data-theme='ivory'] body::before,
[data-theme='light'] body::before {
  background:
    radial-gradient(60% 60% at 22% 8%, rgba(110, 154, 139, 0.20), transparent 70%),
    radial-gradient(52% 52% at 82% 0%, rgba(191, 161, 115, 0.16), transparent 72%);
}

@keyframes drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(0, 2.5%, 0) scale(1.06); }
}

img, svg, video, canvas { max-width: 100%; height: auto; display: block; }

h1, h2, h3, h4 {
  font-family: var(--serif);
  font-weight: 400;
  line-height: 1.18;
  letter-spacing: -0.011em;
  margin: 0;
  text-wrap: balance;
}

h1 { font-size: clamp(1.75rem, 5vw, var(--t-3xl)); }
h2 { font-size: clamp(1.35rem, 4vw, var(--t-2xl)); }
h3 { font-size: var(--t-lg); }
h4 { font-size: var(--t-md); }

p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--text); }

hr { border: 0; border-top: 1px solid var(--line); margin: var(--s-6) 0; }

/* One focus style, everywhere, never removed. */
:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-radius: var(--r-sm);
}

/* The .option and .chip patterns hide their real input with opacity:0 and
   style the label instead. The ring above was therefore being painted on an
   invisible element — a keyboard user tabbing through My World had no idea
   where they were. Lift the ring onto the visible container. */
.option:has(input:focus-visible),
.chip:has(input:focus-visible) {
  box-shadow: var(--ring);
}

::selection { background: var(--accent-quiet); color: var(--text); }

/* ── utilities ───────────────────────────────────────────────────────────── */

.eyebrow {
  font-size: var(--t-xs);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--brass);
  font-weight: 500;
  margin: 0 0 var(--s-3);
}

.lede    { font-size: var(--t-md); color: var(--text-muted); line-height: 1.7; }
.muted   { color: var(--text-muted); }
.faint   { color: var(--text-faint); }
.small   { font-size: var(--t-sm); }
.serif   { font-family: var(--serif); }
.center  { text-align: center; }
.stack > * + * { margin-top: var(--s-4); }
.stack-lg > * + * { margin-top: var(--s-6); }
.row { display: flex; align-items: center; gap: var(--s-3); }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }
.wrap { flex-wrap: wrap; }
.grow { flex: 1 1 auto; min-width: 0; }
.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;
}

/* ── app shell ───────────────────────────────────────────────────────────── */

/*
 * The bottom padding reserves room for the fixed tab bar. It has to include
 * the home-indicator inset, because .tabbar adds that same inset to its own
 * padding and is therefore TALLER than --nav-h on every iPhone since the X.
 * Without it the last card on any screen sits behind the tab bar and cannot
 * be scrolled clear of it.
 */
.shell {
  max-width: var(--shell-max);
  margin: 0 auto;
  padding: var(--s-5) var(--s-4)
           calc(var(--nav-h) + var(--s-8) + env(safe-area-inset-bottom));
  padding-left: max(var(--s-4), env(safe-area-inset-left));
  padding-right: max(var(--s-4), env(safe-area-inset-right));
}

/*
 * Both layouts set `viewport-fit=cover`, which is what lets the background
 * run edge to edge behind the notch — and also what puts the top of the page
 * underneath the status bar and Dynamic Island. Every inset here is paying
 * that back: without the top one the wordmark and the theme button sit under
 * the clock on any iPhone since the X, and in landscape the whole bar runs
 * under the notch.
 *
 * -webkit- prefix because Safari needed it until version 18. Modern Safari
 * honours the unprefixed property (verified in WebKit 26.5), but iOS 17 and
 * earlier do not, and there the bar is transparent rather than frosted —
 * text scrolling visibly through it, because the background is deliberately
 * only 88% opaque.
 */
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: calc(var(--s-3) + env(safe-area-inset-top)) var(--s-4) var(--s-3);
  padding-left: max(var(--s-4), env(safe-area-inset-left));
  padding-right: max(var(--s-4), env(safe-area-inset-right));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid var(--line);
}

/* The wordmark is also the link home, so it needs a real target rather than
   the 28px height of its own text. Padding only — it does not move. */
.topbar .brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 44px;
  padding-right: var(--s-2);
  color: var(--text);
  text-decoration: none;
}

/* The mark takes its colourway from the palette, via --mark-url. The fallback
   is the ivory file rather than nothing: a theme that somehow reaches this
   without declaring one should show the wrong colourway, which is visible and
   reportable, rather than an empty box, which reads as a layout bug. */
.mark {
  display: block;
  flex: none;
  background-image: var(--mark-url, url('/brand/mark-ivory.png'));
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

/* Tracked wide, because generous letterspacing is what buys a text serif the
   composure of a display one. The old wordmark put a brass E on the end; the
   mark carries that job now, so the word is left alone. */
.brand-word {
  font-family: var(--serif);
  font-size: var(--t-md);
  font-weight: 500;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  line-height: 1;
}

/* The lockup on the public pages: bigger, stacked over the heading, and a link
   home rather than a label. */
.auth-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
  color: var(--text);
  text-decoration: none;
}

.auth-brand .brand-word {
  font-size: var(--t-lg);
  letter-spacing: 0.16em;
}

.auth-brand:hover { color: var(--text); }

.page-head { margin-bottom: var(--s-6); }
.page-head h1 { margin-bottom: var(--s-2); }

/* ── bottom navigation (mobile-first, the primary nav) ───────────────────── */

.tabbar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  z-index: 30;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  align-items: stretch;
  min-height: var(--nav-h);
  padding-bottom: env(safe-area-inset-bottom);
  background: color-mix(in srgb, var(--bg-deep) 92%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border-top: 1px solid var(--line);
  overflow-x: auto;
  scrollbar-width: none;
}

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

.tabbar a {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-width: 60px;
  min-height: 44px;
  padding: var(--s-2) var(--s-1);
  color: var(--text-faint);
  text-decoration: none;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transition: color var(--dur-fast) var(--ease);
  position: relative;
  cursor: pointer;
}

.tabbar a svg { width: 21px; height: 21px; stroke-width: 1.5; flex: 0 0 auto; }
.tabbar a span { white-space: nowrap; }
.tabbar .tab-full { display: none; }
.tabbar a:hover { color: var(--text-muted); }
.tabbar a[aria-current='page'] { color: var(--text); }

.tabbar a[aria-current='page']::before {
  content: '';
  position: absolute;
  top: 0; left: 50%;
  width: 22px; height: 2px;
  transform: translateX(-50%);
  background: var(--brass);
  border-radius: 0 0 2px 2px;
}

/*
 * Narrow phones: let the seven tabs share the width instead of overflowing.
 *
 * `min-width: 60px` on seven tabs demands 420px. An iPhone 12/13/14 is 390.
 * The grid overflowed, `overflow-x: auto` turned the bar into a scrollable
 * strip, and My World sat off the right edge — permanently unreachable,
 * because nobody thinks to swipe a fixed bottom bar sideways. It looked like
 * a six-tab app with a clipped seventh icon.
 *
 * 390 / 7 = 55px per tab, still comfortably over the 44px minimum touch
 * target. The type has to come down slightly for "Gratitude" to fit on one
 * line at that width; the icons carry the recognition anyway.
 */
@media (max-width: 440px) {
  .tabbar a {
    min-width: 0;
    padding-inline: 2px;
    font-size: 9px;
    letter-spacing: 0.01em;
  }

  .tabbar a svg { width: 20px; height: 20px; }

  /* Belt and braces: if a translation ever makes a label longer than its
     column, clip it rather than reintroducing the overflow. */
  .tabbar a span {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

@media (min-width: 900px) {
  /*
   * Scoped to .app-shell, and that scope is the whole point.
   *
   * This was a bare `body` selector, so it also caught the auth pages — which
   * set `display:grid; place-items:center` inline but say nothing about
   * columns, so `grid-template-columns: 236px 1fr` from here applied to them
   * too. The sign-in form, max-width 26rem, was laid into the 236px sidebar
   * column and centred inside it: a 236px sliver pinned to the left edge of a
   * 1440px screen with the other 1200px empty. It is the first screen anyone
   * sees, and on desktop it looked broken because it was.
   */
  body.app-shell {
    display: grid;
    grid-template-columns: 236px 1fr;
    grid-template-rows: auto 1fr;
  }

  /*
   * Every child is placed explicitly. Only .topbar used to be, and the other
   * two landed in the right cells purely because they happened to be the
   * second and third children — nothing said so.
   *
   * Adding one element to the shell was therefore enough to turn the whole
   * desktop layout inside out: the install tip took the sidebar cell, the nav
   * shifted into the wide column, and the entire page was auto-placed into the
   * 236px sliver. Which is the same failure the note above describes for the
   * sign-in form, arriving again by a different route.
   *
   * Both axes are named, not just columns, so nothing here depends on anybody's
   * reading of the auto-placement algorithm.
   *
   * The install tip that caused all this now lives inside <main> rather than
   * being a child of the shell. Giving it a row of its own was tried first and
   * lost to the sidebar: .tabbar is sticky and a full viewport tall, so it
   * covers anything placed in a row above it. Content belongs in the content
   * column; only the three boxes below are part of the frame.
   */
  .topbar { grid-column: 1 / -1; grid-row: 1; }
  .tabbar { grid-column: 1;      grid-row: 2; }
  .shell  { grid-column: 2;      grid-row: 2; }

  /*
   * The nav lives inside @auth, so a signed-out render of this layout has no
   * column 1 to fill and the page would sit in the sidebar on its own. That
   * predates the breakage above and is the same bug; it gets closed here rather
   * than left as a known hole beside the one being fixed.
   */
  body.app-shell:not(:has(.tabbar)) .shell { grid-column: 1 / -1; }
  .tabbar {
    position: sticky;
    /*
     * Below the header, not behind it.
     *
     * This said `top: 0` while .topbar is also `position: sticky; top: 0` — so
     * on any page long enough to scroll, the sidebar rode up to the viewport
     * top and collided with the header. And because the mobile rule leaves it
     * at z-index 30 against the header's 20, the sidebar won: the nav links
     * painted OVER the wordmark and the theme button.
     *
     * The height was wrong by the same amount for the same reason — 57px
     * against a real 69px — which left the sidebar overflowing the bottom of
     * the viewport by 12px.
     */
    top: var(--topbar-h);
    z-index: 10;
    align-self: start;
    height: calc(100dvh - var(--topbar-h));
    grid-auto-flow: row;
    grid-auto-columns: auto;
    align-content: start;
    gap: var(--s-1);
    padding: var(--s-5) var(--s-3);
    border-top: 0;
    border-right: 1px solid var(--line);
    background: transparent;
    backdrop-filter: none;
  }
  .tabbar a {
    flex-direction: row;
    justify-content: flex-start;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-md);
    font-size: var(--t-sm);
    letter-spacing: 0.02em;
    text-transform: none;
  }
  .tabbar .tab-short { display: none; }
  .tabbar .tab-full { display: inline; }
  .tabbar a[aria-current='page'] { background: var(--surface); }
  .tabbar a[aria-current='page']::before {
    top: 50%; left: 0;
    width: 2px; height: 18px;
    transform: translateY(-50%);
    border-radius: 0 2px 2px 0;
  }
  .shell { padding-bottom: var(--s-9); }
}

/* ── card ────────────────────────────────────────────────────────────────── */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: var(--shadow-sm);
}

.card-quiet { background: transparent; box-shadow: none; }
.card-raised { background: var(--surface-raised); box-shadow: var(--shadow-md); }
.card + .card { margin-top: var(--s-4); }

a.card, button.card {
  display: block;
  width: 100%;
  text-align: left;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--dur) var(--ease), border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

a.card:hover, button.card:hover {
  transform: translateY(-2px);
  border-color: var(--line-strong);
  box-shadow: var(--shadow-md);
}

a.card:active, button.card:active { transform: translateY(0); }

/* ── buttons ─────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 46px;
  padding: 0 var(--s-5);
  border: 1px solid transparent;
  border-radius: var(--r-full);
  background: var(--accent);
  color: var(--on-accent);
  font: 500 var(--t-sm)/1 var(--sans);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
  -webkit-tap-highlight-color: transparent;
}

.btn:hover  { background: var(--sage-300); color: var(--on-accent); }
.btn:active { transform: scale(0.985); }
.btn[disabled], .btn[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; transform: none; }

.btn-ghost {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--text);
}
.btn-ghost:hover { background: var(--surface-raised); color: var(--text); border-color: var(--text-faint); }

.btn-quiet {
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--t-base);
  padding: 0 var(--s-3);
}
.btn-quiet:hover { background: var(--surface); color: var(--text); }

.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { background: var(--clay-500); color: #fff; }

.btn-sm { min-height: 38px; padding: 0 var(--s-4); font-size: var(--t-xs); }
.btn-full { width: 100%; }

.btn-icon {
  min-height: 44px;
  min-width: 44px;
  padding: 0;
  border-radius: var(--r-full);
  background: transparent;
  border-color: var(--line-strong);
  color: var(--text-muted);
}
.btn-icon:hover { background: var(--surface-raised); color: var(--text); }
.btn-icon svg { width: 20px; height: 20px; stroke-width: 1.6; }

/* ── forms ───────────────────────────────────────────────────────────────── */

.field { display: block; margin-bottom: var(--s-5); }

.field > label,
.label {
  display: block;
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--text);
  margin-bottom: var(--s-2);
}

.hint {
  display: block;
  font-size: var(--t-sm);
  color: var(--text-muted);
  margin: calc(var(--s-1) * -1) 0 var(--s-3);
  line-height: 1.55;
}

.input, .textarea, .select {
  width: 100%;
  min-height: 48px;
  padding: var(--s-3) var(--s-4);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  color: var(--text);
  font: 400 var(--t-base)/1.6 var(--sans);
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.textarea { min-height: 132px; resize: vertical; font-family: var(--serif); line-height: 1.75; }

/* ── show the password you are typing ─────────────────────────────────────
   A long password typed blind on a phone keyboard, with a failure message that
   cannot tell you whether you mistyped or misremembered. The button sits inside
   the field's right edge, and the input gains room so the text never runs under
   it — including when the field is full and scrolled to its end.

   Painted from --text-muted so it reads as secondary on all ten palettes
   without a per-theme rule, and lifts to --text on hover and focus.
   ------------------------------------------------------------------------ */

.field-reveal { position: relative; display: block; }

.field-reveal .input { padding-right: 52px; }

.reveal-toggle {
  position: absolute;
  top: 0;
  right: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Fills the field's height rather than a fixed box, so it stays centred if
     the input ever grows past its 48px minimum. */
  height: 100%;
  min-height: 44px;
  width: 48px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--r-md);
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease);
}

.reveal-toggle:hover { color: var(--text); }

.reveal-toggle:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  color: var(--text);
}

/* Both icons are in the markup so revealing never waits on a request. The
   second one, eye-off, is the pressed state. */
.reveal-toggle svg:last-child { display: none; }
.reveal-toggle[aria-pressed='true'] svg:first-of-type { display: none; }
.reveal-toggle[aria-pressed='true'] svg:last-child { display: inline; }

.input::placeholder, .textarea::placeholder { color: var(--text-faint); }
.input:hover, .textarea:hover, .select:hover { border-color: var(--line-strong); }
.input:focus, .textarea:focus, .select:focus {
  border-color: var(--accent);
  background: var(--surface);
  box-shadow: var(--ring);
  outline: none;
}

.select {
  appearance: none;
  padding-right: var(--s-7);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237FA898' stroke-width='1.6'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-4) center;
  background-size: 18px;
  cursor: pointer;
}

.error {
  display: block;
  font-size: var(--t-sm);
  color: var(--danger);
  margin-top: var(--s-2);
}

.input[aria-invalid='true'], .textarea[aria-invalid='true'] { border-color: var(--danger); }

/*
 * 16px on touch, and not for legibility.
 *
 * iOS Safari zooms the entire page in when a text field smaller than 16px
 * receives focus, and it does not zoom back out when the field is blurred.
 * --t-base is 15px, so every field in the app did it: tap "What do you want?"
 * and the layout lurches, the tab bar slides off-screen, and you are left
 * pinching your way back. On a journal the user is meant to sink into, that
 * is the single most disruptive thing the interface can do.
 *
 * Scoped to coarse pointers so the desktop typography is untouched, and the
 * one-pixel difference is invisible on a phone anyway.
 */
@media (pointer: coarse) {
  .input, .textarea, .select { font-size: 16px; }
}

/*
 * Standalone "back" links.
 *
 * These sit alone on a line and are the only way out of a screen, but they
 * were bare anchors — 15px tall, the height of their own text, and a real
 * miss on a phone. An inline link inside a sentence is fine at text height;
 * a navigation control on its own line is not.
 */
.link-back {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 44px;
  padding-right: var(--s-2);
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--t-sm);
}

.link-back::before { content: '←'; opacity: 0.6; }
.link-back:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }

.counter { font-size: var(--t-xs); color: var(--text-faint); text-align: right; margin-top: var(--s-1); }

/* option grid — the radio/checkbox pattern used all through the wizard */

.options { display: grid; gap: var(--s-2); }
.options-2 { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

/* A grid of free-text fields — the same shape as .options-2, deliberately a
   different class. .options carries a meaning: "these are the choices". Using
   it for text inputs made My World's values look like six buttons that ignored
   every tap. */
.field-grid {
  display: grid;
  gap: var(--s-2);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

.option {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  width: 100%;
  min-height: 52px;
  padding: var(--s-3) var(--s-4);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  color: var(--text);
  font: 400 var(--t-base)/1.5 var(--sans);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

.option:hover { border-color: var(--line-strong); background: var(--surface); }
.option:active { transform: scale(0.995); }

.option.is-on,
.option:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
}

.option input { position: absolute; opacity: 0; pointer-events: none; }
.option .option-body { flex: 1; min-width: 0; }
.option .option-label { font-weight: 500; display: block; }
.option small { display: block; color: var(--text-muted); font-size: var(--t-sm); margin-top: 2px; line-height: 1.5; }

.option .tick {
  flex: 0 0 auto;
  width: 20px; height: 20px;
  margin-top: 2px;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-full);
  position: relative;
  transition: border-color var(--dur-fast) var(--ease);
}

.option:has(input:checked) .tick { border-color: var(--accent); }
.option:has(input:checked) .tick::after {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: var(--r-full);
  background: var(--accent);
}
.option:has(input[type='checkbox']) .tick { border-radius: 6px; }
.option:has(input[type='checkbox']:checked) .tick::after { border-radius: 3px; }

/* ── week dots (gratitude streak) ────────────────────────────────────────
   Deliberately low-contrast for days not written. A missed day should read as
   "nothing here", not as a failure — this is a reflection app, not a habit
   tracker with a guilt mechanic. */

.week-dot {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: var(--r-full);
  border: 1px solid var(--line);
  font-size: var(--t-xs);
  color: var(--text-faint);
  background: var(--surface-sunk);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

.week-dot.is-written {
  background: color-mix(in srgb, var(--accent) 24%, var(--surface));
  border-color: var(--accent-quiet);
  color: var(--text);
}

.week-dot.is-today {
  border-color: var(--brass);
  color: var(--text);
}

.week-dot.is-future { opacity: .45; }

/* ── chips & pills ───────────────────────────────────────────────────────── */

.chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 36px;
  padding: 0 var(--s-4);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  color: var(--text-muted);
  font-size: var(--t-sm);
  text-decoration: none;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease);
}

.chip:hover { border-color: var(--line-strong); color: var(--text); }
.chip.is-on { background: color-mix(in srgb, var(--accent) 16%, var(--surface)); border-color: var(--accent); color: var(--text); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px var(--s-3);
  border-radius: var(--r-full);
  font-size: var(--t-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 500;
  border: 1px solid var(--line-strong);
  color: var(--text-muted);
  white-space: nowrap;
}

.pill::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: var(--r-full);
  background: currentColor;
}

/* Status colours are deliberately close together — this is an archive, not a
   traffic light. Difference in hue, not in loudness. */
.pill-desired    { color: var(--sage-300); border-color: color-mix(in srgb, var(--sage-300) 34%, transparent); }
.pill-unfolding  { color: var(--brass-300); border-color: color-mix(in srgb, var(--brass-300) 34%, transparent); }
.pill-manifested { color: #A8CFA0; border-color: rgba(168,207,160,.34); }
.pill-answered   { color: #C9BBE0; border-color: rgba(201,187,224,.34); }
.pill-achieved   { color: #9FC6D8; border-color: rgba(159,198,216,.34); }
.pill-redirected { color: #D8B79F; border-color: rgba(216,183,159,.34); }
.pill-evolved    { color: #BFD0A0; border-color: rgba(191,208,160,.34); }
.pill-released   { color: var(--text-faint); border-color: var(--line-strong); }
.pill-paused     { color: var(--text-faint); border-color: var(--line-strong); }

/* ── prose (stories, rewinds, anything read rather than scanned) ──────────── */

.prose {
  font-family: var(--serif);
  font-size: var(--t-md);
  line-height: 1.85;
  color: var(--text);
}

.prose p { margin: 0 0 1.15em; }
.prose p:last-child { margin-bottom: 0; }
.prose-lg { font-size: var(--t-lg); line-height: 1.8; }

/* ── empty states ────────────────────────────────────────────────────────── */

.empty {
  text-align: center;
  padding: var(--s-8) var(--s-4);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
}

.empty svg { width: 34px; height: 34px; margin: 0 auto var(--s-4); color: var(--text-faint); stroke-width: 1.2; }
.empty h3 { margin-bottom: var(--s-2); }
.empty p { color: var(--text-muted); max-width: 34ch; margin: 0 auto var(--s-5); }

/* ── notice / flash ──────────────────────────────────────────────────────── */

.notice {
  display: flex;
  gap: var(--s-3);
  padding: var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  font-size: var(--t-sm);
  margin-bottom: var(--s-5);
}

.notice svg { flex: 0 0 auto; width: 18px; height: 18px; margin-top: 1px; stroke-width: 1.6; }
.notice-ok    { border-color: color-mix(in srgb, var(--accent) 40%, transparent); color: var(--sage-300); }
.notice-warn  { border-color: color-mix(in srgb, var(--brass) 44%, transparent); color: var(--brass-300); }
.notice-error { border-color: color-mix(in srgb, var(--danger) 44%, transparent); color: var(--clay-400); }

[data-theme='parchment'] .notice-ok, [data-theme='linen'] .notice-ok { color: var(--sage-600); }
[data-theme='parchment'] .notice-warn, [data-theme='linen'] .notice-warn { color: var(--brass-500); }
[data-theme='parchment'] .notice-error, [data-theme='linen'] .notice-error { color: var(--clay-500); }

/* ── toast ───────────────────────────────────────────────────────────────── */

/* Announcement bodies are Markdown, so they arrive as real paragraphs and
   lists rather than one block of text. The first and last get their margins
   trimmed so the card does not gain a dead band top and bottom. */
.announcement-body > :first-child { margin-top: 0; }
.announcement-body > :last-child { margin-bottom: 0; }
.announcement-body p { margin: 0 0 var(--s-3); }
.announcement-body ul, .announcement-body ol { margin: 0 0 var(--s-3) var(--s-5); }

/* ── the install tip ──────────────────────────────────────────────────────
   Anchored under the topbar rather than floating over the bottom of the
   screen, where it would sit on top of the tab bar — the one piece of
   navigation somebody needs while deciding whether to dismiss this.
   ------------------------------------------------------------------------ */

/* A card that is mostly a link but carries an action of its own. The link
   fills the card so the whole row is still clickable; the action sits outside
   it, because a <form> inside an <a> is invalid and gets reparented. */
.card-link {
  display: block;
  color: inherit;
  text-decoration: none;
}

.install-tip {
  /* In normal flow, not sticky. The topbar's height is padding plus
     env(safe-area-inset-top), so it has no fixed value to anchor to — and the
     note above .tabbar records what guessing a bar height costs on an iPhone.
     A one-time tip that scrolls away with the page is the right behaviour
     anyway. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin: 0 0 var(--s-5);
  padding: var(--s-3) var(--s-4);
  background: var(--surface-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
}

.install-tip-text {
  flex: 1 1 12rem;
  margin: 0;
  font-size: var(--t-sm);
  color: var(--text-muted);
}

.install-tip-actions {
  display: flex;
  gap: var(--s-2);
  margin-left: auto;
}

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--nav-h) + var(--s-4) + env(safe-area-inset-bottom));
  z-index: 60;
  transform: translate(-50%, 14px);
  max-width: min(92vw, 30rem);
  padding: var(--s-3) var(--s-5);
  background: var(--surface-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  box-shadow: var(--shadow-lg);
  font-size: var(--t-sm);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.toast.is-shown { opacity: 1; transform: translate(-50%, 0); }

@media (min-width: 900px) { .toast { bottom: var(--s-5); } }

/* ── divider with a word in it ───────────────────────────────────────────── */

.rule {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin: var(--s-6) 0;
  color: var(--text-faint);
  font-size: var(--t-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.rule::before, .rule::after { content: ''; flex: 1; height: 1px; background: var(--line); }

/* ── progress ────────────────────────────────────────────────────────────── */

.progress {
  height: 2px;
  background: var(--line);
  border-radius: var(--r-full);
  overflow: hidden;
}

.progress > i {
  display: block;
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--accent-quiet), var(--brass));
  border-radius: inherit;
  transition: width var(--dur-slow) var(--ease);
}

/* ── reduced motion ──────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  body::before { animation: none; }
}

/* ── print: a reading should be printable, it belongs to the user ────────── */

/*
 * Touch targets, last in the file on purpose.
 *
 * 44px is the smallest reliably hittable target on a phone. Chips were 36 and
 * some buttons measured 38 — the size where a thumb lands beside the thing it
 * aimed at.
 *
 * These live at the end because an earlier attempt put them beside the form
 * rules, above `.chip`'s own `min-height: 36px`. A media query adds no
 * specificity, so the later plain rule simply won and the chips stayed 36px
 * while everything else in the same block worked. Anything overriding a
 * component belongs after that component.
 */
@media (pointer: coarse) {
  .chip { min-height: 44px; }
  .btn  { min-height: 48px; }
}


/* ── the signed-out pages ────────────────────────────────────────────────
   One column on a phone: what the screen is, then the form, in that order.
   Two columns from 1000px, because a 26rem strip centred in 1440px of empty
   dark is not a design — it is a mobile layout that nobody looked at on a
   laptop. The left column carries the words, the right the work. */

.auth-page {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--s-6) var(--s-4);
}

.auth-main { width: 100%; max-width: 26rem; }
.auth-say h1 { margin-bottom: var(--s-3); }
.auth-say .lede { margin-bottom: var(--s-6); }

/* Reserved for the second column; there is nowhere to put it on a phone. */
.auth-aside { display: none; }

@media (min-width: 1000px) {
  .auth-page { padding: var(--s-8) var(--s-7); }

  .auth-main {
    max-width: 62rem;
    display: grid;
    grid-template-columns: 1fr 26rem;
    gap: var(--s-9);
    align-items: center;
  }

  .auth-say { max-width: 30rem; }
  .auth-say h1 { font-size: var(--t-3xl); line-height: 1.1; }

  .auth-aside {
    display: block;
    max-width: 26rem;
    padding-top: var(--s-5);
    border-top: 1px solid var(--line);
    line-height: 1.7;
  }

  /* The form column keeps its own width so the fields stay a comfortable
     measure — a 40rem-wide email input looks like a mistake. */
  .auth-do { width: 100%; }
}

/* ── My Circle rows ──────────────────────────────────────────────────────
   Each person is a bordered group so it is obvious which details belong to
   whom once there are more than a couple of them — with five fixed rows that
   was never in doubt, and with twenty it would be. */
.circle-person {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-4);
  margin-bottom: var(--s-3);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

.circle-person [data-notes] { display: grid; gap: var(--s-2); }
.circle-person [data-add-note] { justify-self: start; }


/* ── My Journey ──────────────────────────────────────────────────────────
   A timeline, so the vertical rule matters more than the rows: it is the
   thing that turns a list of events into a length of time. */

.journey-stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s-3);
  margin-bottom: var(--s-7);
}

@media (min-width: 560px) {
  .journey-stats { grid-template-columns: repeat(4, 1fr); }
}

.stat {
  display: grid;
  gap: var(--s-1);
  padding: var(--s-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

.stat-n { font-family: var(--serif); font-size: var(--t-2xl); line-height: 1; }
.stat-l { font-size: var(--t-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; }

.journey-month {
  font-size: var(--t-md);
  color: var(--text-muted);
  margin: var(--s-7) 0 var(--s-4);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--line);
}

.timeline { list-style: none; margin: 0; padding: 0 0 0 var(--s-6); position: relative; }

/* The rule sits behind the marks, inset by half a mark so it runs through
   their centres. */
.timeline::before {
  content: '';
  position: absolute;
  left: 11px;
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: var(--line);
}

.timeline-item { position: relative; display: flex; gap: var(--s-4); padding: var(--s-3) 0; }

.timeline-mark {
  position: absolute;
  left: calc(var(--s-6) * -1);
  top: var(--s-3);
  display: grid;
  place-items: center;
  width: 23px;
  height: 23px;
  border-radius: var(--r-full);
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--text-faint);
}

/* Arrival is the only event that gets colour. If every kind were tinted the
   timeline would read as decoration; one tinted mark reads as an event. */
.timeline-manifested, .timeline-achieved, .timeline-answered,
.timeline-redirected, .timeline-evolved, .timeline-released {
  border-color: var(--accent);
  color: var(--accent);
}

.timeline-body { display: grid; gap: 2px; text-decoration: none; color: inherit; min-height: 44px; }
.timeline-body:hover .timeline-title { color: var(--accent); }
.timeline-title { font-family: var(--serif); font-size: var(--t-md); line-height: 1.35; }

/* ── Affirmation cards ───────────────────────────────────────────────────
   A card is a real <details>, so turning it over needs no JavaScript. The
   summary marker is removed on both engines — Safari needs the ::-webkit-
   pseudo-element, everything else honours list-style — because the default
   triangle sits badly against a serif line and reads as a file tree.
   ------------------------------------------------------------------------ */

.affirm-card > summary {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  cursor: pointer;
  list-style: none;
  /* The whole card is the target, so it is comfortably over 44px. */
  min-height: 44px;
}

.affirm-card > summary::-webkit-details-marker { display: none; }

.affirm-card > summary:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-radius: var(--r-sm);
}

.affirm-front {
  font-size: var(--t-md);
  line-height: 1.5;
}

.affirm-card[open] .affirm-hint { display: none; }

@media print {
  .topbar, .tabbar, .toast, .btn, body::before { display: none !important; }
  body { background: #fff; color: #000; }
  .shell { padding: 0; max-width: none; }
  .card { border: 0; box-shadow: none; padding: 0; }
  /* Print a card face-up and face-down: a kept affirmation is worth more with
     the line saying what it stands on. */
  .affirm-card > *:not(summary) { display: block !important; }
}
