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

/* foltzplatform base — the components that were implemented once per service.
 *
 * ⚠ EVERY CLASS IS PREFIXED .fz-, for the same reason the tokens are: a service
 * adopting this must not have its existing .btn, .card or .empty change under it.
 * Adoption is additive; migration is a separate, later, deliberate edit.
 *
 * 🚨 WHAT IS HERE AND WHY, in one line each — the inventory count is the argument:
 *   focus ring     6 systems, and 4 services had NONE
 *   .fz-btn        9 implementations, 2 incompatible modifier conventions
 *   .fz-empty      9 implementations, 5 visual languages
 *   .fz-notice     12 implementations, 3 class names
 *   .fz-card       11 + 3 .panel + 2 .tile
 *   .fz-badge      11, six radii for one idea (3/5/8/20/99/999px)
 *   .fz-nav        6 vocabularies; foltzbuild alone ships 3 for 6 pages
 *   .fz-table      6, and only one of them wraps for overflow
 */

/* ── the fixes every service should already have had ──────────────────────── */

/* 🚨 THIS ONE LINE FIXES A BUG THAT PASSES EVERY TEST. board.css:18 records how
 * it hid: "Nothing errored, and every test passed." A stylesheet rule with any
 * display value beats the [hidden] attribute, so an element the code has
 * carefully hidden stays on screen and nothing anywhere says so. Every service
 * without this is still exposed. */
[hidden] { display: none !important; }

*, *::before, *::after { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--fz-bg);
    color: var(--fz-text);
    font: var(--fz-size)/var(--fz-leading) var(--fz-font);
}

/* ⚠ THE LINK RULE, AND IT IS THE REASON THIS PACKAGE EXISTS.
 *
 * 🚨 THE SAME DEFECT SHIPPED TWICE. wrk_24aa8023 fixed a bare <a> rendering as
 * browser-default blue at about 1.9:1 against a dark background — below the 4.5:1
 * normal text needs and below even the 3:1 allowed for large text. foltzconvo then
 * shipped it again on its own header link, found on 2026-09-05 by looking at the
 * page: "it sat two lines from the fix, untouched, because nobody looked at the
 * header while fixing the row."
 *
 * ⭐ THE COLOUR BELONGS TO THE TEXT AND THE UNDERLINE SAYS "TAPPABLE". That is
 * what makes it legible first and link-like second, which is the right order for
 * something read on a phone, outdoors. */
a {
    color: var(--fz-text);
    text-decoration: underline;
    text-decoration-color: var(--fz-faint);
    text-underline-offset: 2px;
}
a:hover { text-decoration-color: var(--fz-accent); }

/* 🚨 ONE FOCUS RING, ON EVERYTHING, AND FOUR SERVICES HAD NONE AT ALL.
 * :focus-visible rather than :focus so a mouse click does not draw it — the
 * reason every service that hand-rolled this eventually got there. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--fz-accent);
    outline-offset: 2px;
}

/* ⭐ VISUALLY HIDDEN, SHARED. Two services had this and each wrote its own. */
.fz-vh {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* 🚨 NO ANIMATION WITHOUT THIS GUARD. foltzday's charter is the fleet's:
 * "LOW STIMULATION... a migraine-prone adult has to be able to walk past it."
 * benditsolutions animates a button hover with no guard at all. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

.fz-wrap { max-width: var(--fz-wrap); margin: 0 auto; padding: 0 1.1rem; }

/* ── nav ──────────────────────────────────────────────────────────────────
 * ⭐ THE CURRENT PAGE IS NAMED, NOT LINKED. reachable_test.go already refuses a
 * route with no link — Chris, 2026-08-29: "user is unable to reach hexmap via ui
 * — this is a pattern we avoid" — and a page linking to itself is how a nav grows
 * a dead click. foltzui.Nav() marks the current entry so nobody has to remember
 * to de-link it by hand in four copies, which is exactly how one of them drifted. */
/* 🚨🚨 IT STICKS, AND IT IS BIG ENOUGH TO HIT.
 *
 * Chris, 2026-09-25: "users have to scroll all the way to the top to see the
 * menu and navigate elsewhere" and "ashley cannot navigate the UI".
 *
 * MEASURED ON message.foltzcloud.com: a conversation page is 16,628px tall —
 * SEVENTEEN AND A HALF SCREENS — and this nav was `position: static` at the very
 * top of it. Leaving a room meant scrolling the entire conversation back up.
 * The links themselves were 17px tall at 13px type: below the 24px WCAG 2.5.8
 * floor, well below the 44px everyone actually uses, and five times under the
 * 88px tap target foltzmessage declares for its OWN buttons. A nav the design
 * system will not give a tap target to is not navigation, it is a footnote.
 *
 * ⚠ AND THE MOBILE RULE MADE IT WORSE, NOT BETTER — it shrank the nav to .78rem
 * on the smallest screens, which is where a thumb has the least to aim at. That
 * rule is inverted below.
 *
 * ⭐ FIXED HERE AND NOT IN foltzmessage, because 19 services render this and the
 * complaint is about all of them. A local override would be the second design
 * system ui-standard.md exists to prevent.
 *
 * ⚠ IT NEEDS AN OPAQUE BACKGROUND. A sticky bar over scrolling content with a
 * transparent background is unreadable the moment anything passes under it. */
.fz-nav {
    position: sticky;
    top: 0;
    z-index: 20;
    /* 🚨🚨 THE NOTCH. Chris, 2026-09-25: "the menu bar in chat shows up at the
     * very top of the phone screen and users cannot select it because the ear
     * portion of the phone is in the way - but in non-chat the menu bar is at a
     * reasonable height."
     *
     * ⭐ THAT DIFFERENCE IS THE WHOLE DIAGNOSIS, and it is a property of sticky
     * rather than of either page. foltzmessage sets viewport-fit=cover and pads
     * its BODY by env(safe-area-inset-top), so in normal flow this bar sits
     * below the Dynamic Island — which is why the list pages were fine. But a
     * sticky element pins to the SCROLLPORT, not to the padded body box, so the
     * moment a long conversation scrolls it snaps to the physical top of the
     * screen and slides under the ear, where iOS will not deliver a tap.
     *
     * ⇒ THE BAR OWNS THE INSET ITSELF. Its own padding fills the notch strip
     * with its own background, so it reads correctly whether it is in flow or
     * pinned, and there is no gap for content to scroll through above it.
     *
     * ⚠ SAFE ON EVERY OTHER SERVICE. env() resolves to 0 without
     * viewport-fit=cover, so the other 18 navs are unchanged to the pixel.
     *
     * 🚨 THE INSET IS DECLARED AFTER THE `padding` SHORTHAND, NOT BEFORE IT, AND
     * THAT ORDER IS THE WHOLE RULE. Written above the shorthand it computed to
     * 0px — the shorthand resets all four sides — so the declaration was present
     * and did nothing. Caught by reading the COMPUTED style on the live page;
     * the stylesheet said the right thing the entire time. */
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 .25rem;
    margin: 0 0 1.2rem;
    padding: 0 0 .35rem;
    padding-top: calc(.35rem + env(safe-area-inset-top));
    background: var(--fz-bg);
    border-bottom: 1px solid var(--fz-line);
    color: var(--fz-faint);
    font-size: .9rem;
}
.fz-nav a,
.fz-nav .fz-here {
    /* ⭐ THE TAP TARGET IS THE PADDING, not the word. "Us" and "Documents" are
     * very different widths and both have to be reachable by a thumb. */
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
    padding: 0 .5rem;
    border-radius: var(--fz-radius);
}
.fz-nav a { color: var(--fz-dim); }
.fz-nav a:hover { color: var(--fz-text); background: var(--fz-raised); }
/* ⚠ THE KEYBOARD RING MUST SURVIVE THE PADDING. An outline on an inline element
 * wraps the text; on this inline-flex box it wraps the target, which is the
 * thing being focused. */
.fz-nav a:focus-visible { outline: 2px solid var(--fz-text); outline-offset: -2px; }
.fz-nav .fz-here { color: var(--fz-text); font-weight: 600; }
.fz-nav .fz-svc {
    text-transform: uppercase; letter-spacing: .09em;
    color: var(--fz-faint); margin-right: .5rem;
}

/* ── buttons ──────────────────────────────────────────────────────────────
 * ⚠ ONE MODIFIER CONVENTION: .fz-btn.fz-primary, not .fz-btn-primary. The fleet
 * had both and they are not interchangeable — a page that mixes them silently
 * loses the variant. Picked the dot form because it composes: an element can be
 * primary AND small without inventing .fz-btn-primary-sm. */
.fz-btn {
    font: inherit; cursor: pointer;
    color: var(--fz-text); background: var(--fz-raised);
    border: 1px solid var(--fz-line); border-radius: var(--fz-radius);
    padding: .5rem 1rem;
    /* ⚠ 44px MINIMUM, ALWAYS. --fz-tap (88px) is the standard for a primary
     * target on a wall tablet; this is the floor below which nothing goes. */
    min-height: 44px;
}
.fz-btn:hover { border-color: var(--fz-accent); }
.fz-btn:disabled { cursor: default; color: var(--fz-faint); border-color: var(--fz-line); }
.fz-btn.fz-primary { background: var(--fz-accent); color: var(--fz-bg); border-color: var(--fz-accent); font-weight: 600; }
.fz-btn.fz-danger  { color: var(--fz-bad); border-color: var(--fz-bad); }
.fz-btn.fz-quiet   { background: none; border-color: transparent; color: var(--fz-faint); }
.fz-btn.fz-quiet:hover { color: var(--fz-text); border-color: var(--fz-line); }
/* ⚠ SMALL IS STILL 44px TALL. It is smaller in TYPE, never in target. */
.fz-btn.fz-sm { font-size: .8rem; padding: .3rem .7rem; }

/* ── state: colour + glyph + word, never colour alone ─────────────────────
 * 🚨 THE GLYPH AND THE WORD ARE NOT DECORATION. From ci.css:41, which already
 * had this right: "the page reads correctly for red/green colour blindness and
 * in a grey screenshot". Use as: <span class="fz-state fz-is-ok">✓ done</span> */
.fz-state { font-size: .78rem; }
.fz-state.fz-is-ok   { color: var(--fz-ok); }
.fz-state.fz-is-warn { color: var(--fz-warn); }
.fz-state.fz-is-bad  { color: var(--fz-bad); }
.fz-state.fz-is-off  { color: var(--fz-faint); }

/* ── surfaces ─────────────────────────────────────────────────────────────── */
.fz-card {
    background: var(--fz-surface);
    border: 1px solid var(--fz-line);
    border-radius: var(--fz-radius);
    padding: .8rem 1rem;
}

/* ⚠ ONE RADIUS FOR A BADGE. The fleet used 3px, 5px, 8px, 20px, 99px and 999px
 * for the same visual idea. Pill, because that is what six of the eleven chose
 * and it is the one that reads as a label rather than as a small button. */
.fz-badge {
    display: inline-block; border-radius: 999px;
    padding: .1rem .55rem; font-size: .72rem;
    border: 1px solid currentColor;
}

/* ── notice ───────────────────────────────────────────────────────────────
 * ⚠ ONE NAME. The fleet had .notice, .alert, .flash, .banner, .warn, .trouble and
 * .defects — and hexmap's .defects and foltzevents' .alert are the SAME TWO HEXES
 * under two names. The left rule carries the meaning so the text stays readable
 * on a coloured background, which the filled variants did not always manage. */
.fz-notice {
    padding: .6rem .8rem; margin: 1rem 0;
    background: var(--fz-surface);
    border-left: 3px solid var(--fz-line);
    border-radius: 0 var(--fz-radius) var(--fz-radius) 0;
    color: var(--fz-dim); font-size: .86rem;
}
.fz-notice.fz-is-ok   { border-left-color: var(--fz-ok); }
.fz-notice.fz-is-warn { border-left-color: var(--fz-warn); }
.fz-notice.fz-is-bad  { border-left-color: var(--fz-bad); color: var(--fz-text); }

/* ── empty state ──────────────────────────────────────────────────────────
 * ⭐ IT IS A PANEL WITH AN ACTION, NOT A SENTENCE. Five of the nine hand-rolled
 * empty states were prose only, and foltzconvo's shipped an add box that looked
 * live and did nothing. An empty state's job is to say what there is to DO. */
.fz-empty {
    padding: 1.25rem; text-align: center;
    background: var(--fz-surface);
    border: 1px dashed var(--fz-line);
    border-radius: var(--fz-radius);
}
.fz-empty p { margin: 0 0 .75rem; color: var(--fz-dim); }
.fz-empty .fz-note { margin: .75rem 0 0; font-size: .78rem; color: var(--fz-faint); }

/* ── table ────────────────────────────────────────────────────────────────
 * 🚨 THE WRAPPER IS THE POINT. Six services render tables and only foltzbuild
 * wraps one for overflow; foltzvpn's device table has no wrapper and overflows on
 * a phone. Always: <div class="fz-tablewrap"><table class="fz-table">. */
.fz-tablewrap { overflow-x: auto; }
.fz-table { width: 100%; border-collapse: collapse; }
.fz-table th {
    text-align: left; font-size: .72rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .08em;
    color: var(--fz-faint); padding: .4rem .6rem;
    border-bottom: 1px solid var(--fz-line);
}
.fz-table td { padding: .5rem .6rem; border-bottom: 1px solid var(--fz-line); }

/* ⚠ AN ID OR A TIMESTAMP IS FAINT AND TABULAR. Reading a column of ids where the
 * digits do not line up is the difference between scanning and checking. */
.fz-meta { font-size: .72rem; color: var(--fz-faint); }
.fz-mono { font-family: var(--fz-font-mono); font-variant-numeric: tabular-nums; }

@media (max-width: 34rem) {
    .fz-btn { padding: .55rem .9rem; }
    /* 🚨 BIGGER ON A PHONE, NOT SMALLER. This rule used to shrink the nav to
     * .78rem on the narrowest screens — the place with the least room to aim.
     * ⭐ AND THE SERVICE NAME GOES, not the links: on a 360px screen "MESSAGE"
     * costs a third of the bar to say something the person already knows. */
    .fz-nav { font-size: .95rem; }
    .fz-nav .fz-svc { display: none; }
}

/* ── the answer ───────────────────────────────────────────────────────────
 * ⭐⭐ EVERY PAGE IS FOR ONE QUESTION, AND MOST OF OURS BURY IT. foltzmeal's week
 * opened with a heading, three warning banners and seven identical rows, one of
 * which happened to be today — for a person whose question is "what is for
 * dinner tonight". foltzday has the same shape (is anything due), foltzbuild has
 * it (is main green), foltzconvo has it (what are we doing).
 *
 * ⇒ .fz-answer is the ONE thing a page exists to say, at a size nothing else on
 * the page competes with. Not a hero, not a banner — the answer.
 *
 * 🚨 THE ACCENT IS SPENT HERE AND NOWHERE ELSE ON THE PAGE. One colour, one job:
 * if --fz-accent appears twice on a screen, one of them is wrong. That is what
 * makes an unaccented row read as "not the one you asked about" rather than
 * "styled slightly differently". */
.fz-answer {
    background: var(--fz-surface);
    border: 1px solid var(--fz-line);
    border-left: 3px solid var(--fz-accent);
    border-radius: var(--fz-radius);
    padding: 1.1rem 1.2rem 1.2rem;
    margin: 0 0 1.6rem;
}
/* ⚠ THE LABEL IS SENTENCE CASE AND IT IS NOT AN EYEBROW. A tracked-out all-caps
 * word above a heading is decoration pretending to be structure; this is the
 * date, which is information. */
.fz-answer .fz-answer-when {
    margin: 0 0 .35rem; font-size: .82rem; color: var(--fz-dim);
}
.fz-answer .fz-answer-what {
    margin: 0; font-weight: 600; line-height: 1.15;
    /* ⭐ ONE ENORMOUS JUMP, NOT SIX POLITE ONES. No display face is loaded: this
     * page has to open in a shop on bad signal, and a font download is the
     * accessory to remove. The scale carries the personality instead. */
    font-size: clamp(1.7rem, 1.1rem + 3.2vw, 2.6rem);
    letter-spacing: -0.015em;
}
.fz-answer .fz-answer-what a { text-decoration-color: var(--fz-line); }
.fz-answer .fz-answer-sub {
    margin: .5rem 0 0; color: var(--fz-dim); font-size: .9rem;
}
/* ⚠ AN UNANSWERED QUESTION IS STILL AN ANSWER, and it must not look broken.
 * "Nobody has decided" is a real state that invites an action; a blank space is
 * a page that failed to load. */
.fz-answer.fz-is-open .fz-answer-what { color: var(--fz-dim); font-weight: 500; }

/* ── ruled rows ───────────────────────────────────────────────────────────
 * ⚠ DELIBERATELY NOT CARDS. Seven identical rounded boxes ask the eye to rank
 * them and give it nothing to rank on — the card kit is what a page reaches for
 * when it has not decided what matters. A rule is cheaper, quieter, and reads as
 * a list of the same KIND of thing, which is what a week is.
 *
 * ⭐ IT IS ALSO THE FIX FOR A REAL BREAKAGE. The week laid days out as flex rows
 * with a fixed-width label and controls on the end; adding two forms squeezed the
 * meal name to about 40px and it wrapped one word per line. Rows wrap to a block
 * on a narrow screen instead of compressing the thing you came to read. */
.fz-rows { border-top: 1px solid var(--fz-line); }
.fz-row {
    display: flex; gap: .9rem; align-items: baseline;
    padding: .7rem .2rem;
    border-bottom: 1px solid var(--fz-line);
}
.fz-row-key { flex: 0 0 5.5rem; color: var(--fz-dim); font-size: .88rem; }
.fz-row-main { flex: 1 1 auto; min-width: 0; }
/* ⚠ 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); }

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

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