/* 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; }
/* ⭐ A FULL-BLEED VIEW SAYS SO (job_251824af6895d6d7): a map, a canvas or an
 * editor fills the screen, so it drops the reading column rather than living in
 * a 46rem strip (map's design review, 2026-10-28: a map 330px wide on a 1280
 * screen). Declared, so a page that runs edge to edge is never an accident. */
.fz-wrap.fz-bleed, .fz-wrap[data-fz-bleed] { max-width: none; }

/* ── 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;
}
/* ⭐⭐ THE DESIGN PASS, 2026-10-28 (job_251824af6895d6d7). Chris: "I was not
 * looking for broken, I was looking for design." This bar was a row of underlined
 * links joined by middle dots under a letter-spaced capital service name — three
 * of the tells the rendered check counts, drawn by the library on 19 services.
 * Now it is the kit's top bar in miniature: the service named in sentence case
 * like a brand, the links as quiet chips with no underline and no separators,
 * and the current page a filled chip (the idea from Primer's underline nav and
 * GOV.UK's service navigation: the place you are is shown by shape, not glyphs).
 *
 * ⚠ ONE ROW THAT SCROLLS SIDEWAYS ON A PHONE, never a second line: meal's seven
 * links wrapped to two lines at 360px and pushed the page about (design review,
 * 2026-10-28). The kit's .fz-topnav already worked this way. */
.fz-nav { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; gap: var(--fz-space-1); }
.fz-nav::-webkit-scrollbar { display: none; }
.fz-nav > * { flex: none; }
.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); text-decoration: none; white-space: nowrap; border-radius: var(--fz-radius-pill); padding: 0 .75rem; }
.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; white-space: nowrap;
    border-radius: var(--fz-radius-pill); padding: 0 .75rem;
    background: var(--fz-raised); box-shadow: inset 0 0 0 1px var(--fz-line);
}
/* ⚠ SENTENCE CASE, NOT TRACKED CAPITALS. "FOLTZMEAL" at .09em is read letter by
 * letter; the name is set like a brand instead — the display face, heavy, tight. */
.fz-nav .fz-svc {
    font: 800 var(--fz-text-md)/1 var(--fz-font-display); letter-spacing: -.01em;
    color: var(--fz-text); margin-right: var(--fz-space-2); white-space: nowrap;
}

/* ── 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); }
/* ⚠ A NOTE WITH SOMETHING AFTER IT keeps its distance (job_21ce4c0cd6ee7f9a): school
 * put its help text above the button, and the button sat flush on the last line. */
.fz-empty .fz-note:not(:last-child) { margin-bottom: .75rem; }
/* ⚠ CAPPED TO THE MEASURE, SO CENTRED TOO: a capped note in a centred panel otherwise
 * sits left of the button above it on a wide screen. */
.fz-empty .fz-note { margin-inline: auto; }

/* ── 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 {
    /* ⚠ SENTENCE CASE (job_251824af6895d6d7): a column heading is a word to
     * read, and weight plus a rule already set it apart. */
    text-align: left; font-size: var(--fz-text-sm); font-weight: 600;
    color: var(--fz-dim); 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); }

/* ── the measure: a line of text stops at a readable length ───────────────
 * ⭐ job_251824af6895d6d7. The render scan of 2026-10-28 found 30 paragraphs on
 * 21 sites running 700 to 1870px wide at 1920 — mostly foltzui's own .fz-meta
 * intro line, whose small type makes a 46rem column ~61em long. A line is
 * capped by its OWN measure (64ch of its own font), so small type stops sooner.
 * Only foltzui's text classes and a bare <p> directly in a foltzui column are
 * capped; an app's own classed paragraph is the app's to decide, and a centred
 * block (.fz-empty, a state page) is left alone so its text stays centred.
 * ⇒ A view that should run edge to edge (a map, a canvas) opts out with
 * data-fz-bleed on any ancestor, or Page{Bleed: true}. */
:where(p.fz-meta, ul.fz-meta, p.fz-note, blockquote, .fz-item-meta, .fz-media-meta, .fz-lead-meta,
       :where(.fz-main, .fz-wrap, .fz-section, .fz-card, .fz-panel, .fz-item-main, .fz-media-body, .fz-disclose-body, .fz-split) > p:not([class])) {
    max-width: var(--fz-measure);
}
:where([data-fz-bleed], .fz-bleed) :where(p, ul, blockquote, span) { max-width: none; }
.fz-mono { font-family: var(--fz-font-mono); font-variant-numeric: tabular-nums; }
/* .fz-break: one unbroken token (a URL, a path, a sound code) wraps inside its
 * column instead of running past a phone's edge (job_1d7e725f453b8601: a watched
 * page's URL ran 300px past its panel at 390). */
.fz-break { overflow-wrap: anywhere; }

@media (max-width: 34rem) {
    /* 🚨 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 row scrolls; the fade at its end says there is more of it */
    .fz-nav {
        -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 1.5rem), transparent);
        mask-image: linear-gradient(90deg, #000 calc(100% - 1.5rem), transparent);
    }
}

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

/* ⭐ fz-listrow: A DISCLOSURE THAT IS A ROW IN A LIST (job_c3a53db6eb14e3de:
 * /services' seventy services). The summary carries the row's name and its
 * state chips in the text's own colour, and it WRAPS — the plain summary is one
 * inline-flex line, so five chips ran past a phone's edge. The body opens under
 * it with the row's detail.
 *   <details class="fz-panel fz-disclose fz-listrow"><summary><strong>name</strong> <span class="fz-chip">…</span></summary><div class="fz-disclose-body">…</div></details> */
.fz-disclose.fz-listrow > summary {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--fz-space-2);
    color: var(--fz-text); font-size: var(--fz-text-md); padding: 0; min-height: var(--fz-control);
}
.fz-disclose.fz-listrow > summary > strong { overflow-wrap: anywhere; }
.fz-disclose.fz-listrow > .fz-disclose-body { padding-top: var(--fz-space-4); }

/* ── 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;
}
/* A paragraph whose last line holds one word leaves a hole under it. Chrome and
 * Safari re-break such a paragraph under `pretty`; elsewhere it is ignored. A
 * library default rather than a rendered rule (job_01a2fd2d0306aab4). */
:where(p, li, dd, blockquote) { text-wrap: pretty; }
/* A bare <button> is drawn in the browser's own face (Arial on Windows) while the
 * page around it is in --fz-font; foreign-font found it on the first run
 * (job_01a2fd2d0306aab4). The FAMILY only, at zero specificity: its size and box
 * are left alone, so nothing a service already styled moves. */
:where(button, input[type=submit], input[type=button], input[type=reset]) { font-family: inherit; }

: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; }
/* ⭐ A SLIDER'S TARGET IS 24px TALL, NOT THE BROWSER'S 16 (job_a225a83ad833a1a7):
 * the science pages' Volume, Glass and Ball size were each a 16px strip at 390.
 * The track is drawn where it was; only the pressable box grows. */
:where(input[type=range]) { min-height: 1.5rem; }
: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; }
}

/* foltzui buttons (job_bb10d264d896af18).
 *
 * ⭐ ITS OWN FILE SO A SERVICE WITH ITS OWN BASE SHEET CAN TAKE IT. foltzday keeps
 * board.css (pinned dark, 88px targets) and links no foltzui.css; to draw a page
 * header's primary action it needs .fz-btn and nothing else from base.css.
 * PartsCSS() serves this beside the kit's parts; Base() carries it right after
 * base.css, where these rules always sat, so a page on the whole sheet is
 * unchanged. Every selector here is a .fz- class: nothing global. */
/* ── 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); }
/* ⭐ A TOGGLE IS A BUTTON WITH aria-pressed (job_a225a83ad833a1a7): Sweep, Tone,
 * Labels. Pressed reads as the accent outline AND a ✓ before the words, so the
 * state is never colour alone; a screen reader hears "pressed". */
.fz-btn[aria-pressed="true"] { border-color: var(--fz-accent); color: var(--fz-accent); box-shadow: inset 0 0 0 1px var(--fz-accent); }
.fz-btn[aria-pressed="true"]::before { content: "✓\00a0"; }
/* ⚠ A LINK DRAWN AS A BUTTON IS A BUTTON: no underline, its words centred.
 * The page header's primary action is usually a link (job_251824af6895d6d7). */
a.fz-btn, label.fz-btn { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }
/* A label drawn as a button (it opens a file input, snes and gridray, job_e3356d3ffae1e996) is centred and pressable the same way. */
label.fz-btn { cursor: pointer; margin: 0; }
/* ⚠ SMALL IS STILL 44px TALL. It is smaller in TYPE, never in target. */
.fz-btn.fz-sm { font-size: .8rem; padding: .3rem .7rem; }

@media (max-width: 34rem) {
    .fz-btn { padding: .55rem .9rem; }
}

/* 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: 600 .7rem/1 var(--fz-font); 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-sm); font-weight: 600;
    /* ⚠ --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, .fz-shell label.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; }
/* ── the bar's one menu: the pages that are not the family's (BarMenu) ────
 * ⭐ NAVIGATION IS NOT A SITE MAP (GOV.UK's service navigation, the idea only).
 * Upload, apps and the admin's views keep a door, here, out of the nav row a
 * family member reads (job_1357cabc646f738e, job_913e020ed58be638). It opens
 * DOWN from the bar and is not inside .fz-topnav, whose sideways scroll would
 * clip it. */
.fz-barmenu { position: relative; }
.fz-barlink {
    display: inline-flex; align-items: center; min-height: 2.5rem; padding: 0 .8rem;
    border-radius: var(--fz-radius-pill); color: var(--fz-dim); font-weight: 600;
    text-decoration: none; white-space: nowrap;
}
.fz-barlink:hover { color: var(--fz-text); background: var(--fz-raised); }
.fz-barlink.fz-here { color: var(--fz-text); background: var(--fz-raised); box-shadow: inset 0 0 0 1px var(--fz-line); }
.fz-barmenu > summary {
    list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: .35em;
    min-height: 2.5rem; padding: 0 .8rem; border-radius: var(--fz-radius-pill);
    color: var(--fz-dim); font-weight: 600; white-space: nowrap;
}
.fz-barmenu > summary::-webkit-details-marker { display: none; }
.fz-barmenu-label { max-width: 9rem; overflow: hidden; text-overflow: ellipsis; }
@media (min-width: 40rem) { .fz-barmenu-label { max-width: 16rem; } }
.fz-barmenu > summary::after { content: "▾"; font-size: .8em; }
.fz-barmenu > summary:hover, .fz-barmenu[open] > summary,
.fz-barmenu.fz-is-here > summary { color: var(--fz-text); background: var(--fz-raised); }
.fz-barmenu.fz-is-here > summary { box-shadow: inset 0 0 0 1px var(--fz-line); }
.fz-barmenu > summary:focus-visible { outline: var(--fz-focus) solid var(--fz-accent); outline-offset: 2px; }
.fz-barmenu-list {
    position: absolute; right: 0; top: calc(100% + 4px); z-index: 40;
    min-width: 12rem; max-width: min(20rem, 90vw); padding: var(--fz-space-1);
    background: var(--fz-raised); border: 1px solid var(--fz-line);
    border-radius: var(--fz-radius-md); box-shadow: var(--fz-shadow-2);
    display: grid; gap: 2px;
}
.fz-barmenu-list a, .fz-barmenu-list .fz-here {
    display: flex; align-items: center; min-height: 2.75rem; padding: 0 .8rem;
    border-radius: var(--fz-radius-sm); color: var(--fz-text); text-decoration: none; white-space: nowrap;
}
.fz-barmenu-list a:hover { background: var(--fz-surface); }
.fz-barmenu-list .fz-here { font-weight: 700; background: var(--fz-surface); }
@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; }
/* align-content: start — a stack in a stretched box (a card in a .fz-grid row
 * beside a taller one) keeps its gap instead of spreading the spare height
 * between its children (job_4388c4824606cdb7: /fleet's cards opened 100px gaps
 * at 1280). */
.fz-stack { display: grid; gap: var(--fz-space-4); align-content: start; }
/* ⭐ A SCROLL BOX THAT BELONGS TO THE PAGE (job_55631d83e7a2ffb8). A box with its
 * own overflow draws the BROWSER's scrollbar, which on Windows is a light bar on
 * this dark ground: a white stripe down a mail list. .fz-scroll gives it a thin
 * bar in the page's own line colour, in the light set too, because it reads the
 * tokens. The overflow itself stays the page's choice. */
.fz-scroll { scrollbar-width: thin; scrollbar-color: var(--fz-line) transparent; }
.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; }
}
/* ⭐ A SCENE: A PICTURE WITH ITS CONTROLS BESIDE IT (job_a225a83ad833a1a7). The
 * orrery, the Chladni plate, the cymatics drop and the voids page each drew their
 * canvas and put the knobs underneath, out of view at 1280 — so turning a knob
 * and watching what it did took a scroll each way. .fz-split.fz-scene keeps the
 * canvas and a 22rem aside of controls in one view from 64rem up; the aside
 * scrolls on its own if it is longer than the screen. Below 64rem the controls
 * follow the picture, as .fz-split does. .fz-scene-view is the dark frame the
 * canvas sits in (--fz-viewer-bg, the light set included), .fz-scene-overlay a
 * caption laid on it, and .fz-scene-readout the one big number a scene reads
 * out (the frequency, the date). */
.fz-scene-view {
    position: relative; display: flex; justify-content: center; align-items: center;
    overflow: hidden; min-width: 0;
    background: var(--fz-viewer-bg); color: var(--fz-viewer-text);
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg);
}
.fz-scene-view > canvas { display: block; max-width: 100%; touch-action: none; user-select: none; }
.fz-scene-overlay {
    position: absolute; top: var(--fz-space-2); left: var(--fz-space-3); right: var(--fz-space-3);
    margin: 0; pointer-events: none; font-size: var(--fz-text-sm); color: var(--fz-viewer-text);
}
/* .fz-scene-cover dims the whole picture behind one centred message — "Click to
 * play" over a game that has not taken the mouse yet. It never takes a click:
 * the press goes through to the canvas under it. */
.fz-scene-cover {
    position: absolute; inset: 0; display: grid; place-content: center; gap: var(--fz-space-2);
    padding: var(--fz-space-4); text-align: center; pointer-events: none;
    background: color-mix(in srgb, var(--fz-viewer-bg) 70%, transparent); color: var(--fz-viewer-text);
}
.fz-scene-cover > * { margin: 0; }
.fz-scene-readout { margin: 0; font: 500 var(--fz-text-2xl)/1.1 var(--fz-font-mono); }
@media (min-width: 64rem) {
    .fz-split.fz-scene { grid-template-columns: minmax(0, 1fr) 22rem; }
    .fz-split.fz-scene > aside { max-height: calc(100vh - 6rem); overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--fz-line) transparent; }
}
.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); }
/* Large print (job_a8102c090d388470): reading text a step up, roomier lines, and
   everything inside it in proportion — for a reader who asked for big type. */
.fz-large-print { font-size: 1.25rem; line-height: 1.6; }
.fz-large-print h2 { font-size: 1.6rem; line-height: 1.3; }
.fz-large-print h3 { font-size: 1.4rem; line-height: 1.3; }
.fz-large-print a { text-underline-offset: .2em; }
.fz-large-print li { margin: var(--fz-space-2) 0; }
.fz-large-print .fz-panel > h2 { font-size: 1.5rem; line-height: 1.3; }
.fz-large-print .fz-panel > h3 { font-size: 1.25rem; }
.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; overflow-wrap: anywhere; }   /* a file name with no spaces wraps instead of running under the edge (torr at 390, job_c8e3c4e8f22c6bed) */
.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); }

/* ── lead and quiet tiles (job_97414029c7b4acae, foltzcloud.com) ──────────
   A launcher is not a site map: the few doors this person opens most lead,
   raised and larger; everything else sits below them, quiet. Same links. */
.fz-tiles-lead { grid-template-columns: repeat(auto-fill, minmax(min(100%, 11rem), 1fr)); }
@media (max-width: 40rem) { .fz-tiles-lead { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.fz-tiles-lead .fz-tile { padding: var(--fz-space-3) var(--fz-space-4); background: var(--fz-raised); box-shadow: var(--fz-shadow-1); }
.fz-tiles-lead .fz-tile-name { font-size: var(--fz-text-xl); }
.fz-tiles-quiet .fz-tile { border-color: transparent; background: transparent; padding: var(--fz-space-2) var(--fz-space-3); }
.fz-tiles-quiet .fz-tile:hover { background: var(--fz-surface); border-color: var(--fz-line); }
.fz-tiles-quiet .fz-tile-name { font-size: var(--fz-text-md); font-weight: 700; }
.fz-tiles-quiet .fz-tile-what { color: var(--fz-faint); }

/* ── a keyboard hint: "press / to jump" means nothing on a phone ──────────
   Anything that names a key is wrapped in .fz-keyhint and is not drawn on a
   touch screen (job_97414029c7b4acae). */
@media (hover: none), (pointer: coarse) { .fz-keyhint { display: none !important; } }

/* ── a word-chip: status as a WORD, snowbrush's .sb-fix-state, shared ──────
 * ⚠ IN THE WORDS AS WRITTEN, NOT TRACKED CAPITALS (job_251824af6895d6d7): the
 * render scan found "⚠ NOT READ YET" and "✓ ON" on 25 chips across the fleet,
 * read letter by letter. A chip is set apart by its shape, weight and colour;
 * the type stays a word. */
.fz-chip {
    display: inline-flex; align-items: center; gap: .3em;
    font: 600 var(--fz-text-sm)/1.1 var(--fz-font-display);
    padding: .3rem .55rem; border-radius: var(--fz-radius-pill);
    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 field with its own button: an input that grows and a "Choose…" or
 * "Copy" that keeps its size (job_b6203dd6e60601f6: content's source video and
 * footage folder). The input never pushes the button past a phone's edge.
 *   <div class="fz-inputrow"><input id="src"><button type="button" class="fz-btn">Choose…</button></div> */
.fz-inputrow { display: flex; gap: var(--fz-space-2); align-items: stretch; min-width: 0; }
.fz-inputrow > input, .fz-inputrow > select { flex: 1 1 auto; min-width: 0; width: auto; }
.fz-inputrow > .fz-btn { flex: none; }

/* ── a log or a config file, shown as written (job_4388c4824606cdb7: a
 * service's journal tail on /fleet, a WireGuard peer's .conf on /services).
 * Monospace, its own scroll, and a long line WRAPS rather than pushing the
 * page sideways at 390. Read-only text, never a terminal.
 *   <pre class="fz-log">…</pre> */
.fz-log {
    margin: 0; max-height: 28rem; overflow: auto;
    padding: var(--fz-space-3); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-md);
    background: var(--fz-raised); color: var(--fz-dim);
    font: var(--fz-text-sm)/1.5 var(--fz-font-mono);
    white-space: pre-wrap; overflow-wrap: anywhere;
}

/* ── a row of chips you PRESS: pick a kind, roll one (job_ff8eabb09238cd13) ──
 * .fz-chip above is a status word; .fz-chips holds chips that are buttons or
 * links — foley's sound categories, a filter row. Same shape and words as a
 * status chip, but a full control height (never under 24px, so a thumb hits it
 * at 390px), the text's own colour, and aria-pressed for the one that is on.
 *   <div class="fz-chips" role="group" aria-label="Sound"><button type="button" class="fz-chip">shoot</button>…</div> */
.fz-chips { display: flex; flex-wrap: wrap; gap: var(--fz-space-2); }
.fz-chips > .fz-chip { min-height: var(--fz-control); padding: 0 .95rem; font-size: var(--fz-text-md); color: var(--fz-text); cursor: pointer; text-decoration: none; }
.fz-chips > .fz-chip:hover { border-color: var(--fz-accent); }
.fz-chips > .fz-chip:focus-visible { outline: var(--fz-focus) solid var(--fz-accent); outline-offset: 2px; }
.fz-chips > .fz-chip[aria-pressed="true"], .fz-chips > .fz-chip[aria-current] { background: var(--fz-accent); border-color: var(--fz-accent); color: var(--fz-on-accent); }
.fz-chips > .fz-chip:disabled { color: var(--fz-faint); cursor: not-allowed; }

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

/* ── a detail line: facts on one quiet line, each its own item ─────────────
 * (job_251824af6895d6d7) DetailLine / MetaLine. Space and a hairline separate
 * the facts — never a middle dot — and a label, when there is one, is quieter
 * than its value. Inherits size and colour from where it sits (.fz-meta,
 * .fz-item-meta), so it changes the separators and nothing else around it. */
/* ⚠ THE HAIRLINE MUST NOT START A WRAPPED LINE. Each item carries its rule on
 * the left and is pulled left by the rule plus its padding, and the line clips
 * what sticks out — so the first fact on EVERY line, not only the first one,
 * starts clean. (A :first-child rule only fixed line one; seen at 390px.) */
.fz-details { display: inline-flex; flex-wrap: wrap; align-items: baseline; row-gap: .15rem; max-width: 100%; overflow: hidden; vertical-align: bottom; }
.fz-detail {
    display: inline-block;
    margin-left: calc(-1px - var(--fz-space-2));
    padding: 0 var(--fz-space-4) 0 var(--fz-space-2);
    border-left: 1px solid var(--fz-line);
}
.fz-detail:last-child { padding-right: 0; }
.fz-detail-k { color: var(--fz-faint); }
.fz-meter-word { font-weight: 600; margin-left: var(--fz-space-1); }

/* ── a summary list: key, value, and its one action per row ──────────────
 * (job_251824af6895d6d7) Summary(rows). GOV.UK's summary-list idea, our own
 * numbers: a hairline per row, the key a third of the width on a desk, the
 * action a quiet word at the row's end. Stacked on a phone. */
.fz-summary { margin: 0 0 var(--fz-space-6); border-top: 1px solid var(--fz-line); }
.fz-summary-row { display: grid; gap: var(--fz-space-1) var(--fz-space-5); padding: var(--fz-space-3) 0; border-bottom: 1px solid var(--fz-line); }
.fz-summary dt { font-weight: 700; color: var(--fz-text); }
.fz-summary dd { margin: 0; overflow-wrap: anywhere; }
.fz-summary-act { font-size: var(--fz-text-sm); }
.fz-summary-none { color: var(--fz-faint); }
@media (min-width: 40rem) {
    .fz-summary-row { grid-template-columns: minmax(8rem, 30%) minmax(0, 1fr) auto; align-items: baseline; }
    .fz-summary-row.fz-no-action { grid-template-columns: minmax(8rem, 30%) minmax(0, 1fr); }
    .fz-summary-act { justify-self: end; text-align: right; }
}

/* the page header's action slot holds Action forms as well as links */
.fz-action-form { display: contents; }

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

/* ── a line icon (Icon(name), job_bb10d264d896af18): the text's size and colour,
 * so it takes a nav item's state like the label beside it. */
.fz-icon { width: 1.25em; height: 1.25em; flex: none; vertical-align: -.25em; }

/* 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 DATES — a month, an agenda, a timeline (job_9544e790bd1475a5).
 *
 * Month     <div class="fz-cal"> bar + <table class="fz-cal-grid">; each <td class="fz-cal-day">
 *           holds .fz-cal-num, .fz-cal-dots (a phone) and .fz-cal-evs (wider)
 * Agenda    <div class="fz-agenda"> of <section class="fz-agenda-day" id="fz-day-…">,
 *           an <h3 class="fz-agenda-date"> and a .fz-list; .fz-agenda-time leads a row
 * Timeline  <ol class="fz-timeline"> of <li class="fz-tl-step"> with .fz-tl-mark on
 *           the rail and .fz-tl-body: .fz-tl-when, .fz-tl-title, .fz-tl-note
 *
 * ⭐ ON A PHONE THE MONTH SHOWS DOTS, NOT WORDS. Seven columns at 390px are 50px
 * each; a title cut to "Wor…" says less than a dot, and the agenda under the
 * month carries the words. The day's link names how many things it has. */

.fz-cal { margin: 0 0 var(--fz-space-5); }
.fz-cal-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--fz-space-2); margin: 0 0 var(--fz-space-2); }
.fz-cal-title { margin: 0; font: var(--fz-weight-heading) var(--fz-text-xl)/var(--fz-leading-tight) var(--fz-font-display); text-align: center; flex: 1 1 auto; }
.fz-cal-step { flex: none; min-width: var(--fz-control); justify-content: center; }
.fz-cal-grid { width: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 0; border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg); background: var(--fz-surface); overflow: hidden; }
.fz-cal-grid th { padding: var(--fz-space-2) 0; font: 700 var(--fz-text-xs)/1 var(--fz-font-display);  color: var(--fz-dim); text-align: center; border-bottom: 1px solid var(--fz-line); }
.fz-cal-grid abbr { text-decoration: none; }
.fz-cal-day { vertical-align: top; height: 6.5rem; padding: var(--fz-space-1); border-top: 1px solid var(--fz-line); border-left: 1px solid var(--fz-line); }
.fz-cal-grid tr:first-child .fz-cal-day { border-top: 0; }
.fz-cal-day:first-child { border-left: 0; }
.fz-cal-day.fz-is-out { background: var(--fz-bg); }
.fz-cal-day.fz-is-out .fz-cal-num { color: var(--fz-faint); }
.fz-cal-day.fz-is-today { box-shadow: inset 0 0 0 2px var(--fz-accent); }
.fz-cal-num { display: inline-flex; align-items: center; justify-content: center; min-width: 1.9rem; height: 1.9rem; border-radius: var(--fz-radius-pill); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--fz-text); text-decoration: none; }
a.fz-cal-num:hover { background: var(--fz-raised); }
.fz-cal-day.fz-is-today .fz-cal-num { background: var(--fz-accent); color: var(--fz-on-accent); }
.fz-cal-day.fz-is-past:not(.fz-is-today) .fz-cal-num { color: var(--fz-dim); }
.fz-cal-dots { display: none; justify-content: center; gap: 3px; margin-top: 2px; }
.fz-cal-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--fz-accent); }
.fz-cal-evs { list-style: none; margin: var(--fz-space-1) 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; font-size: var(--fz-text-xs); line-height: 1.3; }
.fz-cal-ev { padding: 2px 4px; border-radius: var(--fz-radius-sm); background: var(--fz-raised); border-left: 3px solid var(--fz-accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fz-cal-ev a { color: var(--fz-text); text-decoration: none; }
.fz-cal-ev a:hover { text-decoration: underline; }
.fz-cal-ev.fz-is-ok   { border-left-color: var(--fz-ok); }
.fz-cal-ev.fz-is-warn { border-left-color: var(--fz-warn); }
.fz-cal-ev.fz-is-bad  { border-left-color: var(--fz-bad); }
.fz-cal-time { color: var(--fz-dim); font-variant-numeric: tabular-nums; }
.fz-cal-more { padding: 0 4px; color: var(--fz-dim); }
@media (max-width: 40rem) {
    .fz-cal-day { height: 3.4rem; padding: 2px; text-align: center; }
    .fz-cal-evs { display: none; }
    .fz-cal-dots { display: flex; }
    .fz-cal-num { min-width: 2.25rem; height: 2.25rem; }   /* a fingertip, not the 1.9rem a mouse needs */
}

.fz-agenda { display: flex; flex-direction: column; gap: var(--fz-space-5); margin: 0 0 var(--fz-space-5); }
.fz-agenda-day { scroll-margin-top: var(--fz-space-7); }
.fz-agenda-date { display: flex; align-items: center; gap: var(--fz-space-2); margin: 0 0 var(--fz-space-2); font: var(--fz-weight-heading) var(--fz-text-md)/var(--fz-leading-tight) var(--fz-font-display); }
.fz-agenda-day.fz-is-past .fz-agenda-date { color: var(--fz-dim); }
.fz-agenda-day.fz-is-today .fz-agenda-date { color: var(--fz-accent); }
.fz-agenda-day:target .fz-list { box-shadow: 0 0 0 var(--fz-focus) var(--fz-accent); }
.fz-agenda-time { flex: none; width: 4.6rem; color: var(--fz-dim); font-size: var(--fz-text-sm); font-variant-numeric: tabular-nums; }
@media (max-width: 40rem) { .fz-agenda-time { width: 3.6rem; } }

.fz-timeline { list-style: none; margin: 0 0 var(--fz-space-5); padding: 0; position: relative; }
.fz-tl-step { position: relative; display: grid; grid-template-columns: 1.6rem minmax(0, 1fr); gap: var(--fz-space-3); padding: 0 0 var(--fz-space-5); }
.fz-tl-step::before { content: ""; position: absolute; left: calc(.8rem - 1px); top: 1.1rem; bottom: 0; width: 2px; background: var(--fz-line); }
.fz-tl-step:last-child { padding-bottom: 0; }
.fz-tl-step:last-child::before { display: none; }
.fz-tl-step.fz-is-done::before { background: var(--fz-ok); }
.fz-tl-mark { position: relative; z-index: 1; width: 1rem; height: 1rem; margin: .2rem 0 0 .3rem; border-radius: 50%; border: 2px solid var(--fz-dim); background: var(--fz-bg); }
.fz-tl-step.fz-is-done .fz-tl-mark { border-color: var(--fz-ok); background: var(--fz-ok); }
.fz-tl-step.fz-is-now .fz-tl-mark { border-color: var(--fz-accent); background: var(--fz-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--fz-accent) 30%, transparent); }
.fz-tl-body { min-width: 0; }
.fz-tl-when { display: block; color: var(--fz-dim); font-size: var(--fz-text-sm); font-variant-numeric: tabular-nums; }
.fz-tl-title { margin: .1rem 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--fz-space-1) var(--fz-space-2); overflow-wrap: anywhere; }
.fz-tl-step.fz-is-now .fz-tl-title { font-size: var(--fz-text-lg); }
.fz-tl-note { margin: .2rem 0 0; color: var(--fz-dim); max-width: var(--fz-measure); }

/* ── StepStrip (job_2b557c3347bd2d45): how far one thing has got, on one line.
   The compact sibling of the timeline above; wraps at 390px, never scrolls. ── */
.fz-strip { list-style: none; margin: var(--fz-space-1) 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--fz-space-1); }
.fz-strip-step { display: inline-flex; align-items: baseline; gap: var(--fz-space-1); padding: 2px var(--fz-space-2); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-md); color: var(--fz-dim); font-size: var(--fz-text-sm); min-width: 0; }
.fz-strip-mark { font-weight: var(--fz-weight-heading); }
.fz-strip-note { color: var(--fz-dim); font-size: var(--fz-text-xs); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.fz-strip-step.fz-is-done { color: var(--fz-ok); border-color: currentColor; }
.fz-strip-step.fz-is-now { color: var(--fz-accent); border-color: currentColor; box-shadow: 0 0 0 2px color-mix(in srgb, var(--fz-accent) 25%, transparent); }
.fz-strip-step.fz-is-bad { color: var(--fz-bad); border-color: currentColor; }
.fz-strip-step.fz-is-unknown { color: var(--fz-warn); border-style: dashed; border-color: currentColor; }
.fz-strip-step.fz-is-done .fz-strip-name, .fz-strip-step.fz-is-now .fz-strip-name, .fz-strip-step.fz-is-bad .fz-strip-name { color: var(--fz-text); }

/* 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) */
.fz-lead-next { margin: 0 0 .15rem; flex-basis: 100%; }
.fz-lead-go { font-weight: 700; color: var(--fz-text); text-decoration-color: var(--fz-accent); text-decoration-thickness: 2px; text-underline-offset: 4px; }
.fz-lead-go::after { content: " →"; }

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

/* THE MORE MENU (job_d8093d50562dbda3): the actions a card or row offers that are
 * not opening it (duplicate, delete, archive), behind one "⋯" so they are not on
 * every card's face. A <details>, so it opens with no script; MoreMenu writes it.
 * In a media card it sits at the end of the foot. ⚠ The card clips its picture
 * (overflow: hidden), so a card with an open menu stops clipping and comes forward,
 * or the menu would be cut off by its own card and hidden under the next one. */
.fz-more { position: relative; flex: none; margin-left: auto; }
.fz-more > summary {
    list-style: none; cursor: pointer; display: grid; place-items: center;
    min-width: var(--fz-control); min-height: var(--fz-control); border-radius: var(--fz-radius-md);
    color: var(--fz-dim); font-size: 1.2rem; line-height: 1;
}
.fz-more > summary::-webkit-details-marker { display: none; }
.fz-more > summary:hover, .fz-more[open] > summary { color: var(--fz-text); background: var(--fz-raised); }
.fz-more > summary:focus-visible { outline: var(--fz-focus) solid var(--fz-accent); outline-offset: 2px; }
.fz-more-menu {
    position: absolute; right: 0; bottom: calc(100% + 2px); z-index: 5;
    width: max-content; min-width: 10rem; max-width: min(18rem, 80vw); padding: var(--fz-space-1);
    background: var(--fz-raised); border: 1px solid var(--fz-line);
    border-radius: var(--fz-radius-md); box-shadow: var(--fz-shadow-2);
    display: grid; gap: 2px;
}
.fz-more-menu > form { margin: 0; display: grid; }
.fz-more-menu .fz-btn { width: 100%; justify-content: flex-start; white-space: normal; text-align: left; }
.fz-media:has(.fz-more[open]) { overflow: visible; z-index: 4; }
/* ⚠ IN A CARD THE MENU OPENS ACROSS THE CARD, NOT FROM THE ⋯: on a phone's two-
 * across grid a menu hung off the left card's ⋯ ran off the screen's left edge. */
.fz-media-foot > .fz-more { position: static; }
.fz-media-foot > .fz-more > summary { position: relative; z-index: 1; }
.fz-media .fz-more-menu { left: var(--fz-space-2); right: var(--fz-space-2); bottom: calc(var(--fz-control) + var(--fz-space-4) + 2px); width: auto; min-width: 0; max-width: none; }

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

/* SOCIAL POST PREVIEW (Post, job_62d9df1b4580a70f): a post the way a feed draws
 * it, so the person sees where the fold falls before they paste it. The fold is
 * a native <details>, so it opens with no script and from the keyboard. */
.fz-post { max-width: 34rem; background: var(--fz-surface); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg); padding: var(--fz-space-4); }
.fz-post-head { display: flex; align-items: center; gap: var(--fz-space-3); margin-bottom: var(--fz-space-3); }
.fz-post-mark { flex: none; display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; border-radius: 50%; background: var(--fz-raised); color: var(--fz-text); font: 700 var(--fz-text-md)/1 var(--fz-font-display); }
.fz-post-who { display: flex; flex-direction: column; min-width: 0; }
.fz-post-name { font-weight: 700; }
.fz-post-line { color: var(--fz-dim); font-size: var(--fz-text-sm); font-weight: 400; }
.fz-post-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.45; }
.fz-post-rest > summary { cursor: pointer; color: var(--fz-dim); list-style: none; margin-top: var(--fz-space-1); }
.fz-post-rest > summary::-webkit-details-marker { display: none; }
.fz-post-rest[open] > summary { display: none; }
.fz-post-fold { display: block; margin-top: var(--fz-space-1); padding-top: var(--fz-space-1); border-top: 1px dashed var(--fz-warn); color: var(--fz-faint); font-size: var(--fz-text-xs); }
.fz-post-pic { margin: var(--fz-space-3) 0 0; }
.fz-post-pic img { display: block; max-width: 100%; height: auto; border-radius: var(--fz-radius); }
.fz-post-pic figcaption, .fz-post-file { color: var(--fz-dim); font-size: var(--fz-text-sm); }
/* A DOCUMENT POST (Pages, job_39e399de09401ebe): the pages of an uploaded PDF
 * in a strip that swipes, each at its own shape (a 4:5 carousel page stays 4:5),
 * numbered the way a feed numbers them. Native overflow scroll, no script. */
.fz-post-pages { display: flex; gap: var(--fz-space-2); margin: 0; padding: 0 0 var(--fz-space-2); list-style: none; overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain; }
.fz-post-page { position: relative; flex: 0 0 min(80%, 16rem); scroll-snap-align: start; }
.fz-post-page:focus-visible { outline: 2px solid var(--fz-accent); outline-offset: 2px; border-radius: var(--fz-radius); }
.fz-post-page img { display: block; width: 100%; height: auto; border: 1px solid var(--fz-line); border-radius: var(--fz-radius); }
.fz-post-pageno { position: absolute; right: var(--fz-space-2); bottom: var(--fz-space-2); padding: 0 var(--fz-space-2); border-radius: var(--fz-radius); background: var(--fz-surface); color: var(--fz-dim); font-size: var(--fz-text-xs); font-variant-numeric: tabular-nums; }
.fz-post-comment { margin-top: var(--fz-space-3); padding: var(--fz-space-3); background: var(--fz-raised); border-radius: var(--fz-radius); }
.fz-post-comment .fz-post-name { display: block; margin-bottom: var(--fz-space-1); font-size: var(--fz-text-sm); }

/* SUMMARY CARDS (job_5f86e77eb1e10f50): a picture, the title, ONE figure set
 * large, and a short summary list of the facts the decision turns on. One
 * across on a phone, as many as keep key beside value on a desk. Inside a card
 * the summary keeps its key beside its value at every width, smaller and
 * tighter than a page's. No picture is a short band that says so in words. */
.fz-media-grid.fz-is-summaries { grid-template-columns: repeat(auto-fill, minmax(min(100%, 19rem), 1fr)); gap: var(--fz-space-4); }
@media (max-width: 40rem) { .fz-media-grid.fz-is-summaries { grid-template-columns: 1fr; gap: var(--fz-space-3); } }
.fz-media-pic.fz-is-none { aspect-ratio: auto; height: 3.25rem; }
.fz-media-slot.fz-has-note { gap: var(--fz-space-2); font-size: 1.25rem; }
.fz-media-slot-note { font-size: var(--fz-text-sm); color: var(--fz-dim); }
.fz-media-body > .fz-media-figure { margin: var(--fz-space-1) 0 0; font: 800 var(--fz-text-2xl)/1.1 var(--fz-font-display); font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.fz-media-figure-note { font: 400 var(--fz-text-sm)/1.4 var(--fz-font); color: var(--fz-dim); letter-spacing: 0; }
.fz-media.fz-is-summary .fz-summary { margin: var(--fz-space-2) 0 0; font-size: var(--fz-text-sm); }
.fz-media.fz-is-summary .fz-summary-row { grid-template-columns: minmax(5.5rem, 34%) minmax(0, 1fr); gap: 0 var(--fz-space-3); padding: var(--fz-space-2) 0; align-items: start; }
.fz-media.fz-is-summary .fz-summary dt { font-weight: 600; color: var(--fz-dim); }

/* A PICTURE (Picture): the one big photo a detail page leads with, framed
 * before it loads; none is the same short band a summary card draws. */
.fz-picture { margin: 0 0 var(--fz-space-5); overflow: hidden; border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg); background: var(--fz-raised); }
.fz-picture img { display: block; width: 100%; aspect-ratio: 3 / 2; max-height: 30rem; object-fit: cover; }
.fz-picture .fz-media-slot { min-height: 4.5rem; }
.fz-picture-credit { padding: var(--fz-space-1) var(--fz-space-3); color: var(--fz-dim); font-size: var(--fz-text-sm); }   /* whose picture it is (foltznews: the outlet's) */

/* THE LIVE SCREEN (job_3d8eeed9d42668af): another machine's screen as live
 * video, as foltzdesk shows the laptop on a phone. The frame holds 16:9 until
 * the stream says its own size (the page sets aspect-ratio from it), the picture
 * is letterboxed, never cropped, and the status line under it says in words what
 * the picture is doing: connecting, paused for a secure screen, ended. */
.fz-screen { display: flex; flex-direction: column; gap: var(--fz-space-2); margin: 0; }
.fz-screen-frame {
    position: relative; aspect-ratio: 16 / 9; width: 100%; overflow: hidden;
    background: var(--fz-raised); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-lg);
}
.fz-screen-frame video { display: block; width: 100%; height: 100%; object-fit: contain; }
.fz-screen-status { margin: 0; color: var(--fz-dim); font-size: var(--fz-text-sm); }

/* THE SCREEN REMOTE (job_dd1d84403af334e3): the phone's hands on the machine it
 * is watching. .fz-screen-touch makes the frame a surface the page reads touches
 * from — the browser's own pan and zoom are off there, because a slide is a
 * pointer move, not a page scroll; the page zooms the picture itself (pinch)
 * with a transform from the top-left. .fz-remote holds a .fz-remote-bar (a
 * .fz-seg of modes and a keyboard button) and a .fz-keyrow of .fz-iconbtn.fz-wide
 * keys — a held modifier is aria-pressed, so it reads as held by colour, weight
 * AND state. .fz-keycatch is the off-screen field that brings up the phone's
 * keyboard: focusable, never seen, 16px so iOS does not zoom when it focuses. */
.fz-screen-touch { touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.fz-screen-touch video { transform-origin: 0 0; }
.fz-remote { display: flex; flex-direction: column; gap: var(--fz-space-2); position: relative; }
.fz-remote-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fz-space-2); }
.fz-keyrow { display: flex; flex-wrap: wrap; gap: var(--fz-space-1); }
.fz-keyrow .fz-iconbtn { min-width: 2.75rem; }
.fz-keycatch {
    position: absolute; left: 0; bottom: 0; width: 1px; height: 1px; padding: 0; border: 0;
    opacity: 0; font-size: 16px; resize: none; overflow: hidden;
}
.fz-remote-status { margin: 0; min-height: 1.2em; color: var(--fz-dim); font-size: var(--fz-text-sm); }

/* FILES AND THE CLIPBOARD ACROSS (job_0b61a672558b4a0f): what a phone sends to
 * and takes from the machine it watches. .fz-transfers lists .fz-transfer rows —
 * the name, a meta line that says in WORDS how far it has got and which way it
 * goes, and a .fz-transfer-bar whose .fz-transfer-fill the page widens; a done
 * row is .fz-is-ok and a failed one .fz-is-bad, each with its word too.
 * .fz-picker is a folder picker over the folders the other machine shares: a
 * .fz-picker-crumbs row of buttons back up the path, then .fz-picker-item
 * buttons (.fz-picker-name, .fz-picker-meta) — a folder opens, a file is taken.
 * .fz-clip shows a clipboard that came across: .fz-clip-text keeps the text's
 * own lines, .fz-clip-img the picture, never wider than the column. */
.fz-transfers { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--fz-space-2); }
.fz-transfer {
    display: grid; gap: var(--fz-space-1); padding: var(--fz-space-2) var(--fz-space-3);
    background: var(--fz-raised); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm);
}
.fz-transfer-name { font-weight: 600; overflow-wrap: anywhere; }
.fz-transfer-meta { color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-transfer-bar {
    display: block; height: .5rem; border-radius: var(--fz-radius-sm); background: var(--fz-surface);
    border: 1px solid var(--fz-line); overflow: hidden;
}
.fz-transfer-fill { display: block; height: 100%; width: 0; background: var(--fz-accent); }
.fz-transfer.fz-is-ok .fz-transfer-fill { background: var(--fz-ok); }
.fz-transfer.fz-is-ok .fz-transfer-meta { color: var(--fz-ok); }
.fz-transfer.fz-is-bad .fz-transfer-fill { background: var(--fz-bad); }
.fz-transfer.fz-is-bad .fz-transfer-meta { color: var(--fz-bad); }
.fz-picker { display: flex; flex-direction: column; gap: var(--fz-space-2); }
.fz-picker-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fz-space-1); font-size: var(--fz-text-sm); }
.fz-picker-list {
    list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm); max-height: 22rem; overflow-y: auto;
}
.fz-picker-item {
    display: flex; justify-content: space-between; align-items: baseline; gap: var(--fz-space-2); width: 100%;
    padding: var(--fz-space-2) var(--fz-space-3); background: none; border: 0; border-bottom: 1px solid var(--fz-line);
    color: var(--fz-text); font: inherit; text-align: left; cursor: pointer; min-height: 2.75rem;
}
.fz-picker-list li:last-child .fz-picker-item { border-bottom: 0; }
.fz-picker-item:hover, .fz-picker-item:focus-visible { background: var(--fz-raised); }
.fz-picker-item:focus-visible { outline: 2px solid var(--fz-accent); outline-offset: -2px; }
.fz-picker-name { overflow-wrap: anywhere; }
.fz-picker-meta { color: var(--fz-dim); font-size: var(--fz-text-sm); white-space: nowrap; }
.fz-clip {
    display: flex; flex-direction: column; gap: var(--fz-space-2); padding: var(--fz-space-3);
    background: var(--fz-raised); border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm);
}
.fz-clip-text {
    margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 14rem; overflow-y: auto;
    font-family: var(--fz-font-mono); font-size: var(--fz-text-sm);
}
.fz-clip-img { display: block; max-width: 100%; height: auto; border-radius: var(--fz-radius-sm); }

/* THE WINDOW PICKER (job_a59789fa1ccdc7c8): which of the other machine's open
 * windows a phone watches. .fz-winpick holds a .fz-switch (shape it to this
 * phone) and ul.fz-winpick-list of button.fz-winpick-item rows — a
 * .fz-winpick-icon (the window's own icon as an img, or a glyph span when it has
 * none), .fz-winpick-title and .fz-winpick-app. The one being shown is
 * aria-current="true" and says "Showing" in a .fz-winpick-now as well as by
 * its border, so the state is never colour alone. */
.fz-winpick { display: flex; flex-direction: column; gap: var(--fz-space-2); }
.fz-winpick-list {
    list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column;
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius-sm); max-height: 22rem; overflow-y: auto;
}
.fz-winpick-item {
    display: grid; grid-template-columns: 2rem 1fr auto; grid-template-rows: auto auto; column-gap: var(--fz-space-2);
    align-items: center; width: 100%; padding: var(--fz-space-2) var(--fz-space-3); background: none; border: 0;
    border-bottom: 1px solid var(--fz-line); border-left: 3px solid transparent; color: var(--fz-text); font: inherit;
    text-align: left; cursor: pointer; min-height: 3rem;
}
.fz-winpick-list li:last-child .fz-winpick-item { border-bottom: 0; }
.fz-winpick-item:hover, .fz-winpick-item:focus-visible { background: var(--fz-raised); }
.fz-winpick-item:focus-visible { outline: 2px solid var(--fz-accent); outline-offset: -2px; }
.fz-winpick-item[aria-current="true"] { border-left-color: var(--fz-accent); background: var(--fz-raised); }
.fz-winpick-icon {
    grid-row: 1 / span 2; width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center;
    font-size: 1.25rem; object-fit: contain;
}
.fz-winpick-title { overflow-wrap: anywhere; font-weight: 600; }
.fz-winpick-app { grid-column: 2; color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-winpick-now { grid-column: 3; grid-row: 1 / span 2; color: var(--fz-accent); font-size: var(--fz-text-sm); font-weight: 600; }

/* foltzui task list (job_bb10d264d896af18).
 *
 * ⭐ THE IDEA IS GOV.UK's "COMPLETE MULTIPLE TASKS": a list of things to do, one
 * row each, its status as a WORD at the end of the row, and what is finished out
 * of the way. Our own numbers and look; nothing of theirs is copied (Chris,
 * 2026-10-16).
 *
 * Built for foltzday's Today, where a wall of equal colour-filled cards kept
 * finished things full size at the top (Chris, 2026-10-28: "I was not looking for
 * broken, I was looking for design"). So:
 *   - rows, not cards: a hairline between them, no fill, no box around the list;
 *   - the status is a word (Now, Next, 7:00 PM, Done), never colour alone;
 *   - done rows go in .fz-tasks-more, a <details> that is closed by default and
 *     says how many it holds;
 *   - a row may carry a person's colour as a thin mark (--fz-task-mark): it tells
 *     two people apart at a glance and is never the only signal.
 *
 * Markup (TaskList builds it; a template may write the classes):
 *   <ul class="fz-tasks">
 *     <li class="fz-task" style="--fz-task-mark: …">
 *       <a class="fz-task-link" href=…>
 *         <span class="fz-task-glyph" aria-hidden="true">…</span>
 *         <span class="fz-task-body"><span class="fz-task-title">…</span>
 *           <span class="fz-task-meta">…a DetailLine…</span></span>
 *         <span class="fz-task-status fz-is-now">Now</span>
 *       </a></li></ul>
 *   <details class="fz-tasks-more"><summary>Done (3)</summary><ul class="fz-tasks">…</ul></details>
 */
.fz-tasks { list-style: none; margin: 0 0 var(--fz-space-6); padding: 0; border-top: 1px solid var(--fz-line); }
.fz-task { border-bottom: 1px solid var(--fz-line); }
.fz-task-link {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
    gap: var(--fz-space-3); min-height: 56px; padding: var(--fz-space-3) var(--fz-space-2) var(--fz-space-3) var(--fz-space-3);
    color: inherit; text-decoration: none;
    box-shadow: inset 3px 0 0 var(--fz-task-mark, transparent);
}
.fz-task-link:hover .fz-task-title { text-decoration: underline; text-decoration-color: var(--fz-accent); text-underline-offset: 3px; }
.fz-task-link:focus-visible { outline: var(--fz-focus, 2px) solid var(--fz-accent); outline-offset: -2px; }
/* a row with no picture keeps the column, so titles line up down the list */
.fz-task-glyph { width: 1.6em; text-align: center; font-size: 1.15em; line-height: 1; }
.fz-task-body { display: grid; gap: .1rem; min-width: 0; }
.fz-task-title { font-weight: 600; font-size: var(--fz-text-lg); line-height: var(--fz-leading-tight, 1.25); overflow-wrap: anywhere; }
.fz-task-meta { color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-task-status { font-size: var(--fz-text-sm); font-weight: 600; color: var(--fz-dim); white-space: nowrap; text-align: right; }
.fz-task-status.fz-is-now { color: var(--fz-accent); }
.fz-task-status.fz-is-done { color: var(--fz-ok); }
.fz-task-status.fz-is-late { color: var(--fz-warn); }
/* a finished row stays readable, only quieter: no strike-through to read past */
.fz-task.fz-task-done .fz-task-title { font-weight: 400; color: var(--fz-dim); }
.fz-task.fz-task-done .fz-task-glyph { opacity: .6; }

.fz-tasks-more { margin: 0 0 var(--fz-space-6); }
.fz-tasks-more > summary {
    cursor: pointer; display: flex; align-items: center; gap: var(--fz-space-2);
    min-height: 44px; color: var(--fz-dim); font-weight: 600;
}
.fz-tasks-more > summary::-webkit-details-marker { display: none; }
/* the fold says it opens: a triangle that turns, since a flex summary loses the marker */
.fz-tasks-more > summary::before { content: "\25B8"; font-size: .9em; }
.fz-tasks-more[open] > summary::before { content: "\25BE"; }
.fz-tasks-more > summary:hover { color: var(--fz-text); }
.fz-tasks-more[open] > summary { margin-bottom: var(--fz-space-2); }
.fz-tasks-more .fz-task-title { font-size: var(--fz-text-md); }

/* a section of tasks: a heading that names it, and an optional link at its end */
.fz-tasks-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--fz-space-3); margin: 0 0 var(--fz-space-2); }
.fz-tasks-head h2 { margin: 0; font-size: var(--fz-text-xl); }
.fz-tasks-head > a { font-size: var(--fz-text-sm); }

@media (max-width: 34rem) {
    .fz-task-link { gap: var(--fz-space-2); padding-left: var(--fz-space-2); }
    .fz-task-title { font-size: var(--fz-text-md); }
}

/* ── 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); font-weight: 600;
    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-sm); font-weight: 600;
    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); font-weight: 600;
    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; }

/* ── timeline ─────────────────────────────────────────────────────────────
 * The transport under an animated stage (job_b5b23b4ce141c78b, foltz 3D's
 * actions): a bar of controls — which action, play, key, the frame number — over
 * a scrubber with the keyed frames marked under it as diamonds. The scrubber is a
 * native range input, so arrows step a frame and Home/End jump to the ends.
 *   <div class="fz-timeline" role="group" aria-label="Timeline">
 *     <div class="fz-timeline-bar">…controls…<output class="fz-timeline-frame">Frame 12 of 24</output></div>
 *     <div class="fz-timeline-track"><input class="fz-timeline-scrub" type="range" aria-label="Frame">
 *       <div class="fz-timeline-keys" aria-hidden="true"><span class="fz-timeline-key" style="left:50%"></span></div></div>
 *   </div>
 * A key is a diamond AND is listed in words beside the frame (the bar says "keyed")
 * — status is never a shape alone either. */
.fz-timeline {
    display: grid; gap: var(--fz-space-1); padding: var(--fz-space-1) var(--fz-space-2);
    border-top: 1px solid var(--fz-line); background: var(--fz-surface); min-width: 0;
}
.fz-timeline-bar { display: flex; flex-wrap: wrap; gap: var(--fz-space-1) var(--fz-space-2); align-items: center; min-width: 0; }
.fz-timeline-bar select { min-height: 2.25rem; max-width: 100%; }
.fz-timeline-frame { font-family: var(--fz-font-mono); font-size: var(--fz-text-xs); color: var(--fz-dim); min-width: 6.5rem; }
.fz-timeline-track { position: relative; padding-bottom: 10px; }
.fz-timeline-scrub { display: block; width: 100%; margin: 0; min-height: 2.25rem; }
.fz-timeline-keys { position: absolute; left: .55rem; right: .55rem; bottom: 0; height: 9px; pointer-events: none; }
.fz-timeline-key {
    position: absolute; top: 1px; width: 7px; height: 7px; transform: translateX(-50%) rotate(45deg);
    background: var(--fz-warn); border: 1px solid var(--fz-text);
}
.fz-timeline[hidden], .fz-timeline-bar[hidden] { display: none; }

/* ⚠ 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; }
    /* a segmented group in a sideways-scrolling toolbar keeps its width: its own
     * min-width: 0 let the row squeeze it to a sliver (found on foltz 3D at 390px,
     * job_e6ddfd9f44f977fe) */
    .fz-toolbar > * { flex: 0 0 auto; }
    /* ⭐ .fz-toolbar-wrap: a toolbar that is a PAGE's row of commands (foltzdaw's
     * transport, job_d866d5a597550436), not an editor's strip of tools, wraps on a
     * phone instead of scrolling — a scrolled-away "History" reads as missing. */
    .fz-toolbar.fz-toolbar-wrap { flex-wrap: wrap; overflow-x: visible; }
    .fz-toolbar.fz-toolbar-wrap > .fz-toolbar-grow { flex: 1 1 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; }
}

/* ── chat: a conversation app's shell (job_e07ec5036c9a22ae) ─────────────────
 *
 * Chris, 2026-10-27, on ask.foltzcloud.com: "It is not like other chat apps
 * like claude.ai or gemini". The category has a shape everybody already knows,
 * and every piece of it was missing, so it is built HERE, once:
 *
 *   .fz-chat           the shell: a sidebar of threads beside one conversation
 *   .fz-chat-side      the sidebar — a slide-out panel under 48rem of width
 *   .fz-chat-top       a slim top bar: the menu button and the thread's title
 *   .fz-msg            one message: the person's as a bubble, a reply as prose
 *   .fz-msg-actions    Copy / Retry / Edit under a message
 *   .fz-msg-tools      "Used 2 tools ▸" — a reply's tool steps, folded
 *   .fz-chat-home      on the shell, with no thread open: a phone opens on the
 *                      list itself (a messaging app's home), not behind a button
 *   .fz-chat-row-line / -sub   a row's two lines when people, not a title, name it
 *   .fz-composer       ONE rounded input pinned to the bottom, growing as you type
 *   .fz-composer-mic   the composer's microphone: opens .fz-talk over the thread
 *   .fz-talk           talking instead of typing: one big .fz-talk-orb whose
 *                      data-phase says listening / hearing / thinking / speaking
 *
 * ⭐ THE INPUT IS THE ACTION, so it is on every screen of the shell, the empty
 * home included, and focused on load. A chat that makes you press "New" before
 * you can type has put a form in front of the conversation.
 *
 * ⚠ CONTAINER QUERIES, NOT MEDIA QUERIES: the shell decides its phone layout
 * from its OWN width, so the gallery's small demo and a real phone agree. The
 * sidebar is absolutely placed inside the shell (which is position:relative),
 * never fixed to the viewport, for the same reason.
 */
.fz-chat {
    container: fz-chat / inline-size;
    position: relative;
    display: grid;
    grid-template-columns: 17rem minmax(0, 1fr);
    height: 100vh;
    height: 100dvh;
    overflow: clip;
    background: var(--fz-bg);
}

/* ── the sidebar ── */
/* A scrollbar that is the browser's own white bar on a dark page (Windows,
 * measured live 2026-10-27) — the shell's two scrollers take the line colour. */
.fz-chat-list, .fz-chat-scroll { scrollbar-width: thin; scrollbar-color: var(--fz-line) transparent; }
.fz-chat-side {
    display: flex; flex-direction: column; min-height: 0;
    background: var(--fz-surface);
    border-right: 1px solid var(--fz-line);
    /* the sidebar owns the left, top and bottom safe areas (see SAFE AREAS below) */
    padding-top: env(safe-area-inset-top);
    padding-left: env(safe-area-inset-left);
    padding-bottom: env(safe-area-inset-bottom);
}
.fz-chat-side-head {
    display: grid; gap: var(--fz-space-2);
    padding: var(--fz-space-3);
}
.fz-chat-side-head .fz-btn { width: 100%; justify-content: flex-start; gap: var(--fz-space-2); text-decoration: none; }
.fz-chat-side-head input[type=search] { width: 100%; }
.fz-chat-list {
    flex: 1 1 auto; min-height: 0;
    overflow-y: auto; overscroll-behavior: contain;
    padding: 0 var(--fz-space-2) var(--fz-space-3);
}
.fz-chat-list ul { list-style: none; margin: 0; padding: 0; }
.fz-chat-group {
    margin: var(--fz-space-4) var(--fz-space-2) var(--fz-space-1);
    font-size: var(--fz-text-xs); font-weight: 600;
    color: var(--fz-faint); letter-spacing: 0;
}
.fz-chat-row {
    position: relative;
    display: flex; align-items: center; gap: var(--fz-space-1);
    border-radius: var(--fz-radius-md);
}
.fz-chat-row:hover, .fz-chat-row:focus-within { background: var(--fz-raised); }
.fz-chat-row[aria-current="page"], .fz-chat-row.fz-here { background: var(--fz-raised); }
.fz-chat-row > a {
    flex: 1 1 auto; min-width: 0;
    display: block; padding: .5rem .6rem; min-height: 2.25rem;
    color: var(--fz-dim); text-decoration: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    font-size: var(--fz-text-sm);
}
.fz-chat-row[aria-current="page"] > a, .fz-chat-row.fz-here > a { color: var(--fz-text); }
.fz-chat-row > a:hover { color: var(--fz-text); }

/* the ⋯ menu on a row: a <details> so it works with no script at all */
.fz-chat-more { position: relative; flex: none; }
.fz-chat-more > summary {
    list-style: none; cursor: pointer;
    display: grid; place-items: center;
    width: 2rem; height: 2rem; border-radius: var(--fz-radius-md);
    color: var(--fz-faint); font-size: 1.1rem; line-height: 1;
}
.fz-chat-more > summary::-webkit-details-marker { display: none; }
.fz-chat-more > summary:hover { color: var(--fz-text); background: var(--fz-surface); }
@media (hover: hover) {
    .fz-chat-row:not(:hover, :focus-within) .fz-chat-more:not([open]) > summary { opacity: 0; }
}
.fz-chat-menu {
    position: absolute; right: 0; top: calc(100% + 2px); z-index: 5;
    min-width: 11rem; padding: var(--fz-space-1);
    background: var(--fz-raised); border: 1px solid var(--fz-line);
    border-radius: var(--fz-radius-md); box-shadow: var(--fz-shadow-2);
    display: grid; gap: 2px;
}
.fz-chat-menu form { margin: 0; display: grid; gap: var(--fz-space-1); }
.fz-chat-menu input[type=text] { width: 100%; }
.fz-chat-menu .fz-btn { width: 100%; justify-content: flex-start; }
/* a labelled choice in the menu (a conversation's category): label over control */
.fz-chat-menu label { display: grid; gap: 2px; padding: 0 var(--fz-space-1) var(--fz-space-1); }
.fz-chat-menu select { width: 100%; }

.fz-chat-side-foot {
    display: flex; flex-wrap: wrap; gap: var(--fz-space-1) var(--fz-space-3);
    padding: var(--fz-space-3); border-top: 1px solid var(--fz-line);
    font-size: var(--fz-text-sm);
}
.fz-chat-side-foot a { color: var(--fz-dim); }

/* ── a messaging row: two lines (job_469a4f58676972bc, foltzmessage) ──
 * Ask's rows are one line because a chat with an assistant is known by its
 * title. A conversation between PEOPLE is known by who is in it and when it
 * last moved, so a row may carry a second line:
 *
 *   .fz-chat-row-line   the first line: the name, and the time at its end
 *   .fz-chat-row-name   the conversation's name, cut with an ellipsis
 *   .fz-chat-row-when   when it last moved, never cut
 *   .fz-chat-row-sub    the second line: who is in it, and any badges (a
 *                       count of new messages is a WORD and a number, never
 *                       a coloured dot alone)
 *   .fz-unread          on the row: the name goes bold, beside its badge */
.fz-chat-row > a .fz-chat-row-line { display: flex; align-items: baseline; gap: var(--fz-space-2); min-width: 0; }
.fz-chat-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.fz-chat-row-when { flex: none; font-size: var(--fz-text-xs); color: var(--fz-faint); }
.fz-chat-row-sub {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--fz-space-1) var(--fz-space-2);
    margin-top: 2px; white-space: normal;
    font-size: var(--fz-text-xs); color: var(--fz-faint);
}
.fz-chat-row.fz-unread .fz-chat-row-name { color: var(--fz-text); font-weight: 600; }
.fz-chat-empty { margin: var(--fz-space-4) var(--fz-space-2); color: var(--fz-dim); font-size: var(--fz-text-sm); }
/* a segmented control in the sidebar wraps rather than scrolling sideways:
 * a 17rem column has no room for a scrollbar under three words */
.fz-chat-side-head .fz-seg { display: flex; flex-wrap: wrap; max-width: 100%; overflow: visible; }
/* ⚠ THE SIDE HEAD'S TITLE IS THE PAGE'S TOP when it is the h1 (message's
 * "Everyday"): at the thread title's 1rem it sat 1.07x over 15px text and the
 * page had no top (job_70943f4a63f51f7f). The thread title stays quiet. */
.fz-chat-side-head .fz-chat-title { white-space: normal; font-size: var(--fz-text-xl); font-weight: 700; line-height: var(--fz-leading-tight); }
.fz-chat-side-foot button { font: inherit; color: var(--fz-dim); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: underline; }
.fz-chat-side-foot button:hover { color: var(--fz-text); }

.fz-chat-side svg, .fz-chat-top svg { width: 1.1rem; height: 1.1rem; flex: none; vertical-align: -.2em; }
.fz-chat-row > a svg { width: .95rem; height: .95rem; margin-right: .3rem; color: var(--fz-faint); }

.fz-chat-scrim { display: none; }
/* shown only on a phone, beside the menu button: New chat in the top bar.
 * ⚠ SCOPED TO THE SHELL so it outranks `a.fz-btn { display: inline-flex }`:
 * a bare .fz-chat-phone lost to it, and an <a> meant for the phone showed at
 * every width (measured 2026-10-29, job_469a4f58676972bc). */
.fz-chat .fz-chat-phone { display: none; }

/* ── the conversation ── */
.fz-chat-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
/* 🚨🚨 SAFE AREAS (job_0b222534dd5fcf2a). Chris, 2026-10-27, on an iPhone 17:
 * "I cannot click some ui elements on the foltzassistant like the menu on the
 * top left and it seems the ui elements go all the way to the edge."
 *
 * Ask is a home-screen app (display: standalone) with viewport-fit=cover and a
 * black-translucent status bar, so the page draws from the PHYSICAL top of the
 * screen. This bar sat at y=0 — under the status bar and the Dynamic Island,
 * where iOS keeps the tap for itself — so the menu button looked pressable and
 * was not. Same lesson as .fz-nav in base.css (2026-09-25): THE BAR OWNS THE
 * INSET, its own padding filling the strip with its own background.
 *
 * ⚠ EACH INSET IS DECLARED AFTER THE `padding` SHORTHAND. Above it, the
 * shorthand resets it to 0 and the rule is present and does nothing.
 * ⚠ env() is 0 on a page without viewport-fit=cover, and on a desktop, so every
 * other user of the shell is unchanged to the pixel.
 * The left inset (a phone on its side) belongs to whichever piece is at the
 * left edge: the sidebar when it is beside the thread, the thread on a phone. */
.fz-chat-top {
    display: flex; align-items: center; gap: var(--fz-space-2);
    min-height: calc(3rem + env(safe-area-inset-top));
    padding: var(--fz-space-1) var(--fz-space-4);
    padding-top: calc(var(--fz-space-1) + env(safe-area-inset-top));
    padding-right: max(var(--fz-space-4), env(safe-area-inset-right));
    border-bottom: 1px solid var(--fz-line);
}
.fz-chat-title {
    flex: 1 1 auto; min-width: 0; margin: 0;
    font-size: var(--fz-text-md); font-weight: 600;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fz-chat-toggle { display: none; }
.fz-chat-scroll {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: clip;
    padding-right: env(safe-area-inset-right);
}
.fz-chat-col {
    max-width: 48rem; margin: 0 auto;
    padding: var(--fz-space-5) var(--fz-space-4);
    display: flex; flex-direction: column; gap: var(--fz-space-5);
}
.fz-chat-hello { margin: 18vh 0 0; text-align: center; }
.fz-chat-hello h2 { margin: 0 0 var(--fz-space-2); font-size: var(--fz-text-2xl); font-weight: var(--fz-weight-heading); }
.fz-chat-hello p { margin: 0 auto var(--fz-space-4); color: var(--fz-dim); }
.fz-chat-suggest { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--fz-space-2); }
.fz-chat-live {
    display: flex; align-items: center; gap: var(--fz-space-2);
    color: var(--fz-dim); font-size: var(--fz-text-sm);
}
.fz-chat-live::before {
    content: ""; width: .45rem; height: .45rem; border-radius: 50%;
    background: var(--fz-accent); animation: fz-chat-pulse 1.4s ease-in-out infinite;
}
@keyframes fz-chat-pulse { 50% { opacity: .25; } }

/* ── a message ── */
.fz-msg { display: flex; flex-direction: column; gap: var(--fz-space-1); min-width: 0; }
.fz-msg-body { overflow-wrap: anywhere; line-height: var(--fz-leading); }
.fz-msg-body > :first-child { margin-top: 0; }
.fz-msg-body > :last-child { margin-bottom: 0; }
.fz-msg-me { align-items: flex-end; }
.fz-msg-me > .fz-msg-body {
    max-width: min(36rem, 85%);
    padding: .6rem .9rem;
    background: var(--fz-raised); border-radius: var(--fz-radius-lg);
    white-space: pre-wrap;
}
/* the other person in a shared thread: a request like yours, on the left, named */
.fz-msg-other { align-items: flex-start; }
.fz-msg-other > .fz-msg-body {
    /* the same bubble as yours, outlined: it had the border and none of the
     * padding, so the words sat against the line (job_469a4f58676972bc) */
    max-width: min(36rem, 85%);
    padding: .6rem .9rem; border-radius: var(--fz-radius-lg);
    border: 1px solid var(--fz-line); background: var(--fz-surface);
    white-space: pre-wrap;
}
.fz-msg-author { margin: 0; font-size: var(--fz-text-xs); color: var(--fz-faint); }
.fz-msg-them > .fz-msg-body { max-width: none; }
.fz-msg-them > .fz-msg-body.fz-prose { max-width: none; }
.fz-msg img { max-width: min(100%, 18rem); border-radius: var(--fz-radius-md); }
.fz-msg-pending > .fz-msg-body { opacity: .65; }
.fz-msg-unsent > .fz-msg-body { opacity: .65; text-decoration: line-through; }
.fz-msg-error > .fz-msg-body { color: var(--fz-bad); }
.fz-msg-error > .fz-msg-body::before { content: "⚠ "; }

/* ── between people (job_469a4f58676972bc, foltzmessage) ──
 *   .fz-msg-quote     the message this one answers, one line, above the body
 *   .fz-msg-deleted   a message its sender took back: the words are gone and
 *                     the place stays, so the replies around it still read
 *   .fz-msg-pointed   the message something else on the page pointed at
 *   .fz-chat-divider  a line across the thread: "New since you were here" */
.fz-msg-quote {
    margin: 0; max-width: min(36rem, 85%);
    padding-left: var(--fz-space-2); border-left: 2px solid var(--fz-line);
    font-size: var(--fz-text-xs); color: var(--fz-faint);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fz-msg-deleted > .fz-msg-body { font-style: italic; color: var(--fz-faint); }
.fz-msg-pointed > .fz-msg-body { outline: 2px solid var(--fz-accent); outline-offset: 3px; }
.fz-chat-divider {
    display: flex; align-items: center; gap: var(--fz-space-2);
    margin: 0; color: var(--fz-dim); font-size: var(--fz-text-xs);
}
.fz-chat-divider::before, .fz-chat-divider::after { content: ""; flex: 1 1 auto; border-top: 1px solid var(--fz-line); }

.fz-msg-tools > summary {
    cursor: pointer; list-style: none; display: inline-flex; gap: .35rem; align-items: center;
    color: var(--fz-faint); font-size: var(--fz-text-sm); padding: .15rem 0;
}
.fz-msg-tools > summary::-webkit-details-marker { display: none; }
.fz-msg-tools > summary::after { content: "▸"; }
.fz-msg-tools[open] > summary::after { content: "▾"; }
.fz-msg-tools > summary:hover { color: var(--fz-text); }
.fz-msg-tools ul { margin: .2rem 0 .3rem; padding-left: 1.1rem; color: var(--fz-dim); font-size: var(--fz-text-sm); }

.fz-msg-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; min-height: 2rem; }
.fz-msg-me > .fz-msg-actions { justify-content: flex-end; }
@media (hover: hover) {
    .fz-msg:not(:hover, :focus-within, .fz-msg-last) > .fz-msg-actions > .fz-msg-act { opacity: 0; }
}
.fz-msg-act {
    display: inline-flex; align-items: center; gap: .3rem;
    min-height: 2rem; min-width: 2rem; padding: 0 .45rem;
    font: inherit; font-size: var(--fz-text-xs);
    color: var(--fz-faint); background: none;
    border: 1px solid transparent; border-radius: var(--fz-radius-md); cursor: pointer;
}
.fz-msg-act:hover { color: var(--fz-text); background: var(--fz-raised); }
.fz-msg-act[aria-pressed="true"] { color: var(--fz-accent); }
.fz-msg-act svg { width: 1rem; height: 1rem; flex: none; }
.fz-msg-note { color: var(--fz-dim); font-size: var(--fz-text-xs); }
.fz-msg-edit { display: grid; gap: var(--fz-space-2); width: min(36rem, 100%); }
.fz-msg-edit textarea { width: 100%; min-height: 4rem; }
.fz-msg-edit > div { display: flex; gap: var(--fz-space-2); justify-content: flex-end; }

/* ── the composer ── */
.fz-composer-foot {
    flex: none;
    padding: var(--fz-space-2) var(--fz-space-4) max(var(--fz-space-3), env(safe-area-inset-bottom));
    padding-right: max(var(--fz-space-4), env(safe-area-inset-right));
}
.fz-composer-foot > * { max-width: 48rem; margin-left: auto; margin-right: auto; }
.fz-composer {
    /* the attach slot and the input are the two declared columns; every
     * button after them (stop, mic, send) takes an auto column of its own, so
     * a hidden one costs no gap and a page without a mic needs no variant */
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    grid-auto-flow: column; grid-auto-columns: auto;
    align-items: end; gap: var(--fz-space-1);
    padding: .4rem;
    background: var(--fz-raised);
    border: 1px solid var(--fz-line); border-radius: 1.6rem;
    box-shadow: var(--fz-shadow-1);
}
.fz-composer:focus-within { border-color: var(--fz-dim); }
.fz-composer-input {
    display: block; width: 100%; resize: none;
    min-height: 2.5rem; max-height: 12rem; overflow-y: auto;
    padding: .55rem .3rem; margin: 0;
    font: inherit; line-height: 1.4; color: var(--fz-text);
    background: transparent; border: 0; border-radius: 0; box-shadow: none;
}
.fz-composer-input:focus, .fz-composer-input:focus-visible { outline: none; }
.fz-composer-attach, .fz-composer-send, .fz-composer-stop, .fz-composer-mic {
    display: grid; place-items: center;
    width: 2.5rem; height: 2.5rem; padding: 0;
    border-radius: 50%; border: 1px solid transparent; cursor: pointer;
    background: none; color: var(--fz-dim);
}
.fz-composer-attach:hover, .fz-composer-mic:hover { color: var(--fz-text); background: var(--fz-surface); }
.fz-composer-attach:focus-within { outline: 2px solid var(--fz-accent); outline-offset: 2px; }
.fz-composer-send { background: var(--fz-accent); color: var(--fz-on-accent); }
.fz-composer-send:disabled { background: var(--fz-surface); color: var(--fz-faint); cursor: default; }
.fz-composer-stop { color: var(--fz-text); border-color: var(--fz-line); }
.fz-composer svg { width: 1.25rem; height: 1.25rem; }
.fz-composer-extra { display: flex; align-items: center; gap: var(--fz-space-2); margin-bottom: var(--fz-space-2); }
.fz-composer-extra img { max-height: 3.5rem; border-radius: var(--fz-radius-md); border: 1px solid var(--fz-line); }
.fz-composer-hint { margin-top: var(--fz-space-1); text-align: center; color: var(--fz-faint); font-size: var(--fz-text-xs); }

/* the options for THIS message and the room's quiet actions, one small line
 * under the composer: "urgent", "Need a break" (job_469a4f58676972bc). Never
 * a second row of buttons as loud as Send. */
.fz-composer-opts {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: var(--fz-space-1) var(--fz-space-3);
    margin-top: var(--fz-space-1); color: var(--fz-dim); font-size: var(--fz-text-xs);
}
.fz-composer-opts label { display: inline-flex; align-items: center; gap: var(--fz-space-1); min-height: 2rem; font-weight: 400; }

/* ── talk: speaking instead of typing (job_ca5702613177a472) ────────────────
 *
 * Chris, 2026-10-27: "On gemini I hit a button and it takes me to the voice
 * agent". .fz-composer-mic opens .fz-talk, a full-screen <dialog> over the
 * thread, and the tap that opened it is the tap that starts listening — a
 * second page would spend that tap, and on a phone the reply would then be
 * silent until somebody tapped again.
 *
 * ⭐ ONE BIG CONTROL, AND IT SAYS WHAT IT IS DOING. .fz-talk-orb carries
 * data-phase, set by the page's script:
 *
 *   off        not listening — tap to start
 *   listening  the microphone is open and the room is quiet
 *   hearing    words are arriving: it is taking down what you say
 *   thinking   your turn ended and it is working on the answer
 *   speaking   it is answering out loud — talk over it to stop it
 *   muted      the session is up and the microphone is not
 *
 * ⚠ STATUS IS NEVER COLOUR ALONE: .fz-talk-state says the phase in words
 * beside the orb, every time. --fz-talk-level (0..1, set from the microphone)
 * swells the ring while it listens, which is the evidence the mic works.
 */
.fz-talk {
    width: 100%; height: 100%; max-width: none; max-height: none;
    margin: 0; padding: 0; border: 0;
    background: var(--fz-bg); color: var(--fz-text);
}
.fz-talk[open] { display: flex; flex-direction: column; }
.fz-talk::backdrop { background: var(--fz-bg); }
.fz-talk-top {
    display: flex; align-items: center; justify-content: space-between; gap: var(--fz-space-2);
    padding: max(var(--fz-space-2), env(safe-area-inset-top)) max(var(--fz-space-3), env(safe-area-inset-right))
             var(--fz-space-2) max(var(--fz-space-3), env(safe-area-inset-left));
}
.fz-talk-title { margin: 0; font-size: var(--fz-text-md); font-weight: var(--fz-weight-heading); }
.fz-talk-log {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    display: flex; flex-direction: column; gap: var(--fz-space-2);
    width: 100%; max-width: 40rem; margin: 0 auto;
    padding: var(--fz-space-3) max(var(--fz-space-3), env(safe-area-inset-right)) var(--fz-space-3) max(var(--fz-space-3), env(safe-area-inset-left));
    scrollbar-width: thin; scrollbar-color: var(--fz-line) transparent;
}
.fz-talk-log > p { margin: 0; line-height: var(--fz-leading); overflow-wrap: anywhere; color: var(--fz-dim); }
.fz-talk-log > p.is-reply { color: var(--fz-text); }
.fz-talk-log > p.is-tool { font-size: var(--fz-text-sm); color: var(--fz-faint); }
.fz-talk-log > p.is-error { font-size: var(--fz-text-sm); color: var(--fz-bad); }
.fz-talk-log > p.is-you { align-self: flex-end; max-width: 85%; color: var(--fz-text);
    background: var(--fz-raised); border-radius: 1.1rem; padding: .45rem .85rem; }
.fz-talk-heard {
    min-height: 1.5em; margin: 0; padding: 0 var(--fz-space-3);
    text-align: center; color: var(--fz-faint); font-style: italic;
}
.fz-talk-stage {
    flex: none; display: flex; flex-direction: column; align-items: center; gap: var(--fz-space-3);
    padding: var(--fz-space-4) var(--fz-space-3) max(var(--fz-space-5), env(safe-area-inset-bottom));
}
.fz-talk-state { margin: 0; min-height: 1.4em; color: var(--fz-dim); text-align: center; }
/* align-items: center, or the row's min-height stretches each button into a tall box (seen live at 390px) */
.fz-talk-controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--fz-space-2); min-height: var(--fz-tap); }
.fz-talk-orb {
    --fz-talk-level: 0;
    position: relative; display: grid; place-items: center;
    width: 7.5rem; height: 7.5rem; padding: 0;
    border-radius: 50%; border: 2px solid var(--fz-line);
    background: var(--fz-raised); color: var(--fz-text);
    cursor: pointer; box-shadow: var(--fz-shadow-1);
    transition: background var(--fz-dur) var(--fz-ease), color var(--fz-dur) var(--fz-ease), border-color var(--fz-dur) var(--fz-ease);
}
.fz-talk-orb svg { width: 2.75rem; height: 2.75rem; }
.fz-talk-orb:focus-visible { outline: 2px solid var(--fz-focus); outline-offset: 4px; }
.fz-talk-orb::before {
    content: ""; position: absolute; inset: -8px; border-radius: 50%;
    border: 3px solid var(--fz-accent); opacity: 0; pointer-events: none;
    transform: scale(calc(1 + var(--fz-talk-level) * .3));
    transition: transform 120ms linear, opacity var(--fz-dur) var(--fz-ease);
}
.fz-talk-orb[data-phase="listening"], .fz-talk-orb[data-phase="hearing"] { border-color: var(--fz-accent); color: var(--fz-accent); }
.fz-talk-orb[data-phase="listening"]::before { opacity: .35; }
.fz-talk-orb[data-phase="hearing"]::before { opacity: .8; }
.fz-talk-orb[data-phase="thinking"] { border-color: var(--fz-accent); color: var(--fz-accent); }
.fz-talk-orb[data-phase="thinking"]::before {
    opacity: 1; transform: none; border-color: var(--fz-accent) transparent transparent transparent;
    animation: fz-talk-spin 1s linear infinite;
}
.fz-talk-orb[data-phase="speaking"] { background: var(--fz-accent); color: var(--fz-on-accent); border-color: var(--fz-accent); }
.fz-talk-orb[data-phase="speaking"]::before { opacity: .5; animation: fz-talk-pulse 1.6s var(--fz-ease) infinite; }
.fz-talk-orb[data-phase="muted"] { border-style: dashed; color: var(--fz-faint); }
@keyframes fz-talk-spin { to { transform: rotate(1turn); } }
@keyframes fz-talk-pulse { 0%, 100% { transform: scale(1); opacity: .5; } 50% { transform: scale(1.12); opacity: .15; } }
@media (prefers-reduced-motion: reduce) {
    .fz-talk-orb, .fz-talk-orb::before { transition: none; }
    .fz-talk-orb[data-phase="thinking"]::before, .fz-talk-orb[data-phase="speaking"]::before { animation: none; }
}

/* ── a phone: the sidebar slides in from the menu button ── */
@container fz-chat (max-width: 48rem) {
    .fz-chat-main { grid-column: 1 / -1; grid-row: 1; }
    .fz-chat-side {
        grid-column: 1; grid-row: 1;
        position: absolute; inset: 0 auto 0 0; z-index: 20;
        width: min(20rem, 86%);
        transform: translateX(-100%); visibility: hidden;
        transition: transform var(--fz-dur) var(--fz-ease), visibility 0s linear var(--fz-dur);
        box-shadow: var(--fz-shadow-2);
    }
    .fz-chat[data-side="open"] .fz-chat-side {
        transform: none; visibility: visible;
        transition: transform var(--fz-dur) var(--fz-ease);
    }
    .fz-chat[data-side="open"] .fz-chat-scrim {
        display: block; position: absolute; inset: 0; z-index: 15;
        background: rgb(0 0 0 / .45);
    }
    .fz-chat .fz-chat-toggle, .fz-chat .fz-chat-phone { display: grid; }
    /* the thread is at the left edge now, so it takes the left inset */
    .fz-chat-top {
        padding-left: max(var(--fz-space-2), env(safe-area-inset-left));
        padding-right: max(var(--fz-space-2), env(safe-area-inset-right));
    }
    .fz-chat-scroll { padding-left: env(safe-area-inset-left); }
    .fz-chat-col { padding: var(--fz-space-4) var(--fz-space-3); }
    .fz-composer-foot {
        padding-left: max(var(--fz-space-2), env(safe-area-inset-left));
        padding-right: max(var(--fz-space-2), env(safe-area-inset-right));
    }
    /* 44px, the floor .fz-btn already keeps: a thumb, not a pointer */
    .fz-chat-toggle, .fz-chat-phone { min-width: 44px; place-items: center; padding-left: .6rem; padding-right: .6rem; }
    .fz-msg-act { min-height: 44px; min-width: 44px; }
    .fz-msg-actions { min-height: 44px; }
    .fz-msg-tools > summary { min-height: 44px; }
    .fz-chat-side-foot a { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; }
    .fz-composer-attach, .fz-composer-send, .fz-composer-stop, .fz-composer-mic { width: 2.75rem; height: 2.75rem; }

    /* ⭐ A MESSAGING HOME OPENS ON THE LIST (job_469a4f58676972bc). With no
     * conversation open there is nothing behind the sidebar to show, so on a
     * phone the list IS the page, in flow, the way every messaging app opens.
     * .fz-chat-home on the shell says so; a page with a thread open leaves it off. */
    .fz-chat.fz-chat-home .fz-chat-side {
        position: static; grid-column: 1 / -1; width: auto;
        transform: none; visibility: visible; box-shadow: none; border-right: 0;
        transition: none;
    }
    .fz-chat.fz-chat-home .fz-chat-main { display: none; }
    .fz-composer-opts label { min-height: 44px; }
    .fz-chat-hello { margin-top: 10vh; }
    .fz-msg-me > .fz-msg-body, .fz-msg-other > .fz-msg-body { max-width: 90%; }
    .fz-chat-row > a { padding: .7rem .6rem; }
    .fz-chat-more > summary { width: 2.75rem; height: 2.75rem; }
}
@media (prefers-reduced-motion: reduce) {
    .fz-chat-side { transition: none !important; }
    .fz-chat-live::before { animation: none; }
}

/* ── question pages (foltzui.Question, job_6723fe31b58363b0): one question on a
 * page of its own. The question IS the heading and the label; the hint and the
 * "why we ask" line sit between it and the field; one column, no wider than a
 * panel, so a phone never wraps a label beside its input. */
.fz-question { display: grid; gap: var(--fz-space-2); max-width: var(--fz-panel); margin: 0 0 var(--fz-space-5); border: 0; padding: 0; min-width: 0; }
.fz-question-legend { display: grid; gap: var(--fz-space-1); padding: 0; margin: 0 0 var(--fz-space-1); color: var(--fz-text); font-weight: inherit; }
.fz-question-caption { color: var(--fz-dim); font-size: var(--fz-text-md); font-weight: 600; }
.fz-question-heading { margin: 0; font-size: var(--fz-text-2xl); line-height: var(--fz-leading-tight); }
.fz-question-heading label { font-weight: inherit; }
.fz-question-why { margin: 0; padding: var(--fz-space-2) var(--fz-space-3); border-left: 3px solid var(--fz-line); color: var(--fz-dim); font-size: var(--fz-text-sm); }
.fz-question > input:not([type=checkbox], [type=radio]) { width: 100%; }
.fz-question > input[type=date] { max-width: 14rem; }
.fz-question-options { display: grid; gap: var(--fz-space-1); }
.fz-question-option > span { display: grid; gap: .1rem; }

/* the quiet way back to the question before */
.fz-back { display: inline-block; justify-self: start; margin: 0 0 var(--fz-space-4); font-size: var(--fz-text-sm); color: var(--fz-dim); }
.fz-back::before { content: "‹ "; }
/* a submit button that reads as a link: Back, and Change on a check-answers
 * Summary, when the answers ride in the form (BackButton, ChangeButton) */
button.fz-back, .fz-linkbtn { display: inline-block; justify-self: start; width: auto; text-align: left; min-height: 0; padding: 0; border: 0; background: none; font: inherit; font-size: var(--fz-text-sm); color: var(--fz-dim); text-decoration: underline; cursor: pointer; }
.fz-linkbtn { color: var(--fz-accent); }
button.fz-back:hover, .fz-linkbtn:hover { color: var(--fz-text); }

/* ── a question ANSWERED on the page (job_9d247d2d0084a007, foltzquiz's drill):
 * the same question page, where the answer is checked where it was given instead
 * of on the next page. quiz.js builds this markup; /ui/gallery#question-answered
 * shows it.
 *
 * ⭐ THE CHOICES ARE THE PAGE'S WORK, so each is a whole row to tap: a bordered
 * row at the control's height, its letter first (the key that picks it), the
 * picked one in the accent. Once checked, the right choice and a wrong pick each
 * say so in a WORD under the answer, never by colour alone; the rest step back. */
.fz-question-heading.fz-is-long { font-size: var(--fz-text-lg); font-weight: 600; line-height: var(--fz-leading); }
.fz-question-heading p { margin: 0 0 var(--fz-space-2); }
.fz-question-heading p:last-child { margin-bottom: 0; }
.fz-question-heading .fz-tablewrap { margin: var(--fz-space-2) 0; font-size: var(--fz-text-sm); font-weight: 400; }
.fz-question-heading .fz-table { width: auto; }
/* an exhibit the question refers to — a graph drawn for paper keeps a light ground in either room */
.fz-question-figure { display: block; max-width: 100%; height: auto; padding: var(--fz-space-2); border-radius: var(--fz-radius); background: var(--fz-viewer-text); }
.fz-question-choices .fz-question-option {
    align-items: center; gap: var(--fz-space-3); margin: 0; padding: var(--fz-space-2) var(--fz-space-3);
    min-height: var(--fz-control); border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    background: var(--fz-surface); cursor: pointer;
}
.fz-question-choices .fz-question-option:hover { border-color: var(--fz-accent); }
.fz-question-choices .fz-question-option:has(input:checked) { border-color: var(--fz-accent); background: var(--fz-raised); }
.fz-question-choices .fz-question-option:has(input:focus-visible) { outline: 2px solid var(--fz-accent); outline-offset: 2px; }
.fz-question-choices .fz-question-option input { margin: 0; }
.fz-question-choices .fz-question-option > span:not(.fz-question-key) { flex: 1 1 auto; min-width: 0; }
.fz-question-option > .fz-question-key { flex: none; }
.fz-question-key { font-family: var(--fz-font-mono); font-weight: 600; color: var(--fz-accent); min-width: 1.2em; }
.fz-question-choices .fz-question-option:has(input:disabled) { cursor: default; }
.fz-question-choices .fz-question-option:has(input:disabled):not(.fz-is-right, .fz-is-wrong):hover { border-color: var(--fz-line); }
.fz-question-choices .fz-question-option.fz-is-right:has(input) { border-color: var(--fz-ok); }
.fz-question-choices .fz-question-option.fz-is-wrong:has(input) { border-color: var(--fz-bad); }
.fz-question-option.fz-is-dim { opacity: .6; }
.fz-question-mark { font-size: var(--fz-text-sm); font-weight: 600; }
.fz-question-mark:empty { display: none; }
.fz-is-right .fz-question-mark { color: var(--fz-ok); }
.fz-is-wrong .fz-question-mark { color: var(--fz-bad); }

/* foltzui STAGE — a page that IS its picture: a full-bleed stage, one floating
 * action group over it, and everything else in a drawer (job_577354740d056476).
 *
 * 🚨 Chris, 2026-10-28, on map.foltzcloud.com: "design, not breakage." The map
 * sat in a 700px reading column — about 330px wide on a 1280 screen — beside a
 * long text panel, and on a phone "Find me" and "Start recording" were below
 * the fold. A map, a globe or a canvas is the page; a column of settings beside
 * it is the page's furniture.
 *
 * ⭐ THE SHAPE, three classes and a native popover (the full markup is the
 * "stage" entry on /ui/gallery; it is not written out here because this sheet is
 * inlined into pages, and a tag in a comment is a tag to a test that reads HTML):
 *
 *   body.fz-shell.fz-stage-page
 *     the TopBar
 *     main.fz-stage
 *       .fz-stage-canvas            the map, globe or canvas
 *       .fz-fabs
 *         p.fz-fab-status           role=status, what to do next
 *         .fz-fab-row               the one primary (data-fz-primary) and the
 *                                   button with popovertarget (the drawer id)
 *     aside.fz-drawer[popover=manual]
 *       .fz-drawer-head             h2 and a Close (popovertargetaction=hide)
 *       .fz-drawer-body             everything else, in .fz-disclose folds
 *
 * ⭐ THE DRAWER IS A NATIVE POPOVER, so it opens and closes with NO SCRIPT: the
 * button's popovertarget toggles it and the browser sets aria-expanded on the
 * button. It sits in the top layer, above a map library's own panes and
 * controls, without a z-index war. "manual" because a tap on the map must not
 * close it — the map is where a waypoint is dropped while the form is open.
 * stage.js adds Escape-to-close and fzDrawer.show(el) for a script that has just
 * revealed a form inside it.
 *
 * ⭐ A BOTTOM SHEET ON A PHONE, A SIDE SHEET AT 64rem AND UP — the same element.
 * The phone keeps the top of the map visible above the sheet; the desktop keeps
 * the map's left two thirds. */

.fz-stage-page { height: 100vh; height: 100dvh; min-height: 0; overflow: hidden; }
.fz-stage { position: relative; flex: 1 1 auto; min-height: 0; isolation: isolate; }
.fz-stage-canvas { position: absolute; inset: 0; }

/* ── the floating actions: the page's primary, over the picture, always in reach */
.fz-fabs {
    position: absolute; z-index: 1000; left: 50%; transform: translateX(-50%);
    bottom: calc(var(--fz-space-6) + env(safe-area-inset-bottom));
    display: flex; flex-direction: column; align-items: center; gap: var(--fz-space-2);
    width: max-content; max-width: calc(100% - 2 * var(--fz-space-3));
    pointer-events: none;   /* the gap between buttons is still the map */
}
.fz-fabs > * { pointer-events: auto; }
.fz-fab-row { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--fz-space-2); }
.fz-fabs .fz-btn { min-height: var(--fz-control); box-shadow: var(--fz-shadow-2); }
.fz-fab-status {
    margin: 0; max-width: min(100%, 32rem); padding: var(--fz-space-1) var(--fz-space-3);
    border-radius: var(--fz-radius-pill); box-shadow: var(--fz-shadow-1);
    background: color-mix(in srgb, var(--fz-bg) 88%, transparent);
    color: var(--fz-text); font-size: var(--fz-text-sm); text-align: center;
}
.fz-fab-status:empty { display: none; }

/* ── the drawer: everything that is not the primary, one press away ───────── */
.fz-drawer {
    position: fixed; inset: auto 0 0 0; margin: 0; padding: 0;
    width: 100%; max-width: none; height: auto; max-height: 75vh; max-height: 75dvh;
    border: 0; border-top: 1px solid var(--fz-line);
    border-radius: var(--fz-radius-lg) var(--fz-radius-lg) 0 0;
    background: var(--fz-surface); color: var(--fz-text); box-shadow: var(--fz-shadow-2);
    overflow: hidden; flex-direction: column;
}
.fz-drawer:popover-open { display: flex; }
.fz-drawer::backdrop { background: transparent; }
.fz-drawer-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--fz-space-3);
    padding: var(--fz-space-2) var(--fz-space-4); border-bottom: 1px solid var(--fz-line);
}
.fz-drawer-head h2 { margin: 0; font-size: var(--fz-text-lg); }
.fz-drawer-body {
    flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain;
    padding: var(--fz-space-3) var(--fz-space-4) calc(var(--fz-space-6) + env(safe-area-inset-bottom));
}
.fz-drawer-body > :first-child { margin-top: 0; }
/* ⭐ A DRAWER IS A STACK OF FOLDS, and a fold's summary there is a SECTION'S
 * NAME, not metadata: .fz-disclose's quiet .82rem summary is right under a row
 * and wrong as the only way into "Sky". A heading inside the summary keeps the
 * page's outline (summary may hold one heading) at the fold's own size. */
.fz-drawer-body .fz-disclose { border-bottom: 1px solid var(--fz-line); }
.fz-drawer-body .fz-disclose > summary { display: flex; width: 100%; color: var(--fz-text); font-size: var(--fz-text-md); font-weight: 600; }
.fz-drawer-body .fz-disclose > summary > :is(h2, h3) { margin: 0; font: inherit; }
@media (min-width: 64rem) {
    .fz-drawer {
        inset: 0 0 0 auto; width: min(26rem, 100vw); height: 100vh; height: 100dvh; max-height: none;
        border-top: 0; border-left: 1px solid var(--fz-line); border-radius: 0;
    }
}
@media (prefers-reduced-motion: no-preference) {
    .fz-drawer:popover-open { animation: fz-drawer-in var(--fz-dur-fast) var(--fz-ease); }
}
@keyframes fz-drawer-in { from { opacity: 0; transform: translateY(1rem); } to { opacity: 1; transform: none; } }

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