/* fox-tokens:light:start - generated by cdn/generate-fox-styles.js from
   cdn/fox-tokens.js. Hand-edit fox-tokens.js, not the values below. */
:root {
    color-scheme: light;

    /* --- Polarfuchs Aesthetic Palette --- */
    --ink: #26343f; /* Deep polar night navy */
    --ink-soft: #4a5a66; /* Frosty slate for secondary text */
    --snow: #fdfdfb; /* Card face / main panel background */
    --dusk: #e8eef2; /* Screen layout backdrop */
    --hairline: #b9c6ce; /* Structured divider borders */

    /* --- Functional Category Signals (AAA Accessible) --- */
    --ice: #9fc4d6; /* Calm / Status */
    --moss: #8fae9b; /* Bedürfnis / Need */
    --dawn: #c96b5e; /* Wichtig / Urgent Crisis */
    /* Added for mtools/bob-tree's 7-way register category encoding, which
       needs more distinct accent hues than the 3-signal set above provides.
       Always paired with a glyph + printed category name, never hue alone. */
    --birch: #c9b38a; /* Bark tan */
    --slate: #7c8fa3; /* Stone blue-grey */
    --aurora: #a68fc9; /* Aurora violet */

    /* --- Overlay/shadow tokens (RGB triples, not resolved rgba(), so each
       call site can keep its own alpha while hue/lightness flips as one
       unit with the theme) --- */
    --shadow-ink: 38, 52, 63; /* box-shadows: stays black-based in dark mode too, see below */
    --overlay-glass: 253, 253, 251; /* glassmorphism panel tint, tracks --snow/--dusk */
    --tint-hairline: 185, 198, 206; /* --hairline tinge, used for subtle fills */
    --tint-ice: 159, 196, 214; /* --ice tinge, used for focus rings/notes */
    --tint-dawn: 201, 107, 94; /* --dawn tinge, used for warning-banner fills */
    --tint-moss: 143, 174, 155; /* --moss tinge, used for the dialog success icon's fill */
    --btn-primary-hover: #354756; /* .fox-btn-primary hover step */
    /* The focus indicator, and NOT --ice, which every focus rule here used to
       reach for. Against --snow, --ice measures 1.8 to 1 - a ring a sighted
       keyboard user cannot find. This one clears 3 to 1 against --snow,
       --dusk and --ink, the last because the ring's inner edge sits on
       .fox-btn-primary's filled face. Re-pin it to var(--ice) if you want
       the old look back. */
    --focus-ring: #3f8aab;

    /* Aliases resolved once here so .fox-toolbar/.fox-settings can re-pin
       to them below without duplicating literal colors per theme (see
       those selectors' comments for why the re-pin exists at all). */
    --fox-ink: var(--ink);
    --fox-ink-soft: var(--ink-soft);
    --fox-hairline: var(--hairline);
    --fox-ice: var(--ice);
    --fox-focus-ring: var(--focus-ring);

    /* --- Typography System --- */
    --font-sans: "Atkinson Hyperlegible", system-ui, -apple-system, sans-serif;
    --font-mono: "Atkinson Hyperlegible Mono", "SF Mono", "Cascadia Mono", Consolas, monospace;
    --font-serif: "Sylexiad Serif Medium", Georgia, "Times New Roman", serif;

    /* Opt-in alternate faces. Nothing in this stylesheet points at them:
       they exist so a consumer can re-pin --font-sans/--font-serif at one,
       either with the [data-font] attribute below or by hand. */
    --font-luciole: "Luciole", "Atkinson Hyperlegible", system-ui, sans-serif;
    --font-lexend: "Lexend", "Atkinson Hyperlegible", system-ui, sans-serif;
    --font-opendyslexic: "OpenDyslexic", "Atkinson Hyperlegible", system-ui, sans-serif;

    /* --font-heading, the face h1-h6 use, is deliberately NOT declared here.
       It is read as var(--font-heading, var(--font-sans)) further down, so
       leaving it unset is what makes headings follow whatever face is in
       play. Declaring it here would resolve that fallback once, against
       :root's --font-sans, and a subtree that re-pinned --font-sans would
       then move its body text without its headings. See the Headings
       section below. */

    /* --- Reading metrics (WCAG 2.1 SC 1.4.12, Text Spacing) ---
       The dimensions a reader controls, not the product. 1.4.12 says content
       must stay usable at line-height 1.5, letter-spacing 0.12em,
       word-spacing 0.16em and paragraph spacing 2em - those are what a
       setting has to be able to reach, so the floor is the default here and
       your settings UI raises it. Every value below is what this stylesheet
       already rendered, so nothing moved when they were extracted.

       Override them wherever you like - :root, a reading pane, or inline on
       <html> from a preferences panel (that is what fox.js's
       initReadingPrefs() does). --paragraph-spacing is read by .fox-prose
       and by nothing else, because the reset below zeroes every margin:
       applying it globally would retypeset page content this stylesheet
       does not own. Opt in per block, the same way you opt into a face. */
    --line-height: 1.5;
    --letter-spacing: 0em;
    --word-spacing: 0em;
    --paragraph-spacing: 1em;
    --measure: 65ch; /* line LENGTH, not a container width */

    /* --- Spacing & Layout Constraints --- */
    --radius-ui: 8px;
    --radius-card: 10px;

    /* Focus indicator width. The OFFSET is deliberately not a token: it is
       per-component (outset on a button, inset on .fox-settings-header,
       which its parent clips), the same way padding is. */
    --focus-width: 2px;

    /* --- Motion ---
       The state, not the lever. --motion reads "full" or "reduced" so a
       script, a canvas loop or an SVG animation can branch on the same
       answer the stylesheet used (getComputedStyle(root).getPropertyValue),
       and --motion-scale is that answer as a multiplier you can fold into a
       duration: calc(200ms * var(--motion-scale)).
       Both are SET by [data-motion] and prefers-reduced-motion below - a
       subtree that re-pins --motion on its own changes its reported state
       without suppressing anything, which is why the suppression rules key
       off the attribute instead. */
    --motion: full;
    --motion-scale: 1;
}
/* fox-tokens:light:end */

/* --- Self-hosted Typography ---
   Atkinson Hyperlegible (SIL OFL), fetched from Google Fonts' own CDN and
   re-hosted here so --font-sans doesn't depend on a third-party origin at
   all (previously this was referenced by name only, with tools linking
   fonts.googleapis.com directly - see cdn/fox-usage.md). woff2 first for
   browsers, ttf as the fallback format Qt's QFontDatabase/QML's FontLoader
   need instead. Relative URLs so this works whether fox.css is served from
   the real CDN or a local checkout (see masyu/tools/verify-browser.mjs). */
@font-face {
    font-family: "Atkinson Hyperlegible";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("./atkinson-hyperlegible-regular.woff2") format("woff2"),
         url("./atkinson-hyperlegible-regular.ttf") format("truetype");
}
@font-face {
    font-family: "Atkinson Hyperlegible";
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url("./atkinson-hyperlegible-italic.woff2") format("woff2"),
         url("./atkinson-hyperlegible-italic.ttf") format("truetype");
}
@font-face {
    font-family: "Atkinson Hyperlegible";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("./atkinson-hyperlegible-bold.woff2") format("woff2"),
         url("./atkinson-hyperlegible-bold.ttf") format("truetype");
}
@font-face {
    font-family: "Atkinson Hyperlegible";
    font-style: italic;
    font-weight: 700;
    font-display: swap;
    src: url("./atkinson-hyperlegible-bolditalic.woff2") format("woff2"),
         url("./atkinson-hyperlegible-bolditalic.ttf") format("truetype");
}

/* Atkinson Hyperlegible Mono (SIL OFL, see cdn/OFL-MONO.txt), same
   Braille Institute lineage and re-hosting rationale as the sans block
   above - fetched from the googlefonts/atkinson-hyperlegible-next-mono
   repo's own static instances. */
@font-face {
    font-family: "Atkinson Hyperlegible Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("./atkinson-hyperlegible-mono-regular.woff2") format("woff2"),
         url("./atkinson-hyperlegible-mono-regular.ttf") format("truetype");
}
@font-face {
    font-family: "Atkinson Hyperlegible Mono";
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url("./atkinson-hyperlegible-mono-italic.woff2") format("woff2"),
         url("./atkinson-hyperlegible-mono-italic.ttf") format("truetype");
}
@font-face {
    font-family: "Atkinson Hyperlegible Mono";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("./atkinson-hyperlegible-mono-bold.woff2") format("woff2"),
         url("./atkinson-hyperlegible-mono-bold.ttf") format("truetype");
}
@font-face {
    font-family: "Atkinson Hyperlegible Mono";
    font-style: italic;
    font-weight: 700;
    font-display: swap;
    src: url("./atkinson-hyperlegible-mono-bolditalic.woff2") format("woff2"),
         url("./atkinson-hyperlegible-mono-bolditalic.ttf") format("truetype");
}

/* Sylexiad Serif Medium, self-hosted under Robert Hillier's own EULA (see
   cdn/LICENSE-SYLEXIAD.txt) - not SIL OFL like the two font families above.
   Free for commercial/personal/educational use and web embedding, but the
   files must not be independently redistributable and the credit line
   below is a license term, not a courtesy: "Sylexiad Serif Medium" by
   Robert Hillier (sylexiad.com), used under EULA. */
@font-face {
    font-family: "Sylexiad Serif Medium";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("./sylexiad-serif-medium-regular.woff2") format("woff2"),
         url("./sylexiad-serif-medium-regular.ttf") format("truetype");
}
@font-face {
    font-family: "Sylexiad Serif Medium";
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url("./sylexiad-serif-medium-italic.woff2") format("woff2"),
         url("./sylexiad-serif-medium-italic.ttf") format("truetype");
}
@font-face {
    font-family: "Sylexiad Serif Medium";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("./sylexiad-serif-medium-bold.woff2") format("woff2"),
         url("./sylexiad-serif-medium-bold.ttf") format("truetype");
}
@font-face {
    font-family: "Sylexiad Serif Medium";
    font-style: italic;
    font-weight: 700;
    font-display: swap;
    src: url("./sylexiad-serif-medium-bolditalic.woff2") format("woff2"),
         url("./sylexiad-serif-medium-bolditalic.ttf") format("truetype");
}

/* --- Opt-in alternate faces ---
   Self-hosted on the same terms as the three families above, but with no
   default target: nothing here or in any other channel points at them until
   a consumer does. Which face reads best is a property of the reader, not of
   the product, so this belongs in an app's (ideally a user's) preferences
   rather than in the design system's defaults. See the [data-font] block at
   the end of this section for the switch, and cdn/fox-usage.md for the
   Qt/QML/Godot equivalents.

   Luciole, by Laurent Bourcellier and Jonathan Perez for the CTRDV, drawn
   for low-vision readers - large x-height, and 0/O, l/I/1 kept apart.
   CC BY 4.0 (cdn/LICENSE-LUCIOLE.txt): unlike SIL OFL, attribution is a
   licence term, so ship the credit line with anything that embeds it -
   "Luciole" by Laurent Bourcellier & Jonathan Perez, CC BY 4.0. */
@font-face {
    font-family: "Luciole";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("./luciole-regular.woff2") format("woff2"),
         url("./luciole-regular.ttf") format("truetype");
}
@font-face {
    font-family: "Luciole";
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url("./luciole-italic.woff2") format("woff2"),
         url("./luciole-italic.ttf") format("truetype");
}
@font-face {
    font-family: "Luciole";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("./luciole-bold.woff2") format("woff2"),
         url("./luciole-bold.ttf") format("truetype");
}
@font-face {
    font-family: "Luciole";
    font-style: italic;
    font-weight: 700;
    font-display: swap;
    src: url("./luciole-bolditalic.woff2") format("woff2"),
         url("./luciole-bolditalic.ttf") format("truetype");
}

/* Lexend, by Bonnie Shaver-Troup and Thomas Jockin, drawn to reduce visual
   stress and raise reading proficiency (SIL OFL, cdn/OFL-LEXEND.txt). It is
   a weight-axis family with no italics upstream, so only 400 and 700 are
   real here and italic text is synthesised by the browser - see
   cdn/fox-usage.md. The variable face comes first so the two static
   instances below win for 400/700 where both are supported, leaving the
   variable file to cover only the weights the statics don't. */
@font-face {
    font-family: "Lexend";
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url("./lexend-variable.woff2") format("woff2-variations");
}
@font-face {
    font-family: "Lexend";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("./lexend-regular.woff2") format("woff2"),
         url("./lexend-regular.ttf") format("truetype");
}
@font-face {
    font-family: "Lexend";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("./lexend-bold.woff2") format("woff2"),
         url("./lexend-bold.ttf") format("truetype");
}

/* OpenDyslexic 3, by Abbie Gonzalez - weighted letter bottoms to anchor
   glyphs against the rotation and swapping dyslexic readers report. The sans
   counterpart to the Sylexiad serif above (SIL OFL, cdn/OFL-OPENDYSLEXIC.txt).
   Shipped as .otf rather than .ttf: upstream compiles CFF outlines and
   publishes no TrueType build, and re-rendering someone else's curves to
   ship a different file extension is not an improvement. Both formats load
   everywhere this design system goes. */
@font-face {
    font-family: "OpenDyslexic";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("./opendyslexic-regular.woff2") format("woff2"),
         url("./opendyslexic-regular.otf") format("opentype");
}
@font-face {
    font-family: "OpenDyslexic";
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url("./opendyslexic-italic.woff2") format("woff2"),
         url("./opendyslexic-italic.otf") format("opentype");
}
@font-face {
    font-family: "OpenDyslexic";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("./opendyslexic-bold.woff2") format("woff2"),
         url("./opendyslexic-bold.otf") format("opentype");
}
@font-face {
    font-family: "OpenDyslexic";
    font-style: italic;
    font-weight: 700;
    font-display: swap;
    src: url("./opendyslexic-bolditalic.woff2") format("woff2"),
         url("./opendyslexic-bolditalic.otf") format("opentype");
}

/* The switch. Set data-font on <html> and body text plus h1-h6 both move,
   the same one-attribute shape as data-theme="dark" further down. Kept out
   of the generated token regions on purpose - these are rules, not values.

   Unqualified rather than :root[data-font], so it also works on any subtree -
   a reading pane can carry one face while the chrome around it keeps the
   default. That leaves it tied on specificity with the :root block above and
   winning on source order, which is what you want at both ends: it beats the
   defaults, and a consumer's own later `--font-sans` still beats it.

   The font-family declaration is not redundant with the two custom
   properties. Redefining --font-sans on a <section> does nothing for the
   text inside it: a <p> has no font-family of its own, so it inherits the
   *resolved* value from <body>, where --font-sans still meant Atkinson. Only
   elements that declare font-family: var(--font-sans) themselves - h1-h6,
   .fox-toolbar and friends - would follow. Re-declaring it here gives the
   subtree a resolved value of its own to hand down, so plain prose moves too.

   --font-mono is deliberately left alone: none of the three is a monospace
   family, so repointing it would silently drop code and tabular figures onto
   a proportional face. Anything under here that wants monospace asks for it
   by name, and still gets it. */
[data-font="luciole"] {
    --font-sans: var(--font-luciole);
    --font-serif: var(--font-luciole);
    font-family: var(--font-sans);
}
[data-font="lexend"] {
    --font-sans: var(--font-lexend);
    --font-serif: var(--font-lexend);
    font-family: var(--font-sans);
}
[data-font="opendyslexic"] {
    --font-sans: var(--font-opendyslexic);
    --font-serif: var(--font-opendyslexic);
    font-family: var(--font-sans);
}

/* The heading face, as an attribute - the same one-attribute shape as
   [data-font] above, and an attribute rather than something a script writes
   into --font-heading directly for two reasons. The mapping from a name to a
   stack belongs in the stylesheet, where a consumer can add a row of their
   own next to these. And an attribute value is only ever compared against a
   literal in a selector, so a name out of localStorage never reaches a style
   property - which is what lets fox.js's allow-list be the whole check.

   Setting --font-heading yourself still works and still wins; this is the
   shorthand, not a replacement.

   No font-family re-declaration here, unlike [data-font]. h1-h6 declare
   font-family: var(--font-heading, ...) themselves, so there is no
   inherited-resolved-value problem to work around - and --font-heading stays
   undeclared at :root, which is the whole reason that fallback resolves at
   the heading. See the Headings section below.

   Note that --font-serif is itself repointed by [data-font] on the same
   element, so on <html data-font="opendyslexic" data-font-heading="serif">
   the headings come out OpenDyslexic, not Sylexiad. That is deliberate: a
   reader who switched because they cannot read the default should not get
   headings they still cannot read. Pick an alternate by name instead - those
   custom properties are never repointed. */
[data-font-heading="serif"]        { --font-heading: var(--font-serif); }
[data-font-heading="luciole"]      { --font-heading: var(--font-luciole); }
[data-font-heading="lexend"]       { --font-heading: var(--font-lexend); }
[data-font-heading="opendyslexic"] { --font-heading: var(--font-opendyslexic); }

/* --- Dark palette ---
   Applies automatically via OS preference unless the page opts out with
   data-theme="light", and can be forced on with data-theme="dark"
   regardless of OS preference. Mirrors the pattern already used in
   mtools/puzzle/report-template.html so the whole repo stays consistent.
   Functional signal hues are lifted (not mirrored) — a literal luminance
   mirror of these already-midtone accents goes muddy on a dark backdrop
   and breaks the "AAA Accessible" intent documented above. --shadow-ink
   stays black-based in both themes since a shadow represents occluded
   light, not a themed surface. */
@media (prefers-color-scheme: dark) {
    /* fox-tokens:media-dark:start */
    :root:where(:not([data-theme="light"])) {
        color-scheme: dark;
        --ink: #e7edf1;
        --ink-soft: #b6c4cd;
        --snow: #1e262d;
        --dusk: #12171b;
        --hairline: #3a4750;
        --ice: #8fc9e6;
        --moss: #9fc4ab;
        --dawn: #e08a7d;
        --birch: #dcc9a3;
        --slate: #9db0c4;
        --aurora: #bfa8e0;
        --shadow-ink: 0, 0, 0;
        --overlay-glass: 20, 26, 31;
        --tint-hairline: 58, 71, 80;
        --tint-ice: 143, 201, 230;
        --tint-dawn: 224, 138, 125;
        --tint-moss: 159, 196, 171;
        --btn-primary-hover: #c3d2db;
    --focus-ring: #4488aa;
    }
    /* fox-tokens:media-dark:end */
}
/* fox-tokens:explicit-dark:start */
:root[data-theme="dark"] {
    color-scheme: dark;
    --ink: #e7edf1;
    --ink-soft: #b6c4cd;
    --snow: #1e262d;
    --dusk: #12171b;
    --hairline: #3a4750;
    --ice: #8fc9e6;
    --moss: #9fc4ab;
    --dawn: #e08a7d;
    --birch: #dcc9a3;
    --slate: #9db0c4;
    --aurora: #bfa8e0;
    --shadow-ink: 0, 0, 0;
    --overlay-glass: 20, 26, 31;
    --tint-hairline: 58, 71, 80;
    --tint-ice: 143, 201, 230;
    /* Was missing here (only the @media dark block above had it) even
       though .fox-banner--warning depends on it - forced dark mode via
       data-theme="dark" was getting the light-mode tint on a dark
       backdrop. Filled in to match, while adding fox-tokens generation. */
    --tint-dawn: 224, 138, 125;
    --tint-moss: 159, 196, 171;
    --btn-primary-hover: #c3d2db;
    --focus-ring: #4488aa;
}
/* fox-tokens:explicit-dark:end */

/* --- Motion ---
   Two triggers, one behaviour, the same shape as data-theme above: the OS
   preference with an explicit opt-out, then an explicit opt-in.

   The opt-out is what makes this more than the one-line media query it
   replaces. prefers-reduced-motion is a system-wide setting, and a reader
   who turned it on for one hostile app - or inherited it from a locked-down
   device profile - previously had no way to say "not here". data-motion
   ="full" is that way. It needs no rules of its own: the :where() guard
   below simply stops matching, exactly as data-theme="light" does.

   :where() so the guard costs no specificity, and a consumer's own
   animation rule still wins the way it would against the bare media query.

   Root-scoped on purpose, unlike [data-font], which works on any subtree.
   Which typeface reads best can differ between a reading pane and the chrome
   around it; how much movement is safe cannot - vestibular disorders and
   photosensitivity are properties of the person looking at the screen, not
   of a region of it.

   0.01ms rather than `none`: a transition that is removed never fires
   transitionend, and code that waits for one then hangs until its own
   timeout. A near-instant transition is visually identical and still
   completes. (fox.js's initSettings() is one such listener, which is why
   the old rule needed the 350ms fallback timer it still carries.)

   animation-iteration-count is capped as well as the duration, so an
   infinite spinner stops spinning rather than spinning infinitely fast.
   scroll-behavior sits on the :root rules and not the * ones because
   ":root ... *" is a descendant selector and never matches <html> itself,
   which is the element smooth scrolling is set on. */
@media (prefers-reduced-motion: reduce) {
    :root:where(:not([data-motion="full"])) {
        --motion: reduced;
        --motion-scale: 0;
        scroll-behavior: auto !important;
    }

    :root:where(:not([data-motion="full"])) *,
    :root:where(:not([data-motion="full"])) *::before,
    :root:where(:not([data-motion="full"])) *::after,
    :root:where(:not([data-motion="full"])) *::backdrop {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

:root[data-motion="reduced"] {
    --motion: reduced;
    --motion-scale: 0;
    scroll-behavior: auto !important;
}

/* ::backdrop is its own pseudo-element, not reached by the *, *::before,
   *::after sweep above - .fox-dialog is the first ::backdrop user in this
   file, and without this line its entrance/exit fade would keep animating
   under prefers-reduced-motion / data-motion="reduced". */
:root[data-motion="reduced"] *,
:root[data-motion="reduced"] *::before,
:root[data-motion="reduced"] *::after,
:root[data-motion="reduced"] *::backdrop {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
}

/* --- Global Reset Blueprint --- */
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}
body {
    font-family: var(--font-sans);
    background: var(--dusk);
    color: var(--ink);
    line-height: var(--line-height);
    letter-spacing: var(--letter-spacing);
    word-spacing: var(--word-spacing);
}

/* --- Headings (h1-h6) ---
   Headings take the selected face and separate themselves by size and weight
   instead, matching the foxClass="h1".."h6" scale in
   fox-light.qss/fox-dark.qss and Godot's HeaderLabel variation. The pt scale
   over there (22/18/15/13/11/9) is carried across as rem here.

   They used to be pinned to Sylexiad Serif Medium, which put a second,
   unrelated face in the one place a reader could not override it: someone
   who switched to OpenDyslexic because they cannot read the default still
   got a serif for every heading. The pairing is still one line away, it is
   just opt-in now:

       :root { --font-heading: var(--font-serif); }

   Any stack works there - one of the [data-font] alternates, or a face of
   your own. Setting it moves headings only; body text stays put.

   [data-font-heading] above is the same thing by name, for the case where
   the answer comes from a reader rather than from you: data-font-heading
   ="serif" | "luciole" | "lexend" | "opendyslexic", absent meaning "follow
   the body face". It is a shorthand for exactly the line above, so the two
   compose the ordinary way and yours wins if it comes later.

   The fallback is doing real work: --font-heading is left undeclared so this
   var() resolves at the heading element, against whatever --font-sans means
   *there*. Declared at :root it would resolve there instead, and freeze. */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-heading, var(--font-sans));
    font-weight: 700;
    color: var(--ink);
}
h1 { font-size: 2rem; }
h2 { font-size: 1.65rem; }
h3 { font-size: 1.375rem; }
h4 { font-size: 1.185rem; }
h5 { font-size: 1rem; }
h6 { font-size: 0.815rem; }

/* --- Toolbar (action buttons kept outside the collapsible settings box) ---
   Pins its own copy of the tokens it needs (same guard as .fox-settings
   below), so a page that redefines --ink/--ink-soft/--sans for its own
   content (e.g. puzzle.html's monochrome palette) can't leak into this
   shared chrome. */
.fox-toolbar {
    --ink: var(--fox-ink);
    --ink-soft: var(--fox-ink-soft);
    --hairline: var(--fox-hairline);
    --ice: var(--fox-ice);
    --focus-ring: var(--fox-focus-ring);
    font-family: var(--font-sans);
    max-width: 760px;
    margin: 24px auto 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* --- Collapsible Settings Section --- */
.fox-settings {
    --ink: var(--fox-ink);
    --ink-soft: var(--fox-ink-soft);
    --hairline: var(--fox-hairline);
    --focus-ring: var(--fox-focus-ring);
    font-family: var(--font-sans);
    max-width: 760px;
    margin: 24px auto 16px;
    background: rgba(var(--overlay-glass), 0.85); /* Glassmorphism snow */
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(var(--tint-hairline), 0.5); /* Hairline accent */
    border-radius: var(--radius-card);
    box-shadow: 0 8px 30px rgba(var(--shadow-ink), 0.08), 0 2px 6px rgba(var(--shadow-ink), 0.04);
    overflow: hidden;
    transition: box-shadow 0.3s ease, border-color 0.3s ease;
}

.fox-settings:hover {
    border-color: rgba(var(--tint-hairline), 0.8);
    box-shadow: 0 12px 40px rgba(var(--shadow-ink), 0.12), 0 4px 10px rgba(var(--shadow-ink), 0.06);
}

.fox-settings-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px 24px;
    cursor: pointer;
    list-style: none; /* Hide default chevron */
    user-select: none;
    background: rgba(var(--overlay-glass), 0.4);
    transition: background-color 0.2s ease;
}

.fox-settings-header::-webkit-details-marker {
    display: none; /* Safari default marker reset */
}

.fox-settings-header:hover {
    background: rgba(var(--overlay-glass), 0.8);
}

/* Inset, unlike every other ring here: .fox-settings sets overflow: hidden
   to clip its rounded corners, so an outset ring on the summary is cropped
   away on three sides. */
.fox-settings-header:focus-visible {
    outline: var(--focus-width) solid var(--focus-ring);
    outline-offset: -2px;
}

.fox-settings-title-wrap {
    display: flex;
    align-items: center;
    gap: 12px;
}

.fox-settings-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--ink);
}

.fox-settings-title {
    font-family: var(--font-sans);
    font-size: 1.35rem;
    font-weight: 700;
    /* Design tracking, not reader spacing - this and the four other
       letter-spacing literals below are deliberate tracking on titles and
       uppercase labels, so they are composed WITH --letter-spacing rather
       than replaced by it. A reader who widens their spacing still widens
       these; the typographic intent survives underneath. */
    letter-spacing: calc(0.01em + var(--letter-spacing));
    color: var(--ink);
}

.fox-settings-chevron {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    color: var(--ink-soft);
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Rotate chevron when open */
.fox-settings[open] .fox-settings-chevron {
    transform: rotate(180deg);
}

.fox-settings-content {
    padding: 0 24px 24px 24px;
    border-top: 1px solid rgba(var(--tint-hairline), 0.3);
    background: rgba(var(--overlay-glass), 0.2);
}

.fox-settings-desc {
    margin: 16px 0 20px;
    font-size: 0.9rem;
    line-height: var(--line-height);
    color: var(--ink-soft);
    max-width: var(--measure);
}

/* --- Prose ---
   The one thing in this stylesheet that reads --paragraph-spacing. The token
   shipped declared and pointed at nothing because the global reset zeroes
   every margin: applying it to bare <p> would retypeset page content the
   design system does not own, and 1em is not what any existing consumer
   renders today - 0 is. So it is opt-in, and nothing moved for anyone.

   That gives WCAG 2.1 SC 1.4.12's fourth metric something to act on: a
   reader can push paragraph spacing to the 2em the criterion names and see
   it happen. The owl selector rather than `p + p` because a list or a
   blockquote between two paragraphs is still a paragraph break, and
   --measure because prose is the thing line LENGTH was extracted for. */
.fox-prose {
    max-width: var(--measure);
}

.fox-prose > * + * {
    margin-block-start: var(--paragraph-spacing);
}

/* --- Form Controls & Grid --- */
.fox-cfg-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 12px 16px;
    margin-bottom: 16px;
}

.fox-field-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.fox-label {
    display: block;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: calc(0.08em + var(--letter-spacing));
    color: var(--ink-soft);
}

.fox-input, .fox-select, .fox-textarea {
    width: 100%;
    font-family: var(--font-sans);
    font-size: 0.9rem;
    padding: 8px 12px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-ui);
    background: var(--snow);
    color: var(--ink);
    transition: border-color 0.2s ease, box-shadow 0.2s ease, outline 0.2s ease;
}

/* The border recolour is the mouse affordance and stays on :focus, because
   a <select> does not match :focus-visible on click in every engine and
   losing all feedback there would be a regression. */
.fox-input:focus, .fox-select:focus, .fox-textarea:focus {
    border-color: var(--focus-ring);
}

/* The indicator proper. This used to be `outline: none` plus a box-shadow
   ring, which forced-colors mode erases outright - so the one display mode
   that exists for people who need high contrast was the one with no focus
   indicator at all. An outline is honoured and recoloured by the OS there,
   which is why the shadow is now decoration layered under it rather than
   the thing carrying the meaning. */
.fox-input:focus-visible, .fox-select:focus-visible, .fox-textarea:focus-visible {
    outline: var(--focus-width) solid var(--focus-ring);
    outline-offset: 2px;
    box-shadow: 0 0 0 3px rgba(var(--tint-ice), 0.3);
}

/* --- Grouped Field Boxes (bundle related fields into one box) --- */
.fox-group-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 18px 16px;
    margin-bottom: 16px;
}

.fox-group-box {
    background: rgba(var(--tint-hairline), 0.18);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-ui);
    padding: 12px 14px 14px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    box-shadow: 0 1px 2px rgba(var(--shadow-ink), 0.06);
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.fox-group-box:hover {
    border-color: var(--ink-soft);
    box-shadow: 0 4px 10px rgba(var(--shadow-ink), 0.1);
}

.fox-group-title {
    font-size: 0.78rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: calc(0.07em + var(--letter-spacing));
    color: var(--ink);
    padding-bottom: 8px;
    border-bottom: 1px solid var(--hairline);
}

.fox-group-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
    gap: 10px 12px;
}

/* --- Buttons System --- */
.fox-btn-row {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    margin-top: 20px;
}

.fox-btn {
    font-family: var(--font-sans);
    font-size: 0.875rem;
    font-weight: 700;
    padding: 10px 20px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-ui);
    background: var(--snow);
    color: var(--ink);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    user-select: none;
    transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275),
                background-color 0.2s ease,
                border-color 0.2s ease,
                box-shadow 0.2s ease;
}

.fox-btn:hover {
    background: rgba(var(--tint-hairline), 0.1);
    border-color: var(--ink-soft);
    transform: translateY(-1px);
    box-shadow: 0 4px 8px rgba(var(--shadow-ink), 0.05);
}

.fox-btn:active {
    transform: translateY(0);
    box-shadow: none;
}

.fox-btn-primary {
    background: var(--ink);
    color: var(--snow);
    border-color: var(--ink);
}

.fox-btn-primary:hover {
    background: var(--btn-primary-hover);
    border-color: var(--btn-primary-hover);
    color: var(--snow);
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(var(--shadow-ink), 0.15);
}

.fox-btn-primary:active {
    background: var(--ink);
}

.fox-btn:focus-visible {
    outline: var(--focus-width) solid var(--focus-ring);
    outline-offset: 2px;
}

/* --- Hit area (WCAG 2.5.5 Target Size) ---
   .fox-btn's own box measures 39px tall: 17px of text, plus 20px of padding,
   plus 2px of border. Note the 17 - the UA's `font` shorthand on a form
   control resets line-height, so a button does NOT inherit body's 1.5 and
   this is 14px at `normal`. Five pixels under the 44x44 floor, and arriving
   at it by arithmetic rather than by measuring gets you the wrong number.

   The pseudo-element grows the HIT region without touching layout or paint.
   It has no background and is absolutely positioned, so it is out of flow
   and invisible; being a child of the button, a pointer landing on it still
   activates the button. Growing the padding instead would have moved every
   consumer's layout to fix a problem nobody can see.

   Buttons only. <input> and <select> are replaced elements and have no
   ::before/::after at all, so the same trick is unavailable there and the
   requirement is documented in fox-usage.md instead of silently changing
   the height of every form in every consuming app. */
.fox-btn { position: relative; }

.fox-btn::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    height: 100%;
    min-width: 44px;
    min-height: 44px;
}

/* Layered on .fox-btn like .fox-btn-primary; wired up by
   PolarfuchsSystem.initTheme() via the [data-fox-theme-toggle] hook. */
.fox-theme-toggle {
    min-width: 128px; /* avoid layout shift between "Dark mode"/"Light mode" labels */
}

/* --- Totals & Info elements --- */
.fox-totals {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--ink-soft);
    margin: 16px 0 8px;
    padding: 6px 10px;
    background: rgba(var(--tint-hairline), 0.15);
    border-radius: var(--radius-ui);
    display: inline-block;
}

.fox-print-note {
    font-size: 0.8rem;
    line-height: var(--line-height);
    background: rgba(var(--tint-ice), 0.1);
    border-left: 3px solid var(--ice);
    padding: 12px 16px;
    border-radius: 0 var(--radius-ui) var(--radius-ui) 0;
    margin-top: 20px;
    color: var(--ink-soft);
}

.fox-print-note ol, .fox-print-note ul {
    margin-left: 20px;
    margin-top: 8px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.fox-legend {
    font-size: 0.8rem;
    line-height: var(--line-height);
    background: rgba(var(--tint-hairline), 0.1);
    border-left: 3px solid var(--hairline);
    padding: 12px 16px;
    border-radius: 0 var(--radius-ui) var(--radius-ui) 0;
    margin-top: 16px;
    color: var(--ink);
}

.fox-legend-detail {
    color: var(--ink-soft);
    font-weight: normal;
    margin-left: 8px;
}

.fox-print-note > strong:first-of-type,
.fox-legend > strong:first-of-type {
    display: block;
    font-size: 0.85rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: calc(0.06em + var(--letter-spacing));
    margin-bottom: 8px;
    color: var(--ink);
}

/* --- Banners (non-blocking advisory notices) ---
   Same left-border-callout language as .fox-print-note/.fox-legend, but
   meant for dismissible/conditional messages (staleness, reprint,
   overflow) rather than static instructional copy. */
.fox-banner {
    font-size: 0.85rem;
    line-height: var(--line-height);
    display: flex;
    align-items: flex-start;
    gap: 10px;
    background: rgba(var(--tint-hairline), 0.1);
    border-left: 3px solid var(--hairline);
    padding: 12px 16px;
    border-radius: 0 var(--radius-ui) var(--radius-ui) 0;
    margin: 16px 0;
    color: var(--ink);
}

.fox-banner--warning {
    background: rgba(var(--tint-dawn), 0.12);
    border-left-color: var(--dawn);
}

/* --- Save-state indicator ---
   .fox-totals's pill styling, repurposed as a dirty/saving/saved/error
   readout. The state lives in data-state so a consumer can transition it
   without rebuilding the element. */
.fox-savestate {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 10px;
    border-radius: var(--radius-ui);
    background: rgba(var(--tint-hairline), 0.15);
    color: var(--ink-soft);
}

.fox-savestate[data-state="saving"] { opacity: 0.7; }
.fox-savestate[data-state="dirty"] { background: rgba(var(--tint-ice), 0.25); }
.fox-savestate[data-state="error"] {
    border: 1px solid var(--dawn);
    color: var(--dawn);
    background: rgba(var(--tint-dawn), 0.1);
}

/* --- Dialog (native <dialog>, showModal()-driven) ---
   Chrome modeled on .fox-settings' glassmorphism panel. The <dialog> itself
   carries the panel chrome directly (padding:0, box shadow/border/blur on
   the element, not a wrapper) so a plain click-target check (event.target
   === the <dialog>) reliably tells a ::backdrop click from a click on the
   visible <form> content - see fox-dialog.ts's wireDialog(). */
.fox-dialog {
    --ink: var(--fox-ink);
    --ink-soft: var(--fox-ink-soft);
    --hairline: var(--fox-hairline);
    --focus-ring: var(--fox-focus-ring);
    padding: 0;
    /* Centered via fixed top/left/transform rather than the UA stylesheet's
       inset:0 + margin:auto trick - this file's global reset zeroes margin
       on every element, and author rules always beat the UA stylesheet, so
       margin: auto here would resolve to 0 and leave the dialog pinned to
       the top-left corner. translate(-50%, -50%) doesn't depend on margin
       at all, so it centers regardless. */
    position: fixed;
    top: 50%;
    left: 50%;
    max-width: min(480px, calc(100vw - 32px));
    width: 100%;
    max-height: calc(100vh - 32px);
    overflow-y: auto;
    border: 1px solid rgba(var(--tint-hairline), 0.5);
    border-radius: var(--radius-card);
    background: rgba(var(--overlay-glass), 0.85);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    box-shadow: 0 8px 30px rgba(var(--shadow-ink), 0.08), 0 2px 6px rgba(var(--shadow-ink), 0.04);
    color: var(--ink);
    font-family: var(--font-sans);
    opacity: 0;
    /* The -50%/-50% recenters the box on its own top-left anchor; the
       trailing translateY/scale is purely the entrance-animation offset. */
    transform: translate(-50%, -50%) translateY(-12px) scale(0.98);
    transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                overlay 0.2s allow-discrete, display 0.2s allow-discrete;
}

.fox-dialog[open] {
    opacity: 1;
    transform: translate(-50%, -50%) translateY(0) scale(1);
}

/* Entrance-only: closing a native <dialog> removes [open] synchronously, so
   there is no exit transition to animate without extra JS to defer the
   close - accepted here as the simpler, native-first tradeoff. Browsers
   without @starting-style/allow-discrete support (anything short of
   Chrome/Edge 117, Safari 17.5, Firefox 129) just skip the fade; showModal()
   still opens the dialog instantly either way. */
@starting-style {
    .fox-dialog[open] {
        opacity: 0;
        transform: translate(-50%, -50%) translateY(-12px) scale(0.98);
    }
}

.fox-dialog::backdrop {
    background: rgba(var(--shadow-ink), 0.35);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    opacity: 1;
    transition: opacity 0.2s ease, overlay 0.2s allow-discrete, display 0.2s allow-discrete;
}

@starting-style {
    .fox-dialog[open]::backdrop { opacity: 0; }
}

.fox-dialog > form {
    display: flex;
    flex-direction: column;
}

.fox-dialog-header {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 20px 24px;
    background: rgba(var(--overlay-glass), 0.4);
    border-bottom: 1px solid rgba(var(--tint-hairline), 0.3);
}

.fox-dialog-icon {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-ui);
}

.fox-dialog-icon--info    { color: var(--ice);  background: rgba(var(--tint-ice), 0.15); }
.fox-dialog-icon--success { color: var(--moss); background: rgba(var(--tint-moss), 0.15); }
.fox-dialog-icon--warning { color: var(--dawn); background: rgba(var(--tint-dawn), 0.12); }
.fox-dialog-icon--error   { color: var(--dawn); background: rgba(var(--tint-dawn), 0.12); }

/* Severity reaches the header too, not just the icon chip - the same tinted
   fill .fox-banner--warning uses, so a warning dialog and a warning banner
   read as the same signal. Set on the <dialog> itself rather than inferred
   from the icon, so a severity with no icon still colors. */
.fox-dialog--info    .fox-dialog-header { background: rgba(var(--tint-ice), 0.12); }
.fox-dialog--success .fox-dialog-header { background: rgba(var(--tint-moss), 0.12); }
.fox-dialog--warning .fox-dialog-header { background: rgba(var(--tint-dawn), 0.12); }
.fox-dialog--error   .fox-dialog-header { background: rgba(var(--tint-dawn), 0.12); }

.fox-dialog-title {
    flex: 1;
    margin: 0;
    font-size: 1.185rem;
    font-weight: 700;
    /* Design tracking composed with the reader's spacing, never replacing
       it - the same calc() every other title in this file uses. See
       .fox-settings-title for the full reasoning. */
    letter-spacing: calc(0.01em + var(--letter-spacing));
    color: var(--ink);
}

.fox-dialog-close {
    flex: none;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: none;
    border-radius: var(--radius-ui);
    background: transparent;
    color: var(--ink-soft);
    cursor: pointer;
}

.fox-dialog-close:hover { background: rgba(var(--tint-hairline), 0.15); color: var(--ink); }

.fox-dialog-close:focus-visible {
    outline: var(--focus-width) solid var(--focus-ring);
    outline-offset: 2px;
}

/* Same WCAG 2.5.5 hit-area trick as .fox-btn::after - see that rule's
   comment for why this can't just be bigger padding. */
.fox-dialog-close::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    height: 100%;
    min-width: 44px;
    min-height: 44px;
}

.fox-dialog-body {
    padding: 20px 24px 0;
    font-size: 0.9rem;
    line-height: var(--line-height);
    color: var(--ink-soft);
}

/* Paired with .fox-btn-row in markup for its flex/gap/wrap; margin-top
   reset to 0 since this block already carries its own padding-top and
   .fox-btn-row's 20px would otherwise stack on top of it. */
.fox-dialog-actions {
    margin-top: 0;
    padding: 16px 24px 24px;
    justify-content: flex-end;
}

/* --- Card Editor Specifics --- */
.fox-editor-row {
    background: rgba(var(--tint-hairline), 0.1);
    border: 1px solid rgba(var(--tint-hairline), 0.4);
    border-radius: var(--radius-ui);
    padding: 14px;
    margin-bottom: 12px;
    display: grid;
    grid-template-columns: 1fr 1fr auto;
    gap: 12px;
    align-items: start;
    transition: border-color 0.2s ease;
}

.fox-editor-row:hover {
    border-color: var(--hairline);
}

.fox-row-meta {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 130px;
}

.fox-big-toggle {
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: calc(0.05em + var(--letter-spacing));
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    user-select: none;
    color: var(--ink-soft);
}

.fox-big-toggle input[type="checkbox"] {
    width: 14px;
    height: 14px;
    accent-color: var(--ink);
    cursor: pointer;
}

.fox-big-toggle input[type="checkbox"]:focus-visible {
    outline: var(--focus-width) solid var(--focus-ring);
    outline-offset: 2px;
}

/* --- Print Rules --- */
@media print {
    .fox-settings, .fox-toolbar {
        display: none !important;
    }
}

@media screen and (max-width: 700px) {
    .fox-editor-row {
        grid-template-columns: 1fr;
    }
    .fox-group-grid {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
   Print Sheet Mechanics
   Shared by any tool that lays out A7-ish cutout-card grids
   (mtools/cards, mtools/rules) or A5-ish half-sheets
   (mtools/puzzle) via fox-sheets.js. Dimensions are supplied
   per-sheet as CSS custom properties by the JS renderer, so the
   same classes serve both A4-portrait grid sheets and
   A4-landscape half sheets.
   ============================================================ */

/* Pinned to light literals (not the --fox-* alias trick) since this
   previews literal paper output on-screen — printed paper is white
   regardless of the page's theme, and must never flip dark. */
.fox-sheets {
    --ink: #26343f;
    --ink-soft: #4a5a66;
    --snow: #fdfdfb;
    --hairline: #b9c6ce;
    --ice: #9fc4d6;
    --moss: #8fae9b;
    --dawn: #c96b5e;
    --birch: #c9b38a;
    --slate: #7c8fa3;
    --aurora: #a68fc9;
    --shadow-ink: 38, 52, 63;
    --overlay-glass: 253, 253, 251;
    --tint-hairline: 185, 198, 206;
    --tint-ice: 159, 196, 214;
    display: flex; flex-direction: column; align-items: center; gap: 18px; padding-bottom: 60px;
}

.fox-sheet {
    width: var(--fox-sheet-w);
    height: var(--fox-sheet-h);
    background: var(--snow);
    box-shadow: 0 2px 14px rgba(38, 52, 63, 0.14);
    position: relative;
}

.fox-sheet--grid {
    /* Duplex registration calibration: printers/copiers rarely align the two
       passes of a duplex job perfectly, so back sheets may need a small
       manual mm correction (set via --fox-duplex-offset-x/-y). This
       reallocates padding between opposite edges rather than transforming
       the sheet itself — .fox-sheet IS the page (fixed size, page-break
       boundary), so moving the whole box just adds blank margin on one
       edge and clips content past the other; shifting padding keeps the
       box's total size (and therefore the page) unchanged and instead
       nudges the grid within it. Front sheets never set the offset
       variables, so this falls back to the original 6.5mm/5mm split there.
       (If a caller passes an explicit `sheetPadding`, that shorthand wins
       outright and offsets don't apply — no current tool does this.) */
    padding: var(--fox-sheet-pad,
        calc(6.5mm + var(--fox-duplex-offset-y, 0mm))
        calc(5mm - var(--fox-duplex-offset-x, 0mm))
        calc(6.5mm - var(--fox-duplex-offset-y, 0mm))
        calc(5mm + var(--fox-duplex-offset-x, 0mm)));
    display: grid;
    grid-template-columns: var(--fox-cols-tpl);
    grid-template-rows: var(--fox-rows-tpl);
}

.fox-sheet--flex {
    display: flex;
    flex-direction: row;
    overflow: hidden;
}

/* --- cards (A7-ish grid) --- */

.fox-card {
    width: var(--fox-card-w);
    height: var(--fox-card-h);
    padding: var(--fox-card-pad, 6mm 7mm 5mm);
    display: flex;
    flex-direction: column;
    position: relative;
    overflow: hidden;
}

.fox-cut-r { border-right: 0.4pt dashed var(--hairline); }
.fox-cut-b { border-bottom: 0.4pt dashed var(--hairline); }

.fox-card-empty { background: none; }

@media screen {
    .fox-card-empty::after {
        content: "";
        position: absolute;
        inset: 5mm;
        border: 0.5pt dashed var(--hairline);
        border-radius: 2mm;
    }
}

/* Opt-in faint guide for the safe-content inset (distinct from fox-cut-r/-b,
   which mark the cut line ON the shared edge between cards). --fox-safe-margin
   sets the inset per card format. Deliberately not @media screen-scoped like
   .fox-card-empty::after above — it must also render (faintly, so it
   disappears under lamination) on the printed sheet, since its whole purpose
   is to guide trimming after printing. */
.fox-safe-margin-guide {
    position: relative;
}
.fox-safe-margin-guide::before {
    content: "";
    position: absolute;
    inset: var(--fox-safe-margin, 5mm);
    border: 0.25pt dashed rgba(var(--tint-hairline), 0.5);
    pointer-events: none;
}

/* Generalizes the recurring "solid / chevron / diagonal-stripe" 3-category
   color+pattern system: colors come from custom properties so each tool's
   own category map supplies its own palette without new CSS. */
.fox-strip {
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 4mm;
}
.fox-strip--solid { background: var(--fox-strip-color); }
.fox-strip--chevron { background: var(--fox-strip-color); }
.fox-strip--chevron svg { display: block; width: 100%; height: 100%; }
.fox-strip--stripe {
    background: repeating-linear-gradient(45deg, var(--fox-stripe-a) 0 3mm, var(--fox-stripe-b) 3mm 6mm);
}

.fox-card .fox-icon { align-self: flex-end; }
.fox-card .fox-icon svg { display: block; width: 10mm; height: 10mm; }

/* --- half-sheets (A5-ish, 2-up) --- */

.fox-half {
    width: var(--fox-half-w);
    height: var(--fox-sheet-h);
    padding: var(--fox-half-pad, 11mm 10mm 9mm);
    display: flex;
    flex-direction: column;
    position: relative;
}
.fox-half + .fox-half { border-left: 1px dashed var(--hairline); }

.fox-cutmark {
    position: absolute;
    left: var(--fox-half-w);
    top: 4mm;
    transform: translateX(-50%);
    background: var(--snow);
    padding: 0.5mm 0;
    line-height: 0;
}
.fox-cutmark.bottom {
    top: auto;
    bottom: 4mm;
    transform: translateX(-50%) rotate(180deg);
}

/* --- print --- */

@media print {
    body { background: #fff; }
    .fox-sheets { gap: 0; padding: 0; display: block; }
    .fox-sheet {
        box-shadow: none;
        page-break-after: always; break-after: page;
        margin: 0 !important;
        transform: none !important;
    }
    /* Without this, some print drivers emit a spurious trailing blank page
       after the last sheet's page-break-after: always. */
    .fox-sheet:last-child { page-break-after: auto; break-after: auto; }
    .fox-strip { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

@media screen and (max-width: 700px) {
    .fox-sheet {
        transform: scale(var(--fox-scale, 0.44));
        transform-origin: top center;
        margin-bottom: calc(var(--fox-sheet-h) * (var(--fox-scale, 0.44) - 1));
    }
}
