/* This app's design tokens. Its own file, served from its own origin.
 *
 * It began (Sept 2026) as a copy of poma-portal's portal/css/poma-tokens.css,
 * which this app used to <link> straight off poma.life — so its entire
 * appearance depended on another product staying up and unchanged. It is not
 * a mirror any more and is not re-synced: the values here are tuned for this
 * app (26 Oct 2026: a darker light-mode page and border, a deeper
 * --destructive; 3 Nov 2026: the Ice Blue light theme, and orange as the
 * default accent in both themes) and the comments below that still speak of
 * "every app" or "the portal" describe where the structure came from, not
 * who owns it now.
 */
/* ═══════════════════════════════════════════════════════════════════
   poma.life — Shared Design Tokens
   Served from https://poma.life/css/poma-tokens.css and linked by every
   app (portal, projects, model, hub). Cache-Control is no-cache, so edits
   here reach all four apps on the next portal deploy.

   TWO SYSTEMS LIVE IN THIS FILE:

   1. LEGACY `--poma-*` (bottom of file) — HSL channel triplets consumed as
      `hsl(var(--poma-bg))`. Used only by the portal's vanilla CSS/JS. Frozen:
      do not change these values, they are load-bearing for the current
      portal look. They retire when the portal static pass lands.

   2. CURRENT `--p-*` + shadcn channels (below) — the Blue Slate / Warm Slate
      system ported from Opum. OPT-IN: everything is scoped under
      `:root[data-poma-tokens]`, so an app sees none of it until its
      index.html declares `<html data-poma-tokens>`. That gate is what lets
      this ship ahead of the per-app migrations without moving a single
      pixel in an app that hasn't adopted yet.

   WHY TWO TOKEN SHAPES. The `--p-*` tokens are raw hex, for hand-written CSS
   and bespoke chrome. The unprefixed names (`--background`, `--card`,
   `--primary`…) are HSL *channels* for the shadcn/Tailwind layer that
   projects/model/portal-SPA are built on — setting those is what re-skins
   ~900-1100 existing utility classes per app for free.

   The `--p-` prefix is deliberate and is the one place this diverges from
   Opum's naming. Opum calls the raw tokens `--accent`/`--border`/`--success`
   and had to rename its shadcn channels to `--accent-ui`/`--border-ui`/
   `--success-ui` to stop the two colliding — a collision that fails silently
   (a hex where a channel triplet is expected yields no border, or
   currentColor). Prefixing the raw side instead keeps every poma app's
   existing Tailwind config untouched and makes the collision impossible.
   Colours are identical to Opum's; only the variable names differ.
   ═══════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════
   1. LIGHT — "Ice Blue"
   (3 Nov 2026; replaces "Warm Slate", the neutral grey this block held.)
   Calm ice-blue surfaces and deep navy-slate ink, taken from a reference
   Mike supplied: a frosted-glass phone UI on an ice-to-slate wallpaper. Its
   surfaces sampled at hue 198-207 (#C0D9E5, #D9E7EE, #EAEFF2) and its ink at
   #1A2A39, and that is the family every step below sits in — hue ~205 for
   surfaces, ~210 for text — so the light theme reads as one cool, quiet
   palette rather than as grey with a blue button on it.

   CARDS ARE ICE, THE PAGE IS THE PALE STEP (4 Nov 2026). The first cut of
   this theme had an ice page under WHITE cards, and the cards were what did
   not match: the reference's frosted widgets are ice themselves, laid over a
   lighter ground. So the order is now the reference's, and the tonal order
   Material 3 uses for filled cards: a pale ice page (#F2F7FB), ice cards a
   step deeper (#E1EEF7), an inset fill a step deeper again (#D7E6F1), and a
   rail deeper still, which the white-ish fields inside a card — inputs,
   active tabs, select triggers all paint --background — sit in as fields.

   HOW DEEP A CARD CAN GO IS SET BY THE ACCENTS, NOT BY TASTE. The accent is
   drawn as link text on cards, and orange (#B6460F) clears AA only on a
   surface at least as light as luminance 0.82. #E1EEF7 sits at 0.84 — about
   as ice as a card can be while every accent stays legible on it, and the
   same luminance as the page this block held before, so a ratio measured
   against "the ice" did not move. The reference's own widgets (#BDD8EB) are
   deeper than any accent could be read on.

   What a white card cost: every colour drawn ON a card loses about 15% of
   its contrast against an ice one. Measured on the card:
       --destructive 4.94 (5.83 on white)   muted text 6.84 (8.08)
       orange 4.59   gold 4.75   teal 4.71   steel 6.01   plum 6.05
   Of the hard-coded Tailwind text colours, two crossed the line and were
   moved a shade darker with this change (amber-700 and green-700 to -800);
   the rest either still clear or never cleared on white either.

   A card sits 1.10:1 off the page, which is a quiet step by design: its
   1.5px edge (1.56:1 on the card, 1.71 on the page) does the separating,
   as the frosted edges do in the reference. Floating surfaces (elev2) sit
   near-white, above everything, so a menu or popover opened from an ice
   card reads as a sheet over it.

   The text ramp has four steps because the apps use four, and it is darker
   than a typical grey scale: on a tinted (not white) background the usual
   #6B7280/#9CA3AF pairing measures 4.43 and 2.33, both below AA.
   ═══════════════════════════════════════════════════════════════════ */
:root[data-poma-tokens] {
  /* ── Raw surfaces ──────────────────────────────────────────────── */
  --p-bg:       #F2F7FB;   /* app background — the pale ice the cards sit on */
  --p-canvas:   #C4D9E8;   /* app frame — top bars, side rails; the deepest ice */
  --p-panel:    #E1EEF7;   /* cards, panels, tables — ice, after the reference's widgets */
  --p-elev:     #D7E6F1;   /* inset fill — tab strips, chips, secondary buttons */
  --p-elev2:    #F9FBFD;   /* modals, popovers, dropdowns, tooltips */
  --p-hover:    #D8E7F2;
  /* An ice-slate edge, a step deeper than it was over white so a card's edge
     keeps the weight the 26 Oct contrast pass settled on: 1.56:1 against
     the ice card, 1.71 against the page (that pass took a white card's edge
     from 1.38 to 1.65, which is what made a card hold its shape). */
  --p-border:   #AAC2D5;
  --p-borderS:  #92AEC4;   /* stronger border — inputs, dividers under load */

  /* ── Raw text ramp ────────────────────────────────────────────── */
  /* Navy-slate rather than grey, after the reference's #1A2A39.
                    on card   on page   on rail (its foot)
       text          13.94     15.27     10.44
       text2          6.84      7.49      5.12   (the rail's own labels)
       text3          5.66      6.20        —
       text4          3.56      3.90        —   (placeholder / faint only) */
  --p-text:     #13202F;   /* never pure black */
  --p-text2:    #3E5265;
  --p-text3:    #4B5E71;
  --p-text4:    #697E91;

  /* ── Raw status ───────────────────────────────────────────────── */
  /* Taken a step deeper with the ice cards (4 Nov 2026): the previous
     #15803D and #946200 measured 4.25 and 4.44 on the card. Now 4.65 and
     4.67. Danger matches --destructive, which it is the raw form of. */
  --p-success:  #147939;
  --p-warn:     #8F5F00;
  --p-danger:   #C52020;
  --p-info:     #1D4ED8;

  /* ── Status chips ─────────────────────────────────────────────── */
  /* A status needs two presentations, because one value cannot do both jobs.
     The tokens above are the colour as TEXT on a page surface, and on white
     that forces them dark — which is why a "Pending" chip built from
     --p-warn came out brown rather than yellow.
     A chip is a fill, an ink and an edge instead. The fill and edge carry the
     hue (so warn reads unmistakably YELLOW), and the ink is dark enough to
     stay legible on it. Every ink clears 4.5 against its own fill. */
  --p-success-fill: #ECFDF3; --p-success-ink: #14532D; --p-success-edge: #34C36B;
  --p-warn-fill:    #FEF6D9; --p-warn-ink:    #6B4A00; --p-warn-edge:    #E0AE1A;
  --p-danger-fill:  #FEF0F0; --p-danger-ink:  #991B1B; --p-danger-edge:  #EF7676;
  --p-info-fill:    #EFF4FF; --p-info-ink:    #1E40AF; --p-info-edge:    #7EA6F8;

  /* ── shadcn/Tailwind channels ─────────────────────────────────── */
  --background:           205 55% 96.6%; /* --p-bg */
  --foreground:           212 42% 13%;   /* --p-text */
  --card:                 204 58% 92.5%; /* --p-panel */
  --card-foreground:      212 42% 13%;
  --card-border:          206 34% 75%;   /* --p-border */
  --popover:              205 45% 98.5%; /* --p-elev2 */
  --popover-foreground:   212 42% 13%;
  --popover-border:       206 30% 67%;   /* --p-borderS */
  --secondary:            205 48% 89.3%; /* --p-elev */
  --secondary-foreground: 212 42% 13%;
  --muted:                205 48% 89.3%; /* --p-elev */
  --muted-foreground:     210 24% 32%;   /* --p-text2 */
  /* NB: shadcn's `--accent` is the hover/selection SURFACE, not the brand
     accent. The brand accent is --p-accent / --primary, set per accent below. */
  --accent:               205 50% 89.7%; /* --p-hover */
  --accent-foreground:    212 42% 13%;
  /* 45%, not 51%. --destructive is a FILL token — light carries white text on
     it — but 89 places across the app also read it as a TEXT colour, and at
     51% that measured 4.36:1 on --background, just under the 4.5 AA asks. It
     was the only surface that failed; dark is a light red on a dark page and
     clears easily.
  
     Darkening helps BOTH jobs at once: as text it goes 4.36 -> 5.30, and white
     on it as a button goes 4.80 -> 5.83. Six points of lightness, and the only
     cost is a slightly deeper red. On the ice card it reads at 4.94. */
  --destructive:          0 72% 45%;     /* --p-danger */
  --destructive-foreground: 0 0% 100%;
  --border:               206 34% 75%;   /* --p-border */
  --input:                206 34% 75%;

  /* ── Elevation & outline helpers (consumed by the projects app) ── */
  /* Tinted with the navy ink rather than pure black, so a hover or a pressed
     state darkens ice toward slate instead of greying it. */
  --button-outline: rgba(19,32,47, .10);
  --badge-outline:  rgba(19,32,47, .05);
  --opaque-button-border-intensity: -8;  /* percentage */
  --elevate-1: rgba(19,32,47, .035);
  --elevate-2: rgba(19,32,47, .08);

  /* ── Shadows ──────────────────────────────────────────────────── */
  /* Restrained by design. Separation is carried by the borders above — a
     dense UI with a shadow under every card turns to soup. Cast in a cool
     slate (hue 210) rather than the ink itself: a shadow on ice that leans
     blue reads as depth, one that leans grey reads as dirt. */
  --shadow-2xs: 0px 1px 2px 0px hsl(210 40% 22% / 0.05);
  --shadow-xs:  0px 1px 2px 0px hsl(210 40% 22% / 0.08);
  --shadow-sm:  0px 1px 3px 0px hsl(210 40% 22% / 0.10), 0px 1px 2px -1px hsl(210 40% 22% / 0.08);
  --shadow:     0px 2px 4px -1px hsl(210 40% 22% / 0.08), 0px 4px 6px -1px hsl(210 40% 22% / 0.10);
  --shadow-md:  0px 4px 6px -1px hsl(210 40% 22% / 0.10), 0px 2px 4px -2px hsl(210 40% 22% / 0.06);
  --shadow-lg:  0px 10px 15px -3px hsl(210 40% 22% / 0.10), 0px 4px 6px -4px hsl(210 40% 22% / 0.06);
  --shadow-xl:  0px 20px 25px -5px hsl(210 40% 22% / 0.10), 0px 8px 10px -6px hsl(210 40% 22% / 0.06);
  --shadow-2xl: 0px 25px 50px -12px hsl(210 40% 22% / 0.25);

  /* ── Categorical chart ramp ───────────────────────────────────── */
  /* Deliberately NOT derived from the accent: these encode unrelated series,
     so they must stay distinguishable from each other and stable when the
     user changes accent. Carried forward from the existing apps, which
     already tuned them per mode. */
  --chart-1: 239 84% 57%;
  --chart-2: 142 76% 28%;
  --chart-3: 24 85% 32%;
  --chart-4: 262 83% 35%;
  --chart-5: 173 58% 30%;

  /* ── Task dots (the run strip and the task map) ───────────────── */
  /* Status colour on a 6px dot. Done is --p-success; blocked is amber,
     because somebody has to act on it (red is late, and only late);
     running is a plain blue — the accent is not used, so the dots do not
     change meaning when a tenant picks plum. To-do is muted-foreground at
     35%, set where the dots are drawn. Each has a dark value below.
     Each of the three clears 3:1 (WCAG 1.4.11) on card, page, popover and
     muted in both themes — __tests__/runDotContrast.test.ts. Blocked was
     32 95% 44%, 2.67:1 on the ice card: the one dot that asks for somebody
     was the faintest of the three. Deepened and moved toward gold, so it
     stays amber and apart from the burnt-orange accent. */
  --dot-done:    142 72% 27.5%;
  --dot-running: 221 83% 53%;
  --dot-blocked: 36 100% 34%;

  /* ── Sidebar (shadcn convention) ──────────────────────────────── */
  /* A sidebar is the design's `canvas` surface — the app frame rather than a
     card — so it sits one step off the page rather than matching it. The
     primary/ring entries are references, not values, so a sidebar follows the
     user's accent choice without needing its own per-accent rules. */
  --sidebar-background: 205 41% 84%;   /* --p-canvas */
  --sidebar-foreground: 210 24% 32%;   /* --p-text2 */
  --sidebar-primary: var(--primary);
  --sidebar-primary-foreground: var(--primary-foreground);
  --sidebar-accent: 205 50% 89.7%;     /* --p-hover */
  --sidebar-accent-foreground: 212 42% 13%;
  --sidebar-border: 206 34% 75%;       /* --p-border */
  --sidebar-ring: var(--ring);

  /* ── App-specific extras ──────────────────────────────────────── */
  /* Dependency arrows on the projects Gantt. A dedicated token rather than a
     borrowed chart colour: --primary already means "now" and selection,
     green reads as done and red as danger, so links get a hue that means
     nothing else. Defined here so the app can delete its local copy. */
  --gantt-link: 195 85% 38%;
}


/* ═══════════════════════════════════════════════════════════════════
   2. DARK — "Blue Slate"
   Replaces the old near-black ramp (#0f172a…), which swallowed cards and
   controls: at ~1.0-1.1 contrast between layers there was nothing to
   separate a panel from the page except its border — and on projects that
   border was itself 1% off the card, so it was separated by nothing at all.

   The floor is raised to #15181D and every step carries a blue-grey
   undertone (hue ~215), so surfaces read as stacked layers rather than
   holes. panel→border is 1.49, which is what does the separating; shadows
   stay off in dark entirely.
   ═══════════════════════════════════════════════════════════════════ */
:root[data-poma-tokens].dark {
  --p-bg:       #15181D;
  --p-canvas:   #1C2128;
  --p-panel:    #242A33;
  --p-elev:     #2C3440;
  --p-elev2:    #323B49;
  --p-hover:    #2F3844;
  --p-border:   #3B4553;
  --p-borderS:  #4A5566;

  --p-text:     #F4F6F8;   /* never pure white */
  --p-text2:    #B6C0CC;
  --p-text3:    #A2ACBA;
  --p-text4:    #7C8797;

  --p-success:  #4ADE80;
  --p-warn:     #FACC15;
  --p-danger:   #FB8C8C;   /* lighter than the light-mode red — #DC2626 fails
                              AA on every surface in this ramp */
  --p-info:     #72B2FC;

  /* Chips, dark. Same three-part structure; the fills are deep tints of the
     hue rather than pale ones, each held clear of --p-panel so the chip is a
     surface rather than a hole. */
  --p-success-fill: #1B3326; --p-success-ink: #6EE79B; --p-success-edge: #2F6B47;
  --p-warn-fill:    #3A3312; --p-warn-ink:    #FBD24E; --p-warn-edge:    #6E5C18;
  --p-danger-fill:  #43262B; --p-danger-ink:  #FCA5A5; --p-danger-edge:  #6E3B3F;
  --p-info-fill:    #1E2E45; --p-info-ink:    #8FC2FD; --p-info-edge:    #345A85;

  --background:           218 16% 10%;   /* --p-bg */
  --foreground:           210 22% 96%;   /* --p-text */
  --card:                 216 17% 17%;   /* --p-panel */
  --card-foreground:      210 22% 96%;
  --card-border:          215 17% 28%;   /* --p-border */
  /* Popovers/dropdowns/tooltips ride the elevated surface, so they read as
     floating above the card they were opened from rather than merging. */
  --popover:              217 19% 24%;   /* --p-elev2 */
  --popover-foreground:   210 22% 96%;
  --popover-border:       216 16% 35%;   /* --p-borderS */
  --secondary:            216 19% 21%;   /* --p-elev */
  --secondary-foreground: 210 22% 96%;
  --muted:                216 19% 21%;
  --muted-foreground:     213 18% 76%;   /* --p-text2 */
  --accent:               217 19% 24%;   /* --p-elev2 (hover/selection surface) */
  --accent-foreground:    210 22% 96%;
  --destructive:          0 93% 77%;     /* --p-danger */
  --destructive-foreground: 200 30% 6%;  /* near-black — white fails on this red */
  --border:               215 17% 28%;   /* --p-border */
  --input:                215 17% 28%;

  --button-outline: rgba(255,255,255, .10);
  --badge-outline:  rgba(255,255,255, .05);
  --opaque-button-border-intensity: 9;
  --elevate-1: rgba(255,255,255, .04);
  --elevate-2: rgba(255,255,255, .09);

  --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.12);
  --shadow-xs:  0px 1px 2px 0px hsl(0 0% 0% / 0.16);
  --shadow-sm:  0px 1px 3px 0px hsl(0 0% 0% / 0.20), 0px 1px 2px -1px hsl(0 0% 0% / 0.20);
  --shadow:     0px 2px 4px -1px hsl(0 0% 0% / 0.12), 0px 4px 6px -1px hsl(0 0% 0% / 0.20);
  --shadow-md:  0px 4px 6px -1px hsl(0 0% 0% / 0.20), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
  --shadow-lg:  0px 10px 15px -3px hsl(0 0% 0% / 0.25), 0px 4px 6px -4px hsl(0 0% 0% / 0.15);
  --shadow-xl:  0px 20px 25px -5px hsl(0 0% 0% / 0.30), 0px 8px 10px -6px hsl(0 0% 0% / 0.18);
  --shadow-2xl: 0px 25px 50px -12px hsl(0 0% 0% / 0.45);

  --sidebar-background: 215 18% 13%;   /* --p-canvas */
  --sidebar-foreground: 213 18% 76%;   /* --p-text2 */
  --sidebar-primary: var(--primary);
  --sidebar-primary-foreground: var(--primary-foreground);
  --sidebar-accent: 217 19% 24%;       /* --p-elev2 */
  --sidebar-accent-foreground: 210 22% 96%;
  --sidebar-border: 215 17% 28%;       /* --p-border */
  --sidebar-ring: var(--ring);

  --chart-1: 239 84% 67%;
  --chart-2: 142 76% 55%;
  --chart-3: 24 85% 58%;
  --chart-4: 262 83% 62%;
  --chart-5: 173 58% 52%;

  --dot-done:    142 69% 58%;
  --dot-running: 213 94% 68%;
  --dot-blocked: 43 96% 56%;

  --gantt-link: 195 85% 62%;
}


/* ═══════════════════════════════════════════════════════════════════
   3. ACCENTS
   Each accent is ONE hue at two brightnesses — a bright tint that survives a
   dark surface, and a deep shade that survives a white one. The hues are held
   within a few degrees across the pair so the two themes read as day and
   night of the same product rather than as two identities.

   The light shades are deep BY NECESSITY, not taste: the accent is used as
   link text on cards, and every mid-brightness candidate fails AA there
   (teal #14B8A6 measures 2.49 even on white).

   Light is measured on the ICE CARD (#E1EEF7), not on white: since 4 Nov
   2026 the card is the darkest surface accent text commonly sits on, and the
   page is lighter. The dark hues are checked as TEXT against --p-elev2
   (#323B49, the worst case), because the accent is used far more as text —
   links, active nav, stat figures — than as a fill. Anything under 4.5:1 on
   either is rejected.

                     as text, dark vs #323B49   light vs ice card #E1EEF7
       orange (default)       4.52                   4.59
       gold                   5.56                   4.75
       teal                   4.54                   4.71
       steel                  5.23                   6.01
       plum                   5.24                   6.05

   INDIGO AND PURPLE WERE RETIRED (4 Nov 2026, Mike's call: neither looked
   right in either theme). Steel and plum took their places: steel is the
   ice theme's own blue (hue 210, desaturated), plum a muted berry (hue 318)
   in place of an electric violet. Both are held well clear of what they
   replace in dark, where indigo and purple were pale lavenders — a pale blue
   like #92B2EA was tried for steel and dropped because it read as indigo
   again. A cookie still holding `indigo` or `purple` matches no rule here
   and falls through to the default, the graceful retirement theme.js
   describes; the picker then shows no accent selected until one is chosen.

   THE ACCENT MAY NOT OCCUPY A STATUS HUE. Red means destructive/failed/
   overdue across every app, amber means warning, green means success. An
   accent that reads as a status is worse than one fewer choice, so red,
   amber/mustard and green are all excluded and the set is three.

   This is not a matter of taste — the hue space is genuinely occupied. Both
   mustard and green were carried over from the source system and had to be
   dropped after measurement: on white, mustard resolved to #946200 and green
   to #15803D, which are the EXACT hexes of --p-warn and --p-success. They
   collide because the accent and the status ramp are derived under the same
   constraint (deep enough to clear AA on a white card), and that constraint
   has one answer per hue. Searching for a non-colliding value in the same
   family only makes it worse: staying legible while separating from the
   status colour forces mustard to #5C3600, which is brown rather than
   mustard, and for green on the DARK surface there is no solution at all —
   every green that clears 4.5 against --p-elev2 is indistinguishable from
   --p-success.

   Teal, steel and plum sit in hues that carry no status meaning, which is
   why they are offered. (Steel does share blue with --gantt-link, the Gantt's
   dependency arrows, and in dark the two would be the same light blue — so
   choosing steel moves the arrows to teal; see its rule below.)

   GOLD IS THE DELIBERATE EXCEPTION (added 2026-08-24 at Mike's request, with
   the brand gold #DFAE33 supplied). It shares the warn hue family, and the
   reason it works where mustard did not is that warn was restructured at the
   same time: a status is now a chip (fill + ink + edge), so warn presents as
   a PALE YELLOW rather than as the deep amber text colour that mustard
   duplicated exactly. Deep gold accent against pale yellow chip is a 5.19
   (light) / 6.16 (dark) separation. Hue proximity is therefore accepted here
   and enforced as a luminance separation instead — see the accent tests.

   ORANGE IS THE SECOND EXCEPTION, AND THE DEFAULT (3 Nov 2026, Mike's call,
   with the Ice Blue light theme). The colour asked for is the coral
   highlight in the reference image, #E05A40 at hue 10°. As supplied it
   cannot be an accent in either theme: it measures 3.68 as text on white,
   3.07 on the ice page and 3.07 on --p-elev2, and white on it is 3.68. The
   nearest hue-10° shade that clears AA, #C13A1F, measures 1.12:1 against
   --destructive (#C52020) — a second red, so every primary button would
   read as a delete button.

   So the hue moved as well as the lightness: 10° toward orange, to 20°, and
   deep enough to clear AA, which is the same treatment gold gets. Here the
   separation from a status CANNOT be luminance, as it is for gold — any
   orange that clears AA on white lands at the same luminance as both
   --destructive (1.07:1) and --p-warn (1.04:1), the one-answer-per-hue
   constraint described above. It is hue: 20° from red (hue 0°) on one side
   and 20° from warn (hue 40°, and a pale yellow CHIP in use) on the other.
   Side by side the three read as orange, red and brown-gold. Dark is the
   same hue at 18°, held off the dark --destructive's pink (#FB8E8E, 0°) by
   hue and by saturation.

   ACCENT INK (4 Nov 2026). The accent is also drawn as TEXT ON A WASH OF
   ITSELF — selected chips, toggles, tabs, status badges, about 30 sites —
   and there the plain accent fell short in BOTH themes. Light, at its worst
   (a 25% wash over the inset fill): orange 3.02, gold 3.19, teal 3.16,
   steel 3.92, plum 3.89. Dark is fine on the common 10% wash over a card
   (4.9-5.9) but not on anything heavier or higher: orange 3.09 at 25% over
   --p-elev2, the rest 3.08-3.50. A status needs a fill, an ink and an edge
   (section 1, status chips); so does the accent on a tint.

   --primary-ink is the same hue moved AWAY from its wash — deeper in light,
   lighter in dark — until it clears 4.5 on its own 5-25% wash over every
   surface a wash sits on (light: page, card, inset fill, popover; dark: bg,
   canvas, card, elev, elev2). Worst cases, light / dark:
       orange 4.66 / 4.63   gold 4.75 / 4.61   teal 4.73 / 4.63
       steel  4.62 / 4.64   plum 4.65 / 4.63
   and about 6:1 on a typical 10-15% chip in light.

   It is applied by one rule in index.css, not per site: text-primary reads
   --primary-text (tailwind.config.ts), which is --primary everywhere except
   on an element carrying a wash — bg-primary/5 to /25, their hover: forms
   while hovered, data-[state=on]: while on — and its descendants, where it
   is --primary-ink. Fills, borders and rings never change, so a primary
   button inside a tinted panel is the same button as anywhere; neither does
   the rail's active pill, which has its own white base (.rail-active).

   Specificity note: the `.dark[data-accent=…]` rules are (0,4,0) and so beat
   both the plain accent rules (0,3,0) and the dark surface block (0,3,0)
   regardless of source order. Nothing here depends on ordering.
   ═══════════════════════════════════════════════════════════════════ */

/* ── Orange — this app's default. Applies when data-accent is absent. ─
   theme.js names it as the default (PREFS.accent.def), which is what makes
   it the absent case; the two have to change together. */
:root[data-poma-tokens] {
  --p-accent: #B6460F;
  --p-accent-tint:        #B6460F29;  /* ~16% — the wash behind selected nav,
                                         active pills and hover rows */
  --p-accent-tint-strong: #B6460F44;
  --primary:            20 85% 38.5%;
  --primary-foreground: 0 0% 100%;
  --ring:               20 85% 38.5%;
  --primary-ink:        20 85% 28.5%;  /* #86340B — text on the accent's own wash */
}
:root[data-poma-tokens].dark {
  --p-accent: #F2875A;
  --p-accent-tint:        #F2875A29;
  --p-accent-tint-strong: #F2875A44;
  --primary:            18 85% 65%;
  --primary-foreground: 200 30% 6%;   /* near-black — white fails on every
                                         one of the dark accent hues */
  --ring:               18 85% 65%;
  --primary-ink:        18 85% 80%;    /* #F7BBA1 — lighter, as dark inks are */
}

/* ── Steel — the ice theme's own blue (in indigo's place, 4 Nov 2026) ── */
/* --gantt-link moves to teal under steel. The arrows were given a hue that
   means nothing else (section 1), and with steel as the accent, blue means
   "now" and "selected": in dark the two measured 1.10:1, the same light
   blue. Teal is free while steel is the accent. */
:root[data-poma-tokens][data-accent="steel"] {
  --p-accent: #325B83;
  --p-accent-tint: #325B8329; --p-accent-tint-strong: #325B8344;
  --primary: 210 45% 35.5%; --primary-foreground: 0 0% 100%; --ring: 210 45% 35.5%;
  --primary-ink: 210 45% 31.5%;  /* #2C5074 */
  --gantt-link: 173 76% 26%;
}
:root[data-poma-tokens].dark[data-accent="steel"] {
  --p-accent: #81B5E9;
  --p-accent-tint: #81B5E929; --p-accent-tint-strong: #81B5E944;
  --primary: 210 70% 71%; --primary-foreground: 200 30% 6%; --ring: 210 70% 71%;
  --primary-ink: 210 70% 83%;  /* #B5D4F2 */
  --gantt-link: 173 80% 40%;
}

/* ── Teal (Opum's default, offered here as an alternate) ─────────── */
:root[data-poma-tokens][data-accent="teal"] {
  --p-accent: #107569;
  --p-accent-tint: #10756929; --p-accent-tint-strong: #10756944;
  --primary: 173 76% 26%; --primary-foreground: 0 0% 100%; --ring: 173 76% 26%;
  --primary-ink: 173 76% 19.5%;  /* #0C584F */
}
:root[data-poma-tokens].dark[data-accent="teal"] {
  --p-accent: #14B8A6;
  --p-accent-tint: #14B8A629; --p-accent-tint-strong: #14B8A644;
  --primary: 173 80% 40%; --primary-foreground: 200 30% 6%; --ring: 173 80% 40%;
  --primary-ink: 173 60% 64.5%;  /* #6EDBCE — less saturated than the accent,
                                    which at this lightness goes neon */
}

/* ── Plum — a muted berry (in purple's place, 4 Nov 2026) ─────────── */
/* Hue 318: 42° from --destructive's red, which it measures 1.23:1 against
   in light — a wine beside a red, not a second red. */
:root[data-poma-tokens][data-accent="plum"] {
  --p-accent: #873D71;
  --p-accent-tint: #873D7129; --p-accent-tint-strong: #873D7144;
  --primary: 318 38% 38.5%; --primary-foreground: 0 0% 100%; --ring: 318 38% 38.5%;
  --primary-ink: 318 38% 33.5%;  /* #763562 */
}
:root[data-poma-tokens].dark[data-accent="plum"] {
  --p-accent: #E797CF;
  --p-accent-tint: #E797CF29; --p-accent-tint-strong: #E797CF44;
  --primary: 318 63% 75%; --primary-foreground: 200 30% 6%; --ring: 318 63% 75%;
  --primary-ink: 318 63% 85%;  /* #F1C1E2 */
}

/* ── Gold ─────────────────────────────────────────────────────────── */
/* The one accent that shares a hue family with a status (warn). It is only
   viable because warn now presents as a CHIP: the accent is a deep gold at
   43°, the warn chip is a pale yellow fill, and they sit 5.19 (light) and
   6.16 (dark) apart in contrast — never mistakable as swatches, even though
   the hue is close. The dark value is the brand gold as supplied; the light
   value is the same hue taken deep enough to clear AA on the ice card, which
   the brand gold itself cannot do on any light surface (2.05 even on white).
   4 Nov 2026: #996E00 became #876100 with the ice cards — #996E00 measured
   3.88 on the card, the new value 4.75. */
:root[data-poma-tokens][data-accent="gold"] {
  --p-accent: #876100;
  --p-accent-tint: #87610029; --p-accent-tint-strong: #87610044;
  --primary: 43 100% 26.5%; --primary-foreground: 0 0% 100%; --ring: 43 100% 26.5%;
  --primary-ink: 43 100% 20%;  /* #664900 */
}
:root[data-poma-tokens].dark[data-accent="gold"] {
  --p-accent: #DFAE33;
  --p-accent-tint: #DFAE3329; --p-accent-tint-strong: #DFAE3344;
  --primary: 43 73% 54%; --primary-foreground: 200 30% 6%; --ring: 43 73% 54%;
  --primary-ink: 43 73% 71.5%;  /* #EBCD81 */
}

/* ═══════════════════════════════════════════════════════════════════
   4. DENSITY, TYPE SCALE & GEOMETRY

   Density scales the rhythm tokens by 0.88 / 1.0 / 1.14. It intentionally
   does NOT scale the type ramp: shrinking text is a legibility decision, not
   a density one, and the two get conflated the moment they share a control.

   The type ramp is defined but NOT applied to body here. The apps currently
   run on Tailwind's 14/16px defaults; adopting a 12.5px base is a per-app
   decision that belongs with each app's migration, not with the token drop.
   ═══════════════════════════════════════════════════════════════════ */
:root[data-poma-tokens] {
  /* Rhythm — balanced (default) */
  --p-row-h:  30px;
  --p-gap:    12px;
  --p-gap-lg: 20px;

  /* Type ramp */
  --p-fs-micro: 10.5px;   /* dot labels, table superscripts */
  --p-fs-cap:   11.5px;   /* captions, field labels */
  --p-fs-body:  12.5px;
  --p-fs-sub:   13.5px;
  --p-fs-h3:    17px;
  --p-fs-h2:    22px;
  --p-fs-h1:    28px;

  /* Faces. Geist needs a <link> in the app's index.html; until then this
     degrades to Inter, which every app already loads. Geist Mono is
     load-bearing for the numeric treatment — every figure renders in it, so
     columns of numbers align and digits stop shifting width as they update.
     CJK faces trail the Latin ones: neither Geist nor Inter carries Chinese
     glyphs, so without them a share link opened in Chinese falls back to
     whatever the browser picks, which varies by OS and often mismatches the
     Latin text mid-sentence. */
  /* 'Geist Variable' is the family name Fontsource registers for the
     self-hosted build, which is how the apps ship it (Google Fonts is blocked
     in mainland China and our Shenzhen partners open share links from there).
     Plain 'Geist' follows for any context that does load it from Google. */
  --p-font-sans: 'Geist Variable', 'Geist', 'Inter Variable', Inter, -apple-system,
    BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB',
    'Microsoft YaHei', 'Noto Sans SC', sans-serif;
  --p-font-mono: 'Geist Mono Variable', 'Geist Mono', 'JetBrains Mono Variable',
    'JetBrains Mono', ui-monospace, 'Courier New', monospace;

  /* Corners. House radius is 6px; 10px for panels and modals. */
  --p-radius:    6px;
  --p-radius-lg: 10px;
  --radius:      6px;   /* shadcn's knob, kept in step with --p-radius */
}

:root[data-poma-tokens][data-density="compact"] {
  --p-row-h: 26px; --p-gap: 11px; --p-gap-lg: 18px;
}
:root[data-poma-tokens][data-density="roomy"] {
  --p-row-h: 34px; --p-gap: 14px; --p-gap-lg: 23px;
}


/* ═══════════════════════════════════════════════════════════════════
   4b. LEGACY BRIDGE
   Re-points the `--poma-*` tokens at the current palette — but ONLY inside
   the gate.

   The reason this exists: poma-panels.css and poma-messenger.css are shared
   widgets, loaded by projects, model and hub as well as the portal, and they
   style themselves from `--poma-*`. Once an app opts into the token system
   its own chrome is Blue Slate while those widgets would still be painting
   the retired ramp — the same palette, two eras, on one screen.

   Because the bridge is scoped to `[data-poma-tokens]`, the widgets follow
   whichever app they are embedded in: new palette inside a migrated app,
   untouched inside the portal's un-migrated static pages, which still read
   the frozen values in section 6.

   These are channel triplets, so they can reference the shadcn channels
   directly — `hsl(var(--poma-bg))` in the widget CSS resolves through to
   `--background`. The `/ 0.n` entries work for the same reason: the value is
   substituted textually inside the consumer's own hsl().
   ═══════════════════════════════════════════════════════════════════ */
:root[data-poma-tokens] {
  --poma-bg:             var(--background);
  --poma-surface:        var(--card);
  --poma-surface-raised: var(--secondary);
  --poma-text:           var(--foreground);
  --poma-text-muted:     var(--muted-foreground);
  --poma-text-faint:     209 16% 49%;        /* --p-text4 */
  --poma-primary:        var(--primary);
  /* No distinct hover shade: --primary is one value, and a channel triplet
     cannot be color-mix()ed. The widgets that hover carry their own opacity
     shift, so this reads as "no extra tint" rather than as a broken state. */
  --poma-primary-hover:  var(--primary);
  --poma-primary-subtle: var(--primary) / 0.15;
  --poma-success:        142 72% 27.5%;      /* --p-success */
  --poma-warning:        40 100% 28%;        /* --p-warn */
  --poma-danger:         var(--destructive);
  --poma-border:         var(--border);
  --poma-border-subtle:  var(--border) / 0.4;
  --poma-input-bg:       var(--secondary);
  --poma-input-border:   var(--input);
  --poma-ring:           var(--ring);
  --poma-font-sans:      var(--p-font-sans);
  --poma-font-mono:      var(--p-font-mono);
  --poma-glass-bg:       rgba(255, 255, 255, 0.55);
  --poma-glass-bg-heavy: rgba(255, 255, 255, 0.85);
  --poma-glass-border:   rgba(170, 194, 213, 0.5);   /* --p-border */
}

:root[data-poma-tokens].dark {
  --poma-text-faint:     216 11% 54%;        /* --p-text4, dark */
  --poma-success:        142 69% 58%;        /* --p-success, dark */
  --poma-warning:        48 96% 53%;         /* --p-warn, dark */
  --poma-glass-bg:       rgba(36, 42, 51, 0.55);     /* --p-panel */
  --poma-glass-bg-heavy: rgba(36, 42, 51, 0.85);
  --poma-glass-border:   rgba(59, 69, 83, 0.5);      /* --p-border, dark */
}


/* ═══════════════════════════════════════════════════════════════════
   5. GATED GLOBALS
   Selection and scrollbar chrome, token-aware. Scoped to the gate so an
   un-migrated app keeps the legacy dark-tuned versions at the bottom of
   this file.
   ═══════════════════════════════════════════════════════════════════ */
:root[data-poma-tokens] ::selection {
  background-color: var(--p-accent-tint-strong);
  color: var(--p-text);
}
:root[data-poma-tokens] ::-webkit-scrollbar { width: 8px; height: 8px; }
:root[data-poma-tokens] ::-webkit-scrollbar-track { background: transparent; }
:root[data-poma-tokens] ::-webkit-scrollbar-thumb {
  background: var(--p-borderS);
  border-radius: 4px;
}
:root[data-poma-tokens] ::-webkit-scrollbar-thumb:hover { background: var(--p-text4); }

/* Firefox equivalent — thin, and coloured from the same two tokens. */
:root[data-poma-tokens] { scrollbar-width: thin; scrollbar-color: var(--p-borderS) transparent; }


/* ═══════════════════════════════════════════════════════════════════
   6. LEGACY `--poma-*` — FROZEN
   Values below are unchanged from the pre-Blue-Slate file. The portal's
   vanilla CSS/JS reads them (~30 references) and the glass utilities are
   used by the portal shell. Projects, model and hub link this file but
   reference none of these — for them it is inert.

   Do not add to this block. New work uses the `--p-*` system above.
   ═══════════════════════════════════════════════════════════════════ */
:root {
  /* Backgrounds */
  --poma-bg:              222 47% 11%;     /* #0f172a */
  --poma-surface:         217 33% 17%;     /* #1e293b */
  --poma-surface-raised:  217 33% 20%;     /* #253348 */

  /* Text */
  --poma-text:            214 32% 91%;     /* #e2e8f0 */
  --poma-text-muted:      215 20% 65%;     /* #94a3b8 */
  --poma-text-faint:      217 19% 45%;     /* #5d6b82 */

  /* Brand / Primary (Indigo) */
  --poma-primary:         239 84% 67%;
  --poma-primary-hover:   239 84% 57%;
  --poma-primary-subtle:  239 84% 67% / 0.15;

  /* Status */
  --poma-success:         160 84% 39%;
  --poma-warning:         45 93% 47%;
  --poma-danger:          0 84% 60%;

  /* Borders */
  --poma-border:          217 33% 18%;
  --poma-border-subtle:   217 33% 18% / 0.4;

  /* Input */
  --poma-input-bg:        217 33% 15%;
  --poma-input-border:    217 33% 25%;
  --poma-ring:            239 84% 67%;

  /* Typography */
  --poma-font-sans:       'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --poma-font-mono:       'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
  --poma-text-xs:         0.6875rem;
  --poma-text-sm:         0.8125rem;
  --poma-text-base:       0.875rem;
  --poma-text-lg:         1.125rem;
  --poma-text-xl:         1.5rem;

  /* Spacing (4px base) */
  --poma-space-1:         0.25rem;
  --poma-space-2:         0.5rem;
  --poma-space-3:         0.75rem;
  --poma-space-4:         1rem;
  --poma-space-5:         1.25rem;
  --poma-space-6:         1.5rem;
  --poma-space-8:         2rem;

  /* Radius */
  --poma-radius-sm:       0.375rem;
  --poma-radius-md:       0.5rem;
  --poma-radius-lg:       0.75rem;
  --poma-radius-xl:       1rem;

  /* Shadows */
  --poma-shadow-sm:       0 1px 2px rgba(0, 0, 0, 0.3);
  --poma-shadow-md:       0 4px 6px rgba(0, 0, 0, 0.3);
  --poma-shadow-lg:       0 10px 25px rgba(0, 0, 0, 0.4);
  --poma-shadow-xl:       0 25px 50px rgba(0, 0, 0, 0.5);

  /* Glass */
  --poma-glass-bg:        rgba(15, 23, 42, 0.5);
  --poma-glass-bg-heavy:  rgba(15, 23, 42, 0.8);
  --poma-glass-border:    rgba(30, 41, 59, 0.4);
  --poma-glass-blur:      30px;
  --poma-glass-blur-sm:   4px;
  --poma-glass-blur-md:   12px;
}

.poma-glass {
  background: var(--poma-glass-bg);
  backdrop-filter: blur(var(--poma-glass-blur));
  -webkit-backdrop-filter: blur(var(--poma-glass-blur));
  border: 1px solid var(--poma-glass-border);
  border-radius: var(--poma-radius-xl);
}

.poma-glass-heavy {
  background: var(--poma-glass-bg-heavy);
  backdrop-filter: blur(var(--poma-glass-blur));
  -webkit-backdrop-filter: blur(var(--poma-glass-blur));
  border: 1px solid var(--poma-glass-border);
  border-radius: var(--poma-radius-xl);
}

/* Legacy selection + scrollbar. `:root:not([data-poma-tokens])` keeps these
   off migrated apps, which get the token-aware versions in section 5. */
:root:not([data-poma-tokens]) ::selection {
  background-color: rgba(99, 102, 241, 0.3);
  color: hsl(var(--poma-text));
}
:root:not([data-poma-tokens]) ::-webkit-scrollbar { width: 6px; height: 6px; }
:root:not([data-poma-tokens]) ::-webkit-scrollbar-track { background: rgba(30, 41, 59, 0.3); }
:root:not([data-poma-tokens]) ::-webkit-scrollbar-thumb { background: rgba(71, 85, 105, 0.5); border-radius: 3px; }
:root:not([data-poma-tokens]) ::-webkit-scrollbar-thumb:hover { background: rgba(71, 85, 105, 0.7); }

/* Legacy light mode — overrides the dark defaults when .dark is absent. */
:root:not(.dark) {
  --poma-bg:             0 0% 97%;
  --poma-surface:        210 17% 95%;
  --poma-surface-raised: 210 17% 92%;
  --poma-text:           222 47% 11%;
  --poma-text-muted:     215 20% 40%;
  --poma-text-faint:     217 19% 58%;
  --poma-glass-bg:       rgba(255, 255, 255, 0.55);
  --poma-glass-bg-heavy: rgba(255, 255, 255, 0.85);
}
