/* foltzplatform design tokens.
 *
 * 🚨 EVERY NAME IS PREFIXED --fz-, AND THAT IS THE WHOLE ADOPTION STRATEGY.
 * foltzday has 3,863 lines of CSS built on --surface/--ink-dim/--accent. If these
 * collided with a service's own names, adopting them would be a rewrite instead
 * of an import — and a rewrite is the thing that does not happen. A service can
 * import this today, use one token, and change nothing else.
 *
 * ⚠ --ink IS NOT HERE AND NEVER WILL BE. It means BACKGROUND in ci.css, flow.css,
 * foltzphotos and benditlearn, and TEXT in work.css, foltzday, benditsolutions and
 * hearth. intake links two of those sheets into one binary, and flow.css's own
 * header calls it a live hazard: "linking the wrong one renders the diagram as a
 * grid of solid dark boxes". A shared vocabulary cannot contain that word.
 *
 * ⭐ OVERRIDE ANYTHING, WITH A REASON. Redeclare a token in your own :root after
 * importing this. foltzconvo's warm paper and foltzbuild's cold machine greys are
 * both correct and both deliberate — the standard sets defaults, not a cage. What
 * it stops is drift nobody chose.
 */

:root {
    /* ── surfaces ──────────────────────────────────────────────────────────
     * Dark-first, because 13 of the fleet's 16 UIs are dark and only two pages
     * anywhere honour prefers-color-scheme — neither of them an app UI. The
     * light set below is opt-in for services that are light ON PURPOSE.
     *
     * ⚠ NEVER PURE BLACK, NEVER PURE WHITE. Every palette in this repo already
     * avoids both, and foltzday says why: a board in someone's peripheral vision
     * all day, read by a migraine-prone adult. */
    --fz-bg:      #0f1115;   /* the page */
    --fz-surface: #171a21;   /* a card, a panel, a row sitting on the page */
    --fz-raised:  #1e222b;   /* a control ON a surface: input, button, chip */
    --fz-line:    #2a2f3a;   /* borders and rules */

    /* ── text, in three weights of importance ──────────────────────────────
     * ⚠ THREE, NOT TWO. Every service that only had two ended up abusing one of
     * them: an id, a timestamp and a body sentence are not the same thing, and
     * a register that renders all three alike is one you stop scanning. */
    --fz-text:  #e6e8ee;   /* body */
    --fz-dim:   #98a0b0;   /* secondary, still meant to be read */
    --fz-faint: #828a99;   /* skimmable: timestamps, ids, hints — still 4.5:1 on --fz-raised */

    /* ── meaning ───────────────────────────────────────────────────────────
     * 🚨 COLOUR IS THE SECOND SIGNAL, NEVER THE ONLY ONE. Every state must also
     * carry a glyph and its word spelled out — see .fz-state below. A screenshot
     * pasted into a chat is greyscale for the purpose of arguing about it, and
     * red/green is the commonest colour blindness there is. */
    --fz-accent: #c8783c;   /* the ONE colour a service spends on what matters */
    --fz-ok:     #5e9d78;
    --fz-warn:   #d6a94a;
    --fz-bad:    #e0605c;
    /* text ON the accent: a filled primary button, the one bold panel */
    --fz-on-accent: var(--fz-bg);

    /* ── the photo viewer (fz-zoom, job_e04d7eaa3c40392e) ─────────────────
     * DARK IN BOTH SETS, ON PURPOSE: a photo is judged against a dark surround,
     * whatever the room the page is read in. So .fz-light does not redefine
     * these. Near-black and near-white, never the pure ones (above). */
    --fz-viewer-bg:   #121412;
    --fz-viewer-text: #f4f2ec;

  /* --fz-mark BELONGS TO NO STATE, AND THAT IS THE WHOLE POINT.
     --fz-warn means "this just happened", --fz-ok "it did", --fz-bad "it went
     wrong". A mark says only "look here".

     🚨 IF A MARK BORROWED A STATE COLOUR, THEN ASKING A QUESTION ABOUT A STEP
     WOULD CHANGE THE ANSWER — and a flow diagram is worth having only because
     an unlit shape means "this did not happen". */
  --fz-mark: #b08ae8;

    /* ── the brand's own mark ───────────────────────────────────
     * ⭐ A BRAND HAS ONE THING BEYOND A PALETTE, and for the fleet's first two
     * it is this: benditsolutions is recognised by a blue→purple sweep before
     * anybody reads the wordmark. A component that wants to paint the brand
     * uses this rather than reaching for --fz-accent and hoping.
     *
     * ⚠ THE PLATFORM'S OWN IS DELIBERATELY FLAT. foltzplatform has no gradient
     * and inventing one here would be this file choosing a brand nobody asked
     * for. Declared anyway so that no brand can leave the token undefined and
     * no component has to guard for that — see brand.go. */
    --fz-brand-grad: linear-gradient(115deg, var(--fz-accent), var(--fz-accent));

    /* ── categorical ──────────────────────────────────────────────
     * Eight hues for encoding a CATEGORY — a package's role, a series on a
     * chart, a lane on a diagram. They mean nothing on their own; the caller
     * decides what each one stands for and must also say so in a legend.
     *
     * 🚨 THEY ARE NOT STATE COLOURS AND MUST NEVER BE SWAPPED FOR ONE. --fz-ok,
     * --fz-warn and --fz-bad say what HAPPENED; these say what something IS. A
     * category drawn in --fz-bad reads as broken to every person who has ever
     * used the rest of the fleet.
     *
     * ⚠ COLOUR IS NEVER THE ONLY CHANNEL. Roughly 1 in 12 men cannot separate
     * -c from -e. Anything encoded here carries a label, a shape or a position
     * as well — the colour is the accelerant, not the message. */
    --fz-cat-a: #6f9ed8;
    --fz-cat-b: #c98a4b;
    --fz-cat-c: #6bab86;
    --fz-cat-d: #b48ad8;
    --fz-cat-e: #d0806f;
    --fz-cat-f: #64a8ab;
    --fz-cat-g: #b9a45c;
    --fz-cat-h: #8f96a8;

    /* ── geometry ──────────────────────────────────────────────────────────
     * ⚠ --fz-tap IS 88px AND IT IS NOT A SUGGESTION. From foltzday's charter,
     * which is the family's constraint rather than anyone's taste: "Hypermobile
     * hands tire fast; precision tapping is a tax we don't charge." */
    --fz-radius: 4px;
    --fz-gap:    1.4rem;
    --fz-tap:    88px;
    --fz-wrap:   46rem;

    /* ⭐ ONE SANS STACK. The fleet had eleven, and body size ranged 14px to 17px
     * with no reason recorded for any of it. A service wanting a display face
     * loads it and sets --fz-font-display; the BODY stack stays shared, because
     * body text is the thing every page has. */
    --fz-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --fz-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    /* ⚠ DEFAULTS TO THE BODY STACK, which is what "no display face" means. The
     * comment above has promised this token since the file was written and never
     * declared it, so a page setting it was declaring a variable nothing else
     * agreed existed. A brand with a display face sets it — see brand.go. */
    --fz-font-display: var(--fz-font);
    --fz-size: 15px;
    --fz-leading: 1.55;

    /* ── TOKENS v2 (job_38505c2a6ef7b133) ─────────────────────────────────
     * Chris, 2026-10-17: "many of the apps we spin up look bad at first and I
     * am always having to go back to fix the ui ... snowbrush looks good." Looked
     * at side by side, the difference was not colour: snowbrush has a TYPE SCALE,
     * a SPACING RHYTHM and a MEASURE, and foltzui had one radius and one gap. So
     * every new app set its own sizes, or none, and came out as a styled text
     * file. These are snowbrush's numbers, made shared.
     *
     * ⚠ ADDED, NOT CHANGED. Nothing above moves, so the 52 front ends already
     * importing this render exactly as they did; the page kit (kit.css) is what
     * spends these. */

    /* type: a steep scale, tight leading on headings. --fz-text-hero is the one
     * fluid size, for the single largest line a page has. */
    --fz-text-xs:   .75rem;
    --fz-text-sm:   .875rem;
    --fz-text-md:   1rem;
    --fz-text-lg:   1.15rem;
    --fz-text-xl:   1.35rem;
    --fz-text-2xl:  1.55rem;
    --fz-text-3xl:  clamp(1.75rem, 1.2rem + 2.4vw, 2.3rem);
    --fz-text-hero: clamp(2.2rem, 8vw, 3.8rem);
    --fz-leading-tight: 1.15;
    --fz-weight-heading: 800;
    /* ⭐ THE KIT'S BODY IS 17px (snowbrush's), set on .fz-shell — not here, so a
     * page that only imports the tokens keeps --fz-size. */
    --fz-size-roomy: 1.0625rem;

    /* space: one rhythm. --fz-space-section is the gap between a page's
     * sections, the number that makes snowbrush read as composed. */
    --fz-space-1: .25rem;
    --fz-space-2: .5rem;
    --fz-space-3: .75rem;
    --fz-space-4: 1rem;
    --fz-space-5: 1.25rem;
    --fz-space-6: 1.6rem;
    --fz-space-7: 2rem;
    --fz-space-section: 2.6rem;

    /* measure: prose stops at 64ch, a panel or a form at 34rem, a page at 72rem */
    --fz-measure: 64ch;
    --fz-panel:   34rem;
    --fz-page:    72rem;

    /* corners: --fz-radius above stays 4px for the components that use it */
    --fz-radius-sm: 4px;
    --fz-radius-md: 8px;
    --fz-radius-lg: 12px;
    --fz-radius-pill: 999px;

    /* elevation: two steps, both soft. A page that needs a third has a
     * hierarchy problem, not a shadow problem. */
    --fz-shadow-1: 0 1px 2px rgb(0 0 0 / .18), 0 1px 1px rgb(0 0 0 / .12);
    --fz-shadow-2: 0 6px 20px rgb(0 0 0 / .28), 0 2px 6px rgb(0 0 0 / .18);

    /* motion: short, and nothing moves under prefers-reduced-motion (base.css) */
    --fz-ease: cubic-bezier(.2, .7, .25, 1);
    --fz-dur-fast: 120ms;
    --fz-dur: 200ms;

    /* control height: snowbrush's 3.1rem buttons, comfortably over 44px */
    --fz-control: 2.75rem;
    --fz-control-lg: 3.1rem;

    /* focus: 3px, as snowbrush draws it */
    --fz-focus: 3px;

    /* ⚠ BREAKPOINTS CANNOT BE VARIABLES — a media query does not read custom
     * properties. The kit uses exactly two, and they are written here so a page
     * that adds a third has to come and say why:
     *     40rem  a phone ends    ·    64rem  a wide desktop begins */
}

/* ⭐ THE LIGHT SET, OPT-IN VIA A CLASS — NOT prefers-color-scheme.
 *
 * 🚨 AND THAT IS DELIBERATE. foltzday PINS dark, ignoring the OS, because it
 * hangs on a kitchen wall; hearth and benditsolutions are light because their
 * readers are not us. Those are decisions about the ROOM, and a media query would
 * override all three with a preference set on a laptop.
 *
 * ⇒ A light service adds class="fz-light" to <html> and means it. */
.fz-light {
    --fz-bg:      #faf6f0;
    --fz-surface: #ffffff;
    --fz-raised:  #f4efe7;
    --fz-line:    #e7ddd0;
    --fz-text:    #2b2622;
    --fz-dim:     #6b6259;
    --fz-faint:   #726860;
    --fz-accent:  #c2562b;
    --fz-ok:      #2f6b3a;
    --fz-warn:    #8a5a12;
    --fz-bad:     #a8351f;
    /* ⚠ THE PAGE COLOUR ON THE ACCENT MEASURED 4.19:1 HERE (debug_a11y_audit,
     * 2026-10-17, the kit's landing layout); white measures 4.5:1. */
    --fz-on-accent: #ffffff;

    /* ⚠ DARKENED, NOT REUSED. The dark set is tuned to sit on #0f1115; the
     * same hues on #faf6f0 fall to about 2:1 and stop being readable as a label
     * or as a thin stroke. Same order, same meaning, different ground. */
    --fz-cat-a: #2f6091;
    --fz-cat-b: #8a5312;
    --fz-cat-c: #2c6b4c;
    --fz-cat-d: #6b4397;
    --fz-cat-e: #a03f2c;
    --fz-cat-f: #1f6467;
    --fz-cat-g: #74601f;
    --fz-cat-h: #4d5361;

    /* a shadow on a light ground needs far less to read */
    --fz-shadow-1: 0 1px 2px rgb(40 30 20 / .08), 0 1px 1px rgb(40 30 20 / .05);
    --fz-shadow-2: 0 6px 20px rgb(40 30 20 / .12), 0 2px 6px rgb(40 30 20 / .08);
}

/* foltzui data table v2 (job_062c7f5bebed4483) — sort, filter, numbers, a phone
 * layout of cards, and pages.
 *
 * ⭐ THE NEEDS CATALOGUE (job_38505c2a6ef7b133) COUNTED TABLES HAND-ROLLED IN 35
 * SERVICES, with sort, filter or a phone layout in only a handful. base.css's
 * .fz-table is the plain grid and stays as it is; .fz-dt is the table a person
 * works in, and it is opt-in so no page changes shape until it asks to.
 *
 * ⚠ A MARKUP CONTRACT FIRST, A GO FUNCTION SECOND, as the form controls are.
 * (No real href or currency sign in this comment: pages that inline the sheet
 * are scanned for links and leaks — the trap commit 77fad965 hit.)
 *
 *   <div class="fz-dt" [data-fz-sort] [data-fz-filter] [data-fz-per="25"]>
 *     <div class="fz-tablewrap"><table class="fz-table">
 *       <caption>Bills</caption>
 *       <thead><tr><th scope="col" aria-sort="ascending"><a (the sort link)>Name</a></th>
 *                  <th scope="col" class="fz-num">Amount</th></tr></thead>
 *       <tbody><tr><td data-label="Name">Power</td><td class="fz-num" data-label="Amount">84.10</td></tr></tbody>
 *     </table></div>
 *     <nav class="fz-pager" aria-label="Pages">…</nav>
 *   </div>
 *
 * foltzui.DataTable{…}.HTML() writes exactly that, with the sort links and the
 * pager built from a TableQuery. table.js adds what a template left out.
 *
 * 🚨 READING NEEDS NO SCRIPT. A server-sorted header is a link; the filter is a
 * GET form; the pager is links; the cards are CSS. What the script adds is the
 * in-page version of each — sort without a round trip, filter as you type, pages
 * of a long table the server sent whole — and a page without it shows every row. */

/* ⚠ SELF-CONTAINED: a service that serves Tokens() + TableCSS() without base.css
 * (foltzday, foltzphotos) still gets the wrapper's scroll and the hidden words
 * table.js writes — its filter label and its "Sorted by…" announcement. Both
 * rules match base.css's, so loading both changes nothing. */
.fz-dt .fz-tablewrap { overflow-x: auto; }
.fz-dt .fz-vh {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.fz-dt { margin: 0 0 var(--fz-space-5); }
.fz-dt .fz-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.fz-dt caption { text-align: left; font-weight: 600; padding: 0 0 var(--fz-space-2); color: var(--fz-text); }
.fz-dt th {
    text-align: left; vertical-align: middle; white-space: nowrap; height: 2.5rem;
    font-size: var(--fz-text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .08em;
    /* ⚠ --fz-dim, NOT --fz-faint: a header is a control here (a sort link or
     * button), and a service that points --fz-faint at its own palette (foltzday's
     * #6b7280) drew these at 3.1:1 — measured live by debug_a11y_audit. */
    color: var(--fz-dim); padding: var(--fz-space-2) var(--fz-space-3);
    border-bottom: 1px solid var(--fz-line);
}
.fz-dt td { padding: var(--fz-space-2) var(--fz-space-3); border-bottom: 1px solid var(--fz-line); vertical-align: top; }
.fz-dt tbody tr:hover { background: var(--fz-surface); }
.fz-dt tfoot td { font-weight: 600; border-bottom: 0; border-top: 2px solid var(--fz-line); }

/* ── numbers ──────────────────────────────────────────────────────────────
 * ⭐ RIGHT-ALIGNED AND TABULAR, so the decimal points stack and a column of money
 * is read by its length before its digits. The header aligns with its column. */
.fz-num, .fz-dt .fz-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.fz-dt .fz-neg { color: var(--fz-bad); }

/* ── sort ─────────────────────────────────────────────────────────────────
 * A sortable header holds ONE control: a link (the server sorts) or a button
 * (table.js sorts in the page). The arrow is drawn by aria-sort, so what a
 * screen reader hears and what the eye sees cannot disagree. */
.fz-dt th > a, .fz-dt th > button.fz-dt-sort {
    font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit;
    background: none; border: 0; padding: var(--fz-space-1) 0; margin: 0; cursor: pointer;
    display: inline-flex; align-items: center; gap: var(--fz-space-1);
    min-height: 1.75rem; text-decoration: none;
}
.fz-dt th > a:hover, .fz-dt th > button.fz-dt-sort:hover { color: var(--fz-text); }
.fz-dt th > a::after, .fz-dt th > button.fz-dt-sort::after { content: "↕"; opacity: .45; font-size: .9em; }
.fz-dt th[aria-sort=ascending] { color: var(--fz-text); }
.fz-dt th[aria-sort=descending] { color: var(--fz-text); }
.fz-dt th[aria-sort=ascending] > a::after, .fz-dt th[aria-sort=ascending] > button.fz-dt-sort::after { content: "▲"; opacity: 1; }
.fz-dt th[aria-sort=descending] > a::after, .fz-dt th[aria-sort=descending] > button.fz-dt-sort::after { content: "▼"; opacity: 1; }
.fz-dt th.fz-num > a, .fz-dt th.fz-num > button.fz-dt-sort { flex-direction: row-reverse; }

/* ── filter ───────────────────────────────────────────────────────────────
 * The row over the table: a GET form the server answers, or — with
 * data-fz-filter — a box table.js adds that hides rows as you type, with the
 * count in words. */
.fz-dt-tools { display: flex; flex-wrap: wrap; gap: var(--fz-space-2); align-items: center; margin: 0 0 var(--fz-space-3); }
.fz-dt-tools > .fz-search, .fz-dt-tools > .fz-grow { flex: 1 1 14rem; }
.fz-dt-tools > select { flex: 0 1 12rem; }
.fz-dt-count { color: var(--fz-dim); font-size: var(--fz-text-sm); font-variant-numeric: tabular-nums; }
.fz-dt-none { color: var(--fz-dim); padding: var(--fz-space-4) var(--fz-space-3); text-align: center; }
.fz-dt-none[hidden] { display: none; }

/* ── pages ────────────────────────────────────────────────────────────────
 * kit.css's .fz-pager is the bar; inside a table it carries the range in words
 * and previous / next as links (or buttons, when table.js pages in the page). */
.fz-dt .fz-pager { margin-top: var(--fz-space-3); }
.fz-dt .fz-pager-links { display: flex; gap: var(--fz-space-2); flex-wrap: wrap; }
.fz-dt .fz-pager [aria-disabled=true] { opacity: .5; pointer-events: none; }
.fz-pager span.fz-btn { display: inline-flex; align-items: center; }

/* ── phone: rows become cards ─────────────────────────────────────────────
 * ⭐ A TABLE THAT SCROLLS SIDEWAYS ON A PHONE HIDES ITS RIGHT HALF, and the right
 * half is where the amount is. Under 34rem each row is a card: the first cell is
 * its title, every other cell a line of label and value, the label from
 * data-label (table.js copies it from the header when a template left it out).
 *
 * 🚨 THE HEADER STAYS, AS A ROW OF SORT CHIPS. Hiding it would hide the sort
 * links with it — a focusable control nobody can see, which the a11y audit fails
 * and a keyboard user falls into. A header with no control in it is hidden.
 *
 * ⚠ .fz-dt-scroll OPTS OUT: a matrix (a budget by month) is read across, and
 * keeps the sideways scroll. */
@media (max-width: 34rem) {
    .fz-dt:not(.fz-dt-scroll) .fz-tablewrap { overflow: visible; }
    .fz-dt:not(.fz-dt-scroll) table,
    .fz-dt:not(.fz-dt-scroll) tbody,
    .fz-dt:not(.fz-dt-scroll) tfoot { display: block; }
    .fz-dt:not(.fz-dt-scroll) caption { display: block; }
    .fz-dt:not(.fz-dt-scroll) thead { display: block; margin: 0 0 var(--fz-space-3); }
    .fz-dt:not(.fz-dt-scroll) thead tr { display: flex; flex-wrap: wrap; gap: var(--fz-space-2); align-items: center; }
    .fz-dt:not(.fz-dt-scroll) thead th { display: none; padding: 0; border: 0; }
    .fz-dt:not(.fz-dt-scroll) thead th:has(> a, > button) {
        display: block; border: 1px solid var(--fz-line); border-radius: var(--fz-radius-pill);
        padding: 0 var(--fz-space-3); background: var(--fz-raised);
    }
    .fz-dt:not(.fz-dt-scroll) thead th > a, .fz-dt:not(.fz-dt-scroll) thead th > button.fz-dt-sort { min-height: var(--fz-control); }
    .fz-dt:not(.fz-dt-scroll) thead tr::before {
        content: "Sort by"; color: var(--fz-dim); font-size: var(--fz-text-sm);
    }
    .fz-dt:not(.fz-dt-scroll) thead tr:not(:has(th > a, th > button))::before { content: none; }
    .fz-dt:not(.fz-dt-scroll) tbody tr, .fz-dt:not(.fz-dt-scroll) tfoot tr {
        display: block; background: var(--fz-surface);
        border: 1px solid var(--fz-line); border-radius: var(--fz-radius-md);
        padding: var(--fz-space-2) var(--fz-space-3); margin: 0 0 var(--fz-space-2);
    }
    .fz-dt:not(.fz-dt-scroll) tbody tr[hidden] { display: none; }
    /* ⚠ THE LABEL IS POSITIONED, NOT A GRID CELL: a cell holding a link and a chip
     * would otherwise be split into one grid item per child. */
    .fz-dt:not(.fz-dt-scroll) td {
        display: block; position: relative; border: 0; overflow-wrap: anywhere;
        padding: var(--fz-space-1) 0 var(--fz-space-1) 40%; min-height: 1.5rem;
    }
    .fz-dt:not(.fz-dt-scroll) td::before {
        content: attr(data-label); position: absolute; left: 0; top: calc(var(--fz-space-1) + .15em); width: calc(40% - var(--fz-space-3));
        color: var(--fz-dim); font-size: var(--fz-text-sm); overflow-wrap: anywhere;
    }
    .fz-dt:not(.fz-dt-scroll) td:first-child {
        padding-left: 0; font-weight: 600; font-size: var(--fz-text-md);
    }
    .fz-dt:not(.fz-dt-scroll) td:first-child::before { content: none; }
    .fz-dt:not(.fz-dt-scroll) td.fz-num { text-align: left; }
    .fz-dt:not(.fz-dt-scroll) td:empty { display: none; }
    .fz-dt:not(.fz-dt-scroll) .fz-dt-none { display: block; padding-left: 0; }
    .fz-dt:not(.fz-dt-scroll) .fz-dt-none::before { content: none; }
}

/* foltzui CARDS v2 (job_3e5bb1d3e5356103): the card shapes 42 services drew
 * for themselves. In CSS() like every sheet, and alone as CardsCSS() for a
 * service that keeps its own page (photos, benditlearn).
 *
 * ⚠ NOT UNDER .fz-shell, so adopting the card does not wait on the shell — and it
 * leans only on tokens, so Tokens()+CardsCSS() is enough to draw one.
 *
 * THE LEAD LINE: the one answer a page gives, set large, its meaning on the
 * number's baseline (under it on a phone), and a rule beneath that ends it. */
.fz-lead { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--fz-space-2) var(--fz-space-6); padding: var(--fz-space-5) 0 var(--fz-space-3); border-bottom: 2px solid var(--fz-text); margin: 0 0 var(--fz-space-5); }
.fz-lead-main { min-width: 0; }
.fz-lead-label { margin: 0 0 var(--fz-space-1); color: var(--fz-dim); font-size: var(--fz-text-sm); font-weight: 600; }
.fz-lead-n { margin: 0; font: 800 clamp(1.9rem, 1.4rem + 2.4vw, 2.6rem)/1 var(--fz-font-display); font-variant-numeric: tabular-nums; letter-spacing: -.02em; overflow-wrap: anywhere; }
.fz-lead-meta { margin: 0; color: var(--fz-dim); font-size: var(--fz-text-md); padding-bottom: .15rem; }
.fz-lead-meta b { color: var(--fz-text); font-variant-numeric: tabular-nums; }
.fz-lead-meta .fz-state { font-size: inherit; }   /* a signed figure in the meta keeps the line's size (trade's day change) */

/* THE MEDIA CARD: a picture, a title that IS the link (the whole card is the
 * target, as .fz-item does it), one quiet line, a foot for the state as a word.
 * 🚨 THE PICTURE'S FRAME IS FIXED BEFORE IT LOADS (aspect-ratio), so a grid of
 * covers does not jump as they arrive, and a missing one is a hatched slot. */
.fz-media-grid { display: grid; gap: var(--fz-space-3); grid-template-columns: repeat(auto-fill, minmax(min(100%, 12.5rem), 1fr)); margin: 0; padding: 0; list-style: none; }
.fz-media-grid > li { min-width: 0; }
.fz-media-grid.fz-is-rows { grid-template-columns: 1fr; gap: var(--fz-space-2); }
@media (max-width: 40rem) { .fz-media-grid:not(.fz-is-rows) { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--fz-space-2); } }
.fz-media {
    position: relative; display: flex; flex-direction: column; height: 100%; min-width: 0; margin: 0;
    background: var(--fz-surface); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg);
    overflow: hidden; box-shadow: var(--fz-shadow-1);
}
.fz-media:has(.fz-media-title :is(a, button)):hover { border-color: var(--fz-accent); }
.fz-media:has(.fz-media-title :is(a, button):focus-visible) { outline: var(--fz-focus) solid var(--fz-accent); outline-offset: 2px; }
.fz-media-pic { position: relative; flex: none; aspect-ratio: 3 / 2; background: var(--fz-raised); }   /* relative: a service's overlay (a play mark) sits on the picture */
.fz-media.fz-is-square .fz-media-pic { aspect-ratio: 1; }
.fz-media.fz-is-tall .fz-media-pic { aspect-ratio: 2 / 3; }
.fz-media.fz-is-wide .fz-media-pic { aspect-ratio: 16 / 9; }
.fz-media-pic img { display: block; width: 100%; height: 100%; object-fit: cover; }
.fz-media-slot {
    display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;
    color: var(--fz-faint); font-size: 2rem; line-height: 1;
    background: repeating-linear-gradient(135deg, transparent 0 14px, var(--fz-line) 14px 15px), var(--fz-raised);
}
.fz-media-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: var(--fz-space-1); min-width: 0; padding: var(--fz-space-3) var(--fz-space-4) var(--fz-space-4); }
.fz-media-title { margin: 0; font: 700 var(--fz-text-md)/1.25 var(--fz-font-display); overflow-wrap: anywhere; }
.fz-media-title :is(a, button) { color: inherit; text-decoration: none; }
/* a card that opens in place (an album in a single-page app) has a button for a title */
.fz-media-title button { font: inherit; background: none; border: 0; padding: 0; margin: 0; text-align: left; cursor: pointer; min-height: 0; }
.fz-media-title :is(a, button)::after { content: ""; position: absolute; inset: 0; }   /* the whole card is the target */
.fz-media-title :is(a, button):focus-visible { outline: none; }
.fz-media-meta { margin: 0; color: var(--fz-dim); font-size: var(--fz-text-sm); overflow-wrap: anywhere; }
.fz-media-foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fz-space-2); margin-top: auto; padding-top: var(--fz-space-2); color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-media-body > p { margin: 0; }
/* a second link, a form, a fold: pressable over the card's own link, whose
 * ::after covers the card — so the title's link stays static and anchors it */
.fz-media-body :is(a, button, input, select, textarea, summary, label), .fz-media-foot > * { position: relative; z-index: 1; }
.fz-media-body .fz-media-title :is(a, button) { position: static; }
/* on its side: a small square picture left of the words, for a list read down */
.fz-media.fz-is-row { flex-direction: row; align-items: stretch; }
.fz-media.fz-is-row .fz-media-pic { width: 5.5rem; aspect-ratio: auto; min-height: 5.5rem; }
.fz-media.fz-is-row .fz-media-body { padding: var(--fz-space-3) var(--fz-space-4); }
@media (min-width: 40rem) { .fz-media.fz-is-row .fz-media-pic { width: 7rem; min-height: 7rem; } }

/* STAT TILES: the numbers an overview leads with (Stats). Here with the other
 * two card shapes, so Tokens()+CardsCSS() draws all three. */
.fz-stats { display: grid; gap: var(--fz-space-3); grid-template-columns: repeat(auto-fit, minmax(min(100%, 9.5rem), 1fr)); margin: 0 0 var(--fz-space-section); padding: 0; list-style: none; }
.fz-stat { background: var(--fz-surface); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg); padding: var(--fz-space-4); }
.fz-stat-n { display: block; font: 800 var(--fz-text-3xl)/1 var(--fz-font-display); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.fz-stat-label { display: block; margin-top: var(--fz-space-2); color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-stat-note { display: block; margin-top: var(--fz-space-1); font-size: var(--fz-text-xs); color: var(--fz-faint); }
.fz-stat a { color: inherit; text-decoration: none; }
.fz-stat:has(a):hover { border-color: var(--fz-accent); }

/* zoom.css — the photo viewer (job_e04d7eaa3c40392e), from snowbrush's zoom.js. Each photo
   in a .fz-figs is a plain link to its full file (.fz-zoom-link), so with no
   script a tap still opens the picture; zoom.js opens it in a <dialog>
   instead, with the arrow keys, Home/End and a sideways swipe between the photos
   of one grid. Dark in both sets: a photo is judged against a dark surround.
   Its own file, like zoom.js, so a page can load the viewer without the kit
   (foltzphotos); Base() carries it for everyone else.
   ⚠ SOLID, NOT SEE-THROUGH, WHEREVER TEXT SITS: a translucent viewer reads the
   page through it, and debug_a11y_audit measured the caption against the LIGHT
   page behind (1.04:1) — the colour a reader would get if the mix ever failed. */
.fz-zoom-link { display: block; border-radius: var(--fz-radius-md); cursor: zoom-in; }
.fz-zoom-link:focus-visible { outline: var(--fz-focus) solid var(--fz-accent); outline-offset: 2px; }
.fz-zoom { width: 100vw; height: 100vh; height: 100dvh; max-width: none; max-height: none; margin: 0; padding: 0; border: 0; background: var(--fz-viewer-bg); color: var(--fz-viewer-text); overscroll-behavior: contain; }
.fz-zoom::backdrop { background: color-mix(in srgb, var(--fz-viewer-bg) 60%, transparent); }
.fz-zoom figure { position: absolute; inset: 3.4rem .5rem 1rem; margin: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; }
.fz-zoom img { max-width: 100%; max-height: calc(100% - 2.6rem); object-fit: contain; border-radius: var(--fz-radius-sm); pointer-events: auto; }
.fz-zoom figcaption { padding: var(--fz-space-2) var(--fz-space-3) 0; font-size: var(--fz-text-md); text-align: center; }
.fz-zoom-count { position: absolute; top: 1.1rem; left: 1rem; font-size: var(--fz-text-sm); opacity: .85; }
.fz-zoom button { position: absolute; min-width: var(--fz-control); min-height: var(--fz-control); border: 0; border-radius: var(--fz-radius-pill); background: var(--fz-viewer-bg); color: var(--fz-viewer-text); box-shadow: 0 0 0 1px color-mix(in srgb, var(--fz-viewer-text) 35%, transparent); font: 700 1.6rem/1 var(--fz-font-display); cursor: pointer; }
.fz-zoom button:hover, .fz-zoom button:focus-visible { outline: 2px solid var(--fz-viewer-text); }
.fz-zoom button[hidden] { display: none; }
.fz-zoom-close { top: .5rem; right: .6rem; }
.fz-zoom-prev, .fz-zoom-next { top: 50%; transform: translateY(-50%); }
.fz-zoom-prev { left: .5rem; }
.fz-zoom-next { right: .5rem; }
