/* ============================================================================
   MinMax Pro - design system
   Emerald operator console: deep ink-green chrome, vivid emerald brand, warm
   paper surfaces. Bricolage Grotesque display + Plus Jakarta Sans body.
   ========================================================================== */
/* Fonts (Bricolage Grotesque / Plus Jakarta Sans / JetBrains Mono) are loaded
   by <link rel="preconnect"> + <link rel="stylesheet"> tags in every HTML
   shell that links this file (app, portal, field, enroll; login and index
   already carried their own). They used to be an @import here, which
   serializes: the browser cannot even REQUEST the font CSS until theme.css
   has fully downloaded and parsed. If a new shell links theme.css, copy the
   three <link> tags from app.html - the URL must match login.html's exactly. */

/* ── Icon size floor ────────────────────────────────────────────────────────
   An <svg> that carries a viewBox and no intrinsic size expands to fill its
   container. Every icon in this app is a 24-grid stroke glyph meant to render
   at 13–26px, and each one gets there from a rule written for its specific
   container (`.btn svg`, `.stat-go svg`, `.fp-tab svg`, `.ac-card-ic svg`, …).
   Miss one of those rules - or ship markup whose stylesheet hasn't reached the
   browser yet - and the icon does not degrade, it detonates: a 15px corner mark
   renders as a 144px black shape across the middle of the card.

   This is the floor that stops that. It is inside an @layer on purpose: every
   other rule in this codebase is unlayered, and unlayered styles beat layered
   ones no matter their specificity, so this can never win against a real rule -
   not even a bare `svg { }`. It only applies where nothing else has an opinion.

   Scoped to the 24-grid viewBox so it cannot touch a chart: charts.js builds
   every SVG with a `0 0 <width> <height>` viewBox from real pixel dimensions,
   never `0 0 24 24`. Verified across all 20 console routes plus the field
   portal. Keep it that way - do not widen the selector to bare `svg`. */
@layer icon-floor {
  svg[viewBox="0 0 24 24"] { width: 16px; height: 16px; }
}

/* ── CANVAS POLARITY ─────────────────────────────────────────────────────────
   The product is LIGHT: cool off-white paper, white cards, a cyan brand, and a
   lifted graphite rail. One palette across all four modes - see the scheme note
   further down for why the per-mode accent was retired.

   DARK IS A TENANT CHOICE, not a product mode. A brand pack that paints a dark
   canvas (brand-packs/minmax.json is the reference one) gets every surface token
   written INLINE on :root by brand.js - see darkCanvas() - and applyBrand()
   stamps data-brand-polarity="dark". The handful of things a token re-point
   cannot carry are collected in the [data-brand-polarity="dark"] blocks in this
   file; there is no other dark styling anywhere and there should not be.

   Surfaces that stay light regardless of the tenant's pack:
     • the client portal - portal-skin.css re-points every token under
       html[data-portal="on"] ("Daylight"). A dark client portal was proposed
       and rejected; do not flip it.
     • the Field Portal WORKSHEET - field-v2.css. Light because of sunlight, not
       style: it has to be readable on a tablet in a driveway.
     • print - the @media print block forces white paper with !important, over
       inline pack tokens included.

   Because all of those work by RE-POINTING TOKENS, every rule in this file must
   stay token-driven. A literal #ffffff in a component rule is invisible to them
   and will leak a white card onto a dark tenant's page. Reach for var(--card) /
   var(--line) / var(--tx), never the hex - an inline pack token then simply
   wins, with no !important and without this file knowing the pack exists. */
:root {
  color-scheme: light;

  /* Brand - CYAN on a light canvas. --ink is the CHROME near-black: heading ink
     and the toast/tooltip surface, not the page. */
  --ink:        #0d1b20;   /* deep cyan-charcoal - chrome + headings */
  --ink-2:      #13262c;
  --ink-3:      #1b3138;
  /* --teal is the accent AS TEXT - links, eyebrows, active labels - so it is the
     cyan stepped down until it passes on paper (4.7:1 on white). The brand cyan
     itself is #00E5FF and lives in --teal-bright, where it is used for fills and
     glows rather than for text. This is the same pair the brand engine derives
     (--brand-primary #00E5FF → --brand-primary-text #007c89); keeping the
     stylesheet's default in step with it means an unbranded org and a
     cyan-branded org render the same, instead of one being legible and the
     other not. */
  --teal:       #007c89;
  --teal-deep:  #00626d;   /* darker still - hover, pressed, deep labels */
  --teal-bright:#00E5FF;   /* THE brand cyan - fills, glows, the wash */
  --coral:      #f4744e;   /* attention accent */
  /* NO GOLD (Jeremy, 2026-09-20). The token NAME survives because ~200 call
     sites read it; its VALUE is the soft-coral step of the warning band - the
     early "look at this" below --coral, never amber. A neutral accent tile is
     .stat.is-gold, which paints the mint secondary instead. */
  --gold:       #f79a7c;   /* soft coral - early warning */
  /* Their text steps, same split as --ok/--ok-tx. Both of these are drawn to be
     seen as a FILL - a tile's accent rail, a chart bar, a star - and both were
     also being spent on words: a 21px gold stat value measured 1.75:1 on a card.
     Reach for these the moment the color becomes a glyph or a number. */
  --coral-tx:   #b05438;
  --gold-tx:    #a34a2e;   /* 5.9:1 on white */

  /* Secondary harmonizing accent - the mint-green thread that runs under the
     cyan (the brand pair is cyan + #2EE0A8). */
  --accent-2:        #2EE0A8;
  --accent-2-bright: #6BEDC4;
  --accent-2-deep:   #1b8261;   /* the mint stepped down until it reads AS TEXT on paper */

  /* Atmosphere - shared "pre-login" feel: glow + faint grid on dark chrome.
     --glow follows the mode accent; --glow-2 is the secondary thread. */
  --glow:    var(--teal);
  --glow-2:  var(--accent-2);
  --grid-ln: color-mix(in srgb, var(--ink) 5.5%, transparent);

  /* ── GRAIN ───────────────────────────────────────────────────────────────
     A fractal-noise tile laid over the canvas wash (components.css, the top
     layer of body::before). Two flat gradients meeting on a near-white page is
     what makes a light UI read as a lit screen rather than as a surface; a few
     percent of noise gives the eye something to land on and takes the glare off
     without moving a single color.

     It is a TOKEN and not a literal in components.css because it has to flip
     polarity: the speckle is black on paper and white on a dark pack's canvas,
     which a color re-point cannot express - an SVG data URI cannot read a
     custom property, so there are two of them and the dark block below swaps
     the whole image.

     The recipe: feTurbulence → feColorMatrix zeroes RGB and moves the noise's
     luminance into ALPHA (so the tile is transparent, not gray) → feFuncA gamma
     3 pushes most of that alpha toward zero, leaving a sparse speckle rather
     than an even fog. `opacity='.14'` on the rect is the strength knob and the
     only number worth touching; mean coverage lands near 3%. `stitchTiles`
     makes the 160px tile seamless - without it every repeat shows its edge. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .21 .72 .07 0 0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='gamma' exponent='3'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='.14'/%3E%3C/svg%3E");
  /* THE SAME SPECKLE, IN WHITE, for a dark object on a light page - a drawer
     head, a footer bar, the graphite panel. The black tile above is invisible
     on graphite, and the dark-polarity block swaps --grain wholesale, which
     does not help a surface that is dark while the page around it is not.
     This has lived in portal-skin.css under :root[data-portal="on"] since the
     portal rail needed it; the console needs it too now, so it is declared
     here and the portal copy simply keeps winning inside the portal. */
  --grain-lit: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 .21 .72 .07 0 0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='gamma' exponent='3'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='.15'/%3E%3C/svg%3E");


  /* Surfaces - cool off-white with a whisper of light blue.
     ── THE PAPER STEP ───────────────────────────────────────────────────────
     Jeremy's read on 2026-08-11: the console is "too bright", light-on-light,
     with no edge between a card and the page. The colors were right, so the
     fix is NOT a darker palette - it is a bigger STEP between the two lightest
     surfaces. Paper was #eff5f9 against a #ffffff card: 94.4% vs 100% relative
     luminance, a 1.06:1 ratio, which is below what an eye resolves as an edge
     at all. So a card floated on nothing and the 1px --line was doing the whole
     job of saying "this is a tile".
     Paper now sits at #e4ecf4 (~87.7% L, 1.14:1 against white). Same hue, same
     temperature, one step down - enough that every card, tile, table and drawer
     reads as sitting ON something. Do not close this gap again; if a surface
     needs to be nearly-white, it wants --card or --card-2, not --paper. */
  --paper:      #e4ecf4;   /* light-blue app background */
  --card:       #ffffff;
  --card-2:     #f2f7fb;
  /* Borders carry the tile edge on top of the paper step, so they moved with
     it - at the old values a --line hairline over the new paper was fainter
     than it had been over the old. */
  --line:       #d2dfea;
  --line-soft:  #e0e9f2;

  /* Text - warm charcoal, and DELIBERATELY DARK. The other half of the "too
     bright" note is that type sat too close to its surface: --tx was #20262b
     and --tx-soft #59606a, which are correct-by-the-numbers and still read as
     washed out against a near-white card at 13–14px. Each rung moved down one
     step; the hierarchy (tx < soft < mute < dim) is unchanged, everything is
     further from the paper than it was, and --tx-mute - the 11–12px rung, where
     WCAG AA wants 4.5:1 - improves from ~4.6:1 to ~5.8:1 on white. */
  --tx:         #151b21;
  --tx-soft:    #454d58;
  --tx-mute:    #5a6068;
  --tx-dim:     #9aa1ad;   /* placeholders and disabled glyphs - decorative only */
  /* Declared rather than left to fall back through `var(--head, var(--ink))`.
     On this polarity --ink happens to be the right heading ink, so the fallback
     worked by luck; naming it means a dark pack (which sets --head inline) and
     the product agree about what the token MEANS instead of one of them relying
     on --ink doing double duty as chrome and as heading ink. */
  --head:       #0d1b20;
  --on-ink:     #eaf3ee;
  --on-teal:    #ffffff;   /* white on --teal #007c89 is 4.7:1 */

  /* Status - THREE steps, the same shape as --teal / --teal-deep / --teal-bright.
     A status color is asked to do two incompatible jobs: be a FILL you can see
     from across the room (a dot, a bar, a badge) and be TEXT at 11px inside its
     own pale chip. One value cannot do both - #1f9d57 on #e4f5ea is 3.1:1 and
     #d98a12 on #fbf0db is 2.5:1, so every status pill in the product was below
     AA. The `-tx` step is the same hue carried down until it clears 4.5:1 on its
     own soft chip (and ≥5:1 on white and on --card-2, the other two places these
     land). Use --ok for the fill and --ok-tx the moment it is a glyph or a word;
     a dark pack re-points all three. */
  --ok:         #1f9d57;
  --ok-tx:      #187a44;
  --ok-soft:    #e4f5ea;
  /* NO GOLD (2026-09-24). --warn was amber #d98a12 and painted the timesheet
     ring, High pills, the On Site pill and the "Recommended" banner gold. It is
     now the soft-coral caution step (same family as --gold), kept lighter and
     pinker than --bad so a warning never reads as an error. */
  --warn:       #ee8660;
  --warn-tx:    #9c4428;
  --warn-soft:  #fdefe8;
  --bad:        #d6492f;
  --bad-tx:     #b83f28;
  --bad-soft:   #fbe7e2;
  --info:       #2f72c4;
  --info-tx:    #2c6bb8;
  --info-soft:  #e6eef9;

  /* Type */
  --f-head: 'Bricolage Grotesque', Georgia, serif;
  --f-body: 'Plus Jakarta Sans', system-ui, sans-serif;
  --f-mono: 'JetBrains Mono', ui-monospace, monospace;
  /* The wordmark face, and only the wordmark - see src/core/brand-name.js.
     Falls back to --f-head so a blocked font request degrades to the old rail. */
  --f-word: 'Sora', 'Bricolage Grotesque', Georgia, serif;
  /* Figures - every KPI value, money total, score and count. Deliberately NOT
     --f-head: a display face set at 800 has to carry "$14,040.00" at 30px, and
     brand serifs (Fraunces, Nanum Myeongjo) render that with stroke contrast,
     proportional figures and - when the pack never loaded the 800 weight - a
     synthesized bold. Inter Tight is drawn for it: lining tabular figures,
     narrow enough that a six-figure currency string does not wrap, and neutral
     enough to sit under any white-label pack. A brand may override it with
     fonts.num; nothing else should reach for this token. */
  --f-num: 'Inter Tight', 'Inter', system-ui, sans-serif;

  /* Type scale - one intentional ramp (replaces scattered literal px). */
  --fs-micro: 12px;   /* eyebrows, mono labels, axis, table th */
  --fs-sm:    13px;   /* secondary text, pills, btn-sm */
  --fs-body:  14px;   /* base body size - set ONCE here */
  --fs-md:    15.5px; /* card-head h3 */
  --fs-lg:    18px;   /* section leads */
  --fs-h1:    27px;   /* page-head h1 */
  --fs-stat:  clamp(22px, 2.4vw, 30px); /* tile values */

  /* Geometry */
  --r-xs: 7px; --r-sm: 10px; --r: 13px; --r-lg: 18px; --r-pill: 999px;
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 22px; --sp-6: 30px; --sp-7: 44px;
  --rail-w: 244px; --rail-w-collapsed: 72px; --topbar-h: 60px; --content-max: 1480px;

  /* ── Nav rail chrome - a FIXED neutral graphite, intentionally decoupled from
     the brand-hue ink and from any org white-label color. No [data-scheme] or
     org-brand override touches these, so the rail stays a calm, premium
     near-black in every mode; the mode/brand accent (teal + glow) threads
     through ONLY as the active pill, indicator bar, and a faint top glow. */
  /* LIFTED graphite, not the page color. These are unchanged from the light
     product and they stay that way on purpose - the note above says the rail is
     decoupled from the brand hue and from any org color, which also means it is
     decoupled from the CANVAS. Painting it #0C0C0E to match the new charcoal
     page collapsed it into the page: on the dark canvas it lost its edge, and on
     a light-canvas tenant (an identity-only pack keeps the product's surfaces)
     it turned a lit graphite sidebar into a flat black slab. A rail should read
     as a panel sitting in front of the page in either polarity. */
  --rail-bg:      #20242c;   /* medium graphite-slate base (lifted from near-black) */
  --rail-bg-2:    #262b34;   /* lifted panel top/bottom */
  --rail-bg-3:    #313742;   /* hover surfaces, tooltips, edge toggle */
  --rail-line:    rgba(255,255,255,.09);
  --rail-line-2:  rgba(255,255,255,.05);
  --rail-tx:      #eef0f5;   /* near-white label */
  --rail-tx-dim:  rgba(238,240,245,.68);
  --rail-tx-mute: rgba(238,240,245,.44);

  /* Depth. Raised alongside the paper step above - the two do the same job from
     opposite sides, and a shadow tuned against a 94%-luminance page is nearly
     invisible on it. --sh-1 is on every resting card, so it stays a CONTACT
     shadow (tight, close, no spread); the read comes from the ambient second
     layer, not from a bigger blur. */
  --sh-1: 0 1px 2px rgba(16,21,28,.07), 0 2px 6px -2px rgba(16,21,28,.07);
  --sh-2: 0 6px 18px -10px rgba(16,21,28,.26);
  --sh-3: 0 22px 50px -24px rgba(16,21,28,.40);
}

/* ── DARK-CANVAS TENANTS ─────────────────────────────────────────────────────
   The product ships light. A tenant whose brand pack paints a dark canvas gets
   every SURFACE token inline from brand.js (see darkCanvas()), but these four
   things cannot come from a token re-point, so they are stated here and keyed
   off the polarity attribute applyBrand() already stamps:

     · color-scheme  - the UA paints <select> popups, date pickers, scrollbars
                       and autofill itself; left light they flash white.
     · --grid-ln     - the ambient grid is a faint DARK line on paper and a faint
                       LIGHT one on charcoal. It has to flip, not re-point.
     · --wash-*      - see the note in components.css; 22% cyan is a pale tint
                       over white and a saturated fog over #0C0C0E.
     · --tx-dim      - placeholder ink, which descends on paper and ascends on
                       charcoal.
   Everything else a dark pack needs already arrives inline. */
:root[data-brand-polarity="dark"] {
  color-scheme: dark;
  --grid-ln: rgba(255,255,255,.05);
  --tx-dim:  #4A4A50;
  --wash-1:  16%;
  --wash-2:  8%;
  --wash-3:  9%;
  /* The status TEXT steps descend on paper and have to ASCEND on charcoal -
     #187a44 on a dark chip is the same failure the light values were fixing,
     inverted. Derived from whatever --ok/--warn/--bad/--info the pack landed on
     rather than restated, so a tenant's own status hues carry through. */
  --ok-tx:   color-mix(in srgb, var(--ok) 62%, #ffffff);
  --warn-tx: color-mix(in srgb, var(--warn) 62%, #ffffff);
  --bad-tx:  color-mix(in srgb, var(--bad) 62%, #ffffff);
  --info-tx: color-mix(in srgb, var(--info) 62%, #ffffff);
  /* White speckle - the same tile with the color matrix's RGB rows offset to 1.
     Black grain on charcoal is invisible; see the --grain note above. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 .21 .72 .07 0 0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='gamma' exponent='3'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='.16'/%3E%3C/svg%3E");
}

/* ============================================================================
   MODE COLOR SCHEMES - ONE design system, two modes.
   Applied globally via <html data-scheme="home|consulting">. Any other value
   matches no block below and renders as :root, which is Field Service.
   Every mode shares the SAME light styling defined in :root above - surfaces,
   text, status colors, type, geometry, shadows, and the whole component layer.
   Each scheme below overrides ONLY the brand accent (--teal/-deep/-bright), the
   chrome ink that tints the rail (--ink*), and the rail's on-ink text. So the
   modes are visually identical except for the color that identifies them.

   ONE PALETTE, BOTH MODES. Every mode renders in the product's cyan - the
   modes are the same product wearing the same clothes, and switching between
   them should not feel like switching apps. That is a deliberate reversal of the
   old per-mode accent: a color that changes under you is a poor way to say
   "you are in Consulting now", and it meant the same button was a different
   color depending on where you stood.

   MODE IDENTITY still exists - it is carried by the one control built for it,
   the mode-switcher dot in core/shell.js, which keeps a distinct hue per mode.
   That is a 9px mark in a menu that shows both at once, which is where a color
   code actually reads; the whole chrome is not.

   The block below is kept rather than deleted so a mode can diverge again by
   editing one line, and so `[data-scheme]` stays a real hook. It is
   intentionally identical to :root.
   ========================================================================== */
:root[data-scheme="consulting"] {
  --teal: #007c89; --teal-deep: #00626d; --teal-bright: #00E5FF;
  --accent-2: #2EE0A8; --accent-2-bright: #6BEDC4; --accent-2-deep: #1b8261;
}

/* ── Light surfaces INSIDE a dark-pack tenant ────────────────────────────────
   The product ships light, so the portal and a light-pack tenant inherit the
   right polarity and need nothing. This block is the remaining case: a tenant
   whose pack paints a dark canvas still has surfaces that stay light no matter
   what - portal-skin.css re-points the client portal to "Daylight", and
   field-v2.css re-points the Field Portal worksheet, both unconditionally. The
   four values below cannot ride along on a token re-point (see the note on the
   dark block above), so they are put back explicitly for those surfaces. */
:root[data-brand-polarity="dark"][data-portal="on"],
:root[data-brand-polarity="dark"][data-field="on"] {
  color-scheme: light;
  --grid-ln: color-mix(in srgb, var(--ink) 5.5%, transparent);
  --tx-dim:  #9aa1ad;
  --wash-1: 22%; --wash-2: 11%; --wash-3: 14%;
  /* Status text descends again on these light surfaces. Same derivation as the
     dark block, mixed toward black rather than white. */
  --ok-tx:   color-mix(in srgb, var(--ok) 78%, #000000);
  --warn-tx: color-mix(in srgb, var(--warn) 68%, #000000);
  --bad-tx:  color-mix(in srgb, var(--bad) 86%, #000000);
  --info-tx: color-mix(in srgb, var(--info) 94%, #000000);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--f-body);
  background: var(--paper);
  color: var(--tx);
  font-size: var(--fs-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
/* --head, then --ink. `--ink` is the CHROME color: a deep brand-hue near-black
   that happens to be the right heading ink on a light canvas, which is why it
   was used here. On a dark canvas it is the PAGE, so every heading rendered
   invisible. `--head` is the contrast-checked heading color and is emitted only
   by a custom-tier pack, so an org without one falls through to exactly the
   value this rule has always used. */
h1, h2, h3, h4 { font-family: var(--f-head); color: var(--head, var(--ink)); margin: 0; letter-spacing: -.01em; }
a { color: var(--teal-deep); text-decoration: none; }
.mono { font-family: var(--f-mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--tx-mute); }
.soft { color: var(--tx-soft); }

/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; gap: 8px; justify-content: center;
  font-family: var(--f-body); font-size: 13.5px; font-weight: 600; line-height: 1;
  padding: 0 15px; height: 38px; border-radius: var(--r-sm); border: 1px solid transparent;
  cursor: pointer; white-space: nowrap; transition: background .14s, border-color .14s, transform .06s, box-shadow .14s;
}
.btn:active { transform: translateY(1px); }
.btn svg { width: 16px; height: 16px; }
/* ── The buttons are the pre-login bar's buttons (2026-09-20) ───────────────
   Jeremy: cut the glow behind the cyan buttons and use the ones from the home
   page's top bar instead - `.topnav .btn-sm` / `.btn-sm-go` in index.html.

   WHAT WAS WRONG. .btn-primary carried a 18px cast shadow mixed from the fill
   itself (components.css), so a cyan button sat in a cyan haze: the edge of
   the control was a gradient rather than a line, and on the graphite panel -
   where the fill lifts to the brand cyan - the haze was brighter than most of
   what was around it. A button should be an OBJECT: a lit top edge, a body,
   and a short shadow that says how far off the surface it is. Not a lamp.

   WHAT REPLACES IT, ported from that bar verbatim in structure and re-pointed
   from --acc-fill to --teal so it follows the tenant's accent and lifts to the
   bright cyan inside a panel exactly as everything else does:
     - the fill runs bright → deep, so the pill has a lit top and a grounded
       base instead of reading as a flat rectangle;
     - two insets draw the edges - white along the top, a dark hairline along
       the bottom;
     - the cast shadow is TIGHT (2px 6px, and a 1px contact line) and mixed to
       45%, which reads as height rather than as light;
     - hover brightens the fill and lifts 1px; active puts it back down and
       collapses the shadow inward, so the button presses instead of just
       changing color.
   --on-teal, never a literal: the fill is not one color everywhere. */
/* THE FIVE RE-IMPLEMENTATIONS JOIN THE RULE RATHER THAN COPYING IT.
   .tour-primary, .fn-primary, .fp-btn.primary, .fd-btn.primary and
   .ctrl-go were each "the primary button" written again in a page's own
   <style> - five flat accent fills with white on them, which is five places
   for this to drift and, on the bright-cyan surfaces, five white-on-cyan
   labels. They are listed here so the shape has exactly one definition; each
   page keeps its own geometry and has had its fill declarations removed. */
.btn-primary,
.tour-primary,
.fn-primary,
.fp-btn.primary,
.fd-btn.primary,
.ctrl-mini.ctrl-go {
  /* The fill has its own handle, for the same reason the glass family has
     --btn-hue: a surface can need this button in a different color without
     re-declaring its shape. The pre-login pages set it to the brand cyan so
     their primary matches the one in the home page's top bar; everything
     else takes --teal, which is the deep step on paper and lifts to the same
     cyan inside a panel. */
  --btn-fill: var(--teal);
  position: relative; overflow: hidden;
  border-color: transparent;
  color: var(--on-teal);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--btn-fill) 88%, #ffffff 12%),
    var(--btn-fill) 58%,
    color-mix(in srgb, var(--btn-fill) 86%, #0b1015 14%));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.45),
              inset 0 -1px 0 rgba(0,0,0,.12),
              0 2px 6px -1px color-mix(in srgb, var(--btn-fill) 45%, transparent),
              0 1px 2px rgba(0,0,0,.30);
}
.btn-primary:hover,
.tour-primary:hover,
.fn-primary:hover,
.fp-btn.primary:hover,
.fd-btn.primary:hover,
.ctrl-mini.ctrl-go:hover {
  transform: translateY(-1px);
  /* LIGHT DOES NOT TURN A COLOR WHITE (2026-09-22). The hover here used to ramp
     from #ffffff at the top edge down to the fill, so pointing at a cyan button
     painted the top half of it in paper white: the pill stopped being a lit
     object and became a color with white spilled over it, and the label - which
     sits in that band - lost contrast at the very moment it was being aimed at.
     A real highlight does two things that one did not. It stays the button's OWN
     color, brighter. And it falls off well before the middle.
       - the ramp lifts the top third and leaves the label band at the rest fill,
         so hover reads brighter without pushing white behind the text;
       - the specular is where a specular belongs - a 1px lit edge with a short
         bloom under it - rather than a half-height wash;
       - the base darkens a touch, because more light from above means MORE
         contrast top to bottom, not a flatter pill.
     Two background declarations on purpose. The srgb mix is the fallback; the
     `oklch(from ...)` line that follows it raises LIGHTNESS while holding hue and
     chroma, which is the difference between a brighter cyan and a whiter one, and
     an engine without relative color simply keeps the fallback. The lift is a
     fraction of the headroom left ((1 - l) * .30), so a fill that is already
     bright - the panel's --teal-bright - brightens a little and a deep one
     brightens a lot, instead of both being pushed the same distance into white. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--btn-fill) 80%, #ffffff 20%),
    color-mix(in srgb, var(--btn-fill) 93%, #ffffff 7%) 34%,
    var(--btn-fill) 62%,
    color-mix(in srgb, var(--btn-fill) 91%, #0b1015 9%));
  background: linear-gradient(180deg,
    oklch(from var(--btn-fill) calc(l + (1 - l) * .30) c h),
    oklch(from var(--btn-fill) calc(l + (1 - l) * .10) c h) 34%,
    var(--btn-fill) 62%,
    oklch(from var(--btn-fill) calc(l * .94) c h));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.62),
              inset 0 10px 16px -12px rgba(255,255,255,.70),
              inset 0 -1px 0 rgba(0,0,0,.16),
              0 5px 14px -6px color-mix(in srgb, var(--btn-fill) 58%, transparent),
              0 2px 4px rgba(0,0,0,.28);
}
/* The sheen that crosses the pill on hover - a moving highlight on a lit
   surface, which is the one texture a flat fill genuinely lacks. Clipped by
   the button's own overflow so nothing escapes. */
.btn-primary::after,
.tour-primary::after,
.fn-primary::after,
.fp-btn.primary::after,
.fd-btn.primary::after,
.ctrl-mini.ctrl-go::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: -60%; width: 45%;
  pointer-events: none; transform: skewX(-18deg);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.10) 28%, rgba(255,255,255,.34) 50%, rgba(255,255,255,.10) 72%, transparent);
  opacity: 0;
}
:is(.btn-primary, .tour-primary, .fn-primary, .fp-btn.primary, .fd-btn.primary, .ctrl-mini.ctrl-go):hover::after { animation: btn-sheen .72s ease-out; }
@keyframes btn-sheen {
  from { left: -60%; opacity: 0; }
  22%  { opacity: 1; }
  to   { left: 115%; opacity: 0; }
}
:is(.btn-primary, .tour-primary, .fn-primary, .fp-btn.primary, .fd-btn.primary, .ctrl-mini.ctrl-go):active {
  transform: translateY(1px);
  box-shadow: inset 0 2px 6px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.18);
}

/* THE SECONDARY IS THE BAR'S SIGN IN BUTTON, AND SO IS EVERY OTHER NON-CYAN ONE.
   One object, three weights, written ONCE against --btn-hue and --btn-tint so
   the three cannot drift apart the way they had before (ghost was a --card
   rectangle, quiet was bare text, danger was a third thing again - three
   answers to "a button that is not the primary action").

   It is a pane of tinted glass: the hue at --btn-tint over WHATEVER is behind
   it, an edge mixed from the same hue, and the top-light inset. Mixing against
   `transparent` rather than against --card is what lets one rule serve a white
   page, the silver panel and the graphite footer bar without three versions.

   THE WEIGHTS ARE THE HIERARCHY. Jeremy asked for .btn-quiet to match too, and
   matching does not mean identical: 237 tertiary buttons painted exactly like
   the 694 secondary ones would leave the product with two rungs where it has
   three. Quiet is the same object at half the tint with a hairline edge - it
   reads as the same family from across the screen and as the lesser action up
   close, which is the job it already had. */
.btn-ghost, .btn-quiet, .btn-danger {
  --btn-hue: var(--teal);
  --btn-tint: 19%;
  --btn-tint-2: 9%;
  --btn-edge: 42%;
  /* Hover is a variant-level step too, not a fixed 30%. With one hard-coded
     hover tint the QUIET button lit to 30% - louder than the secondary is at
     rest - so pointing at the lesser action made it the loudest thing in the
     row. Each weight now brightens within its own band. */
  --btn-tint-h: 30%;
  --btn-tint-h2: 16%;
  color: var(--tx);
  border-color: color-mix(in srgb, var(--btn-hue) var(--btn-edge), transparent);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--btn-hue) var(--btn-tint), transparent),
    color-mix(in srgb, var(--btn-hue) var(--btn-tint-2), transparent));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22),
              0 1px 2px rgba(0,0,0,.18);
}
.btn-ghost:hover, .btn-quiet:hover, .btn-danger:hover {
  transform: translateY(-1px);
  border-color: color-mix(in srgb, var(--btn-hue) 72%, transparent);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--btn-hue) var(--btn-tint-h), transparent),
    color-mix(in srgb, var(--btn-hue) var(--btn-tint-h2), transparent));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.30),
              0 4px 12px -4px color-mix(in srgb, var(--btn-hue) 42%, transparent),
              0 2px 4px rgba(0,0,0,.20);
}
.btn-ghost:active, .btn-quiet:active, .btn-danger:active {
  transform: translateY(1px);
  box-shadow: inset 0 2px 5px rgba(0,0,0,.22);
}
.btn-ghost:hover { color: var(--teal-deep); }
/* Quiet: the same glass, half the tint, a hairline edge. */
.btn-quiet { --btn-tint: 10%; --btn-tint-2: 4%; --btn-edge: 22%; --btn-tint-h: 18%; --btn-tint-h2: 9%; color: var(--tx-soft); }
.btn-quiet:hover { color: var(--tx); border-color: color-mix(in srgb, var(--btn-hue) 48%, transparent); }
/* Danger is the same object in its own hue, so the family holds together and a
   destructive control is still plainly a button and not a red word. */
.btn-danger { --btn-hue: var(--bad); --btn-tint: 17%; --btn-tint-2: 8%; color: var(--bad-tx, var(--bad)); }
.btn-danger:hover { color: var(--bad-tx, var(--bad)); }
/* A disabled control is not pressable and should not look it. */
.btn:disabled, .btn:disabled:hover { transform: none; box-shadow: none; }
.btn:disabled::after { display: none; }
.btn-sm { height: 31px; padding: 0 11px; font-size: 12.5px; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* ── Cards & layout ─────────────────────────────────────────────────────── */
.card { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--sh-1); }
.card-pad { padding: var(--sp-5); }
/* Wraps. A card head is "title + .grow spacer + one or more actions", and on a
   phone that row is routinely wider than the card - the title and a segmented
   control, a title and two buttons. Several pages had already patched
   `flex-wrap:wrap` inline at the call site, which is the tell that the default
   was wrong. `row-gap` keeps a wrapped action row off the title. */
.card-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--line-soft); }
.card-head.nowrap { flex-wrap: nowrap; }
.card-head h3 { font-size: 15.5px; font-weight: 700; }
.card-head .grow { flex: 1; }
/* `<span class="grow"></span>` is this app's spacer idiom - ~200 call sites use
   it to push the trailing controls of a flex row to the right. Only .card-head
   ever gave it a rule, so in every OTHER flex row (drawer and modal footers
   most of all) it collapsed to zero width and the buttons bunched together
   against the right edge. This is the rule those call sites were written for. */
.grow { flex: 1; }
.grid { display: grid; gap: var(--sp-4); }
/* The generic toolbar idiom - filters, search, buttons. Wrapping is the default
   because these rows are assembled from a variable number of controls and a
   phone cannot hold them on one line; `.row.nowrap` is the opt-out for the few
   places that must stay single-line (an inline label + field, a stat + delta). */
.row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); }
.row.nowrap { flex-wrap: nowrap; }
/* min-width:0 is what actually lets the clamps above take effect. A flex item
   defaults to `min-width: auto` - it will not shrink below its own min-content
   width - so a .row nested in .page-head sized itself to the widest thing inside
   it (a segmented control's full track) and the scroller's `max-width: 100%`
   then resolved against that already-overflowed parent and clamped to nothing.
   These are all flex CONTAINERS that are commonly flex ITEMS themselves; each
   one is a link in the chain that has to be allowed to give.
   The direct-children half matters as much as the containers: pages routinely
   wrap a control in a plain div, and that anonymous wrapper is the flex item
   that refuses to give. Items that must keep their size already say so with
   `flex: 0 0 auto`, which this does not touch. */
.row, .card-head, .spread, .stack,
.page-head > *, .row > *, .card-head > *, .spread > * { min-width: 0; }
.stack { display: flex; flex-direction: column; gap: var(--sp-3); }
.spread { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }

/* ── Page chrome ────────────────────────────────────────────────────────── */
/* Top padding runs one step larger than the other three sides: the sticky
   topbar sits directly above it, and an even inset reads as cramped under it. */
.page { padding: var(--sp-6); padding-top: var(--sp-7); max-width: var(--content-max); margin: 0 auto; }
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-4); margin-bottom: var(--sp-5); flex-wrap: wrap; }
.page-head .eyebrow { font-family: var(--f-mono); font-size:12px; letter-spacing: .14em; text-transform: uppercase; color: var(--teal-deep); font-weight: 600; }
.page-head h1 { font-size: 27px; font-weight: 800; margin-top: 4px; }
/* A page's own description is a hint on its <h1> (core/hint.js), not a line
   under it - one sentence is not worth a band of vertical space on every
   page. What is left here is the standing notice a couple of pages carry
   under the title (Templates' editor note, Training's trademark line), which
   has to stay on screen and cannot live in a tooltip. */
.page-head .lede { color: var(--tx-soft); margin-top: 3px; max-width: 60ch; }

/* ── Stat tiles ─────────────────────────────────────────────────────────── */
/* The track max is capped at 260px (report-kit's library grid does the same)
   so a strip that wraps - five tiles need ~1014px and a 1280px viewport offers
   ~961px of report body - reads as an intentional short row instead of one
   orphan tile stretched across the hole. grid-auto-rows:1fr keeps the wrapped
   row the same height as the full one; without it the orphan rendered ~19px
   shorter than its row-one siblings, which is the reported "inconsistent
   tiles" complaint. */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 260px)); justify-content: start; grid-auto-rows: 1fr; gap: var(--sp-4); }
/* Exactly four tiles is the commonest strip in the product (Invoices, Clients,
   Projects...) and the capped tracks above wrapped it 3 + 1 at 1280px - one
   orphan under three. Four is one row at desktop width and an even 2 + 2
   below it, never 3 + 1 (layout sweep 2026-09-23). */
.stats:has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1100px) { .stats:has(> :nth-child(4):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.stat { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-lg); padding: var(--sp-4) var(--sp-5); position: relative; overflow: hidden; }
.stat::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--teal); }
.stat.is-coral::before { background: var(--coral); } .stat.is-gold::before { background: var(--accent-2); } .stat.is-caution::before { background: var(--gold); } .stat.is-ok::before { background: var(--ok); }
.stat .k { font-size:12px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--tx-mute); }
.stat .v { font-family: var(--f-num); font-weight: 800; font-size: clamp(20px, 2.2vw, 30px); color: var(--head, var(--ink)); margin-top: 6px; overflow-wrap: anywhere; word-break: break-word; max-width: 100%; font-variant-numeric: tabular-nums; }
.stat .d { font-size: 12.5px; margin-top: 4px; color: var(--tx-soft); }
.stat .d .up { color:var(--ok-tx, var(--ok)); font-weight: 700; } .stat .d .down { color:var(--bad-tx, var(--bad)); font-weight: 700; }

/* ── Tables ─────────────────────────────────────────────────────────────── */
.tbl { width: 100%; border-collapse: collapse; }
.tbl th { text-align: left; font-family: var(--f-mono); font-size:12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--tx-mute); padding: 11px var(--sp-4); border-bottom: 1px solid var(--line); white-space: nowrap; }
.tbl td { padding: 12px var(--sp-4); border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
.tbl tr:last-child td { border-bottom: 0; }
.tbl tbody tr { transition: background .1s; }
.tbl tbody tr:hover { background: var(--card-2); }
.tbl .num { text-align: right; font-family: var(--f-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.tbl .clickable { cursor: pointer; }

/* ── Badges / pills ─────────────────────────────────────────────────────── */
/* Status pills are fed raw enum values in dozens of places ('in_progress' →
   'in progress', 'no show', 'partially paid'), which rendered as all-lowercase
   chips. `capitalize` fixes every one of them at the presentation layer without
   touching the data: it only uppercases each word's first letter, so pills that
   already carry an acronym (MTD, A/R) or a proper label are unaffected. */
/* NO text-transform. It used to be `capitalize`, which is a runtime title-caser
   over source strings that are already correct - and it gets Title Case WRONG:
   "Sent to Customer" rendered as "Sent To Customer", "Out of Service" as "Out Of
   Service", on every pill in the product. CLAUDE.md is explicit that case is
   fixed at the source string, because a text-transform does not survive a CSV
   export, an email or a printed document; here it was actively corrupting copy
   that was right in the source. Every pill's text now comes from a literal or
   from core/enum-labels.js label(), both of which are Title Case by contract and
   guarded by scripts/check-case.mjs. */
.pill { display: inline-flex; align-items: center; gap: 5px; font-size:12px; font-weight: 700; padding: 3px 9px; border-radius: var(--r-pill); line-height: 1.3; white-space: nowrap; }
/* The `-tx` step, not the fill - a pill IS text (see the status note above).
   `var(--ok-tx, var(--ok))` rather than a bare token because a brand pack that
   overrides status colors emits the fill only; falling back to it keeps a
   tenant's own green rather than showing the product's. */
/* WARNING AND ERROR DIFFER BY SHAPE, NOT ONLY BY HUE. With gold gone (NO GOLD,
   2026-09-24) a caution and an error are both in the coral-red family, and as
   two pale chips with rust text Due Soon and Overdue, High and Urgent read as
   the same pill. So a warning is an OUTLINED chip (coral ring, coral text, no
   fill) and an error is a SOLID red chip with white text: the difference
   survives color blindness, a dark panel and a grayscale print. The fill is
   derived from --bad so a tenant's own red carries through, darkened until
   white clears 4.5:1 on it (#d6492f -> ~6:1). */
.pill.ok { background: var(--ok-soft); color: var(--ok-tx, var(--ok)); }
.pill.warn { background: transparent; color: var(--warn-tx, var(--warn)); box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--warn) 75%, transparent); }
.pill.bad { background: color-mix(in srgb, var(--bad) 82%, #000000); color: #ffffff; }
.pill.info { background: var(--info-soft); color: var(--info-tx, var(--info)); }
.pill.neutral { background: var(--line-soft); color: var(--tx-soft); }
/* `iris` is the tone the consulting Clients page gives a referral source, and
   it had no rule anywhere: the chip fell through to the bare .pill, inheriting
   whatever text color it landed next to and drawing a 16%-of-that ring around
   it. On the dossier's graphite that is an invisible chip. Accent-tinted, on
   the same fill/-tx split every other tone uses. */
.pill.iris { background: color-mix(in srgb, var(--teal) 13%, transparent); color: var(--teal-deep, var(--teal)); }
/* ── Brand Studio preview (Settings) - the `-tx` step the engine can't take ──
   The preview's "Confirmed" pill and outline "Reschedule" button spend the
   ENGINE-derived text colors (--brand-accent-deep, --brand-primary-text),
   which brand.js floors at 4.5:1 against WHITE - but the pill sits on its own
   accent-tinted chip and the outline button on --paper, both darker than
   white, so each measured just under AA there (4.06:1 and 4.15:1). Same rule
   as --ok vs --ok-tx above: a color used as TEXT needs a darker step for the
   surface it actually sits on. 88% of the derived color toward black clears
   4.5:1 with margin on the default brand (5.0:1 / 5.1:1, verified against
   #d6f7ed and #e4ecf4) while keeping whatever hue the tenant chose.
   `!important` because the preview paints these inline; the [style*=] hook
   picks the transparent outline button out from the filled one beside it. */
#bs-preview .pill { color: color-mix(in srgb, var(--brand-accent-deep) 88%, #000) !important; }
#bs-preview .btn[style*="transparent"] { color: color-mix(in srgb, var(--brand-primary-text) 88%, #000) !important; }
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }

/* The UA sheet hides [hidden] with `display:none`, which ANY later `display`
   rule beats. Every layout primitive here sets one - so `.field[hidden]` stayed
   on screen, and the Custom Fee or Discount modal showed the Dollars box and the
   Percent box at the same time. Typing a dollar amount under a percent discount
   then filed a 0% discount. `hidden` has to mean hidden. */
[hidden] { display: none !important; }

/* ── Forms ──────────────────────────────────────────────────────────────── */
.field { display: flex; flex-direction: column; gap: 5px; }
.field label { font-size: 12.5px; font-weight: 600; color: var(--tx-soft); }
.input, .select, .textarea { font-family: var(--f-body); font-size: 14px; color: var(--tx); background: var(--card); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px 11px; outline: none; transition: border-color .14s, box-shadow .14s; width: 100%; }
.input:focus, .select:focus, .textarea:focus { border-color: var(--teal); box-shadow: 0 0 0 3px color-mix(in srgb, var(--teal) 16%, transparent); }
.textarea { resize: vertical; min-height: 84px; }
/* A form field is where a dark theme fails loudest if it is missed. The field
   itself is already token-driven (--card / --line / --tx), but the placeholder
   had no rule at all and fell through to the UA's, which is tuned for a white
   input - near-black hint text inside a charcoal box. */
.input::placeholder, .textarea::placeholder { color: var(--tx-dim, var(--tx-mute)); opacity: 1; }
.search { position: relative; }
.search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--tx-mute); }
.search .input { padding-left: 34px; }
/* An input with a text affix beside it: "$ [   ]", "Net [ 30 ] days",
   "$ [   ] / hr". Written across the product as a plain .row holding only
   spans and the control - and .row WRAPS, while .input is width:100%, so the
   control could never share a line with its own affix. The "$" sat on a line
   of its own above the box, "days" on a line below it, and every such field
   sat 20-30px lower than its neighbors in the row (layout sweep 2026-09-23).
   Scoped to rows that hold NOTHING but spans and controls, so a toolbar with
   buttons in it keeps wrapping on a phone. */
.row:has(> :is(.input, .select)):has(> span):not(:has(> :not(span, .input, .select))) { flex-wrap: nowrap; }
.row:has(> span):not(:has(> :not(span, .input, .select))) > :is(.input, .select) { flex: 1 1 auto; width: auto; min-width: 0; }
/* A row of form fields lines its fields up from the TOP. .row centers its
   items, which is right for a toolbar and wrong for fields: the moment one
   field carries a help line under its input (a "Preselected from your
   browser" note, a validation hint), every other field in the row is centered
   against that taller one and its input drops below its neighbors'. Only
   rows made of nothing but .field children, so a field-plus-button toolbar
   keeps its centering. */
.row:has(> .field):not(:has(> :not(.field))) { align-items: flex-start; }

/* ── Segmented control / tabs ───────────────────────────────────────────── */
/* Pans rather than wraps - see the note on .segctl in components.css. Without
   max-width these tracks pushed the whole document wider than the viewport. */
.seg { display: inline-flex; max-width: 100%; overflow-x: auto; scrollbar-width: none; background: var(--line-soft); border-radius: var(--r-sm); padding: 3px; gap: 2px; }
.seg::-webkit-scrollbar { display: none; }
.seg button { flex: 0 0 auto; font-family: var(--f-body); font-size: 13px; font-weight: 600; color: var(--tx-soft); background: transparent; border: 0; padding: 7px 14px; border-radius: 8px; cursor: pointer; transition: all .14s; white-space: nowrap; }
.seg button.on { background: var(--card); color: var(--head, var(--ink)); box-shadow: var(--sh-1); }
/* On a dark canvas --card sits within a couple of percent of the --line-soft
   track, so the selected segment loses its edge; the lifted surface restores it. */
:root[data-brand-polarity="dark"] .seg button.on { background: var(--card-2); }

/* ── Drawer (right-side detail) ─────────────────────────────────────────── */
/* Deeper than the light theme's .42 tinted ink: a scrim has to separate a panel
   from a page that is ALREADY near-black, so a weak veil reads as nothing
   happened. Alpha over the page, so a light-pack tenant and the client portal
   both still get a sensible veil from the same rule. */
.scrim { position: fixed; inset: 0; background: rgba(16,21,28,.42); opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 80; }
/* A .42 tinted veil over a page that is ALREADY near-black reads as nothing
   happened, so a dark-pack tenant gets a deeper, neutral one. */
:root[data-brand-polarity="dark"] .scrim { background: rgba(0,0,0,.66); }
.scrim.on { opacity: 1; pointer-events: auto; }
/* ── Panel width ──────────────────────────────────────────────────────────
   Record panels used to be 520px, which is narrower than most of what goes in
   them: an invoice document, a job with its checklist, a drill-down table. The
   width is a variable so the shell can pad itself by exactly the same number -
   two places reading one value, never two numbers kept in step by hand. */
:root { --drawer-w: min(1040px, 94vw); --drawer-w-wide: min(1180px, 96vw); }
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: var(--drawer-w); background: var(--card); box-shadow: var(--sh-3); transform: translateX(100%); transition: transform .26s cubic-bezier(.4,0,.2,1); z-index: 81; display: flex; flex-direction: column; }
.drawer.drawer-wide { width: var(--drawer-w-wide); }
.drawer.on { transform: none; }

/* ── Make room rather than cover ──────────────────────────────────────────
   The same behavior the chat dock has: on a viewport wide enough that the page
   is still usable beside the panel, the shell shifts over and the scrim goes
   away, so the record you opened sits NEXT TO the list you opened it from
   instead of on top of it.
   The breakpoint is the panel width plus a working minimum for the page - below
   it the panel would leave a column too narrow to read, so it goes back to
   overlaying with a scrim. The `.chat-open` compound is not redundant: the chat
   dock injects its own `body.chat-open .shell { padding-right: var(--chat-w) }`
   at runtime, which lands later in the cascade and would otherwise win at equal
   specificity - the drawer is the thing on top, so it sets the padding. */
@media (min-width: 1660px) {
  .shell { transition: padding-right .26s cubic-bezier(.4,0,.2,1); }
  body.drawer-open .shell,
  body.chat-open.drawer-open .shell { padding-right: var(--drawer-w); }
  body.drawer-open .drawer-scrim { display: none; }
}
/* The head carries a whisper of the brand wash so the panel opens with some
   light in it instead of bare white (Jeremy, 2026-08-13: the right-side panels
   were "white on white on white"). Token-driven - a brand pack re-points
   --teal-bright and the wash follows the tenant's own color. */
/* ── ONE DRAWER SCHEME, THE CHAT DOCK'S (2026-09-20) ──────────────────────
   The Team Chat and Customer Chat drawers were built on the rail's graphite -
   the same surface the left rail and the tooltips use - while every other
   drawer in the product was paper-white. Opening a task beside a chat thread
   put two panels of the same shape and the same width side by side in two
   different products, and the graphite one was plainly the better-looking of
   the two.

   This repaints the rest to match. It is done by REDEFINING THE SURFACE TOKENS
   inside `.drawer` rather than by restyling the drawer's parts, because a
   drawer's body is arbitrary page markup - a task panel, a record panel, a
   dossier, a form somebody adds next month. Every one of those reads --card,
   --tx, --line and the rest, so moving the tokens moves all of it, and a
   component written tomorrow lands in the scheme without knowing it exists.

   `color-scheme: dark` comes with it so the browser paints scrollbars, form
   controls and the caret to match; without it a native <select> inside the
   drawer stays a white box on graphite - the same hole that made the portal's
   company switcher unreadable.

   AND A MODAL IS A PANEL. This rule named `.drawer` only, so "Edit Team
   Member" - a modal, not a drawer - stayed a white box opened over a graphite
   page, which reads as an unfinished screen rather than as a second kind of
   surface. `.modal .box` is in the same rule now: there is ONE panel scheme and
   every panel is in it.

   AND THEN IT WAS TURNED BACK (2026-09-20). Graphite everywhere is correct at
   card size and wrong at chip size: a KPI tile, a status pill and a gauge are
   read at a glance, and seven points of lift behind a 9%-white hairline is not
   a surface, it is a rectangle you have to look for. The client dossier is
   where Jeremy saw it. So this whole block is now the OPT-IN - everything below
   applies only under `html[data-panel-theme="dark"]` - and the default is the
   silver panel with the graphite frame that follows it. The default still needs
   no attribute and no JavaScript, so a cold boot, a blocked localStorage or a
   shell that never runs core/panel-theme.js all paint the intended scheme;
   only somebody who has actively asked for graphite carries a mark for it.

   The *-tx status steps are re-derived upward exactly as the dark brand
   polarity does (see :root[data-brand-polarity="dark"]): #187a44 on a dark chip
   is the light values' failure, inverted. */
/* THE SURFACES HAVE TO SIT ON THE PAPER, NOT AT IT.
   The first cut of this pointed --card AND --paper at the same #20242c, so a
   tile, a card, a table row and an <input> were painted the exact color of the
   background behind them: every edge in the panel was a 5%-white hairline doing
   the whole job, and a form was a field of graphite with a caret somewhere in
   it. Jeremy's note, and it is the right one - keep the background, lift the
   things that sit on it.

   So the paper stays exactly where it was (the graphite the rail uses, with the
   two corner blooms below) and the CONTENT steps climb above it instead of
   matching it. --card is the tile/card/row surface, --card-2 the one that has
   to read as raised again on top of a card (a footer, a nested head), --card-3
   the hover step. `.drawer-head` and `.drawer-foot` are re-pinned to the paper
   below so the FRAME is unchanged - only what is inside it moves. */
html[data-panel-theme="dark"] :is(.drawer, .modal .box) {
  color-scheme: dark;
  --drawer-paper: var(--rail-bg, #20242c);
  --card:      #2b313b;
  --card-2:    #343b47;
  --card-3:    #414957;
  --paper:     var(--drawer-paper);
  --surface:   var(--drawer-paper);
  /* Edges carry more weight here than on white: a tile that is four points
     lighter than its background needs a border you can actually find. */
  --line:      rgba(255,255,255,.15);
  --line-soft: rgba(255,255,255,.09);
  --hairline:  rgba(255,255,255,.09);
  --tx:        var(--rail-tx, #eef0f5);
  --tx-soft:   var(--rail-tx-dim, rgba(238,240,245,.68));
  --tx-mute:   var(--rail-tx-mute, rgba(238,240,245,.44));
  --tx-dim:    rgba(238,240,245,.30);
  --head:      var(--rail-tx, #eef0f5);
  --ink:       var(--drawer-paper);
  /* The accent AS TEXT has to ascend here for the same reason the status steps
     do: --teal is drawn to pass on white and is unreadable on graphite. */
  --teal:      var(--teal-bright, #00E5FF);
  --teal-deep: var(--teal-bright, #00E5FF);
  /* AND THE THING THAT SITS ON IT. --on-teal is #ffffff at :root, because
     there --teal is the deep #007c89 that white clears 4.7:1 on. Lifting
     --teal to the brand cyan without lifting this is how a primary button in
     a panel came out white-on-#00E5FF - 1.4:1, the least legible control in
     the product. Cyan is a LIGHT color: what goes on it is the near-ink,
     which is what the brand does on its own site (Jeremy, 2026-09-20). */
  --on-teal:   #06181B;
  --ok-tx:     color-mix(in srgb, var(--ok) 62%, #ffffff);
  --warn-tx:   color-mix(in srgb, var(--warn) 62%, #ffffff);
  --bad-tx:    color-mix(in srgb, var(--bad) 62%, #ffffff);
  --info-tx:   color-mix(in srgb, var(--info) 62%, #ffffff);
  --coral-tx:  color-mix(in srgb, var(--coral) 70%, #ffffff);
  --gold-tx:   color-mix(in srgb, var(--gold) 70%, #ffffff);
  /* Components that read --grain get nothing here: the black speckle this
     token carries is invisible on graphite. The panel body takes the WHITE
     tile instead, through --panel-grain, which is the same swap the portal
     rail makes for the same reason. */
  --grain: none;
  --panel-grain: var(--grain-lit);
  color: var(--tx);
}
/* ── SILVER BODY, GRAPHITE FRAME - THE DEFAULT PANEL (2026-09-20) ───────────
   Every drawer and every dialog in the product opens like this unless the
   browser has asked for Graphite above.

   THE FRAME IS THE DARK PART. The head and the footer are the two bars a panel
   is recognized by - they carry the title, the close mark and the actions, and
   they are the same two bars on a task panel, a record panel, a dossier and a
   chat thread. Keeping them on the rail's graphite is what makes a panel read
   as a panel over the page rather than as more page; putting the CONTENT back
   on paper is what makes the small things on it - a KPI tile, a status chip, a
   gauge, a caption - legible again, which is the whole reason the all-graphite
   version was turned back.

   --panel-frame is declared on the panel, not on the two bars, so a brand pack
   or a future third scheme has one place to re-point. The Graphite block above
   leaves it alone deliberately: there the frame and the body are the same
   surface, which is exactly what that scheme is.

   The silver is a NEUTRAL step, not the page's --paper. The page behind a
   drawer is #e4ecf4, a light blue; a panel painted the same color reads as a
   hole in the page rather than as something laid over it. Pulling the blue out
   and dropping two points of value gives a surface that is plainly in front,
   still lets a white card lift off it, and does not introduce a second accent. */
:is(.drawer, .modal .box) { --panel-frame: var(--rail-bg, #20242c); }

html:not([data-panel-theme="dark"]) :is(.drawer, .modal .box) {
  --drawer-paper: #e6e9ee;
  --paper:   var(--drawer-paper);
  --surface: var(--drawer-paper);
  /* Texture on the silver: the black speckle, which is what a light surface
     takes. It is the one thing keeping a large flat panel from reading as a
     printed swatch, and at 3% mean coverage it is felt rather than seen. */
  --panel-grain: var(--grain);
}
/* The panel's own backing, for the sliver of it any body does not cover. */
html:not([data-panel-theme="dark"]) .drawer { background: var(--drawer-paper); }
/* A neutral chip paints --line-soft, which is a pale gray drawn to read on
   WHITE; on the silver it is the same value as the surface behind it and the
   chip disappears - "Past Client" beside a company name becomes floating text.
   On paper the answer is the opposite of the graphite one: bring the chip UP
   to the card the rest of the panel is drawn on and let the edge close it. */
html:not([data-panel-theme="dark"]) :is(.drawer, .modal .box) .pill.neutral {
  background: var(--card);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--tx-soft);
}

/* THE TWO BARS. Written as ONE rule over the drawer's head/foot and the modal
   box's, because they are the same object twice and had drifted apart once
   already (the modal's head kept a hairline the drawer's had replaced).

   The tokens are re-pointed INSIDE the bar rather than the parts being
   restyled, for the same reason the Graphite block does it: a footer holds
   arbitrary controls - a ghost button, a More menu, a count, a link somebody
   adds next month - and every one of them reads --tx, --line and --card. Move
   the tokens and all of it lands on the dark bar correctly, including the
   component that does not exist yet. `color-scheme: dark` comes along so a
   native control or a scrollbar inside a bar is painted to match. */
:is(.drawer-head, .drawer-foot, .modal .box-head, .modal .box-foot) {
  color-scheme: dark;
  --card:      #2b313b;
  --card-2:    #343b47;
  --card-3:    #414957;
  --line:      rgba(255,255,255,.15);
  --line-soft: rgba(255,255,255,.10);
  --hairline:  rgba(255,255,255,.10);
  --tx:        var(--rail-tx, #eef0f5);
  --tx-soft:   var(--rail-tx-dim, rgba(238,240,245,.70));
  --tx-mute:   var(--rail-tx-mute, rgba(238,240,245,.48));
  --head:      var(--rail-tx, #eef0f5);
  --ink:       var(--panel-frame);
  /* The accent as TEXT has to ascend on graphite exactly as it does in the
     Graphite scheme - --teal is drawn to pass on white. */
  --teal:      var(--teal-bright, #00E5FF);
  --teal-deep: var(--teal-bright, #00E5FF);
  /* AND THE THING THAT SITS ON IT. --on-teal is #ffffff at :root, because
     there --teal is the deep #007c89 that white clears 4.7:1 on. Lifting
     --teal to the brand cyan without lifting this is how a primary button in
     a panel came out white-on-#00E5FF - 1.4:1, the least legible control in
     the product. Cyan is a LIGHT color: what goes on it is the near-ink,
     which is what the brand does on its own site (Jeremy, 2026-09-20). */
  --on-teal:   #06181B;
  /* A status word on the dark bar needs the lifted step for the same reason
     the Graphite panel derives one: #187a44 is drawn to pass on white and is
     unreadable here. Same derivation, so a pill in a footer and the same pill
     in a graphite body are the same color. */
  --ok-tx:     color-mix(in srgb, var(--ok) 62%, #ffffff);
  --warn-tx:   color-mix(in srgb, var(--warn) 62%, #ffffff);
  --bad-tx:    color-mix(in srgb, var(--bad) 62%, #ffffff);
  --info-tx:   color-mix(in srgb, var(--info) 62%, #ffffff);
  --coral-tx:  color-mix(in srgb, var(--coral) 70%, #ffffff);
  --grain: none;
  color: var(--tx);
}
/* The head keeps the whisper of brand wash it has always had, now over the
   frame rather than over whatever the body is standing on. */
.drawer-head, .modal .box-head {
  background:
    var(--grain-lit) 0 0 / 160px 160px,
    linear-gradient(180deg, color-mix(in srgb, var(--teal-bright) 10%, var(--panel-frame)), var(--panel-frame));
}
.drawer-foot, .modal .box-foot {
  background:
    var(--grain-lit) 0 0 / 160px 160px,
    linear-gradient(0deg, color-mix(in srgb, var(--teal-bright) 5%, var(--panel-frame)), var(--panel-frame));
}
/* Both edges are drawn against the body below/above them, so they are a shade
   of the frame rather than of the bar's own re-pointed --line. */
.drawer-head, .modal .box-head { border-bottom: 1px solid rgba(0,0,0,.35); }
.drawer-foot, .modal .box-foot { border-top: 1px solid rgba(0,0,0,.35); }
/* A ghost button on the dark bar: the card step it paints is now a graphite
   one (re-pointed above), so it only needs its hover brought back to a value
   that reads on it. */
:is(.drawer-foot, .modal .box-foot) .btn-ghost:hover {
  background: color-mix(in srgb, var(--teal-bright) 14%, var(--card-2));
  border-color: color-mix(in srgb, var(--teal-bright) 45%, var(--line));
  color: var(--teal-bright);
}
/* THE BARS ARE IN FRONT OF THE BODY, AND THEY SHOULD LOOK IT.
   A flex column puts head, body and foot edge to edge with nothing between
   them, so the frame and the content met on a 1px line and the panel read as
   three stripes. Lifting the two bars out of flow (z-index only - the layout
   is unchanged) lets each cast a short shadow onto the body, which is what
   makes the content read as sitting BETWEEN them. The values are deliberately
   shared by both schemes: the graphite body wants a deeper cast and the silver
   a softer one, and a single -14px spread lands inside both. */
:is(.drawer-head, .drawer-foot, .modal .box-head, .modal .box-foot) { position: relative; z-index: 1; }
:is(.drawer-head, .modal .box-head) { box-shadow: 0 10px 20px -14px rgba(0,0,0,.62); }
:is(.drawer-foot, .modal .box-foot) { box-shadow: 0 -10px 20px -14px rgba(0,0,0,.62); }

/* A DIALOG GETS THE SAME LIT PAPER A DRAWER HAS.
   `.modal .box-body` never had a background of its own - it showed the box's
   flat fill - so a dialog was the one panel in the product with no light in it
   at all, sitting beside drawers that are lit from two corners. Same stack,
   same brand pair (cyan high-right, mint low-left). */
.modal .box-body {
  background:
    var(--panel-grain, none) 0 0 / 160px 160px,
    radial-gradient(120% 300px at 100% 0%, color-mix(in srgb, var(--teal-bright) 9%, transparent), transparent 72%),
    radial-gradient(100% 280px at 0% 100%, color-mix(in srgb, var(--accent-2) 7%, transparent), transparent 72%),
    var(--paper, var(--card));
}

/* ── SILVER: THE THINGS ON THE PAPER ────────────────────────────────────────
   The mirror of the Graphite block further up, and the same argument in the
   other direction. There the problem was a surface too close in value to the
   background; here it is a surface that is the right value and has nothing
   holding its shape - a white tile on silver is clearly in front, but with a
   --line-soft hairline and no shadow its corners dissolve and a row of four
   reads as one pale band.

   So: white with the faintest brand tint down the diagonal (the tile has had
   that since August, this only makes it the rule for the whole family), the
   full --line edge rather than the soft one, and a two-stage shadow - a tight
   contact shadow to seat the card on the paper and a wide, nearly-invisible
   one to give it height. --ink rather than black, so the shadow carries the
   product's cyan-charcoal and not a gray that fights the paper. */
html:not([data-panel-theme="dark"]) :is(.drawer, .modal .box) :is(.dossier-tile, .doss-score, .doss-why, .doss-line, .px-kpi, .sched-staff) {
  background: linear-gradient(168deg, var(--card), color-mix(in srgb, var(--teal) 4%, var(--card)));
  border: 1px solid var(--line);
  /* Three layers, and the first one is the point: a 1px white line inside the
     top edge is what a raised object does to light, and it is the difference
     between a card that sits ON the paper and a rectangle printed on it. The
     other two seat it - a tight contact shadow, then a wide soft one for
     height. */
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9),
              0 1px 2px color-mix(in srgb, var(--ink) 7%, transparent),
              0 10px 20px -16px color-mix(in srgb, var(--ink) 45%, transparent);
}
/* The score is the first thing a reader looks at in a dossier, so it is lit
   from the brand rather than left neutral - the same move its graphite
   counterpart makes, at the weight paper can carry. */
html:not([data-panel-theme="dark"]) :is(.drawer, .modal .box) .doss-score {
  background: linear-gradient(150deg, color-mix(in srgb, var(--teal) 8%, var(--card)), var(--card) 64%);
  border-color: color-mix(in srgb, var(--teal) 30%, var(--line));
}
html:not([data-panel-theme="dark"]) :is(.drawer, .modal .box) .doss-line.is-link:hover {
  background: linear-gradient(168deg, var(--card), color-mix(in srgb, var(--teal) 9%, var(--card)));
  border-color: color-mix(in srgb, var(--teal) 42%, var(--line));
  box-shadow: 0 2px 4px color-mix(in srgb, var(--ink) 8%, transparent),
              0 14px 24px -16px color-mix(in srgb, var(--ink) 55%, transparent);
}
.drawer-head { padding: var(--sp-5); display: flex; align-items: flex-start; gap: var(--sp-3); }
/* The body sits on PAPER, not on card - the same paper step the page itself
   got on Aug 11: tiles and tables inside the panel are white cards, and white
   cards need something under them to read as tiles at all. The two corner
   blooms are the brand pair (cyan high-right, mint low-left), the same two
   corners the public pages light from. */
.drawer-body { flex: 1; overflow-y: auto; padding: var(--sp-5);
  background:
    /* GRAIN FIRST, so it sits OVER the wash rather than under it - a noise layer
       beneath a gradient is just a gradient. Same argument, same 160px tile, as
       the page background in components.css. --panel-grain is per scheme: the
       black speckle on silver, the white one on graphite. */
    var(--panel-grain, none) 0 0 / 160px 160px,
    radial-gradient(120% 320px at 100% 0%, color-mix(in srgb, var(--teal-bright) 9%, transparent), transparent 72%),
    radial-gradient(100% 300px at 0% 100%, color-mix(in srgb, var(--accent-2) 7%, transparent), transparent 72%),
    var(--paper); }
/* Footers wrap. A drawer is min(520px, 94vw) wide, so any footer with more than
   three controls used to overflow it - the buttons crushed together and the
   last one clipped at the edge. Wrapping is the floor, not the design: a footer
   should still hold at most one primary action, one secondary, and a More menu
   (see core/record-panel.js). `row-gap` keeps a wrapped row from touching. */
.drawer-foot { padding: var(--sp-4) var(--sp-5); display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); justify-content: flex-end; }

/* FORM CONTROLS GET THEIR OWN STEP, ABOVE THE CARD THEY SIT IN.
   `.input/.select/.textarea` paint `background: var(--card)`, which is how they
   read as a well on white - the field is the same color as the card and the
   border draws the box. On graphite that produced an invisible control: a
   1px 9%-white line around nothing. A drawer's fields are half of what a drawer
   is for, so they are lifted a step ABOVE --card (a field inside a card and a
   field straight on the paper both have to be findable), given a border with
   enough weight to close the box, and a placeholder that is legible without
   being mistaken for a typed value. */
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.input, .select, .textarea) {
  background: #3a424f;
  border-color: rgba(255,255,255,.2);
}
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.input, .select, .textarea):hover:not(:focus) { border-color: rgba(255,255,255,.3); }
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.input, .select, .textarea):disabled,
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.input, .select, .textarea)[readonly] { background: #2b313b; color: var(--tx-soft); }
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.input, .textarea)::placeholder { color: rgba(238,240,245,.42); }
/* The native calendar / time / select indicators are drawn by the browser in
   the UA's own dark foreground; `color-scheme: dark` on .drawer handles most of
   it, but the date pickers still paint their glyph near-black on WebKit. */
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.input[type="date"], .input[type="time"], .input[type="datetime-local"])::-webkit-calendar-picker-indicator {
  filter: invert(1) opacity(.7);
}

/* ── THE THINGS ON THE PAPER, NOT AT IT (part two) ──────────────────────────
   The token block above lifts --card off the paper by seven points and leaves
   the edge to --line-soft, and on a card-sized surface that is enough. It is
   not enough for the SMALL ones. A 96px KPI tile, a status chip, a score card
   and a gauge are read at a glance and mostly in the corner of the eye, and at
   that size a 7-point step behind a 9%-white hairline is not a raised surface -
   it is a rectangle you have to look for. Jeremy's note on the client dossier,
   and he is right: Lifetime Value / Open Deals / Active Projects / Last Touch
   sat on graphite as four darker holes with their captions at 44% white, so the
   labels and the figures were fighting the background rather than sitting on
   something.

   So: one step higher (--card-2 over --card, not --card over paper), a top-lit
   gradient so the tile has a direction, a border with enough weight to close
   the shape, a 1px inner highlight along the top edge, and the caption lifted
   out of the mute step. That is the whole move, and it is written against the
   COMPONENT classes rather than the tokens because the tokens are correct for
   the big surfaces - moving them would flatten cards against tiles again.

   Light panels keep everything they had. These rules are inside the dark
   negation, so a tenant on paper is untouched. */
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.dossier-tile, .doss-score, .doss-why, .doss-line, .px-kpi, .sched-staff) {
  background: linear-gradient(168deg, var(--card-2), var(--card));
  border: 1px solid var(--line);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.055), 0 8px 18px -14px rgba(0,0,0,.9);
}
/* The score card is the one thing in the dossier a reader looks at first, so it
   gets the brand's own light rather than the neutral one - the same cyan the
   drawer head and the body's top corner are lit from. */
html[data-panel-theme="dark"] :is(.drawer, .modal .box) .doss-score {
  background: linear-gradient(150deg, color-mix(in srgb, var(--teal-bright) 8%, var(--card-2)), var(--card));
  border-color: color-mix(in srgb, var(--teal-bright) 20%, var(--line));
}
/* A caption at 44% white over a lifted tile is the one the eye drops. --tx-soft
   is the step meant for a label that has to be read but not first. */
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.dossier-tile .k, .sched-figs .fk) { color: var(--tx-soft); }
/* Rows that are links keep their hover, measured off the lifted surface rather
   than off --card, or the hover state came out darker than the rest. */
html[data-panel-theme="dark"] :is(.drawer, .modal .box) .doss-line.is-link:hover {
  background: linear-gradient(168deg, color-mix(in srgb, var(--teal) 7%, var(--card-3)), var(--card-2));
  border-color: color-mix(in srgb, var(--teal) 42%, var(--line));
}
/* CHIPS ARE SURFACES TOO.
   `.pill.neutral` paints --line-soft, a 9%-white wash that on white paper is a
   pale gray chip and on graphite is nothing at all - "Past Client" beside a
   company name was floating text with a faint ring around it. The tone pills
   (ok/warn/bad/info) carry their own color and are left alone. */
html[data-panel-theme="dark"] :is(.drawer, .modal .box) .pill.neutral {
  background: var(--card-3);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--tx);
}
html[data-panel-theme="dark"] :is(.drawer, .modal .box) :is(.doss-group-n, .doss-line-becomes) {
  background: var(--card-2);
  border-color: var(--line);
  color: var(--tx-soft);
}
/* The close mark is a 14px stroke X (inline SVG in ui.js), not the text glyph
   '×' - grid centering keeps it optically dead-center; font-size still sizes
   the legacy text-glyph remove buttons that share the class. */
.drawer .x { margin-left: auto; width: 34px; height: 34px; border: 0; background: var(--line-soft); border-radius: var(--r-sm); cursor: pointer; color: var(--tx-soft); font-size: 18px; display: grid; place-items: center; }
.drawer .x:hover { background: var(--line); color: var(--tx); }

/* ── Modal ──────────────────────────────────────────────────────────────── */
.modal { position: fixed; inset: 0; z-index: 90; display: grid; place-items: center; padding: var(--sp-5); }
/* 96vw was measured against the VIEWPORT while the box is laid out inside a
   wrapper that has already spent --sp-5 on each side, so on a phone the box
   asked for 360px in a 331px slot and every dialog in the product overflowed by
   ~15px per side. Subtracting the wrapper's own padding is the whole fix. */
/* The box is the FRAME, so it takes the panel scheme's paper step - the same
   surface a drawer's head and foot sit on - and the cards, tiles and fields
   inside it lift off that on --card. In the light scheme --drawer-paper is
   never defined and the fallback is the white card the box has always been. */
.modal .box { background: var(--drawer-paper, var(--card)); border-radius: var(--r-lg); box-shadow: var(--sh-3); width: min(540px, calc(100vw - 2 * var(--sp-5))); max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; }
/* A sheet holding a DOCUMENT rather than a question. 540px is the width of a
   confirmation dialog; a proposal read through it wraps every line of prose and
   squeezes the money table to nothing. 1040px is as wide as body copy can run
   before the line length itself becomes the problem. */
.modal .box.box-doc { width: min(1040px, calc(100vw - 2 * var(--sp-5))); max-height: 92vh; }
/* The head is a row so the close X can sit at its right edge. h3 keeps min-width:0
   so a long dialog title wraps instead of pushing the X off the box. */
/* The same whisper of brand wash the drawer head carries, for the same reason:
   a panel that opens with some light in it rather than bare surface. */
.modal .box-head { padding: var(--sp-5); display: flex; align-items: flex-start; gap: var(--sp-3); }
.modal .box-head > h3 { flex: 1; min-width: 0; }
.modal .box-x { flex: none; margin: -3px -3px 0 auto; width: 32px; height: 32px; border: 0; background: var(--line-soft); border-radius: var(--r-sm); cursor: pointer; color: var(--tx-soft); display: grid; place-items: center; }
.modal .box-x:hover { background: var(--line); color: var(--tx); }
.modal .box-body { padding: var(--sp-5); overflow-y: auto; }
.modal .box-foot { padding: var(--sp-4) var(--sp-5); display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); justify-content: flex-end; }

/* ── Choice row ───────────────────────────────────────────────────────────
   A pickable option with a title and a sentence explaining what it does, for
   a question whose answers are not simply yes and no. NOT a .btn: a button is
   built around a single non-wrapping label, so the explanation ran off the
   right edge of the dialog. Lives here rather than on a page because dialogs
   are appended to <body>, outside any page's scope. */
.choice-row { display: block; width: 100%; text-align: left; font-family: var(--f-body); cursor: pointer;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-sm, 10px);
  padding: 11px 13px; transition: border-color .12s, background .12s; }
.choice-row:hover { border-color: var(--teal); background: var(--card-2, var(--card)); }
.choice-row:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.choice-row > b { display: block; font-family: var(--f-head); font-size: 13.5px; font-weight: 700; color: var(--head, var(--ink)); margin-bottom: 3px; }
.choice-row > span { display: block; font-size: 12px; line-height: 1.5; color: var(--tx-soft); white-space: normal; }

/* ── Toast ────────────────────────────────────────────────────────────────
   TOP RIGHT, under the topbar - not floating over the bottom of the page.
   Bottom-center put every confirmation directly on top of the action bar of
   whatever was open (the booking overlay's Book button, a drawer's footer),
   so the message covered the control it was about. Up here it sits beside the
   bell, which is where the app's other messages live, and covers nothing.
   Duration and dismissal are handled in core/ui.js toast(). */
.toasts { position: fixed; top: calc(var(--topbar-h) + 10px); right: 18px; z-index: 100; display: flex; flex-direction: column; gap: 8px; align-items: flex-end; max-width: min(420px, calc(100vw - 36px)); }
.toast { display: flex; align-items: flex-start; gap: 10px; background: var(--ink); border: 1px solid transparent; color: var(--on-ink); font-size: 13.5px; font-weight: 500; line-height: 1.45; text-align: left; padding: 11px 14px 11px 16px; border-radius: 12px; box-shadow: var(--sh-3); animation: toast-in .22s ease; cursor: pointer; }
/* --ink is the CHROME near-black, which on paper is a dark chip floating over a
   pale page - exactly right. On a dark tenant --ink IS the page, so the same
   rule paints the page color onto the page and every confirmation becomes an
   invisible rectangle with text on it. There it needs a lifted surface and a
   real border to have an edge at all. */
:root[data-brand-polarity="dark"] .toast { background: var(--card-2); border-color: var(--line); color: var(--tx); }
.toast .toast-x { flex: none; margin: -2px -4px 0 0; width: 22px; height: 22px; border: 0; border-radius: 7px; background: rgba(255,255,255,.14); color: inherit; font-size: 15px; line-height: 1; cursor: pointer; }
.toast .toast-x:hover { background: rgba(255,255,255,.28); }
/* Status toasts keep a solid fill so they still read as an alert, but the status
   hues are LIFTED on this polarity (#3DDC97, #FF6B6B) - light text on either is
   unreadable, so they carry the same near-ink label the accent button does. */
.toast.ok { background: var(--ok); color: var(--on-teal); border-color: transparent; }
.toast.err { background: var(--bad); color: var(--on-teal); border-color: transparent; }
@keyframes toast-in { from { opacity: 0; transform: translateY(-8px); } }
@media (max-width: 700px) { .toasts { left: 18px; align-items: stretch; } }

/* ── Misc ───────────────────────────────────────────────────────────────── */
.empty { text-align: center; padding: var(--sp-7) var(--sp-5); color: var(--tx-mute); }
/* Empty-state icon: a tasteful badge, never a full-bleed graphic. The icon SVGs
   ship with only a viewBox (no intrinsic width/height), so without an explicit
   size they balloon to fill the container - this constrains them everywhere. */
.empty .ic { display: inline-grid; place-items: center; width: 56px; height: 56px; margin: 0 auto 14px; border-radius: 16px; background: var(--surface-2, rgba(255,255,255,.05)); border: 1px solid var(--line-soft); color: var(--accent, var(--teal)); font-size: 26px; line-height: 1; }
.empty .ic svg { width: 26px; height: 26px; }
.empty > svg { width: 42px; height: 42px; color: var(--tx-mute); }
.empty h3 { font-size: 16px; color: var(--tx-soft); margin-bottom: 4px; }
.empty p { font-size: 13px; max-width: 360px; margin: 4px auto 0; line-height: 1.6; }
.skel { background: linear-gradient(90deg, var(--line-soft), var(--line), var(--line-soft)); background-size: 200% 100%; animation: skel 1.2s infinite; border-radius: var(--r-sm); }
@keyframes skel { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.reveal { opacity: 0; transform: translateY(8px); animation: reveal .42s cubic-bezier(.22,1,.36,1) forwards; }
@keyframes reveal { to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .reveal, .toast, .skel { animation: none; opacity: 1; transform: none; } }
.avatar { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; font-family: var(--f-head); font-weight: 700; font-size: 14px; background: color-mix(in srgb, var(--teal) 16%, var(--card, #fff)); color: var(--teal-deep); flex-shrink: 0; }

/* ── Accessibility ──────────────────────────────────────────────────────────
   Visible keyboard focus for every interactive element, an honored
   reduced-motion preference, and a readable minimum body size. */
:where(a, button, input, select, textarea, summary, [tabindex], [role="button"], [draggable="true"]):focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 2px;
}
/* Don't show the focus ring for plain mouse clicks (focus-visible already
   handles this in modern browsers; this keeps older fallbacks tidy). */
:where(button, a):focus:not(:focus-visible) { outline: none; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Confirmation dialog (ui.js confirmAction) ────────────────────────────── */
.confirm-body { font-size: 14px; line-height: 1.55; color: var(--tx); }
.confirm-body b { color: var(--head, var(--ink)); }
.confirm-body .warn-note {
  margin-top: 12px; padding: 10px 12px; border-radius: 10px;
  background: var(--bad-soft); color:var(--bad-tx, var(--bad)); font-size: 13px;
}
/* Error toasts: pages call toast(msg,'bad') as often as 'err' - style both. */
.toast.bad { background: var(--bad); color: var(--on-teal); border-color: transparent; }
/* And 'warn', which nine call sites already used while no rule existed for it
   (3C review §3.5): a compliance override, a clamped rate, a canceled job, and
   the "email is not configured on this deployment" notice an administrator has
   to act on all rendered as the neutral default pill. `.pill.warn` and the
   --warn tokens have been here the whole time; only the toast was missing. */
.toast.warn { background: var(--warn); color: var(--on-teal); border-color: transparent; }

/* ── Phone widths ─────────────────────────────────────────────────────────────
   The spacing scale is a fixed ladder (--sp-6: 30px, --sp-7: 44px) that was
   tuned on a desktop console and never stepped down, so a 375px phone spent 60px
   of its width - 16% - on .page padding before a single pixel of content, and
   every fixed-minimum grid track inside then overflowed BECAUSE the column had
   been narrowed. Reported as "excessive deadspace"; it is the same rule seen
   from the other side.

   Reductions only. Nothing here changes layout structure - the grids, tables
   and panels collapse through their own rules above. */
@media (max-width: 640px) {
  .page { padding: var(--sp-4); padding-top: var(--sp-5); }
  .card-pad { padding: var(--sp-4); }
  .card-head { padding: var(--sp-3) var(--sp-4); }
}

/* Stat tiles: the 260px track cap plus `justify-content: start` is right on a
   wide report body (it reads as a deliberate short row rather than one orphan
   tile stretched across the hole) and wrong on a phone, where it produces ONE
   tile parked left with 55–94px of permanently dead gutter beside it. Below the
   wrap point there is no row to keep tidy, so the tiles take the width. */
@media (max-width: 700px) {
  .stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); justify-content: stretch; }
}

/* Panels take the whole screen on a phone. 94vw left a 6vw scrim strip that is
   too narrow to aim at and too wide to ignore, and the 22px head/body padding
   left ~308px of usable content inside a 352px panel. This is the treatment the
   Field Portal's .fp-sheet already uses, and it is why that surface reads
   correctly on a phone while the console does not. */
@media (max-width: 600px) {
  :root { --drawer-w: 100vw; --drawer-w-wide: 100vw; }
  .drawer-head, .drawer-body { padding: var(--sp-4); }
  .drawer-foot { padding: var(--sp-3) var(--sp-4); }
  /* A centered rounded card is a desktop shape. Anchored to the bottom edge, the
     dialog's actions land under the thumb and the sheet can use the full width;
     dvh keeps it correct while a mobile browser's toolbar collapses. */
  .modal { padding: 0; place-items: end stretch; }
  .modal .box { width: 100%; max-width: none; max-height: 92dvh; border-radius: var(--r-lg) var(--r-lg) 0 0; }
  .modal .box-head, .modal .box-body { padding: var(--sp-4); }
  .modal .box-foot { padding: var(--sp-3) var(--sp-4); }
}

/* ── Print ────────────────────────────────────────────────────────────────────
   Reports/Analytics are the pages people actually print or "Save as PDF".
   Without this, window.print() dumps the whole app shell - rail, topbar,
   drawers and all - onto the page. Strip the chrome, force plain white so the
   tinted card washes and the dark rail never print, and stop cards/tables
   from being sliced across page breaks. (The [data-theme="dark"] selector is
   future-proofing for a dark mode that does not exist yet - the app itself is
   the light operator console.) */
@media print {
  /* !important on custom properties is not decoration here. A custom-tier brand
     pack sets --paper/--card/--tx INLINE on :root, and an inline declaration
     beats any selector - so without this, a tenant whose canvas is dark (see
     darkCanvas() in src/core/brand.js) printed near-white text on black cards
     straight out of the browser's Print. A printed page is white paper at every
     tier; these are the values that make that true. */
  :root, :root[data-theme="dark"], :root[data-scheme] {
    --paper: #fff !important; --card: #fff !important; --card-2: #fff !important;
    --ink: #111 !important; --tx: #1a1a1a !important; --tx-soft: #333 !important;
    --tx-mute: #555 !important; --line: #d5d5d5 !important; --line-soft: #e6e6e6 !important;
    --head: #111 !important; --surface: #fff !important; --hairline: #d5d5d5 !important;
  }
  html, body { background: #fff !important; color: #111 !important; }

  /* App chrome has no meaning on paper. */
  .rail, .topbar, .toasts, .scrim, .drawer, .modal,
  .segctl, .cmdk, .trial-banner, .viewas-banner, .notif,
  [data-print="hide"] { display: none !important; }

  .shell { display: block !important; }
  #view, .page { margin: 0 !important; padding: 0 !important; width: 100% !important; }

  /* Flat cards read better than shadowed ones in print. */
  .card, .kpi, .stat {
    box-shadow: none !important; border: 1px solid #d5d5d5 !important;
    background: #fff !important; break-inside: avoid; page-break-inside: avoid;
  }
  .kpi .drill { display: none !important; }   /* the drill affordance can't be used on paper */

  table { break-inside: auto; }
  tr, .listrow { break-inside: avoid; page-break-inside: avoid; }
  thead { display: table-header-group; }      /* repeat headers on every page */

  a[href]::after { content: ""; }             /* don't append raw URLs */
  @page { margin: 14mm; }
}

/* ── The decision buttons on a client-facing sheet ─────────────────────────
   A proposal's "yes" is the brightest thing in the room, and the question next
   to it has to look askable. Both reuse the button shapes above - no second
   fill, no second shadow (see the note over .btn-primary). */
.btn.qt-go { --btn-fill: var(--teal-bright, var(--teal)); --on-teal: #06181B; }
.btn.btn-ghost.qt-ask {
  border-color: color-mix(in srgb, var(--teal) 42%, var(--line));
  color: var(--teal-deep, var(--teal));
  font-weight: 650;
}
.btn.btn-ghost.qt-ask:hover { border-color: var(--teal); background: color-mix(in srgb, var(--teal) 8%, transparent); }
