/* ============================================================
   INROOTS — the brand layer. THE single source of colour.
   ------------------------------------------------------------
   Spec: docs/superpowers/specs/2026-09-19-inroots-rebrand-design.md
   Brand book: inroots-web docs/BRANDING.md (the public site is the
   reference rendering; this file must agree with it).

   This is the ONLY file in app/static/v2 allowed to spell a colour
   out. scripts/check_brand.py enforces that — every other
   stylesheet and every .jsx reaches colour through the token names
   declared here. Reviewing a change to this file is reviewing the
   brand.

   WHY IT EXISTS AT ALL. Before this, three files each declared the
   full ramp at html[data-aesthetic=]/html[data-theme=] specificity
   — styles.css, styles-editorial.css and styles-refined-dark.css —
   with three different purples (#7c3aed, #a78bfa, #7c6cf6). Which
   one you got depended on load order, and the dark theme people
   actually saw came from refined-dark, not from styles.css. Those
   palette blocks are deleted; this replaces all three.

   This repo already learned the cost of a parallel ramp: commit
   378a83a added --sig-* as its own hand-tuned values and put two
   subtly different greens side by side across 109 badges. So this
   file REMAPS the app's existing token names rather than
   introducing new ones for components to migrate onto. Components
   keep asking for --bg and --brand; only the answers change.
   ============================================================ */

/* ── Type ─────────────────────────────────────────────────────
   Inter Tight, self-hosted. It replaced Fraunces + Manrope +
   JetBrains Mono loaded from fonts.googleapis.com — the only
   third-party request the app made, in a product sold on nothing
   having to leave your servers. Arabic chains to Noto Kufi Arabic.

   unicode-range splits latin from latin-ext so a page of English
   fetches 22KB per weight instead of 60KB. font-display:swap keeps
   text visible while they load: the alternative is a blank
   dashboard on a slow connection.

   Regenerate by copying from @fontsource — these are the same
   files the website ships, so the two render identically. */
@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/v2/fonts/inter-tight-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/v2/fonts/inter-tight-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/v2/fonts/inter-tight-600.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/static/v2/fonts/inter-tight-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('/static/v2/fonts/inter-tight-800.woff2') format('woff2');
}
@font-face {
  font-family: 'Noto Kufi Arabic';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/v2/fonts/noto-kufi-arabic-400.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891,
    U+0898-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41,
    U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}

html,
html[data-aesthetic="editorial"] {
  --font-sans:    'Inter Tight', 'Noto Kufi Arabic', -apple-system,
                  BlinkMacSystemFont, system-ui, sans-serif;
  --font-body:    var(--font-sans);
  /* The brand sets labels, eyebrows and tabular numerals in mono. No webfont
     for it: the system stack is already there, costs nothing, and the brand
     book asks for ui-monospace by name. */
  --font-mono:    ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
                  monospace;
  /* Fraunces is retired. --font-display and --font-serif are kept as aliases
     rather than deleted, because a stray consumer resolving to nothing would
     silently fall back to Times. */
  --font-display: var(--font-sans);
  --font-serif:   var(--font-sans);
}

/* ── Semantics, so the next person does not have to guess ─────
   tangerine  actions, section labels, on-call, progress. NEVER a
              person's state — that rule comes from the brand book
              and it is what keeps "late" from looking like a button.
   ice        good: on time, present, healthy.
   coral      late, at risk, flagged.
   deep coral breach, failure.
   dim grey   PTO, off, inactive.

   Three tangerine tokens exist because one cannot do every job in
   the light twin. --brand is the display colour (fills, type above
   24px). --tan-ink is the same idea at a contrast that passes AA as
   small text — on the light ground the display tangerine reaches
   only 2.9:1, --tan-ink reaches 5.1:1. In the dark twin they are
   the same value. --ctl-bd is the boundary for controls whose only
   visual edge is a border, at or above the 3:1 WCAG 1.4.11 wants;
   --line stays decorative and must not be used for that.
   ──────────────────────────────────────────────────────────── */

/* ── The raw values, declared ONCE ────────────────────────────
   Both twins live here under -d- / -l- names, and the mappings
   below point the app's token names at one set or the other. The
   reason for the indirection is the mobile shell: it carries its
   own theme (its own storage key, its own .m-theme-* classes on
   <html> and .m-dark / .m-light on containers) and can be showing
   light while the desktop preference says dark. Without this, the
   ramp would have to be written out twice and the two copies would
   drift — which is the exact failure this whole file exists to end. */
:root {
  --ir-d-graphite:   #111214;
  --ir-d-panel:      #1a1b1e;
  --ir-d-bubble:     #202226;
  --ir-d-hover:      #1f2023;
  --ir-d-active:     #26282c;
  --ir-d-line:       #2a2b2f;
  --ir-d-line2:      #35373c;
  --ir-d-ctl:        #6a6c72;
  --ir-d-ink:        #F2EFE7;
  --ir-d-body:       #c9c6bd;
  --ir-d-muted:      #9a9891;
  --ir-d-dim:        #8a8983;
  --ir-d-tan:        #FF7138;
  --ir-d-tan-2:      #ff8a5c;
  --ir-d-tan-ink:    #FF7138;
  --ir-d-ice:        #A8D5F5;
  --ir-d-ice-fg:     #0b2434;
  --ir-d-coral:      #e0785b;
  --ir-d-coral-deep: #f2553c;

  --ir-l-ground:     #F2EFE7;
  --ir-l-panel:      #ffffff;
  --ir-l-bubble:     #faf8f3;
  --ir-l-hover:      #f3efe6;
  --ir-l-active:     #eae5d8;
  --ir-l-line:       #e2ddd2;
  --ir-l-line2:      #d2ccbf;
  --ir-l-ctl:        #85806f;
  --ir-l-ink:        #111214;
  --ir-l-body:       #3a3a38;
  --ir-l-muted:      #6b6a65;
  --ir-l-dim:        #63625d;
  --ir-l-tan:        #e35c26;
  --ir-l-tan-2:      #c74f1f;
  --ir-l-tan-ink:    #b03f0c;
  --ir-l-ice:        #1f6690;
  --ir-l-ice-fg:     #F2EFE7;
  --ir-l-coral:      #a8442a;
  --ir-l-coral-deep: #8c2f18;

  --ir-tan-fg:       #1a0e04;   /* ink ON tangerine, identical in both twins */
}

/* ── DARK (default) ─────────────────────────────────────────── */
html[data-theme="dark"],
html[data-aesthetic="editorial"][data-theme="dark"],
html.m-theme-dark,
.m-dark {
  --bg:         var(--ir-d-graphite);
  --bg-elev-1:  var(--ir-d-panel);
  --bg-elev-2:  var(--ir-d-bubble);
  --bg-hover:   var(--ir-d-hover);
  --bg-active:  var(--ir-d-active);

  --line:       var(--ir-d-line);
  --line-hi:    var(--ir-d-line2);
  --ctl-bd:     var(--ir-d-ctl);

  --text:       var(--ir-d-ink);
  --text-2:     var(--ir-d-body);
  --text-3:     var(--ir-d-muted);
  --muted:      var(--ir-d-dim);

  --brand:      var(--ir-d-tan);
  --brand-2:    var(--ir-d-tan-2);
  --brand-soft: color-mix(in oklab, var(--ir-d-tan) 16%, transparent);
  /* Ink for anything sitting ON the tangerine: buttons, CTAs, the user's
     own chat bubble. White is only 2.8:1 on the dark twin's tangerine and
     3.9:1 on the light twin's, so it fails AA at both. Graphite is 6.5:1
     and 4.7:1 — and dark ink on a warm accent is what the brand does. */
  --on-brand:   var(--ir-d-graphite);
  --on-err:     var(--ir-d-graphite);
  --tan-ink:    var(--ir-d-tan-ink);   /* dark twin: display == ink */
  --tan-fg:     var(--ir-tan-fg);

  /* --accent was a cyan reserved for italic <em> and focus rings. Ice is the
     brand's only free non-action hue, so it takes the role — which also means
     a focus ring reads as "good" rather than as a warning. */
  --accent:      var(--ir-d-ice);
  --accent-soft: color-mix(in oklab, var(--ir-d-ice) 18%, transparent);
  --ice-fg:      var(--ir-d-ice-fg);

  --ok:    var(--ir-d-ice);
  --warn:  var(--ir-d-coral);
  --err:   var(--ir-d-coral-deep);
  --info:  var(--ir-d-ice);
  --gold:  var(--ir-d-tan);

  --grid-line: rgba(242, 239, 231, 0.045);
  --pop-bg:    rgba(26, 27, 30, 0.97);
  --shadow:    none;                  /* hairlines carry the depth */
  color-scheme: dark;
}

/* ── LIGHT ────────────────────────────────────────────────────
   After dark on purpose. <html> can briefly carry data-theme="dark"
   from the desktop preference AND m-theme-light from the mobile
   shell; when both match the same element at the same specificity,
   source order decides, and the shell that is actually rendering
   should win. */
html[data-theme="light"],
html[data-aesthetic="editorial"][data-theme="light"],
html.m-theme-light,
.m-light {
  --bg:         var(--ir-l-ground);
  --bg-elev-1:  var(--ir-l-panel);
  --bg-elev-2:  var(--ir-l-bubble);
  --bg-hover:   var(--ir-l-hover);
  --bg-active:  var(--ir-l-active);

  --line:       var(--ir-l-line);
  --line-hi:    var(--ir-l-line2);
  --ctl-bd:     var(--ir-l-ctl);

  --text:       var(--ir-l-ink);
  --text-2:     var(--ir-l-body);
  --text-3:     var(--ir-l-muted);
  --muted:      var(--ir-l-dim);

  --brand:      var(--ir-l-tan);
  --brand-2:    var(--ir-l-tan-2);
  --brand-soft: color-mix(in oklab, var(--ir-l-tan) 10%, transparent);
  --on-brand:   var(--ir-l-ink);
  --on-err:     var(--ir-l-ground);
  --tan-ink:    var(--ir-l-tan-ink);   /* 5.13:1 on --bg; --brand is 3.14:1 */
  --tan-fg:     var(--ir-tan-fg);

  --accent:      var(--ir-l-ice);
  --accent-soft: color-mix(in oklab, var(--ir-l-ice) 14%, transparent);
  --ice-fg:      var(--ir-l-ice-fg);

  --ok:    var(--ir-l-ice);
  --warn:  var(--ir-l-coral);
  --err:   var(--ir-l-coral-deep);
  --info:  var(--ir-l-ice);
  --gold:  var(--ir-l-tan);

  --grid-line: rgba(17, 18, 20, 0.04);
  --pop-bg:    rgba(255, 255, 255, 0.97);
  --shadow:    none;
  color-scheme: light;
}

/* ── Derived, identical in both twins ─────────────────────────
   These follow the ramp above, so they are declared once. */
html {
  /* Signal aliases. PURE aliases on purpose — 378a83a proved that a
     second hand-tuned set puts two different greens side by side. */
  --sig-ok:   var(--ok);
  --sig-warn: var(--warn);
  --sig-err:  var(--err);
  --sig-idle: var(--text-2);
  --sig-done: var(--text-3);

  /* Person state. Four distinguishable hues — blue, salmon, red,
     grey — and deliberately no tangerine: the brand book says a
     person's state is never the action colour. */
  --color-on-time: var(--ok);
  --color-late:    var(--warn);
  --color-absent:  var(--err);
  --color-pto:     var(--muted);
  --color-ai:      var(--brand);    /* a product marker, not a person */

  /* Support tab readouts — were their own blue, now the shared ramp. */
  --sup-accent:  var(--brand);
  --sup-readout: var(--ok);

  /* The blue ramp survives only because --blue-500/700 are still
     referenced by a few charts. Retinted onto ice so a stray
     reference cannot reintroduce a foreign hue. */
  --blue-500: var(--ok);
  --blue-700: var(--ok);
}

/* ── Legacy aliases ───────────────────────────────────────────
   Twenty-four token names are USED across the app and declared
   nowhere. Two different failures came out of that, and both are
   invisible:

   • `var(--ink, #333)` — the fallback fires, so the element renders
     a colour from a palette nobody has shipped in months. Eight of
     these were still painting: --acc #4f7fe4, --card #101014,
     --ink #333, --m-bg #000, --rule #eee, --surface-2, --bg-2 and
     --glass-shadow-ink.
   • `color: var(--danger)` with no fallback — the declaration is
     invalid at computed-value time and is DROPPED, so the element
     silently inherits whatever is above it. No error, no warning.

   Aliasing them here fixes both at once and costs nothing: a name
   that resolves is a name that cannot rot. Prefer the real token in
   new code — these exist so old call sites stop lying. */
html {
  --acc:        var(--brand);
  --m-accent:   var(--brand);
  --danger:     var(--err);
  --red:        var(--err);
  --green:      var(--ok);

  --ink:        var(--text);
  --fg-muted:   var(--text-3);

  --card:       var(--bg-elev-1);
  --bg-elev:    var(--bg-elev-1);
  --bg-1:       var(--bg-elev-1);
  --bg-2:       var(--bg-elev-2);
  --bg2:        var(--bg-elev-2);
  --bg-elev-3:  var(--bg-elev-2);
  --surface-2:  var(--bg-elev-2);
  --m-bg:       var(--bg);

  --rule:       var(--line);
  --border:     var(--line);
  --border-1:   var(--line);
  --bd:         var(--line);

  /* non-colour, same problem */
  --page-pad:    var(--pad-page);
  --r:           var(--r-md);
  --gl-dur:      180ms;
  --line-length: 1000;
}

/* Shadow ink differs per twin: a shadow on graphite wants true black,
   one on ivory wants the page's own ink so it reads as depth, not soot. */
html[data-theme="dark"],
html[data-aesthetic="editorial"][data-theme="dark"],
html.m-theme-dark,
.m-dark { --glass-shadow-ink: #000000; }

html[data-theme="light"],
html[data-aesthetic="editorial"][data-theme="light"],
html.m-theme-light,
.m-light { --glass-shadow-ink: #2b2a27; }

/* ── Categorical chart palette ────────────────────────────────
   Eight slots, assigned in FIXED ORDER and never cycled to make a
   ninth. A chart that needs more series needs "Other", small
   multiples, or a different form — not a generated hue.

   These are NOT the identity colours. Charts have a job identity
   does not: eight fills that stay apart for a colourblind reader on
   a given ground. --brand at L 0.713 sits outside the band that
   makes that work, so slot 1 is the tangerine HUE re-stepped into
   it — recognisably the brand, correctly legible as a fill.

   Each twin is selected against its own surface rather than flipped
   from the other. Both sets were generated at fixed OKLCH L/C by
   walking hue from the brand's 48°, then checked with the dataviz
   validator (six checks: lightness band, chroma floor, adjacent CVD
   separation, normal-vision floor, contrast):

     dark  on #111214 — L 0.62 C 0.14, all 8 PASS,
           worst adjacent deutan ΔE 10.8 · tritan 5.7 · normal 22.9
     light on #F2EFE7 — L 0.58 C 0.15, all 8 PASS,
           worst adjacent deutan ΔE 11.2 · tritan 5.9 · normal 24.6

   If you change one, re-run the validator. Do not eyeball it. */
html[data-theme="dark"],
html[data-aesthetic="editorial"][data-theme="dark"],
html.m-theme-dark,
.m-dark {
  --chart-1: #c86830;   /* tangerine hue, re-stepped */
  --chart-2: #288dd4;
  --chart-3: #5f983a;
  --chart-4: #a569bf;
  --chart-5: #8c8c00;
  --chart-6: #777ad8;
  --chart-7: #cd6151;
  --chart-8: #009f99;
}

html[data-theme="light"],
html[data-aesthetic="editorial"][data-theme="light"],
html.m-theme-light,
.m-light {
  --chart-1: #bf5914;
  --chart-2: #0080cc;
  --chart-3: #508d23;
  --chart-4: #9b5bb6;
  --chart-5: #818000;
  --chart-6: #6b6cd0;
  --chart-7: #c35141;
  --chart-8: #00948e;
}

/* ── The mark ─────────────────────────────────────────────────
   The roots grow, hold, then fade — the brand's own gesture, and
   the reason the app no longer needs a mascot. Reduced motion gets
   the mark at full root length, static, rather than nothing: the
   roots are part of the drawing, not decoration on top of it. */
.ir-mark { display: block; overflow: visible; }

/* Scoped to .ir-mark-grow, which is what <Mark animate>  puts on the svg.
   It was not, and that made `animate={false}` a no-op: the AI button and its
   popover icon — both permanently on screen — grew and faded their roots on a
   2.1s loop forever. Scoping it also fixes the other half of the same bug: a
   static mark now inherits no dasharray at all, so its roots are simply drawn,
   instead of being hidden at dashoffset 19 by a rule meant for the animation. */
.ir-mark-grow .ir-root {
  /* The dash has to equal the root's own length or the growth is not linear:
     a 19-unit dash on an 11-unit root fills in the first third of the travel
     and then sits there. A small mark overrides this and nothing else. */
  --ir-root-len: 19;
  stroke-dasharray: var(--ir-root-len);
  stroke-dashoffset: var(--ir-root-len);
  animation: ir-grow 2.1s ease-in-out infinite;
}
/* The cropped mark used at launcher/header size draws shorter roots, because a
   fading gradient smears below ~32px. */
.ir-mark-tight .ir-root { --ir-root-len: 11; }
.ir-mark-grow .ir-r2 { animation-delay: 0.18s; }
.ir-mark-grow .ir-r3 { animation-delay: 0.36s; }

@keyframes ir-grow {
  0%   { stroke-dashoffset: var(--ir-root-len); opacity: 1; }
  45%  { stroke-dashoffset: 0;  opacity: 1; }
  80%  { stroke-dashoffset: 0;  opacity: 1; }
  100% { stroke-dashoffset: 0;  opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ir-mark-grow .ir-root { animation: none; stroke-dashoffset: 0; opacity: 1; }
}

/* ── Boot splash ──────────────────────────────────────────────
   #m-boot is plain HTML in index.html that paints before React
   exists. It draws the app's own ground (the splash script stamps
   data-theme first) so it reads as the dimmed app rather than a
   separate grey screen, and fades out to reveal the same colour
   underneath. app.jsx adds .m-boot-gone once auth resolves. */
#m-boot {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  background: var(--bg);
  animation: m-boot-fade-in 240ms ease both;
}
#m-boot.m-boot-gone {
  animation: m-boot-fade-out 320ms ease both;
  pointer-events: none;
}

@keyframes m-boot-fade-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes m-boot-fade-out { from { opacity: 1; } to { opacity: 0; visibility: hidden; } }

/* Used by the refresh overlay in app.jsx. */
@keyframes m-boot-breathe {
  0%, 100% { transform: scale(1);    opacity: .92; }
  50%      { transform: scale(1.04); opacity: 1; }
}
