/* ============================================================================
   FieldOps design system — the single source of colour, type and spacing.

   WHY THIS FILE EXISTS. Before it, app.css was the stock Blazor template (five
   colours, all of them from the error boundary) and every page carried its own
   <style> block. Across 21 pages that produced 607 hardcoded colours and 118
   distinct shades — mostly greens invented one page at a time. That is not a
   styling problem, it is the absence of a system, and it is what William meant
   by "messy busy and the colors are blah... it looks like a 10 year old kid
   built it" (2026-08-13).

   THE RULE THAT MATTERS: pages must not name colours. They use these tokens.
   A page with a #hex in it has escaped the system and will drift again.

   Because everything routes through here, re-tinting the whole product later is
   editing the block below — not revisiting 21 files.

   WHY IT LIVES IN FieldOps.UI AND NOT THE WEBSITE. FieldOps.UI is a shared Razor
   class library: Dashboard and OperatorScoreDetail render on the WEBSITE and
   inside the CREW APP from these same files. Parked in the website's wwwroot,
   the tokens reached the website only, and those two components rendered in the
   app with every var() undefined — i.e. unstyled. From here both ends load the
   one file as _content/FieldOps.UI/theme.css.

   NOTHING IN THIS FILE MAY TARGET A BARE ELEMENT (body, input, a). It is loaded
   by the crew app too, whose pages style plain `input` for gloved hands; an
   `input[type=password]` rule here outranks their `input` rule and would shrink
   every field on a phone. Element-level rules live in the website's app.css,
   which only the website loads.
   ============================================================================ */

/* ══ COLOURS ARE GENERATED (house style piece 0a, 2026-10-02) ════════════════════════════════════
   Every colour — dark AND light, the six meanings, the table rows, the chrome, and the old names
   (--paper, --ink-3, --brand-purple …) as aliases — is written between the markers below by
   `dotnet run --project backend/FieldOps.Cli -- gen-theme`, from FieldOps.Contracts/HouseTokens.cs.
   The crew app reads the same file, so the two surfaces cannot drift. Edit THAT file, not this block;
   `check-theme` fails when they disagree.

   WHAT THE OLD COMMENTS HERE SAID ABOUT THE PALETTE (black chrome, purple = what you click, white
   paper) NO LONGER HOLDS: William, 2026-10-02 — Sturdy Ox yellow is the click colour (filled, with
   #1a1a1a text), purple survives only as the "a person's decision" meaning, the house default is the
   dark slate theme, and a cream light theme is one switch away (Match device / Dark / Light, stored
   per person). The rationale for each value is beside it in HouseTokens.cs. */

/* BEGIN GENERATED TOKENS */
/* ⚠️ GENERATED by `dotnet run --project backend/FieldOps.Cli -- gen-theme` from
   FieldOps.Contracts/HouseTokens.cs. Do not edit between the markers: edit HouseTokens.cs and
   re-run it; `check-theme` fails when this block and that file disagree. The crew app reads
   the SAME file (Views/ThemeTokens.g.cs, Resources/Styles/NativeTheme.xaml), so the two cannot drift.

   Dark is the default (the house theme). data-theme on <html> is stamped from the signed-in
   person's setting: "dark", "light", or "auto" (Match device). auto follows the OS; where the
   device has no preference the media query does not match, so it stays dark. The light block is
   written twice (auto on a light OS, and an explicit light) from one function, so they are identical. */
:root, .fo-theme-dark {
    color-scheme: dark;
    --color-background: #18212e;   /* the page. Dark: #0f172a 95% + identity 5%. Light: Berean's cream. */
    --color-card: #2b363f;   /* cards, header bar, table bodies. Dark: #1e293b 93% + identity 7%. Light: never #FFFFFF. */
    --color-popover: #2b363f;   /* = card */
    --color-sunk: #252f38;   /* table heads, inputs, inset areas. Dark: #172033 92% + identity 8%. */
    --color-secondary: #414e58;   /* shadcn surface. Dark: #334155 92% + identity 8%. */
    --color-muted: #414e58;   /* = secondary */
    --color-accent: #414e58;   /* = secondary */
    --color-border: #414e58;   /* every hairline. Dark: #334155 92% + identity 8%. */
    --color-input: #414e58;   /* = border */
    --color-border-strong: #54616a;   /* ext: dark #475569 92% + identity 8%; light = hairline 70% + muted text 30% (oklab). */
    --color-foreground: #e2e8f0;   /* body text */
    --color-card-foreground: #f8fafc;   /* titles, values */
    --color-popover-foreground: #f8fafc;   /* = card-foreground */
    --color-secondary-foreground: #e2e8f0;   /* = foreground */
    --color-accent-foreground: #f8fafc;   /* = card-foreground */
    --color-muted-foreground: #94a3b8;   /* subtitles, labels, freshness line */
    --color-foreground-2: #cbd5e1;   /* ext: the old --ink-2, secondary text */
    --color-identity: #fcf000;   /* Sturdy Ox yellow. Chrome + the primary fill. */
    --color-identity-foreground: #1a1a1a;   /* text ON the identity: #1a1a1a in both themes */
    --color-identity-bright: #fff64d;   /* hover / bright shade of the identity */
    --color-identity-text: #fcf000;   /* identity as TEXT. Dark: the yellow (it is light). Light: ink, never yellow on cream. */
    --color-identity-hover: #cbc100;   /* a filled primary's hover: identity 85% + black 15% (darker, never brighter) */
    --color-identity-dim: #c9be00;   /* ext: the old --brand-yellow-dim, an outline for a yellow pill */
    --color-primary: #fcf000;   /* = identity: the safe action, the active chip */
    --color-primary-foreground: #1a1a1a;   /* = identity-foreground */
    --color-link: #fcf000;   /* ext: click-text. Dark: yellow. Light: ink (yellow text on cream is unreadable). */
    --color-link-hover: #fff64d;   /* ext: click-text hover. Dark: bright yellow. Light: black. */
    --color-ring: #fcf000;   /* keyboard focus ring = identity */
    --color-destructive: #ef4444;   /* danger border / outline */
    --color-destructive-foreground: #ffffff;   /* text on a filled destructive (kept for shadcn; a danger BUTTON is outlined) */
    --color-good: #10b981;   /* good / automatic */
    --color-good-text: #6ee7b7;   /* good, as text */
    --color-good-bg: #233e47;   /* good badge fill */
    --color-suggested: #3b82f6;   /* suggested */
    --color-suggested-text: #93c5fd;   /* suggested, as text */
    --color-suggested-bg: #233656;   /* suggested badge fill */
    --color-attention: #f59e0b;   /* needs attention */
    --color-attention-text: #fcd34d;   /* attention, as text */
    --color-attention-bg: #3d3c40;   /* attention badge fill */
    --color-decision: #8b5cf6;   /* a person's decision (violet stays ONLY as this meaning) */
    --color-decision-text: #c4b5fd;   /* decision, as text */
    --color-decision-bg: #2e3256;   /* decision badge fill */
    --color-neutral: #94a3b8;   /* keep / neutral */
    --color-neutral-text: #cbd5e1;   /* neutral, as text */
    --color-neutral-bg: #2f3b4d;   /* neutral badge fill */
    --color-fault: #ef4444;   /* fault / danger */
    --color-fault-text: #fca5a5;   /* fault, as text */
    --color-fault-bg: #3f323f;   /* fault badge fill */
    --color-row-a: #222b32;   /* odd rows. Dark: #19222f 95% + identity 5%. Light: = card. */
    --color-row-b: #303d4b;   /* even rows. Dark: #243148 93% + identity 7%. */
    --color-row-hover: #586048;   /* row under the pointer: dark #1e293b 70% + identity 30% (oklab); light = the approved #FBF6AB. */
    --color-row-press: #797d4a;   /* a pressed / opened row: dark 55% + identity 45% (oklab); light card 55% + identity 45% (sRGB). */
    --color-col-stripe: rgba(255, 255, 255, 0.035);   /* every other column, over the row shade */
    --color-chrome: #2b363f;   /* ext: header bar + table head fill (the old --brand-black). Dark = card; light = black. */
    --color-chrome-soft: #18212e;   /* ext: side rail fill (the old --brand-black-soft). Dark = canvas. */
    --color-chrome-foreground: #f8fafc;   /* ext: text on the chrome */
    --color-chrome-muted: #cbd5e1;   /* ext: quiet text on the chrome (the old --rail-ink) */
    --color-chrome-wash: rgba(255, 255, 255, 0.15);   /* ext: a translucent lift for a button on the chrome (the app header's buttons) */
    --color-zebra-row: rgba(62, 79, 100, 0.5);   /* the website's even-row wash: a 50% overlay that turns row-a into row-b, so a state colour under it still shows */
    --color-stripe-on-row-a: #2a3239;   /* the app's odd-row, even-column cell: col-stripe flattened onto row-a */
    --color-stripe-on-row-b: #374451;   /* the app's even-row, even-column cell: col-stripe flattened onto row-b */

    --font-sans: "Inter", "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
    /* Widths (house rule 1): the page has NO maximum width; what is capped is the thing that
       gets hard to read when it stretches — a block of text (80ch). */
    --read-width: 80ch;
    --gutter: 24px;
}
@media (max-width: 639px) { :root, .fo-theme-dark { --gutter: 16px; } }

/* The OLD FieldOps names, kept alive as aliases so the ~40 page stylesheets re-theme untouched.
   One rule for the root and both wrappers, so each resolves against ITS OWN --color-* values. */
:root, .fo-theme-dark, .fo-theme-light {
    --paper: var(--color-card);   /* the old white card/surface. (Also used as text ON a fill: the page stylesheets that did that are covered by the compat block in theme.css.) */
    --paper-2: var(--color-sunk);   /* the old off-white inset/hover surface */
    --line: var(--color-border);   /* hairline */
    --line-strong: var(--color-border-strong);   /* input borders, chip outlines */
    --ink: var(--color-foreground);   /* body text */
    --ink-2: var(--color-foreground-2);   /* secondary text */
    --ink-3: var(--color-muted-foreground);   /* labels, subtitles */
    --brand-black: var(--color-chrome);   /* the header / table-head fill */
    --brand-black-soft: var(--color-chrome-soft);   /* the side-rail fill */
    --brand-yellow: var(--color-identity);   /* the identity yellow */
    --brand-yellow-dim: var(--color-identity-dim);   /* a yellow pill's outline */
    --rail-ink: var(--color-chrome-muted);   /* quiet text on the chrome */
    --brand-purple: var(--color-link);   /* click-text/outline. Fills are re-pointed at --color-primary in theme.css's compat block. */
    --brand-purple-dk: var(--color-link-hover);   /* click-text hover */
    --brand-purple-lt: var(--color-row-hover);   /* the soft highlight behind a hovered or selected row */
    --good: var(--color-good-text);   /* good, as text (a fill that used it keeps working: it just reads light-on-dark) */
    --bad: var(--color-fault-text);   /* fault, as text */
    --warn: var(--color-attention-text);   /* attention, as text */
    --good-bg: var(--color-good-bg);   /* badge fill */
    --bad-bg: var(--color-fault-bg);   /* badge fill */
    --warn-bg: var(--color-attention-bg);   /* badge fill */
    --zebra-col: var(--color-col-stripe);   /* every other column */
    --zebra-row: var(--color-zebra-row);   /* every other row (a derived overlay, see Derived) */
}

@media (prefers-color-scheme: light) {
    :root[data-theme="auto"] {
        color-scheme: light;
        --color-background: #f7f4ef;
        --color-card: #fbf9f4;
        --color-popover: #fbf9f4;
        --color-sunk: #efebe3;
        --color-secondary: #efebe3;
        --color-muted: #efebe3;
        --color-accent: #efebe3;
        --color-border: #e3ded4;
        --color-input: #e3ded4;
        --color-border-strong: #bdbab3;
        --color-foreground: #2c2c2c;
        --color-card-foreground: #1a1a1a;
        --color-popover-foreground: #1a1a1a;
        --color-secondary-foreground: #2c2c2c;
        --color-accent-foreground: #1a1a1a;
        --color-muted-foreground: #6b6b6b;
        --color-foreground-2: #4a4a4a;
        --color-identity: #fcf000;
        --color-identity-foreground: #1a1a1a;
        --color-identity-bright: #fff64d;
        --color-identity-text: #1a1a1a;
        --color-identity-hover: #cbc100;
        --color-identity-dim: #c9be00;
        --color-primary: #fcf000;
        --color-primary-foreground: #1a1a1a;
        --color-link: #1a1a1a;
        --color-link-hover: #000000;
        --color-ring: #fcf000;
        --color-destructive: #ef4444;
        --color-destructive-foreground: #ffffff;
        --color-good: #10b981;
        --color-good-text: #047857;
        --color-good-bg: #d1fae5;
        --color-suggested: #3b82f6;
        --color-suggested-text: #1d4ed8;
        --color-suggested-bg: #dbeafe;
        --color-attention: #f59e0b;
        --color-attention-text: #92400e;
        --color-attention-bg: #fef3c7;
        --color-decision: #8b5cf6;
        --color-decision-text: #5b21b6;
        --color-decision-bg: #ede9fe;
        --color-neutral: #94a3b8;
        --color-neutral-text: #475569;
        --color-neutral-bg: #f1f5f9;
        --color-fault: #ef4444;
        --color-fault-text: #b91c1c;
        --color-fault-bg: #fee2e2;
        --color-row-a: #fbf9f4;
        --color-row-b: #f3efe6;
        --color-row-hover: #fbf6ab;
        --color-row-press: #fbf586;
        --color-col-stripe: rgba(0, 0, 0, 0.028);
        --color-chrome: #1a1a1a;
        --color-chrome-soft: #262626;
        --color-chrome-foreground: #fbf9f4;
        --color-chrome-muted: #d6d6d6;
        --color-chrome-wash: rgba(255, 255, 255, 0.15);
        --color-zebra-row: rgba(235, 229, 216, 0.5);
        --color-stripe-on-row-a: #f4f2ed;
        --color-stripe-on-row-b: #ece8e0;
    }
}
:root[data-theme="light"], .fo-theme-light {
    color-scheme: light;
    --color-background: #f7f4ef;
    --color-card: #fbf9f4;
    --color-popover: #fbf9f4;
    --color-sunk: #efebe3;
    --color-secondary: #efebe3;
    --color-muted: #efebe3;
    --color-accent: #efebe3;
    --color-border: #e3ded4;
    --color-input: #e3ded4;
    --color-border-strong: #bdbab3;
    --color-foreground: #2c2c2c;
    --color-card-foreground: #1a1a1a;
    --color-popover-foreground: #1a1a1a;
    --color-secondary-foreground: #2c2c2c;
    --color-accent-foreground: #1a1a1a;
    --color-muted-foreground: #6b6b6b;
    --color-foreground-2: #4a4a4a;
    --color-identity: #fcf000;
    --color-identity-foreground: #1a1a1a;
    --color-identity-bright: #fff64d;
    --color-identity-text: #1a1a1a;
    --color-identity-hover: #cbc100;
    --color-identity-dim: #c9be00;
    --color-primary: #fcf000;
    --color-primary-foreground: #1a1a1a;
    --color-link: #1a1a1a;
    --color-link-hover: #000000;
    --color-ring: #fcf000;
    --color-destructive: #ef4444;
    --color-destructive-foreground: #ffffff;
    --color-good: #10b981;
    --color-good-text: #047857;
    --color-good-bg: #d1fae5;
    --color-suggested: #3b82f6;
    --color-suggested-text: #1d4ed8;
    --color-suggested-bg: #dbeafe;
    --color-attention: #f59e0b;
    --color-attention-text: #92400e;
    --color-attention-bg: #fef3c7;
    --color-decision: #8b5cf6;
    --color-decision-text: #5b21b6;
    --color-decision-bg: #ede9fe;
    --color-neutral: #94a3b8;
    --color-neutral-text: #475569;
    --color-neutral-bg: #f1f5f9;
    --color-fault: #ef4444;
    --color-fault-text: #b91c1c;
    --color-fault-bg: #fee2e2;
    --color-row-a: #fbf9f4;
    --color-row-b: #f3efe6;
    --color-row-hover: #fbf6ab;
    --color-row-press: #fbf586;
    --color-col-stripe: rgba(0, 0, 0, 0.028);
    --color-chrome: #1a1a1a;
    --color-chrome-soft: #262626;
    --color-chrome-foreground: #fbf9f4;
    --color-chrome-muted: #d6d6d6;
    --color-chrome-wash: rgba(255, 255, 255, 0.15);
    --color-zebra-row: rgba(235, 229, 216, 0.5);
    --color-stripe-on-row-a: #f4f2ed;
    --color-stripe-on-row-b: #ece8e0;
}
/* END GENERATED TOKENS */

:root {
    /* ---- type scale. Four sizes and two weights, so the eye has somewhere to
       land. The old pages ran everything between .75 and 1rem, which is why a
       $146,884 total looked exactly like a column label.                      */
    --fs-display: 1.75rem;
    --fs-title:   1.15rem;
    --fs-body:    0.9rem;
    --fs-small:   0.78rem;

    /* ---- spacing: one 4px rhythm, so gaps stop being guessed per page ----- */
    --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
    --s5: 24px; --s6: 32px; --s7: 48px;

    /* ---- the house sizes (palettes.md "Sizes"; piece 0b) ---- */
    --hs-title: 17px;          /* page title (weight 650) */
    --hs-ui: 13px;             /* body text, buttons */
    --hs-dense: 12px;          /* tables and chips */
    --hs-row: 32px;            /* a table row */
    --hs-btn-radius: 8px;
    --hs-tile-max: 20rem;      /* a tile never grows past 320px */

    --radius:    10px;
    --radius-sm: 6px;
    --shadow:    0 1px 2px rgba(20,20,15,.06), 0 4px 16px rgba(20,20,15,.06);
}

/* ---- page shell ---------------------------------------------------------- */
/* HOUSE RULE 1 (2026-10-02): PAGES FILL THE WINDOW — no maximum page width. This was
   `max-width: 1400px; margin: 0 auto`, and `.wide` raised it to 1700px for the map pages; both caps are
   gone, so the header, title and tables reach the same gutters at both edges on any monitor. What is
   capped now is the thing that gets hard to read when it stretches — a block of text, at --read-width
   (80ch): see .fo-note and .fo-read. The gutter is 24px, 16px below 640px. (A few page stylesheets still
   set their own max-width on their own wrapper, e.g. .fo-page.opgrade; they are migrated page by page.) */
.fo-page { padding: var(--s5) var(--gutter) var(--s7); }
/* Kept so existing markup (`fo-page wide`) still parses; it no longer changes anything. */
.fo-page.wide { }
.fo-read { max-width: var(--read-width); }

/* Page heading: title left, actions right, a hairline under. One per page —
   the old pages opened with a heading, a paragraph, a banner and a note before
   any data appeared. */
.fo-head { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
           padding-bottom: var(--s3); border-bottom: 2px solid var(--color-chrome);
           margin-bottom: var(--s5); }
.fo-head h1 { font-size: var(--fs-display); font-weight: 700; margin: 0; letter-spacing: -.01em; }
.fo-head .sub { color: var(--ink-3); font-size: var(--fs-small); }
.fo-head .spacer { margin-left: auto; }

/* ---- numbers are the hero ------------------------------------------------
   Money and scores get size and tabular figures; labels shrink and go grey.  */
.fo-figure { font-size: var(--fs-display); font-weight: 700; font-variant-numeric: tabular-nums;
             letter-spacing: -.02em; }
.fo-label  { font-size: var(--fs-small); color: var(--ink-3); text-transform: uppercase;
             letter-spacing: .06em; }
.num       { font-variant-numeric: tabular-nums; }

/* ---- cards --------------------------------------------------------------- */
.fo-card { background: var(--paper); border: 1px solid var(--line);
           border-radius: var(--radius); box-shadow: var(--shadow); padding: var(--s4); }
.fo-cards { display: grid; gap: var(--s3);
            grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }

/* ---- tables --------------------------------------------------------------
   Black header, not brand-coloured: the header is structure, and colouring it
   competes with the data underneath.                                          */
.fo-table { width: 100%; border-collapse: collapse; background: var(--color-row-a); }
.fo-table th, .fo-table td { padding: var(--s2) var(--s3); text-align: right; white-space: nowrap; }
.fo-table thead th { background: var(--color-chrome); color: var(--color-chrome-foreground); font-weight: 600;
                     font-size: var(--fs-small); text-transform: uppercase; letter-spacing: .05em;
                     position: sticky; top: 0; z-index: 2; }
.fo-table th:first-child, .fo-table td:first-child { text-align: left; }
.fo-table tbody tr { border-bottom: 1px solid var(--line); }
.fo-table tbody tr:hover td { background: var(--color-row-hover); }
.fo-table tfoot th { background: var(--color-sunk); font-weight: 700; border-top: 2px solid var(--color-chrome); }
.fo-table td.money, .fo-table th.money { font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---- zebra: every other ROW and every other COLUMN, on every table ----------
   (2026-10-02, house rule 5) The values are the house row-a / row-b / col-stripe: a table's base is
   row-a, --zebra-row is the generated 50% overlay that turns row-a into row-b (translucent on purpose,
   so a state colour under it still shows), and --zebra-col is col-stripe, laid over the row shade.
   William, 2026-09-28: "make every other line and column slightly different tint so it's easier to
   fallow rows and columns" - on ALL pages, so this is on bare `table`, not .fo-table: several pages
   draw their own (.rt-tbl on Rates, the billing tree, pivots). Header rows (thead) keep their own look.
   - ROWS paint a translucent background-IMAGE, not a colour: a row that already means something
     (warn, bad, orphan, hover, the row being edited) keeps its own colour, just a shade darker.
   - COLUMNS paint an inset shadow, which sits over any background, so the column band carries on
     straight through a coloured row instead of breaking at it.
   - `of :not([hidden])` counts only the rows on screen: the filter bars hide rows with [hidden], and
     plain nth-child would then put two tinted rows together wherever one was filtered out.
   - A table that must not stripe (a layout table) opts out with class="no-zebra". */
/* A bare table with no background of its own starts from row-a (low specificity: a page's own wins). */
table:not(.no-zebra) { background-color: var(--color-row-a); }
table:not(.no-zebra) > tbody > tr:nth-child(even of :not([hidden])) > td,
table:not(.no-zebra) > tbody > tr:nth-child(even of :not([hidden])) > th {
    background-image: linear-gradient(var(--zebra-row), var(--zebra-row));
}
table:not(.no-zebra) > tbody > tr > td:nth-child(even),
table:not(.no-zebra) > tbody > tr > th:nth-child(even),
table:not(.no-zebra) > tfoot > tr > th:nth-child(even) {
    box-shadow: inset 0 0 0 100vmax var(--zebra-col);
}

/* Wide tables scroll in their own box — never the page body, which would drag
   any sticky column off-screen with it. */
.fo-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius);
             background: linear-gradient(to left, rgba(20,20,15,.08), rgba(20,20,15,0) 24px) right / 24px 100% no-repeat; }
.fo-scroll .fo-table { border: 0; }

/* ⚠️ THE HEADER ONLY STICKS IF SOMETHING BOUNDS THE BOX. `position: sticky` above needs a
   scrolling ANCESTOR; .fo-scroll only ever set overflow-X, so on every table but the two pages that
   capped it themselves the rule was written, shipped, and did nothing - scroll a long list and the
   column headings left with it. Audited 2026-09-16: working on 7 tables, silently dead on the rest.
   One rule here fixes all of them. A table shorter than the cap is untouched: max-height never
   stretches anything, it only stops the long ones. */
.fo-scroll { max-height: min(72vh, 780px); overflow-y: auto; }

/* Lists are not money. .fo-table right-aligns because it was built for invoices, and eleven pages
   then turned that back off one at a time in their own stylesheets. This is that patch, written
   once: put `list` on the table and every column reads from the left. */
.fo-table.list th, .fo-table.list td { text-align: left; }
.fo-table.list td.num, .fo-table.list th.num,
.fo-table.list td.money, .fo-table.list th.money { text-align: right; }

/* ---- sortable columns ----------------------------------------------------
   William, 2026-09-16, asked for these outright. Nothing in the product could
   be sorted before - not one of ~38 tables. The header carries the affordance;
   the page supplies the click. An arrow only ever shows on the sorted column,
   so the header does not turn into a row of decorations. */
.fo-table th.sort { cursor: pointer; user-select: none; }
.fo-table th.sort:hover { background: var(--color-chrome-soft); }
.fo-table th.sort::after { content: ""; display: inline-block; width: .7em; opacity: .0; }
.fo-table th.sort.asc::after  { content: "↑"; opacity: .9; }
.fo-table th.sort.desc::after { content: "↓"; opacity: .9; }

/* ---- a row that opens ----------------------------------------------------
   Seven pages grew their own copy of this - Assets, Breakdowns, People,
   Receipts, Service & parts, Hours and Vendors - because each one converted to
   "the row IS the edit control" separately. Same four declarations, seven
   times, in seven files, each with its own prefix.

   ⚠️ THE CHEVRON IS NOT DECORATION AND MUST NOT BE "TIDIED AWAY". cursor:pointer
   and :hover are MOUSE-ONLY hints, and William works from a tablet in a combine.
   Without the drawn marker in the last column there is no way to tell a row that
   opens from one that does not. That is why .chev is here beside the pointer
   rather than left to each page: they are one affordance, not two.

   A page adds .click to a row it will open - and only when it really will open,
   because a pointer on a row that does nothing is a lie (Breakdowns gates it on
   admin for exactly that reason). */
.fo-table tbody tr.click { cursor: pointer; }
/* th as well as td: width:1% + nowrap is the CSS way to say "shrink to fit", and it only works
   if the HEADER cell shrinks too - otherwise the column keeps whatever the header claims. */
.fo-table th.rowact,
.fo-table td.rowact { width: 1%; text-align: right; white-space: nowrap; }
.fo-table .chev { color: var(--ink-3); }
.fo-table tbody tr.click:hover .chev { color: var(--color-link); }

/* ---- buttons -------------------------------------------------------------
   ONE primary per view. William on the grading page: "there is a million
   buttons all seem to do the same thing but I dont know for sure so when I edit
   something I click everything just incase." A screen with three equal-looking
   buttons is a screen nobody can act on confidently.                          */
/* ONE SIZE (house rule 9, piece 0b): 13px text, 8px 14px padding, radius 8px — for primary, secondary and
   danger alike. It was 0.9rem / 8px 16px / 6px, so every button on every page shrinks by a hair, which is
   the point: a screen of buttons that are all one size reads as one set. */
.fo-btn { display: inline-flex; align-items: center; gap: var(--s2);
          padding: 8px 14px; border-radius: var(--hs-btn-radius);
          border: 1px solid var(--line-strong); background: var(--paper); color: var(--ink);
          font-size: var(--hs-ui); font-weight: 600; cursor: pointer; }
.fo-btn:hover:not(:disabled) { background: var(--paper-2); border-color: var(--ink-3); }
.fo-btn:disabled { opacity: .45; cursor: not-allowed; }
/* PRIMARY = filled Sturdy Ox yellow with #1a1a1a text, in BOTH themes (2026-10-02; purple retired). */
.fo-btn.primary { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-foreground); }
.fo-btn.primary:hover:not(:disabled) { background: var(--color-identity-hover); border-color: var(--color-identity-hover); }
/* DANGER is OUTLINED red, never filled (house rule 9, 2026-10-02) — a filled red would read as the
   primary on any company whose colour is red, and "this changes things" is told by the outline.
   The confirm step is confirm.js (section 6 below); the ⚠ lives only there. */
.fo-btn.danger  { background: transparent; border-color: var(--color-destructive); color: var(--color-fault-text); }
.fo-btn.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--color-destructive) 15%, transparent);
                                      border-color: var(--color-destructive); }
.fo-btn.ghost   { border-color: transparent; background: transparent; color: var(--color-link); }
:where(.fo-btn, a, button, input, select, textarea):focus-visible {
    outline: 3px solid var(--color-ring); outline-offset: 2px; }

/* ---- chips / states ------------------------------------------------------ */
.fo-chip { display: inline-flex; align-items: center; gap: var(--s1);
           padding: 2px var(--s2); border-radius: 999px; font-size: var(--fs-small);
           background: var(--paper-2); border: 1px solid var(--line); color: var(--ink-2); }
.fo-chip.good { background: var(--good-bg); border-color: transparent; color: var(--good); }
.fo-chip.warn { background: var(--warn-bg); border-color: transparent; color: var(--warn); }
.fo-chip.bad  { background: var(--bad-bg);  border-color: transparent; color: var(--bad); }
.score.good { color: var(--good); font-weight: 700; }
.score.warn { color: var(--warn); font-weight: 700; }
.score.bad  { color: var(--bad);  font-weight: 700; }

/* ---- notes ---------------------------------------------------------------
   William: "get rid of all the notes. only very simple necessary ones should
   remain." One quiet style, one line. Anything longer belongs behind .fo-why.  */
.fo-note { color: var(--ink-3); font-size: var(--fs-small); margin: var(--s2) 0 0; max-width: var(--read-width); }
.fo-alert { display: flex; gap: var(--s2); padding: var(--s3);
            border-radius: var(--radius-sm); font-size: var(--fs-small);
            background: var(--warn-bg); color: var(--warn);
            border-left: 3px solid var(--warn); }

/* Explanation on demand, not permanently on the page. */
.fo-why > summary { cursor: pointer; color: var(--color-link); font-size: var(--fs-small);
                    list-style: none; }
.fo-why > summary::-webkit-details-marker { display: none; }
.fo-why > summary::before { content: "ⓘ "; }
.fo-why > div { margin-top: var(--s2); padding: var(--s3); background: var(--paper-2); max-width: var(--read-width);
                border-radius: var(--radius-sm); color: var(--ink-2); font-size: var(--fs-small); }

/* ---- forms ------------------------------------------------------------- */
.fo-field { display: flex; flex-direction: column; gap: var(--s1); margin-bottom: var(--s3); }
.fo-field > span { font-size: var(--fs-small); color: var(--ink-3); }

/* ---- links --------------------------------------------------------------- */

@media (max-width: 700px) {
    :root { --fs-display: 1.4rem; --fs-title: 1.05rem; }
    /* The SIDE padding is the house gutter (--gutter: 24px, 16px below 640px), the same as the header and the sub-tab
       bar, so the title and the page's blocks start exactly under the logo (rule 1). It was a fixed 12px left over
       from before the gutter existed, which put every page 4px left of the header on a phone (house style piece 1,
       found on the Clock page at 375px). Only the top and bottom are phone-specific. */
    .fo-page { padding: var(--s4) var(--gutter) var(--s6); }
}

/* ---- group-by chips (FieldOps.UI/GroupByChips.razor) ----------------------
   Shared by the website and the app, so the styles live here rather than on any
   one page. Left to right IS top to bottom in the table, so the control looks
   like the thing it controls. Arrows rather than drag: drag does not fire for
   touch at all, and this has to work on a phone. */
.fo-groupby { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
              margin: var(--s3) 0 var(--s2); }
.fo-groupby .gb-lbl { font-size: var(--fs-small); color: var(--ink-2); margin-right: var(--s1); }
.fo-groupby .gb-chip { display: inline-flex; align-items: center; gap: 2px;
                       border: 1px solid var(--line-strong); border-radius: 999px;
                       padding: 2px 4px 2px 2px; background: var(--paper);
                       font-size: var(--fs-small); }
.fo-groupby .gb-chip .nm { padding: 0 var(--s1); font-weight: 600; color: var(--ink); }
/* Big enough to hit with a thumb: 30px is about the smallest reliable touch target. */
.fo-groupby .gb-chip button { border: 0; background: none; cursor: pointer; color: var(--ink-2);
                              min-width: 30px; min-height: 30px; padding: 0; border-radius: 999px;
                              font-size: var(--fs-small); line-height: 1; }
.fo-groupby .gb-chip button:hover:not(:disabled) { background: var(--paper-2); color: var(--ink); }
.fo-groupby .gb-chip button:disabled { opacity: .3; cursor: default; }
.fo-groupby .gb-chip .drop { color: var(--ink-3); font-size: 1rem; }
.fo-groupby .gb-chip.add { border-style: dashed; color: var(--ink-2); cursor: pointer;
                           padding: 0 var(--s3); min-height: 30px; }
.fo-groupby .gb-chip.add:hover { background: var(--paper-2); color: var(--ink); }
.fo-groupby .gb-sep { color: var(--ink-3); }
.fo-groupby .gb-msg { font-size: var(--fs-small); color: var(--bad); }

/* ══ THE PIECES PAGES ACTUALLY DIFFER ON ═══════════════════════════════════════════════════════
   William, 2026-09-16: "why do we keep having different layouts for different pages that largely
   do the same thing... i feel like somehow you are just slapping code ontop of code rather than
   keeping this thing as uniform as possible."

   He was right, and this file was the reason. Everything above describes things that SIT STILL -
   a page frame, a card, a table, a button. Nothing above describes a filter bar, an input, a
   search box, a side rail, a panel or a map. So every page that needed one typed its own:
   231 lines of shared styling against ~2,151 lines of page-private styling across 28 files.

   HIS RULE FOR WHERE THINGS GO, decided 2026-09-16:
     * controls that NARROW what you are looking at   -> a bar across the TOP (.fo-filters)
     * a browsable LIST of things to click            -> a rail down the LEFT (.fo-listrail)
   That keeps Season > Fields' reading order (Season -> Grower -> Farm -> Field) and Admin > Fields'
   scrollable list of 149 fields, under one rule instead of two accidents.                        */

/* ---- the filter bar ---- */
.fo-filters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s3);
              padding: var(--s3); margin-bottom: var(--s3);
              background: var(--paper); border: 1px solid var(--line);
              border-radius: var(--radius); }
.fo-filters .spacer { margin-left: auto; }

/* A labelled control. The label sits ABOVE the control, so a row of them lines up on the control
   rather than on text of differing length. */
.fo-ctl { display: inline-flex; flex-direction: column; gap: var(--s1);
          font-size: var(--fs-small); color: var(--ink-3); }
.fo-ctl > select, .fo-ctl > input { min-width: 9rem; }

/* ⚠️ .fo-input EXISTED IN THE MARKUP AND NOWHERE ELSE. Two selects on the needs-a-look page carry
   this class and the repo has never defined it - they have been falling back to the bare `select`
   rule in app.css. Defining it here is a bug fix, not a new coat of paint.
   Typed by ELEMENT as well, because a Blazor `<input @bind="x" />` emits NO type attribute, so
   app.css's type-list rule (input[type=text], input[type=number], ...) never matches one. That is
   the mechanical reason six pages each re-typed the same padding/border/radius block. */
.fo-input, .fo-filters select, .fo-filters input,
.fo-panel select, .fo-panel input, .fo-listrail select, .fo-listrail input {
    font: inherit; color: var(--ink); background: var(--paper);
    padding: var(--s2) var(--s3); border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm); }
.fo-input:focus-visible, .fo-filters select:focus-visible, .fo-filters input:focus-visible {
    outline: 2px solid var(--color-ring); outline-offset: 1px; }
.fo-input:disabled { opacity: .5; cursor: not-allowed; }
.fo-search { min-width: 14rem; }

/* ---- the left rail: a browsable list you click ---- */
/* ⚠️ NAMED .fo-listrail, NOT .fo-rail, AND THAT IS NOT A STYLE PREFERENCE.
   `.fo-rail` is ALREADY TAKEN by the site's navigation — MainLayout renders
   <nav class="fo-rail"> and styles it dark (150px, --brand-black-soft, white links). Component
   styles load AFTER this file, so MainLayout wins every tie: a list panel wearing .fo-rail came
   out DARK WITH WHITE TEXT and 150px wide. Caught on Admin › Fields, 2026-09-17, before it
   shipped. Two different things cannot share a class name just because both are "a rail". */
.fo-listrail { flex: 0 0 250px; display: flex; flex-direction: column; min-height: 0;
               background: var(--paper); border: 1px solid var(--line);
               border-radius: var(--radius); overflow: hidden; }
.fo-listrail > .head { padding: var(--s2); border-bottom: 1px solid var(--line); }
.fo-listrail > .list { flex: 1 1 auto; overflow-y: auto; padding: var(--s1); }
.fo-listrail .row { padding: var(--s2); border-radius: var(--radius-sm); cursor: pointer; }
.fo-listrail .row:hover { background: var(--paper-2); }
.fo-listrail .row.on { background: var(--color-row-hover); }

/* ---- the detail panel beside a map or list ---- */
.fo-panel { flex: 0 0 330px; min-height: 0; overflow-y: auto; padding: var(--s3);
            background: var(--paper); border: 1px solid var(--line);
            border-radius: var(--radius); }

/* ---- one map frame ----
   Four map pages used four heights (82vh, 80vh, 78vh, 460px) and four page widths, and none of
   them used .fo-page, so they alone rendered flush against the window edge with no gutter. */
/* ⚠️⚠️ THE HEIGHT LIVES ON THE ROW, NOT ON THE MAP. `align-items: stretch` only grows a child
   that is SHORTER than the row — it never shrinks one that is taller. Admin › Fields' list holds
   152 fields, so with the height on the map alone the list and the panel set the row's height
   themselves and came out 7,406px tall, scrolling the page forever (measured on the live site
   2026-09-17, which is the only way this shows up: every computed style was individually correct).
   A definite height here is also what lets the rail and panel scroll INSIDE themselves, and what
   keeps .fo-mapframe's height definite for Leaflet, which measures its container exactly once. */
.fo-maprow { display: flex; gap: var(--s3); align-items: stretch;
             height: min(78vh, 900px); min-height: 380px; }
/* position:relative because two of the three map pages fill the frame with an absolutely
   positioned Leaflet div (inset: 0) rather than sizing the container itself. Without it those
   maps escape the frame and lay themselves out against the page.
   ⚠️ The height is EXPLICIT on purpose. Leaflet measures its container once, at creation, and a
   percentage of an auto-height parent measures zero — you get a map that exists and draws nothing. */
.fo-mapframe { position: relative; flex: 1 1 auto; height: 100%; min-height: 0;
               border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.fo-maprow.short, .fo-mapframe.short { height: 460px; }
/* .fill — the frame is inside a flex COLUMN that already has the height (because something sits
   above the map inside the same column, like the "what am I looking at" bar on Season › Fields).
   The column keeps the explicit height Leaflet needs; the frame takes what is left of it. */
.fo-mapframe.fill { height: auto; min-height: 0; flex: 1 1 auto; }

/* ---- you are not looking at this season ----
   William, 2026-09-16, on letting the season follow you between pages: "follow but make a color
   band at the top that never scrolls that tells you if your not on the active season so it's
   obvious". Sticky, full width, and only ever rendered when the season being shown is NOT the
   active one - a band that is always there teaches you to stop seeing it. */
.fo-offseason { position: sticky; top: 0; z-index: 50;
                display: flex; align-items: center; justify-content: center; gap: var(--s2);
                padding: var(--s2) var(--s3);
                background: var(--warn-bg); color: var(--warn);
                border-bottom: 1px solid var(--warn);
                font-size: var(--fs-small); font-weight: 700;
                text-transform: uppercase; letter-spacing: .04em; }
.fo-offseason .fo-btn { text-transform: none; letter-spacing: 0; }

@media (max-width: 900px) {
    /* Stacked, so the row goes back to being as tall as its parts and each part caps itself. */
    .fo-maprow { flex-direction: column; height: auto; min-height: 0; }
    .fo-listrail, .fo-panel { flex: 1 1 auto; max-height: 45vh; }
    .fo-mapframe { height: 55vh; flex: none; }
}

/* ---- every table exports itself ----
   The button is attached by js/tablecsv.js to every .fo-table on the page, so no page had to be
   edited to get one. It sits ABOVE the scroll box, never inside it - inside, it would scroll away
   with the rows, which is exactly the moment you reach for it. */
.fo-csvbar { display: flex; justify-content: flex-end; gap: var(--s2); margin: 0 0 var(--s1); }
.fo-csvbar .fo-csv { padding: 2px var(--s2); font-size: var(--fs-small); }

/* ---- the season picker on the tab strip ----
   Years as links, not a dropdown: the site is static Razor, interactive circuits AND four maps
   drawn by their own scripts, and a link is the one control that behaves identically on all three
   with no JavaScript. Pushed to the right so it never crowds the tabs. */
.fo-seasonpick { margin-left: auto; display: inline-flex; gap: 2px; align-items: center;
                 padding-bottom: var(--s2); }
/* ⚠️ EVERY RULE HERE IS WRITTEN TWICE AND THAT IS NOT REDUNDANT. The picker renders INSIDE
   <nav class="fo-tabs">, whose link rules live in MainLayout's component style block — which the
   browser loads AFTER this file. `.fo-tabs a.on` and `.fo-seasonpick a.on` have the SAME
   specificity (0,2,1), so source order decided it and the tab strip won:

       .fo-tabs a.on       { color: var(--brand-purple) }                      <- won the colour
       .fo-seasonpick a.on { color: var(--paper); background: var(--brand-black) }  <- kept the box

   which put PURPLE TEXT ON A BLACK BOX. William, 2026-09-17: "the current season numbers on the
   right are purple text highlighted by a black box. it's unreadable."

   Prefixing with .fo-tabs takes these to 0,3,1 so they win outright wherever the picker sits in the
   strip; the bare selectors stay for any use outside it. Do not tidy one of each pair away. */
.fo-seasonpick a,
.fo-tabs .fo-seasonpick a { padding: 2px var(--s2); font-size: var(--fs-small); font-weight: 700;
                            color: var(--ink-3); text-decoration: none;
                            border-radius: var(--radius-sm);
                            /* the tab strip gives its links an underline and a -1px nudge; a pill
                               wants neither, and inheriting them left the box sitting low */
                            border-bottom: 0; margin-bottom: 0; }
.fo-seasonpick a:hover,
.fo-tabs .fo-seasonpick a:hover { color: var(--ink); background: var(--paper-2); }
.fo-seasonpick a.on,
.fo-tabs .fo-seasonpick a.on { color: var(--color-chrome-foreground); background: var(--color-chrome); }

/* ══ COMPAT: PAGE STYLESHEETS THAT USED A COLOUR NAME TWO WAYS (house style piece 0a) ═══════════════
   The ~40 page stylesheets were not edited in piece 0a; the old names became aliases (see the
   generated block). Two old names meant TWO things, and an alias can only be one:

     --paper         the card surface (a background)   AND   "white text on a dark/coloured fill"
     --brand-purple  click-text / an outline           AND   a button / chip FILL

   Aliasing --paper to the card is right for the first use and, in the dark theme, would make text on
   the chrome dark-on-dark. So the few selectors that use the name as TEXT-ON-A-FILL re-declare the
   name INSIDE their own subtree (a custom property is scoped to the element and its children): the
   page's own rule — `color: var(--paper)`, `background: var(--brand-purple)` — is untouched and now
   resolves to the right thing there. Nothing here sets a colour directly.

   REMOVE EACH LINE when its page stylesheet is migrated to the house tokens (pieces 1+). */

/* text on the chrome (header, the menu panel, the login brand panel) */
.fo-top, .fo-menu, .brand,
.da table.piv thead th { --paper: var(--color-chrome-foreground); }

/* a FILL that used the click colour: the yellow primary, with its #1a1a1a text, in both themes */
.fo-subtabs a.on,
.da tbody tr:hover .peek-tag,
.fx-info .fx-i-yr.on { --brand-purple: var(--color-primary); --paper: var(--color-primary-foreground); }

/* ══ THE STURDY OX MARK ON THE CHROME (house style, palettes.md "Logos") ═══════════════════════════
   The standard mark has BLACK outlines (fixed 2026-10-03; the old one was a flat yellow shape), and the
   header chrome is dark in both themes, so the outlines would vanish on it. The mark therefore always sits
   on a light tile, #f2efe9, radius 7px, in both themes. HOUSE-STYLE-EXCEPTION: the logo tile's fixed colour
   is part of the standard logo rule, not a theme colour. */
/* 2026-10-04: the standard mark is now the Android icon (black ox on its OWN yellow rounded tile), so it needs no
   extra tile here — the old #f2efe9 tile was for the outlined ox, which vanished on the black bar. */
.fo-logo { border-radius: 7px; }

/* ══ A SEGMENTED CHOICE (one of N, always one on): the Match device / Dark / Light switch ═════════════
   The active segment is the yellow primary with #1a1a1a text, in both themes. A row of real <button>s
   with role="radio", so a keyboard and a screen reader get it for free. */
.fo-seg { display: inline-flex; border: 1px solid var(--color-border-strong); border-radius: var(--radius-sm);
          overflow: hidden; }
.fo-seg button { font: inherit; font-size: var(--fs-body); font-weight: 600; padding: var(--s2) var(--s4);
                 border: 0; background: var(--color-card); color: var(--color-foreground); cursor: pointer; }
.fo-seg button + button { border-left: 1px solid var(--color-border-strong); }
.fo-seg button:hover:not([aria-checked="true"]) { background: var(--color-sunk); }
.fo-seg button[aria-checked="true"] { background: var(--color-primary); color: var(--color-primary-foreground); }

/* ══ THE SHARED HOUSE-STYLE PIECES (piece 0b, 2026-10-02) ═══════════════════════════════════════════
   Header row, tile row, chip bar, state line, table (whole-row links), buttons, the review wrappers.
   Written with the generated tokens ONLY — no colour is named here. Every page adopts these in its OWN
   later piece: nothing below changes an existing page unless the page opts in (a new class, or
   `.fo-table.house`), except the button size and `.fo-btn.danger[data-confirm]`, which are the point of
   the buttons piece. The website's partials (Pages/Shared/_PageHeader, _TileRow, _ChipBar, _StateLine) and the
   app's Views/HousePieces.cs draw the same thing; the wording is FieldOps.Contracts/HouseUi.cs. */

/* ── 1. PAGE HEADER ROW (rule 2): title 17px/650 with an identity bar under it, one muted line, and the
      freshness line at the right. A fixed minimum height so it is the same height on every page. ───── */
.fo-pagehead { display: flex; align-items: flex-start; flex-wrap: wrap; gap: var(--s2) var(--s4);
               min-height: 3.5rem; margin: 0 0 var(--s4); }
.fo-pagehead .ph-text { flex: 1 1 18rem; min-width: 0; }
.fo-pagehead h1 { position: relative; display: inline-block; margin: 0; padding-bottom: 7px;
                  font-size: var(--hs-title); font-weight: 650; line-height: 1.3; letter-spacing: 0;
                  color: var(--color-card-foreground); }
.fo-pagehead h1::after { content: ""; position: absolute; left: 0; bottom: 0; width: 2.25rem; height: 3px;
                         border-radius: 2px; background: var(--color-identity); }
.fo-pagehead .ph-sub { margin: 4px 0 0; max-width: var(--read-width); font-size: var(--hs-ui);
                       color: var(--color-muted-foreground); }
.fo-pagehead .ph-fresh { margin-left: auto; padding-top: 3px; white-space: nowrap; font-size: var(--hs-dense);
                         color: var(--color-identity-text); font-variant-numeric: tabular-nums; }

/* ── 2. TILE ROW (rule 3): equal tiles, value on top, label under, an identity top edge. Every tile is a
      real <a>: middle-click and Tab+Enter work. auto-fill keeps the tracks equal, so a short row of tiles
      is never stretched wide; they wrap on a narrow window. ──────────────────────────────────────── */
.fo-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(10.5rem, var(--hs-tile-max)));
            gap: var(--s3); margin: 0 0 var(--s4); }
.fo-tile { position: relative; display: flex; flex-direction: column; gap: 2px; min-width: 0;
           padding: var(--s3) var(--s4) var(--s3); text-decoration: none; color: inherit;
           background: var(--color-card); border: 1px solid var(--color-border);
           border-top: 3px solid var(--color-identity); border-radius: 0.75rem; }
.fo-tile .t-val { font-size: 1.5rem; font-weight: 700; line-height: 1.2; font-variant-numeric: tabular-nums;
                  color: var(--color-card-foreground); }
/* A tile whose figure IS a verdict (piece 2: an operator's score) colours the VALUE in its meaning. */
.fo-tile .t-val.t-good { color: var(--color-good-text); }
.fo-tile .t-val.t-attention { color: var(--color-attention-text); }
.fo-tile .t-val.t-fault { color: var(--color-fault-text); }
.fo-tile .t-lbl { font-size: var(--hs-dense); color: var(--color-muted-foreground); }
/* The ↗ says "this goes somewhere". It is part of the tile, not a second link. */
.fo-tile .t-go { position: absolute; top: 8px; right: 10px; font-size: var(--hs-dense); line-height: 1;
                 color: var(--color-muted-foreground); }
.fo-tile:hover { background: var(--color-row-hover); }
.fo-tile:hover .t-go { color: var(--color-link); }
.fo-tile:active { background: var(--color-row-press); }
/* The tile whose filter is on: an identity outline (and a screen reader hears aria-current). */
.fo-tile.on, .fo-tile[aria-current="true"] { outline: 2px solid var(--color-identity); outline-offset: -1px; }

/* ── 3. CHIP BAR (rule 4): filters on the left, actions on the right, wrapping to at most two rows (a
      page keeps its chips to what fits two rows). The active chip is FILLED identity with #1a1a1a text, in
      both themes. The older .ff-bar (fieldfilter.js's select cascade) shares the bar's rhythm below. ── */
.fo-chipbar, .ff-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s3);
                       margin: 0 0 var(--s3); }
.fo-chipbar .cb-filters { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); min-width: 0; }
.fo-chipbar .cb-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); margin-left: auto; }
.fo-fchip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 999px;
            font: inherit; font-size: var(--hs-dense); font-weight: 600; line-height: 1.4; cursor: pointer;
            text-decoration: none; color: var(--color-foreground);
            background: var(--color-sunk); border: 1px solid var(--color-border-strong); }
.fo-fchip:hover { background: var(--color-secondary); border-color: var(--color-muted-foreground); }
.fo-fchip .n { font-variant-numeric: tabular-nums; opacity: .8; }
.fo-fchip.on, .fo-fchip[aria-pressed="true"], .fo-fchip[aria-current="true"] {
    background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-foreground); }

/* A SECTION TITLE inside a page (house style piece 3 — the same look piece 2 wrote into dashboard.css as `.fo-dash h2`,
   now one shared class so the pages after it do not each copy it): 17px, 650, the identity rail at its left (colour roles:
   "every section title's rail") and a hairline under. Use on an <h2>, or on a <summary> that opens a section. NEW class:
   nothing that exists today changes. The page title itself is the header row above. */
.fo-section { font-size: var(--hs-title); font-weight: 650; color: var(--color-card-foreground);
              margin: var(--s5) 0 var(--s3); padding: 1px 0 var(--s2) var(--s2);
              border-left: 3px solid var(--color-identity); border-bottom: 1px solid var(--color-border); }
.fo-section .muted { font-weight: 400; color: var(--color-muted-foreground); font-size: var(--hs-dense); }

/* ── 4. STATE LINE (rule 7): loading / empty / degraded, one component. Degraded is the amber one. ── */
.fo-state { display: flex; align-items: center; gap: var(--s2); margin: 0 0 var(--s3);
            padding: var(--s3) var(--s4); font-size: var(--hs-ui); border-radius: var(--radius-sm);
            color: var(--color-muted-foreground); background: var(--color-sunk);
            border: 1px solid var(--color-border); }
.fo-state.degraded { color: var(--color-attention-text); background: var(--color-attention-bg);
                     border-color: transparent; border-left: 3px solid var(--color-attention); }
/* DONE (StateKind.Done, house style piece 1): a one-line confirmation of what the person just did, in the
   GOOD meaning — the same shape as the amber one, green. A state line may carry a meaning (HOUSE-STYLE.md,
   colour roles); this is the one for "Clocked in." / "Saved ✓". */
.fo-state.done { color: var(--color-good-text); background: var(--color-good-bg);
                 border-color: transparent; border-left: 3px solid var(--color-good); }

/* ── 5. THE TABLE, house version (rules 5 and 12): opt in with `.fo-table.house` inside `.fo-scroll`.
      Everything the older tables do (sorting, CSV, zebra) still works: this only restyles and pins. ─────
      · border-collapse: SEPARATE — a collapsed table does not carry a sticky cell's borders with it.
      · 32px rows, 12px text; the head is the sunk surface underlined in the identity.
      · FIRST COLUMN and the › CHEVRON are sticky and opaque. They take the row's own shade (the zebra
        rule above paints row-b onto an even row's cells as a background-IMAGE over row-a, so a sticky
        cell matches its row by simply being a cell), and the hover/press rules below recolour them with
        the row, so a pinned cell never sits a shade off its row. */
.fo-table.house { font-size: var(--hs-dense); border-collapse: separate; border-spacing: 0; }
.fo-table.house th, .fo-table.house td { height: var(--hs-row); padding: 0 var(--s3); line-height: 1.2;
                                         border-bottom: 1px solid var(--color-border); }
.fo-table.house thead th { background: var(--color-sunk); color: var(--color-card-foreground);
                           font-size: var(--hs-dense); font-weight: 600; text-transform: none; letter-spacing: 0;
                           border-bottom: 0; z-index: 3;
                           box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--color-identity) 70%, transparent); }
.fo-table.house th.sort:hover { background: var(--color-secondary); }
.fo-table.house th:first-child, .fo-table.house td:first-child { position: sticky; left: 0; z-index: 2; }
.fo-table.house thead th:first-child { z-index: 4; }
.fo-table.house tbody td:first-child, .fo-table.house td.rowact { background-color: var(--color-row-a); }
.fo-table.house tfoot th { border-top: 2px solid var(--color-identity); }
.fo-table.house td.num, .fo-table.house th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* The row under the pointer or with keyboard focus in it, then the one being pressed or already opened.
   `background-image: none` drops the even-row overlay so the hover is ONE uniform shade across the row. */
table.fo-table.house > tbody > tr:is(:hover, :focus-within) > :is(td, th) {
    background-color: var(--color-row-hover); background-image: none; }
table.fo-table.house > tbody > tr[data-rowlink]:active > :is(td, th),
table.fo-table.house > tbody > tr[data-open] > :is(td, th) {
    background-color: var(--color-row-press); background-image: none; }

/* A row that leads somewhere (rule 12). The whole row is clickable (rowlink.js); the REAL links inside it
   are the first cell's anchor and the chevron's, so middle-click, "open in new tab" and Tab+Enter work. */
.fo-table.house tbody tr[data-rowlink] { cursor: pointer; }
.fo-table.house a.rl { color: inherit; font-weight: 600; text-decoration: none; }
.fo-table.house a.rl:hover { color: var(--color-link-hover); text-decoration: underline; }
/* The › chevron: its own sticky column at the right edge, so it stays in view while a wide table slides. */
.fo-table.house th.rowact, .fo-table.house td.rowact { position: sticky; right: 0; z-index: 2; width: 1%;
                                                       padding: 0; text-align: center; }
.fo-table.house thead th.rowact { z-index: 4; }
.fo-table.house a.chev { display: flex; align-items: center; justify-content: center; height: var(--hs-row);
                         min-width: 2.25rem; text-decoration: none; font-size: 1.1rem; line-height: 1;
                         color: var(--color-muted-foreground); }
.fo-table.house tr:is(:hover, :focus-within) a.chev { color: var(--color-link); }

/* "Show more" (tablecap.js): rows past the cap are display:none, the rest keep their stripes. */
.fo-table tr[data-over] { display: none; }
.fo-showmore { display: flex; align-items: center; gap: var(--s3); margin: var(--s2) 0 var(--s3);
               font-size: var(--hs-dense); color: var(--color-muted-foreground); }
.fo-showmore[hidden] { display: none; }

/* ── BADGES: the six meanings (rule 5). A colour never means anything else on another page. ───────── */
.fo-badge { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: var(--hs-dense);
            font-weight: 600; line-height: 1.5; white-space: nowrap; }
.fo-badge.good { color: var(--color-good-text); background: var(--color-good-bg); }
.fo-badge.suggested { color: var(--color-suggested-text); background: var(--color-suggested-bg); }
.fo-badge.attention { color: var(--color-attention-text); background: var(--color-attention-bg); }
.fo-badge.decision { color: var(--color-decision-text); background: var(--color-decision-bg); }
.fo-badge.neutral { color: var(--color-neutral-text); background: var(--color-neutral-bg); }
.fo-badge.fault { color: var(--color-fault-text); background: var(--color-fault-bg); }

/* ── 6. BUTTONS (rule 9). One size is on .fo-btn itself. Primary is the identity fill; SECONDARY is a
      subtle text button; DANGER is an outlined red with a confirm step (confirm.js, from `data-confirm`).
      THE ⚠ IS NOT ON THE BUTTON (William, 2026-10-03: "the little sign is distracting. That should only
      show on the confirm delete step") — confirm.js puts it on the dialog's yes button, nowhere else.
      A button that needs a colour gets it as an OUTLINE, never a fill (same day). */
.fo-btn.secondary { background: transparent; border-color: transparent; color: var(--color-foreground); }
.fo-btn.secondary:hover:not(:disabled) { background: var(--color-sunk); border-color: transparent; }

/* The confirm dialog (confirm.js): a native <dialog>, so Escape and focus-trapping come for free. */
dialog.fo-confirm { max-width: min(28rem, calc(100vw - 2 * var(--s4))); padding: var(--s5);
                    color: var(--color-popover-foreground); background: var(--color-popover);
                    border: 1px solid var(--color-border-strong); border-radius: 0.75rem; }
dialog.fo-confirm::backdrop { background: color-mix(in srgb, var(--color-background) 72%, transparent); }
dialog.fo-confirm .cf-msg { margin: 0 0 var(--s4); font-size: var(--hs-ui); }
dialog.fo-confirm .cf-acts { display: flex; justify-content: flex-end; gap: var(--s2); }

/* ── THE REVIEW WRAPPERS (/design): the same page content under each theme's tokens at once. The token
      sets are generated for `.fo-theme-dark` / `.fo-theme-light` (gen-theme); a wrapper also needs its own
      surface and text, because the page's <body> is wearing the OTHER theme. ─────────────────────────── */
.fo-theme-dark, .fo-theme-light { background: var(--color-background); color: var(--color-foreground);
                                  padding: var(--s4); border: 1px solid var(--color-border-strong);
                                  border-radius: 0.75rem; min-width: 0; }
.fo-themepair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 38rem), 1fr));
                gap: var(--s4); align-items: start; }
.fo-themepair > h2 { grid-column: 1 / -1; margin: var(--s4) 0 0; font-size: var(--hs-title); font-weight: 650; }
