/* ===========================================================================
   Tophat — the single stylesheet.
   3 Point Forward. Rewritten 2026-08-28: palette, type scale, elevation.

   ---------------------------------------------------------------------------
   WHY THE OLD PALETTE IS GONE
   ---------------------------------------------------------------------------
   The navy #14213D + cyan #0C9ED9 pair was lifted from a former client's
   Elementor site. It was never chosen for this product, and it is the single
   most-used combination in proptech and fintech, so the software looked like
   every other tile in a comparison grid. Nothing about it said commercial real
   estate; it said "software company".

   ---------------------------------------------------------------------------
   THE DIRECTION: QUARRY
   ---------------------------------------------------------------------------
   Commercial real estate is a physical business — land, stone, steel, survey
   markers, brass plaques beside a lobby door, a deed that outlives everyone who
   signed it. So the ground is quarried stone rather than a screen colour: warm
   grey-greens with a genuine olive bias, never the blue-grey every dashboard
   defaults to. Put a Quarry surface beside a proptech surface and the proptech
   one looks cold.

     ACCENT      Pine  #1E5741.  Deep institutional green. This is the colour
                 the actual industry has used for a century — the brokerages,
                 the land trusts, the survey plates. It carries weight without
                 shouting, and it takes white text at 8.4:1 with room to spare,
                 which is what lets a primary button use a real gradient.
     SECONDARY   Brass #B08D3A / #7C5D18.  Building hardware, elevator plates,
                 the plaque with the address on it. Used for the accent spine,
                 the avatar, the mark on the hat, informational edges. Quiet
                 next to the pine, and warm enough to keep the page from
                 reading as a single-hue exercise.
     NEUTRALS    Stone. Hue-biased to ~80deg at very low chroma. Deliberately
                 not grey: a true grey next to pine reads as an absence of a
                 decision.

   GREEN IS A STATUS COLOUR AND NOTHING ELSE. An earlier pass made pine the
   accent as well, and the phone review caught what that costs: every primary
   button, every current tab and every link was the same green as "done" and
   "healthy", so the eye could not tell an action from an outcome, and a screen
   with four controls had four equally loud ones. Green now appears only on ok /
   done / healthy. Bronze carries the actions, one per screen region, and the
   rest step down to ghost or quiet. A page where exactly one thing is coloured
   tells you where to click before you have finished reading it.

   ---------------------------------------------------------------------------
   THE THREE THINGS THAT DO NOT MOVE
   ---------------------------------------------------------------------------
   1. THE SPINE. A 3px status edge on the left of every panel, row and card. It
      is the one device that is genuinely this product's. With the radii now at
      14-28px a square 3px border would ride the corner arc and taper to a
      crescent, so the spine is now drawn as an INSET ROUNDED PILL inside the
      padding (see "the spine, as a pill" near the end of this file). Same 3px,
      same colours, same meaning, correct geometry.
   2. STATE IS NEVER COLOUR ALONE. Every spine and every dot sits beside the
      word it stands for. Greyscale, colour blindness and a screen reader all
      get the same information.
   3. WCAG AA, BOTH THEMES, COMPUTED NOT EYEBALLED. Every pair below was run
      through scripts/contrast.js, including both endpoints of every gradient,
      because a gradient is where AA quietly dies: the button passes where you
      measured it and fails 40px lower.

   ---------------------------------------------------------------------------
   DEPTH
   ---------------------------------------------------------------------------
   Surfaces are dimensional rather than drawn. The page ground is stone; panels
   are lighter than the ground and sit ON it, with a layered shadow (three
   offsets, not one) and a 1px inner highlight along the top edge that reads as
   a lit surface. Primary buttons and the rail carry a short vertical gradient.
   Hover lifts by 1px and deepens the shadow.

   Under prefers-reduced-motion: no transform, no shadow animation, no
   transition at all. State changes land instantly. See the block at the end.
   =========================================================================== */

/* ------------------------------------------------------------------ fonts --
   Two self-hosted variable webfonts, then system stacks written out in full
   behind them. No CDN: this box runs on networks with no guaranteed internet
   and a strict CSP blocks third-party origins, so a webfont from someone else's
   server is a broken font rather than a nicer one. Same-origin woff2 has
   neither problem. The declarations are at the foot of this file.

   The previous build declared Anton and Montserrat with local() fallbacks
   against files that were never dropped in, so the display stack silently
   resolved to Arial Narrow: condensed, too light for a wordmark, and the
   weakest thing on the page. Those declarations are deleted rather than
   commented out.

   One family does the whole interface, separated by weight, size and tracking
   rather than by mixing three faces — which is most of what "mature" means
   typographically. Segoe UI Variable leads on Windows because it is a genuinely
   good face with real optical sizes and is not the sans every product uses;
   -apple-system picks up SF on macOS; Inter is honoured if the machine happens
   to have it; then the documented fallbacks. */
:root {
  /* DISPLAY: Franklin Gothic. Morris Fuller Benton, 1902 — the American gothic
     that period deeds, title cards and property certificates were actually set
     in, and it ships with Windows, which is what these machines are. Choosing
     it is the opposite of choosing Inter because Inter is safe: it is a face
     with a date and a place, and it is most of why the headings here do not
     look like every other product's headings. Falls back through the newer
     Libre Franklin, then to the system display face. */
  --fx-font-display: "Franklin Gothic Medium", "Franklin Gothic", "ITC Franklin Gothic",
                     "Libre Franklin", "Segoe UI Variable Display", -apple-system,
                     BlinkMacSystemFont, "Segoe UI Semibold", "Segoe UI", system-ui,
                     "Helvetica Neue", Arial, sans-serif;
  --fx-font-ui:      "Segoe UI Variable Text", -apple-system, BlinkMacSystemFont,
                     "Inter", "Segoe UI", system-ui,
                     "Helvetica Neue", Arial, sans-serif;
  --fx-font-body:    "Segoe UI Variable Text", -apple-system, BlinkMacSystemFont,
                     "Inter", "Segoe UI", system-ui,
                     "Helvetica Neue", Arial, sans-serif;
  --fx-font-mono:    ui-monospace, "Cascadia Mono", "Cascadia Code", Consolas,
                     "SF Mono", Menlo, monospace;
}

/* ------------------------------------------------------- tokens: the light --
   CANONICAL tokens are declared first. LEGACY names below point at them with
   var(), so the ~2900 lines written against --fx-color-cyan and friends keep
   working and re-hue themselves. The dark block overrides only the canonical
   set; the aliases resolve per element, so they follow automatically. Never
   override a legacy alias directly. */
:root {
  /* ---- surfaces. Ground is the page; bg is what sits ON the page. ---------
     These are two different things now, which they were not before: the old
     --fx-color-bg painted both the body and every panel, so a panel could not
     be lighter than the page it sat on and nothing could look raised. */
  /* THE BASE IS QUIET. Near-white ground, white cards, thin borders, generous
     air — the register of Linear or Notion. The warm deed-stock palette that
     preceded this put colour in the ONE place it should not go: everywhere. A
     tinted page cannot show you a tinted card.
     Neutrals carry a faint warm bias (a couple of points of red over blue)
     rather than being pure grey, which is what keeps white from going clinical
     next to the bronze accent. */
  --fx-color-ground:        #f7f8fb;   /* the page */
  --fx-color-bg:            #ffffff;   /* a card with no state, and every input */
  --fx-color-surface:       #f4f6fa;   /* inset tint inside a card */
  --fx-color-surface-2:     #e9edf6;   /* stronger inset: table heads, wells */
  --fx-color-zebra:         #fafbfd;   /* the striped row */

  /* ---- ink. Near-black with the same olive bias as the stone, so text sits
     in the palette rather than on top of it. */
  --fx-color-ink:           #1a2230;
  --fx-color-muted:         #525c6e;
  --fx-color-border:        #e7e9f0;   /* thin. It is a hairline, not a frame. */
  --fx-color-border-strong: #7c8595;   /* control boundaries, gated at 3:1 */

  /* ---- accent: BRONZE ----------------------------------------------------
     Revised 2026-08-28 after the phone review. The accent was pine, and pine is
     also what "done" and "healthy" are made of, so a primary action and a
     success state were the same colour — and with green on every button a
     screen with four controls had four equally loud ones.

     Green went back to being semantic and nothing else. The action colour is
     bronze: the plaque beside a lobby door, the plate on a survey marker, the
     hardware on a building that has been there eighty years. It is warm, it is
     unmistakably not a status colour, and it is the one thing on a page that is
     allowed to be saturated. One primary action per screen wears it; everything
     else steps down to ghost or quiet. That restraint IS the expensive look. */
  /* ACCENT: BRONZE, and the one-line defence — on a white Linear-register base
     every other candidate is either taken or generic. Green is the success
     state, amber is the warning, red is the failure, and indigo-on-white is the
     single most recognisable default in the category. Bronze is none of those,
     it reads as building hardware rather than as a software colour, and it is
     the only warm thing on a cool page, so the eye finds the primary action
     without being told where it is. */
  --fx-color-primary:       #1f347f;
  --fx-color-primary-lift:  #1f347f;
  --fx-color-primary-deep:  #1f347f;
  --fx-color-primary-hover: #14235c;
  --fx-color-on-primary:    #ffffff;
  --fx-color-primary-tint:  #eef1fb;
  --fx-color-accent-ink:    #1f347f;

  /* ---- the gold that is not text -----------------------------------------
     brass-solid is the block, the keyline and the mark. It is gated at 3:1 and
     it never carries a label — anything with words on it uses the darker
     --fx-color-primary, which clears 5.4:1 with white. */
  --fx-color-brass:         #1f347f;
  --fx-color-brass-solid:   #2b46b8;
  --fx-color-brass-tint:    #eef1fb;
  --fx-color-on-brass:      #ffffff;

  /* ---- state. Each tint carries the text colour of the same name.
     Two tokens per state on purpose: the darker one is the only one allowed to
     carry text, the -solid one is for 3px spines and 8px dots where the bar is
     3:1 rather than 4.5:1. Using the text colour for a spine makes a muddy
     spine; using the spine colour for text fails AA. */
  --fx-color-ok:            #0f6b43;
  --fx-color-ok-solid:      #1b8a57;
  --fx-color-on-ok:         #ffffff;
  --fx-color-ok-tint:       #e9f5ef;
  /* Burnt orange, pushed toward red. It has to be separable from the bronze
     accent at a glance, and an amber warn beside a bronze button was not. */
  --fx-color-warn:          #8a3d06;
  --fx-color-warn-solid:    #c2600c;
  --fx-color-warn-tint:     #fdf3e4;
  --fx-color-bad:           #9c1a23;
  --fx-color-bad-solid:     #ce3a42;
  --fx-color-bad-tint:      #fdedee;
  --fx-color-danger-bg:     #9c1a23;
  --fx-color-danger-hover:  #7d1119;
  --fx-color-on-danger:     #ffffff;
  --fx-color-neutral:       #4a5364;
  --fx-color-neutral-tint:  #eceef4;
  --fx-color-info:          #1f347f;
  --fx-color-info-tint:     #eef1fb;

  /* ---- focus. One ring, and it is deliberately NOT the accent: a pine ring
     around a pine button is a thicker button. This is pine driven almost to
     black, which reads against every surface in the theme (11.5:1 on the
     stone ground) and still belongs to the palette. */
  /* Bronze driven almost to black. Not the accent itself — a bronze ring
     around a bronze button is a thicker button — and deliberately not green,
     because green now means "ok" and a focus ring is not a success message. */
  --fx-color-focus:         #2b46b8;
  /* The rail is board felt, and a near-black ring vanishes on it. This is a lit
     brass at 8.4:1 there — the one place the interface genuinely needs a second
     ring colour, which is why this token survived the rewrite. */
  --fx-color-focus-on-dark: #2b46b8;   /* the rail is light; same ring as everywhere */
  --fx-color-selection-bg:  #1f347f;
  --fx-color-selection-fg:  #ffffff;

  /* ---- the rail. It is NOT a dark slab any more.
     A navy bar across the top is the tell that made this look like a template.
     A light stone chrome with one strong accent reads as the more expensive
     decision, and it also means the focus ring, the icons and the type all
     obey the same rules as the rest of the page instead of needing a second
     set of "on dark" tokens. */
  /* THE CHROME IS THE QUIETEST THING ON THE PAGE. It has been navy, then felt
     green, then press ink; every one of those made the rail the loudest element
     on every screen, and the rail is not what the product is about.

     GREEN EXISTS IN EXACTLY ONE PLACE NOW: --fx-color-ok and its tint. Not the
     rail, not the drawer, not a ground, not a hover. A green anywhere below
     outside the ok tokens is a bug.

     White, a hairline under it, and the bronze reserved for the section you are
     actually in. */
  --fx-color-railbg:        #ffffff;
  --fx-color-rail-2:        #ffffff;
  --fx-color-rail-ink:      #1a2230;
  --fx-color-rail-muted:    #525c6e;
  --fx-color-rail-line:     #e7e9f0;
  --fx-color-rail-accent:   #1f347f;

  /* ---- LEGACY ALIASES. Do not override these; override what they point at.
     --fx-color-cyan is now brass. That single indirection re-hues the stat
     spines, the assistant marker, the timeline dots, the citation numbers and
     the avatar without touching any of those rules. */
  --fx-color-cyan:          var(--fx-color-brass-solid);
  --fx-color-cyan-pale:     var(--fx-color-brass-tint);
  --fx-color-on-cyan:       var(--fx-color-on-brass);
  /* The engraved rule on a certificate. Inset on the two document surfaces
     (.doc and the first-run card) and nowhere else, so it stays ceremony
     rather than decoration. */
  --fx-color-keyline:       #d6dcf3;

  /* ------------------------------------------------------------- the type --
     A real scale. The old one ran 12 / 13 / 15 / 18 / 22 / 28 with the three
     working sizes inside 3px of each other, so a table label, a table cell and
     a paragraph were the same thing and no page had a shape.

     Two decisions do the work. The reading sizes step properly (12 / 14 / 16)
     instead of crowding, and the headline end opens right up (22 / 30 / 48) so
     a page title is unmistakably the title. Body went 15 -> 16px, which is
     paid for by tightening space rather than by shrinking type — a dense tool
     should be dense in its SPACING and generous in its type, and the previous
     build had that exactly backwards.

     11px exists for one job (legal, provenance marks) and is never body text. */
  --fx-text-2xs:  0.6875rem;   /* 11px */
  --fx-text-xs:   0.75rem;     /* 12px — eyebrows, chips, meta */
  --fx-text-sm:   0.875rem;    /* 14px — table body, secondary */
  --fx-text-md:   1rem;        /* 16px — body */
  --fx-text-lg:   1.125rem;    /* 18px — lead, panel titles */
  --fx-text-xl:   1.375rem;    /* 22px — h2 */
  --fx-text-2xl:  1.875rem;    /* 30px — stat figures, h1 inner */
  --fx-text-3xl:  clamp(2.25rem, 1.65rem + 2.6vw, 3rem);   /* 36 -> 48px */

  /* Line height by ROLE, not by size. A 48px headline at 1.6 is a poster with
     holes in it; a 16px paragraph at 1.2 is unreadable. */
  --fx-lh-display: 1.04;
  --fx-lh-head:    1.18;
  --fx-lh-snug:    1.35;   /* UI: labels, buttons, nav, chips */
  --fx-lh-body:    1.6;

  /* Tracking. Headings tighten as they grow, which is the whole reason large
     type set at 0 looks amateur. Eyebrows open up because they are small caps
     doing a signposting job. */
  --fx-track-display: -0.032em;
  --fx-track-head:    -0.018em;
  --fx-track-ui:      -0.004em;
  --fx-track-eyebrow:  0.1em;

  --fx-w-reg:   400;
  --fx-w-med:   500;
  --fx-w-semi:  600;
  --fx-w-bold:  700;
  --fx-w-black: 800;

  /* ------------------------------------------------------------- the space --
     ~12% tighter than the previous ramp at every step. The complaint was
     "roomy by default", and roomy-by-default is what a scale built on round
     rem values gives you. */
  --fx-space-1: 0.25rem;   --fx-space-2: 0.375rem;  --fx-space-3: 0.625rem;
  --fx-space-4: 0.875rem;  --fx-space-5: 1.25rem;   --fx-space-6: 1.75rem;
  --fx-space-7: 2.5rem;    --fx-space-8: 3.5rem;

  /* ----------------------------------------------------------- the corners --
     Materially rounder than the 6/10/14 this replaces. xs is for a mark inside
     a control, sm for chips and small controls, md for buttons and inputs, lg
     for a panel or card, xl for a surface that owns a region of the page, and
     pill for anything whose shape should read as a token rather than a box. */
  --fx-radius-xs:   6px;
  --fx-radius-sm:   10px;
  --fx-radius-md:   14px;
  --fx-radius-lg:   20px;
  --fx-radius-xl:   28px;
  --fx-radius-pill: 999px;
  --fx-spine: 3px;
  --fx-spine-inset: 7px;   /* how far the pill sits in from the left edge */

  /* -------------------------------------------------------- the elevation --
     Three offsets per step, not one. A single shadow is a drop shadow; three —
     a contact shadow, a form shadow and a wide ambient one — is how a surface
     reads as physically above the page. All of them are tinted with the
     palette's own near-black rather than pure black, so they sit in the stone
     instead of dirtying it.

     --fx-hi is the inner highlight along the top edge of a raised surface. It
     is what stops a rounded white panel from looking like a sticker. */
  /* Quiet. In this register a card is a hairline and a whisper; depth arrives
     on hover and on the things that genuinely float. The inner highlight is
     retired outright — it is a gloss trick, and it is actively wrong on a
     coloured card surface, where a white top edge reads as a rendering seam. */
  --fx-shadow-1: 0 1px 2px rgba(20, 20, 18, 0.05);
  --fx-shadow-2: 0 2px 4px rgba(20, 20, 18, 0.05),
                 0 8px 16px -6px rgba(20, 20, 18, 0.10);
  --fx-shadow-3: 0 4px 8px rgba(20, 20, 18, 0.06),
                 0 16px 32px -8px rgba(20, 20, 18, 0.14);
  --fx-hi:       0 0 transparent;
  --fx-hi-soft:  0 0 transparent;
  --fx-lo:       inset 0 -1px 0 rgba(20, 20, 18, 0.04);

  /* Gradients. Short and vertical: a surface catching light from above, not a
     decorative sweep. Both endpoints of every one of these is contrast-checked
     against the text that sits on it. */
  --fx-grad-primary: linear-gradient(180deg, var(--fx-color-primary-lift) 0%, var(--fx-color-primary-deep) 100%);
  --fx-grad-rail:    linear-gradient(180deg, var(--fx-color-railbg) 0%, var(--fx-color-rail-2) 100%);
  --fx-grad-raise:   linear-gradient(180deg, var(--fx-color-bg) 0%, var(--fx-color-surface) 100%);

  /* ---------------------------------------------------------- the motion --
     Short and purposeful. 120ms for a colour, 160ms for a lift, 200ms for
     something that moves a distance. The ease is a hard-out curve: it starts
     immediately and settles, which is what "smooth" means in an interface —
     nothing snaps and nothing floats. Every one of these is switched off
     wholesale under prefers-reduced-motion. */
  --fx-dur-1: 120ms;
  --fx-dur-2: 160ms;
  --fx-dur-3: 200ms;
  --fx-dur:   160ms;                                   /* legacy alias */
  --fx-ease:     cubic-bezier(0.22, 0.72, 0.24, 1);
  --fx-ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  --fx-gutter: clamp(0.875rem, 0.4rem + 1.6vw, 1.75rem);
  --fx-maxw: 1440px;
  --fx-rail-h: 60px;
  --fx-tap: 44px;
}

/* -------------------------------------------------------- tokens: the dark --
   Designed, not inverted. The ground goes to a near-black that keeps the olive
   bias, panels are LIGHTER than the ground exactly as they are in the light
   theme, and the accent stays a real pine rather than flipping to a pale mint —
   a filled dark-green button on a near-black page, separated by its gradient,
   its hairline and its shadow, is what this palette actually wants. Only the
   accent-as-TEXT flips light, because #1e5741 on #181b16 is unreadable and no
   amount of design intent changes that.

   Shadows are much deeper here: a 4% black shadow is invisible on #0f110e, so
   an unadjusted elevation scale silently stops working and the floating things
   stop floating. The inner highlight drops to 6% white for the same reason in
   reverse — 85% white on a dark surface is a scratch, not a lit edge. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --fx-color-ground:        #07182b;
    --fx-color-bg:            #0f2a46;
    --fx-color-surface:       #0a2138;
    --fx-color-surface-2:     #12314f;
    --fx-color-zebra:         #0c2440;

    --fx-color-ink:           #eef5fc;
    --fx-color-muted:         #b0c6db;
    --fx-color-border:        #1e3a55;
    --fx-color-border-strong: #5a80a3;

    /* Gold with a near-black label, rather than a dark fill with a white one.
       On a near-black ground a dark bronze button barely separates from the
       page, and a light gold at 4.5:1 against WHITE does not exist. Flipping
       the label is the honest fix and it is the better-looking one. */
    --fx-color-primary:       #1ec5da;
    --fx-color-primary-lift:  #2ad0e4;
    --fx-color-primary-deep:  #17b0c4;
    --fx-color-primary-hover: #42ddef;
    --fx-color-on-primary:    #042028;
    --fx-color-primary-tint:  #0d3547;
    --fx-color-accent-ink:    #42ddef;

    --fx-color-brass:         #42ddef;
    --fx-color-brass-solid:   #1ec5da;
    --fx-color-brass-tint:    #0d3547;
    --fx-color-on-brass:      #042028;

    --fx-color-ok:            #37d99a;
    --fx-color-ok-solid:      #37d99a;
    --fx-color-on-ok:         #04231a;
    --fx-color-ok-tint:       #0d2e26;
    --fx-color-warn:          #f0a165;
    --fx-color-warn-solid:    #f0a165;
    --fx-color-warn-tint:     #2e2416;
    --fx-color-bad:           #ff9aa0;
    --fx-color-bad-solid:     #ff9aa0;
    --fx-color-bad-tint:      #331920;
    --fx-color-danger-bg:     #ff9aa0;
    --fx-color-danger-hover:  #ffbcc0;
    --fx-color-on-danger:     #2a0b0d;
    --fx-color-neutral:       #b0c6db;
    --fx-color-neutral-tint:  #122b45;
    --fx-color-info:          #42ddef;
    --fx-color-info-tint:     #0d3547;

    --fx-color-focus:         #42ddef;
    --fx-color-focus-on-dark: #42ddef;
    /* Dark elevation still needs real depth; the light values are invisible on
       a near-black ground. */
    --fx-color-selection-bg:  #1ec5da;
    --fx-color-selection-fg:  #042028;

    /* One step above the ground, exactly as a card is. The rail is chrome, not
       a feature. */
    --fx-color-railbg:        #0f2a46;
    --fx-color-rail-2:        #0f2a46;
    --fx-color-rail-ink:      #eef5fc;
    --fx-color-rail-muted:    #b0c6db;
    --fx-color-rail-line:     #1e3a55;
    --fx-color-rail-accent:   #42ddef;
    --fx-color-keyline:       #1d5064;

    --fx-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45),
                   0 3px 8px rgba(0, 0, 0, 0.35);
    --fx-shadow-2: 0 2px 6px rgba(0, 0, 0, 0.50),
                   0 12px 28px -6px rgba(0, 0, 0, 0.55);
    --fx-shadow-3: 0 4px 12px rgba(0, 0, 0, 0.55),
                   0 24px 56px -10px rgba(0, 0, 0, 0.70);
    --fx-hi:       inset 0 1px 0 rgba(255, 255, 255, 0.07);
    --fx-hi-soft:  inset 0 1px 0 rgba(255, 255, 255, 0.04);
    --fx-lo:       inset 0 -1px 0 rgba(0, 0, 0, 0.35);
  }
}
/* ------------------------------------------------------------------ reset -- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  overflow-x: clip;                    /* clip, not hidden: keeps sticky working */
}

body {
  /* The page is stone; panels are white and sit ON it. This one line is what
     lets anything look raised — the old build painted body and panel the same
     colour, so every shadow had nothing to fall on. */
  background: var(--fx-color-ground);
  color: var(--fx-color-ink);
  font-family: var(--fx-font-body);
  font-size: var(--fx-text-md);
  line-height: var(--fx-lh-body);
  letter-spacing: var(--fx-track-ui);
  min-height: 100vh;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-synthesis-weight: none;
}

img, svg, video, canvas { display: block; max-width: 100%; }
/* Links take the accent-as-TEXT token, not the button fill. In dark mode those
   two are deliberately different colours: #2f7b5d is a good button and an
   unreadable link. */
a {
  color: var(--fx-color-accent-ink);
  text-decoration-thickness: 1px; text-underline-offset: 0.18em;
  text-decoration-color: color-mix(in srgb, currentColor 38%, transparent);
  transition: color var(--fx-dur-1) var(--fx-ease),
              text-decoration-color var(--fx-dur-1) var(--fx-ease);
}
a:hover { color: var(--fx-color-primary-hover); text-decoration-color: currentColor; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
ul, ol { padding: 0; }
table { border-collapse: collapse; }

/* THE HEADING SCALE.
   Sizes step properly now (48 / 30 / 22 / 18 / 16) instead of the old 25.6 / 22
   / 18 / 15 huddle, and each step carries its own tracking and leading rather
   than inheriting one compromise value. Tightening as type grows is most of
   what separates set headings from default ones: -0.032em at 48px and 0 at
   16px are the same optical amount of space. */
h1, h2, h3, h4, h5 {
  font-family: var(--fx-font-display);
  font-weight: var(--fx-w-bold);
  line-height: var(--fx-lh-head);
  letter-spacing: var(--fx-track-head);
  text-wrap: balance;
}
h1 {
  font-size: var(--fx-text-3xl); font-weight: var(--fx-w-black);
  line-height: var(--fx-lh-display); letter-spacing: var(--fx-track-display);
}
h2 { font-size: var(--fx-text-xl); }
h3 { font-size: var(--fx-text-lg); letter-spacing: -0.012em; }
h4 { font-size: var(--fx-text-md); letter-spacing: -0.006em; }
h5 { font-size: var(--fx-text-sm); letter-spacing: 0; }

:focus-visible {
  outline: 3px solid var(--fx-color-focus);
  outline-offset: 2px;
  border-radius: var(--fx-radius-md);
}
/* On the navy rail the light-surface focus blue measures 3.05:1 — technically
   past the non-text floor and not enough to actually see. The cyan is 5.25:1
   there, so the ring swaps rather than the person squinting. */
.rail :focus-visible,
.navdrawer :focus-visible { outline-color: var(--fx-color-focus-on-dark); }
:focus:not(:focus-visible) { outline: none; }
::selection { background: var(--fx-color-selection-bg); color: var(--fx-color-selection-fg); }

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

/* ---------------------------------------------------------------- helpers -- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.skip-link {
  position: absolute; top: -100px; left: var(--fx-space-4); z-index: 999;
  background: var(--fx-color-primary); color: var(--fx-color-on-primary);
  padding: var(--fx-space-3) var(--fx-space-5);
  font-family: var(--fx-font-ui); font-weight: 700;
  transition: top var(--fx-dur) var(--fx-ease);
}
.skip-link:focus { top: var(--fx-space-2); }

.mono { font-family: var(--fx-font-mono); font-size: 0.95em; font-variant-numeric: tabular-nums; }
.num  { font-family: var(--fx-font-mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.muted { color: var(--fx-color-muted); }
.nowrap { white-space: nowrap; }
.trunc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 34ch; }
.stack > * + * { margin-top: var(--fx-space-4); }
.stack-sm > * + * { margin-top: var(--fx-space-2); }
.row { display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: center; }
.row--end { justify-content: flex-end; }
.push { margin-left: auto; }
.hr { border: 0; border-top: 1px solid var(--fx-color-border); margin-block: var(--fx-space-5); }

.kicker {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: var(--fx-w-bold);
  letter-spacing: var(--fx-track-eyebrow); text-transform: uppercase; color: var(--fx-color-muted);
  line-height: var(--fx-lh-snug);
}

/* ------------------------------------------------------------------- rail --
   Rebuilt 2026-08-28.

   It was a flat navy slab with a cyan underline. That combination is the most
   common one in the category, so the product read as a template before anyone
   clicked anything. It is now a LIGHT stone chrome sitting above the page on a
   soft shadow, with a single strong accent doing the work the slab was doing by
   brute force. A light chrome with one confident accent reads as the more
   expensive decision; a dark slab reads as the safe one.

   The dividend is bigger than the look. With the rail on the same ground as the
   rest of the interface, the focus ring, the icons and the type obey ONE set of
   rules instead of a parallel "on dark" palette somebody has to remember to
   keep in step.

   MOTION, and only these three: the hover ground fades in (120ms), the 3px
   current-marker scales out from its centre (200ms), and the account menu rises
   6px as it opens (200ms). Transform and opacity only. All of it is switched
   off wholesale by the prefers-reduced-motion block at the end of this file. */
.rail {
  position: sticky; top: 0; z-index: 60;
  background: var(--fx-grad-rail);
  color: var(--fx-color-rail-ink);
  /* The brass keyline. On a Monopoly board the felt is bounded by a printed
     rule, and that one 2px line is what stops a dark band from reading as a
     generic dark band. */
  border-bottom: 2px solid var(--fx-color-rail-accent);
  box-shadow: var(--fx-shadow-2);
}
.rail__inner {
  max-width: var(--fx-maxw); margin-inline: auto;
  padding-inline: var(--fx-gutter);
  min-height: var(--fx-rail-h);
  display: flex; align-items: center; gap: var(--fx-space-3);
}

/* ---- the mark ---- */
.brand {
  display: inline-flex; align-items: center; gap: var(--fx-space-3);
  min-height: var(--fx-tap); color: var(--fx-color-rail-ink);
  text-decoration: none; flex: none;
  padding-right: var(--fx-space-2); border-radius: var(--fx-radius-md);
}
.brand__mark {
  width: 30px; height: 30px; flex: none;
  color: var(--fx-color-rail-ink);
  transition: transform var(--fx-dur-3) var(--fx-ease-out);
}
/* The hat lifts its brim a millimetre. It is the only piece of personality in
   the chrome and it costs one transform. */
.brand:hover .brand__mark { transform: translateY(-2px); }
/* The band on the hat. It used to be a hardcoded #0C9ED9 in the SVG, which is
   how a "no raw hex" rule quietly stops being true. */
.brand__band { fill: var(--fx-color-rail-accent); }
.brand__name {
  font-family: var(--fx-font-display); font-size: 1.375rem;
  font-weight: var(--fx-w-black);
  letter-spacing: 0.005em; text-transform: uppercase; line-height: 1;
}
.brand__sub {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: var(--fx-w-bold);
  letter-spacing: var(--fx-track-eyebrow); color: var(--fx-color-rail-accent); line-height: 1;
  border-left: 1px solid var(--fx-color-rail-line); padding-left: var(--fx-space-3);
}

/* ---- the menu button on narrow screens ---- */
.rail__toggle {
  margin-left: auto; display: inline-flex; align-items: center; gap: var(--fx-space-2);
  min-height: 40px; min-width: var(--fx-tap); justify-content: center;
  color: var(--fx-color-rail-ink); font-family: var(--fx-font-ui);
  font-size: var(--fx-text-sm); font-weight: var(--fx-w-semi);
  letter-spacing: var(--fx-track-ui); text-transform: none;
  background: transparent;
  border: 1px solid var(--fx-color-rail-line); border-radius: var(--fx-radius-pill);
  padding-inline: var(--fx-space-4);
  transition: border-color var(--fx-dur-1) var(--fx-ease),
              box-shadow var(--fx-dur-2) var(--fx-ease),
              transform var(--fx-dur-2) var(--fx-ease-out);
}
.rail__toggle:hover { border-color: var(--fx-color-rail-accent); background: var(--fx-color-rail-line); }

/* ---- the sections ---- */
.nav { display: none; }
.nav__list { display: flex; align-items: center; gap: 2px; list-style: none; }
.nav__link {
  position: relative;
  display: flex; align-items: center; min-height: var(--fx-tap);
  padding: 0 var(--fx-space-3);
  font-family: var(--fx-font-ui);
  /* 13px sentence case, not 12px uppercase with 0.08em tracking. It is more
     legible AND narrower — caps are about 15% wider per glyph before the
     tracking is added — so the twelve-item budget measured further down this
     file gets easier rather than harder. */
  font-size: 0.8125rem; font-weight: var(--fx-w-semi);
  letter-spacing: var(--fx-track-ui); text-transform: none;
  line-height: var(--fx-lh-snug);
  color: var(--fx-color-rail-muted); text-decoration: none;
  border-radius: var(--fx-radius-md);
  white-space: nowrap;
  transition: color var(--fx-dur-1) var(--fx-ease),
              background-color var(--fx-dur-1) var(--fx-ease);
}
/* THE CURRENT MARKER. A 3px rounded bar — the same device as the spine, laid on
   its side — that scales out from its centre when a section becomes current.
   Hover previews it at a third of its weight, so the row tells you what it will
   do before you commit to it. */
.nav__link::after {
  content: "";
  position: absolute; left: 6px; right: 6px; bottom: 5px;
  height: var(--fx-spine); border-radius: var(--fx-radius-pill);
  background: var(--fx-color-rail-accent);
  transform: scaleX(0); transform-origin: 50% 50%; opacity: 0;
  transition: transform var(--fx-dur-3) var(--fx-ease-out),
              opacity var(--fx-dur-2) var(--fx-ease);
}
.nav__link:hover {
  color: var(--fx-color-rail-ink);
  background: var(--fx-color-rail-line);
}
.nav__link:hover::after { transform: scaleX(1); opacity: 0.4; }
.nav__link[aria-current="page"] {
  color: var(--fx-color-rail-accent);
  background: var(--fx-color-rail-line);
  font-weight: var(--fx-w-bold);
}
.nav__link[aria-current="page"]::after { transform: scaleX(1); opacity: 1; }

/* ---- the account control ---- */
.who { margin-left: auto; position: relative; flex: none; }
.who__btn {
  display: inline-flex; align-items: center; gap: var(--fx-space-2);
  min-height: 40px; padding-inline: var(--fx-space-2) var(--fx-space-3);
  color: var(--fx-color-rail-ink);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: var(--fx-w-semi);
  letter-spacing: var(--fx-track-ui);
  border: 1px solid transparent; border-radius: var(--fx-radius-pill);
  transition: background-color var(--fx-dur-1) var(--fx-ease),
              border-color var(--fx-dur-1) var(--fx-ease);
}
.who__btn:hover, .who__btn[aria-expanded="true"] {
  border-color: var(--fx-color-rail-line); background: var(--fx-color-rail-line);
}
.who__name { display: none; }
.who__menu {
  position: absolute; right: 0; top: calc(100% + 8px); min-width: 250px; z-index: 70;
  background: var(--fx-color-bg); color: var(--fx-color-ink);
  border: 1px solid var(--fx-color-border);
  border-radius: var(--fx-radius-lg);
  padding: var(--fx-space-2);
  box-shadow: var(--fx-shadow-3), var(--fx-hi);
  transform-origin: top right;
}
.who__menu[hidden] { display: none; }
/* nav.js toggles [hidden] and cannot be edited, so the entrance is keyed off
   the attribute rather than a class the script would have to add. */
.who__menu:not([hidden]) { animation: fx-rise var(--fx-dur-3) var(--fx-ease-out) both; }
@keyframes fx-rise {
  from { opacity: 0; transform: translateY(-6px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}
.who__meta { padding: var(--fx-space-3); border-bottom: 1px solid var(--fx-color-border); margin-bottom: var(--fx-space-2); }
.who__meta b { display: block; font-family: var(--fx-font-ui); font-weight: var(--fx-w-bold); }
.who__menu a, .who__menu button {
  display: flex; align-items: center; width: 100%; min-height: 40px;
  padding: 0 var(--fx-space-3); text-align: left; text-decoration: none;
  color: var(--fx-color-ink); font-family: var(--fx-font-ui);
  font-size: var(--fx-text-sm); font-weight: var(--fx-w-med);
  border-radius: var(--fx-radius-md);
  transition: background-color var(--fx-dur-1) var(--fx-ease), color var(--fx-dur-1) var(--fx-ease);
}
.who__menu a:hover, .who__menu button:hover {
  background: var(--fx-color-primary-tint); color: var(--fx-color-accent-ink);
}

.avatar {
  width: 32px; height: 32px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--fx-color-primary); color: var(--fx-color-on-primary);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: var(--fx-w-bold);
  letter-spacing: 0.01em;
  box-shadow: var(--fx-hi-soft), 0 1px 2px rgba(26, 28, 23, 0.18);
}
.avatar--lg { width: 46px; height: 46px; font-size: var(--fx-text-md); }

/* ---- the drawer, below 1280px ----
   Not hover-driven; nav.js toggles [hidden]. It is a full-width panel under the
   rail rather than a modal, so nothing is trapped and nothing is dimmed. */
.navdrawer {
  background: var(--fx-grad-rail);
  border-bottom: 2px solid var(--fx-color-rail-accent);
  box-shadow: var(--fx-shadow-2);
  padding: var(--fx-space-3) var(--fx-gutter) var(--fx-space-5);
}
.navdrawer[hidden] { display: none; }
.navdrawer:not([hidden]) { animation: fx-drop var(--fx-dur-3) var(--fx-ease-out) both; }
@keyframes fx-drop {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}
.navdrawer a {
  position: relative;
  display: flex; align-items: center; min-height: var(--fx-tap);
  color: var(--fx-color-rail-muted); text-decoration: none;
  font-family: var(--fx-font-ui); font-size: var(--fx-text-md); font-weight: var(--fx-w-med);
  letter-spacing: var(--fx-track-ui); text-transform: none;
  border-bottom: 1px solid var(--fx-color-rail-line);
  border-left: 0 solid transparent;
  padding-left: 1.125rem; padding-right: var(--fx-space-3);
  border-radius: var(--fx-radius-md);
  transition: color var(--fx-dur-1) var(--fx-ease), background-color var(--fx-dur-1) var(--fx-ease);
}
.navdrawer a:hover { color: var(--fx-color-rail-ink); background: var(--fx-color-rail-line); }
.navdrawer a[aria-current="page"] {
  color: var(--fx-color-rail-accent); background: var(--fx-color-rail-line);
  font-weight: var(--fx-w-semi);
  border-left-color: var(--fx-color-rail-accent);   /* the block reads this */
}

/* 1280px, not 900px, and measured: see the "nav at twelve items" block further
   down. Between 900 and 1280 the nav used to become a horizontal scroller,
   which is a menu whose last items nobody scrolls to find. The drawer shows all
   of them at once instead. public/js/nav.js carries the same number. */
@media (min-width: 1280px) {
  .nav { display: block; margin-left: var(--fx-space-3); }
  .rail__toggle { display: none; }
  .navdrawer { display: none !important; }
  .who__name { display: inline; }
  .who { margin-left: auto; }
}

/* ------------------------------------------------------------------ shell -- */
.main {
  max-width: var(--fx-maxw); margin-inline: auto;
  padding: var(--fx-space-5) var(--fx-gutter) var(--fx-space-8);
}
.main--narrow { max-width: 760px; }
.main--plain { display: grid; place-items: center; min-height: 100vh; padding-block: var(--fx-space-6); }

/* One rhythm for the top of every screen: title block, a hairline, then 2rem
   of air before the first section. Every page in the system used to close that
   gap with whatever margin the section below happened to carry. */
.pagehead {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-5);
  align-items: flex-end; justify-content: space-between;
  padding-bottom: var(--fx-space-5); margin-bottom: var(--fx-space-6);
  border-bottom: 1px solid var(--fx-color-border);
}
.pagehead__t { min-width: 0; }
/* The deck under the title is a sentence, so it is measured like one. Left to
   the full 1440px column it ran to 180 characters and stopped being read. */
.pagehead__t p {
  color: var(--fx-color-muted); margin-top: var(--fx-space-2);
  max-width: 68ch; line-height: 1.55; text-wrap: pretty;
}
.pagehead__actions { display: flex; flex-wrap: wrap; gap: var(--fx-space-3); }

/* A trail, not a headline. Uppercase micro-tracking on a path made every
   breadcrumb read as loud as the h1 under it and cost about a third of the
   legibility at 12px. Sentence case, one step up in size, quiet. */
.crumbs { list-style: none; display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--fx-space-2);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: 600;
  letter-spacing: 0; text-transform: none; line-height: 1.4;
  margin-bottom: var(--fx-space-3); }
/* The slash is a rendered character, so it answers to the text ratio and not
   to the non-text one. At --fx-color-border-strong it measured 1.94:1 and was
   effectively absent; muted is the colour the crumbs beside it already use. */
.crumbs li + li::before { content: "/"; color: var(--fx-color-muted); margin-right: var(--fx-space-2); }
.crumbs a { color: var(--fx-color-muted); text-decoration: none; }
.crumbs a:hover { color: var(--fx-color-primary); }

.footer {
  border-top: 1px solid var(--fx-color-border);
  padding: var(--fx-space-4) var(--fx-gutter);
  max-width: var(--fx-maxw); margin-inline: auto;
  color: var(--fx-color-muted); font-size: var(--fx-text-xs);
  display: flex; flex-wrap: wrap; gap: var(--fx-space-4); justify-content: space-between;
}

/* ------------------------------------------------------------------ grid -- */
.grid { display: grid; gap: var(--fx-space-4); }
.grid--2 { grid-template-columns: 1fr; }
.grid--3 { grid-template-columns: 1fr; }
.grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--main { grid-template-columns: 1fr; }
@media (min-width: 760px) {
  .grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1080px) {
  .grid--main { grid-template-columns: minmax(0, 1fr) 340px; align-items: start; }
  .grid--main--wide { grid-template-columns: minmax(0, 1fr) 400px; }
}

/* ----------------------------------------------------------------- panel --
   A panel is a raised surface, not a drawn rectangle. It is lighter than the
   page it sits on, it carries a three-offset shadow and a 1px inner highlight
   along its top edge, and its corners are 20px. The highlight is the detail
   that matters: without it a rounded white box on a light ground looks like a
   sticker, and with it the surface looks lit from above. */
.panel {
  position: relative;
  background: var(--fx-color-bg);
  border: 1px solid var(--fx-color-border);
  /* Transparent by default so a panel with no state wears no colour block. The
     --ok / --warn / --bad / --info / --primary modifiers below set this, and
     the block inherits it. */
  border-left-color: transparent;
  border-radius: var(--fx-radius-lg);
  box-shadow: var(--fx-shadow-1), var(--fx-hi);
  min-width: 0;
}
/* Flat means inset, not raised: it belongs to the page rather than floating
   above it, so it loses the shadow and the highlight rather than keeping them
   at half strength. */
.panel--flat {
  background: var(--fx-color-surface);
  box-shadow: none;
}
.panel--ok      { border-left-color: var(--fx-color-ok); }
.panel--warn    { border-left-color: var(--fx-color-warn-solid); }
.panel--bad     { border-left-color: var(--fx-color-bad-solid); }
.panel--info    { border-left-color: var(--fx-color-brass-solid); }
.panel--primary { border-left-color: var(--fx-color-primary); }

/* Density. The old padding budget was an internal tool's, and the content was
   touching four edges at once; the new one is tighter than that at every step
   because the space ramp itself came in about 12%. The wider step still only
   arrives when the screen can pay for it, so a phone does not spend 40px of a
   343px column on margin. */
.panel__head {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: center;
  padding: var(--fx-space-4);
  border-bottom: 1px solid var(--fx-color-border);
}
.panel__head h2, .panel__head h3 { font-size: var(--fx-text-md); letter-spacing: -0.008em; }
.panel__body { padding: var(--fx-space-4); }
.panel__body--tight { padding: var(--fx-space-3) var(--fx-space-4); }
.panel__foot {
  padding: var(--fx-space-4);
  border-top: 1px solid var(--fx-color-border);
  background: var(--fx-color-surface);
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: center;
}
@media (min-width: 760px) {
  .panel__head { padding: var(--fx-space-4) var(--fx-space-5); }
  .panel__body { padding: var(--fx-space-5); }
  .panel__body--tight { padding: var(--fx-space-3) var(--fx-space-5); }
  .panel__foot { padding: var(--fx-space-4) var(--fx-space-5); }
}
/* A scroller that IS the panel's body draws no second frame. Two 1px borders
   with nothing between them is the tell that two components were built by
   different people on different days. */
.panel > .table-scroll { border: 0; border-radius: 0; box-shadow: none; }

/* ------------------------------------------------------------------ stat --
   A figure tile. Raised like a panel, because a headline number that sits flat
   on the page while the panels around it float reads as an afterthought. */
.stat {
  position: relative;
  background: var(--fx-color-bg);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-lg);
  box-shadow: var(--fx-shadow-1), var(--fx-hi);
  padding: var(--fx-space-4) var(--fx-space-4) var(--fx-space-4) var(--fx-space-5);
  /* The figure is sized against this box, not the viewport. Four commission
     totals in a four-up grid are narrow at 768px and narrow again at 390px in
     the two-up, and a viewport rule cannot tell those apart. */
  container-type: inline-size;
  transition: box-shadow var(--fx-dur-2) var(--fx-ease),
              transform var(--fx-dur-2) var(--fx-ease-out);
}
.stat__v {
  display: block; font-family: var(--fx-font-display); font-size: var(--fx-text-2xl);
  font-weight: var(--fx-w-black);
  line-height: var(--fx-lh-display); letter-spacing: var(--fx-track-display);
  font-variant-numeric: tabular-nums;
}
/* A gross commission figure runs to fourteen characters. At the full display
   size that is wider than a quarter of a phone screen, and it was being sheared
   mid-digit: "$1,222,500.0". Shrink to fit the column instead of clipping, and
   never below the point where it stops reading as a headline number. Guarded
   because a browser without container queries must keep the fixed size rather
   than resolve cqi to zero. */
@supports (container-type: inline-size) {
  .stat__v { font-size: clamp(1.125rem, 12cqi, var(--fx-text-2xl)); }
}
.stat__l {
  display: block; margin-top: var(--fx-space-2);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: var(--fx-w-bold);
  letter-spacing: var(--fx-track-eyebrow); line-height: var(--fx-lh-snug);
  text-transform: uppercase; color: var(--fx-color-muted);
}
.stat--ok   { border-left-color: var(--fx-color-ok); }
.stat--warn { border-left-color: var(--fx-color-warn-solid); }
.stat--bad  { border-left-color: var(--fx-color-bad-solid); }
a.stat { text-decoration: none; color: inherit; display: block; }
/* A tile that is a link lifts. A tile that is not, does not — the lift is what
   says "this goes somewhere". */
a.stat:hover { box-shadow: var(--fx-shadow-2), var(--fx-hi); transform: translateY(-2px); }
a.stat:active { transform: translateY(0); box-shadow: var(--fx-shadow-1), var(--fx-hi); }

/* --------------------------------------------------------------- buttons --
   Three genuinely different objects, which is the point. Before this they were
   one flat pill with three colours, so nothing on a screen told you which
   control was the one to press.

     PRIMARY  a filled, gradient, lifted object. It has a shadow, an inner top
              highlight and a darker hairline under it, so it reads as a
              physical key sitting on the page. One per screen region.
     GHOST    a raised but EMPTY surface: panel-white fill, a real border, a
              shallow shadow. It is a button, it just isn't the button.
     QUIET    not an object at all. Text with a hover ground and no border, no
              fill and no shadow. Used where a control must be available but
              must not compete.

   Every state is defined for all three: rest, hover, active, focus-visible and
   disabled. Active always removes the lift and the shadow — a key that is being
   pressed goes DOWN, and getting that one detail right is most of why an
   interface feels responsive rather than animated. */
.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--fx-space-2);
  min-height: var(--fx-tap); min-width: var(--fx-tap);
  padding: 0 var(--fx-space-5);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: var(--fx-w-semi);
  letter-spacing: var(--fx-track-ui); text-transform: none; text-decoration: none;
  line-height: var(--fx-lh-snug);
  border: 1px solid transparent; border-radius: var(--fx-radius-pill);
  cursor: pointer; white-space: nowrap;
  color: var(--fx-color-ink);
  transition: background-color var(--fx-dur-1) var(--fx-ease),
              background-image var(--fx-dur-1) var(--fx-ease),
              border-color var(--fx-dur-1) var(--fx-ease),
              color var(--fx-dur-1) var(--fx-ease),
              box-shadow var(--fx-dur-2) var(--fx-ease),
              transform var(--fx-dur-2) var(--fx-ease-out);
}

/* ---- primary ---- */
.btn--primary {
  background-image: var(--fx-grad-primary);
  background-color: var(--fx-color-primary);
  color: var(--fx-color-on-primary);
  border-color: var(--fx-color-primary-deep);
  box-shadow: var(--fx-shadow-1), inset 0 1px 0 rgba(255, 255, 255, 0.16);
}
.btn--primary:hover {
  background-image: none;
  background-color: var(--fx-color-primary-hover);
  border-color: var(--fx-color-primary-hover);
  color: var(--fx-color-on-primary);
  box-shadow: var(--fx-shadow-2), inset 0 1px 0 rgba(255, 255, 255, 0.16);
  transform: translateY(-1px);
}
.btn--primary:active {
  transform: translateY(0);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.22);
  background-image: none;
}

/* ---- ghost ---- */
.btn--ghost {
  background: var(--fx-color-bg);
  border-color: var(--fx-color-border-strong);
  color: var(--fx-color-ink);
  box-shadow: var(--fx-shadow-1), var(--fx-hi);
}
.btn--ghost:hover {
  border-color: var(--fx-color-primary);
  color: var(--fx-color-accent-ink);
  background: var(--fx-color-primary-tint);
  box-shadow: var(--fx-shadow-2), var(--fx-hi);
  transform: translateY(-1px);
}
.btn--ghost:active {
  transform: translateY(0);
  box-shadow: inset 0 2px 4px rgba(26, 28, 23, 0.10);
}

/* ---- quiet ---- */
.btn--quiet {
  color: var(--fx-color-muted); background: none; border-color: transparent;
  box-shadow: none; padding-inline: var(--fx-space-4);
}
.btn--quiet:hover {
  color: var(--fx-color-accent-ink); background: var(--fx-color-surface);
  box-shadow: none; transform: none;
}
.btn--quiet:active { background: var(--fx-color-surface-2); }

/* ---- accent. Brass carries a DARK label; it is the one solid brass surface
   in the system besides the avatar. ---- */
.btn--accent {
  background: var(--fx-color-primary); color: var(--fx-color-on-primary);
  border-color: var(--fx-color-primary-deep);
  box-shadow: var(--fx-shadow-1), inset 0 1px 0 rgba(255, 255, 255, 0.16);
}
.btn--accent:hover {
  background: var(--fx-color-primary-hover); color: var(--fx-color-on-primary);
  border-color: var(--fx-color-primary-hover);
  box-shadow: var(--fx-shadow-2); transform: translateY(-1px);
}
.btn--accent:active { transform: translateY(0); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.20); }

/* ---- danger ---- */
.btn--danger {
  background: var(--fx-color-danger-bg); color: var(--fx-color-on-danger);
  border-color: var(--fx-color-danger-bg);
  box-shadow: var(--fx-shadow-1), inset 0 1px 0 rgba(255, 255, 255, 0.14);
}
.btn--danger:hover {
  background: var(--fx-color-danger-hover); border-color: var(--fx-color-danger-hover);
  color: var(--fx-color-on-danger);
  box-shadow: var(--fx-shadow-2); transform: translateY(-1px);
}
.btn--danger:active { transform: translateY(0); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.22); }

.btn:focus-visible { outline: 3px solid var(--fx-color-focus); outline-offset: 2px; }
.btn--sm { min-height: 36px; padding-inline: var(--fx-space-4); font-size: var(--fx-text-xs); }
.btn--block { width: 100%; }
/* Disabled loses the lift as well as the colour. A greyed control that still
   casts a shadow looks broken rather than unavailable. */
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: 0.45; cursor: not-allowed; box-shadow: none; transform: none;
  background-image: none;
}

/* Any button that only carries an icon still needs a 44px box and a label. */
.iconbtn {
  display: inline-grid; place-items: center; width: var(--fx-tap); height: var(--fx-tap);
  border-radius: var(--fx-radius-pill); color: var(--fx-color-muted);
  transition: background-color var(--fx-dur-1) var(--fx-ease), color var(--fx-dur-1) var(--fx-ease);
}
.iconbtn:hover { background: var(--fx-color-surface); color: var(--fx-color-ink); }

/* ----------------------------------------------------------------- forms -- */
.field { display: block; margin-bottom: var(--fx-space-4); min-width: 0; }
.field__label, .label {
  display: block; margin-bottom: var(--fx-space-2);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--fx-color-ink);
}
.field__hint { display: block; margin-top: var(--fx-space-2); font-size: var(--fx-text-sm); line-height: 1.5; color: var(--fx-color-muted); }
.field__err {
  display: block; margin-top: var(--fx-space-2);
  font-size: var(--fx-text-sm); color: var(--fx-color-bad); font-weight: 600;
}
.req { color: var(--fx-color-bad); }

/* One height and one inset for every control on the system. The 0.75rem inset
   put the caret three pixels off the border and made a filled field look like
   a table cell. */
.input, .select, .textarea {
  width: 100%; min-height: var(--fx-tap);
  padding: var(--fx-space-2) var(--fx-space-4);
  background: var(--fx-color-bg); color: var(--fx-color-ink);
  border: 1px solid var(--fx-color-border-strong); border-radius: var(--fx-radius-md);
  /* A text field has no label INSIDE it, so its border is the only thing that
     says "type here" — which is exactly the case WCAG 1.4.11 puts at 3:1.
     --fx-color-border-strong measures 1.94:1 on white and 2.61:1 on the dark
     page, so every input in the system was under the bar. This is the same
     token stepped toward the ink until it clears: 3.66:1 on the page, 3.41 on
     a panel tint and 3.19 on the header tint in light, 5.70 / 4.90 in dark.
     A browser without color-mix() drops the line and keeps the old border, so
     the failure mode is the status quo rather than an invisible field. */
  border-color: color-mix(in srgb, var(--fx-color-muted) 78%, var(--fx-color-bg));
  font-family: var(--fx-font-body); font-size: var(--fx-text-md); line-height: 1.5;
  transition: border-color var(--fx-dur) var(--fx-ease);
}
.input:hover, .select:hover, .textarea:hover { border-color: var(--fx-color-primary); }
/* The global ring sits 2px outside the control. Recolouring the border to match
   closes the gap so the ring and the field read as one focused object rather
   than as a box with a halo near it. */
.input:focus-visible, .select:focus-visible, .textarea:focus-visible {
  border-color: var(--fx-color-focus);
}
.input[aria-invalid="true"], .select[aria-invalid="true"], .textarea[aria-invalid="true"] {
  border-color: var(--fx-color-bad); border-width: 2px;
}
.textarea { min-height: 7rem; resize: vertical; line-height: 1.6; padding-block: var(--fx-space-3); }
.select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 1.05rem center, right 0.75rem center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: var(--fx-space-6);
}
.input--mono { font-family: var(--fx-font-mono); }
.input--sm, .select--sm { min-height: 36px; font-size: var(--fx-text-sm); padding-block: 2px; padding-inline: var(--fx-space-3); }

.check, .radio {
  display: flex; align-items: flex-start; gap: var(--fx-space-3);
  min-height: var(--fx-tap); padding: var(--fx-space-2) 0; cursor: pointer;
}
.check input, .radio input { width: 20px; height: 20px; margin-top: 2px; flex: none; accent-color: var(--fx-color-primary); }
.check span, .radio span { font-size: var(--fx-text-md); }

.inline-form { display: inline-flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center; }
.filters {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-4); align-items: flex-end;
  padding: var(--fx-space-4); background: var(--fx-color-surface);
  border: 1px solid var(--fx-color-border); border-left: 0 solid transparent;
  border-radius: var(--fx-radius-md); margin-bottom: var(--fx-space-4);
}
.filters .field { margin-bottom: 0; flex: 1 1 180px; min-width: 0; }
.filters .field--grow { flex: 3 1 260px; }

/* ----------------------------------------------------------------- chips --
   A chip is a token, so it is a full pill with a hairline of its own colour
   rather than a flat lozenge. The border is what keeps a pale tint from
   dissolving into a pale panel. */
.chip {
  display: inline-flex; align-items: center; gap: var(--fx-space-2);
  padding: 2px var(--fx-space-3); border-radius: var(--fx-radius-pill);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: var(--fx-w-bold);
  letter-spacing: 0.005em; line-height: 1.5; white-space: nowrap;
  background: var(--fx-color-neutral-tint); color: var(--fx-color-neutral);
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
}
.chip--ok      { background: var(--fx-color-ok-tint);   color: var(--fx-color-ok); }
.chip--warn    { background: var(--fx-color-warn-tint); color: var(--fx-color-warn); }
.chip--bad     { background: var(--fx-color-bad-tint);  color: var(--fx-color-bad); }
.chip--info    { background: var(--fx-color-info-tint); color: var(--fx-color-info); }
.chip--neutral { background: var(--fx-color-neutral-tint); color: var(--fx-color-neutral); }
/* Brass chip, dark label. */
.chip--accent  { background: var(--fx-color-primary); color: var(--fx-color-on-primary); border-color: transparent; }
.chip__dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; }

/* The filter bar on the import review. These ARE pressable, so they get the
   ghost button's treatment: a raised surface that lifts on hover and goes down
   when pressed. */
.chipbar { display: flex; flex-wrap: wrap; gap: var(--fx-space-2); list-style: none; }
.chipbar a, .chipbar button {
  display: inline-flex; align-items: center; gap: var(--fx-space-2);
  min-height: 40px; padding: 0 var(--fx-space-4);
  border: 1px solid var(--fx-color-border-strong); border-radius: var(--fx-radius-pill);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: var(--fx-w-semi);
  letter-spacing: var(--fx-track-ui); text-transform: none;
  color: var(--fx-color-ink); text-decoration: none; background: var(--fx-color-bg);
  box-shadow: var(--fx-shadow-1), var(--fx-hi);
  transition: background-color var(--fx-dur-1) var(--fx-ease),
              border-color var(--fx-dur-1) var(--fx-ease),
              box-shadow var(--fx-dur-2) var(--fx-ease),
              transform var(--fx-dur-2) var(--fx-ease-out);
}
.chipbar a:hover, .chipbar button:hover {
  border-color: var(--fx-color-primary); background: var(--fx-color-primary-tint);
  box-shadow: var(--fx-shadow-2), var(--fx-hi); transform: translateY(-1px);
}
.chipbar a:active, .chipbar button:active { transform: translateY(0); box-shadow: none; }
.chipbar a[aria-current="true"], .chipbar button[aria-pressed="true"] {
  background-image: var(--fx-grad-primary);
  background-color: var(--fx-color-primary);
  border-color: var(--fx-color-primary-deep); color: var(--fx-color-on-primary);
}
.chipbar .n {
  font-family: var(--fx-font-mono); font-size: var(--fx-text-xs);
  padding: 1px 7px; border-radius: var(--fx-radius-pill);
  background: var(--fx-color-surface-2); color: var(--fx-color-ink);
}
.chipbar a[aria-current="true"] .n { background: rgba(255, 255, 255, 0.22); color: var(--fx-color-on-primary); }
/* These used to be 3px left borders. A square edge on a 999px pill is wrong at
   any radius, so the state reads as a dot instead — the same device the tables
   and board cards already use, and it sits beside the word as always. */
.chipbar .is-ok::before,
.chipbar .is-warn::before,
.chipbar .is-bad::before,
.chipbar .is-dup::before,
.chipbar .is-info::before {
  content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--fx-color-neutral);
}
.chipbar .is-ok::before   { background: var(--fx-color-ok-solid); }
.chipbar .is-warn::before { background: var(--fx-color-warn-solid); }
.chipbar .is-bad::before  { background: var(--fx-color-bad-solid); }
.chipbar .is-dup::before  { background: var(--fx-color-neutral); }
.chipbar .is-info::before { background: var(--fx-color-brass-solid); }
.chipbar a[aria-current="true"]::before,
.chipbar button[aria-pressed="true"]::before { background: var(--fx-color-on-primary); }

/* ----------------------------------------------------------------- alert -- */
.alert {
  display: flex; gap: var(--fx-space-3); align-items: flex-start;
  padding: var(--fx-space-3) var(--fx-space-4);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid var(--fx-color-primary);
  border-radius: var(--fx-radius-md);
  background: var(--fx-color-surface);
  margin-bottom: var(--fx-space-4);
}
.alert__b { min-width: 0; }
.alert__t { font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-md); }
.alert p { margin-top: var(--fx-space-1); }
.alert--ok   { border-left-color: var(--fx-color-ok);   background: var(--fx-color-ok-tint); }
.alert--warn { border-left-color: var(--fx-color-warn-solid); background: var(--fx-color-warn-tint); }
.alert--bad  { border-left-color: var(--fx-color-bad-solid);  background: var(--fx-color-bad-tint); }
.alert--info { border-left-color: var(--fx-color-cyan); background: var(--fx-color-info-tint); }

.flash { max-width: var(--fx-maxw); margin: var(--fx-space-4) auto 0; padding-inline: var(--fx-gutter); }
.flash .alert { margin-bottom: 0; }

/* ----------------------------------------------------------------- table -- */
.table-scroll {
  overflow-x: auto; overflow-y: visible;
  border: 1px solid var(--fx-color-border); border-radius: var(--fx-radius-md);
  background: var(--fx-color-bg);
  -webkit-overflow-scrolling: touch;
}
.table-scroll--tall { max-height: 70vh; overflow-y: auto; }
.table { width: 100%; min-width: 720px; font-size: var(--fx-text-sm); }
.table--wide { min-width: 1000px; }
.table th, .table td {
  padding: var(--fx-space-3); text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--fx-color-border);
}
/* The header sits on its own plane. It used to share --fx-color-surface with
   the row hover, so on a long table the eye could not tell the labels from a
   row the mouse happened to be over. Three planes now, in order of loudness:
   hovered row, header, everything else. */
.table thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--fx-color-surface-2);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--fx-color-muted);
  white-space: nowrap; border-bottom: 1px solid var(--fx-color-border-strong);
}
/* Zebra at about half the strength of the panel tint. Enough to hold the eye on
   one line across seven columns, quiet enough that the row spine is still the
   loudest thing on the left edge. Dropped whole on a browser without
   color-mix(), which costs the striping and nothing else. */
.table tbody tr:nth-child(even) {
  background: color-mix(in srgb, var(--fx-color-surface) 55%, var(--fx-color-bg));
}
.table tbody tr:hover { background: var(--fx-color-primary-tint); }
.table tbody tr:last-child td { border-bottom: 0; }
.table td a { font-weight: 600; }
.table .num { text-align: right; }
.table th.num { text-align: right; }

/* Sortable column headers are real links, so they work without JS. */
.th-sort { display: inline-flex; align-items: center; gap: var(--fx-space-2);
  min-height: 32px; color: inherit; text-decoration: none; }
.th-sort:hover { color: var(--fx-color-primary); }
.th-sort .arrow { font-size: 0.7em; opacity: 0; }
.th-sort[aria-sort] .arrow, .table th[aria-sort] .th-sort .arrow { opacity: 1; }
.table th[aria-sort] { color: var(--fx-color-ink); }

/* The row spine: 3px of state on the left edge of a data row. Always paired
   with a text chip in the same row, never colour on its own. */
.table tbody tr td:first-child { border-left: var(--fx-spine) solid transparent; }
.row-ok   td:first-child { border-left-color: var(--fx-color-ok); }
.row-warn td:first-child { border-left-color: var(--fx-color-warn-solid); }
.row-bad  td:first-child { border-left-color: var(--fx-color-bad-solid); }
.row-dup  td:first-child { border-left-color: var(--fx-color-neutral); }
.row-muted { color: var(--fx-color-muted); }

.pager { display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center; margin-top: var(--fx-space-4); }
.pager a, .pager span {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--fx-tap); min-width: var(--fx-tap); padding-inline: var(--fx-space-3);
  border: 1px solid var(--fx-color-border); border-radius: var(--fx-radius-md);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); text-decoration: none;
}
.pager [aria-current="page"] { background: var(--fx-color-primary); color: var(--fx-color-on-primary); border-color: var(--fx-color-primary); }

/* ------------------------------------------------------------ empty state -- */
.empty {
  padding: var(--fx-space-7) var(--fx-space-5);
  text-align: center; color: var(--fx-color-muted);
  border: 1px dashed var(--fx-color-border-strong); border-radius: var(--fx-radius-md);
  background: var(--fx-color-surface);
}
.empty h2, .empty h3 { color: var(--fx-color-ink); font-size: var(--fx-text-lg); margin-bottom: var(--fx-space-2); }
.empty p { max-width: 56ch; margin-inline: auto; }
.empty .row { justify-content: center; margin-top: var(--fx-space-4); }
.empty--sm { padding: var(--fx-space-5) var(--fx-space-4); }

/* -------------------------------------------------------------- subnav --
   The tab strip. Same grammar as the rail: a rounded hover ground, and a 3px
   rounded bar that scales out from its centre for the current tab, previewed
   at a third of its weight on hover. Two nav surfaces behaving identically is
   most of what makes an interface feel like one piece of software. */
.subnav {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-1); list-style: none;
  border-bottom: 1px solid var(--fx-color-border); margin-bottom: var(--fx-space-5);
}
.subnav a {
  position: relative;
  display: inline-flex; align-items: center; min-height: var(--fx-tap);
  padding: 0 var(--fx-space-4); text-decoration: none; color: var(--fx-color-muted);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: var(--fx-w-semi);
  letter-spacing: var(--fx-track-ui); text-transform: none;
  border-radius: var(--fx-radius-md) var(--fx-radius-md) var(--fx-radius-xs) var(--fx-radius-xs);
  margin-bottom: -1px;
  transition: color var(--fx-dur-1) var(--fx-ease), background-color var(--fx-dur-1) var(--fx-ease);
}
.subnav a::after {
  content: "";
  position: absolute; left: var(--fx-space-3); right: var(--fx-space-3); bottom: 0;
  height: var(--fx-spine); border-radius: var(--fx-radius-pill);
  background: var(--fx-color-primary);
  transform: scaleX(0); transform-origin: 50% 50%; opacity: 0;
  transition: transform var(--fx-dur-3) var(--fx-ease-out),
              opacity var(--fx-dur-2) var(--fx-ease);
}
.subnav a:hover { color: var(--fx-color-ink); background: var(--fx-color-surface); }
.subnav a:hover::after { transform: scaleX(1); opacity: 0.32; }
.subnav a[aria-current="true"] {
  color: var(--fx-color-accent-ink); background: var(--fx-color-primary-tint);
  font-weight: var(--fx-w-bold);
}
.subnav a[aria-current="true"]::after { transform: scaleX(1); opacity: 1; }

/* --------------------------------------------------------------- kanban -- */
.kanban {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(280px, 1fr);
  gap: var(--fx-space-4); overflow-x: auto; padding-bottom: var(--fx-space-4);
  align-items: start; -webkit-overflow-scrolling: touch;
}
.kcol {
  background: var(--fx-color-surface);
  border: 1px solid var(--fx-color-border);
  border-top: 0 solid transparent;
  border-radius: var(--fx-radius-lg); min-width: 0;
}
.kcol--scoping   { border-top-color: var(--fx-color-cyan); }
.kcol--active    { border-top-color: var(--fx-color-primary); }
.kcol--blocked   { border-top-color: var(--fx-color-bad-solid); }
.kcol--delivered { border-top-color: var(--fx-color-ok); }
.kcol--closed    { border-top-color: var(--fx-color-neutral); }
.kcol__head {
  display: flex; align-items: center; gap: var(--fx-space-2);
  padding: var(--fx-space-3) var(--fx-space-4); border-bottom: 1px solid var(--fx-color-border);
}
.kcol__head h2 {
  font-size: var(--fx-text-xs); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--fx-color-muted);
}
.kcol__n { margin-left: auto; font-family: var(--fx-font-mono); font-size: var(--fx-text-xs); color: var(--fx-color-muted); }
.kcol__body { padding: var(--fx-space-3); display: grid; gap: var(--fx-space-3); }
.kcard {
  background: var(--fx-color-bg); border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-md); padding: var(--fx-space-3);
}
.kcard--good   { border-left-color: var(--fx-color-ok); }
.kcard--watch  { border-left-color: var(--fx-color-warn-solid); }
.kcard--at_risk{ border-left-color: var(--fx-color-bad-solid); }
.kcard__t { font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-md); }
.kcard__t a { color: inherit; text-decoration: none; }
.kcard__t a:hover { color: var(--fx-color-primary); text-decoration: underline; }
.kcard__meta { margin-top: var(--fx-space-2); font-size: var(--fx-text-xs); color: var(--fx-color-muted);
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2) var(--fx-space-3); }
.kcard__move { margin-top: var(--fx-space-3); display: flex; gap: var(--fx-space-2); align-items: center; }
.kcard__move .select { flex: 1 1 auto; }

/* -------------------------------------------------------------- timeline -- */
.tl { list-style: none; }
.tl li {
  position: relative; padding: 0 0 var(--fx-space-4) var(--fx-space-5);
  border-left: 1px solid var(--fx-color-border);
}
.tl li:last-child { border-left-color: transparent; padding-bottom: 0; }
.tl li::before {
  content: ""; position: absolute; left: -4px; top: 6px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--fx-color-cyan);
}
.tl__h { display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: baseline; }
.tl__h b { font-family: var(--fx-font-ui); font-size: var(--fx-text-md); }
.tl__when { font-family: var(--fx-font-mono); font-size: var(--fx-text-xs); color: var(--fx-color-muted); }
.tl__body { margin-top: var(--fx-space-1); color: var(--fx-color-muted); white-space: pre-wrap; }

/* -------------------------------------------------------------------- kv -- */
.kv { display: grid; grid-template-columns: minmax(7rem, auto) 1fr; gap: var(--fx-space-2) var(--fx-space-4); }
.kv dt {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--fx-color-muted);
  padding-top: 2px;
}
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* ------------------------------------------------------------------ list -- */
.list { list-style: none; }
.list > li {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2) var(--fx-space-3); align-items: center;
  padding: var(--fx-space-3) 0; border-bottom: 1px solid var(--fx-color-border);
}
.list > li:last-child { border-bottom: 0; }
.list__t { font-family: var(--fx-font-ui); font-weight: 600; min-width: 0; }
.list__t a { color: inherit; text-decoration: none; }
.list__t a:hover { color: var(--fx-color-primary); text-decoration: underline; }
.list__meta { color: var(--fx-color-muted); font-size: var(--fx-text-xs); }

/* ------------------------------------------------------------------ drop -- */
.drop {
  border: 2px dashed var(--fx-color-border-strong); border-radius: var(--fx-radius-md);
  background: var(--fx-color-surface);
  padding: var(--fx-space-7) var(--fx-space-5); text-align: center;
  transition: border-color var(--fx-dur) var(--fx-ease), background-color var(--fx-dur) var(--fx-ease);
}
.drop[data-over="true"] { border-color: var(--fx-color-primary); background: var(--fx-color-primary-tint); }
.drop h2 { font-size: var(--fx-text-lg); }
.drop p { color: var(--fx-color-muted); max-width: 52ch; margin: var(--fx-space-2) auto 0; }
.drop input[type="file"] {
  display: block; margin: var(--fx-space-4) auto 0; max-width: 420px;
  padding: var(--fx-space-3); min-height: var(--fx-tap);
  border: 1px solid var(--fx-color-border-strong); border-radius: var(--fx-radius-md);
  background: var(--fx-color-bg); width: 100%;
}
.dropfile {
  margin-top: var(--fx-space-4); font-family: var(--fx-font-mono); font-size: var(--fx-text-sm);
  color: var(--fx-color-ink);
}

/* -------------------------------------------------------- import mapping -- */
.maptable { width: 100%; min-width: 900px; font-size: var(--fx-text-sm); }
.maprow th {
  padding: var(--fx-space-2) var(--fx-space-3);
  background: var(--fx-color-surface-2);
  border-bottom: 1px solid var(--fx-color-border-strong);
  text-align: left; vertical-align: top; min-width: 190px;
}
.mapfield {
  display: block; font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--fx-color-ink);
  margin-bottom: var(--fx-space-2);
}
.mapconf {
  display: inline-flex; align-items: center; gap: var(--fx-space-2);
  margin-top: var(--fx-space-2); font-size: var(--fx-text-xs); color: var(--fx-color-muted);
  font-family: var(--fx-font-mono);
}
.meter { width: 44px; height: 4px; border-radius: 2px; background: var(--fx-color-border); overflow: hidden; flex: none; }
.meter i { display: block; height: 100%; background: var(--fx-color-ok); }
.meter--warn i { background: var(--fx-color-warn-solid); }
.meter--bad i  { background: var(--fx-color-bad-solid); }

.reason { color: var(--fx-color-muted); max-width: 42ch; }
.row-bad .reason { color: var(--fx-color-bad); }
.row-warn .reason { color: var(--fx-color-warn); }

.commitbar {
  position: sticky; bottom: 0; z-index: 30;
  background: var(--fx-color-bg);
  border: 1px solid var(--fx-color-border);
  border-top: var(--fx-spine) solid var(--fx-color-primary);
  border-radius: var(--fx-radius-md);
  padding: var(--fx-space-4);
  margin-top: var(--fx-space-4);
  display: flex; flex-wrap: wrap; gap: var(--fx-space-4); align-items: center;
}
.commitbar__note { color: var(--fx-color-muted); font-size: var(--fx-text-sm); flex: 1 1 260px; min-width: 0; }

/* ------------------------------------------------------------- assistant -- */
.chat { display: flex; flex-direction: column; gap: var(--fx-space-4); }
.msg {
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-md); padding: var(--fx-space-3) var(--fx-space-4);
  background: var(--fx-color-bg); min-width: 0;
}
.msg--user { background: var(--fx-color-surface); border-left-color: var(--fx-color-neutral); }
.msg--assistant { border-left-color: var(--fx-color-cyan); }
.msg--tool { border-left-color: var(--fx-color-warn-solid); background: var(--fx-color-surface); font-family: var(--fx-font-mono); font-size: var(--fx-text-sm); }
.msg__head {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center;
  margin-bottom: var(--fx-space-2);
}
.msg__who {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--fx-color-muted);
}
.msg__body { overflow-wrap: anywhere; }
.msg__body > * + * { margin-top: var(--fx-space-3); }
.msg__body ul, .msg__body ol { padding-left: var(--fx-space-5); }
.msg__body li { list-style: disc; }
.msg__body ol li { list-style: decimal; }
.msg__body pre {
  background: var(--fx-color-surface); padding: var(--fx-space-3);
  border-radius: var(--fx-radius-md); overflow-x: auto; font-family: var(--fx-font-mono);
  font-size: var(--fx-text-sm);
}
.msg__body code { font-family: var(--fx-font-mono); font-size: 0.92em; }
.msg__plain { white-space: pre-wrap; }

.cites { margin-top: var(--fx-space-3); padding-top: var(--fx-space-3); border-top: 1px solid var(--fx-color-border); }
.cites__h {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--fx-color-muted);
  margin-bottom: var(--fx-space-2);
}
.cites ol { list-style: none; display: grid; gap: var(--fx-space-2); counter-reset: cite; }
.cites li { display: flex; gap: var(--fx-space-3); font-size: var(--fx-text-sm); counter-increment: cite; }
.cites li::before {
  content: "[" counter(cite) "]"; font-family: var(--fx-font-mono); color: var(--fx-color-cyan); flex: none;
}
.cites .unresolved { color: var(--fx-color-warn); }

.composer { display: grid; gap: var(--fx-space-3); }
.composer__row { display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: center; }

/* --------------------------------------------------------- proposal doc -- */
.doc {
  background: var(--fx-color-bg); color: var(--fx-color-ink);
  border: 1px solid var(--fx-color-border); border-radius: var(--fx-radius-md);
  padding: clamp(1.25rem, 0.5rem + 3vw, 3.5rem);
  max-width: 900px; margin-inline: auto;
}
.doc__head { display: flex; flex-wrap: wrap; gap: var(--fx-space-5); justify-content: space-between;
  padding-bottom: var(--fx-space-5); border-bottom: var(--fx-spine) solid var(--fx-color-primary); }
.doc__brand { font-family: var(--fx-font-display); font-size: var(--fx-text-2xl); letter-spacing: 0.03em; text-transform: uppercase; line-height: 1; }
.doc__brand span { display: block; font-family: var(--fx-font-ui); font-size: var(--fx-text-xs);
  font-weight: 700; letter-spacing: 0.16em; color: var(--fx-color-muted); margin-top: var(--fx-space-2); }
.doc__num { text-align: right; font-family: var(--fx-font-mono); font-size: var(--fx-text-sm); }
.doc h1 { font-size: var(--fx-text-2xl); margin-top: var(--fx-space-5); }
.doc h2 { font-size: var(--fx-text-md); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--fx-color-muted); margin-top: var(--fx-space-6); margin-bottom: var(--fx-space-3);
  padding-bottom: var(--fx-space-2); border-bottom: 1px solid var(--fx-color-border); }
.doc p { max-width: 70ch; }
.doc__prose { white-space: pre-wrap; }
.doc table { width: 100%; margin-top: var(--fx-space-2); font-size: var(--fx-text-sm); }
.doc th, .doc td { padding: var(--fx-space-2) var(--fx-space-3); border-bottom: 1px solid var(--fx-color-border); text-align: left; }
.doc th { font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--fx-color-muted); }
.doc .num { text-align: right; }
.doc__totals { margin-left: auto; margin-top: var(--fx-space-4); width: min(320px, 100%); }
.doc__totals div { display: flex; justify-content: space-between; gap: var(--fx-space-4); padding: var(--fx-space-2) 0; }
.doc__totals .grand {
  border-top: var(--fx-spine) solid var(--fx-color-primary); margin-top: var(--fx-space-2);
  padding-top: var(--fx-space-3); font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-lg);
}
.doc__sign { display: grid; gap: var(--fx-space-6); margin-top: var(--fx-space-7); }
.doc__sigline { border-top: 1px solid var(--fx-color-ink); padding-top: var(--fx-space-2);
  font-size: var(--fx-text-xs); color: var(--fx-color-muted); max-width: 320px; }
@media (min-width: 700px) { .doc__sign { grid-template-columns: 1fr 1fr; } }

/* --------------------------------------------------------- proposal edit -- */
.items { width: 100%; min-width: 780px; font-size: var(--fx-text-sm); }
.items th, .items td { padding: var(--fx-space-2); border-bottom: 1px solid var(--fx-color-border); vertical-align: top; text-align: left; }
.items thead th { font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--fx-color-muted); background: var(--fx-color-surface); }
.items .input, .items .select { min-height: 40px; }
.items .amount { font-family: var(--fx-font-mono); font-variant-numeric: tabular-nums; text-align: right;
  white-space: nowrap; padding-top: var(--fx-space-3); }
.totals { margin-left: auto; width: min(340px, 100%); margin-top: var(--fx-space-4); }
.totals div { display: flex; justify-content: space-between; gap: var(--fx-space-4); padding: var(--fx-space-2) 0;
  border-bottom: 1px solid var(--fx-color-border); }
.totals .grand { border-bottom: 0; border-top: var(--fx-spine) solid var(--fx-color-primary);
  font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-lg); margin-top: var(--fx-space-1); }
.totals .num { font-family: var(--fx-font-mono); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ auth -- */
.auth { width: min(420px, 100%); }
.auth__brand { text-align: center; margin-bottom: var(--fx-space-6); }
.auth__brand .brand__name { font-size: 2rem; color: var(--fx-color-ink); display: block; }
.auth__brand .brand__sub { border: 0; padding: 0; margin-top: var(--fx-space-2); display: block; color: var(--fx-color-muted); }
.auth__panel { border-top: 0 solid transparent; border-left-width: 0;
  border-left-color: transparent; }
.auth__foot { text-align: center; margin-top: var(--fx-space-5); font-size: var(--fx-text-sm); color: var(--fx-color-muted); }

/* ----------------------------------------------------------------- nodes -- */
.nodegrid { display: grid; gap: var(--fx-space-4); grid-template-columns: 1fr; }
@media (min-width: 760px) { .nodegrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1200px) { .nodegrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.domains { display: flex; flex-wrap: wrap; gap: var(--fx-space-2); list-style: none; }
.domains li {
  font-family: var(--fx-font-mono); font-size: var(--fx-text-xs);
  padding: 2px var(--fx-space-2); border-radius: var(--fx-radius-sm);
  background: var(--fx-color-surface-2); color: var(--fx-color-ink);
}

/* ----------------------------------------------------------------- print -- */
@media print {
  :root {
    --fx-color-bg: #ffffff; --fx-color-ink: #000000; --fx-color-muted: #444444;
    --fx-color-border: #bbbbbb; --fx-color-border-strong: #888888;
    --fx-color-primary: #25408f; --fx-color-surface: #ffffff;
  }
  .rail, .navdrawer, .footer, .flash, .noprint, .skip-link, .subnav, .pagehead__actions { display: none !important; }
  body { background: #fff; color: #000; font-size: 11pt; }
  .main { max-width: none; padding: 0; }
  .doc { border: 0; padding: 0; max-width: none; }
  .doc h2 { break-after: avoid; }
  .doc table { break-inside: auto; }
  .doc tr { break-inside: avoid; }
  .doc__totals, .doc__sign { break-inside: avoid; }
  a { color: #000; text-decoration: none; }
  @page { margin: 18mm 16mm; }
}

/* ---- demonstration banner -------------------------------------------------
   Rendered above the nav on every page when the workspace is flagged as a demo.
   Deliberately loud: the failure this prevents is somebody screenshotting a
   fictional cap rate and repeating it as a real one.

   Its own token pair, declared here and NEVER overridden in the dark block.
   The reason is contrast, not laziness. The background is brand orange in both
   themes, and dark-mode --fx-color-ink is #e8edf5, which lands at 2.43:1 on it
   and fails AA. The fixed dark label measures 6.57:1 and passes in both. Do not
   "fix" this by pointing it at --fx-color-ink.
   Carries the word "Demonstration" so it never depends on colour alone. */
:root {
  --fx-color-demobar-bg:  #f68b1f;
  --fx-color-demobar-ink: #14213d;
}
.demobar {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem 1rem;
  align-items: baseline;
  padding: .6rem 1.25rem;
  background: var(--fx-color-demobar-bg);
  color: var(--fx-color-demobar-ink);
  border-bottom: 1px solid var(--fx-color-border-strong);
  font-size: .875rem;
  line-height: 1.45;
}
.demobar strong { font-weight: 700; letter-spacing: .01em; }
@media print { .demobar { display: none; } }

/* ===========================================================================
   WORKFLOW, PLACEMENTS, SOCIAL PLANNER, INBOX
   Appended for the request-to-listing screens (005_workflow.sql). Same rules as
   everything above: no raw hex, no shadow, no card, a 3px spine carrying state
   and a text chip carrying the same state in words right beside it.
   =========================================================================== */

/* --------------------------------------------------- shared: late, progress -- */
/* Past a date and still open. Never the only signal: the word "was due" or
   "Overdue" sits in the same line. */
.is-late { color: var(--fx-color-bad); font-weight: 600; }
.input.is-late { color: var(--fx-color-ink); border-color: var(--fx-color-bad); border-width: 2px; }

/* Done-of-total. The NUMBER is the readout; the bar is decoration behind it and
   is aria-hidden in the markup. */
.prog {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--fx-space-2) var(--fx-space-3);
  margin-top: var(--fx-space-3);
  font-size: var(--fx-text-xs);
}
.prog__n {
  font-family: var(--fx-font-mono); font-variant-numeric: tabular-nums;
  font-weight: 600; color: var(--fx-color-ink); white-space: nowrap;
}
.prog__l { color: var(--fx-color-muted); min-width: 0; }
.prog__bar {
  flex: 1 1 80px; min-width: 60px; height: 4px; border-radius: 2px;
  background: var(--fx-color-border); overflow: hidden;
}
.prog__bar i { display: block; height: 100%; background: var(--fx-color-primary); }
.prog--lg { font-size: var(--fx-text-md); margin-top: 0; }
.prog--lg .prog__n { font-size: var(--fx-text-lg); }
.prog--lg .prog__bar { height: 6px; flex-basis: 160px; }

/* ---------------------------------------------------------- workflow board -- */
.kcol--wf-open        { border-top-color: var(--fx-color-warn-solid); }
.kcol--wf-in_progress { border-top-color: var(--fx-color-cyan); }
.kcol--wf-on_hold     { border-top-color: var(--fx-color-bad-solid); }
.kcol--wf-listed      { border-top-color: var(--fx-color-ok); }
.kcol--wf-closed      { border-top-color: var(--fx-color-neutral); }
.kcol--wf-cancelled   { border-top-color: var(--fx-color-border-strong); }

/* Generic tone spines for a tile, alongside the health ones already defined. */
.kcard--ok      { border-left-color: var(--fx-color-ok); }
.kcard--warn    { border-left-color: var(--fx-color-warn-solid); }
.kcard--bad     { border-left-color: var(--fx-color-bad-solid); }
.kcard--info    { border-left-color: var(--fx-color-cyan); }
.kcard--neutral { border-left-color: var(--fx-color-neutral); }

.kcard__ref {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center;
  margin-bottom: var(--fx-space-2); font-size: var(--fx-text-xs);
}
.kcard__ref .mono { color: var(--fx-color-muted); }

/* ------------------------------------------------------------- checklist -- */
.ck { list-style: none; display: grid; gap: var(--fx-space-4); }
.ck__i {
  background: var(--fx-color-bg);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-md);
  padding: var(--fx-space-4);
  min-width: 0;
}
.ck__i--ok      { border-left-color: var(--fx-color-ok); }
.ck__i--warn    { border-left-color: var(--fx-color-warn-solid); }
.ck__i--bad     { border-left-color: var(--fx-color-bad-solid); }
.ck__i--info    { border-left-color: var(--fx-color-cyan); }
.ck__i--neutral { border-left-color: var(--fx-color-neutral); }
/* A gated step is set back by SURFACE, not by opacity, and always carries the
   .ck__gate sentence naming the step it waits on. Grey on its own would only
   say "something is wrong here". */
.ck__i--gated { background: var(--fx-color-surface); }

.ck__head { display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: flex-start; }
.ck__n {
  flex: none; width: 28px; height: 28px; display: grid; place-items: center;
  border: 1px solid var(--fx-color-border-strong); border-radius: 50%;
  font-family: var(--fx-font-mono); font-size: var(--fx-text-xs);
  color: var(--fx-color-muted);
}
.ck__title { flex: 1 1 240px; min-width: 0; }
.ck__title h3 {
  font-size: var(--fx-text-md); display: flex; flex-wrap: wrap;
  gap: var(--fx-space-2); align-items: center;
}
.ck__title h3:focus-visible { outline: 3px solid var(--fx-color-focus); outline-offset: 3px; }
.ck__title p { font-size: var(--fx-text-sm); margin-top: var(--fx-space-1); max-width: 62ch; }
.ck__badges { display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center; }

.ck__gate {
  margin-top: var(--fx-space-3);
  padding: var(--fx-space-2) var(--fx-space-3);
  background: var(--fx-color-warn-tint); color: var(--fx-color-warn);
  border-left: 0 solid var(--fx-color-warn-solid);
  border-radius: var(--fx-radius-sm);
  font-size: var(--fx-text-sm);
}
.ck__gate a { color: var(--fx-color-warn); }
.ck__done {
  margin-top: var(--fx-space-3);
  font-size: var(--fx-text-sm); color: var(--fx-color-ok);
}
.ck__hint { grid-column: 1 / -1; font-size: var(--fx-text-sm); color: var(--fx-color-muted); }

.ck__ctl {
  display: grid; gap: var(--fx-space-3);
  grid-template-columns: 1fr;
  margin-top: var(--fx-space-4);
  padding-top: var(--fx-space-4);
  border-top: 1px solid var(--fx-color-border);
  align-items: end;
}
.ck__ctl .field { margin-bottom: 0; min-width: 0; }
.ck__save { justify-self: start; }

/* Yes / No / not answered. Three real radios, because "not answered" has to be
   selectable and has to stay distinguishable from "no". */
.trio { border: 0; padding: 0; margin: 0; min-width: 0; }
.trio legend { padding: 0; }
.trio__o {
  display: inline-flex; align-items: center; gap: var(--fx-space-2);
  min-height: var(--fx-tap); padding: 0 var(--fx-space-3);
  margin: 0 var(--fx-space-2) var(--fx-space-2) 0;
  border: 1px solid var(--fx-color-border-strong); border-radius: var(--fx-radius-md);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: 600;
  cursor: pointer; background: var(--fx-color-bg);
}
.trio__o:hover { border-color: var(--fx-color-primary); }
.trio__o input { width: 18px; height: 18px; flex: none; accent-color: var(--fx-color-primary); }
.trio__o:has(input:checked) {
  border-color: var(--fx-color-primary); background: var(--fx-color-primary-tint);
  border-width: 2px;
}
.trio__o:has(input:disabled) { color: var(--fx-color-muted); cursor: not-allowed; }

/* links and notes under a step */
.ck__sub { margin-top: var(--fx-space-4); padding-top: var(--fx-space-3); border-top: 1px solid var(--fx-color-border); }
.ck__none { font-size: var(--fx-text-sm); max-width: 62ch; margin-top: var(--fx-space-1); }
.ck__links, .ck__notes { list-style: none; margin-top: var(--fx-space-2); }
.ck__links > li {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2) var(--fx-space-3);
  align-items: center; padding: var(--fx-space-2) 0;
  border-bottom: 1px solid var(--fx-color-border); font-size: var(--fx-text-sm);
}
.ck__links > li:last-child { border-bottom: 0; }
.ck__links a { overflow-wrap: anywhere; min-width: 0; }
.ck__notes > li {
  padding: var(--fx-space-2) 0 var(--fx-space-2) var(--fx-space-3);
  border-left: 1px solid var(--fx-color-border);
  margin-bottom: var(--fx-space-2);
}
.ck__nb { white-space: pre-wrap; overflow-wrap: anywhere; }
.ck__by {
  font-size: var(--fx-text-xs); color: var(--fx-color-muted);
  font-family: var(--fx-font-ui); margin-top: var(--fx-space-1);
}

/* A native <details> rather than a JS disclosure, so adding a link works with
   scripts blocked and needs no wiring at all. */
.ck__add { margin-top: var(--fx-space-3); }
.ck__add > summary {
  display: inline-flex; align-items: center; min-height: var(--fx-tap);
  padding: 0 var(--fx-space-3); cursor: pointer;
  border: 1px solid var(--fx-color-border-strong); border-radius: var(--fx-radius-md);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.07em; text-transform: uppercase; color: var(--fx-color-ink);
  list-style: none;
}
.ck__add > summary::-webkit-details-marker { display: none; }
.ck__add > summary::before { content: "+"; margin-right: var(--fx-space-2); color: var(--fx-color-cyan); }
.ck__add[open] > summary::before { content: "\2212"; }
.ck__add > summary:hover { border-color: var(--fx-color-primary); color: var(--fx-color-primary); }
.ck__add > form {
  margin-top: var(--fx-space-3); padding: var(--fx-space-3);
  background: var(--fx-color-surface);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-md);
}

@media (min-width: 700px) {
  .ck__ctl { grid-template-columns: minmax(0, 12rem) minmax(0, 12rem) minmax(0, 9rem) auto; }
  .ck__ctl .trio { grid-column: 1 / -1; }
}

/* ------------------------------------------------------ listing placements -- */
.miss { list-style: none; display: grid; gap: var(--fx-space-2); }
.miss__i {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: end;
  padding: var(--fx-space-3);
  background: var(--fx-color-bg);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid var(--fx-color-warn-solid);
  border-radius: var(--fx-radius-md);
}
.miss__n {
  flex: 0 0 auto; align-self: center; min-width: 8rem;
  font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-md);
}
.miss__f { display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: end; flex: 1 1 340px; }

/* ------------------------------------------------------- social: the month -- */
.calbar {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: center;
  padding-bottom: var(--fx-space-3); margin-bottom: var(--fx-space-3);
  border-bottom: 1px solid var(--fx-color-border);
}
.calbar__m {
  font-family: var(--fx-font-display); font-size: var(--fx-text-2xl);
  letter-spacing: 0.02em; text-transform: uppercase; line-height: 1; margin-inline: auto;
}

.cal__dow { display: none; list-style: none; }
.cal {
  list-style: none; display: grid; gap: 1px;
  background: var(--fx-color-border);
  border: 1px solid var(--fx-color-border);
  border-radius: var(--fx-radius-md); overflow: hidden;
}
.cal__d {
  background: var(--fx-color-bg);
  border-left: 0 solid transparent;
  padding: var(--fx-space-2) var(--fx-space-3);
  min-width: 0; min-height: 3rem;
}
.cal__d--has { border-left-color: var(--fx-color-cyan); }
.cal__d--today { background: var(--fx-color-primary-tint); border-left-color: var(--fx-color-primary); }
.cal__date {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: baseline;
  font-size: var(--fx-text-xs);
}
.cal__date time { display: inline-flex; gap: var(--fx-space-2); align-items: baseline; }
.cal__wd {
  font-family: var(--fx-font-ui); font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--fx-color-muted);
}
.cal__dn { font-family: var(--fx-font-mono); font-size: var(--fx-text-md); font-weight: 600; }
.cal__n { color: var(--fx-color-muted); margin-left: auto; }

.cal__posts { list-style: none; display: grid; gap: var(--fx-space-2); margin-top: var(--fx-space-2); }
.cal__p {
  border-left: 0 solid transparent;
  background: var(--fx-color-surface);
  border-radius: var(--fx-radius-sm);
  padding: var(--fx-space-1) var(--fx-space-2);
  min-width: 0;
}
.cal__p--ok      { border-left-color: var(--fx-color-ok); }
.cal__p--warn    { border-left-color: var(--fx-color-warn-solid); }
.cal__p--bad     { border-left-color: var(--fx-color-bad-solid); }
.cal__p--info    { border-left-color: var(--fx-color-cyan); }
.cal__p--neutral { border-left-color: var(--fx-color-neutral); }
.cal__p > a {
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  min-height: var(--fx-tap); text-decoration: none; color: inherit; min-width: 0;
}
.cal__p > a:hover .cal__pt { color: var(--fx-color-primary); text-decoration: underline; }
.cal__pc {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--fx-color-muted);
}
.cal__pt {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cal__pm {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-1) var(--fx-space-2);
  align-items: center; font-size: var(--fx-text-xs); color: var(--fx-color-muted);
  padding-bottom: var(--fx-space-1);
}

@media (min-width: 760px) {
  .cal, .cal__dow { grid-template-columns: repeat(7, minmax(0, 1fr)); }
  .cal__dow {
    display: grid; gap: 1px; margin-bottom: 1px;
    font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
    letter-spacing: 0.1em; text-transform: uppercase; color: var(--fx-color-muted);
  }
  .cal__dow li { padding: var(--fx-space-2) var(--fx-space-3); }
  .cal__d { min-height: 7rem; }
  .cal__d--out { background: var(--fx-color-surface); color: var(--fx-color-muted); }
  /* The weekday sits in the header row on a wide screen, so repeating it in
     every one of forty-two cells is noise. */
  .cal__wd { display: none; }
}
@media (max-width: 759px) {
  /* One column, oldest first. The leading and trailing days belong to another
     month and are only there to square off the grid, so they go. */
  .cal__d--out { display: none; }
}

/* ------------------------------------------------------- social: one post -- */
.creative {
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid var(--fx-color-cyan);
  border-radius: var(--fx-radius-md); overflow: hidden;
  background: var(--fx-color-surface);
}
.creative img { width: 100%; height: auto; }
.creative figcaption {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2) var(--fx-space-3); align-items: center;
  padding: var(--fx-space-3); border-top: 1px solid var(--fx-color-border);
  font-size: var(--fx-text-sm);
}

.metrics__when {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center;
  font-size: var(--fx-text-sm); color: var(--fx-color-muted);
  padding-bottom: var(--fx-space-3); margin-bottom: var(--fx-space-3);
  border-bottom: 1px solid var(--fx-color-border);
}
.metrics { display: grid; gap: var(--fx-space-4); grid-template-columns: repeat(2, minmax(0, 1fr)); }
.metric { border-left: 0 solid var(--fx-color-cyan); padding-left: var(--fx-space-3); min-width: 0; }
.metric__v {
  display: block; font-family: var(--fx-font-display); font-size: var(--fx-text-xl);
  line-height: 1.1; font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
.metric__l {
  display: block; margin-top: var(--fx-space-1);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--fx-color-muted);
}
/* Never a zero for a figure nobody entered. */
.metric--none { border-left-color: var(--fx-color-border); }
.metric--none .metric__v {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: 600;
  color: var(--fx-color-muted); text-transform: none; letter-spacing: 0;
}
@media (min-width: 760px) { .metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* ------------------------------------------------------------------ inbox -- */
.inbox { list-style: none; display: grid; gap: var(--fx-space-2); }
.inbox__i {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3);
  align-items: flex-start; justify-content: space-between;
  padding: var(--fx-space-3) var(--fx-space-4);
  background: var(--fx-color-bg);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-md);
}
.inbox__i--ok      { border-left-color: var(--fx-color-ok); }
.inbox__i--warn    { border-left-color: var(--fx-color-warn-solid); }
.inbox__i--bad     { border-left-color: var(--fx-color-bad-solid); }
.inbox__i--info    { border-left-color: var(--fx-color-cyan); }
.inbox__i--neutral { border-left-color: var(--fx-color-neutral); }
/* A read row sits back on the surface. The chip still says which is which. */
.inbox__i:not(.inbox__i--unread) { background: var(--fx-color-surface); }
.inbox__b { flex: 1 1 280px; min-width: 0; }
.inbox__h {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2) var(--fx-space-3);
  align-items: center; font-size: var(--fx-text-xs);
}
.inbox__when { font-family: var(--fx-font-mono); font-size: var(--fx-text-xs); color: var(--fx-color-muted); }
.inbox__s { font-family: var(--fx-font-ui); font-weight: 700; margin-top: var(--fx-space-2); overflow-wrap: anywhere; }
.inbox__d {
  color: var(--fx-color-muted); font-size: var(--fx-text-sm); margin-top: var(--fx-space-1);
  max-width: 70ch; white-space: pre-wrap; overflow-wrap: anywhere;
}
.inbox__a { display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center; }

/* -------------------------------------------------------------- form page -- */
.formwrap { max-width: 860px; }

/* Completes the .chipbar state set above. The workflow board tones a status
   chip 'info', which had no left spine until now. */
.chipbar .is-info { border-left: var(--fx-spine) solid var(--fx-color-cyan); }

/* A wide table that carries a form control per row. A control sitting past the
   viewport inside a horizontal scroller makes Chrome grow the ROOT scroll
   width, so the whole page slides sideways on a phone while the rail goes with
   it. The scroller already clips; saying so explicitly keeps the overflow off
   the root. Measured at 360px: the rail holds at 0 instead of -300, and the
   table still scrolls inside its own box. Scoped to the placements table so no
   other screen's scroller changes behaviour. */
.table-scroll--contained { contain: paint; }

/* Workflow tiles only. The title is the way into a request, so it gets a real
   44px target rather than the 20px line box a heading link gets by default.
   The projects board keeps its own sizing. */
.kcard--wf .kcard__t a {
  display: flex; align-items: center; min-height: var(--fx-tap);
}

/* ---- single-column grid tracks: shrink below content ----------------------
   `1fr` is shorthand for `minmax(auto, 1fr)`, and that `auto` minimum is the
   item's min-content width. A grid cell holding a wide table therefore forces
   its own track wider than the container instead of letting the table scroll,
   and because body carries `overflow-x: clip` the excess is silently cut off
   rather than showing a scrollbar. On a 360px dashboard the panels were laying
   out at 726px with everything past 360 invisible.

   The multi-column rules already used `minmax(0, 1fr)`. These are the
   single-column base cases that were missed, so the failure only appeared
   below the first breakpoint, which is exactly where nobody was looking.

   Appended rather than edited in place so the original block stays readable as
   what it was; these are the same selectors at equal specificity, later in the
   file, so they win. */
.grid--2,
.grid--3,
.grid--main { grid-template-columns: minmax(0, 1fr); }
.nodegrid   { grid-template-columns: minmax(0, 1fr); }
.ck__ctl    { grid-template-columns: minmax(0, 1fr); }

/* Same fault, one level down: a grid or flex ITEM defaults to min-width:auto,
   so a long unbroken string or a wide table inside a panel pushes its parent
   out. The panel is the common container for every wide thing in this app. */
.grid > *,
.stack > .panel { min-width: 0; }

/* A table that is wider than the page is expected: it scrolls inside its own
   box. That only works if the box is allowed to be narrower than the table. */
.table-scroll { max-width: 100%; }

/* ---- the proposal document's fee table ------------------------------------
   On a phone this table lays out wider than the viewport and the Amount column
   was being clipped away, which on a client-facing document is the one column
   that must never go missing. It scrolls in its own box instead.

   `contain: paint` stops the overflowing table from growing the root scroll
   width, which is the same fault the placements table had. For print the
   scroller is dissolved: paper is wide enough, and a clipped table on a printed
   proposal would be worse than the original bug. */
.table-scroll--doc {
  overflow-x: auto;
  contain: paint;
  -webkit-overflow-scrolling: touch;
}
.table-scroll--doc > table { min-width: 26rem; }
.table-scroll--doc:focus-visible { outline: 2px solid var(--fx-color-focus); outline-offset: 2px; }
@media print {
  .table-scroll--doc { overflow: visible; contain: none; }
  .table-scroll--doc > table { min-width: 0; }
}

/* ---- header on a 320px phone ----------------------------------------------
   `.who` is `margin-left: auto; flex: none`, so on the narrowest common handset
   the account button was pushed 3px past the right edge and its focus ring was
   clipped. The button itself is not too wide; the row simply has nothing left
   to give. Allowing the rail to shrink and trimming the button's inline padding
   below 360px recovers the space without touching the 44px tap target. */
@media (max-width: 359px) {
  .who { min-width: 0; }
  .who__btn { padding-inline: var(--fx-space-1, 4px); max-width: 100%; }
  .who__btn .avatar { width: 28px; height: 28px; }
}

/* ---- every table scroller contains its own overflow ------------------------
   `.table` carries `min-width: 720px` by design: a data table with eight
   columns is unreadable squeezed into a phone, so it keeps its width and the
   box around it scrolls. What was missing is that an overflowing child still
   contributes to the ROOT scroll width unless the scroller says otherwise, so
   on project detail a 720px table inside a 284px box dragged the whole page
   568px wide and the header rail slid off with it.

   `contain: paint` tells the browser nothing painted outside this box affects
   layout beyond it. The scroller still scrolls; the page no longer does.

   An earlier pass applied this to `.table-scroll--contained` only, on the
   reasoning that a narrower fix is safer. It is the wrong scope: every
   `.table-scroll` in this stylesheet exists precisely because its table is
   wider than its box, so every one of them wants this. The variant is kept so
   existing markup does not break. */
.table-scroll { contain: paint; }

/* overflow-y on a `contain: paint` box clips rather than escaping, which is
   what the tall variant already wants. Stated so nobody "fixes" it later. */
.table-scroll--tall { contain: paint; }

/* ---- restore the desktop columns ------------------------------------------
   The `minmax(0, 1fr)` fix above is a later rule at equal specificity, so it
   was also overriding the wide-screen definitions further up the file and the
   dashboard stayed one column at 1440px. Cascade order, not a logic error:
   appending a base rule after a media query silently defeats it.

   Restated here, after the fix, with the same `minmax(0, ...)` minimum so the
   blowout stays cured at every width rather than only below 1080px. */
@media (min-width: 760px) {
  .grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .nodegrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1080px) {
  .grid--main { grid-template-columns: minmax(0, 1fr) 340px; align-items: start; }
  .grid--main--wide { grid-template-columns: minmax(0, 1fr) 400px; }
}
@media (min-width: 1200px) {
  .nodegrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ---- the header row with a full nav ---------------------------------------
   The nav grew from eight items to twelve when Properties, Requests, Social and
   Inbox were added. At 1440px the row then measures 1385px of children against
   a 1363px content box, and because `.nav` is `flex: 0 1 auto` with the default
   `min-width: auto` it refuses to shrink below its content. The overflow lands
   on `.who`, which is the last item, so the account button and its focus ring
   were sitting past the header's right padding.

   A flex item cannot shrink below min-content unless told to. `min-width: 0`
   lets the nav give way, and scrolling inside it keeps every destination
   reachable rather than hiding the tail. `.who` keeps `flex: none` so the
   account control never becomes the thing that shrinks. */
.rail__inner > .nav {
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: thin;
  overscroll-behavior-x: contain;
}
/* The scroller must not clip a focus ring vertically, so containment is only
   applied on the inline axis. */
.rail__inner > .nav { overflow-y: visible; }
.rail__inner > .who { flex: none; }

/* ---- nav at twelve items ---------------------------------------------------
   Measured, not guessed. `.rail__inner` caps at max-width 1440px, so the header
   never grows past that however wide the monitor is. Inside it: 64px padding,
   161px brand, 168px account control and 32px of gaps leave 1015px for the nav.
   Twelve links at 12px side padding need 1024px. Nine pixels short, which is
   why the last item read as clipped on every desktop capture.

   Trimming the side padding to 10px returns 48px across twelve links, which
   clears it with room for a thirteenth. The 44px tap target is set by
   min-height, not by this padding, so it is unaffected. The nav keeps its
   scroller for the narrow widths where twelve items genuinely cannot fit. */
.rail__inner > .nav a { padding-inline: 10px; }

/* ===========================================================================
   THE ARTWORK, WHICH IS NOT THE POST
   ---------------------------------------------------------------------------
   Appended for db/migrations/006_board_parity.sql, which gave a social post a
   design_status separate from its own status. Everything below exists to keep
   those two states looking like two states.

   A post reaches Scheduled while its artwork is still at Edit Brand Colors,
   because the coordinator and the designer finish on different days. If the
   design control sits inside the same panel as the status control, every person
   who opens the page reads one as a phase of the other, and the question the
   column was added to answer stops being asked. So the artwork gets a panel of
   its own with a wider gap above it, and the tiles on the month grid carry the
   design state as a second chip beside the first.

   No raw hex below. Colour is never the only carrier either: every chip and
   every spine here sits beside its own word.
   =========================================================================== */

/* The neutral spine. .panel--ok, --warn, --bad, --info and --primary were
   already defined; a design status of Not Started or Not Needed maps to neutral
   and had nothing to land on, so the panel fell back to the default border and
   read as a different kind of panel rather than a calm one. */
.panel--neutral { border-left-color: var(--fx-color-neutral); }

/* The artwork panel. Set apart by space and by a shaded head rather than by a
   colour of its own, because its colour is already carrying the design state
   and a second signal in the same place would fight it. */
.panel--design .panel__head { background: var(--fx-color-surface); }
.stack > .panel--design { margin-top: var(--fx-space-6); }
.panel--design .kv dd .chip { margin-bottom: var(--fx-space-1); }

/* The header count on the planner, which links to the artwork queue. A button
   shape rather than a chip: the number on its own is trivia, and the useful
   move is to open the list. */
.artwork-count {
  gap: var(--fx-space-2);
  border-left: 0 solid var(--fx-color-warn-solid);
  text-align: left;
}
.artwork-count__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.6em; padding: 0 var(--fx-space-1);
  background: var(--fx-color-warn-tint); color: var(--fx-color-warn);
  border-radius: 999px;
  font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-sm);
}

/* A content category is free text, so it can be as long as somebody wants. On a
   month tile it gets one line and an ellipsis rather than being allowed to push
   the day out of shape. The full text is on the post page. */
.cal__pcat {
  max-width: 14ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  display: inline-block;
}
@media (min-width: 1200px) {
  .cal__pcat { max-width: 20ch; }
}

/* A datalist is a suggestion and not a menu, and browsers give no hint that one
   is attached. The hint text under the field says so in words; this keeps the
   input from looking like a select while it does. */
.field input[list] { font-family: var(--fx-font-body); }

/* ============================================================================
   REFINEMENT PASS — 2026-08-21
   ============================================================================
   The first build applied one decorative motif uniformly and labelled every
   field in uppercase. Both are template tells, and both actively slow a broker
   down. This layer is opinionated about three things.

   1. THE SPINE MEANS SOMETHING OR IT GOES. A 3px accent on every panel is
      wallpaper. Kept where it encodes state (a row, a card, a status tile),
      removed from panels and containers that have no state to encode.

   2. LABELS ARE READ, NOT ANNOUNCED. Uppercase with letter-spacing on a field
      label is a poster technique. On a form with twenty fields it costs
      legibility and buys nothing, so labels are sentence case, small and quiet,
      and the VALUE carries the weight instead.

   3. THIS IS A NUMBERS TOOL. Money and dates are the content. Tabular figures
      everywhere they appear, money right-aligned, and tighter rows so more of
      the book fits on one screen. A CRE broker scans hundreds of rows; the
      previous density suited a marketing page.
   ========================================================================= */

/* ---- 1. spine restraint --------------------------------------------------- */
/* Panels and their heads are containers, not states. */
.panel { border-left: 0 solid transparent; }
.panel--ok, .panel--warn, .panel--bad, .panel--info, .panel--neutral {
  /* Width intentionally NOT re-thickened: the spine is an inset pill now and a
     3px left border behind it would paint a second, square one. The modifier
     still does its real job, which is setting border-left-COLOR for the pill to
     inherit. */
  border-left-width: 0;
}
.pagehead, .filters, .sec-head { border-left: 0; }

/* ---- 2. labels ------------------------------------------------------------ */
.field__label,
.kv dt,
.stat l, .stat__l,
.panel__head > h2, .panel__head > h3 {
  text-transform: none;
  letter-spacing: 0;
}
.field__label, .kv dt {
  font-family: var(--fx-font-ui);
  font-size: .8125rem;
  font-weight: 500;
  color: var(--fx-color-muted);
}
/* The panel title is a heading, so it reads as one rather than as a caption. */
.panel__head > h2, .panel__head > h3 {
  font-size: 1rem; font-weight: 700; color: var(--fx-color-ink);
  letter-spacing: -.008em; line-height: 1.3; text-wrap: balance;
}
/* Column headers keep their case: in a data table that convention aids
   scanning, which is the opposite of its effect on a form label. */
.table th { letter-spacing: .06em; font-size: var(--fx-text-xs); }

/* ---- 3. density and figures ---------------------------------------------- */
.table th, .table td { padding: var(--fx-space-3) var(--fx-space-4); line-height: 1.5; vertical-align: middle; }
.table td { font-variant-numeric: tabular-nums; }
.table tbody tr + tr td { border-top: 0; }
/* Money and counts right-align. A column of left-ragged prices cannot be
   compared down the column, which is the only reason to put them in a column. */
.table .num, .table td.num, .table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.mono, code, .kv dd, .stat__v, .stat v { font-variant-numeric: tabular-nums; }

/* ---- 4. hierarchy --------------------------------------------------------- */
/* The record's name should dominate its own page. Previously the h1 sat only
   fractionally above the panel titles, so nothing announced what you opened. */
.pagehead__t h1 {
  font-size: clamp(1.6rem, 1.2rem + 1.6vw, 2.4rem);
  letter-spacing: -.022em; line-height: 1.12; text-wrap: balance;
}
.pagehead__t p { color: var(--fx-color-muted); }

/* ---- 5. chips only where the value is not self-evident -------------------- */
/* A chip around a word that is already a word adds a box and no meaning. Kept
   for state, dropped for classification like industry or property type. */
.chip--plain {
  background: none; border: 0; padding: 0; color: var(--fx-color-muted);
  font-weight: 500; letter-spacing: 0; text-transform: none;
}

/* Nav group separators. A hairline, not a heading: the rail has no room for
   labels, and the grouping only has to read as "these belong together". */
.nav__sep {
  width: 1px; align-self: center; height: 1.15rem;
  background: var(--fx-color-rail-line); margin-inline: .3rem; flex: none;
}
.navdrawer__h {
  margin: .9rem 0 .2rem; padding: 0 var(--fx-space-4);
  font-family: var(--fx-font-ui); font-size: .7rem; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; color: var(--fx-color-muted);
}
.navdrawer__h:first-child { margin-top: .2rem; }

/* ---- chips: a dot and a word, not a pill ----------------------------------
   A filled pastel pill around every enum is the most recognisable design-system
   demo device there is, and in a table it puts a coloured box on every row so
   nothing stands out. Real deal software marks state with a small dot and plain
   text, which keeps the row scannable and leaves saturated colour available for
   the one row that genuinely needs attention.

   Scoped to tables and cards. The import review keeps its filled chips, because
   there they are filter buttons carrying counts and they should look pressable. */
.table .chip,
.kcard .chip,
.listing .chip {
  background: none;
  border: 0;
  padding: 0;
  font-weight: 550;
  font-size: .8125rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--fx-color-ink);
  display: inline-flex;
  align-items: center;
  gap: .4rem;
}
.table .chip::before,
.kcard .chip::before,
.listing .chip::before {
  content: "";
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--fx-color-neutral); flex: none;
}
.table .chip--ok::before,      .kcard .chip--ok::before      { background: var(--fx-color-ok); }
.table .chip--warn::before,    .kcard .chip--warn::before    { background: var(--fx-color-warn-solid); }
.table .chip--bad::before,     .kcard .chip--bad::before     { background: var(--fx-color-bad-solid); }
.table .chip--info::before,    .kcard .chip--info::before    { background: var(--fx-color-cyan); }
.table .chip--neutral::before, .kcard .chip--neutral::before { background: var(--fx-color-border-strong); }
/* The one state worth shouting about still gets colour on the word itself. */
.table .chip--bad { color: var(--fx-color-bad); font-weight: 650; }

/* Provenance marker. Present, findable, and not competing with the name. */
.src {
  font-size: .68rem; color: var(--fx-color-muted); letter-spacing: .04em;
  vertical-align: .35em; margin-left: .35rem; font-style: normal;
}

/* ---- pipeline funnel ------------------------------------------------------
   Replaces five stat tiles. Segments are flex-sized by their share of the
   total, so the shape of the funnel is the graphic and the numbers are still
   text. A minimum share keeps a one-project stage clickable and readable
   rather than collapsing to a sliver, which is the usual failure of this
   pattern. */
.funnel { display: flex; gap: .25rem; align-items: stretch; flex-wrap: wrap; }
.funnel__seg {
  flex: var(--share, 10) 1 7.5rem;
  display: flex; flex-direction: column; gap: .3rem;
  padding: .2rem .1rem .1rem; text-decoration: none; color: inherit;
  border-radius: 2px;
}
.funnel__seg:hover .funnel__bar { filter: brightness(1.12); }
.funnel__seg:focus-visible { outline: 2px solid var(--fx-color-focus); outline-offset: 2px; }
.funnel__bar {
  display: block; height: .5rem; border-radius: 1px;
  background: var(--fx-color-primary);
}
.funnel__seg.is-bad .funnel__bar { background: var(--fx-color-bad-solid); }
.funnel__seg.is-empty .funnel__bar { background: var(--fx-color-border); }
.funnel__n {
  font-size: 1.5rem; line-height: 1; font-weight: 650;
  font-variant-numeric: tabular-nums; color: var(--fx-color-ink);
}
.funnel__seg.is-empty .funnel__n { color: var(--fx-color-muted); font-weight: 500; }
.funnel__l { font-size: .78rem; color: var(--fx-color-muted); }
@media (max-width: 560px) { .funnel__seg { flex-basis: 5.5rem; } }

/* ===========================================================================
   THE TRANSACTION LAYER: offers, escrow, the contingency calendar, commissions
   ---------------------------------------------------------------------------
   Appended for src/views/deals-board.ejs, deal-detail.ejs, offers.ejs,
   deal-new.ejs and the contingency calendar screen.

   ONE IDEA RUNS THROUGH ALL OF IT. A missed contingency deadline is the most
   expensive routine failure in this business, so every overdue thing on these
   screens says so in a WORD before it says so in a colour. The red spine and
   the tinted chip are the second signal, never the only one. Nothing below
   carries meaning in colour alone, which is also what keeps these screens
   readable in the dark palette, in greyscale and to a screen reader.

   No raw hex. Every colour is an --fx-color-* token already defined above.
   =========================================================================== */

/* ---- the banner over the board ------------------------------------------ */
/* A link shaped like an alert. It sits above the board because the question it
   answers, what is owed and by when, outranks the question the board answers.
   A button rather than a chip: the count on its own is trivia, and the useful
   move is to open the list. */
.dbanner {
  display: flex; align-items: center; gap: var(--fx-space-4);
  margin-bottom: var(--fx-space-5);
  padding: var(--fx-space-3) var(--fx-space-4);
  background: var(--fx-color-surface);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid var(--fx-color-info);
  border-radius: var(--fx-radius-sm);
  color: inherit; text-decoration: none;
}
.dbanner:hover { background: var(--fx-color-surface-2); }
.dbanner--late { border-left-color: var(--fx-color-bad); background: var(--fx-color-bad-tint); }
.dbanner__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.2em; padding: var(--fx-space-1) var(--fx-space-2);
  background: var(--fx-color-info-tint); color: var(--fx-color-info);
  border-radius: var(--fx-radius-sm);
  font-family: var(--fx-font-mono); font-weight: 700; font-size: var(--fx-text-lg);
  font-variant-numeric: tabular-nums;
}
.dbanner--late .dbanner__n { background: var(--fx-color-bg); color: var(--fx-color-bad); }
.dbanner__b { display: flex; flex-direction: column; gap: var(--fx-space-1); }
.dbanner__b .muted { font-size: var(--fx-text-sm); }

/* ---- the board ---------------------------------------------------------- */
/* The column spines mirror the status tones the chips use, so a column and the
   chip on a tile inside it never disagree. */
.kcol--deal-open           { border-top-color: var(--fx-color-info); }
.kcol--deal-contingencies  { border-top-color: var(--fx-color-warn-solid); }
.kcol--deal-clear_to_close { border-top-color: var(--fx-color-ok); }
.kcol--deal-closed         { border-top-color: var(--fx-color-neutral); }
.kcol--deal-fell_through   { border-top-color: var(--fx-color-bad); }
.kcol--deal-cancelled      { border-top-color: var(--fx-color-neutral); }

.kcard--deal .kcard__price {
  display: flex; align-items: baseline; gap: var(--fx-space-2);
  margin: var(--fx-space-2) 0;
  font-size: var(--fx-text-lg); font-weight: 700;
}
.kcard--deal .kcard__price .muted { font-size: var(--fx-text-xs); font-weight: 400; }

/* THE LINE THE TILE EXISTS FOR. The count of outstanding dates and the nearest
   one. It is boxed rather than run in with the rest of the metadata because a
   number nobody sees is a number nobody acts on. */
.ddates {
  display: flex; flex-direction: column; gap: var(--fx-space-1);
  margin: var(--fx-space-2) 0;
  padding: var(--fx-space-2) var(--fx-space-3);
  border-left: 0 solid transparent;
  background: var(--fx-color-surface);
  border-radius: var(--fx-radius-sm);
}
.ddates__w { font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-sm); }
.ddates__d { font-size: var(--fx-text-xs); color: var(--fx-color-muted); }
.ddates--ok   { border-left-color: var(--fx-color-ok);   background: var(--fx-color-ok-tint); }
.ddates--info { border-left-color: var(--fx-color-info); background: var(--fx-color-info-tint); }
.ddates--warn { border-left-color: var(--fx-color-warn); background: var(--fx-color-warn-tint); }
.ddates--bad  { border-left-color: var(--fx-color-bad);  background: var(--fx-color-bad-tint); }
.ddates--bad .ddates__w { color: var(--fx-color-bad); }

/* ---- the contingency calendar ------------------------------------------- */
/* The list on the calendar screen and on a deal. Three columns on a wide
   screen: when it is due, what it is, and what can be done about it. It stacks
   on a narrow one, with the WHEN staying first, because the word is the point. */
.dcal { list-style: none; margin: var(--fx-space-3) 0 0; padding: 0; }
.dcal__i {
  display: grid;
  grid-template-columns: minmax(9rem, 12rem) 1fr auto;
  gap: var(--fx-space-3);
  align-items: start;
  padding: var(--fx-space-3);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-sm);
}
.dcal__i + .dcal__i { margin-top: var(--fx-space-2); }
.dcal__i--ok      { border-left-color: var(--fx-color-ok); }
.dcal__i--info    { border-left-color: var(--fx-color-info); }
.dcal__i--warn    { border-left-color: var(--fx-color-warn-solid); }
.dcal__i--bad     { border-left-color: var(--fx-color-bad); }
.dcal__i--neutral { border-left-color: var(--fx-color-neutral); }
/* Past its date. The tint is a second signal behind the word, never the first. */
.dcal__i--late { border-left-color: var(--fx-color-bad); background: var(--fx-color-bad-tint); }
.dcal__i--late .dcal__word { color: var(--fx-color-bad); }

.dcal__when { display: flex; flex-direction: column; gap: var(--fx-space-1); margin: 0; }
.dcal__word {
  font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-sm);
  line-height: 1.25;
}
.dcal__when .mono { font-size: var(--fx-text-xs); color: var(--fx-color-muted); }
.dcal__b { min-width: 0; }
.dcal__t { margin: 0; font-size: var(--fx-text-md); }
.dcal__meta {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center;
  margin: var(--fx-space-1) 0 0;
  font-size: var(--fx-text-sm); color: var(--fx-color-muted);
}
.dcal__note {
  margin: var(--fx-space-2) 0 0;
  font-size: var(--fx-text-sm); white-space: pre-wrap;
}
.dcal__act { display: flex; flex-direction: column; gap: var(--fx-space-2); align-items: flex-end; }
.dcal__act form { margin: 0; }

/* A waiver is a decision somebody made, so recording one takes a reason and the
   reason field is not hidden behind a second page. It is behind a disclosure
   because most rows get satisfied rather than waived. */
.dcal__waive summary {
  cursor: pointer;
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: 700;
  color: var(--fx-color-primary);
}
.dcal__waive[open] {
  margin-top: var(--fx-space-2);
  padding: var(--fx-space-3);
  background: var(--fx-color-surface);
  border: 1px solid var(--fx-color-border);
  border-radius: var(--fx-radius-sm);
}
.dcal__waive form { display: flex; flex-direction: column; gap: var(--fx-space-2); min-width: 16rem; }

@media (max-width: 900px) {
  .dcal__i { grid-template-columns: 1fr; }
  .dcal__act { align-items: flex-start; flex-direction: row; flex-wrap: wrap; }
}

/* The filter deals.js builds over the calendar. It is styled here and created
   there, so a browser with the script blocked never shows a dead input. */
.dcal-filter {
  display: flex; flex-direction: column; gap: var(--fx-space-1);
  margin-bottom: var(--fx-space-4);
  max-width: 32rem;
}
.dcal-filter .muted { font-size: var(--fx-text-sm); margin: 0; }

/* The one-line readout at the top of a deal's calendar panel. */
.dsum { margin: 0 0 var(--fx-space-3); font-size: var(--fx-text-md); }

/* ---- disclosures that hold a form --------------------------------------- */
/* Add a date, set the commission, log an offer, send a counter. All four are
   forms that are usually not wanted, so they sit behind a summary rather than
   pushing the record down the page. */
.dadd { margin-top: var(--fx-space-4); }
.dadd > summary {
  cursor: pointer;
  padding: var(--fx-space-2) 0;
  font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-sm);
  color: var(--fx-color-primary);
}
.dadd[open] > summary { margin-bottom: var(--fx-space-3); }
.dadd[open] {
  padding: var(--fx-space-3);
  background: var(--fx-color-surface);
  border: 1px solid var(--fx-color-border);
  border-radius: var(--fx-radius-sm);
}

/* ---- the commission split editor ---------------------------------------- */
.dsplits { display: flex; flex-direction: column; gap: var(--fx-space-2); }
.dsplit {
  display: grid;
  grid-template-columns: minmax(8rem, 1fr) minmax(10rem, 2fr) minmax(8rem, 1fr) 7rem 9rem;
  gap: var(--fx-space-2);
  align-items: end;
  padding: var(--fx-space-2);
  border: 1px solid var(--fx-color-border);
  border-radius: var(--fx-radius-sm);
}
.dsplit .field { margin: 0; }
@media (max-width: 1000px) {
  .dsplit { grid-template-columns: 1fr 1fr; }
}

/* ---- the offer chain on a deal ------------------------------------------ */
/* Indented by depth so a counter reads as an answer to the row above it rather
   than as another offer that happened to arrive later. */
.ochain { list-style: none; margin: 0; padding: 0; }
.ochain__i {
  padding: var(--fx-space-3);
  border-left: 0 solid transparent;
  background: var(--fx-color-surface);
  border-radius: var(--fx-radius-sm);
}
.ochain__i + .ochain__i { margin-top: var(--fx-space-2); }
.ochain__i--d1 { margin-left: var(--fx-space-4); }
.ochain__i--d2 { margin-left: var(--fx-space-6); }
.ochain__i--d3 { margin-left: var(--fx-space-7); }
.ochain__i--d4 { margin-left: var(--fx-space-8); }
.ochain__h {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center;
  margin: 0 0 var(--fx-space-1);
}
.ochain__amt { margin-left: auto; font-size: var(--fx-text-lg); font-weight: 700; }
.ochain__m {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: baseline;
  margin: 0; font-size: var(--fx-text-sm);
}
.ochain__c {
  margin: var(--fx-space-2) 0 0;
  font-size: var(--fx-text-sm); white-space: pre-wrap; color: var(--fx-color-muted);
}

/* ---- the offers list ---------------------------------------------------- */
.olist { list-style: none; margin: 0; padding: 0; }
.olist__i + .olist__i { margin-top: var(--fx-space-3); }
.olist__i--d1 { margin-left: var(--fx-space-5); }
.olist__i--d2 { margin-left: var(--fx-space-7); }
.olist__i--d3 { margin-left: var(--fx-space-8); }
.olist__i--d4 { margin-left: var(--fx-space-8); }

.ocard {
  padding: var(--fx-space-4);
  background: var(--fx-color-bg);
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-sm);
}
.ocard--ok      { border-left-color: var(--fx-color-ok); }
.ocard--info    { border-left-color: var(--fx-color-info); }
.ocard--warn    { border-left-color: var(--fx-color-warn-solid); }
.ocard--bad     { border-left-color: var(--fx-color-bad); }
.ocard--neutral { border-left-color: var(--fx-color-neutral); }

.ocard__h {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: flex-start;
}
.ocard__amt {
  margin: 0;
  font-size: var(--fx-text-xl); font-weight: 700; line-height: 1.1;
  min-width: 8rem;
}
.ocard__t { flex: 1 1 14rem; min-width: 0; }
.ocard__t h2 {
  margin: 0;
  font-size: var(--fx-text-lg);
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center;
}
.ocard__m {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: baseline;
  margin: var(--fx-space-1) 0 0; font-size: var(--fx-text-sm);
}
.ocard__badges { display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center; }
.ocard__terms {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3);
  margin: var(--fx-space-3) 0 0;
  font-size: var(--fx-text-sm); color: var(--fx-color-muted);
}
.ocard__c {
  margin: var(--fx-space-2) 0 0;
  font-size: var(--fx-text-sm); white-space: pre-wrap;
}
.ocard__act {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: flex-start;
  margin-top: var(--fx-space-3);
  padding-top: var(--fx-space-3);
  border-top: 1px solid var(--fx-color-border);
}
.ocard__act form { margin: 0; }
.ocard__counter summary {
  cursor: pointer;
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: 700;
  color: var(--fx-color-primary);
}
.ocard__counter[open] {
  flex: 1 1 100%;
  margin-top: var(--fx-space-2);
  padding: var(--fx-space-3);
  background: var(--fx-color-surface);
  border: 1px solid var(--fx-color-border);
  border-radius: var(--fx-radius-sm);
}

/* ---- what opening a deal will create ------------------------------------ */
/* The eight standard contingency dates, listed on the opening form so nobody
   has to press the button to find out what it does. */
.dseed { list-style: none; margin: var(--fx-space-3) 0; padding: 0; }
.dseed li {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: baseline;
  padding: var(--fx-space-2) 0;
  border-bottom: 1px solid var(--fx-color-border);
  font-size: var(--fx-text-sm);
}
.dseed li:last-child { border-bottom: 0; }

/* Hint under an attach form. Explains the difference between storing a file and
   linking to one, which is the whole reason both options exist. */
.ck__hint { font-size: .78rem; color: var(--fx-color-muted); margin: .4rem 0 0; line-height: 1.45; }

/* ---- board and deal-page width fixes --------------------------------------
   Three separate faults, all of which made a page wider than the screen.

   1. `.kanban` scrolls its columns horizontally, which is intended, but an
      overflowing child still contributes to the ROOT scroll width unless the
      scroller says otherwise. So the deals board dragged the whole page to
      1228px on a 390px phone and took the header rail with it. Exactly the
      fault already fixed on `.table-scroll`; the board was simply missed.

   2. `.dcal__waive form` carried `min-width: 16rem` to keep the reason field
      usable. Inside a narrow cell that is a floor the layout cannot honour, so
      the form ran past the right edge at EVERY width, including 1600px. The
      floor now yields to the container.

   3. The commission table is wrapped in a scroller in the markup; `.table`
      keeps its 720px min-width so the seven columns stay readable. */
.kanban { contain: paint; }
.dcal__waive form { min-width: min(16rem, 100%); max-width: 100%; }
.dcal__waive[open] { max-width: 100%; overflow-wrap: anywhere; }


/* ---- mail -----------------------------------------------------------------
   The three mail screens. Every colour is an existing --fx-color-* token; there
   is no raw hex in this block, because a second palette is how a design system
   stops being one.

   The inbox is a LIST rather than a table. A table of mail wants six columns
   and gets four of them wrong on a phone, and the thing a person is scanning
   for is not in a column: it is "who, about what, and is it filed". So each row
   is a small block with the filing chips along the bottom, and the unfiled ones
   carry a spine on the left in the warn colour so the list reads at a glance
   without anybody having to apply the filter.

   The thread is two columns on a wide screen and one on a narrow one, with the
   side panel BELOW the conversation rather than above it when it collapses. On
   a phone the messages are what somebody came for.

   The message body is the only place in this application that renders markup
   somebody else wrote, so it is boxed, its images are capped to the column, and
   long unbroken strings wrap rather than pushing the page sideways. Everything
   in it was sanitised on the way in by src/mail/html.js; the containment here
   is about layout, not about safety. */

/* ---- the inbox list ---- */
.mail-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--fx-space-3); }

.mail-row {
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid transparent;
  border-radius: var(--fx-radius-md);
  background: var(--fx-color-bg);
  padding: var(--fx-space-3) var(--fx-space-4);
  display: grid;
  gap: var(--fx-space-2);
}
.mail-row--unfiled { border-left-color: var(--fx-color-warn-solid); background: var(--fx-color-surface); }

.mail-row__head { display: flex; gap: var(--fx-space-3); align-items: baseline; justify-content: space-between; }
.mail-row__who { font-family: var(--fx-font-ui); font-weight: 600; overflow-wrap: anywhere; }
.mail-row__when { color: var(--fx-color-muted); font-size: var(--fx-text-sm); }

.mail-row__subject {
  font-family: var(--fx-font-ui);
  font-size: var(--fx-text-lg);
  font-weight: 600;
  text-decoration: none;
  overflow-wrap: anywhere;
}
.mail-row__subject:hover { text-decoration: underline; }

.mail-row__snippet {
  margin: 0;
  color: var(--fx-color-muted);
  font-size: var(--fx-text-sm);
  /* One line. The snippet is a hint about whether to open the thread, and a
     three line preview turns a list of forty into a page nobody scans. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mail-row__meta,
.mail-msg__foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--fx-space-2);
  align-items: center;
}
.mail-row__meta .muted,
.mail-msg__foot .muted { font-size: var(--fx-text-xs); }

.mail-row__file,
.mail-msg__file { display: flex; gap: var(--fx-space-2); align-items: center; flex-wrap: wrap; }
.mail-row__file .select,
.mail-msg__file .select { max-width: 22rem; }

/* An address is data, not prose. Monospace makes a transposed character in a
   domain visible, which is the whole reason anybody reads one of these. */
.mail-addr { font-family: var(--fx-font-mono); font-size: var(--fx-text-sm); overflow-wrap: anywhere; }

.mail-outbox { border-left: 0 solid var(--fx-color-warn-solid); }

/* ---- one conversation ---- */
.mail-thread { align-items: start; gap: var(--fx-space-5); }
.mail-thread__main { min-width: 0; display: grid; gap: var(--fx-space-4); }
.mail-thread__side { min-width: 0; display: grid; gap: var(--fx-space-4); }

@media (min-width: 60rem) {
  .mail-thread { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
}

.mail-msg { border-left: 0 solid transparent; }
.mail-msg--out { border-left-color: var(--fx-color-primary); background: var(--fx-color-primary-tint); }

.mail-msg__head h2 {
  display: flex;
  gap: var(--fx-space-2);
  align-items: center;
  flex-wrap: wrap;
  font-size: var(--fx-text-md);
  margin: 0;
}
.mail-msg__addr { margin-bottom: var(--fx-space-3); }

.mail-link { display: inline-flex; gap: var(--fx-space-2); align-items: center; }
.mail-link form { display: inline; }
.mail-link__why { font-size: var(--fx-text-xs); }

/* ---- the message body ---- */
.mail-body {
  border-top: 1px solid var(--fx-color-border);
  padding-top: var(--fx-space-3);
  overflow-wrap: anywhere;
  /* Mail carries wide tables and pasted rent rolls. They scroll inside the
     message rather than dragging the page sideways, the same rule .table-scroll
     already applies everywhere else. */
  overflow-x: auto;
}
.mail-body--plain { white-space: pre-wrap; font-family: var(--fx-font-body); }
.mail-body img { max-width: 100%; height: auto; }
.mail-body table { border-collapse: collapse; max-width: 100%; }
.mail-body td, .mail-body th { border: 1px solid var(--fx-color-border); padding: var(--fx-space-1) var(--fx-space-2); }
.mail-body blockquote {
  margin: var(--fx-space-3) 0;
  padding-left: var(--fx-space-3);
  border-left: 2px solid var(--fx-color-border);
  color: var(--fx-color-muted);
}
.mail-body pre { overflow-x: auto; }

.mail-atts { list-style: none; margin: var(--fx-space-3) 0 0; padding: 0; display: grid; gap: var(--fx-space-2); }
.mail-atts li { display: flex; gap: var(--fx-space-2); align-items: center; flex-wrap: wrap; font-size: var(--fx-text-sm); }

/* ---- the side panels ---- */
.mail-side-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--fx-space-3); }
.mail-side-list li { display: grid; gap: var(--fx-space-1); }
.mail-side-list form { margin: 0; }
.mail-why { font-size: var(--fx-text-xs); }

/* ---- the mailbox screen ---- */
.mail-fieldset {
  border: 1px solid var(--fx-color-border);
  border-radius: var(--fx-radius-md);
  padding: var(--fx-space-4);
  margin-top: var(--fx-space-4);
}
.mail-fieldset legend {
  font-family: var(--fx-font-ui);
  font-weight: 600;
  font-size: var(--fx-text-sm);
  padding: 0 var(--fx-space-2);
}
.mail-actions { display: flex; flex-wrap: wrap; gap: var(--fx-space-1); align-items: center; }
.mail-actions form { display: inline; margin: 0; }

/* ---- nav at twelve items ---------------------------------------------------
   Mail made it twelve, and the 48px the padding trim bought was exactly what
   the twelfth consumed: measured at 1440px the nav needed 978px against 978px
   available. It fitted, with nothing to spare, and below 1440 it fell back to
   its own scroller. A horizontally scrolling menu on a 1366px laptop is a menu
   whose last items nobody finds, and 1366 and 1280 are what people actually
   have on their desks.

   The width is in the letterforms rather than the layout. These labels were
   uppercase with 0.08em of tracking, which is the one place the shouty label
   treatment survived the refinement pass that took it off every other label in
   the system. Sentence case at the same size is materially narrower, the
   tracking on top of it is pure width, and neither was carrying meaning: the
   current page is marked by the cyan underline and the brighter ink, not by
   being in capitals. */
.rail__inner > .nav a {
  text-transform: none;
  letter-spacing: 0.005em;
  font-size: 0.8125rem;
}

/* ---- people ----------------------------------------------------------------
   Staff administration. Three small things the rest of the system had no need
   for: a password shown exactly once, a session row with an End button, and a
   numeric table column that has to line up.

   The one-time password is set in a monospaced face at a size somebody can read
   off a screen from across a desk, because that is literally how it gets from
   the principal to the person. Letter-spacing is deliberate: the grouping
   dashes do the chunking, and tracking on top of them makes a run of capitals
   scan as characters rather than as a word. */
.onetime { margin-bottom: var(--fx-space-6); }
.onetime__pw {
  margin: var(--fx-space-4) 0;
  font-family: var(--fx-font-mono);
  font-size: clamp(1.125rem, 4cqi, 1.5rem);
  letter-spacing: 0.06em;
  overflow-wrap: anywhere;
}
.onetime__pw code {
  display: inline-block; padding: var(--fx-space-3) var(--fx-space-4);
  background: var(--fx-color-surface); border: 1px solid var(--fx-color-border);
  user-select: all; /* one click selects the whole thing */
}

/* A live session and the control that ends it. */
.sessionrow {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--fx-space-4); padding: var(--fx-space-3) 0;
  border-bottom: 1px solid var(--fx-color-border);
}
.sessionrow:last-of-type { border-bottom: 0; }

/* Counts in the roster. Right-aligned and tabular so a column of numbers can be
   compared down its length rather than read one at a time. */
.table th.num, .table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* The row for the person who was just added, so the eye lands on it. */
.row--new > td { background: var(--fx-color-surface); }

.small { font-size: var(--fx-text-xs); }

/* Prose that has to stay readable inside a wide panel. 62ch is the measure the
   checklist copy already uses; this names it so other screens can ask for it. */
.measure { max-width: 62ch; }

/* ===========================================================================
   WHAT NEEDS YOU
   ---------------------------------------------------------------------------
   The band at the top of Today. This is the one screen element the whole system
   is arranged around, so it gets the page's largest type and its only saturated
   colour, and everything below it is quieter than it used to be.

   THE LEFT EDGE IS A CLOCK, NOT A DECORATION. The original stylesheet put a 3px
   spine on every panel, row, column and timeline entry, which meant it carried
   no information: a device that appears on everything says nothing about
   anything. Here it appears only on rows that have a date, and its colour is
   how close that date is. Every row also carries the same fact in words —
   "7 days late" — so nothing depends on seeing the colour.

   It is a ranked list rather than four grouped ones. A person does not care
   that this row came from a deal and the next from a checklist; they care that
   one is a week late and the other is due Friday. Grouping by source hands that
   comparison back to the reader, which is the work this is meant to do for them.
   =========================================================================== */
.due {
  border-top: 2px solid var(--fx-color-ink);
  margin-bottom: var(--fx-space-6);
}
.due__head {
  display: flex; align-items: baseline; justify-content: space-between;
  flex-wrap: wrap; gap: var(--fx-space-3);
  padding: var(--fx-space-3) 0 var(--fx-space-4);
}
.due__head h2 {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--fx-color-muted);
}
.due__legend { display: flex; align-items: center; gap: var(--fx-space-3); flex-wrap: wrap; }
.due__all {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 600;
  color: var(--fx-color-primary);
}

.due__list { list-style: none; margin: 0; padding: 0; }

/* The row. Grid rather than flex so the four columns line up down the list —
   an urgency column that wanders left and right by a few pixels per row cannot
   be scanned, which is the entire job here. */
.due__row + .due__row { border-top: 1px solid var(--fx-color-border); }
.due__link {
  display: grid;
  /* Three zones, not four columns. The first attempt put `what` and `where` in
     separate flexible columns, which pushed a title and the building it belongs
     to a thousand pixels apart on a wide screen and wrapped the contingency
     rows to triple height. They are one thought, so they occupy one cell: the
     thing on the first line, where it lives on the second. */
  grid-template-columns: 8.5rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--fx-space-2) var(--fx-space-4);
  padding: var(--fx-space-2) var(--fx-space-4);
  /* The clock edge. Transparent by default so the row is flush with the ones
     around it and the colour is the only thing that moves. */
  border-left: 0 solid transparent;
  color: inherit; text-decoration: none;
  min-height: var(--fx-tap);
}
.due__link:hover { background: var(--fx-color-surface); }
.due__row--bad  .due__link { border-left-color: var(--fx-color-bad-solid); }
.due__row--warn .due__link { border-left-color: var(--fx-color-warn-solid); }
.due__row--neutral .due__link { border-left-color: var(--fx-color-border-strong); }

/* Lateness is the column that gets read first, so it is the one set in the
   figure face with tabular numerals: a column of "7 days late" over
   "in 5 days" only scans if the digits sit in the same place on every line. */
.due__when {
  font-family: var(--fx-font-ui); font-weight: 700; font-size: var(--fx-text-sm);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.due__row--bad  .due__when { color: var(--fx-color-bad); }
.due__row--warn .due__when { color: var(--fx-color-warn); }
.due__row--neutral .due__when { color: var(--fx-color-muted); }

.due__mid { min-width: 0; display: grid; gap: 0.15rem; }
.due__what {
  font-weight: 600; overflow-wrap: anywhere;
  display: flex; align-items: baseline; gap: var(--fx-space-2); flex-wrap: wrap;
}
.due__link:hover .due__what { text-decoration: underline; text-underline-offset: 2px; }

.due__where {
  display: flex; align-items: baseline; gap: var(--fx-space-2);
  flex-wrap: wrap; min-width: 0;
  font-size: var(--fx-text-sm); color: var(--fx-color-muted);
}
/* Which of the four streams a row came from. Small, because it is the least
   useful thing on the line and it is here only so the destination is not a
   surprise. */
.due__kind {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--fx-color-muted); white-space: nowrap;
}
.due__ctx { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The reference is set in the mono face on purpose. In this business the
   identifier IS the content — JG-DEAL-2026-001 is what gets read down a phone
   and typed into an escrow portal — so it is treated as data, not as prose. */
.due__ref {
  font-family: var(--fx-font-mono); font-size: var(--fx-text-xs);
  color: var(--fx-color-muted); white-space: nowrap;
}
.due__who {
  font-size: var(--fx-text-sm); color: var(--fx-color-muted);
  text-align: right; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}

.due__clear, .due__more {
  padding: var(--fx-space-4);
  font-size: var(--fx-text-sm); color: var(--fx-color-muted); max-width: 62ch;
}
.due__clear { border-left: 0 solid var(--fx-color-ok); }

/* Below 900px the four columns become two lines: the clock and the thing on
   one, everything else on the next. Squeezing four columns into a phone gives
   four unreadable ones. */
/* Below 760px the clock moves above the thing it is about, because a fixed
   8.5rem column plus a name plus an owner does not fit a phone and squeezing
   all three gives three unreadable ones. */
@media (max-width: 760px) {
  .due__link {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "when who" "mid mid";
    align-items: baseline;
  }
  .due__when { grid-area: when; }
  .due__mid  { grid-area: mid; }
  .due__who  { grid-area: who; }
}

/* Defensive: if a breadcrumb is ever written as a nav wrapping an ol, the list
   markers reappear because list-style sits on .crumbs itself. This makes the
   markup mistake harmless rather than visible on a client's screen. */
.crumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: var(--fx-space-2); margin: 0; padding: 0; }

/* ===========================================================================
   THE ASSEMBLED DOCUMENT
   ---------------------------------------------------------------------------
   The Proposal Creator's output. It reuses .doc for the page frame and adds
   what a property document needs that a quote does not: a photograph grid, a
   figures list where every number carries the inputs it came from, and section
   sources set small enough to be present without competing with the content.

   Everything the broker needs and the client does not carries .noprint, so the
   review furniture disappears the moment somebody prints.
   =========================================================================== */
.creator__title {
  font-family: var(--fx-font-display); font-size: var(--fx-text-3xl);
  letter-spacing: -0.01em; text-wrap: balance; margin-top: var(--fx-space-5);
}
.creator__sub { color: var(--fx-color-muted); margin-top: var(--fx-space-2); }

.creator__sec { margin-top: var(--fx-space-7); break-inside: avoid; }
.creator__sec h2 {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--fx-color-muted);
  border-top: 2px solid var(--fx-color-ink); padding-top: var(--fx-space-3);
  margin-bottom: var(--fx-space-4);
}
.creator__sec h3 {
  font-size: var(--fx-text-md); margin-top: var(--fx-space-4);
}

.creator__facts { display: grid; grid-template-columns: minmax(0, 14rem) minmax(0, 1fr); gap: var(--fx-space-2) var(--fx-space-4); }
.creator__facts dt { color: var(--fx-color-muted); font-size: var(--fx-text-sm); }
.creator__facts dd { font-variant-numeric: tabular-nums; }
/* The inputs behind a figure, on the same line as the figure. A number whose
   derivation is a footnote is a number nobody checks. */
.creator__from { display: block; font-size: var(--fx-text-xs); color: var(--fx-color-muted); }

.creator__photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr)); gap: var(--fx-space-4); }
.creator__photos img { width: 100%; height: auto; display: block; border: 1px solid var(--fx-color-border); }
.creator__photos figcaption { font-size: var(--fx-text-xs); color: var(--fx-color-muted); margin-top: var(--fx-space-2); }

.creator__team { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr)); gap: var(--fx-space-4); }

.creator__src { font-size: var(--fx-text-xs); color: var(--fx-color-muted); margin-top: var(--fx-space-3); max-width: 62ch; }
.creator__sec--fine p { font-size: var(--fx-text-xs); color: var(--fx-color-muted); max-width: 62ch; }
.creator__todo { color: var(--fx-color-warn); font-style: italic; }

@media print {
  .noprint { display: none !important; }
  .creator__sec { page-break-inside: avoid; }
  .creator__title { page-break-after: avoid; }
}

/* ---------------------------------------------------------------- tracker --
   Ellory Barnette, 2026-08-26: a broker "out at a property" needs to see where
   their request is. Built phone-first and only then widened, because the
   opposite order produces a desktop table with a media query bolted on.

   Single column at every width on purpose. The stage list is a sequence, and a
   sequence read in two columns stops being one. */

.trk__now {
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid var(--fx-color-muted);
  border-radius: var(--fx-radius-md);
  padding: var(--fx-space-5);
  margin-bottom: var(--fx-space-6);
  background: var(--fx-color-surface);
}
.trk__now--current { border-left-color: var(--fx-color-primary); }
.trk__now--blocked { border-left-color: var(--fx-color-warn); }
.trk__now--done    { border-left-color: var(--fx-color-ok); }
.trk__eyebrow {
  font-size: var(--fx-text-xs); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--fx-color-muted); margin: 0 0 var(--fx-space-2);
}
/* Large enough to read at arm's length outdoors, which is where this is used. */
.trk__nowt { font-size: var(--fx-text-2xl); margin: 0 0 var(--fx-space-2); line-height: 1.15; }
.trk__nowd { margin: 0; max-width: 60ch; }
.trk__last { margin: var(--fx-space-3) 0 0; color: var(--fx-color-muted); }

.trk { list-style: none; margin: 0 0 var(--fx-space-6); padding: 0; }
.trk__s {
  display: grid; grid-template-columns: 2rem minmax(0, 1fr);
  gap: var(--fx-space-4); padding: 0 0 var(--fx-space-5);
  position: relative;
}
/* The line joining the stages. Drawn on the item rather than between them so
   the last stage does not trail a line into nothing. */
.trk__s:not(:last-child)::before {
  content: ""; position: absolute; left: calc(1rem - 1px); top: 2rem; bottom: 0;
  width: 2px; background: var(--fx-color-border);
}
.trk__s--done:not(:last-child)::before { background: var(--fx-color-ok); }

.trk__mark {
  width: 2rem; height: 2rem; border-radius: 50%;
  display: grid; place-items: center;
  font-size: var(--fx-text-sm); font-weight: 600;
  border: 2px solid var(--fx-color-border);
  background: var(--fx-color-bg); color: var(--fx-color-muted);
  position: relative; z-index: 1;
}
.trk__s--done .trk__mark {
  border-color: var(--fx-color-ok); background: var(--fx-color-ok);
  color: var(--fx-color-on-ok);
}
.trk__s--current .trk__mark { border-color: var(--fx-color-primary); color: var(--fx-color-primary); }
.trk__s--blocked .trk__mark { border-color: var(--fx-color-warn); color: var(--fx-color-warn); }

.trk__t {
  font-size: var(--fx-text-lg); margin: 0.15rem 0 var(--fx-space-1);
  display: flex; align-items: baseline; gap: var(--fx-space-3); flex-wrap: wrap;
}
.trk__s--waiting .trk__t, .trk__s--waiting .trk__d { color: var(--fx-color-muted); }
/* The word beside the marker. State is never colour alone. */
.trk__tag {
  font-size: var(--fx-text-xs); letter-spacing: 0.06em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px; border: 1px solid currentColor; font-weight: 600;
}
.trk__tag--done    { color: var(--fx-color-ok); }
.trk__tag--current { color: var(--fx-color-primary); }
.trk__tag--blocked { color: var(--fx-color-warn); }
.trk__tag--waiting { color: var(--fx-color-muted); }

.trk__d  { margin: 0; max-width: 60ch; }
.trk__ev { margin: var(--fx-space-2) 0 0; font-size: var(--fx-text-sm); }
.trk__sub {
  margin: var(--fx-space-3) 0 0; padding-left: var(--fx-space-4);
  font-size: var(--fx-text-sm); color: var(--fx-color-muted);
}
.trk__sub li { margin-bottom: var(--fx-space-1); }
.trk__sub--done { color: var(--fx-color-ink); }

/* ---- the list ---- */
.trkl { list-style: none; margin: 0; padding: 0; }
.trkl__i {
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid var(--fx-color-muted);
  border-radius: var(--fx-radius-md);
  margin-bottom: var(--fx-space-3);
  background: var(--fx-color-surface);
}
.trkl__i--current { border-left-color: var(--fx-color-primary); }
.trkl__i--blocked { border-left-color: var(--fx-color-warn); }
.trkl__i--done    { border-left-color: var(--fx-color-ok); }
.trkl__link {
  display: grid; gap: var(--fx-space-2);
  padding: var(--fx-space-4); text-decoration: none; color: inherit;
  min-height: var(--fx-tap);
}
.trkl__link:hover { background: var(--fx-color-primary-tint); }
.trkl__name { display: block; font-weight: 600; font-size: var(--fx-text-md); }
.trkl__meta, .trkl__ev { display: block; font-size: var(--fx-text-sm); color: var(--fx-color-muted); }
.trkl__stage { display: block; font-size: var(--fx-text-sm); font-weight: 600; }
.trkl__dots { display: flex; gap: 5px; }
.trkl__dot {
  width: 100%; height: 4px; border-radius: 999px; background: var(--fx-color-border);
}
.trkl__dot--done    { background: var(--fx-color-ok); }
.trkl__dot--current { background: var(--fx-color-primary); }
.trkl__dot--blocked { background: var(--fx-color-warn); }

/* Two columns only once there is genuinely room. Below this the stacked layout
   is not a fallback, it is the better reading order. */
@media (min-width: 44rem) {
  .trkl__link {
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    grid-template-areas: "main state" "dots dots";
    align-items: center;
  }
  .trkl__main  { grid-area: main; }
  .trkl__state { grid-area: state; text-align: right; }
  .trkl__dots  { grid-area: dots; }
}


/* ------------------------------------------------------- elevation, applied --
   Added 2026-08-26 with the rounding pass. The rule for using these: a shadow
   means "this is above the page", so it goes on things that appear over other
   content or that the eye should pick out first. It does not go on every panel,
   because a page where everything is raised has nothing raised. */

/* Menus and popovers genuinely float. */
.menu, .who__menu, .pop, .dropdown__menu { box-shadow: var(--fx-shadow-2); }

/* The attention band is the one thing on the dashboard that should out-rank
   everything around it, which is the whole reason it exists. */
.due { box-shadow: var(--fx-shadow-1); }

/* Panels get depth on hover only where the whole panel is a link, so the lift
   confirms the thing is clickable rather than decorating a static box. */
.trkl__i { transition: box-shadow var(--fx-dur) var(--fx-ease), border-color var(--fx-dur) var(--fx-ease); }
.trkl__i:hover { box-shadow: var(--fx-shadow-1); }

/* The primary action is the one control on a screen worth lifting. */
.btn--primary { box-shadow: var(--fx-shadow-1); }
.btn--primary:hover { box-shadow: var(--fx-shadow-2); }
.btn--primary:active { box-shadow: none; }

/* Rounding the pieces that were built before the scale existed and had their
   corners hard-coded. Collected here rather than edited in place so the change
   is one reviewable block. */
/* Class names verified against the stylesheet rather than assumed. An earlier
   version of this block named .table-wrap, .board__col and .creator__doc, none
   of which exist — so three of the rules silently did nothing and the tables
   kept their old corners while everything around them changed. */
.stat, .panel, .table-scroll, .kcard { border-radius: var(--fx-radius-md); }
.trk__now { border-radius: var(--fx-radius-lg); }

/* A rounded container clips its own children, or the child's square corner
   pokes through the parent's curve — most visible on a table's first header
   cell and on any spine that runs to the edge. */
/* A rounded container clips its own children, or a child square corner pokes
   through the parent curve. NOT applied to .table-scroll: that element scrolls
   horizontally on purpose, and overflow:hidden would kill the scroll on every
   wide table in the system. */
.panel, .trkl__i, .trk__now, .kcard { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .trkl__i, .btn--primary { transition: none; }
}

/* ------------------------------------------------------------- first run --
   What a workspace shows before anybody has put anything in it. Sized to
   out-rank everything below it, because on day one it IS the page. Retires
   itself once there is real work, so it never needs a dismiss control. */
.fr {
  border: 1px solid var(--fx-color-border);
  border-left: 0 solid var(--fx-color-primary);
  border-radius: var(--fx-radius-lg);
  background: var(--fx-color-surface);
  box-shadow: var(--fx-shadow-1);
  padding: var(--fx-space-6);
  margin-bottom: var(--fx-space-6);
  overflow: hidden;
}
.fr__eyebrow {
  font-size: var(--fx-text-xs); letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--fx-color-primary); font-weight: 700; margin: 0 0 var(--fx-space-2);
}
.fr__t { font-size: var(--fx-text-2xl); margin: 0 0 var(--fx-space-3); line-height: 1.15; }
.fr__d { margin: 0 0 var(--fx-space-3); }
.fr__count { margin: 0 0 var(--fx-space-5); color: var(--fx-color-muted); font-size: var(--fx-text-sm); }
.fr__count b { color: var(--fx-color-ink); }

.fr__steps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--fx-space-5); }
.fr__s { display: grid; grid-template-columns: 2rem minmax(0, 1fr); gap: var(--fx-space-4); }
.fr__mark {
  width: 2rem; height: 2rem; border-radius: 50%; display: grid; place-items: center;
  font-size: var(--fx-text-sm); font-weight: 600;
  border: 2px solid var(--fx-color-border-strong); color: var(--fx-color-muted);
  background: var(--fx-color-bg);
}
.fr__s--done .fr__mark {
  border-color: var(--fx-color-ok); background: var(--fx-color-ok); color: #fff;
}
.fr__st {
  font-size: var(--fx-text-lg); margin: 0.15rem 0 var(--fx-space-1);
  display: flex; gap: var(--fx-space-3); align-items: baseline; flex-wrap: wrap;
}
/* State carries a word, not only a colour, exactly as the spine rule requires. */
.fr__tag {
  font-size: var(--fx-text-xs); text-transform: uppercase; letter-spacing: 0.06em;
  font-weight: 600; color: var(--fx-color-ok); border: 1px solid currentColor;
  border-radius: 999px; padding: 1px 8px;
}
.fr__s--done .fr__st, .fr__s--done .fr__sd { color: var(--fx-color-muted); }
.fr__sd { margin: 0; }
.fr__a { margin: var(--fx-space-3) 0 0; display: flex; gap: var(--fx-space-4); align-items: center; flex-wrap: wrap; }
.fr__alt { font-size: var(--fx-text-sm); }
.fr__foot { margin: var(--fx-space-5) 0 0; }

/* ===========================================================================
   ICONS
   ---------------------------------------------------------------------------
   Added 2026-08-28. APPENDED SECTION: nothing above this line was touched.
   The marks themselves live in src/views/partials/icon.ejs, inlined per use.

   WHY THERE IS ALMOST NOTHING HERE
   Each icon is drawn with fill="none" and stroke="currentColor", so it takes
   its colour from whatever it sits inside. That is the whole theming story:
   no light rule, no dark rule, no per-component icon colour, and a control
   that dims its text dims its icon with it. The only thing this stylesheet
   has to decide is how big the mark is and how far it sits from the word.

   Size is in em rather than px so an icon is a property of the type around
   it. A 13px nav label gets a 13px mark, a 1.5rem heading gets a 1.5rem one,
   and nobody has to remember a modifier class for each context.
   =========================================================================== */
.icon {
  /* Slightly over 1em: a stroked mark at exactly cap height reads smaller
     than the letters beside it, because the letters have weight in the middle
     and the mark is hollow. */
  width: 1.08em; height: 1.08em;
  flex: none;
  display: inline-block;
  vertical-align: -0.19em;
  overflow: visible;
}
.icon--sm { width: 0.9em;  height: 0.9em;  vertical-align: -0.13em; }
.icon--lg { width: 1.35em; height: 1.35em; vertical-align: -0.28em; }
.icon--xl { width: 2em;    height: 2em;    vertical-align: -0.4em;  }

/* ---- icons in the top rail -------------------------------------------------
   The rail was already full. The measurement in the "nav at twelve items"
   block above is still the governing one: about 1015px of header goes to the
   nav, and thirteen links at 10px of side padding were using nearly all of it.
   A mark plus its gap costs roughly 17px an item, so the space has to come
   from somewhere or the nav falls into its horizontal scroller, which is the
   thing that measurement existed to prevent.

   It comes from the padding, which is the cheapest thing in the row: the tap
   target is set by min-height, not by this, so 4px instead of 10px costs
   nothing in reach and returns 156px. The 2px list gap and a little of the
   separator margin return the rest.

   Measured in a browser at the 1440px the rail caps at, not estimated. The
   nav is handed 1014px. Thirteen text-only links needed 950px; the same
   thirteen with a 13px mark and a 5px gap need 990px. 24px spare.

   nowrap is doing real work and is not tidiness. Without it a link is a flex
   item that shrinks below its content, so an over-budget rail does not clip
   or scroll: it silently breaks "My requests" across two lines inside a 63px
   header and every measurement above reads as fitting. With it, going over
   budget shows up as the scroller, which is at least visible. If a label
   here gets longer, re-measure. */
.rail__inner > .nav .nav__link { padding-inline: 4px; white-space: nowrap; }
.rail__inner > .nav .nav__list { gap: 0; }
.rail__inner > .nav .nav__sep  { margin-inline: 0.22rem; }

.nav__link .icon {
  /* Under the base size. The rail sets 13px type and this is the one place in
     the interface where the marks come thirteen in a row, so they sit inside
     the line rather than leading it, and every tenth of an em is 13px of
     header that the labels need more. */
  width: 1em; height: 1em;
  margin-right: 0.38em;
  /* A hair under the label. At full strength thirteen marks in a row read as
     a texture and the words stop being the first thing seen. */
  opacity: 0.72;
  transition: opacity var(--fx-dur) var(--fx-ease), color var(--fx-dur) var(--fx-ease);
}
.nav__link:hover .icon { opacity: 1; }
/* The current item is already marked by brighter ink and the cyan underline.
   Tinting its mark to match ties the two together rather than adding a third
   signal. Colour is not carrying anything alone here: the word is right there
   and the underline is the primary mark. */
.nav__link[aria-current="page"] .icon { opacity: 1; color: var(--fx-color-cyan); }

/* ---- icons in the mobile drawer --------------------------------------------
   The drawer has width to spare, so the marks get a real gap and act as the
   left margin of the list. */
.navdrawer a .icon { margin-right: 0.7em; opacity: 0.75; }
.navdrawer a[aria-current="page"] .icon { opacity: 1; color: var(--fx-color-cyan); }

/* A stroked mark at 13px is thin enough that a print driver can drop it, and
   the rail does not print anyway. Left alone deliberately. */

/* ==========================================================================
   LIST-SCREEN COPY. Appended 2026-08-28. Nothing above this line is edited.
   ==========================================================================
   Every list screen now carries two lines under its h1 instead of one: a
   standing sentence saying what the screen holds, and the count, which moves
   when a filter moves. They need different weights or they read as one
   run-on paragraph and neither gets seen.

   The empty states themselves reuse .empty / .empty h2 / .empty p / .empty .row
   from the empty-state block further up. Nothing new was needed there. */

.pagehead__d {
  /* A measure, so the sentence wraps before it becomes a line somebody skips. */
  max-width: 68ch;
}

.pagehead__count {
  /* Quieter than the description above it: this is the changing number, not
     the thing that tells a new person what they are looking at. */
  font-size: var(--fx-text-sm);
}

/* ----------------------------------------------------- tables on a phone --
   A data table is 999px of columns in a 341px viewport. The scroller keeps the
   page from breaking, which is not the same as the table being usable: you
   scroll sideways to read a single row, and every row link is 17px tall, well
   under a thumb.

   Below 46rem each row becomes a card. The header is removed from view but not
   from the accessibility tree, and every cell carries its column name from the
   data-label the shared template now writes, so a value is never orphaned from
   what it means.

   Deliberately NOT display:block on the table elements. That drops the rows out
   of the table role and a screen reader stops announcing them as data. Grid on
   the row keeps the semantics and still stacks. */
@media (max-width: 46rem) {
  .table-scroll { overflow-x: visible; border: 0; }

  /* Off-screen rather than display:none, so the header still names the columns
     for anyone navigating the table by its structure. */
  .table thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }

  .table, .table tbody { display: block; width: 100%; }

  .table tbody tr {
    display: grid;
    grid-template-columns: minmax(6.5rem, auto) minmax(0, 1fr);
    gap: 0 var(--fx-space-3);
    padding: var(--fx-space-3) var(--fx-space-4);
    margin-bottom: var(--fx-space-3);
    border: 1px solid var(--fx-color-border);
    border-left: var(--fx-spine) solid var(--fx-color-border-strong);
    border-radius: var(--fx-radius-md);
    background: var(--fx-color-surface);
  }
  /* The status spine survives, because it is how state reads at a glance and
     the whole interface uses it. */
  .table tbody tr[class*="row--"] { border-left-color: currentColor; }

  .table tbody td {
    display: grid;
    grid-template-columns: subgrid;
    grid-column: 1 / -1;
    align-items: baseline;
    padding: var(--fx-space-1) 0;
    border: 0;
    text-align: left;
  }
  .table tbody td::before {
    content: attr(data-label);
    font-size: var(--fx-text-xs);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--fx-color-muted);
    font-weight: 600;
  }
  /* A cell with no label is a control column; let it use the full width. */
  .table tbody td:not([data-label])::before { content: none; }
  .table tbody td.num { text-align: left; }

  /* The first cell is the row's identity, so it reads as a heading rather than
     as another labelled field. */
  .table tbody td:first-child { grid-template-columns: 1fr; padding-top: 0; }
  .table tbody td:first-child::before { content: none; }
  .table tbody td:first-child a,
  .table tbody td:first-child { font-size: var(--fx-text-md); font-weight: 600; }

  /* Thumbs, not cursors. The row link was 17px tall. */
  .table tbody td a { display: inline-block; min-height: 2.75rem; line-height: 2.75rem; }
  .table tbody td:not(:first-child) a { min-height: 0; line-height: inherit; }
}

/* Subgrid is recent. Without it the label and value stack instead of sitting
   side by side, which is longer but still correct and still labelled. */
@supports not (grid-template-columns: subgrid) {
  @media (max-width: 46rem) {
    .table tbody tr { display: block; }
    .table tbody td { display: block; }
  }
}

/* The min-width that makes a data table readable on a desktop is exactly what
   stops it collapsing on a phone. Once the rows are cards there are no columns
   left to protect, so it goes. Appended after the stacking block so it wins
   without editing the desktop rule. */
@media (max-width: 46rem) {
  .table, .table--wide, .maptable { min-width: 0; width: 100%; }
}


/* ===========================================================================
   THE COLOUR BLOCK, AND THE MATERIAL IT IS MADE OF
   Appended 2026-08-28. Nothing above this line is edited by this block.
   ---------------------------------------------------------------------------
   THE SPINE IS NOW A BLOCK. It was a 3px border on the left edge of a panel,
   row or card. That was right when the corners were 2-3px and wrong the moment
   they went to 20px, because a border follows the corner arc: a 3px left edge
   against a 20px radius tapers to a crescent at both ends and reads as a
   rendering fault rather than as a decision.

   So it is drawn instead as a solid colour block, inset from the card's left
   edge, running the height of the card's content, square-ish on the left and
   properly rounded on the right. 8px wide on a card, 6px in a dense row. It
   reads as a deliberate colour code — a tab on a file — instead of a hairline.

   HOW IT PICKS UP ITS COLOUR, which is the only clever thing here and is worth
   understanding before anyone edits it. The block is a pseudo-element with a
   zero-width box and a thick left BORDER, and that border takes
   `border-left-color: inherit`. `inherit` reads the parent's computed
   border-left-color — which still resolves even though the parent's
   border-left-WIDTH is zero and nothing is painted there.

   The pay-off: all forty-odd state modifiers scattered through this file
   (.panel--bad, .kcard--at_risk, .ck__i--warn, .dcal__i--ok, .row-bad, and so
   on) still say `border-left-color: <token>` and still work, untouched. Nothing
   had to be renamed and nothing can fall out of step. Set a state modifier's
   border-left-color and the block follows.

   TWO RULES FOR ANYONE ADDING A HOST:
     1. The host needs `position: relative` and enough padding-left to clear the
        block (about 1.25rem for a card, 1.125rem for a dense row).
     2. The host's DEFAULT border-left-color must be `transparent`, or every
        instance wears a grey tab whether it has a state or not.

   STATE IS STILL NEVER COLOUR ALONE. The block is an addition to the word, not
   a replacement for it — every card that carries one still names its state in
   text, which is what keeps this readable in greyscale, to a colour-blind
   reader and to a screen reader.

   MATERIAL. The block casts a small shadow so it sits above the card face
   rather than being painted onto it, and its corner treatment matches the
   card's own. It deliberately carries no gradient: at 8px wide a vertical
   gradient is invisible, and an invisible gradient is a rendering cost with no
   design return.

   CONTRAST. The block is a UI boundary carrying meaning, so every colour it can
   take is gated at 3:1 against all three surfaces it can sit on — the panel
   white, the page stone and the inset tint — in both themes. scripts/contrast.js
   checks the full matrix; see the "spine / colour block" rows.
   =========================================================================== */

:root {
  --fx-block-w:        8px;    /* on a card */
  --fx-block-w-dense:  6px;    /* in a table row or a list line */
  --fx-block-inset:    5px;    /* from the host's left edge */
  --fx-block-inset-dense: 4px;
  /* left corners nearly square, right corners properly round. The two radii on
     one horizontal edge must sum to the block's width or the browser scales
     every corner down proportionally, which is how these end up looking like
     nothing at all: 2 + 6 = 8, and 2 + 4 = 6. */
  --fx-block-radius:       2px 6px 6px 2px;
  --fx-block-radius-dense: 2px 4px 4px 2px;
  --fx-block-shadow: 0 1px 3px rgba(26, 28, 23, 0.22);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --fx-block-shadow: 0 1px 4px rgba(0, 0, 0, 0.55); }
}

/* ---- the hosts ---------------------------------------------------------- */
.panel, .stat, .kcard, .ck__i, .msg, .alert, .filters, .fr, .trk__now, .trkl__i,
.miss__i, .dcal__i, .ddates, .commitbar, .dbanner, .due__link, .due__clear,
.ck__gate, .ck__add > form, .cal__d, .cal__p, .creative, .metric, .inbox__i,
.ochain__i, .ocard, .mail-row, .mail-msg, .navdrawer a {
  position: relative;
}

.panel::before, .stat::before, .kcard::before, .ck__i::before, .msg::before,
.alert::before, .filters::before, .fr::before, .trk__now::before,
.trkl__i::before, .miss__i::before, .dcal__i::before, .ddates::before,
.commitbar::before, .dbanner::before, .due__link::before, .due__clear::before,
.ck__gate::before, .ck__add > form::before, .cal__d::before, .cal__p::before,
.creative::before, .metric::before, .inbox__i::before, .ochain__i::before,
.ocard::before, .mail-row::before, .mail-msg::before, .navdrawer a::before {
  content: "";
  position: absolute;
  left: var(--fx-block-inset);
  top: var(--fx-space-3);
  bottom: var(--fx-space-3);
  width: 0;
  border-left: var(--fx-block-w) solid;
  border-left-color: inherit;          /* the whole mechanism, in one line */
  border-radius: var(--fx-block-radius);
  box-shadow: var(--fx-block-shadow);
  pointer-events: none;
}

/* Dense hosts: a list line or a drawer row is 44px tall, so an 8px block with
   10px of inset top and bottom leaves 24px of colour and reads as a smudge. */
.due__link::before, .navdrawer a::before, .metric::before, .ddates::before,
.mail-row::before, .inbox__i::before, .ck__gate::before {
  left: var(--fx-block-inset-dense);
  top: var(--fx-space-2);
  bottom: var(--fx-space-2);
  border-left-width: var(--fx-block-w-dense);
  border-radius: var(--fx-block-radius-dense);
}

/* Room for it. These are the hosts whose padding-left was tighter than the
   block, so their content would have run underneath it. */
.kcard, .dcal__i, .miss__i, .ochain__i, .ocard, .creative, .cal__p, .cal__d {
  padding-left: 1.25rem;
}
.ddates, .metric, .ck__gate, .inbox__i, .mail-row, .navdrawer a, .due__link {
  padding-left: 1.125rem;
}

/* ---- the horizontal version, for the things spined across the top -------- */
/* Same trick rotated: zero-height box, thick top border, colour inherited. Used
   where the state belongs to a whole column rather than to one card. */
.kcol, .auth__panel, .commitbar { position: relative; }
.kcol::after, .auth__panel::after {
  content: "";
  position: absolute;
  left: var(--fx-space-4); right: var(--fx-space-4); top: var(--fx-space-2);
  height: 0;
  border-top: var(--fx-block-w-dense) solid;
  border-top-color: inherit;
  border-radius: 2px 2px 4px 4px;
  box-shadow: var(--fx-block-shadow);
  pointer-events: none;
}
.kcol__head { padding-top: var(--fx-space-4); }
.auth__panel .panel__body { padding-top: var(--fx-space-6); }

/* ---- the data table ------------------------------------------------------ */
/* Above the phone breakpoint the row spine moves off the cell border and onto
   the same colour block, drawn inside the first cell. `position: relative` on a
   td is dependable; on a tr it is not, which is why the block lives on the
   cell. */
@media (min-width: 46.0625rem) {
  .table tbody td:first-child { position: relative; padding-left: 1.25rem; }
  .table tbody tr td:first-child { border-left-width: 0; }
  .table tbody td:first-child::before {
    content: "";
    position: absolute;
    left: var(--fx-block-inset-dense);
    top: var(--fx-space-2); bottom: var(--fx-space-2);
    width: 0;
    border-left: var(--fx-block-w-dense) solid;
    border-left-color: inherit;
    border-radius: var(--fx-block-radius-dense);
    box-shadow: var(--fx-block-shadow);
    pointer-events: none;
  }
  .table thead th:first-child { padding-left: 1.25rem; }
}

/* ===========================================================================
   SURFACES, ROUNDED AND RAISED
   ---------------------------------------------------------------------------
   The 2026-08-26 elevation block set 10px corners and a single shadow on four
   components. This supersedes it: the scale is 10 / 14 / 20 / 28 now, and depth
   is a three-offset shadow plus a 1px inner highlight along the top edge.

   The rule for USING it has not changed and is the thing that keeps a page
   readable: a shadow means "this is above the page". It goes on surfaces that
   float, on the primary action, and on anything the whole of which is a link.
   A page where everything is raised has nothing raised.
   =========================================================================== */

.panel, .stat, .kcard, .ck__i, .trk__now, .trkl__i, .fr, .doc, .table-scroll,
.commitbar, .drop, .empty, .kcol, .filters, .alert, .msg, .dcal__i, .miss__i,
.ocard, .ochain__i, .creative, .inbox__i, .mail-row, .mail-msg, .cal__d {
  border-radius: var(--fx-radius-lg);
}
.ddates, .ck__gate, .cal__p, .metric, .dbanner, .domains li, .msg__body pre {
  border-radius: var(--fx-radius-md);
}
.doc, .trk__now, .fr { border-radius: var(--fx-radius-xl); }

/* Raised: lighter than the page, a layered shadow, a lit top edge. */
.panel, .stat, .kcard, .ck__i, .trkl__i, .fr, .doc, .table-scroll, .commitbar,
.ocard, .mail-msg {
  box-shadow: var(--fx-shadow-1), var(--fx-hi);
}
/* Inset: belongs to the page rather than floating on it. No shadow at all —
   half a shadow on a flat thing is worse than none. */
.panel--flat, .filters, .empty, .drop, .kcol, .ddates, .ck__gate, .panel__foot {
  box-shadow: none;
}
/* Genuinely floating. */
.menu, .who__menu, .pop, .dropdown__menu { box-shadow: var(--fx-shadow-3), var(--fx-hi); }
.due { box-shadow: var(--fx-shadow-2), var(--fx-hi); border-radius: var(--fx-radius-lg); }

/* A rounded container clips its own children, or a child's square corner pokes
   through the parent's curve — most visible on a table's first header cell.
   NOT applied to .table-scroll: that element scrolls horizontally on purpose
   and overflow:hidden would kill the scroll on every wide table here. */
.panel, .trkl__i, .trk__now, .kcard, .kcol, .fr, .ocard { overflow: hidden; }

/* Whole-surface links lift. Nothing else does — the lift is the affordance. */
.trkl__i, .ocard, .mail-row, .inbox__i, .dbanner {
  transition: box-shadow var(--fx-dur-2) var(--fx-ease),
              border-color var(--fx-dur-1) var(--fx-ease),
              background-color var(--fx-dur-1) var(--fx-ease),
              transform var(--fx-dur-2) var(--fx-ease-out);
}
.trkl__i:hover, .ocard:hover, .mail-row:hover, .inbox__i:hover, .dbanner:hover {
  box-shadow: var(--fx-shadow-2), var(--fx-hi);
  transform: translateY(-2px);
}
.trkl__i:active, .ocard:active, .mail-row:active, .inbox__i:active, .dbanner:active {
  transform: translateY(0);
}
.kcard {
  transition: box-shadow var(--fx-dur-2) var(--fx-ease),
              transform var(--fx-dur-2) var(--fx-ease-out);
}
.kcard:hover { box-shadow: var(--fx-shadow-2), var(--fx-hi); transform: translateY(-2px); }

/* ---- the table ---------------------------------------------------------- */
.table-scroll { border: 1px solid var(--fx-color-border); background: var(--fx-color-bg); }
.table thead th {
  background: var(--fx-color-surface-2);
  font-size: var(--fx-text-xs); font-weight: var(--fx-w-bold);
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--fx-color-muted);
}
.table tbody tr {
  transition: background-color var(--fx-dur-1) var(--fx-ease);
}
.table tbody tr:hover { background: var(--fx-color-primary-tint); }

/* ---- controls ----------------------------------------------------------- */
.input, .select, .textarea {
  border-radius: var(--fx-radius-md);
  background: var(--fx-color-bg);
  box-shadow: inset 0 1px 2px rgba(26, 28, 23, 0.05);
  transition: border-color var(--fx-dur-1) var(--fx-ease),
              box-shadow var(--fx-dur-2) var(--fx-ease);
}
.input:focus-visible, .select:focus-visible, .textarea:focus-visible {
  border-color: var(--fx-color-focus);
  box-shadow: inset 0 1px 2px rgba(26, 28, 23, 0.04);
}
.trio__o, .ck__add > summary, .pager a, .pager span, .drop input[type="file"] {
  border-radius: var(--fx-radius-pill);
}
.pager a, .pager span { min-height: 40px; }
.pager a {
  background: var(--fx-color-bg); box-shadow: var(--fx-shadow-1), var(--fx-hi);
  transition: border-color var(--fx-dur-1) var(--fx-ease),
              box-shadow var(--fx-dur-2) var(--fx-ease),
              transform var(--fx-dur-2) var(--fx-ease-out);
}
.pager a:hover { border-color: var(--fx-color-primary); transform: translateY(-1px); box-shadow: var(--fx-shadow-2); }
.pager [aria-current="page"] {
  background-image: var(--fx-grad-primary); background-color: var(--fx-color-primary);
  color: var(--fx-color-on-primary); border-color: var(--fx-color-primary-deep);
}
.drop { border-radius: var(--fx-radius-xl); }
.ck__n, .fr__mark { box-shadow: var(--fx-hi-soft); }
.fr__s--done .fr__mark {
  border-color: var(--fx-color-ok); background: var(--fx-color-ok);
  color: var(--fx-color-on-ok);   /* was a hardcoded #fff, 1.8:1 in dark mode */
}
.meter, .prog__bar { border-radius: var(--fx-radius-pill); }
.meter i, .prog__bar i { border-radius: var(--fx-radius-pill); }
.prog__bar i { background: var(--fx-color-primary); }
.funnel__bar { border-radius: var(--fx-radius-pill); height: 0.625rem; }
.funnel__seg { border-radius: var(--fx-radius-sm); }
.funnel__seg:hover .funnel__bar { filter: none; background: var(--fx-color-primary-hover); }

/* ===========================================================================
   TYPE, APPLIED
   ---------------------------------------------------------------------------
   The scale is declared at the top of the file; this is where the roles are
   assigned. Two jobs: give the display face a weight that reads as display now
   that Anton is gone, and stop the page from having four things at almost the
   same size.
   =========================================================================== */
.brand__name, .stat__v, .doc__brand, .creator__title, .fr__t, .funnel__n {
  font-family: var(--fx-font-display); font-weight: var(--fx-w-black);
  letter-spacing: var(--fx-track-display);
}
.pagehead__t h1 {
  font-size: var(--fx-text-3xl);
  font-weight: var(--fx-w-black);
  letter-spacing: var(--fx-track-display);
  line-height: var(--fx-lh-display);
  text-wrap: balance;
}
.pagehead__t p, .pagehead__d {
  font-size: var(--fx-text-md); line-height: 1.55;
  color: var(--fx-color-muted); max-width: 68ch; text-wrap: pretty;
}
.pagehead__count { font-size: var(--fx-text-sm); }
.panel__head > h2, .panel__head > h3 {
  font-family: var(--fx-font-display);
  font-size: var(--fx-text-lg); font-weight: var(--fx-w-bold);
  letter-spacing: -0.014em; line-height: var(--fx-lh-head);
  color: var(--fx-color-ink);
}
/* Labels are read, not announced. Sentence case, quiet, and the VALUE carries
   the weight — the 2026-08-21 pass established this and the new scale keeps it. */
.field__label, .kv dt, .label {
  font-family: var(--fx-font-ui); font-size: var(--fx-text-sm); font-weight: var(--fx-w-med);
  letter-spacing: 0; text-transform: none; color: var(--fx-color-muted);
}
/* Column headers keep their case: in a data table the convention aids scanning,
   which is the opposite of its effect on a form label. */
.table th { font-size: var(--fx-text-xs); letter-spacing: 0.06em; }
.table { font-size: var(--fx-text-sm); }
.due__head h2, .creator__sec h2, .kcol__head h2, .msg__who, .cites__h, .due__kind {
  font-size: var(--fx-text-xs); font-weight: var(--fx-w-bold);
  letter-spacing: var(--fx-track-eyebrow); text-transform: uppercase;
  color: var(--fx-color-muted);
}
.src { font-size: var(--fx-text-2xs); }
.mono, .num { font-variant-numeric: tabular-nums; }

/* ===========================================================================
   DENSITY
   ---------------------------------------------------------------------------
   The space ramp came in about 12% at every step, which does most of this. What
   is left is the places that had hardcoded a value instead of using the ramp.
   =========================================================================== */
.main { padding: var(--fx-space-5) var(--fx-gutter) var(--fx-space-7); }
.pagehead { padding-bottom: var(--fx-space-4); margin-bottom: var(--fx-space-5); gap: var(--fx-space-4); }
.table th, .table td { padding: var(--fx-space-3) var(--fx-space-4); line-height: 1.45; }
.grid { gap: var(--fx-space-4); }
.stack > * + * { margin-top: var(--fx-space-4); }
.footer { padding: var(--fx-space-5) var(--fx-gutter); font-size: var(--fx-text-xs); }

/* ===========================================================================
   REDUCED MOTION
   ---------------------------------------------------------------------------
   The block near the top of this file collapses every transition and animation
   to 0.001ms. That is necessary and not sufficient: a hover transform with no
   transition still JUMPS the element two pixels, which is exactly the vestibular
   trigger the preference exists to avoid. So every transform and every shadow
   change is removed outright here, and state is carried by colour and border
   alone. Nothing moves and nothing is lost.
   =========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
  .btn:hover, .btn:active,
  .btn--primary:hover, .btn--primary:active,
  .btn--ghost:hover, .btn--ghost:active,
  .btn--accent:hover, .btn--accent:active,
  .btn--danger:hover, .btn--danger:active,
  .chipbar a:hover, .chipbar button:hover,
  .chipbar a:active, .chipbar button:active,
  .pager a:hover, .rail__toggle:hover, .rail__toggle:active,
  a.stat:hover, a.stat:active, .kcard:hover,
  .trkl__i:hover, .ocard:hover, .mail-row:hover, .inbox__i:hover, .dbanner:hover,
  .trkl__i:active, .ocard:active, .mail-row:active, .inbox__i:active, .dbanner:active,
  .brand:hover .brand__mark {
    transform: none !important;
  }
  /* Shadows stop being animated: they hold whatever the rest state gave them,
     so hover still changes colour and border and nothing pulses. */
  .btn--primary:hover, .btn--ghost:hover, .btn--accent:hover, .btn--danger:hover,
  .chipbar a:hover, .chipbar button:hover, .pager a:hover, .rail__toggle:hover,
  a.stat:hover, .kcard:hover, .trkl__i:hover, .ocard:hover, .mail-row:hover,
  .inbox__i:hover, .dbanner:hover {
    box-shadow: var(--fx-shadow-1), var(--fx-hi);
  }
  /* The nav and tab markers stop scaling and simply are, or are not, there. */
  .nav__link::after, .subnav a::after { transform: scaleX(1); }
  .nav__link:hover::after, .subnav a:hover::after { opacity: 0.32; }
}

/* ===========================================================================
   THE COLOUR BLOCK ON A PHONE
   ---------------------------------------------------------------------------
   Below 46rem the table-stacking block turns every row into a card. Those cards
   carried the old border-left version of the spine, so they get the block too —
   appended AFTER that block rather than edited into it, because the stacking
   rules are load-bearing and are left exactly as they were.

   The desktop treatment is switched off here: it lives on td:first-child, and
   on a phone that cell is the row's title rather than its left edge.
   =========================================================================== */
@media (max-width: 46rem) {
  .table tbody tr {
    position: relative;
    padding-left: 1.375rem;
    border-left-width: 0;
    border-left-color: transparent;
  }
  /* The desktop cell block, off. */
  .table tbody td:first-child { position: static; padding-left: 0; }
  .table tbody td:first-child::before { content: none; }

  .table tbody tr::before {
    content: "";
    position: absolute;
    left: var(--fx-block-inset);
    top: var(--fx-space-3); bottom: var(--fx-space-3);
    width: 0;
    border-left: var(--fx-block-w) solid;
    border-left-color: inherit;
    border-radius: var(--fx-block-radius);
    box-shadow: var(--fx-block-shadow);
    pointer-events: none;
  }
  /* The row's state classes are on the tr, so they colour the block directly.
     Named rather than matched with [class*="row-"], because .row-muted and
     .row--new are not states and must not paint one. */
  .table tbody tr.row-ok   { border-left-color: var(--fx-color-ok); }
  .table tbody tr.row-warn { border-left-color: var(--fx-color-warn-solid); }
  .table tbody tr.row-bad  { border-left-color: var(--fx-color-bad-solid); }
  .table tbody tr.row-dup  { border-left-color: var(--fx-color-neutral); }
  .table tbody tr { border-radius: var(--fx-radius-lg); box-shadow: var(--fx-shadow-1), var(--fx-hi); }
}

/* Same state map for the desktop cell block. The existing .row-* rules further
   up already set border-left-color on td:first-child, so nothing is needed
   there; this only re-states the tr-level colour for the phone card above. */

/* ---- the nav budget, restated for 13px sentence case ----------------------
   The measurement further up this file was taken with 12px uppercase labels at
   0.08em tracking. Sentence case at 13px is NARROWER than that — capitals run
   about 15% wider per glyph before the tracking is counted — so the same twelve
   items fit with room to spare, and the padding can go back up from the 4px
   the old rail had been squeezed to. Re-measure if a label gets longer; the
   scroller is still there as the visible failure mode. */
/* MEASURED IN A BROWSER AT 1920px, not estimated. .rail__inner caps at 1440px,
   so the header never grows past that however wide the monitor is; inside it
   the brand, the account control, the gutters and the gaps leave the nav
   1062px. Thirteen items at 8px of side padding needed 1121px, which put the
   rail into its horizontal scroller and clipped the last destination — the
   exact failure the original measurement existed to prevent.
   6px of padding returns 52px, closing the list gap returns 13, and trimming
   the group separators and the icon gap return the rest: 1032px against 1062
   available, with 30px spare for a longer label. Re-measure if one is added. */
.rail__inner > .nav a { padding-inline: 6px; }
.rail__inner > .nav .nav__link { padding-inline: 6px; white-space: nowrap; }
.rail__inner > .nav .nav__list { gap: 0; }
.rail__inner > .nav .nav__sep  { margin-inline: 0.18rem; background: var(--fx-color-rail-line); }
.rail__inner > .nav .nav__link .icon { margin-right: 0.32em; }
/* The current-marker is inset by the padding, so it shrinks with it. */
.rail__inner > .nav .nav__link::after { left: 4px; right: 4px; }
.nav__link .icon { opacity: 0.6; margin-right: 0.4em; }
.nav__link:hover .icon { opacity: 0.95; }
.nav__link[aria-current="page"] .icon { opacity: 1; color: var(--fx-color-primary); }
.navdrawer a[aria-current="page"] .icon { color: var(--fx-color-primary); }

/* ---- the demonstration banner ---------------------------------------------
   Re-hued to brass with a near-black label. Still its own pair, still never
   pointed at --fx-color-ink, and still deliberately NOT overridden in the dark
   block: the background is the same colour in both themes, and dark-mode ink on
   it measures 2.4:1. The fixed dark label is 7.5:1 in both. */
:root {
  --fx-color-demobar-bg:  #d2a53f;
  --fx-color-demobar-ink: #241a03;
}
.demobar {
  border-bottom: 0;
  font-weight: var(--fx-w-med);
  padding: 0.5rem var(--fx-gutter);
}

/* ---- no horizontal page scroll, at any of the four widths ----------------- */
/* The block adds padding to hosts that were already tight at 375px. A grid or
   flex ITEM defaults to min-width:auto, so any of them can push its parent
   past the viewport; this is the same guard the 2026 responsive pass applied,
   restated after the new padding. */
.grid > *, .stack > *, .kcol, .kcard, .panel { min-width: 0; }
.table-scroll { max-width: 100%; contain: paint; }

/* ---- one primary per action group -----------------------------------------
   The rule the owner asked for, enforced where it can be enforced from a
   stylesheet. In a page-level action bar, a primary button that has another
   primary after it steps down to the ghost treatment, so the LAST one — which
   is the rightmost, and by the convention in these templates the real one —
   keeps the accent.

   This is a safety net, not a substitute for the markup being right. Several
   screens still declare more than one .btn--primary in a single group
   (deal-detail, offers, workflow-board, proposal-edit, clients-list,
   deals-board, mail-inbox); those templates are outside this pass and should be
   corrected at source, at which point this rule simply stops matching anything.

   Scoped to .pagehead__actions on purpose. A blanket sibling rule would demote
   the submit button of a filter form sitting beside an unrelated action, and a
   demoted submit button is a worse bug than a second accent. */
.pagehead__actions .btn--primary:has(~ .btn--primary) {
  background-image: none;
  background-color: var(--fx-color-bg);
  border-color: var(--fx-color-border-strong);
  color: var(--fx-color-ink);
  box-shadow: var(--fx-shadow-1), var(--fx-hi);
}
.pagehead__actions .btn--primary:has(~ .btn--primary):hover {
  border-color: var(--fx-color-primary);
  color: var(--fx-color-accent-ink);
  background-color: var(--fx-color-primary-tint);
  box-shadow: var(--fx-shadow-2), var(--fx-hi);
}

/* ---- the login screen -----------------------------------------------------
   .main--plain centres a single panel on the page ground. With the panel now
   lighter than the ground and carrying a real shadow, it reads as a card on a
   surface rather than a box drawn on a page, which is most of what a sign-in
   screen has to do. */
.auth__panel { box-shadow: var(--fx-shadow-3), var(--fx-hi); }
.auth__brand .brand__name { font-size: 2.25rem; letter-spacing: 0.005em; }

/* ---- the kanban board ----------------------------------------------------- */
.kcol__body { padding: var(--fx-space-3); gap: var(--fx-space-3); }
.kanban { gap: var(--fx-space-4); }

/* ---- the timeline and citation marks, re-hued off the retired cyan -------- */
.tl li::before { background: var(--fx-color-brass-solid); width: 8px; height: 8px; left: -4.5px; }
.cites li::before { color: var(--fx-color-accent-ink); }

/* ---- 375px ----------------------------------------------------------------
   The colour block added padding to hosts that were already tight on the
   narrowest common handset. Nothing here may introduce a horizontal scroll, so
   the gutter and the block both come in a step. */
@media (max-width: 24rem) {
  :root { --fx-block-inset: 4px; --fx-block-w: 6px; --fx-block-radius: 2px 4px 4px 2px; }
  .kcard, .dcal__i, .miss__i, .ochain__i, .ocard, .creative, .cal__p, .cal__d { padding-left: 1rem; }
  .pagehead__t h1 { font-size: 1.875rem; letter-spacing: -0.026em; }
}


/* ===========================================================================
   THE BLOCK, FLUSH — and the material it belongs to
   Appended 2026-08-28, after the phone review. Nothing above is edited here.
   ---------------------------------------------------------------------------
   The block was inset from the card's left edge with a rounded left side, so it
   read as a capsule floating NEAR the card rather than a tab attached to it —
   most obviously on a flash note, where the gap on its left was the first thing
   the eye found.

   It is now flush: zero inset, square on the left, rounded only on the right.
   Its left corners are not declared at all — the host clips it, so it inherits
   the card's own radius exactly and nothing can poke out of the curve. That is
   also why every host below carries `overflow: hidden`; without it the block
   would square off the corner it is supposed to follow.

   WHY IT IS NOT ON EVERYTHING. A coloured bar repeated down a page of rounded
   cards is the most recognisable tell of generated work, and it is also just
   bad information design: a marker that appears on every card marks nothing.
   The block appears only where a state exists. Panels, stat tiles, filter bars,
   the sign-in card and disclosure forms have no state, so they have no block.

   AND IT IS NEVER THE ONLY SIGNAL. Every card that carries one names its state
   in words in the same card — which is what keeps this readable in greyscale,
   to a colour-blind reader, and to a screen reader that gets no colour at all.
   =========================================================================== */

:root {
  --fx-block-w:        10px;
  --fx-block-w-dense:  6px;
  --fx-block-radius:       0 6px 6px 0;
  --fx-block-radius-dense: 0 4px 4px 0;
}

.panel, .stat, .kcard, .ck__i, .msg, .alert, .filters, .fr, .trk__now, .trkl__i,
.miss__i, .dcal__i, .ddates, .commitbar, .dbanner, .due__link, .due__clear,
.ck__gate, .ck__add > form, .cal__d, .cal__p, .creative, .metric, .inbox__i,
.ochain__i, .ocard, .mail-row, .mail-msg, .navdrawer a {
  overflow: hidden;
}

.panel::before, .stat::before, .kcard::before, .ck__i::before, .msg::before,
.alert::before, .filters::before, .fr::before, .trk__now::before,
.trkl__i::before, .miss__i::before, .dcal__i::before, .ddates::before,
.commitbar::before, .dbanner::before, .due__link::before, .due__clear::before,
.ck__gate::before, .ck__add > form::before, .cal__d::before, .cal__p::before,
.creative::before, .metric::before, .inbox__i::before, .ochain__i::before,
.ocard::before, .mail-row::before, .mail-msg::before, .navdrawer a::before {
  left: 0;
  top: 0;
  bottom: 0;
  border-left-width: var(--fx-block-w);
  border-radius: var(--fx-block-radius);
  /* No shadow. Flush against the card face there is no gap for one to fall
     into, and a shadow on a shape with no separation just muddies its edge. */
  box-shadow: none;
}
.due__link::before, .navdrawer a::before, .metric::before, .ddates::before,
.mail-row::before, .inbox__i::before, .ck__gate::before {
  left: 0; top: 0; bottom: 0;
  border-left-width: var(--fx-block-w-dense);
  border-radius: var(--fx-block-radius-dense);
}

/* Room for it. A 10px block plus breathing space is 1.5rem of left padding on a
   card and 1.25rem in a dense row; below that the first character sits on the
   colour. */
.kcard, .dcal__i, .miss__i, .ochain__i, .ocard, .creative, .cal__p, .cal__d,
.alert, .msg, .ck__i, .commitbar, .dbanner, .filters {
  padding-left: 1.5rem;
}
.ddates, .metric, .ck__gate, .inbox__i, .mail-row, .navdrawer a, .due__link {
  padding-left: 1.25rem;
}
.panel__head, .panel__body, .panel__foot { padding-left: 1.5rem; }
@media (min-width: 760px) {
  .panel__head, .panel__body, .panel__foot { padding-left: var(--fx-space-5); }
}

/* The data table. Rows are 44px and consecutive, so the block keeps a 3px top
   and bottom inset — flush top to bottom would weld one row's code to the
   next's into a single continuous stripe and stop being per-row. */
@media (min-width: 46.0625rem) {
  .table tbody td:first-child::before {
    left: 0; top: 3px; bottom: 3px;
    border-left-width: var(--fx-block-w-dense);
    border-radius: var(--fx-block-radius-dense);
    box-shadow: none;
  }
  .table tbody td:first-child { padding-left: 1.25rem; }
  .table thead th:first-child { padding-left: 1.25rem; }
}
@media (max-width: 46rem) {
  .table tbody tr { overflow: hidden; padding-left: 1.5rem; }
  .table tbody tr::before {
    left: 0; top: 0; bottom: 0;
    border-left-width: var(--fx-block-w);
    border-radius: var(--fx-block-radius);
    box-shadow: none;
  }
}

/* ---- the engraved keyline -------------------------------------------------
   A printed deed has a rule set in from its edge. Exactly two surfaces get one:
   the proposal document and the first-run card — the two things in this system
   that ARE documents rather than screens. Putting it on every panel would make
   it wallpaper and would flatten the density variation it exists to create. */
.doc, .fr { position: relative; }
.doc::after, .fr::after {
  content: "";
  position: absolute; inset: 7px;
  border: 1px solid var(--fx-color-keyline);
  border-radius: calc(var(--fx-radius-xl) - 7px);
  pointer-events: none;
}
.doc { padding-inline: clamp(1.5rem, 0.6rem + 3vw, 3.5rem); }

/* ---- the felt band --------------------------------------------------------
   The rail and the drawer are the only felt in the interface. Icons on it take
   the lit brass when current, and the rail's focus ring is the one place the
   second ring colour is used. */
.nav__link .icon { opacity: 0.7; }
.nav__link:hover .icon { opacity: 1; }
.nav__link[aria-current="page"] .icon { opacity: 1; color: var(--fx-color-rail-accent); }
.navdrawer a[aria-current="page"] .icon { color: var(--fx-color-rail-accent); }
.navdrawer a .icon { opacity: 0.8; }
.navdrawer__h { color: var(--fx-color-rail-muted); }
.rail :focus-visible, .navdrawer :focus-visible { outline-color: var(--fx-color-focus-on-dark); }

/* The h1 of a page, set the way a title card sets a property name: Franklin
   Gothic, tight, and the largest thing on the screen by a clear margin. */
.pagehead__t h1, .fr__t, .creator__title, .auth__brand .brand__name {
  font-family: var(--fx-font-display);
  font-weight: var(--fx-w-bold);
}
.brand__name { font-weight: var(--fx-w-bold); letter-spacing: 0.01em; }

/* ---- the nav between 1280 and 1440 ----------------------------------------
   The rail caps at 1440px, so above that the nav has 1062px and its thirteen
   items need 1029. Below it the header is narrower than the cap while the item
   count is not: at exactly 1280 the nav gets 890px and falls into its
   horizontal scroller, which clips the last destination — and a menu whose last
   item nobody can see is the fault the drawer breakpoint exists to prevent.

   The marks come off in that band rather than the words. Each icon costs about
   17px of header including its gap, so thirteen of them are 221px — more than
   the 139px shortfall — and a label is worth more than a mark at the width
   where only one of them fits. They return at 1440 where the budget pays for
   both. Nothing is hidden that carries meaning: the icons are decorative and
   already aria-hidden in partials/icon.ejs. */
/* RE-MEASURED after --fx-maxw took the 3PF value of 1280px (it was 1440px).
   The rail caps at the content width, so the nav now gets 917px at ANY viewport
   size, and thirteen labelled items with marks need 1029px. There is no width
   at which both fit, so the marks come off the rail at every desktop size
   rather than in a band. Labels beat marks when only one of them fits, the
   icons are decorative and already aria-hidden, and the drawer keeps them
   because it has the room. 13 items at 6px padding measure 808px against 917. */
@media (min-width: 1280px) {
  .rail__inner > .nav .nav__link .icon { display: none; }
}

/* ===========================================================================
   CARDS, REWORKED — 2026-08-28, after the owner's second look
   ---------------------------------------------------------------------------
   Four faults, all of them mine, all of them the same underlying mistake: the
   flush colour block was bolted on without re-thinking the box it sits in.

   1. UNEVEN INTERNAL PADDING. Making room for the block by pushing only
      padding-left to 1.5rem left every card with 24px on the left and 14px on
      the right. On a phone that is visible without measuring, and it made a
      header and its body look like they belonged to different components.
      Fixed by going back to SYMMETRIC padding everywhere and letting the 10px
      block live inside the left inset with 8px of clearance. No allowance, no
      special case, no card that needs to know whether it has a state.

   2. FRAME INSIDE A FRAME. The elevation pass gave .table-scroll its own 1px
      border and 20px corners, which re-broke the older rule that a scroller
      filling a panel draws no second frame. A bordered box flush inside a
      bordered box is the single thing that stops a card reading as one object.

   3. NESTED RADIUS MATCHING ITS PARENT. A .kcard at 20px inside a .kcol at
      20px, a .ddates at 14px inside a .kcard at 14px. Equal corners read as a
      mistake because the inner curve cannot follow the outer one. There is a
      ladder now and everything sits on a rung of it.

   4. A GREY BLOCK MEANING NOTHING. Hosts that set `border: 1px solid
      var(--fx-color-border)` and never set a left colour were inheriting the
      border grey into the block, so the commit bar and the metric readouts
      wore a tab that encoded no state at all.
   =========================================================================== */

/* ---- 1. symmetric padding ------------------------------------------------- */
.panel__head, .panel__body, .panel__foot {
  padding: var(--fx-space-4);
}
.panel__body--tight { padding: var(--fx-space-3) var(--fx-space-4); }
@media (min-width: 760px) {
  .panel__head, .panel__body, .panel__foot { padding: var(--fx-space-4) var(--fx-space-5); }
  .panel__body--tight { padding: var(--fx-space-3) var(--fx-space-5); }
}
/* Cards that hold their own content rather than delegating to head/body. */
.kcard, .dcal__i, .miss__i, .ochain__i, .ocard, .creative, .cal__p, .cal__d,
.alert, .msg, .ck__i, .commitbar, .dbanner, .filters, .stat {
  padding: 1.125rem;
}
@media (min-width: 760px) {
  .kcard, .dcal__i, .miss__i, .ochain__i, .ocard, .creative, .cal__p, .cal__d,
  .alert, .msg, .ck__i, .commitbar, .dbanner, .filters, .stat {
    padding: 1.25rem;
  }
}
/* Dense rows: a 6px block inside a 14px inset leaves 8px, same clearance as a
   card gets from its 10px block inside 18px. */
.ddates, .metric, .ck__gate, .inbox__i, .mail-row, .navdrawer a, .due__link {
  padding-left: 0.875rem; padding-right: 0.875rem;
}
.due__link { padding-block: var(--fx-space-2); }
.navdrawer a { padding-right: var(--fx-space-3); }

/* ---- 2. one frame per object ---------------------------------------------- */
/* Restated after the elevation pass, which had quietly re-armed the border. */
.panel > .table-scroll,
.panel .table-scroll,
.panel__body > .table-scroll {
  border: 0; border-radius: 0; box-shadow: none; background: none;
}
/* An empty state inside a card is already inside a card. It keeps its dashed
   rule only when it IS the card — standing alone on the page. */
.panel .empty, .panel__body > .empty, .kcol .empty {
  border: 0; background: none; padding: var(--fx-space-6) var(--fx-space-4);
}
/* Same for an alert nested in a panel: it keeps its tint and its block, which
   carry the meaning, and drops the outline, which does not. */
.panel__body > .alert, .panel__body > .alert:last-child { border-color: transparent; }

/* ---- 3. the radius ladder -------------------------------------------------
   region 28 -> card 20 -> card-in-card 14 -> element-in-that 10 -> control 14 /
   pill. A child is always at least one rung below its parent, so no inner curve
   ever has to match an outer one it cannot follow. */
.doc, .fr, .trk__now                       { border-radius: var(--fx-radius-xl); }
.panel, .kcol, .table-scroll, .empty, .drop,
.trkl__i, .mail-msg, .alert, .filters,
.commitbar, .stat, .ck__i, .msg, .ocard    { border-radius: var(--fx-radius-lg); }
.kcard, .dcal__i, .miss__i, .ochain__i,
.inbox__i, .mail-row, .cal__d, .creative,
.dbanner                                   { border-radius: var(--fx-radius-md); }
.ddates, .ck__gate, .cal__p, .metric,
.domains li, .msg__body pre, .chipbar .n   { border-radius: var(--fx-radius-sm); }
/* The block's own right corner steps down with its host so the tab never looks
   rounder than the card it is attached to. */
.kcard::before, .dcal__i::before, .miss__i::before, .ochain__i::before,
.inbox__i::before, .mail-row::before, .cal__d::before, .creative::before,
.dbanner::before { border-radius: 0 5px 5px 0; }
.ddates::before, .ck__gate::before, .cal__p::before, .metric::before {
  border-radius: 0 4px 4px 0;
}

/* ---- 4. no block without a state ------------------------------------------ */
.commitbar, .metric, .cal__d, .creative { border-left-color: transparent; }

/* ---- the phone card ------------------------------------------------------- */
@media (max-width: 46rem) {
  .table tbody tr { padding: var(--fx-space-3) 1.125rem; }
}

/* ---- the ink block on the filter bar --------------------------------------
   `.pagehead, .filters, .sec-head { border-left: 0 }` from the 2026-08-21
   restraint pass is a SHORTHAND, so it resets border-left-color to its initial
   value, which is currentColor — the page's ink. The block reads that colour,
   so every filter bar in the system was wearing a 10px near-black tab that
   encoded nothing. It only appeared once the block got wide enough to see,
   which is why it survived the first pass.

   Longhand from here on. A shorthand that silently re-arms a colour is exactly
   the failure mode the block's inherit trick is otherwise immune to. */
.pagehead, .filters, .sec-head { border-left-width: 0; border-left-color: transparent; }

/* ---- cards that were mostly chrome ---------------------------------------- */
/* A calendar day is a grid cell, not a card. It was picking up the 1.125rem
   card padding and spending 40 of its 112 vertical pixels on air around a date
   and a chip. It also has no state of its own, so it has no block. */
.cal__d {
  padding: var(--fx-space-2) var(--fx-space-3);
  border-radius: var(--fx-radius-sm);
  border-left-color: transparent;
}
.cal__d::before { content: none; }
.cal__p { padding: var(--fx-space-1) var(--fx-space-2); }

/* An empty state was 193px tall around 91px of text, stretched to the full
   1226px column. It is a message, so it is measured like one: the text block
   caps at 48ch and centres, and the box stops being a room. */
.empty {
  padding: var(--fx-space-6) var(--fx-space-5);
  border-style: dashed;
}
.empty h2, .empty h3 { font-size: var(--fx-text-md); margin-bottom: var(--fx-space-1); }
.empty p { max-width: 48ch; }
.empty .row { margin-top: var(--fx-space-4); }
.empty--sm { padding: var(--fx-space-5) var(--fx-space-4); }

/* ---- the tracker row ------------------------------------------------------
   .trkl__i carries the block and .trkl__link carries the padding, so the link's
   inset is what has to clear the block, not the card's. */
.trkl__link { padding-left: 1.125rem; }

/* ---- zebra striping, measured rather than assumed --------------------------
   The stripe was color-mix(surface 55%, bg). In the light theme those two are
   far enough apart that 55% lands at 1.07:1 against a plain row, which is what
   a zebra wants — present when you follow a row across seven columns, absent
   when you are not looking for it.

   In DARK the same formula collapses. #26231a and #1e1b13 are only 1.095 apart
   to begin with, so 55% of that gap is 1.045:1 — measured by painting the
   computed colour to a canvas, because color-mix() serialises to oklab() and
   reading it as text gives a number that is not the colour. At 1.045 the
   striping is not subtle, it is absent, and a dense table loses the one thing
   that keeps a row from being read as the row above it.

   A real token per theme, both landing near 1.08. */
.table tbody tr:nth-child(even) { background: var(--fx-color-zebra); }
/* Hover still out-ranks the stripe, and the header still out-ranks both. */
.table tbody tr:hover, .table tbody tr:nth-child(even):hover {
  background: var(--fx-color-primary-tint);
}

/* ===========================================================================
   THE CARD SYSTEM
   Appended 2026-08-28. This is the composable API another agent builds the
   dashboard out of. Nothing above this line is edited by this block.
   ---------------------------------------------------------------------------
   A card carries colour as a SURFACE, not as an edge on a white box. A warning
   card is a genuinely warm surface, a healthy one is genuinely cool, and the
   type on each is chosen for that surface rather than being one fixed ink
   reused everywhere.

   HOW THE TEXT FOLLOWS THE SURFACE, which is the whole trick and is worth
   understanding before extending it. A tone modifier does not set `color`. It
   re-declares the GLOBAL ink tokens locally on the card:

       .card--warn {
         --card-bg:          <warm surface>;
         --fx-color-ink:     <ink chosen for that surface>;
         --fx-color-muted:   <secondary chosen for that surface>;
         --fx-color-border:  <border chosen for that surface>;
         --fx-color-surface: <a deeper step of the same hue>;
       }

   Custom properties inherit, so every one of the ~3000 lines above that says
   `color: var(--fx-color-muted)` inside that card resolves to the WARM muted
   automatically. No component had to be taught about tones, nothing can fall
   out of step, and a new tone costs six declarations.

   --fx-color-bg is deliberately NOT re-declared. Inputs, menus and popovers
   stay white inside a coloured card, because a tinted text field reads as
   disabled and a tinted popover reads as a rendering fault.

   EVERY PAIR IS COMPUTED. Six tones by {ink, muted} by {bg, surface} by two
   themes, plus links, plus the semantic chip colours that can appear inside any
   tone, plus the phone-softened variant of all of it. scripts/contrast.js
   generates the whole matrix rather than sampling it.

   ---------------------------------------------------------------------------
   THE API
   ---------------------------------------------------------------------------
   Structure    .card
                .card__head   .card__body   .card__foot
                .card__title  .card__meta
   Tone         .card--neutral (default)   .card--ok   .card--warn
                .card--bad   .card--info   .card--accent
   Emphasis     .card--plain   no surface colour; the tone shows only in the
                               left block and the border
                (default)      soft tinted surface
                .card--solid   saturated surface, inverted ink. AT MOST ONE PER
                               SCREEN: it is the loudest thing available.
   Behaviour    .card--flat    no shadow; sits in the page rather than on it
                .card--link    the whole card is a link; adds the hover lift

   Compose freely, e.g. class="card card--warn card--link".

   .panel and its --ok / --warn / --bad / --info / --neutral modifiers are wired
   to the same tokens, so existing templates get the treatment without being
   edited, and .card is the name to build new things with.

   PHONE FIRST. At 375px a column of saturated cards is a stack of loud
   rectangles with nowhere for the eye to rest. Two things prevent that: the
   DEFAULT tone is neutral, so a card is white unless it has a real state to
   report; and below 46rem every tinted surface is mixed 55% toward the page, so
   the hue survives and the volume drops. Softening only ever moves a surface
   AWAY from its ink (lighter in the light theme, darker in the dark one), so it
   raises contrast in both and cannot break the gate.

   STATE IS STILL NEVER COLOUR ALONE. The tone amplifies a word that is already
   in the card. It never replaces it.
   =========================================================================== */

:root {
  /* neutral */
  --fx-tone-neutral-bg:      #ffffff;
  --fx-tone-neutral-ink:     #1a2230;
  --fx-tone-neutral-muted:   #525c6e;
  --fx-tone-neutral-surface: #f4f6fa;
  --fx-tone-neutral-border:  #e7e9f0;
  --fx-tone-neutral-block:   transparent;
  /* ok */
  --fx-tone-ok-bg:           #e9f5ef;
  --fx-tone-ok-ink:          #0d5136;
  --fx-tone-ok-muted:        #3a6a53;
  --fx-tone-ok-surface:      #dfeee7;
  --fx-tone-ok-border:       #cbe5d8;
  --fx-tone-ok-block:        #1b8a57;
  /* warn */
  --fx-tone-warn-bg:         #fdf3e4;
  --fx-tone-warn-ink:        #6d3204;
  --fx-tone-warn-muted:      #7f4d20;
  --fx-tone-warn-surface:    #f8e8d2;
  --fx-tone-warn-border:     #f0dbbe;
  --fx-tone-warn-block:      #c2600c;
  /* bad */
  --fx-tone-bad-bg:          #fdedee;
  --fx-tone-bad-ink:         #8b1620;
  --fx-tone-bad-muted:       #9c4149;
  --fx-tone-bad-surface:     #f9dfe1;
  --fx-tone-bad-border:      #f3d2d5;
  --fx-tone-bad-block:       #ce3a42;
  /* info — the accent hue, for a card drawing attention rather than reporting
     a state */
  --fx-tone-info-bg:         #eef1fb;
  --fx-tone-info-ink:        #1f347f;
  --fx-tone-info-muted:      #47548c;
  --fx-tone-info-surface:    #e4e9f8;
  --fx-tone-info-border:     #d6dcf3;
  --fx-tone-info-block:      #2b46b8;
  /* accent — the solid one. Saturated surface, inverted ink. */
  --fx-tone-accent-bg:       #1f347f;
  --fx-tone-accent-ink:      #ffffff;
  --fx-tone-accent-muted:    #ccd5f2;
  --fx-tone-accent-surface:  #14235c;
  --fx-tone-accent-border:   #14235c;
  --fx-tone-accent-block:    #f5b81f;
  --fx-tone-accent-focus:   #f5b81f;   /* 5.37:1 on the bronze surface */
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --fx-tone-neutral-bg:      #0f2a46;
    --fx-tone-neutral-ink:     #eef5fc;
    --fx-tone-neutral-muted:   #b0c6db;
    --fx-tone-neutral-surface: #0a2138;
    --fx-tone-neutral-border:  #1e3a55;
    --fx-tone-neutral-block:   transparent;

    --fx-tone-ok-bg:           #0d2e26;
    --fx-tone-ok-ink:          #5fe0ac;
    --fx-tone-ok-muted:        #8fc4ad;
    --fx-tone-ok-surface:      #10352c;
    --fx-tone-ok-border:       #1d4a3c;
    --fx-tone-ok-block:        #37d99a;

    --fx-tone-warn-bg:         #2e2416;
    --fx-tone-warn-ink:        #f0ac6a;
    --fx-tone-warn-muted:      #c8a075;
    --fx-tone-warn-surface:    #382c1b;
    --fx-tone-warn-border:     #4b3c22;
    --fx-tone-warn-block:      #d98a3a;

    --fx-tone-bad-bg:          #331920;
    --fx-tone-bad-ink:         #ff9aa0;
    --fx-tone-bad-muted:       #d99196;
    --fx-tone-bad-surface:     #3b1e26;
    --fx-tone-bad-border:      #4f2833;
    --fx-tone-bad-block:       #e0555d;

    --fx-tone-info-bg:         #0d3547;
    --fx-tone-info-ink:        #7fe3f2;
    --fx-tone-info-muted:      #a8cfdc;
    --fx-tone-info-surface:    #114055;
    --fx-tone-info-border:     #1d5064;
    --fx-tone-info-block:      #1ec5da;

    --fx-tone-accent-bg:       #1ec5da;
    --fx-tone-accent-ink:      #042028;
    --fx-tone-accent-muted:    #05303a;
    --fx-tone-accent-surface:  #17b0c4;
    --fx-tone-accent-border:   #17b0c4;
    --fx-tone-accent-block:    #042028;
    --fx-tone-accent-focus:   #042028;   /* 9.21:1 on the gold surface */
  }
}

/* ---- the surface --------------------------------------------------------- */
.card, .panel, .kcard, .ck__i, .alert, .dcal__i, .stat, .trkl__i, .ocard,
.mail-msg, .miss__i, .msg, .commitbar, .ddates, .fr, .trk__now {
  background: var(--card-bg, var(--fx-color-bg));
  color: var(--fx-color-ink);
  border-color: var(--fx-color-border);
  border-left-color: var(--card-block, transparent);
}
.card {
  position: relative;
  border: 1px solid var(--fx-color-border);
  border-left-color: var(--card-block, transparent);
  border-radius: var(--fx-radius-lg);
  box-shadow: var(--fx-shadow-1);
  min-width: 0;
  overflow: hidden;
}
.card__head {
  padding: 1.25rem 1.25rem 0;
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: baseline;
}
.card__head h2, .card__head h3, .card__title {
  font-family: var(--fx-font-display); font-size: var(--fx-text-lg);
  font-weight: var(--fx-w-bold); letter-spacing: -0.014em;
  line-height: var(--fx-lh-head); color: var(--fx-color-ink);
}
.card__body { padding: 1.25rem; }
.card__head + .card__body { padding-top: var(--fx-space-4); }
.card__foot {
  padding: 1rem 1.25rem;
  background: var(--fx-color-surface);
  border-top: 1px solid var(--fx-color-border);
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3); align-items: center;
}
.card__meta { color: var(--fx-color-muted); font-size: var(--fx-text-sm); }
@media (min-width: 760px) {
  .card__head { padding: 1.5rem 1.5rem 0; }
  .card__body { padding: 1.5rem; }
  .card__foot { padding: 1rem 1.5rem; }
}

/* ---- the tones ----------------------------------------------------------- */
.card--neutral, .panel--neutral, .kcard--neutral, .ck__i--neutral,
.dcal__i--neutral, .msg--user {
  --card-bg: var(--fx-tone-neutral-bg);
  --card-block: var(--fx-tone-neutral-block);
  --fx-color-ink: var(--fx-tone-neutral-ink);
  --fx-color-muted: var(--fx-tone-neutral-muted);
  --fx-color-surface: var(--fx-tone-neutral-surface);
  --fx-color-surface-2: var(--fx-tone-neutral-surface);
  --fx-color-border: var(--fx-tone-neutral-border);
}
.card--ok, .panel--ok, .kcard--ok, .kcard--good, .ck__i--ok, .alert--ok,
.dcal__i--ok, .stat--ok, .ddates--ok, .trk__now--done, .due__clear {
  --card-bg: var(--fx-tone-ok-bg);
  --card-block: var(--fx-tone-ok-block);
  --fx-color-ink: var(--fx-tone-ok-ink);
  --fx-color-muted: var(--fx-tone-ok-muted);
  --fx-color-surface: var(--fx-tone-ok-surface);
  --fx-color-surface-2: var(--fx-tone-ok-surface);
  --fx-color-border: var(--fx-tone-ok-border);
}
.card--warn, .panel--warn, .kcard--warn, .kcard--watch, .ck__i--warn,
.alert--warn, .dcal__i--warn, .stat--warn, .ddates--warn, .miss__i,
.trk__now--blocked, .mail-outbox, .msg--tool {
  --card-bg: var(--fx-tone-warn-bg);
  --card-block: var(--fx-tone-warn-block);
  --fx-color-ink: var(--fx-tone-warn-ink);
  --fx-color-muted: var(--fx-tone-warn-muted);
  --fx-color-surface: var(--fx-tone-warn-surface);
  --fx-color-surface-2: var(--fx-tone-warn-surface);
  --fx-color-border: var(--fx-tone-warn-border);
}
.card--bad, .panel--bad, .kcard--bad, .kcard--at_risk, .ck__i--bad,
.alert--bad, .dcal__i--bad, .stat--bad, .ddates--bad {
  --card-bg: var(--fx-tone-bad-bg);
  --card-block: var(--fx-tone-bad-block);
  --fx-color-ink: var(--fx-tone-bad-ink);
  --fx-color-muted: var(--fx-tone-bad-muted);
  --fx-color-surface: var(--fx-tone-bad-surface);
  --fx-color-surface-2: var(--fx-tone-bad-surface);
  --fx-color-border: var(--fx-tone-bad-border);
}
.card--info, .panel--info, .panel--primary, .kcard--info, .ck__i--info,
.alert--info, .dcal__i--info, .ddates--info, .trk__now--current, .fr {
  --card-bg: var(--fx-tone-info-bg);
  --card-block: var(--fx-tone-info-block);
  --fx-color-ink: var(--fx-tone-info-ink);
  --fx-color-muted: var(--fx-tone-info-muted);
  --fx-color-surface: var(--fx-tone-info-surface);
  --fx-color-surface-2: var(--fx-tone-info-surface);
  --fx-color-border: var(--fx-tone-info-border);
}
/* The solid card re-declares the focus ring too. A near-black ring on bronze
   measures 2.59:1 and a lit-brass ring on gold measures 1.33:1, so the ring
   this card inherits from the page would be the one place in the interface a
   keyboard user could not see where they are. It flips to the tone's own ink,
   which is the colour already proven against this surface. */
.card--accent, .card--solid {
  --fx-color-focus: var(--fx-tone-accent-focus);
  --card-bg: var(--fx-tone-accent-bg);
  --card-block: var(--fx-tone-accent-block);
  --fx-color-ink: var(--fx-tone-accent-ink);
  --fx-color-muted: var(--fx-tone-accent-muted);
  --fx-color-surface: var(--fx-tone-accent-surface);
  --fx-color-surface-2: var(--fx-tone-accent-surface);
  --fx-color-border: var(--fx-tone-accent-border);
  --fx-color-accent-ink: var(--fx-tone-accent-ink);
}

/* ---- emphasis ------------------------------------------------------------ */
.card--plain { --card-bg: var(--fx-color-bg); }
.card--flat { box-shadow: none; }
.card--link {
  display: block; text-decoration: none; color: var(--fx-color-ink);
  transition: box-shadow var(--fx-dur-2) var(--fx-ease),
              transform var(--fx-dur-2) var(--fx-ease-out);
}
.card--link:hover { box-shadow: var(--fx-shadow-2); transform: translateY(-2px); }
.card--link:active { transform: translateY(0); box-shadow: var(--fx-shadow-1); }

/* ---- the phone ----------------------------------------------------------- */
@media (max-width: 46rem) {
  .card--ok, .panel--ok, .kcard--ok, .kcard--good, .ck__i--ok, .alert--ok,
  .dcal__i--ok, .stat--ok, .ddates--ok, .trk__now--done, .due__clear {
    --card-bg: color-mix(in srgb, var(--fx-tone-ok-bg) 55%, var(--fx-color-bg));
  }
  .card--warn, .panel--warn, .kcard--warn, .kcard--watch, .ck__i--warn,
  .alert--warn, .dcal__i--warn, .stat--warn, .ddates--warn, .miss__i,
  .trk__now--blocked, .mail-outbox, .msg--tool {
    --card-bg: color-mix(in srgb, var(--fx-tone-warn-bg) 55%, var(--fx-color-bg));
  }
  .card--bad, .panel--bad, .kcard--bad, .kcard--at_risk, .ck__i--bad,
  .alert--bad, .dcal__i--bad, .stat--bad, .ddates--bad {
    --card-bg: color-mix(in srgb, var(--fx-tone-bad-bg) 55%, var(--fx-color-bg));
  }
  .card--info, .panel--info, .panel--primary, .kcard--info, .ck__i--info,
  .alert--info, .dcal__i--info, .ddates--info, .trk__now--current, .fr {
    --card-bg: color-mix(in srgb, var(--fx-tone-info-bg) 55%, var(--fx-color-bg));
  }
  /* The solid card deliberately does NOT soften. It is the single point of
     emphasis on the screen and softening it removes the reason it exists. */
}

/* ---- the chrome joins the quiet base ------------------------------------- */
.rail {
  background: var(--fx-color-railbg);
  border-bottom: 1px solid var(--fx-color-rail-line);
  box-shadow: none;
}
.navdrawer {
  background: var(--fx-color-railbg);
  border-bottom: 1px solid var(--fx-color-rail-line);
}
.brand__mark { color: var(--fx-color-ink); }
.brand__band { fill: var(--fx-color-rail-accent); }
.nav__link::after, .subnav a::after { background: var(--fx-color-rail-accent); }
.nav__link:hover { background: var(--fx-color-surface); color: var(--fx-color-ink); }
.nav__link[aria-current="page"] {
  color: var(--fx-color-accent-ink); background: var(--fx-color-primary-tint);
}
.navdrawer a { color: var(--fx-color-muted); border-bottom-color: var(--fx-color-border); }
.navdrawer a:hover { color: var(--fx-color-ink); background: var(--fx-color-surface); }
.navdrawer a[aria-current="page"] {
  color: var(--fx-color-accent-ink); background: var(--fx-color-primary-tint);
}
.navdrawer__h { color: var(--fx-color-muted); }
.who__btn:hover, .who__btn[aria-expanded="true"] {
  background: var(--fx-color-surface); border-color: var(--fx-color-border);
}
.rail__toggle { color: var(--fx-color-ink); border-color: var(--fx-color-border-strong); }
.rail__toggle:hover { border-color: var(--fx-color-primary); background: var(--fx-color-surface); }
.avatar { background: var(--fx-color-primary); color: var(--fx-color-on-primary); box-shadow: none; }

/* ===========================================================================
   APPENDED BLOCK — THE DASHBOARD GRID.  Layout only.
   Added 2026-08-28 with the composable dashboard (src/dashboard/*).

   EVERYTHING ABOVE THIS LINE IS UNTOUCHED. The panels on the dashboard are
   ordinary .panel / .chip / .empty / .table cards and take their surface,
   spine, radius and colour from the rules above. What is defined here is the
   geometry those cards sit in, plus the handful of small blocks the widget
   templates need that had no existing class: a proportioned bar row, a headline
   figure, and the edit-mode controls.

   NO COLOUR IS INVENTED HERE. Every value below is a token. If the palette
   moves, this block moves with it and nothing has to be found and corrected.

   THE GRID. Twelve columns on a desk monitor, six on a tablet, one on a phone.
   A panel's stored width is a span:

     tile  3 of 12   a quarter of a desk monitor, half a tablet, all of a phone
     half  6 of 12
     wide  9 of 12
     full  12 of 12

   Spans are declared per breakpoint rather than through a --span variable,
   because a variable would need clamping at every breakpoint anyway and the
   three spellings are shorter than the one that works.

   min-width: 0 on the cell is load-bearing and is the reason this page does not
   scroll sideways at 375px: a grid item defaults to min-width:auto, so a wide
   table inside a panel pushes its own column out and the column pushes the page
   out with it. The same fault the stylesheet already guards for .grid > *; the
   dashboard grid needs its own copy because it is not .grid.
   =========================================================================== */

.dash {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--fx-space-5);
  margin-bottom: var(--fx-space-6);
}
.dash__cell { min-width: 0; }
/* A cell is a grid item AND a column of one card, so the card stretches to the
   tallest panel in its row instead of leaving a ragged edge along it. */
.dash__cell > .panel { height: 100%; display: flex; flex-direction: column; }
.dash__cell > .panel > .panel__body,
.dash__cell > .panel > .table-scroll { flex: 1 1 auto; }

@media (min-width: 760px) {
  .dash { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .dash__cell--tile { grid-column: span 3; }
  .dash__cell--half,
  .dash__cell--wide,
  .dash__cell--full { grid-column: span 6; }
}
@media (min-width: 1080px) {
  .dash { grid-template-columns: repeat(12, minmax(0, 1fr)); }
  .dash__cell--tile { grid-column: span 3; }
  .dash__cell--half { grid-column: span 6; }
  .dash__cell--wide { grid-column: span 9; }
  .dash__cell--full { grid-column: span 12; }
}

/* ---- edit mode ------------------------------------------------------------
   The controls sit in the panel head beside the title. They are quiet buttons
   in real forms, so they inherit every state the button rules above define;
   what is added here is only the arrangement. */
.dash__note { margin-bottom: var(--fx-space-5); }
.dash__ctl { display: inline-flex; gap: var(--fx-space-2); align-items: center; flex-wrap: wrap; }
.dash__ctl .inline-form { margin: 0; }
/* A square arrow button. Without a floor the up and down buttons come out two
   different widths, because one glyph is wider than the other in the UI face. */
.dash__ctl .btn { min-width: 2.25rem; justify-content: center; }

.dash__tools {
  display: flex; flex-wrap: wrap; gap: var(--fx-space-3);
  align-items: flex-start;
  padding: var(--fx-space-3) var(--fx-space-5);
  border-bottom: 1px solid var(--fx-color-border);
  background: var(--fx-color-surface);
}
.dash__size { display: inline-flex; gap: var(--fx-space-2); align-items: center; margin: 0; }
.dash__settings { flex: 1 1 14rem; min-width: 0; }
.dash__settings > summary {
  cursor: pointer; font-size: var(--fx-text-xs); font-weight: 700;
  color: var(--fx-color-muted); padding: var(--fx-space-2) 0;
}
.dash__settings > summary:hover { color: var(--fx-color-ink); }
.dash__settings .field { margin-bottom: var(--fx-space-3); }

/* The drag handle: six dots, drawn rather than an icon, because the icon set in
   partials/icon.ejs is hand-drawn for this app and has no grip in it. Hidden in
   the markup and revealed by public/js/dashboard.js, so a browser with the
   script blocked is never shown an affordance that does nothing. */
.dash__grip {
  width: 0.75rem; height: 1rem; flex: 0 0 auto; cursor: grab;
  background-image: radial-gradient(currentColor 1px, transparent 1.2px);
  background-size: 5px 5px; background-position: 0 3px;
  color: var(--fx-color-border-strong);
}
.dash--edit .dash__cell.is-dragging { opacity: 0.45; cursor: grabbing; }
/* While a drag is live the drop targets need an edge, or the page looks frozen
   between the pick-up and the drop. */
.dash.is-dropping .dash__cell > .panel { outline: 1px dashed var(--fx-color-border-strong); }
.dash.is-dropping .dash__cell.is-dragging > .panel { outline: 0; }

/* ---- the picker -----------------------------------------------------------
   Everything not currently on the dashboard: hidden and never-placed alike. */
.dash__picker { margin-bottom: var(--fx-space-6); }
.dash__addlist { list-style: none; display: grid; gap: var(--fx-space-3); grid-template-columns: minmax(0, 1fr); }
@media (min-width: 760px) { .dash__addlist { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.dash__add {
  display: flex; gap: var(--fx-space-4); align-items: flex-start; justify-content: space-between;
  padding: var(--fx-space-3) 0; border-bottom: 1px solid var(--fx-color-border); min-width: 0;
}
.dash__add > div { min-width: 0; }
.dash__add p { margin: var(--fx-space-1) 0 0; font-size: var(--fx-text-xs); }
.dash__add .inline-form { margin: 0; flex: 0 0 auto; }

/* ---- the headline figure --------------------------------------------------
   One number, sized against its own box rather than the viewport, exactly as
   .stat__v is and for the same reason: a fourteen-character currency figure in
   a quarter-width tile at 1280 and in a full-width tile at 390 are the same
   problem, and a media query cannot tell the two apart. */
.dash__figure { display: flex; flex-direction: column; gap: var(--fx-space-2); container-type: inline-size; }
.dash__figurelink { text-decoration: none; color: inherit; display: block; }
.dash__figurelink:hover .dash__figurev { color: var(--fx-color-primary); }
.dash__figurev {
  display: block;
  font-family: var(--fx-font-display); font-weight: 900;
  font-size: var(--fx-text-2xl); line-height: var(--fx-lh-display);
  letter-spacing: var(--fx-track-display); font-variant-numeric: tabular-nums;
}
@supports (container-type: inline-size) {
  .dash__figurev { font-size: clamp(1.375rem, 16cqi, var(--fx-text-3xl)); }
}
.dash__figurel {
  display: block; margin-top: var(--fx-space-2);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-xs); font-weight: 700;
  letter-spacing: var(--fx-track-eyebrow); text-transform: uppercase;
  color: var(--fx-color-muted);
}
.dash__figurec { font-size: var(--fx-text-xs); margin: 0; }

/* ---- the proportioned bar row ---------------------------------------------
   Used by the portfolio panel. The bar is never the only carrier: the chosen
   measure is printed beside it and the other one sits under it, so the row
   reads in greyscale and to a screen reader with nothing lost. */
.dash__bars { list-style: none; display: grid; gap: var(--fx-space-1); }
.dash__bar { min-width: 0; }
.dash__barlink {
  display: grid; gap: 2px var(--fx-space-3); align-items: center;
  grid-template-columns: minmax(5rem, 8rem) minmax(0, 1fr) auto;
  grid-template-areas: "label track value" "label track second";
  padding: var(--fx-space-2);
  border-radius: var(--fx-radius-sm);
  text-decoration: none; color: inherit;
}
.dash__barlink:hover { background: var(--fx-color-primary-tint); }
.dash__barl { grid-area: label; font-weight: 600; font-size: var(--fx-text-sm); min-width: 0; }
.dash__bartrack {
  grid-area: track; height: 6px; border-radius: 3px; overflow: hidden;
  background: var(--fx-color-border);
}
.dash__barfill { display: block; height: 100%; background: var(--fx-color-primary); }
.dash__barv { grid-area: value; font-size: var(--fx-text-xs); font-variant-numeric: tabular-nums; white-space: nowrap; }
.dash__bars2 { grid-area: second; font-size: var(--fx-text-xs); white-space: nowrap; }
/* On a phone the three-column row collapses: the label takes its own line and
   the track runs full width under it, so nothing is squeezed to nothing. */
@media (max-width: 560px) {
  .dash__barlink {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "label value" "track track" "second second";
  }
}

/* ---- small pieces the widget templates needed ----------------------------- */
.dash__legend { display: flex; flex-wrap: wrap; gap: var(--fx-space-2); align-items: center; margin-bottom: var(--fx-space-3); }
/* The listing-expiry row wears the same spine vocabulary as every other row in
   the app, so "expired" reads here the way it reads on the deal board. */
.dash__expiry { border-left: 3px solid transparent; padding-left: var(--fx-space-3); }
.dash__expiry--bad  { border-left-color: var(--fx-color-bad-solid); }
.dash__expiry--warn { border-left-color: var(--fx-color-warn-solid); }
.dash__expiry--info { border-left-color: var(--fx-color-cyan); }

/* ===========================================================================
   SEMANTIC LATTICE — appended block, added for /admin/lattice.
   Everything above this comment is untouched by it.

   Scope: every selector here begins .lattice, .latlegend, .latnear, .latswitch
   or .lattable, so nothing in this block can reach a rule above it. It uses the
   --fx-* design tokens where one exists and falls back with var(token, literal)
   where a palette rewrite might rename one, so this block degrades to readable
   rather than to invisible if the tokens move underneath it.

   The one thing here that is NOT a token: the fourteen categorical point
   colours. There is no categorical scale in the token set, and a record-type
   scale needs hues that stay apart from each other rather than hues that belong
   to a brand. They are declared as --lat-k0 upward on the figure container, and
   public/js/lattice.js reads them from there with getComputedStyle rather than
   holding a second copy, so recolouring the point cloud is an edit to this block
   alone.
   ======================================================================== */

.lattice {
  /* Categorical scale. OKLCH at a fixed lightness and chroma with the hues
     spread around the wheel, so no one type shouts louder than another, and
     every one of them holds contrast against both the light and the dark
     ground. Ordered to put the types that appear together on a brokerage
     workspace, clients and properties and listings, far apart in hue. */
  --lat-k0:  oklch(0.62 0.17 255);   /* clients      blue      */
  --lat-k1:  oklch(0.68 0.14 215);   /* contacts     cyan      */
  --lat-k2:  oklch(0.63 0.16 145);   /* properties   green     */
  --lat-k3:  oklch(0.70 0.15 110);   /* listings     olive     */
  --lat-k4:  oklch(0.72 0.15 85);    /* comparables  amber     */
  --lat-k5:  oklch(0.66 0.16 55);    /* rent roll    orange    */
  --lat-k6:  oklch(0.62 0.19 25);    /* projects     red       */
  --lat-k7:  oklch(0.62 0.19 355);   /* milestones   crimson   */
  --lat-k8:  oklch(0.62 0.18 325);   /* tasks        magenta   */
  --lat-k9:  oklch(0.60 0.17 295);   /* time         violet    */
  --lat-k10: oklch(0.58 0.18 275);   /* proposals    indigo    */
  --lat-k11: oklch(0.70 0.11 240);   /* quotes       slate     */
  --lat-k12: oklch(0.55 0.09 180);   /* tickets      teal      */
  --lat-k13: oklch(0.60 0.02 260);   /* anything else, grey    */

  --lat-ring: var(--fx-color-ink, #1b1b1b);
  --lat-axis: var(--fx-color-muted, #8a8a8a);

  position: relative;
  border: 1px solid var(--fx-color-border, #d8d8d8);
  border-radius: var(--fx-radius-md, 8px);
  background: var(--fx-color-ground, #fbfbfb);
  overflow: hidden;
}

/* The canvas sizes itself from this box; the script reads the box and sets the
   backing store. aspect-ratio rather than a fixed height so a phone gets a
   square and a desktop gets something close to 16:9, with a floor that keeps a
   very narrow window from producing a strip too short to read. */
.lattice__canvas {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  min-height: 260px;
  max-height: 70vh;
  /* The canvas is a focus target, so it needs a visible ring of its own: a
     canvas has no default focus appearance worth relying on. */
  outline-offset: -3px;
  touch-action: none;
}
.lattice__canvas:focus-visible {
  outline: 3px solid var(--fx-color-focus, #2f6fed);
}

.lattice__nojs {
  margin: 0;
  padding: var(--fx-space-4, 1rem);
  color: var(--fx-color-muted, #6b6b6b);
  font-size: var(--fx-text-sm, 0.875rem);
}

.lattice[data-failed] .lattice__canvas { display: none; }

.lattice__keys { margin-top: var(--fx-space-3, 0.75rem); font-size: var(--fx-text-sm, 0.875rem); }
.lattice__keys kbd {
  font-family: var(--fx-font-mono, ui-monospace, monospace);
  font-size: 0.9em;
  padding: 0.05em 0.35em;
  border: 1px solid var(--fx-color-border, #d8d8d8);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--fx-color-ground, #f4f4f4);
  white-space: nowrap;
}

.lattice__readout {
  margin-top: var(--fx-space-4, 1rem);
  padding: var(--fx-space-4, 1rem);
  border: 1px solid var(--fx-color-border, #d8d8d8);
  border-radius: var(--fx-radius-md, 8px);
  background: var(--fx-color-ground, #fbfbfb);
  /* Reserved rather than collapsing: the panel grows and shrinks as points are
     stepped through, and a block that resizes under the figure drags the whole
     page up and down while somebody is reading it. */
  min-height: 7.5rem;
}
.lattice__readout > :first-child { margin-top: 0; }
.lattice__readout > :last-child { margin-bottom: 0; }
.lattice__readout-h { display: flex; align-items: center; gap: var(--fx-space-2, 0.5rem); flex-wrap: wrap; }

/* ---- legend ---------------------------------------------------------------
   Colour is never the only channel. Each swatch also carries one of four
   shapes, the type is written beside it in words, and the table below the
   figure repeats the type in a column of its own. */
.latlegend {
  list-style: none;
  margin: var(--fx-space-2, 0.5rem) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--fx-space-2, 0.5rem) var(--fx-space-4, 1rem);
  font-size: var(--fx-text-sm, 0.875rem);
}
.latlegend__i { display: flex; align-items: center; gap: var(--fx-space-2, 0.5rem); }

.latlegend__sw {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  flex: none;
  background: var(--lat-c, currentColor);
  vertical-align: -0.05em;
}
.latlegend__sw--sm { width: 0.6rem; height: 0.6rem; margin-right: 0.35rem; }

.latlegend__sw[data-shape="0"] { border-radius: 50%; }
.latlegend__sw[data-shape="1"] { border-radius: 2px; }
.latlegend__sw[data-shape="2"] { border-radius: 2px; transform: rotate(45deg); }
.latlegend__sw[data-shape="3"] { clip-path: polygon(50% 0, 100% 100%, 0 100%); }

/* ---- neighbour lists ------------------------------------------------------ */
.latnear { list-style: none; margin: 0; padding: 0; font-size: var(--fx-text-sm, 0.875rem); }
.latnear li { display: flex; gap: var(--fx-space-2, 0.5rem); align-items: baseline; flex-wrap: wrap; }
.latnear .mono { font-variant-numeric: tabular-nums; }

/* ---- the embedder switch -------------------------------------------------- */
.latswitch {
  list-style: none;
  margin: var(--fx-space-2, 0.5rem) 0 var(--fx-space-3, 0.75rem);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--fx-space-2, 0.5rem);
}
.latswitch a {
  display: inline-block;
  padding: 0.25rem 0.7rem;
  border: 1px solid var(--fx-color-border, #d8d8d8);
  border-radius: 999px;
  font-size: var(--fx-text-sm, 0.875rem);
  text-decoration: none;
}
.latswitch a[aria-current="true"] {
  background: var(--fx-color-primary, #2f4858);
  border-color: var(--fx-color-primary, #2f4858);
  color: var(--fx-color-on-primary, #fff);
}

/* ---- the table ------------------------------------------------------------
   Coordinates line up as numbers, and the neighbour column is allowed to be
   the wide one because it is the column carrying the finding. Nothing here sets
   a min-width on the table: the surrounding .table-scroll already scrolls, and
   forcing a width would push the PAGE sideways at 375px, which the rest of this
   app is careful not to do. */
.lattable td.num, .lattable th.num { font-variant-numeric: tabular-nums; }
.lattable th[scope="row"] { font-weight: 600; }
.lattable td:last-child { min-width: 14rem; }

@media (max-width: 560px) {
  .lattice__canvas { aspect-ratio: 4 / 3; }
  .lattable td:last-child { min-width: 0; }
}

/* Dark ground: the same hues lifted, because a mid-lightness colour that sits
   well on white loses its separation against a dark panel. Chroma is held so
   the scale stays the same scale rather than becoming a different one at
   night. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .lattice {
    --lat-k0:  oklch(0.72 0.16 255);
    --lat-k1:  oklch(0.78 0.13 215);
    --lat-k2:  oklch(0.75 0.15 145);
    --lat-k3:  oklch(0.80 0.14 110);
    --lat-k4:  oklch(0.82 0.14 85);
    --lat-k5:  oklch(0.76 0.15 55);
    --lat-k6:  oklch(0.70 0.17 25);
    --lat-k7:  oklch(0.71 0.17 355);
    --lat-k8:  oklch(0.72 0.16 325);
    --lat-k9:  oklch(0.71 0.16 295);
    --lat-k10: oklch(0.70 0.16 275);
    --lat-k11: oklch(0.79 0.10 240);
    --lat-k12: oklch(0.70 0.09 180);
    --lat-k13: oklch(0.74 0.02 260);
  }
}

/* prefers-reduced-motion is honoured in the script rather than here, because
   what it turns off is the idle rotation of the cloud and there is no CSS
   animation to disable. This rule is the belt to that braces: if anything in
   this block ever grows a transition, it stops here too. */
@media (prefers-reduced-motion: reduce) {
  .lattice, .lattice * { animation: none !important; transition: none !important; }
}
/* ---- end semantic lattice block ----------------------------------------- */

/* ===========================================================================
   3 POINT FORWARD HOUSE STYLE
   Appended 2026-08-28. Tophat is a 3PF product and now looks like one.
   Source of truth: D:\proposals\3pointforward-site\index.html (light) and
   index-dark.html (dark). Nothing above this line is edited by this block.
   ---------------------------------------------------------------------------
   TOKEN MAPPING, so the next person can check it against the site rather than
   trusting this file:

     3PF            value      Tophat token
     --paper        #f7f8fb    --fx-color-ground
     --white        #fff       --fx-color-bg
     --ink          #1a2230    --fx-color-ink
     --ink2         #525c6e    --fx-color-muted
     --muted        #8a93a3    NOT used as body text; it is 3.10:1 on white and
                               is a tertiary/non-text token on the site too
     --line         #e7e9f0    --fx-color-border
     --blue         #1f347f    --fx-color-primary, --fx-color-accent-ink
     --blue2        #14235c    --fx-color-primary-hover
     --blue-br      #2b46b8    --fx-color-focus
     --blue-tint    #eef1fb    --fx-color-primary-tint, the info card surface
     --gold         #f5b81f    the ACCENT: nav hover underline, the gold button,
                               the block on a solid card
     --gold2        #e0a300    gold pressed
     --gold-tint    #fdf4dd    gold surfaces
     --teal         #17becf    the dark theme's primary (see below)
     --maxw         1280px     --fx-maxw
     --ease/--pop              --fx-ease / --fx-pop

   GOLD IS AN ACCENT, NOT AN ACTION. Blue carries every action and every current
   state; gold marks and decorates. That is how the site uses them, and it is
   also the only arrangement that passes: #f5b81f is 1.77:1 on white, so gold
   can never carry small text or a state boundary on the light theme. Where 3PF
   underlines a hovered nav item in gold, that underline is decoration on top of
   a state already carried by the label and by aria-current.

   THE DARK THEME IS DERIVED FROM index-dark.html, not inverted from the light
   one. That page is a different scheme on purpose: deep navy grounds
   (--bg #07182b, --panel #0f2a46) with TEAL as the action colour rather than
   blue, --sub #b0c6db for secondary type and --up #37d99a for success.

   ONE DELIBERATE DEPARTURE, and it is an accessibility fix rather than a taste
   call. The dark site's primary button is a gradient from --teal #1ec5da to
   --teal-deep #128aa0 carrying a #042028 label. That label measures 4.17:1 at
   the deep end and fails AA. The gradient here stops at #17b0c4 (6.41:1)
   instead. Same gesture, same colour family, passes.
   =========================================================================== */

/* ------------------------------------------------------------------ type --
   Space Grotesk for display, Inter for body and UI, exactly as the site sets
   them. The site loads both from Google Fonts; this app cannot. A strict CSP
   pins font-src to 'self' and the box has no guaranteed internet, so a CDN link
   here is a broken font rather than a nicer one.

   BOTH FACES NOW SHIP, as two variable woff2 files in public/fonts/. Until
   they were supplied this file declared six per-weight files that did not
   exist, so every face errored and the whole product rendered in the system
   stack while the marketing site rendered correctly. The seam between them was
   the login page, which is the worst place to have one.

   The rules at the foot of this file try local() first, so a designer with the
   family installed gets it with no download. The stacks below are still written
   out in full behind the webfont names, so a deployment that strips the font
   files degrades to a readable page rather than a broken one. */
/*
 * TWO VARIABLE FILES, NOT SIX STATIC ONES.
 *
 * These rules previously named six per-weight woff2 files that were never
 * supplied, so every one of them 404'd and both families resolved to nothing.
 * That is worse than declaring no webfont at all: the stack still falls through
 * correctly, but only after six failed requests, and the page it falls through
 * to is the one this design was not drawn for.
 *
 * A variable font covers the whole weight axis in one file, so font-weight: 640
 * is as available as 600 and the two families cost two requests between them.
 *
 * local() stays first and stays deliberate: a designer with the family already
 * installed gets it with no download at all, which is what the previous author
 * intended and is still right.
 *
 * The CSP is font-src 'self', so these must be same-origin. Do not replace them
 * with a Google Fonts link; on a deployment with no internet that is a broken
 * font rather than a nicer one, which is the note the original comment made and
 * it still holds.
 */
@font-face {
  font-family: "Space Grotesk"; font-style: normal; font-weight: 300 700; font-display: swap;
  src: local("Space Grotesk"),
       url("/fonts/space-grotesk-var.woff2") format("woff2-variations"),
       url("/fonts/space-grotesk-var.woff2") format("woff2");
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 100 900; font-display: swap;
  src: local("Inter"),
       url("/fonts/inter-var.woff2") format("woff2-variations"),
       url("/fonts/inter-var.woff2") format("woff2");
}

:root {
  --fx-font-display: "Space Grotesk", "Segoe UI Variable Display", -apple-system,
                     BlinkMacSystemFont, "Segoe UI Semibold", "Segoe UI", system-ui,
                     "Helvetica Neue", Arial, sans-serif;
  --fx-font-ui:      "Inter", "Segoe UI Variable Text", -apple-system,
                     BlinkMacSystemFont, "Segoe UI", Roboto, system-ui,
                     "Helvetica Neue", Arial, sans-serif;
  --fx-font-body:    "Inter", "Segoe UI Variable Text", -apple-system,
                     BlinkMacSystemFont, "Segoe UI", Roboto, system-ui,
                     "Helvetica Neue", Arial, sans-serif;

  /* 3PF sets headings at -.02em and line-height 1.02. That is a poster setting;
     at the 22px and 18px this app uses for panel titles it closes the lines up
     too far, so the display end takes the site's tracking and the working sizes
     relax toward 1.2. */
  --fx-track-display: -0.02em;
  --fx-track-head:    -0.018em;

  /* Radii. The site runs 10-11px on buttons, 12-16px on cards, 100px on pills. */
  --fx-radius-xs:   6px;
  --fx-radius-sm:   8px;
  --fx-radius-md:   11px;
  --fx-radius-lg:   14px;
  --fx-radius-xl:   20px;
  --fx-radius-pill: 100px;

  /* Shadows. Long, soft and navy-tinted rather than neutral grey — this is most
     of what makes the site's surfaces look like the site's surfaces. */
  --fx-shadow-1: 0 12px 30px -22px rgba(20, 35, 92, 0.35);
  --fx-shadow-2: 0 20px 44px -24px rgba(20, 30, 60, 0.40);
  --fx-shadow-3: 0 40px 90px -38px rgba(20, 35, 92, 0.55);
  --fx-hi:       0 0 transparent;
  --fx-hi-soft:  0 0 transparent;

  --fx-ease:     cubic-bezier(0.16, 1, 0.3, 1);
  --fx-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --fx-pop:      cubic-bezier(0.22, 1.15, 0.36, 1);
  --fx-maxw: 1280px;

  /* Gold. An accent and a mark; never an action, never small text on paper. */
  --fx-color-gold:      #f5b81f;
  --fx-color-gold2:     #e0a300;
  --fx-color-gold-tint: #fdf4dd;
  --fx-color-on-gold:   #26200a;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --fx-shadow-1: 0 12px 30px -18px rgba(0, 0, 0, 0.75);
    --fx-shadow-2: 0 22px 48px -22px rgba(0, 0, 0, 0.80);
    --fx-shadow-3: 0 44px 96px -34px rgba(0, 0, 0, 0.90);
    /* The gold set needs a dark theme of its own. Left unoverridden, --gold-tint
       stayed the light cream #fdf4dd while --fx-color-ink went near-white, so any
       gold surface would have carried invisible text. The gold itself is unchanged
       — it reads well on navy — only the tint and its label move. */
    --fx-color-gold:      #f5b81f;
    --fx-color-gold2:     #e0a300;
    --fx-color-gold-tint: #2e2611;
    --fx-color-on-gold:   #26200a;
  }
}

/* ---- the state block keeps the site's own corner --------------------------
   3PF uses border-radius: 0 4px 4px 0 on its own left-edge markers, which is
   the shape already built here. Matched exactly rather than approximated. */
:root {
  --fx-block-radius:       0 4px 4px 0;
  --fx-block-radius-dense: 0 4px 4px 0;
}

/* ---- buttons -------------------------------------------------------------
   .btn-blue on the site is a FLAT blue that lifts 2px on hover into --blue2
   with a navy-tinted shadow. No gradient. Reproduced rather than reinterpreted,
   including the 10px radius and the Space Grotesk label. */
.btn {
  font-family: var(--fx-font-display);
  font-weight: 600;
  border-radius: var(--fx-radius-md);
  letter-spacing: 0;
  transition: background-color 0.25s var(--fx-ease), border-color 0.25s var(--fx-ease),
              color 0.25s var(--fx-ease), box-shadow 0.25s var(--fx-ease),
              transform 0.25s var(--fx-ease);
}
.btn--primary {
  background-image: none;
  background-color: var(--fx-color-primary);
  border-color: var(--fx-color-primary);
  color: var(--fx-color-on-primary);
  box-shadow: none;
}
.btn--primary:hover {
  background-color: var(--fx-color-primary-hover);
  border-color: var(--fx-color-primary-hover);
  transform: translateY(-2px);
  box-shadow: 0 14px 30px -12px rgba(31, 52, 127, 0.5);
}
.btn--primary:active { transform: translateY(0); box-shadow: none; }
.btn--ghost {
  background: var(--fx-color-bg);
  border-color: var(--fx-color-border);
  color: var(--fx-color-ink);
  box-shadow: none;
}
.btn--ghost:hover {
  border-color: var(--fx-color-primary);
  color: var(--fx-color-primary);
  background: var(--fx-color-bg);
  transform: translateY(-2px);
  box-shadow: none;
}
.btn--ghost:active { transform: translateY(0); }
/* The gold button, straight off the site: gold fill, dark label, gold2 on hover. */
.btn--accent {
  background: var(--fx-color-gold);
  color: var(--fx-color-on-gold);
  border-color: var(--fx-color-gold);
  box-shadow: none;
}
.btn--accent:hover {
  background: var(--fx-color-gold2);
  border-color: var(--fx-color-gold2);
  color: var(--fx-color-on-gold);
  transform: translateY(-2px);
  box-shadow: 0 14px 30px -12px rgba(224, 163, 0, 0.5);
}
.btn--accent:active { transform: translateY(0); box-shadow: none; }
.btn--quiet { box-shadow: none; }

/* ---- nav ------------------------------------------------------------------
   The site sweeps a gold underline in from the left on hover, using a right:
   100% -> 0 transition rather than a transform. Kept as a transform-and-opacity
   scale here, because animating `right` is a layout property and this file's
   motion rule is transform/opacity only.

   The CURRENT item is blue, not gold. Gold is 1.77:1 on white and cannot carry
   a state; the hovered underline is decoration over a label that already says
   where it goes. */
.nav__link::after { background: var(--fx-color-gold); }
.nav__link:hover::after { transform: scaleX(1); opacity: 1; }
.nav__link[aria-current="page"]::after { background: var(--fx-color-primary); opacity: 1; }
.subnav a::after { background: var(--fx-color-gold); }
.subnav a:hover::after { transform: scaleX(1); opacity: 1; }
.subnav a[aria-current="true"]::after { background: var(--fx-color-primary); opacity: 1; }
.brand__band { fill: var(--fx-color-gold); }
.brand .nm b, .brand__name b { color: var(--fx-color-primary); }

/* ---- headings -------------------------------------------------------------
   Space Grotesk, 700, the site's tracking. */
h1, h2, h3, .card__title, .pagehead__t h1, .stat__v, .fr__t, .creator__title {
  font-family: var(--fx-font-display);
  font-weight: 700;
}
.kicker, .due__head h2, .creator__sec h2, .kcol__head h2, .msg__who, .cites__h {
  font-family: var(--fx-font-display);
  font-weight: 600;
  letter-spacing: 0.16em;
}

/* ---- one frame per object, generalised ------------------------------------
   A scroller that fills a container it is already inside draws no second frame.
   The earlier rule named .panel only, so a table nested in an empty state on
   /admin/lattice kept its own 14px corners inside the empty state's 14px
   corners — two curves that cannot follow each other. Every card-like container
   is named now, so the next one added does not reintroduce it. */
.panel .table-scroll,
.card .table-scroll,
.empty .table-scroll,
.kcol .table-scroll,
.fr .table-scroll,
.trk__now .table-scroll {
  border: 0; border-radius: 0; box-shadow: none; background: none;
}

/* The browser's own furniture follows the choice, not the operating system.
   Without this a forced-dark page keeps light scrollbars and a light date
   picker, which is the tell that a theme switch is skin-deep. */
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

/* ---- the theme switch --------------------------------------------------- */
/* Three radio inputs in a segmented control. Radios rather than a button that
   cycles, because a cycling button cannot say what the other options are and
   cannot be operated by anyone who needs to know before committing. The inputs
   carry the state; JavaScript only reads them. */
.themesw { display: flex; gap: 2px; padding: 3px; margin: var(--fx-space-2) 0 0;
  border: 1px solid var(--fx-color-border); border-radius: 100px;
  background: var(--fx-color-surface-2); }
.themesw__o { position: relative; flex: 1 1 0; display: flex; }
.themesw input { position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer; }
.themesw span { flex: 1; display: flex; align-items: center; justify-content: center;
  gap: .35em; min-height: 36px; padding: 0 var(--fx-space-2);
  border-radius: 100px; font-family: var(--fx-font-ui);
  font-size: var(--fx-text-xs); font-weight: var(--fx-w-med); line-height: 1;
  color: var(--fx-color-muted); white-space: nowrap;
  transition: background var(--fx-dur-2) var(--fx-ease-out), color var(--fx-dur-2) var(--fx-ease-out); }
.themesw input:hover + span { color: var(--fx-color-ink); }
.themesw input:checked + span { background: var(--fx-color-primary); color: var(--fx-color-on-primary); }
.themesw input:focus-visible + span { outline: 2px solid var(--fx-color-focus); outline-offset: 2px; }
.themesw__h { margin: var(--fx-space-3) 0 var(--fx-space-1);
  font-family: var(--fx-font-ui); font-size: var(--fx-text-2xs); font-weight: var(--fx-w-bold);
  line-height: 1.2; color: var(--fx-color-muted);
  text-transform: uppercase; letter-spacing: .06em; }

/* In the drawer the rail palette is in force, so the tokens above would put a
   light control on a dark ground.
   The selected segment is marked the way the drawer already marks the current
   section — accent text on the rail's own line colour — rather than a filled
   pill, because rail-accent is navy in one theme and cyan in the other and
   there is no token for a label that works on both. Reusing a treatment the
   screen already teaches beats inventing a colour pair the gate cannot check. */
.navdrawer .themesw { background: transparent; border-color: var(--fx-color-rail-line); }
.navdrawer .themesw span { color: var(--fx-color-rail-muted); }
.navdrawer .themesw input:hover + span { color: var(--fx-color-rail-ink); }
.navdrawer .themesw input:checked + span {
  background: var(--fx-color-rail-line); color: var(--fx-color-rail-accent);
  font-weight: var(--fx-w-bold); }
.navdrawer .themesw__h { color: var(--fx-color-rail-muted); }

/* The whole control is pointless to anyone who cannot run the script that
   applies it, so it is hidden until that script says otherwise. */
.themesw-wrap { display: none; }
:root.js .themesw-wrap { display: block; }


/* ==== EXPLICIT DARK =======================================================
   The same dark values as the prefers-color-scheme blocks above, keyed on an
   attribute this product sets rather than on the operating system's setting.

   THIS IS DUPLICATION AND IT IS DELIBERATE. Plain CSS cannot put one rule
   under both a media query and an attribute selector, and this codebase has no
   build step to generate one from the other. The alternative was to leave the
   theme unswitchable, which is what shipped until now.

   WHAT KEEPS IT HONEST: npm run check:theme re-derives this region from the
   blocks above and fails if a single declaration has drifted. Change a dark
   value in ONE place, up there, then run that script; it rewrites this. Do not
   hand-edit below this line.
   ========================================================================= */
/* ==== BEGIN GENERATED DARK MIRROR ==== */
:root[data-theme="dark"] {
    --fx-color-ground:        #07182b;
    --fx-color-bg:            #0f2a46;
    --fx-color-surface:       #0a2138;
    --fx-color-surface-2:     #12314f;
    --fx-color-zebra:         #0c2440;

    --fx-color-ink:           #eef5fc;
    --fx-color-muted:         #b0c6db;
    --fx-color-border:        #1e3a55;
    --fx-color-border-strong: #5a80a3;

    /* Gold with a near-black label, rather than a dark fill with a white one.
       On a near-black ground a dark bronze button barely separates from the
       page, and a light gold at 4.5:1 against WHITE does not exist. Flipping
       the label is the honest fix and it is the better-looking one. */
    --fx-color-primary:       #1ec5da;
    --fx-color-primary-lift:  #2ad0e4;
    --fx-color-primary-deep:  #17b0c4;
    --fx-color-primary-hover: #42ddef;
    --fx-color-on-primary:    #042028;
    --fx-color-primary-tint:  #0d3547;
    --fx-color-accent-ink:    #42ddef;

    --fx-color-brass:         #42ddef;
    --fx-color-brass-solid:   #1ec5da;
    --fx-color-brass-tint:    #0d3547;
    --fx-color-on-brass:      #042028;

    --fx-color-ok:            #37d99a;
    --fx-color-ok-solid:      #37d99a;
    --fx-color-on-ok:         #04231a;
    --fx-color-ok-tint:       #0d2e26;
    --fx-color-warn:          #f0a165;
    --fx-color-warn-solid:    #f0a165;
    --fx-color-warn-tint:     #2e2416;
    --fx-color-bad:           #ff9aa0;
    --fx-color-bad-solid:     #ff9aa0;
    --fx-color-bad-tint:      #331920;
    --fx-color-danger-bg:     #ff9aa0;
    --fx-color-danger-hover:  #ffbcc0;
    --fx-color-on-danger:     #2a0b0d;
    --fx-color-neutral:       #b0c6db;
    --fx-color-neutral-tint:  #122b45;
    --fx-color-info:          #42ddef;
    --fx-color-info-tint:     #0d3547;

    --fx-color-focus:         #42ddef;
    --fx-color-focus-on-dark: #42ddef;
    /* Dark elevation still needs real depth; the light values are invisible on
       a near-black ground. */
    --fx-color-selection-bg:  #1ec5da;
    --fx-color-selection-fg:  #042028;

    /* One step above the ground, exactly as a card is. The rail is chrome, not
       a feature. */
    --fx-color-railbg:        #0f2a46;
    --fx-color-rail-2:        #0f2a46;
    --fx-color-rail-ink:      #eef5fc;
    --fx-color-rail-muted:    #b0c6db;
    --fx-color-rail-line:     #1e3a55;
    --fx-color-rail-accent:   #42ddef;
    --fx-color-keyline:       #1d5064;

    --fx-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45),
                   0 3px 8px rgba(0, 0, 0, 0.35);
    --fx-shadow-2: 0 2px 6px rgba(0, 0, 0, 0.50),
                   0 12px 28px -6px rgba(0, 0, 0, 0.55);
    --fx-shadow-3: 0 4px 12px rgba(0, 0, 0, 0.55),
                   0 24px 56px -10px rgba(0, 0, 0, 0.70);
    --fx-hi:       inset 0 1px 0 rgba(255, 255, 255, 0.07);
    --fx-hi-soft:  inset 0 1px 0 rgba(255, 255, 255, 0.04);
    --fx-lo:       inset 0 -1px 0 rgba(0, 0, 0, 0.35);
  }

:root[data-theme="dark"] { --fx-block-shadow: 0 1px 4px rgba(0, 0, 0, 0.55); }

:root[data-theme="dark"] {
    --fx-tone-neutral-bg:      #0f2a46;
    --fx-tone-neutral-ink:     #eef5fc;
    --fx-tone-neutral-muted:   #b0c6db;
    --fx-tone-neutral-surface: #0a2138;
    --fx-tone-neutral-border:  #1e3a55;
    --fx-tone-neutral-block:   transparent;

    --fx-tone-ok-bg:           #0d2e26;
    --fx-tone-ok-ink:          #5fe0ac;
    --fx-tone-ok-muted:        #8fc4ad;
    --fx-tone-ok-surface:      #10352c;
    --fx-tone-ok-border:       #1d4a3c;
    --fx-tone-ok-block:        #37d99a;

    --fx-tone-warn-bg:         #2e2416;
    --fx-tone-warn-ink:        #f0ac6a;
    --fx-tone-warn-muted:      #c8a075;
    --fx-tone-warn-surface:    #382c1b;
    --fx-tone-warn-border:     #4b3c22;
    --fx-tone-warn-block:      #d98a3a;

    --fx-tone-bad-bg:          #331920;
    --fx-tone-bad-ink:         #ff9aa0;
    --fx-tone-bad-muted:       #d99196;
    --fx-tone-bad-surface:     #3b1e26;
    --fx-tone-bad-border:      #4f2833;
    --fx-tone-bad-block:       #e0555d;

    --fx-tone-info-bg:         #0d3547;
    --fx-tone-info-ink:        #7fe3f2;
    --fx-tone-info-muted:      #a8cfdc;
    --fx-tone-info-surface:    #114055;
    --fx-tone-info-border:     #1d5064;
    --fx-tone-info-block:      #1ec5da;

    --fx-tone-accent-bg:       #1ec5da;
    --fx-tone-accent-ink:      #042028;
    --fx-tone-accent-muted:    #05303a;
    --fx-tone-accent-surface:  #17b0c4;
    --fx-tone-accent-border:   #17b0c4;
    --fx-tone-accent-block:    #042028;
    --fx-tone-accent-focus:   #042028;   /* 9.21:1 on the gold surface */
  }

:root[data-theme="dark"] .lattice {
    --lat-k0:  oklch(0.72 0.16 255);
    --lat-k1:  oklch(0.78 0.13 215);
    --lat-k2:  oklch(0.75 0.15 145);
    --lat-k3:  oklch(0.80 0.14 110);
    --lat-k4:  oklch(0.82 0.14 85);
    --lat-k5:  oklch(0.76 0.15 55);
    --lat-k6:  oklch(0.70 0.17 25);
    --lat-k7:  oklch(0.71 0.17 355);
    --lat-k8:  oklch(0.72 0.16 325);
    --lat-k9:  oklch(0.71 0.16 295);
    --lat-k10: oklch(0.70 0.16 275);
    --lat-k11: oklch(0.79 0.10 240);
    --lat-k12: oklch(0.70 0.09 180);
    --lat-k13: oklch(0.74 0.02 260);
  }

:root[data-theme="dark"] {
    --fx-shadow-1: 0 12px 30px -18px rgba(0, 0, 0, 0.75);
    --fx-shadow-2: 0 22px 48px -22px rgba(0, 0, 0, 0.80);
    --fx-shadow-3: 0 44px 96px -34px rgba(0, 0, 0, 0.90);
    /* The gold set needs a dark theme of its own. Left unoverridden, --gold-tint
       stayed the light cream #fdf4dd while --fx-color-ink went near-white, so any
       gold surface would have carried invisible text. The gold itself is unchanged
       — it reads well on navy — only the tint and its label move. */
    --fx-color-gold:      #f5b81f;
    --fx-color-gold2:     #e0a300;
    --fx-color-gold-tint: #2e2611;
    --fx-color-on-gold:   #26200a;
  }

:root[data-theme="dark"] .alert__x:hover { background: rgba(255, 255, 255, .1); }
/* ==== END GENERATED DARK MIRROR ==== */


/* ---- show password ------------------------------------------------------- */
/*
 * The eye sits inside the field rather than beside it, so the input keeps the
 * full width it was designed at and the control cannot be mistaken for a second
 * field. Padding on the input, not a narrower input, so the caret never runs
 * underneath the button.
 */
.pw { position: relative; display: block; }
.pw__input { width: 100%; padding-right: 46px; }
.pw__eye {
  position: absolute; top: 50%; right: 4px; transform: translateY(-50%);
  width: 38px; height: 38px; display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; cursor: pointer; border-radius: var(--fx-radius-sm, 8px);
  font-size: 1rem; line-height: 1; opacity: .6; color: var(--fx-color-ink);
  transition: opacity var(--fx-dur-2, 160ms) var(--fx-ease-out, ease);
}
.pw__eye:hover { opacity: 1; background: var(--fx-color-surface-2); }
.pw__eye[aria-pressed="true"] { opacity: 1; color: var(--fx-color-primary); }
.pw__eye:focus-visible { outline: 2px solid var(--fx-color-focus); outline-offset: 2px; opacity: 1; }
.pw__eye[hidden] { display: none; }

/* ---- dismissable notices -------------------------------------------------- */
/*
 * The close control sits in the alert's own corner rather than in its text
 * column, so it never reflows the sentence it is closing. Absolute inside a
 * relative alert, and the alert gains right padding so a long line cannot run
 * underneath it.
 */
.alert { position: relative; }
.alert:has(.alert__x) { padding-right: 44px; }
.alert__x {
  position: absolute; top: 8px; right: 8px;
  width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; cursor: pointer; border-radius: var(--fx-radius-sm, 8px);
  font-size: 1.15rem; line-height: 1; color: currentColor; opacity: .55;
  transition: opacity var(--fx-dur-2, 160ms) var(--fx-ease-out, ease), background var(--fx-dur-2, 160ms) var(--fx-ease-out, ease);
}
.alert__x:hover { opacity: 1; background: rgba(0, 0, 0, .07); }
.alert__x:focus-visible { outline: 2px solid var(--fx-color-focus); outline-offset: 2px; opacity: 1; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .alert__x:hover { background: rgba(255, 255, 255, .1); }
}
