/* 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);
}

/* foltzplatform base — the components that were implemented once per service.
 *
 * ⚠ EVERY CLASS IS PREFIXED .fz-, for the same reason the tokens are: a service
 * adopting this must not have its existing .btn, .card or .empty change under it.
 * Adoption is additive; migration is a separate, later, deliberate edit.
 *
 * 🚨 WHAT IS HERE AND WHY, in one line each — the inventory count is the argument:
 *   focus ring     6 systems, and 4 services had NONE
 *   .fz-btn        9 implementations, 2 incompatible modifier conventions
 *   .fz-empty      9 implementations, 5 visual languages
 *   .fz-notice     12 implementations, 3 class names
 *   .fz-card       11 + 3 .panel + 2 .tile
 *   .fz-badge      11, six radii for one idea (3/5/8/20/99/999px)
 *   .fz-nav        6 vocabularies; foltzbuild alone ships 3 for 6 pages
 *   .fz-table      6, and only one of them wraps for overflow
 */

/* ── the fixes every service should already have had ──────────────────────── */

/* 🚨 THIS ONE LINE FIXES A BUG THAT PASSES EVERY TEST. board.css:18 records how
 * it hid: "Nothing errored, and every test passed." A stylesheet rule with any
 * display value beats the [hidden] attribute, so an element the code has
 * carefully hidden stays on screen and nothing anywhere says so. Every service
 * without this is still exposed. */
[hidden] { display: none !important; }

*, *::before, *::after { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--fz-bg);
    color: var(--fz-text);
    font: var(--fz-size)/var(--fz-leading) var(--fz-font);
}

/* ⚠ THE LINK RULE, AND IT IS THE REASON THIS PACKAGE EXISTS.
 *
 * 🚨 THE SAME DEFECT SHIPPED TWICE. wrk_24aa8023 fixed a bare <a> rendering as
 * browser-default blue at about 1.9:1 against a dark background — below the 4.5:1
 * normal text needs and below even the 3:1 allowed for large text. foltzconvo then
 * shipped it again on its own header link, found on 2026-09-05 by looking at the
 * page: "it sat two lines from the fix, untouched, because nobody looked at the
 * header while fixing the row."
 *
 * ⭐ THE COLOUR BELONGS TO THE TEXT AND THE UNDERLINE SAYS "TAPPABLE". That is
 * what makes it legible first and link-like second, which is the right order for
 * something read on a phone, outdoors. */
a {
    color: var(--fz-text);
    text-decoration: underline;
    text-decoration-color: var(--fz-faint);
    text-underline-offset: 2px;
}
a:hover { text-decoration-color: var(--fz-accent); }

/* 🚨 ONE FOCUS RING, ON EVERYTHING, AND FOUR SERVICES HAD NONE AT ALL.
 * :focus-visible rather than :focus so a mouse click does not draw it — the
 * reason every service that hand-rolled this eventually got there. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--fz-accent);
    outline-offset: 2px;
}

/* ⭐ VISUALLY HIDDEN, SHARED. Two services had this and each wrote its own. */
.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;
}

/* 🚨 NO ANIMATION WITHOUT THIS GUARD. foltzday's charter is the fleet's:
 * "LOW STIMULATION... a migraine-prone adult has to be able to walk past it."
 * benditsolutions animates a button hover with no guard at all. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

.fz-wrap { max-width: var(--fz-wrap); margin: 0 auto; padding: 0 1.1rem; }

/* ── nav ──────────────────────────────────────────────────────────────────
 * ⭐ THE CURRENT PAGE IS NAMED, NOT LINKED. reachable_test.go already refuses a
 * route with no link — Chris, 2026-08-29: "user is unable to reach hexmap via ui
 * — this is a pattern we avoid" — and a page linking to itself is how a nav grows
 * a dead click. foltzui.Nav() marks the current entry so nobody has to remember
 * to de-link it by hand in four copies, which is exactly how one of them drifted. */
/* 🚨🚨 IT STICKS, AND IT IS BIG ENOUGH TO HIT.
 *
 * Chris, 2026-09-25: "users have to scroll all the way to the top to see the
 * menu and navigate elsewhere" and "ashley cannot navigate the UI".
 *
 * MEASURED ON message.foltzcloud.com: a conversation page is 16,628px tall —
 * SEVENTEEN AND A HALF SCREENS — and this nav was `position: static` at the very
 * top of it. Leaving a room meant scrolling the entire conversation back up.
 * The links themselves were 17px tall at 13px type: below the 24px WCAG 2.5.8
 * floor, well below the 44px everyone actually uses, and five times under the
 * 88px tap target foltzmessage declares for its OWN buttons. A nav the design
 * system will not give a tap target to is not navigation, it is a footnote.
 *
 * ⚠ AND THE MOBILE RULE MADE IT WORSE, NOT BETTER — it shrank the nav to .78rem
 * on the smallest screens, which is where a thumb has the least to aim at. That
 * rule is inverted below.
 *
 * ⭐ FIXED HERE AND NOT IN foltzmessage, because 19 services render this and the
 * complaint is about all of them. A local override would be the second design
 * system ui-standard.md exists to prevent.
 *
 * ⚠ IT NEEDS AN OPAQUE BACKGROUND. A sticky bar over scrolling content with a
 * transparent background is unreadable the moment anything passes under it. */
.fz-nav {
    position: sticky;
    top: 0;
    z-index: 20;
    /* 🚨🚨 THE NOTCH. Chris, 2026-09-25: "the menu bar in chat shows up at the
     * very top of the phone screen and users cannot select it because the ear
     * portion of the phone is in the way - but in non-chat the menu bar is at a
     * reasonable height."
     *
     * ⭐ THAT DIFFERENCE IS THE WHOLE DIAGNOSIS, and it is a property of sticky
     * rather than of either page. foltzmessage sets viewport-fit=cover and pads
     * its BODY by env(safe-area-inset-top), so in normal flow this bar sits
     * below the Dynamic Island — which is why the list pages were fine. But a
     * sticky element pins to the SCROLLPORT, not to the padded body box, so the
     * moment a long conversation scrolls it snaps to the physical top of the
     * screen and slides under the ear, where iOS will not deliver a tap.
     *
     * ⇒ THE BAR OWNS THE INSET ITSELF. Its own padding fills the notch strip
     * with its own background, so it reads correctly whether it is in flow or
     * pinned, and there is no gap for content to scroll through above it.
     *
     * ⚠ SAFE ON EVERY OTHER SERVICE. env() resolves to 0 without
     * viewport-fit=cover, so the other 18 navs are unchanged to the pixel.
     *
     * 🚨 THE INSET IS DECLARED AFTER THE `padding` SHORTHAND, NOT BEFORE IT, AND
     * THAT ORDER IS THE WHOLE RULE. Written above the shorthand it computed to
     * 0px — the shorthand resets all four sides — so the declaration was present
     * and did nothing. Caught by reading the COMPUTED style on the live page;
     * the stylesheet said the right thing the entire time. */
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 .25rem;
    margin: 0 0 1.2rem;
    padding: 0 0 .35rem;
    padding-top: calc(.35rem + env(safe-area-inset-top));
    background: var(--fz-bg);
    border-bottom: 1px solid var(--fz-line);
    color: var(--fz-faint);
    font-size: .9rem;
}
.fz-nav a,
.fz-nav .fz-here {
    /* ⭐ THE TAP TARGET IS THE PADDING, not the word. "Us" and "Documents" are
     * very different widths and both have to be reachable by a thumb. */
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
    padding: 0 .5rem;
    border-radius: var(--fz-radius);
}
.fz-nav a { color: var(--fz-dim); }
.fz-nav a:hover { color: var(--fz-text); background: var(--fz-raised); }
/* ⚠ THE KEYBOARD RING MUST SURVIVE THE PADDING. An outline on an inline element
 * wraps the text; on this inline-flex box it wraps the target, which is the
 * thing being focused. */
.fz-nav a:focus-visible { outline: 2px solid var(--fz-text); outline-offset: -2px; }
.fz-nav .fz-here { color: var(--fz-text); font-weight: 600; }
.fz-nav .fz-svc {
    text-transform: uppercase; letter-spacing: .09em;
    color: var(--fz-faint); margin-right: .5rem;
}

/* ── buttons ──────────────────────────────────────────────────────────────
 * ⚠ ONE MODIFIER CONVENTION: .fz-btn.fz-primary, not .fz-btn-primary. The fleet
 * had both and they are not interchangeable — a page that mixes them silently
 * loses the variant. Picked the dot form because it composes: an element can be
 * primary AND small without inventing .fz-btn-primary-sm. */
.fz-btn {
    font: inherit; cursor: pointer;
    color: var(--fz-text); background: var(--fz-raised);
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    padding: .5rem 1rem;
    /* ⚠ 44px MINIMUM, ALWAYS. --fz-tap (88px) is the standard for a primary
     * target on a wall tablet; this is the floor below which nothing goes. */
    min-height: 44px;
}
.fz-btn:hover { border-color: var(--fz-accent); }
.fz-btn:disabled { cursor: default; color: var(--fz-faint); border-color: var(--fz-line); }
.fz-btn.fz-primary { background: var(--fz-accent); color: var(--fz-on-accent); border-color: var(--fz-accent); font-weight: 600; }
.fz-btn.fz-danger  { color: var(--fz-bad); border-color: var(--fz-bad); }
.fz-btn.fz-quiet   { background: none; border-color: transparent; color: var(--fz-faint); }
.fz-btn.fz-quiet:hover { color: var(--fz-text); border-color: var(--fz-line); }
/* ⚠ SMALL IS STILL 44px TALL. It is smaller in TYPE, never in target. */
.fz-btn.fz-sm { font-size: .8rem; padding: .3rem .7rem; }

/* ── state: colour + glyph + word, never colour alone ─────────────────────
 * 🚨 THE GLYPH AND THE WORD ARE NOT DECORATION. From ci.css:41, which already
 * had this right: "the page reads correctly for red/green colour blindness and
 * in a grey screenshot". Use as: <span class="fz-state fz-is-ok">✓ done</span> */
.fz-state { font-size: .78rem; }
.fz-state.fz-is-ok   { color: var(--fz-ok); }
.fz-state.fz-is-warn { color: var(--fz-warn); }
.fz-state.fz-is-bad  { color: var(--fz-bad); }
.fz-state.fz-is-off  { color: var(--fz-faint); }

/* ── surfaces ─────────────────────────────────────────────────────────────── */
.fz-card {
    background: var(--fz-surface);
    border: 1px solid var(--fz-line);
    border-radius: var(--fz-radius);
    padding: .8rem 1rem;
}

/* ⚠ ONE RADIUS FOR A BADGE. The fleet used 3px, 5px, 8px, 20px, 99px and 999px
 * for the same visual idea. Pill, because that is what six of the eleven chose
 * and it is the one that reads as a label rather than as a small button. */
.fz-badge {
    display: inline-block; border-radius: 999px;
    padding: .1rem .55rem; font-size: .72rem;
    border: 1px solid currentColor;
}

/* ── notice ───────────────────────────────────────────────────────────────
 * ⚠ ONE NAME. The fleet had .notice, .alert, .flash, .banner, .warn, .trouble and
 * .defects — and hexmap's .defects and foltzevents' .alert are the SAME TWO HEXES
 * under two names. The left rule carries the meaning so the text stays readable
 * on a coloured background, which the filled variants did not always manage. */
.fz-notice {
    padding: .6rem .8rem; margin: 1rem 0;
    background: var(--fz-surface);
    border-left: 3px solid var(--fz-line);
    border-radius: 0 var(--fz-radius) var(--fz-radius) 0;
    color: var(--fz-dim); font-size: .86rem;
}
.fz-notice.fz-is-ok   { border-left-color: var(--fz-ok); }
.fz-notice.fz-is-warn { border-left-color: var(--fz-warn); }
.fz-notice.fz-is-bad  { border-left-color: var(--fz-bad); color: var(--fz-text); }

/* ── empty state ──────────────────────────────────────────────────────────
 * ⭐ IT IS A PANEL WITH AN ACTION, NOT A SENTENCE. Five of the nine hand-rolled
 * empty states were prose only, and foltzconvo's shipped an add box that looked
 * live and did nothing. An empty state's job is to say what there is to DO. */
.fz-empty {
    padding: 1.25rem; text-align: center;
    background: var(--fz-surface);
    border: 1px dashed var(--fz-line);
    border-radius: var(--fz-radius);
}
.fz-empty p { margin: 0 0 .75rem; color: var(--fz-dim); }
.fz-empty .fz-note { margin: .75rem 0 0; font-size: .78rem; color: var(--fz-faint); }

/* ── table ────────────────────────────────────────────────────────────────
 * 🚨 THE WRAPPER IS THE POINT. Six services render tables and only foltzbuild
 * wraps one for overflow; foltzvpn's device table has no wrapper and overflows on
 * a phone. Always: <div class="fz-tablewrap"><table class="fz-table">. */
.fz-tablewrap { overflow-x: auto; }
.fz-table { width: 100%; border-collapse: collapse; }
.fz-table th {
    text-align: left; font-size: .72rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .08em;
    color: var(--fz-faint); padding: .4rem .6rem;
    border-bottom: 1px solid var(--fz-line);
}
.fz-table td { padding: .5rem .6rem; border-bottom: 1px solid var(--fz-line); }

/* ⚠ AN ID OR A TIMESTAMP IS FAINT AND TABULAR. Reading a column of ids where the
 * digits do not line up is the difference between scanning and checking. */
.fz-meta { font-size: .72rem; color: var(--fz-faint); }
.fz-mono { font-family: var(--fz-font-mono); font-variant-numeric: tabular-nums; }

@media (max-width: 34rem) {
    .fz-btn { padding: .55rem .9rem; }
    /* 🚨 BIGGER ON A PHONE, NOT SMALLER. This rule used to shrink the nav to
     * .78rem on the narrowest screens — the place with the least room to aim.
     * ⭐ AND THE SERVICE NAME GOES, not the links: on a 360px screen "MESSAGE"
     * costs a third of the bar to say something the person already knows. */
    .fz-nav { font-size: .95rem; }
    .fz-nav .fz-svc { display: none; }
}

/* ── the answer ───────────────────────────────────────────────────────────
 * ⭐⭐ EVERY PAGE IS FOR ONE QUESTION, AND MOST OF OURS BURY IT. foltzmeal's week
 * opened with a heading, three warning banners and seven identical rows, one of
 * which happened to be today — for a person whose question is "what is for
 * dinner tonight". foltzday has the same shape (is anything due), foltzbuild has
 * it (is main green), foltzconvo has it (what are we doing).
 *
 * ⇒ .fz-answer is the ONE thing a page exists to say, at a size nothing else on
 * the page competes with. Not a hero, not a banner — the answer.
 *
 * 🚨 THE ACCENT IS SPENT HERE AND NOWHERE ELSE ON THE PAGE. One colour, one job:
 * if --fz-accent appears twice on a screen, one of them is wrong. That is what
 * makes an unaccented row read as "not the one you asked about" rather than
 * "styled slightly differently". */
.fz-answer {
    background: var(--fz-surface);
    border: 1px solid var(--fz-line);
    border-left: 3px solid var(--fz-accent);
    border-radius: var(--fz-radius);
    padding: 1.1rem 1.2rem 1.2rem;
    margin: 0 0 1.6rem;
}
/* ⚠ THE LABEL IS SENTENCE CASE AND IT IS NOT AN EYEBROW. A tracked-out all-caps
 * word above a heading is decoration pretending to be structure; this is the
 * date, which is information. */
.fz-answer .fz-answer-when {
    margin: 0 0 .35rem; font-size: .82rem; color: var(--fz-dim);
}
.fz-answer .fz-answer-what {
    margin: 0; font-weight: 600; line-height: 1.15;
    /* ⭐ ONE ENORMOUS JUMP, NOT SIX POLITE ONES. No display face is loaded: this
     * page has to open in a shop on bad signal, and a font download is the
     * accessory to remove. The scale carries the personality instead. */
    font-size: clamp(1.7rem, 1.1rem + 3.2vw, 2.6rem);
    letter-spacing: -0.015em;
}
.fz-answer .fz-answer-what a { text-decoration-color: var(--fz-line); }
.fz-answer .fz-answer-sub {
    margin: .5rem 0 0; color: var(--fz-dim); font-size: .9rem;
}
/* ⚠ AN UNANSWERED QUESTION IS STILL AN ANSWER, and it must not look broken.
 * "Nobody has decided" is a real state that invites an action; a blank space is
 * a page that failed to load. */
.fz-answer.fz-is-open .fz-answer-what { color: var(--fz-dim); font-weight: 500; }

/* ── ruled rows ───────────────────────────────────────────────────────────
 * ⚠ DELIBERATELY NOT CARDS. Seven identical rounded boxes ask the eye to rank
 * them and give it nothing to rank on — the card kit is what a page reaches for
 * when it has not decided what matters. A rule is cheaper, quieter, and reads as
 * a list of the same KIND of thing, which is what a week is.
 *
 * ⭐ IT IS ALSO THE FIX FOR A REAL BREAKAGE. The week laid days out as flex rows
 * with a fixed-width label and controls on the end; adding two forms squeezed the
 * meal name to about 40px and it wrapped one word per line. Rows wrap to a block
 * on a narrow screen instead of compressing the thing you came to read. */
.fz-rows { border-top: 1px solid var(--fz-line); }
.fz-row {
    display: flex; gap: .9rem; align-items: baseline;
    padding: .7rem .2rem;
    border-bottom: 1px solid var(--fz-line);
}
.fz-row-key { flex: 0 0 5.5rem; color: var(--fz-dim); font-size: .88rem; }
/* ⚠ overflow-wrap: anywhere — one unbroken token (a DKIM key, a long hostname)
 * otherwise widens the whole page, and min-width: 0 alone does not stop it
 * (job_618b18bd53bfa8b7: dns.foltzcloud.com scrolled sideways at 390px). */
.fz-row-main { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
/* ⚠ WHATEVER GOES HERE IS READ LAST ONCE THE ROW STACKS, and that is the trap.
 * foltzmeal put the cooking time in this column — the one fact somebody scans a
 * week for — and on a phone it landed BELOW the change control, after everything
 * else in the row. A right-hand column is a desktop habit; it survives stacking
 * only for something genuinely secondary, like a count or a timestamp. If it
 * matters, put it in .fz-row-main next to what it describes. */
.fz-row-end { flex: 0 0 auto; color: var(--fz-faint); font-size: .8rem; }
@media (max-width: 34rem) {
    /* ⚠ IT STACKS RATHER THAN SHRINKING. A narrow column that keeps its columns
     * is how a meal name ends up one word per line. */
    .fz-row { flex-wrap: wrap; gap: .15rem .9rem; }
    .fz-row-key { flex-basis: 100%; }
    .fz-row-end { flex-basis: 100%; }
}

/* ── disclose ─────────────────────────────────────────────────────────────
 * ⭐ A CONTROL THAT IS NOT THERE UNTIL IT IS WANTED, AND NO SCRIPT. Seven days
 * carrying two permanent forms each is fourteen controls competing with the one
 * sentence somebody opened the page to read. <details> is keyboard-accessible,
 * survives no-JS, and shows what changed when it opens — which is the one kind
 * of motion worth having. */
.fz-disclose > summary {
    cursor: pointer; list-style: none;
    display: inline-flex; align-items: center; gap: .35rem;
    color: var(--fz-dim); font-size: .82rem;
    padding: .5rem .1rem; min-height: 44px;
}
.fz-disclose > summary::-webkit-details-marker { display: none; }
.fz-disclose > summary::before { content: "▸"; color: var(--fz-faint); }
.fz-disclose[open] > summary::before { content: "▾"; }
.fz-disclose > summary:hover { color: var(--fz-text); }
.fz-disclose-body { padding: .2rem 0 .6rem; }

/* ⭐⭐ fz-action: A DISCLOSURE WHOSE SUMMARY *IS* THE ACTION.
 *
 * 🚨 THE PLAIN .fz-disclose SUMMARY IS DELIBERATELY QUIET — --fz-dim at .82rem,
 * which is the same weight as a timestamp — and that is right when it hides
 * detail somebody may not want. It is WRONG when the thing behind it is the only
 * way to do something, because a person scanning for a control does not read
 * metadata.
 *
 * ⚠ MEASURED, NOT SUPPOSED. On meal.foltzcloud.com the only way to set what is
 * for dinner was a <summary> reading "change" at 13.12px in rgb(152,160,176).
 * Ashley opened the page, went looking for a way to edit the week, and reported
 * that she could not — the control was six pixels of triangle and a grey word
 * (job_f07301119144c394). Nothing was broken; nothing looked pressable.
 *
 * ⇒ SO THIS KEEPS THE DISCLOSURE AND CHANGES THE SIGNAL. Still <details>: still
 * keyboard-accessible, still works with no JS, still collapsed so seven nights
 * do not carry fourteen permanent forms — which is the problem the collapse was
 * introduced to fix and must not be undone. Only the summary changes, to look
 * like the button it behaves as.
 *
 * ⚠ IT IS NOT .fz-btn ON A <summary>. A summary is not a button: it must keep
 * its open/closed state and its disclosure glyph, so a reader can tell a control
 * that REVEALS from one that ACTS. This borrows the button's box and keeps the
 * triangle. */
.fz-disclose.fz-action > summary {
    color: var(--fz-text); font-size: .85rem;
    background: var(--fz-raised);
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    padding: .5rem .8rem;
}
.fz-disclose.fz-action > summary::before { color: var(--fz-dim); }
.fz-disclose.fz-action > summary:hover { border-color: var(--fz-accent); }
.fz-disclose.fz-action[open] > summary { border-color: var(--fz-accent); }

/* ── headings and form controls, by default (job_38505c2a6ef7b133) ──────────
 *
 * 🚨 THE CAUSE OF "NEW APPS LOOK BAD", FOUND BY LOOKING. On 2026-10-17
 * inventory.foltzcloud.com rendered its search box and both selects as the
 * browser's own WHITE controls on a dark page, and a two-line h1 at body leading
 * 1.55. foltzui styled a focus ring and an error border on a control and nothing
 * else, so every service that wrote no input rule shipped the browser's.
 *
 * ⭐ :where() MAKES ALL OF THIS SPECIFICITY ZERO, ON PURPOSE. Any rule a service
 * already writes — `input {}`, `.search input {}` — beats these whatever order the
 * sheets load in, so nothing that is already styled changes; only the controls
 * nobody styled stop being the browser's. That is "additive" for elements. */
:where(h1, h2, h3, h4) {
    font-family: var(--fz-font-display);
    line-height: var(--fz-leading-tight);
    text-wrap: balance;
}

:where(input:not([type=checkbox], [type=radio], [type=range], [type=color], [type=file], [type=submit], [type=button], [type=reset], [type=image], [type=hidden]), select, textarea) {
    font: inherit;
    color: var(--fz-text);
    background: var(--fz-raised);
    border: 1px solid var(--fz-line);
    border-radius: var(--fz-radius-sm);
    padding: .5rem .7rem;
    min-height: var(--fz-control);
    max-width: 100%;
}
:where(textarea) { min-height: 6rem; line-height: var(--fz-leading); }
:where(input, select, textarea)::placeholder { color: var(--fz-faint); opacity: 1; }
:where(input, select, textarea):hover { border-color: var(--fz-dim); }
:where(input, select, textarea):disabled { color: var(--fz-faint); cursor: not-allowed; }
/* the select's own arrow is drawn by the browser in ITS colours, so it is
 * replaced by one drawn in currentColor — readable on either ground */
:where(select:not([multiple], [size])) {
    appearance: none;
    padding-right: 2rem;
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 1.05rem) 55%, calc(100% - .7rem) 55%;
    background-size: .35rem .35rem;
    background-repeat: no-repeat;
}
:where(input[type=checkbox], input[type=radio], input[type=range], progress) { accent-color: var(--fz-accent); }
:where(input[type=checkbox], input[type=radio]) { width: 1.15rem; height: 1.15rem; vertical-align: -.2em; }
:where(fieldset) { border: 1px solid var(--fz-line); border-radius: var(--fz-radius-md); padding: var(--fz-space-4); margin: 0 0 var(--fz-space-5); }
:where(legend) { padding: 0 var(--fz-space-2); color: var(--fz-dim); font-weight: 600; }
:where(code, kbd, samp, pre) { font-family: var(--fz-font-mono); font-size: .92em; }

/* ⭐ A FIELD IS LABEL, HINT, CONTROL, ERROR — in that order, stacked, and the
 * control fills the field. A form is a column of fields no wider than a panel. */
.fz-form { display: grid; gap: var(--fz-space-5); max-width: var(--fz-panel); }
.fz-form.fz-wide { max-width: none; }
.fz-field { display: grid; gap: var(--fz-space-1); }
.fz-field > label, .fz-label { font-weight: 600; }
.fz-hint { color: var(--fz-dim); font-size: var(--fz-text-sm); margin: 0; }
.fz-field > input:not([type=checkbox], [type=radio]), .fz-field > select, .fz-field > textarea { width: 100%; }
.fz-check { display: flex; gap: var(--fz-space-2); align-items: flex-start; min-height: var(--fz-control); padding-top: .55rem; }
.fz-check input { margin-top: .2rem; flex: none; }
.fz-actions { display: flex; flex-wrap: wrap; gap: var(--fz-space-2); align-items: center; }
@media (max-width: 40rem) {
    .fz-form .fz-actions .fz-btn { flex: 1 1 10rem; }
}

/* The components docs/reference/ui-standard.md ranked and nobody had built
 * (job_8cda68ae8fdab343): relative time, the fleet modal, alert + toast, copy,
 * tabs, and the error summary with its field errors.
 *
 * ⭐ TOKENS ONLY. Not one literal colour in this file — the scoreboard at
 * build.foltzcloud.com/ui counts literals in every service, and the library that
 * sets the rule is the last place that may break it.
 *
 * ⚠ EACH ONE IS SHOWN, WITH ITS DO AND DON'T, AT /ui/gallery. A component with
 * no gallery entry fails gallery_test.go, so this file and that page cannot drift. */

/* ── relative time ─────────────────────────────────────────────────────────
 * The absolute time is in the title and the datetime attribute; the words are
 * for skimming. Dotted underline says "there is more here if you hover". */
.fz-time { color: var(--fz-faint); white-space: nowrap; }
.fz-time[title] { text-decoration: underline dotted var(--fz-line); text-underline-offset: 3px; }

/* ── alert: .fz-notice already exists; these add the glyph + word ─────────
 * 🚨 Pillar 2: colour is the second signal. foltzui.Notice writes the glyph and
 * the word into the markup, so a greyscale screenshot still says which kind. */
.fz-notice .fz-notice-kind { font-weight: 600; color: var(--fz-text); margin-right: .35rem; }
.fz-notice.fz-is-ok   .fz-notice-kind { color: var(--fz-ok); }
.fz-notice.fz-is-warn .fz-notice-kind { color: var(--fz-warn); }
.fz-notice.fz-is-bad  .fz-notice-kind { color: var(--fz-bad); }

/* ── toast ────────────────────────────────────────────────────────────────
 * One live region per page, created on first use by fzToast(). */
.fz-toasts {
    position: fixed; left: 50%; bottom: 1.2rem; transform: translateX(-50%);
    display: flex; flex-direction: column; gap: .4rem; align-items: center;
    z-index: 60; pointer-events: none; max-width: calc(100vw - 2rem);
}
.fz-toast {
    background: var(--fz-raised); color: var(--fz-text);
    border: 1px solid var(--fz-line); border-left-width: 3px;
    border-radius: var(--fz-radius);
    padding: .55rem .9rem; font-size: .88rem;
    opacity: 0; transition: opacity .18s;
}
.fz-toast.fz-on { opacity: 1; }
.fz-toast.fz-is-ok   { border-left-color: var(--fz-ok); }
.fz-toast.fz-is-warn { border-left-color: var(--fz-warn); }
.fz-toast.fz-is-bad  { border-left-color: var(--fz-bad); }

/* ── modal ────────────────────────────────────────────────────────────────
 * A native <dialog>, so focus trapping, Escape and the inert background are the
 * browser's and not ours to get wrong. */
.fz-modal {
    background: var(--fz-surface); color: var(--fz-text);
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    padding: 1.1rem 1.2rem; width: min(28rem, calc(100vw - 2rem));
}
.fz-modal::backdrop { background: color-mix(in srgb, var(--fz-bg) 75%, transparent); }
.fz-modal h2 { margin: 0 0 .5rem; font-size: 1.05rem; }
.fz-modal p { margin: 0 0 1rem; color: var(--fz-dim); }
.fz-modal .fz-modal-actions { display: flex; gap: .6rem; justify-content: flex-end; flex-wrap: wrap; }
/* html: a trusted body — a list, a path. It scrolls rather than pushing the
 * buttons off a phone's screen. */
.fz-modal .fz-modal-body { max-height: 50vh; overflow: auto; margin: 0 0 1rem; color: var(--fz-dim); }
.fz-modal .fz-modal-body p { margin: 0 0 .6rem; }
.fz-modal .fz-modal-body ul { margin: 0 0 .6rem; padding-left: 1.1rem; font-family: var(--fz-font-mono); font-size: .78rem; }
.fz-modal .fz-modal-body code { font-family: var(--fz-font-mono); font-size: .85em; }
.fz-modal[aria-busy="true"] h2::after { content: " …"; }

/* ── copy button — the markup; internal/copykey is the behaviour ─────────── */
.fz-copy {
    font: inherit; font-family: var(--fz-font-mono); font-size: .82rem;
    color: var(--fz-dim); background: var(--fz-raised);
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    padding: .25rem .6rem; min-height: 32px; cursor: pointer;
}
.fz-copy:hover { color: var(--fz-text); border-color: var(--fz-accent); }
.fz-copy::after { content: " ⧉"; color: var(--fz-faint); }

/* ── tabs ─────────────────────────────────────────────────────────────────
 * Without the script every panel shows, stacked — a page that still reads. */
.fz-tabs [role="tablist"] {
    display: flex; flex-wrap: wrap; gap: .2rem;
    border-bottom: 1px solid var(--fz-line); margin: 0 0 .8rem;
}
.fz-tabs [role="tab"] {
    font: inherit; color: var(--fz-dim); background: none; cursor: pointer;
    border: 1px solid transparent; border-bottom: none;
    border-radius: var(--fz-radius) var(--fz-radius) 0 0;
    padding: .5rem .9rem; min-height: 44px; margin-bottom: -1px;
}
.fz-tabs [role="tab"]:hover { color: var(--fz-text); }
.fz-tabs [role="tab"][aria-selected="true"] {
    color: var(--fz-text); font-weight: 600;
    background: var(--fz-bg); border-color: var(--fz-line);
    border-bottom: 1px solid var(--fz-bg);
}

/* ── error summary + field error (GOV.UK's pattern) ───────────────────────
 * The summary is focused on load and links to each field; each field repeats
 * its own message beside it, so neither half depends on the other being seen. */
.fz-error-summary {
    border: 2px solid var(--fz-bad); border-radius: var(--fz-radius);
    padding: .8rem 1rem; margin: 0 0 1.2rem; background: var(--fz-surface);
}
.fz-error-summary:focus { outline: 3px solid var(--fz-bad); outline-offset: 2px; }
.fz-error-summary h2 { margin: 0 0 .4rem; font-size: 1rem; color: var(--fz-text); }
.fz-error-summary ul { margin: 0; padding-left: 1.1rem; }
.fz-error-summary a { color: var(--fz-text); text-decoration-color: var(--fz-bad); }
.fz-field-error { display: block; margin: .2rem 0 .3rem; color: var(--fz-bad); font-weight: 600; font-size: .86rem; }
.fz-field-error::before { content: "✗ "; }
.fz-has-error input,
.fz-has-error select,
.fz-has-error textarea { border-color: var(--fz-bad); border-width: 2px; }

/* ── sub nav (SubNav, job_77ae92dac0032927) ─────────────────────────────
 * The pages inside the section a Nav tab marks. NOT sticky — one sticky bar
 * is the budget on a phone — and one row that scrolls sideways rather than
 * wrapping. The current page is a filled chip AND aria-current="page". */
.fz-subnav {
    display: flex; gap: .25rem; align-items: center;
    margin: -.8rem 0 1.2rem; padding: 0 0 .1rem;
    overflow-x: auto; scrollbar-width: none;
    font-size: .88rem;
}
.fz-subnav::-webkit-scrollbar { display: none; }
.fz-subnav a,
.fz-subnav .fz-here {
    flex: none; display: inline-flex; align-items: center;
    min-height: 2.75rem; padding: 0 .7rem; white-space: nowrap;
    border-radius: 999px; color: var(--fz-dim); text-decoration: none;
}
.fz-subnav a:hover { color: var(--fz-text); background: var(--fz-raised); }
.fz-subnav .fz-here { color: var(--fz-text); background: var(--fz-raised); font-weight: 600; }

/* ── section nav (SectionNav, job_fb7b8ad4d4a4556c) ─────────────────────
 * The sticky in-page bar for a long one-page site. ONE ROW, ALWAYS: on a phone
 * the chips scroll sideways inside the bar, so the page itself never scrolls
 * sideways and the bar never grows to three lines. The current section is a
 * filled chip AND aria-current="location" — shape for the eye, a word for a
 * screen reader, never colour alone. A page sets --fz-secnav-mark to change
 * the fill; everything else is the page's own tokens. */
.fz-secnav {
    position: sticky; top: 0; z-index: 20;
    background: var(--fz-bg);
    border-bottom: 1px solid var(--fz-line);
    padding-top: env(safe-area-inset-top);
}
.fz-secnav-in {
    display: flex; align-items: center; gap: .5rem;
    max-width: var(--fz-wrap); margin: 0 auto;
    padding: 0 max(1rem, env(safe-area-inset-right)) 0 max(1rem, env(safe-area-inset-left));
}
.fz-secnav-list {
    position: relative;
    display: flex; flex: 1 1 auto; min-width: 0;
    margin: 0 0 0 -.4rem; padding: 0; list-style: none;
    overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
}
.fz-secnav-list::-webkit-scrollbar { display: none; }
.fz-secnav-list[data-fz-more] {
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 2.5rem), transparent);
    mask-image: linear-gradient(to right, black calc(100% - 2.5rem), transparent);
}
.fz-secnav-list li { flex: none; }
/* the tap target is the full bar height; the chip inside it is what you see */
.fz-secnav-list a {
    display: inline-flex; align-items: center;
    min-height: 2.75rem; padding: 0 .1rem;
    color: var(--fz-dim); text-decoration: none; white-space: nowrap;
    font-weight: 600; font-size: .92rem;
}
.fz-secnav-list a > span {
    display: block; padding: .32rem .7rem .26rem;
    border-radius: 999px; border: 1px solid transparent;
}
.fz-secnav-list a:hover > span { background: var(--fz-raised); color: var(--fz-text); }
.fz-secnav-list a[aria-current] > span {
    background: var(--fz-secnav-mark, var(--fz-text)); border-color: var(--fz-secnav-mark, var(--fz-text));
    color: var(--fz-bg);
}
.fz-secnav-list a:focus-visible { outline: none; }
.fz-secnav-list a:focus-visible > span { outline: 2px solid var(--fz-text); outline-offset: 1px; }
.fz-secnav-act { display: flex; flex: none; gap: .35rem; }
.fz-secnav-act .fz-btn { padding: .4rem .8rem; text-decoration: none; display: inline-flex; align-items: center; }
/* a heading jumped to lands below the bar (secnav.js measures it) */
html:has(.fz-secnav) [id] { scroll-margin-top: var(--fz-secnav-h, 4rem); }
@media (prefers-reduced-motion: no-preference) {
    html:has(.fz-secnav) { scroll-behavior: smooth; }
}

/* ── the gallery's own do/don't labels ─────────────────────────────────── */
.fz-dodont { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: .6rem; margin: .6rem 0 0; }
.fz-dodont > div { font-size: .86rem; color: var(--fz-dim); }
.fz-dodont .fz-do::before   { content: "✓ Do: "; color: var(--fz-ok); font-weight: 600; }
.fz-dodont .fz-dont::before { content: "✗ Don't: "; color: var(--fz-bad); font-weight: 600; }

/* ── interview: rounds of multiple-choice questions (interview.go) ──────── */
.fz-interview { display: grid; gap: var(--fz-space-3); margin: 0; padding: 0; }
.fz-iq { border: 1px solid var(--fz-line); border-radius: var(--fz-radius); padding: var(--fz-space-3); margin: 0; display: grid; gap: var(--fz-space-2); }
.fz-iq legend { font-weight: 600; padding: 0 .3rem; }
.fz-iq-n { color: var(--fz-dim); }
.fz-iq-opt { display: flex; gap: var(--fz-space-2); align-items: flex-start; min-height: var(--fz-control); padding: .4rem .5rem; border-radius: var(--fz-radius); cursor: pointer; }
.fz-iq-opt:hover { background: var(--fz-raised); }
.fz-iq-opt input { margin-top: .25rem; flex: none; }
.fz-iq-opt > span { display: grid; gap: .1rem; }
.fz-iq-rec { color: var(--fz-accent); font-size: .82rem; font-weight: 600; }
.fz-iq-why { color: var(--fz-dim); font-size: .88rem; }
.fz-iq-other textarea { min-height: 2.6rem; }
.fz-iq-done { padding-left: 1.4rem; }
.fz-iq-done li { margin: 0 0 var(--fz-space-2); }

/* foltzui form controls v2 (job_3fa462d96e13eedf) — the composite controls.
 *
 * ⭐ THE KIT ALREADY STYLED THE PLAIN ONES (input, select, textarea, checkbox, in
 * base.css). What every service still hand-rolled was the five that are more than
 * one element: the needs catalogue (job_38505c2a6ef7b133) counted form controls
 * in 42 services, upload in 26, search in 21, dates in 19.
 *
 * ⚠ EACH IS A MARKUP CONTRACT FIRST AND A GO FUNCTION SECOND. Most of the fleet's
 * forms are html/template files that cannot call Go, so the classes below are the
 * API and foltzui.DateField / UploadField / SearchField / Switch / Segmented only
 * write the same markup for a page built in Go. controls.js binds by class on load
 * and adds the parts a template left out (the clear button, the switch track, the
 * upload list), so the smallest correct template is the native control in a div.
 *
 *   <div class="fz-date"><input type="date" …><button type="button" class="fz-date-pick" data-fz-date="today">Today</button></div>
 *   <div class="fz-upload" [data-fz-progress]><input type="file" id="x" …><label for="x" class="fz-upload-zone">…</label></div>
 *   <div class="fz-search"><input type="search" …></div>
 *   <label class="fz-switch"><input type="checkbox" role="switch" …> Words</label>
 *   <fieldset class="fz-seg"><legend>View</legend><div class="fz-seg-opts"><label><input type="radio" …><span>Grid</span></label>…</div></fieldset>
 *   <nav class="fz-seg" aria-label="View"><span aria-current="page">Grid</span>, then a link per other view</nav>
 *
 * 🚨 EVERY ONE IS A NATIVE CONTROL UNDERNEATH, which is what makes it keyboard-
 * operable without a line of key handling: a date input, a file input, radios, a
 * checkbox. The styling never replaces the element that takes the keys. */

/* ── date ─────────────────────────────────────────────────────────────────
 * The native picker, in the page's colours, with optional one-press picks.
 * ⚠ color-scheme is what turns the browser's calendar icon and popup dark; a
 * dark page without it draws a black icon on a black field (foltzday learnt this
 * in board.css; nobody else had). */
.fz-date { display: flex; flex-wrap: wrap; gap: var(--fz-space-2); align-items: center; }
.fz-date input { color-scheme: dark; flex: 1 1 11rem; min-width: 0; font-variant-numeric: tabular-nums; }
.fz-light .fz-date input { color-scheme: light; }
.fz-date input::-webkit-calendar-picker-indicator { cursor: pointer; padding: .3rem; margin-right: -.3rem; opacity: .85; }
.fz-date input::-webkit-calendar-picker-indicator:hover { opacity: 1; }
.fz-date-pick {
    font: inherit; font-size: var(--fz-text-sm); cursor: pointer;
    min-height: var(--fz-control); padding: 0 var(--fz-space-3);
    color: var(--fz-dim); background: none;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius-pill);
}
.fz-date-pick:hover { color: var(--fz-text); border-color: var(--fz-dim); }

/* ── upload ───────────────────────────────────────────────────────────────
 * A drop zone that is a <label> for a real file input, a list of what was
 * chosen, and a progress bar with its percentage in words.
 * ⭐ THE INPUT IS HIDDEN ONLY ONCE THE SCRIPT HAS RUN (.fz-js). Without the script
 * there is no list to say what was chosen, so the browser's own "receipt.jpg"
 * stays visible — a page that still works, not one that hides its own state. */
.fz-upload { display: grid; gap: var(--fz-space-2); }
.fz-upload.fz-js > input[type=file] {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.fz-upload-zone {
    display: grid; gap: var(--fz-space-1); justify-items: center; text-align: center;
    padding: var(--fz-space-5) var(--fz-space-4);
    min-height: calc(var(--fz-control) * 2);
    border: 2px dashed var(--fz-line); border-radius: var(--fz-radius-md);
    background: var(--fz-raised); color: var(--fz-text);
    cursor: pointer;
    font-weight: 400;
}
.fz-upload-zone:hover, .fz-upload.fz-is-over .fz-upload-zone { border-color: var(--fz-accent); }
/* ⚠ Over is a border AND words: the script swaps the call to action to "Drop it". */
.fz-upload.fz-is-over .fz-upload-zone { border-style: solid; }
.fz-upload > input[type=file]:focus-visible + .fz-upload-zone { outline: 2px solid var(--fz-accent); outline-offset: 2px; }
.fz-upload > input[type=file]:disabled + .fz-upload-zone { cursor: not-allowed; color: var(--fz-faint); border-color: var(--fz-line); }
.fz-upload-cta { font-weight: 600; }
.fz-upload-cta u { text-decoration-color: var(--fz-accent); text-underline-offset: 2px; }
.fz-upload-hint { color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-upload-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--fz-space-1); font-size: var(--fz-text-sm); }
.fz-upload-list:empty { display: none; }
.fz-upload-list li { overflow-wrap: anywhere; }
.fz-upload-list .fz-is-bad { color: var(--fz-bad); font-weight: 600; }
.fz-upload-bar { width: 100%; height: .6rem; }
.fz-upload-status { font-size: var(--fz-text-sm); color: var(--fz-dim); font-variant-numeric: tabular-nums; }
.fz-upload-status:empty { display: none; }
.fz-upload-status.fz-is-bad { color: var(--fz-bad); font-weight: 600; }

/* ── search ───────────────────────────────────────────────────────────────
 * A search box with a magnifier and a clear button that appears with a value.
 * ⚠ The browser's own cancel cross is removed: it is 12px, unlabelled, absent in
 * Firefox, and does not reset a search the SERVER ran (the clear button does). */
.fz-search { position: relative; display: block; min-width: 0; }
.fz-search > input { width: 100%; padding-left: 2.3rem; padding-right: var(--fz-control); }
.fz-search > input::-webkit-search-cancel-button,
.fz-search > input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
.fz-search::before {
    content: ""; position: absolute; left: .8rem; top: 50%; width: 1rem; height: 1rem; transform: translateY(-50%);
    background: var(--fz-dim); pointer-events: none;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='6.5' cy='6.5' r='5' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M10.3 10.3 15 15' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center/contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='6.5' cy='6.5' r='5' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M10.3 10.3 15 15' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center/contain no-repeat;
}
.fz-search-clear {
    position: absolute; right: 0; top: 0; bottom: 0;
    width: var(--fz-control); min-height: var(--fz-control);
    font: inherit; font-size: 1.05rem; line-height: 1; cursor: pointer;
    color: var(--fz-dim); background: none; border: 0; border-radius: var(--fz-radius-sm);
}
.fz-search-clear:hover { color: var(--fz-text); }

/* ── switch ───────────────────────────────────────────────────────────────
 * An on/off setting that takes effect as a setting, not a "tick to agree".
 * ⭐ A CHECKBOX WITH role="switch", visually hidden, and a track drawn beside it:
 * Space toggles, a screen reader says "switch, on". The track carries the WORD as
 * well as the knob and the fill — status is never colour alone. */
.fz-switch {
    position: relative; display: inline-flex; align-items: center; gap: var(--fz-space-3);
    min-height: var(--fz-control); cursor: pointer; font-weight: 400;
}
.fz-switch > input {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.fz-switch-track {
    position: relative; flex: none; width: 3.6rem; height: 1.9rem;
    border: 1px solid var(--fz-dim); border-radius: var(--fz-radius-pill);
    background: var(--fz-raised);
    transition: background var(--fz-dur-fast) var(--fz-ease);
}
.fz-switch-track::before {
    content: ""; position: absolute; top: .2rem; left: .2rem; width: 1.4rem; height: 1.4rem;
    border-radius: 50%; background: var(--fz-dim);
    transition: transform var(--fz-dur-fast) var(--fz-ease);
}
.fz-switch-track::after {
    content: "off"; position: absolute; right: .45rem; top: 50%; transform: translateY(-50%);
    font: 700 .62rem/1 var(--fz-font); text-transform: uppercase; letter-spacing: .04em; color: var(--fz-dim);
}
.fz-switch > input:checked + .fz-switch-track { background: var(--fz-accent); border-color: var(--fz-accent); }
.fz-switch > input:checked + .fz-switch-track::before { transform: translateX(1.7rem); background: var(--fz-on-accent); }
.fz-switch > input:checked + .fz-switch-track::after { content: "on"; right: auto; left: .55rem; color: var(--fz-on-accent); }
.fz-switch > input:focus-visible + .fz-switch-track { outline: 2px solid var(--fz-accent); outline-offset: 2px; }
.fz-switch > input:disabled + .fz-switch-track { opacity: .5; }
.fz-switch:has(> input:disabled) { cursor: not-allowed; color: var(--fz-faint); }

/* ── segmented ────────────────────────────────────────────────────────────
 * One choice of a few, all visible: a radio group (it posts) or a row of links
 * (it navigates). ⭐ The chosen segment is filled, bold AND ticked — three signals.
 * ⚠ It scrolls sideways on a phone rather than wrapping: a segmented control
 * broken over two lines reads as two controls. */
.fz-seg {
    display: inline-flex; max-width: 100%; overflow-x: auto;
    margin: 0; padding: 2px; min-width: 0;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius-md);
    background: var(--fz-raised);
}
.fz-seg > legend {
    float: left; width: 100%; padding: 0; margin: 0 0 var(--fz-space-1);
    font-weight: 600; color: var(--fz-text);
}
/* a fieldset's legend sits ABOVE the group, so the group is wrapped instead */
fieldset.fz-seg { display: block; border: 0; background: none; padding: 0; overflow: visible; }
.fz-seg-opts {
    display: inline-flex; max-width: 100%; overflow-x: auto; padding: 2px;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius-md); background: var(--fz-raised);
}
.fz-seg label { display: flex; margin: 0; font-weight: 400; }
.fz-seg input {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.fz-seg label > span, .fz-seg > a, .fz-seg > span {
    display: inline-flex; align-items: center; white-space: nowrap;
    min-height: calc(var(--fz-control) - 6px); padding: 0 var(--fz-space-4);
    border-radius: calc(var(--fz-radius-md) - 2px);
    color: var(--fz-dim); text-decoration: none; cursor: pointer;
}
.fz-seg label > span:hover, .fz-seg > a:hover { color: var(--fz-text); }
.fz-seg input:checked + span, .fz-seg > [aria-current] {
    background: var(--fz-accent); color: var(--fz-on-accent); font-weight: 700; cursor: default;
}
.fz-seg input:checked + span::before, .fz-seg > [aria-current]::before { content: "✓\00a0"; }
.fz-seg input:focus-visible + span { outline: 2px solid var(--fz-accent); outline-offset: 2px; }
.fz-seg input:disabled + span { color: var(--fz-faint); cursor: not-allowed; }

@media (prefers-reduced-motion: reduce) {
    .fz-switch-track, .fz-switch-track::before { transition: none; }
}

/* 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 PAGE KIT — whole pages, not just parts (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." foltzui had parts — a button, a card,
 * a nav row — and no PAGE, so every new app composed its own page from them and
 * every one came out differently flat. This is the page: an app shell, a page
 * header, and the layouts a service is made of — overview, list, detail, form,
 * settings, the three states, and a photo-led landing — each on /ui/gallery and
 * each a full page at /ui/gallery/layout/<name>.
 *
 * ⭐ THE NUMBERS ARE SNOWBRUSH'S, the site he named as looking good: a display
 * face, a 17px body, a steep scale with tight heading leading, a 64ch measure, a
 * 2.6rem section rhythm, filled 3.1rem buttons with soft corners, and one bold
 * thing per page. They live as tokens (tokens.css, v2); this file spends them.
 *
 * ⚠ OPT-IN BY ONE CLASS: everything here hangs off .fz-shell on <body>, or is a
 * .fz-* class nobody used before, so the 52 front ends already importing foltzui
 * do not change until somebody moves one onto the shell.
 *
 * ⭐ MOBILE FIRST. Every rule below is the phone; the two media queries add the
 * desktop. 40rem and 64rem are the only breakpoints (tokens.css says why). */

/* ⭐ THE DISPLAY FACE. Overpass (SIL OFL 1.1, the US highway-sign alphabet), one
 * variable woff2 carrying every weight, served by the service from FontPath
 * (Mount does it). A browser only fetches a face something renders in, and only
 * .fz-shell names it, so a page that never takes the shell never asks for it. */
@font-face {
    font-family: "fz-display";
    font-weight: 300 900;
    font-display: swap;
    src: url("/static/foltzui-display.woff2") format("woff2");
}

.fz-shell {
    --fz-font-display: "fz-display", var(--fz-font);
    --fz-size: var(--fz-size-roomy);
    --fz-radius: var(--fz-radius-md);
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    padding-bottom: env(safe-area-inset-bottom);
}
.fz-shell :focus-visible { outline-width: var(--fz-focus); }
.fz-shell .fz-btn { min-height: var(--fz-control); border-radius: var(--fz-radius-md); font-weight: 600; }
.fz-shell .fz-btn.fz-lg { min-height: var(--fz-control-lg); font: 700 var(--fz-text-md)/1 var(--fz-font-display); padding: 0 1.4rem; }
.fz-shell .fz-btn.fz-primary { color: var(--fz-on-accent); }
.fz-shell a.fz-btn { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }

/* ── the top bar: brand on the left, the service's own nav, account on the right.
 * 🚨 THE NAV SCROLLS SIDEWAYS ON A PHONE; IT NEVER WRAPS. Measured 2026-10-17:
 * inventory's eight-item nav wrapped to two lines of underlined links at 390px,
 * and a nav that changes height pushes the page about. One row, scrollable, with
 * the current item a filled chip — snowbrush's section bar, for pages. */
.fz-topbar {
    position: sticky; top: 0; z-index: 30;
    background: color-mix(in srgb, var(--fz-bg) 92%, transparent);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--fz-line);
    padding-top: env(safe-area-inset-top);
}
.fz-topbar-in {
    max-width: var(--fz-page); margin: 0 auto;
    display: flex; align-items: center; gap: var(--fz-space-3);
    padding: 0 var(--fz-space-4); min-height: 3.5rem;
}
.fz-brand {
    display: inline-flex; align-items: center; gap: var(--fz-space-2);
    font: 800 var(--fz-text-lg)/1 var(--fz-font-display);
    letter-spacing: -.01em; color: var(--fz-text); text-decoration: none; white-space: nowrap;
}
.fz-brand-mark {
    width: 1.6rem; height: 1.6rem; border-radius: var(--fz-radius-sm);
    background: var(--fz-brand-grad); color: var(--fz-on-accent);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: .95rem;
}
.fz-topnav {
    display: flex; gap: var(--fz-space-1); overflow-x: auto; scrollbar-width: none;
    flex: 1 1 auto; min-width: 0; margin: 0; padding: var(--fz-space-2) 0;
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 1.5rem), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 1.5rem), transparent);
}
.fz-topnav::-webkit-scrollbar { display: none; }
.fz-topnav a, .fz-topnav .fz-here {
    flex: none; display: inline-flex; align-items: center;
    min-height: 2.5rem; padding: 0 .8rem; border-radius: var(--fz-radius-pill);
    color: var(--fz-dim); text-decoration: none; font-weight: 600; font-size: var(--fz-text-sm);
}
.fz-topnav a:hover { color: var(--fz-text); background: var(--fz-raised); }
.fz-topnav .fz-here { color: var(--fz-text); background: var(--fz-raised); box-shadow: inset 0 0 0 1px var(--fz-line); }
.fz-account { flex: none; margin-left: auto; display: flex; align-items: center; gap: var(--fz-space-2); color: var(--fz-faint); font-size: var(--fz-text-sm); }
.fz-account .fz-who { display: none; }
@media (max-width: 40rem) {
    .fz-topbar-in { flex-wrap: wrap; gap: 0 var(--fz-space-3); padding-top: var(--fz-space-2); }
    .fz-topnav { order: 3; flex-basis: 100%; padding-top: 0; }
}
@media (min-width: 64rem) {
    .fz-account .fz-who { display: inline; }
}

/* ── the page column ─────────────────────────────────────────────────────── */
.fz-main { flex: 1 0 auto; width: 100%; max-width: var(--fz-page); margin: 0 auto; padding: var(--fz-space-6) var(--fz-space-4) var(--fz-space-section); }
.fz-main.fz-narrow { max-width: calc(var(--fz-panel) + 2 * var(--fz-space-4)); }
.fz-main.fz-bleed { max-width: none; padding: 0; }
.fz-main .fz-main { flex: none; }
.fz-footer {
    border-top: 1px solid var(--fz-line); color: var(--fz-faint); font-size: var(--fz-text-xs);
    padding: var(--fz-space-4); text-align: center;
}
.fz-footer a { color: var(--fz-faint); }

/* ── the page header: what this page is, in one line, and its one action ────
 * ⭐ ONE PRIMARY ACTION, on the right on a desktop and full width on a phone. */
.fz-pagehead { display: grid; gap: var(--fz-space-3); margin: 0 0 var(--fz-space-6); }
.fz-pagehead-crumbs { margin: 0; font-size: var(--fz-text-sm); color: var(--fz-faint); }
.fz-pagehead-crumbs a { color: var(--fz-dim); }
.fz-pagehead h1 {
    margin: 0; font-size: var(--fz-text-3xl); font-weight: var(--fz-weight-heading);
    letter-spacing: -.015em;
}
.fz-pagehead-lead { margin: 0; color: var(--fz-dim); max-width: var(--fz-measure); font-size: var(--fz-text-lg); }
.fz-pagehead-actions { display: flex; flex-wrap: wrap; gap: var(--fz-space-2); }
.fz-pagehead-actions .fz-btn { flex: 1 1 auto; }
@media (min-width: 40rem) {
    .fz-pagehead { grid-template-columns: 1fr auto; align-items: end; }
    .fz-pagehead > * { grid-column: 1; }   /* crumbs, title, lead: one column, whatever is present */
    .fz-pagehead-actions { grid-column: 2; grid-row: 1 / span 3; align-self: end; }
    .fz-pagehead-actions .fz-btn { flex: none; }
}

/* ── composition: a section, a stack, a grid ────────────────────────────── */
.fz-section { margin: 0 0 var(--fz-space-section); }
.fz-section > h2, .fz-section-head h2 { margin: 0 0 var(--fz-space-4); font-size: var(--fz-text-2xl); font-weight: var(--fz-weight-heading); letter-spacing: -.01em; }
.fz-section-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--fz-space-2) var(--fz-space-4); margin: 0 0 var(--fz-space-4); }
.fz-section-head h2 { margin: 0; }
.fz-stack { display: grid; gap: var(--fz-space-4); }
.fz-stack.fz-tight { gap: var(--fz-space-2); }
.fz-grid { display: grid; gap: var(--fz-space-4); grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); }
.fz-split { display: grid; gap: var(--fz-space-6); }
@media (min-width: 64rem) {
    .fz-split { grid-template-columns: minmax(0, 1fr) 20rem; align-items: start; }
    .fz-split > aside { position: sticky; top: 5rem; }
}
.fz-prose { max-width: var(--fz-measure); }
.fz-prose p, .fz-prose ul, .fz-prose ol { margin: 0 0 var(--fz-space-4); }
.fz-prose li { margin: var(--fz-space-2) 0; }
.fz-prose li::marker { color: var(--fz-accent); }
.fz-prose h2 { font-size: var(--fz-text-2xl); font-weight: var(--fz-weight-heading); margin: var(--fz-space-7) 0 var(--fz-space-3); }
.fz-prose h3 { font-size: var(--fz-text-lg); font-weight: 700; margin: var(--fz-space-6) 0 var(--fz-space-2); }
/* ⭐ RENDERED MARKDOWN (job_a9062aa0fa87c690). A guide, a doc or a help page is
 * markdown turned into bare <table>, <pre>, <blockquote> and <figure> with no fz-
 * class on any of them, so .fz-prose gives each its look here and the service
 * writes none. howtoplat's own prose sheet was the copy this replaced. A wide
 * table scrolls inside the measure rather than pushing the page sideways. */
.fz-prose > h1:first-child { margin-top: 0; }
.fz-prose table { border-collapse: collapse; width: 100%; margin: 0 0 var(--fz-space-4); display: block; overflow-x: auto; }
.fz-prose th, .fz-prose td { border: 1px solid var(--fz-line); padding: var(--fz-space-2) var(--fz-space-3); text-align: left; vertical-align: top; }
.fz-prose th { background: var(--fz-surface); font-weight: 700; }
.fz-prose pre { background: var(--fz-surface); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-md); padding: var(--fz-space-3) var(--fz-space-4); margin: 0 0 var(--fz-space-4); overflow-x: auto; line-height: 1.45; }
.fz-prose :not(pre) > code { background: var(--fz-surface); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm); padding: 0 .3em; }
.fz-prose blockquote { border-left: 3px solid var(--fz-accent); margin: 0 0 var(--fz-space-4); padding: var(--fz-space-1) var(--fz-space-4); color: var(--fz-dim); }
.fz-prose figure { margin: var(--fz-space-6) 0; }
.fz-prose figcaption { color: var(--fz-dim); font-size: var(--fz-text-sm); margin-top: var(--fz-space-2); }
.fz-prose img { display: block; max-width: 100%; height: auto; border: 1px solid var(--fz-line); border-radius: var(--fz-radius-md); }
.fz-prose dt { font-weight: 700; margin-top: var(--fz-space-3); }
.fz-prose dd { margin: var(--fz-space-1) 0 0 var(--fz-space-4); }

/* ── a panel: the shell's card, softer and roomier than .fz-card ──────────── */
.fz-panel {
    background: var(--fz-surface); border: 1px solid var(--fz-line);
    border-radius: var(--fz-radius-lg); padding: var(--fz-space-5);
    box-shadow: var(--fz-shadow-1);
}
.fz-panel > h2, .fz-panel > h3 { margin: 0 0 var(--fz-space-3); font-size: var(--fz-text-lg); font-weight: 700; }
.fz-panel > :last-child { margin-bottom: 0; }

/* ── a list of things: each item a row with a title that IS the link ────────
 * 🚨 NOT AN UNDERLINED LINK PER ITEM. inventory's list was a column of underlined
 * titles; here the whole row is the target, the title is bold, and the meta is
 * one quiet line under it. */
.fz-filters { display: flex; flex-wrap: wrap; gap: var(--fz-space-2); align-items: center; margin: 0 0 var(--fz-space-4); }
.fz-filters > input[type=search], .fz-filters > .fz-grow { flex: 1 1 14rem; }
.fz-filters > select { flex: 0 1 12rem; }
@media (max-width: 40rem) {
    .fz-filters > * { flex: 1 1 100%; }
    .fz-filters > select { flex: 1 1 calc(50% - var(--fz-space-2)); }
}
.fz-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg); background: var(--fz-surface); overflow: hidden; }
.fz-item { display: flex; gap: var(--fz-space-3); align-items: center; padding: var(--fz-space-3) var(--fz-space-4); border-top: 1px solid var(--fz-line); min-height: 3.75rem; position: relative; }
.fz-item:first-child { border-top: 0; }
.fz-item:hover { background: var(--fz-raised); }
.fz-item[aria-selected="true"] { background: var(--fz-raised); box-shadow: inset 3px 0 0 var(--fz-accent); }   /* a listbox option, e.g. a quick-launch */
.fz-item-main { flex: 1 1 auto; min-width: 0; }
.fz-item-title { display: block; font-weight: 700; color: var(--fz-text); text-decoration: none; overflow-wrap: anywhere; }
.fz-item-title::after { content: ""; position: absolute; inset: 0; }   /* the whole row is the target */
.fz-item-title:focus-visible { outline: none; }
.fz-item:has(.fz-item-title:focus-visible) { outline: var(--fz-focus) solid var(--fz-accent); outline-offset: -3px; }
.fz-item-meta { display: block; color: var(--fz-faint); font-size: var(--fz-text-sm); margin-top: .1rem; }
.fz-item-end { flex: none; color: var(--fz-dim); font-size: var(--fz-text-sm); font-variant-numeric: tabular-nums; text-align: right; }
.fz-item input, .fz-item button, .fz-item select, .fz-item summary, .fz-item a:not(.fz-item-title) { position: relative; z-index: 1; }   /* controls in a row — and a second link, e.g. sunstone's "Weigh" — stay pressable */
.fz-item > input[type=checkbox] { flex: none; }   /* a pick box keeps its size when the title wraps (inventory's bulk list, 390px) */
.fz-pager { display: flex; justify-content: space-between; align-items: center; gap: var(--fz-space-2); margin: var(--fz-space-4) 0 0; color: var(--fz-faint); font-size: var(--fz-text-sm); }

/* ── app tiles: a launcher's doors (job_8389a39f50cb4b8e, foltzcloud.com) ──
   The whole tile is the target and never shorter than --fz-tap: a launcher is
   opened one-handed on a phone. Name, one line of purpose, then a foot for the
   host and a status chip. Grouped under .fz-section headings, two columns on a
   phone so forty apps are not forty screens. */
.fz-tiles { display: grid; gap: var(--fz-space-3); grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr)); margin: 0; padding: 0; list-style: none; }
@media (max-width: 40rem) { .fz-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--fz-space-2); } }
.fz-tile {
    display: flex; flex-direction: column; gap: var(--fz-space-1); min-height: var(--fz-tap); height: 100%;
    padding: var(--fz-space-3) var(--fz-space-4); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg);
    background: var(--fz-surface); color: var(--fz-text); text-decoration: none; overflow-wrap: anywhere;
}
.fz-tile:hover { background: var(--fz-raised); border-color: var(--fz-accent); }
.fz-tile:focus-visible { outline: var(--fz-focus) solid var(--fz-accent); outline-offset: 2px; }
.fz-tile-name { font: 800 var(--fz-text-lg)/1.2 var(--fz-font-display); }
.fz-tile-what { color: var(--fz-dim); font-size: var(--fz-text-sm); line-height: 1.35; }
.fz-tile-foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fz-space-2); margin-top: auto; padding-top: var(--fz-space-1); color: var(--fz-faint); font-size: var(--fz-text-xs); }

/* ── a word-chip: status as a WORD, snowbrush's .sb-fix-state, shared ────── */
.fz-chip {
    display: inline-flex; align-items: center; gap: .3em;
    font: 700 var(--fz-text-xs)/1 var(--fz-font-display); text-transform: uppercase; letter-spacing: .05em;
    padding: .35rem .5rem .3rem; border-radius: var(--fz-radius-sm);
    border: 1px solid var(--fz-line); background: var(--fz-raised); color: var(--fz-dim);
    white-space: nowrap;
}
.fz-chip.fz-is-ok   { color: var(--fz-ok);   border-color: currentColor; background: none; }
.fz-chip.fz-is-warn { color: var(--fz-warn); border-color: currentColor; background: none; }
.fz-chip.fz-is-bad  { color: var(--fz-bad);  border-color: currentColor; background: none; }

/* ── a detail page: a key/value block and a side column ─────────────────── */
.fz-facts { display: grid; grid-template-columns: max-content 1fr; gap: var(--fz-space-2) var(--fz-space-5); margin: 0; }
.fz-facts dt { color: var(--fz-dim); }
.fz-facts dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }
@media (max-width: 40rem) {
    .fz-facts { grid-template-columns: 1fr; gap: 0; }
    .fz-facts dd { margin-bottom: var(--fz-space-3); }
}

/* ── settings: a section per concern, label left and control right on a desk */
.fz-setting { display: grid; gap: var(--fz-space-2); padding: var(--fz-space-4) 0; border-top: 1px solid var(--fz-line); }
.fz-setting:first-of-type { border-top: 0; padding-top: 0; }
.fz-setting h3 { margin: 0; font-size: var(--fz-text-md); font-weight: 700; }
.fz-setting p { margin: 0; color: var(--fz-dim); font-size: var(--fz-text-sm); }
@media (min-width: 40rem) {
    .fz-setting { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; gap: var(--fz-space-1) var(--fz-space-6); }
    .fz-setting > :last-child { grid-column: 2; grid-row: 1 / span 2; justify-self: end; }
}

/* ── the three states a page is ever in besides "working" ─────────────────── */
.fz-state-page { text-align: center; max-width: var(--fz-panel); margin: var(--fz-space-7) auto; padding: var(--fz-space-7) var(--fz-space-5); }
.fz-state-glyph { font-size: 2.4rem; line-height: 1; margin: 0 0 var(--fz-space-3); color: var(--fz-faint); }
.fz-state-page h2 { margin: 0 0 var(--fz-space-2); font-size: var(--fz-text-xl); font-weight: var(--fz-weight-heading); }
.fz-state-page p { margin: 0 0 var(--fz-space-5); color: var(--fz-dim); }
.fz-state-page.fz-is-bad .fz-state-glyph { color: var(--fz-bad); }
/* loading: a skeleton the shape of what is coming, never a spinner alone */
.fz-skeleton { display: block; height: 1rem; border-radius: var(--fz-radius-sm); background: var(--fz-raised); margin: 0 0 var(--fz-space-2); }
.fz-skeleton.fz-is-title { height: 1.6rem; width: 55%; }
.fz-skeleton.fz-is-short { width: 35%; }
@media (prefers-reduced-motion: no-preference) {
    .fz-skeleton { background: linear-gradient(90deg, var(--fz-raised) 0 40%, var(--fz-line) 50%, var(--fz-raised) 60% 100%); background-size: 300% 100%; animation: fz-shimmer 1.6s linear infinite; }
    @keyframes fz-shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
}

/* ── a landing page: photo-led, snowbrush's hero made shared ────────────────
 * The photo is full-bleed, min(78svh, 640px) tall, under a scrim that keeps the
 * title at 4.5:1 whatever the picture; the title is the one --fz-text-hero line. */
.fz-hero {
    position: relative; overflow: hidden; color: #fff;
    min-height: 420px; height: min(78svh, 640px);
    display: flex; align-items: flex-end;
    background: rgb(28 32 30);   /* dark under the photo, so the white title still reads if the image never loads */
}
.fz-hero > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.fz-hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgb(20 24 22 / .15) 0%, rgb(20 24 22 / 0) 35%, rgb(20 24 22 / .78) 100%); }
.fz-hero-text { position: relative; z-index: 1; width: 100%; max-width: var(--fz-page); margin: 0 auto; padding: 0 var(--fz-space-4) var(--fz-space-6); }
.fz-hero h1 { margin: 0; font: 800 var(--fz-text-hero)/1 var(--fz-font-display); letter-spacing: -.01em; }
.fz-hero p { margin: var(--fz-space-2) 0 0; font-size: var(--fz-text-lg); opacity: .92; max-width: var(--fz-measure); }
.fz-hero .fz-chip { background: #e7c25f; color: #23282b; border-color: transparent; margin-bottom: var(--fz-space-3); }
.fz-cta { display: flex; flex-wrap: wrap; gap: var(--fz-space-2); margin: 0 0 var(--fz-space-section); }
.fz-cta .fz-btn { flex: 1 1 10rem; }
.fz-feature { max-width: var(--fz-panel); background: var(--fz-accent); color: var(--fz-on-accent); border-radius: var(--fz-radius-lg); padding: var(--fz-space-5); }
.fz-feature h2 { margin: 0 0 var(--fz-space-2); font-size: var(--fz-text-lg); font-weight: 800; color: inherit; }
.fz-feature p { margin: 0; }
.fz-figs { display: grid; gap: var(--fz-space-3); grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); margin: 0; }
.fz-figs figure { margin: 0; }
.fz-figs img, .fz-figs .fz-fig-slot { display: block; width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; border-radius: var(--fz-radius-md); background: var(--fz-raised); }
/* a photo that is coming: hatched and dashed, so it reads as a slot, not a fault */
.fz-figs .fz-fig-slot { border: 1px dashed var(--fz-line); background: repeating-linear-gradient(135deg, transparent 0 14px, var(--fz-line) 14px 15px), var(--fz-raised); }
.fz-figs figcaption { font-size: var(--fz-text-sm); color: var(--fz-dim); padding: var(--fz-space-1) .1rem 0; }

/* ── a meter: how full, against a limit (job_b8f7e382271e8d9e). The bar is for
   skimming; .fz-meter-text carries the answer and, past a threshold, the word. */
.fz-meter { display: inline-flex; flex-direction: column; gap: var(--fz-space-1); min-width: min(100%, 11rem); }
.fz-meter-bar { display: block; height: .5rem; border-radius: var(--fz-radius-sm); background: var(--fz-raised); border: 1px solid var(--fz-line); overflow: hidden; }
.fz-meter-fill { display: block; height: 100%; background: var(--fz-accent); }
.fz-meter-text { color: var(--fz-dim); font-size: var(--fz-text-sm); white-space: nowrap; }
.fz-meter.fz-is-warn .fz-meter-fill { background: var(--fz-warn); }
.fz-meter.fz-is-warn .fz-meter-text { color: var(--fz-warn); }
.fz-meter.fz-is-bad .fz-meter-fill { background: var(--fz-bad); }
.fz-meter.fz-is-bad .fz-meter-text { color: var(--fz-bad); }
.fz-meter.fz-is-open .fz-meter-text { font-style: italic; }

/* ── print: a page made to be printed (sunstone's go-home packet,
 * job_6f62c47506221b12). Paper is a light ground whatever the room, so the text
 * tokens take the light set's values; the top bar, the footer, the crumbs and
 * anything marked .fz-noprint (a form, "print it from the menu") stay on screen. */
@media print {
    .fz-shell {
        --fz-bg: #ffffff; --fz-surface: #ffffff; --fz-raised: #f4efe7; --fz-line: #e7ddd0;
        --fz-text: #2b2622; --fz-dim: #6b6259; --fz-faint: #726860;
        --fz-shadow-1: none;
        background: var(--fz-bg); color: var(--fz-text);
    }
    .fz-topbar, .fz-footer, .fz-pagehead-crumbs, .fz-noprint, .fz-shell form { display: none !important; }
    .fz-main { max-width: none; padding: 0; }
}

/* 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; }

/* 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); }

/* ── charts (job_fb9e326b42379a51) ──────────────────────────────────────────
 * chart.go draws them; this colours them. Every colour is a token: the series
 * take --fz-cat-*, the furniture takes --fz-line / --fz-dim / --fz-faint.
 *
 * ⚠ SVG TEXT IS COLOURED WITH `color` AND DRAWN WITH fill: currentColor, so the
 * fleet's a11y audit (which reads `color`) measures what is really on screen. */
.fz-cat-a { --fz-c: var(--fz-cat-a); }
.fz-cat-b { --fz-c: var(--fz-cat-b); }
.fz-cat-c { --fz-c: var(--fz-cat-c); }
.fz-cat-d { --fz-c: var(--fz-cat-d); }
.fz-cat-e { --fz-c: var(--fz-cat-e); }
.fz-cat-f { --fz-c: var(--fz-cat-f); }
.fz-cat-g { --fz-c: var(--fz-cat-g); }
.fz-cat-h { --fz-c: var(--fz-cat-h); }

.fz-chart { margin: 0 0 var(--fz-space-5); min-width: 0; container-type: inline-size; }
/* ⭐ TWO DRAWINGS, ONE SHOWN: chart.go draws wide and narrow, and the figure's
 * own width picks — so a chart in a phone column or a half-width grid cell keeps
 * 12px labels instead of scaling them to 6. */
.fz-chart-svg.fz-is-narrow { display: none; }
@container (max-width: 30rem) {
    .fz-chart-svg.fz-is-wide { display: none; }
    .fz-chart-svg.fz-is-narrow { display: block; }
}
.fz-chart-title { font-weight: var(--fz-weight-heading); margin: 0 0 var(--fz-space-1); }
.fz-chart-sum { margin: 0 0 var(--fz-space-2); color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-chart-svg { display: block; width: 100%; height: auto; overflow: visible; }
.fz-chart-axis { color: var(--fz-dim); fill: currentColor; font: 12px/1 var(--fz-font); font-variant-numeric: tabular-nums; }
.fz-chart-grid { stroke: var(--fz-line); stroke-width: 1; }
.fz-chart-zero { stroke: var(--fz-faint); stroke-width: 1; }
.fz-chart-rule { stroke: var(--fz-dim); stroke-width: 1.5; stroke-dasharray: 5 4; }
.fz-chart-rule-label { font-weight: 600; }
/* only Chart.SVG draws these: a chart that stands alone in a fixed box */
.fz-chart-frame { fill: var(--fz-surface); stroke: var(--fz-line); }
.fz-chart-heading { color: var(--fz-text); font-size: 14px; font-weight: 600; }

.fz-chart-line { fill: none; stroke: var(--fz-c, var(--fz-cat-a)); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.fz-chart-area { fill: var(--fz-c, var(--fz-cat-a)); fill-opacity: .22; stroke: none; }
.fz-chart-fill { fill: var(--fz-c, var(--fz-cat-a)); }
.fz-chart-dot { fill: var(--fz-c, var(--fz-cat-a)); stroke: var(--fz-surface); stroke-width: 1; }
/* ⭐ THE SECOND CHANNEL: series differ by dash as well as by hue. */
.fz-chart-dash-1 { stroke-dasharray: 7 4; }
.fz-chart-dash-2 { stroke-dasharray: 2 3; }
.fz-chart-dash-3 { stroke-dasharray: 9 3 2 3; }
.fz-chart-bar:hover, .fz-chart-span:hover, .fz-chart-moment:hover { fill-opacity: .8; }
.fz-chart-moment { stroke: var(--fz-surface); stroke-width: 1; }

.fz-chart-hit { fill: var(--fz-text); fill-opacity: 0; }
.fz-chart-hit:hover, .fz-chart-hit.fz-is-hover { fill-opacity: .07; }

.fz-chart-legend { display: flex; flex-wrap: wrap; gap: var(--fz-space-1) var(--fz-space-4); list-style: none; margin: var(--fz-space-2) 0 0; padding: 0; color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-chart-legend li { display: inline-flex; align-items: center; gap: var(--fz-space-2); }
.fz-chart-key { flex: none; }
.fz-chart-data { margin-top: var(--fz-space-2); }
.fz-chart-num { text-align: right; font-variant-numeric: tabular-nums; }

/* the hover box chart.js places; the <title> on each mark is the fallback */
.fz-chart-tip { position: fixed; z-index: 50; pointer-events: none; max-width: 22rem; padding: var(--fz-space-1) var(--fz-space-2);
    background: var(--fz-raised); color: var(--fz-text); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm);
    box-shadow: var(--fz-shadow-1); font-size: var(--fz-text-sm); }

/* a sparkline sits in a sentence or a stat tile */
.fz-spark { display: inline-block; vertical-align: middle; overflow: visible; max-width: 100%; height: auto; }
.fz-spark .fz-chart-line { stroke-width: 1.5; }
.fz-spark-band { fill: var(--fz-text); fill-opacity: .08; }
.fz-stat-trend { display: block; margin-top: var(--fz-space-2); }
.fz-stat-trend .fz-spark { width: 100%; height: 2rem; }

/* foltzui editor kit (job_5ebf822aef037d1a) — the chrome every editor draws around
 * its canvas: menubar, toolbar, icon button, palette, inspector, layers, segmented.
 *
 * ⭐ THE NEEDS CATALOGUE COUNTED IT (job_38505c2a6ef7b133, step 6): editor toolbars
 * hand-rolled in 12 services, and all 8 editors — studio, canvas, cad, slides,
 * sheets, press, film, daw — each with its own menubar of <details>, its own
 * 2.6rem tool square, its own pressed state and its own panel heading. Eight
 * copies of one idea, in eight slightly different sizes.
 *
 * ⚠ A MARKUP CONTRACT FIRST, GO SECOND — like controls.css. Every editor is a
 * template plus a large script that renders rows at run time, so the classes are
 * the API and foltzui.MenuBar / Toolbar / Palette / IconButton / SegButtons /
 * Inspector / Layers only write the same markup. editor.js adds the keys.
 *
 *   <div class="fz-editor">                      the shell: menubar and toolbar on
 *     <nav class="fz-menubar" aria-label="Menus">  top, palette left, stage centre,
 *       <details class="fz-menu"><summary>File</summary>   inspector right, status under
 *         <div class="fz-menu-list"><button type="button">Undo <span class="fz-key">Ctrl+Z</span></button><hr>…</div>
 *       </details>…<span class="fz-menubar-end">Doc name</span>
 *     </nav>
 *     <div class="fz-toolbar" role="toolbar" aria-label="Formatting">
 *       <button class="fz-iconbtn" aria-label="Bold" aria-pressed="false">B</button>
 *       <span class="fz-toolbar-sep"></span>
 *       <div class="fz-seg" role="group" aria-label="Align" data-fz-single><button aria-pressed="true">Left</button>…</div>
 *     </div>
 *     <div class="fz-palette" role="toolbar" aria-orientation="vertical" aria-label="Tools">…icon buttons…</div>
 *     <div class="fz-stage">…the canvas…</div>
 *     <aside class="fz-inspector" aria-label="Inspector">
 *       <details class="fz-pane" open><summary class="fz-pane-head">Layers</summary><div class="fz-pane-body">
 *         <ul class="fz-layers" aria-label="Layers, top first"><li class="fz-layer" aria-current="true">
 *           <button class="fz-layer-eye" aria-pressed="true" aria-label="Visible: Sky">◉</button>
 *           <button class="fz-layer-pick"><span class="fz-layer-name">Sky</span><span class="fz-layer-meta">normal · 100%</span></button>
 *         </li></ul>
 *         <div class="fz-prop"><label for="x">Opacity</label><input id="x" type="range"></div>
 *       </div></details>
 *     </aside>
 *     <div class="fz-statusbar" aria-live="polite">…</div>
 *   </div>
 *
 * 🚨 A PRESSED TOOL SAYS SO THREE WAYS — a filled square, the accent border AND a
 * bar along its foot — because "status is never colour alone" and the pressed tool
 * is the one state a person reads before every stroke. The same for the chosen
 * layer: a raised row, a bar down its side and its name in bold.
 *
 * ⚠ 44px ON A TOUCH SCREEN, ALWAYS. .fz-sm makes an icon button 36px only where
 * the pointer is fine (a mouse); on a finger it stays at --fz-control. */

/* ── the shell ────────────────────────────────────────────────────────────
 * Optional. An editor whose stage needs its own grid (a timeline under a monitor,
 * a sheet with a formula bar) keeps its own and uses the pieces alone. */
.fz-editor {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) minmax(15rem, 19rem);
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    grid-template-areas:
        "menu    menu  menu"
        "tools   tools tools"
        "palette stage side"
        "status  status status";
    gap: var(--fz-space-2);
    height: calc(100dvh - 7.5rem); min-height: 30rem;
}
.fz-editor > .fz-menubar   { grid-area: menu; }
.fz-editor > .fz-toolbar   { grid-area: tools; }
.fz-editor > .fz-palette   { grid-area: palette; }
.fz-editor > .fz-stage     { grid-area: stage; }
.fz-editor > .fz-inspector { grid-area: side; }
.fz-editor > .fz-statusbar { grid-area: status; }
/* a long palette scrolls inside its column rather than running off the editor
 * (.fz-two halves its height) */
.fz-editor > .fz-palette   { overflow-y: auto; min-height: 0; }

.fz-stage {
    position: relative; overflow: auto; min-height: 0; min-width: 0;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    background: var(--fz-bg); touch-action: none;
}
.fz-statusbar {
    display: flex; flex-wrap: wrap; gap: var(--fz-space-2) var(--fz-space-4); align-items: center;
    font-size: var(--fz-text-xs); color: var(--fz-dim); font-variant-numeric: tabular-nums;
}

/* ── menubar ──────────────────────────────────────────────────────────────
 * A row of <details> menus. Native, so a menu opens with Enter or Space and with
 * no script at all; editor.js adds the menubar keys (arrows across and down,
 * Escape back to the title, one menu open at a time, a click outside closes). */
.fz-menubar {
    display: flex; flex-wrap: wrap; gap: var(--fz-space-1); align-items: center; min-width: 0;
}
.fz-menu { position: relative; }
.fz-menu > summary {
    list-style: none; cursor: pointer; user-select: none;
    display: inline-flex; align-items: center; min-height: 2.25rem;
    padding: 0 var(--fz-space-3); font-size: var(--fz-text-sm);
    border: 1px solid transparent; border-radius: var(--fz-radius);
}
.fz-menu > summary::-webkit-details-marker { display: none; }
.fz-menu > summary:hover, .fz-menu[open] > summary { border-color: var(--fz-line); background: var(--fz-surface); }
.fz-menu[open] > summary { font-weight: 600; }
.fz-menu-list {
    position: absolute; z-index: 30; top: calc(100% + 2px); left: 0;
    min-width: 15rem; max-height: min(70vh, 32rem); overflow-y: auto;
    display: grid; padding: var(--fz-space-1);
    background: var(--fz-raised); border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    box-shadow: var(--fz-shadow-2);
}
.fz-menu-list > button, .fz-menu-list > a, .fz-menu-list > label {
    all: unset; box-sizing: border-box; cursor: pointer;
    display: flex; justify-content: space-between; align-items: center; gap: var(--fz-space-5);
    min-height: 2.1rem; padding: 0 var(--fz-space-3);
    border-radius: calc(var(--fz-radius) - 2px); font-size: var(--fz-text-sm); color: var(--fz-text);
}
.fz-menu-list > button:hover, .fz-menu-list > a:hover, .fz-menu-list > label:hover,
.fz-menu-list > button:focus-visible, .fz-menu-list > a:focus-visible {
    background: var(--fz-surface); outline: 2px solid var(--fz-accent); outline-offset: -2px;
}
.fz-menu-list > button:disabled, .fz-menu-list > [aria-disabled="true"] { color: var(--fz-faint); cursor: default; }
.fz-menu-list > button[aria-checked="true"]::before { content: "✓"; margin-right: -.6rem; }
.fz-menu-list > hr { border: 0; border-top: 1px solid var(--fz-line); margin: var(--fz-space-1) 0; }
.fz-menu-list > .fz-menu-head {
    font-size: var(--fz-text-xs); text-transform: uppercase; letter-spacing: .06em;
    color: var(--fz-dim); padding: var(--fz-space-2) var(--fz-space-3) var(--fz-space-1);
}
.fz-key { color: var(--fz-faint); font-family: var(--fz-font-mono); font-size: .8em; white-space: nowrap; }
.fz-menubar-end {
    margin-left: auto; min-width: 0; max-width: 40%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600;
}
input.fz-menubar-end { font-weight: 600; }

/* ── toolbar ──────────────────────────────────────────────────────────────
 * One row of controls, one tab stop: editor.js roves the arrow keys across its
 * buttons (WAI-ARIA toolbar), so Tab leaves the toolbar instead of walking forty
 * squares. A select or a text field in it keeps its own arrows. */
.fz-toolbar {
    display: flex; flex-wrap: wrap; gap: var(--fz-space-1); align-items: center; min-width: 0;
    padding: var(--fz-space-1) var(--fz-space-2);
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius); background: var(--fz-surface);
    font-size: var(--fz-text-sm);
}
.fz-toolbar select, .fz-toolbar input:not([type=checkbox]):not([type=radio]):not([type=color]) {
    min-height: 2.25rem; width: auto; font-size: var(--fz-text-sm);
}
.fz-toolbar label { display: inline-flex; gap: var(--fz-space-1); align-items: center; margin: 0; font-weight: 400; }
.fz-toolbar input[type=color] {
    width: 2.25rem; height: 2.25rem; padding: 2px; cursor: pointer;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm); background: var(--fz-raised);
}
.fz-toolbar-sep { align-self: stretch; width: 1px; margin: var(--fz-space-1) var(--fz-space-1); background: var(--fz-line); }
.fz-toolbar-group { display: inline-flex; gap: 2px; align-items: center; }
.fz-toolbar-grow { flex: 1 1 auto; }

/* ── icon button ──────────────────────────────────────────────────────────
 * A square holding a glyph. 🚨 IT MUST CARRY aria-label — the glyph is not a
 * name — and a title saying the same with its shortcut. With aria-pressed it is
 * a tool or a toggle. A word fits too: .fz-iconbtn.fz-wide grows to its label. */
.fz-iconbtn {
    font: inherit; font-size: 1.05rem; line-height: 1; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center; gap: var(--fz-space-1);
    min-width: var(--fz-control); height: var(--fz-control); padding: 0;
    color: var(--fz-text); background: var(--fz-raised);
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    position: relative; flex: 0 0 auto;
}
.fz-iconbtn.fz-wide { padding: 0 var(--fz-space-3); font-size: var(--fz-text-sm); }
.fz-iconbtn.fz-quiet { background: none; border-color: transparent; }
.fz-iconbtn:hover { border-color: var(--fz-accent); }
.fz-iconbtn:disabled, .fz-iconbtn[aria-disabled="true"] { color: var(--fz-faint); border-color: var(--fz-line); cursor: default; }
.fz-iconbtn[aria-pressed="true"], .fz-iconbtn[aria-expanded="true"] {
    color: var(--fz-accent); border-color: var(--fz-accent);
    background: color-mix(in srgb, var(--fz-accent) 16%, var(--fz-raised));
    box-shadow: inset 0 -3px 0 var(--fz-accent);
}
.fz-iconbtn.fz-danger { color: var(--fz-bad); }
/* a pressed danger button is a RECORD button: pressed reads in --fz-bad */
.fz-iconbtn.fz-danger[aria-pressed="true"] {
    color: var(--fz-bad); border-color: var(--fz-bad);
    background: color-mix(in srgb, var(--fz-bad) 16%, var(--fz-raised));
    box-shadow: inset 0 -3px 0 var(--fz-bad);
}
@media (pointer: fine) {
    .fz-iconbtn.fz-sm { min-width: 2.25rem; height: 2.25rem; font-size: .95rem; }
}

/* ── palette ──────────────────────────────────────────────────────────────
 * The tools, in a column beside the stage; a row that scrolls sideways on a
 * phone. It is a toolbar (role="toolbar" aria-orientation="vertical"), so it roves
 * with Up and Down. .fz-swatch-pair is the foreground/background colour pair. */
.fz-palette {
    display: grid; grid-auto-rows: min-content; gap: var(--fz-space-1); align-content: start; justify-items: center;
}
.fz-palette.fz-two { grid-template-columns: repeat(2, auto); }
.fz-palette > hr { border: 0; border-top: 1px solid var(--fz-line); margin: var(--fz-space-1) 0; width: 100%; grid-column: 1 / -1; }
.fz-swatch-pair { position: relative; width: var(--fz-control); height: var(--fz-control); margin-top: var(--fz-space-2); }
.fz-swatch-pair input {
    position: absolute; width: 1.7rem; height: 1.7rem; min-height: 0; padding: 0; cursor: pointer;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm);
}
.fz-swatch-pair input:first-child { left: 0; top: 0; z-index: 1; }
.fz-swatch-pair input:last-child { right: 0; bottom: 0; }

/* ── inspector ────────────────────────────────────────────────────────────
 * The column of panes beside the stage. A pane is a <details> (collapsible, no
 * script) or a <section>; its head is the small-caps line every editor drew. */
.fz-inspector {
    display: flex; flex-direction: column; gap: var(--fz-space-2); min-height: 0; min-width: 0; overflow: auto;
}
.fz-pane {
    flex: none; display: flex; flex-direction: column; min-height: 0;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius); background: var(--fz-surface);
}
.fz-pane-head {
    display: flex; align-items: center; gap: var(--fz-space-2);
    margin: 0; padding: var(--fz-space-2) var(--fz-space-3);
    font-size: var(--fz-text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
    color: var(--fz-dim);
}
.fz-pane > .fz-pane-head + * { border-top: 1px solid var(--fz-line); }
summary.fz-pane-head { list-style: none; cursor: pointer; }
summary.fz-pane-head::-webkit-details-marker { display: none; }
summary.fz-pane-head::before { content: "▸"; font-size: .9em; }
details[open] > summary.fz-pane-head::before { content: "▾"; }
.fz-pane-head > .fz-pane-act { margin-left: auto; text-transform: none; letter-spacing: 0; }
/* ⚠ max-content rows: a pane body capped in height SCROLLS; without this its
 * scrolling children (a .fz-layers list) were shrunk to nothing to fit — measured
 * on studio's brush pane, live, 2026-10-19. */
.fz-pane-body { display: grid; grid-auto-rows: max-content; gap: var(--fz-space-2); padding: var(--fz-space-2) var(--fz-space-3); font-size: var(--fz-text-sm); min-height: 0; overflow: auto; }
.fz-pane-body.fz-flush { padding: 0; gap: 0; }
.fz-pane-foot {
    display: flex; flex-wrap: wrap; gap: 2px; padding: var(--fz-space-1) var(--fz-space-2);
    border-top: 1px solid var(--fz-line);
}
.fz-pane-sub {
    margin: var(--fz-space-1) 0 0; font-size: var(--fz-text-xs); text-transform: uppercase;
    letter-spacing: .05em; color: var(--fz-dim);
}
/* A property row: the label on the left, the control on the right. */
.fz-prop {
    display: grid; grid-template-columns: minmax(4.5rem, 7rem) minmax(0, 1fr) auto;
    gap: var(--fz-space-2); align-items: center; margin: 0; font-weight: 400;
}
.fz-prop > :first-child { color: var(--fz-dim); font-weight: 400; margin: 0; }
.fz-prop > :nth-child(2):last-child { grid-column: 2 / -1; }
.fz-prop input:not([type=checkbox]):not([type=radio]):not([type=range]), .fz-prop select { min-height: 2.25rem; width: 100%; }
.fz-prop output { font-family: var(--fz-font-mono); font-size: var(--fz-text-xs); text-align: right; min-width: 2.5rem; }

/* ── layers ───────────────────────────────────────────────────────────────
 * A list, top first. Each row is two buttons — the eye and the name — never a
 * row that is itself a button holding a button (an audit refuses nested
 * controls). The chosen row has aria-current="true". editor.js moves between
 * rows with Up and Down. */
.fz-layers { list-style: none; margin: 0; padding: 0; overflow: auto; min-height: 0; }
.fz-layer {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--fz-space-1); align-items: center;
    margin: 0; padding: 0 var(--fz-space-1); border-bottom: 1px solid var(--fz-line);
}
.fz-layer:last-child { border-bottom: 0; }
.fz-layer[aria-current="true"], .fz-layer[aria-selected="true"] {
    background: var(--fz-raised); box-shadow: inset 3px 0 0 var(--fz-accent);
}
.fz-layer[aria-current="true"] .fz-layer-name, .fz-layer[aria-selected="true"] .fz-layer-name { font-weight: 700; }
.fz-layer-eye, .fz-layer-pick, .fz-layer-act {
    all: unset; box-sizing: border-box; cursor: pointer; min-height: 2.25rem; border-radius: var(--fz-radius-sm);
}
.fz-layer-eye, .fz-layer-act { width: 1.9rem; text-align: center; color: var(--fz-dim); }
.fz-layer-eye[aria-pressed="false"] { color: var(--fz-faint); }
.fz-layer-pick { display: flex; flex-direction: column; justify-content: center; min-width: 0; padding: var(--fz-space-1) var(--fz-space-1); }
.fz-layer-eye:focus-visible, .fz-layer-pick:focus-visible, .fz-layer-act:focus-visible { outline: 2px solid var(--fz-accent); outline-offset: -2px; }
.fz-layer-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fz-text-sm); }
.fz-layer-meta { color: var(--fz-faint); font-size: var(--fz-text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A row with no eye, or the pick alone (history, a style list), lets the name span. */
.fz-layer > .fz-layer-pick:first-child { grid-column: 1 / 3; }
.fz-layer > .fz-layer-pick:only-child { grid-column: 1 / -1; }
/* A list that chooses several (objects) presses each pick: the row reads chosen. */
.fz-layer:has(> .fz-layer-pick[aria-pressed="true"]) { background: var(--fz-raised); box-shadow: inset 3px 0 0 var(--fz-accent); }
.fz-layer:has(> .fz-layer-pick[aria-pressed="true"]) .fz-layer-name { font-weight: 700; }
.fz-layer.fz-layer-off .fz-layer-name { color: var(--fz-faint); text-decoration: line-through; }
.fz-layer-thumb { width: 2rem; height: 1.5rem; object-fit: contain; border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm); }

/* ── segmented, as buttons ────────────────────────────────────────────────
 * controls.css draws .fz-seg as radios (a form) or links (a page). An editor's
 * choice is neither — it acts at once — so its segments are buttons with
 * aria-pressed, the same look. data-fz-single keeps exactly one pressed. */
.fz-seg > button {
    all: unset; box-sizing: border-box; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
    min-height: calc(2.25rem - 6px); min-width: 2rem; padding: 0 var(--fz-space-3);
    border-radius: calc(var(--fz-radius-md) - 2px); color: var(--fz-dim); font-size: var(--fz-text-sm);
}
.fz-seg > button:hover { color: var(--fz-text); }
.fz-seg > button:focus-visible { outline: 2px solid var(--fz-accent); outline-offset: 1px; }
.fz-seg > button[aria-pressed="true"] { background: var(--fz-accent); color: var(--fz-on-accent); font-weight: 700; }
.fz-seg > button:disabled { color: var(--fz-faint); cursor: not-allowed; }

/* ⚠ every piece above sets display, which outranks the hidden attribute — so it
 * is restated here, or an editor that hides its history pane shows it anyway. */
.fz-editor [hidden], .fz-menubar[hidden], .fz-toolbar[hidden], .fz-palette[hidden], .fz-inspector[hidden],
.fz-pane[hidden], .fz-pane-body[hidden], .fz-menu-list[hidden], .fz-layers[hidden], .fz-layer[hidden],
.fz-statusbar[hidden], .fz-iconbtn[hidden] { display: none; }

/* ── a phone ──────────────────────────────────────────────────────────────
 * One column: menus, tools as a sideways row, the stage at most of a screen, the
 * inspector under it. */
@media (max-width: 900px) {
    .fz-editor {
        grid-template-columns: minmax(0, 1fr); grid-template-rows: none;
        grid-template-areas: "menu" "tools" "palette" "stage" "side" "status";
        height: auto; min-height: 0;
    }
    .fz-editor > .fz-stage { height: 60vh; min-height: 18rem; }
    .fz-palette { display: flex; overflow-x: auto; padding-bottom: var(--fz-space-1); justify-items: start; }
    .fz-palette > hr { width: 1px; height: auto; border-top: 0; border-left: 1px solid var(--fz-line); margin: 0 var(--fz-space-1); }
    .fz-swatch-pair { margin: 0 0 0 var(--fz-space-2); flex: 0 0 auto; }
    .fz-toolbar { flex-wrap: nowrap; overflow-x: auto; }
    .fz-menubar-end { max-width: 100%; margin-left: 0; flex-basis: 100%; }
    .fz-menu-list { position: fixed; left: var(--fz-space-2); right: var(--fz-space-2); top: auto; min-width: 0; }
}

/* ── pending: a submit that leaves the page shows it was pressed ───────────
 * 🚨🚨 CHRIS, 2026-09-13: "the allow button on auth callbacks has no user
 * feedback ... users some times press it twice because they thought their first
 * press did nothing." A consent POST is a round trip plus a redirect, and for
 * all of it the page is unchanged — so a press that worked and a press that
 * missed look the same.
 *
 * ⚠ THE ATTRIBUTE IS SET BY pending.js AND NEVER BY A TEMPLATE. It is an
 * attribute rather than a class so that it cannot collide with a service's own
 * .pending, and so a selector can find it without knowing the service.
 *
 * 🚨 WHY NOT :disabled — the honest reason. Disabling the pressed button drops
 * its own name/value from the POST (measured: decision=allow simply vanishes),
 * which on foltzid's consent form reads as a DENIAL. pointer-events does the
 * same job to the person and nothing at all to the form data. */
/* ⚠ THE CONTROLS GO INERT, NOT THE FORM. pointer-events:none on the <form> also
 * suppresses its own cursor, so the page would stop responding AND stop saying
 * why. The form keeps the progress cursor; its controls stop taking clicks. */
form[data-fz-pending] { cursor: progress; }
button[data-fz-pending],
input[data-fz-pending],
a[data-fz-pending] { pointer-events: none; }

/* The pressed control keeps its colour and gains a spinner; the others fade.
 * ⭐ COLOUR IS NOT THE SIGNAL — the glyph is, and the swapped label when a
 * template supplies one. A greyscale screenshot still reads correctly. */
button[data-fz-pending], input[data-fz-pending] {
    position: relative;
    opacity: .72;
}
button[data-fz-pending][aria-busy="true"],
input[data-fz-pending][aria-busy="true"] { opacity: 1; }

button[data-fz-pending][aria-busy="true"]::after {
    content: "";
    display: inline-block;
    width: .72em; height: .72em;
    margin-left: .5em;
    vertical-align: -.08em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: fz-spin .7s linear infinite;
}

@keyframes fz-spin { to { transform: rotate(360deg); } }

/* 🚨 THE GUARD IS NOT OPTIONAL — base.css:77 already says so for the fleet.
 * Without motion the spinner still has to SAY something, so it becomes a static
 * ring rather than nothing at all. */
@media (prefers-reduced-motion: reduce) {
    button[data-fz-pending][aria-busy="true"]::after,
    input[data-fz-pending][aria-busy="true"]::after {
        animation: none;
        border-right-color: currentColor;
        border-top-color: transparent;
    }
}
