/* ==========================================================================
   Kzyx Solutions — design system
   --------------------------------------------------------------------------
   Three layers, in this order, all of them plain CSS custom properties so the
   whole system works with no build step and flips theme from one attribute:

     1. PRIMITIVES  --kz-c-*, --kz-space-*, --kz-text-*, --kz-ease-*, …
        Raw values with no opinion about where they are used. Never referenced
        from a component rule; only the semantic layer reads them.
     2. SEMANTIC    --kz-accent, --kz-bg, --kz-surface, --kz-text, …
        What a value MEANS. This is the only layer the dark theme overrides,
        which is why one attribute re-themes the entire product.
     3. COMPONENT   --kz-btn-*, --kz-card-*, --kz-field-*, …
        Per-component knobs, so a component can be retuned without touching a
        rule that something else also depends on.

   CANONICAL NAMES (the portal, share and setup sheets consume these — do not
   rename): --kz-accent, --kz-accent-ui, --kz-accent-text, --kz-accent-hover,
   --kz-on-accent, --kz-bg, --kz-surface, --kz-surface-2, --kz-text,
   --kz-text-muted, --kz-border, --kz-radius, --kz-font-brand, --kz-font-mono.

   THE THREE ACCENTS, and why there are three. Contrast is not one number:
     --kz-accent       brand fill (#00A1F1). Large blocks of colour only — a
                       button face, a badge, a bar. What must pass AA there is
                       the INK ON IT, and --kz-on-accent does (5.78:1).
     --kz-accent-ui    non-text UI marks: focus rings, control borders, icon
                       strokes, rules. WCAG 1.4.11 wants 3:1 against the
                       background, and the brand blue only reaches 2.85:1 on
                       white — so small, load-bearing marks use this instead.
     --kz-accent-text  the accent COLOURING LETTERS. AA 4.5:1 against every
                       surface it can land on, --kz-surface-2 included (the
                       previous value passed on white and failed there, 4.41:1).
                       --kz-accent-text-hover is its hover partner and moves
                       AWAY from the page (darker in light, lighter in dark) —
                       the opposite direction from --kz-accent-hover, which
                       moves to keep its own ink readable. One token could not
                       do both jobs, and when it tried, every link in the
                       portal hovered to 3.67:1.
   Every ratio above was measured against the actual hex values, not assumed.
   ========================================================================== */

/* ---------- self-hosted fonts (CSP: font-src 'self') ----------
   No external font host is reachable under the policy and none is wanted.
   See wwwroot/fonts/README.md for the roles and the licences. */
@font-face {
    font-family: "Aldrich";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/aldrich-400-latin.woff2") format("woff2");
}

/* Variable, wght 100–700 in one 45 KB file. Declaring the range matters: it
   tells the browser 500/600/700 are real masters on the axis, so it stops
   synthesising a fake bold — which is exactly what the portal used to render
   for every heading, because it asked weight 650 of a face that has only 400. */
@font-face {
    font-family: "IBM Plex Sans";
    font-style: normal;
    font-weight: 100 700;
    font-display: swap;
    src: url("../fonts/ibm-plex-sans-var-latin.woff2") format("woff2");
}

@font-face {
    font-family: "IBM Plex Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/ibm-plex-mono-400-latin.woff2") format("woff2");
}

@font-face {
    font-family: "IBM Plex Mono";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("../fonts/ibm-plex-mono-500-latin.woff2") format("woff2");
}

/* ==========================================================================
   LAYER 1 — PRIMITIVES
   Raw values. Theme-independent: the dark theme re-points the semantic layer
   at different primitives, it never redefines a primitive.
   ========================================================================== */
:root {
    /* -- colour: brand ------------------------------------------------- */
    --kz-c-brand-50: #e8f6fe;
    --kz-c-brand-100: #cceafd;
    --kz-c-brand-200: #99d6fb;
    --kz-c-brand-300: #5ec4f9;
    --kz-c-brand-400: #29b2f6;
    --kz-c-brand-500: #00a1f1;  /* the brand blue itself */
    /* Between 500 and 600. Exists because the fill's hover state has to keep
       --kz-c-brand-ink above 4.5:1 and brand-600 lands at 4.49 — one
       hundredth short, which is still short. */
    --kz-c-brand-550: #0b96dd;
    --kz-c-brand-600: #0e8dce;
    --kz-c-brand-700: #026da3;
    --kz-c-brand-800: #05587f;
    --kz-c-brand-900: #04384f;
    --kz-c-brand-ink: #04222f;  /* dark ink that reads on a brand-500 fill */

    /* -- colour: one neutral family, tinted toward the brand hue --------
       A single family, cool throughout. Mixing a warm grey into a cool one is
       the fastest way to make a palette look accidental. */
    --kz-c-slate-0: #ffffff;
    --kz-c-slate-25: #fafbfd;
    --kz-c-slate-50: #f4f6f9;
    --kz-c-slate-100: #e9edf2;
    --kz-c-slate-200: #dde3ea;
    --kz-c-slate-300: #c3ccd8;
    /* 400 and 500 are the two "quiet but still legible" steps, and both are
       set by measurement rather than by eye: 400 is the lightest value that
       holds 3:1 as a control boundary on every surface, 500 the lightest that
       holds 4.5:1 as small text on every surface. The first cut of this ramp
       was a step lighter at each and failed both — the label style, the table
       column headers and the footer all shipped below AA. */
    --kz-c-slate-400: #6f7d8c;
    --kz-c-slate-500: #576373;
    --kz-c-slate-600: #525f70;
    --kz-c-slate-700: #3d4855;
    --kz-c-slate-800: #2b3540;
    --kz-c-slate-850: #212a35;
    --kz-c-slate-900: #161d26;
    --kz-c-slate-950: #0e1319;
    --kz-c-slate-ink: #151b23;

    /* Neutrals that have to read against a dark ground. */
    /* Same two roles on a dark ground, measured the same way. */
    --kz-c-slate-d-300: #6b8098;
    --kz-c-slate-d-350: #8593a4;
    --kz-c-slate-d-400: #9aa7b6;
    --kz-c-slate-d-500: #e7ecf2;

    /* -- colour: status. Every one measured to AA against --kz-surface,
          --kz-bg and --kz-surface-2 in its own theme. ------------------- */
    --kz-c-red-600: #b3261e;
    --kz-c-red-300: #ff8b80;
    --kz-c-green-700: #177431;
    --kz-c-green-300: #5fcb83;
    --kz-c-amber-700: #8a5d00;
    --kz-c-amber-300: #e8b445;

    /* -- space: 4 px base, expressed in rem so the wide-screen root-font
          steps at the end of this file scale spacing with the type.
          Some rungs are currently unspent, and that is what a SCALE is for:
          the vocabulary is complete so the next component picks from it
          instead of inventing a value. Same for the type, colour and z scales
          below. It is not the same thing as an unused one-off token, which
          this system does delete. ---------------------------------------- */
    --kz-space-1: 0.25rem;
    --kz-space-2: 0.5rem;
    --kz-space-3: 0.75rem;
    --kz-space-4: 1rem;
    --kz-space-5: 1.25rem;
    --kz-space-6: 1.5rem;
    --kz-space-7: 1.75rem;
    --kz-space-8: 2rem;
    --kz-space-10: 2.5rem;
    --kz-space-12: 3rem;
    --kz-space-16: 4rem;
    --kz-space-20: 5rem;
    --kz-space-24: 6rem;

    /* -- type: a modular scale, plus fluid display steps --------------- */
    --kz-text-2xs: 0.6875rem;
    --kz-text-xs: 0.75rem;
    --kz-text-sm: 0.8125rem;
    --kz-text-md: 0.9375rem;
    --kz-text-base: 1rem;
    --kz-text-lg: 1.125rem;
    --kz-text-xl: 1.3125rem;
    --kz-text-2xl: 1.5rem;
    --kz-text-display-sm: clamp(1.35rem, 1.15rem + 0.9vw, 1.85rem);
    --kz-text-display-md: clamp(1.6rem, 1.25rem + 1.6vw, 2.35rem);
    --kz-text-display-lg: clamp(2.1rem, 1.35rem + 3.3vw, 3.6rem);

    --kz-leading-tight: 1.1;
    --kz-leading-snug: 1.3;
    --kz-leading-normal: 1.55;
    --kz-leading-relaxed: 1.65;

    /* Large type wants negative tracking; small label caps want positive. */
    --kz-tracking-display: -0.022em;
    --kz-tracking-tight: -0.011em;
    --kz-tracking-normal: 0;
    --kz-tracking-label: 0.08em;
    --kz-tracking-eyebrow: 0.2em;

    /* -- radius: a scale, so a container can be softer than what sits
          inside it instead of everything sharing one number ------------- */
    --kz-radius-xs: 4px;
    --kz-radius-sm: 6px;
    --kz-radius-md: 10px;
    --kz-radius-lg: 14px;
    --kz-radius-xl: 20px;
    --kz-radius-pill: 999px;

    /* -- motion: durations and curves live here so a component never
          invents its own timing ---------------------------------------- */
    /* Three steps, and they are matched to the SIZE of what moves: a 40px
       control at fast, a component at base, a full-viewport surface at slow.
       A fourth, slower step existed here and was never spent — an unused
       duration is a scale nobody is following. */
    --kz-dur-fast: 120ms;
    --kz-dur-base: 180ms;
    --kz-dur-slow: 260ms;
    /* Entrances only. A page arrives once per visit, so it is the one moment
       that can afford to take its time — the rest of the scale is for state
       changes on things the visitor operates over and over. */
    --kz-dur-enter: 560ms;

    /* Bare `ease` and `ease-in-out` are weak; these are the two curves the
       system actually uses. --kz-ease-out is the arrival curve (fast start,
       long settle) and --kz-ease-standard the balanced one for an element
       changing state while it stays put.

       There is deliberately no departure curve: nothing in this product has an
       exit animation, because an exit competes with whatever the person is
       turning to next. A token for a policy that does not exist is worse than
       no token — it reads as an omission rather than a decision. */
    --kz-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --kz-ease-standard: cubic-bezier(0.32, 0.72, 0, 1);

    /* -- stacking order: named, so nothing has to guess a number -------- */
    --kz-z-base: 0;
    --kz-z-raised: 10;
    --kz-z-sticky: 40;
    --kz-z-header: 60;
    --kz-z-overlay: 80;
    --kz-z-dialog: 100;
    --kz-z-skip: 120;

    /* -- fonts ---------------------------------------------------------- */
    --kz-font-brand: "Aldrich", "Segoe UI", system-ui, sans-serif;
    --kz-font-body: "IBM Plex Sans", "Segoe UI", -apple-system, system-ui, "Helvetica Neue", Arial, sans-serif;
    --kz-font-mono: "IBM Plex Mono", Consolas, "Courier New", monospace;
}

/* ==========================================================================
   LAYER 2 — SEMANTIC (light)
   The only layer a theme overrides.
   ========================================================================== */
:root {
    --kz-accent: var(--kz-c-brand-500);
    --kz-accent-ui: var(--kz-c-brand-600);
    --kz-accent-text: var(--kz-c-brand-700);
    /* The FILL's hover. Deliberately not the same value as the TEXT's hover:
       a fill goes one way to keep its ink readable and a link goes the other
       to stay readable itself. Sharing one token meant every portal link
       hovered to 3.67:1. */
    --kz-accent-hover: var(--kz-c-brand-550);
    --kz-accent-text-hover: var(--kz-c-brand-800);
    --kz-on-accent: var(--kz-c-brand-ink);

    --kz-bg: var(--kz-c-slate-50);
    --kz-surface: var(--kz-c-slate-0);
    --kz-surface-2: var(--kz-c-slate-100);

    --kz-text: var(--kz-c-slate-ink);
    --kz-text-muted: var(--kz-c-slate-600);
    --kz-text-subtle: var(--kz-c-slate-500);

    --kz-border: var(--kz-c-slate-200);
    /* For a control whose OUTLINE IS THE ONLY THING that says where it is — a
       text field, a select, an empty input. Those need 3:1 (WCAG 1.4.11) and
       the hairline above deliberately does not spend it.
       A button carrying a readable label or glyph is not in that class: its
       content already marks it out, so it keeps the hairline. */
    --kz-border-strong: var(--kz-c-slate-400);

    --kz-link: var(--kz-accent-text);

    --kz-danger: var(--kz-c-red-600);
    --kz-ok: var(--kz-c-green-700);
    --kz-warn: var(--kz-c-amber-700);

    /* Shadows carry the ground's hue rather than neutral black, so they read
       as light falling on the page instead of a grey smudge under it. One
       light source, from above, everywhere. */
    --kz-shadow-xs: 0 1px 2px rgba(19, 35, 56, 0.05);
    --kz-shadow-sm: 0 1px 2px rgba(19, 35, 56, 0.06), 0 2px 6px rgba(19, 35, 56, 0.05);
    --kz-shadow-md: 0 1px 2px rgba(19, 35, 56, 0.06), 0 6px 18px rgba(19, 35, 56, 0.07);
    --kz-shadow-lg: 0 2px 6px rgba(19, 35, 56, 0.08), 0 16px 40px rgba(19, 35, 56, 0.11);
    --kz-shadow-xl: 0 4px 10px rgba(19, 35, 56, 0.09), 0 28px 64px rgba(19, 35, 56, 0.15);

    --kz-header-bg: rgba(255, 255, 255, 0.82);
    --kz-hero-glow: rgba(0, 161, 241, 0.11);
    --kz-mark-veil: rgba(21, 27, 35, 0.045);
    /* Grain strength. A flat vector plane reads as sterile; a trace of noise
       gives it a surface. Tuned to be felt, not seen. */
    --kz-grain-opacity: 0.028;
    /* The chevron a native <select> draws for itself is the platform's, not
       ours. An SVG data URI cannot read currentColor, so the mark ships per
       theme as a token — which also keeps it inside the light/dark parity the
       design-system test enforces. */
    --kz-select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23525f70' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");

    color-scheme: light;
}

/* ==========================================================================
   LAYER 2 — SEMANTIC (dark)
   Identical mapping, different primitives. Written out twice below — once for
   the explicit choice, once for the OS default — because a selector list
   cannot combine :root[data-theme] with :root:not() inside a media query
   without one of them losing. Keep the two blocks in lockstep.
   ========================================================================== */
:root[data-theme="dark"] {
    --kz-accent: var(--kz-c-brand-400);
    --kz-accent-ui: var(--kz-c-brand-400);
    --kz-accent-text: var(--kz-c-brand-300);
    --kz-accent-hover: var(--kz-c-brand-300);
    --kz-accent-text-hover: var(--kz-c-brand-200);
    --kz-on-accent: var(--kz-c-brand-ink);

    --kz-bg: var(--kz-c-slate-950);
    --kz-surface: var(--kz-c-slate-900);
    --kz-surface-2: var(--kz-c-slate-850);

    --kz-text: var(--kz-c-slate-d-500);
    --kz-text-muted: var(--kz-c-slate-d-400);
    --kz-text-subtle: var(--kz-c-slate-d-350);

    --kz-border: #2c3743;
    --kz-border-strong: var(--kz-c-slate-d-300);

    --kz-link: var(--kz-accent-text);

    --kz-danger: var(--kz-c-red-300);
    --kz-ok: var(--kz-c-green-300);
    --kz-warn: var(--kz-c-amber-300);

    /* On a dark ground a tinted shadow is nearly invisible, so depth comes
       from near-black spread instead — plus the surface steps above. */
    --kz-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
    --kz-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45), 0 2px 6px rgba(0, 0, 0, 0.3);
    --kz-shadow-md: 0 1px 2px rgba(0, 0, 0, 0.5), 0 6px 18px rgba(0, 0, 0, 0.38);
    --kz-shadow-lg: 0 2px 6px rgba(0, 0, 0, 0.5), 0 16px 40px rgba(0, 0, 0, 0.5);
    --kz-shadow-xl: 0 4px 10px rgba(0, 0, 0, 0.55), 0 28px 64px rgba(0, 0, 0, 0.6);

    --kz-header-bg: rgba(14, 19, 25, 0.82);
    --kz-hero-glow: rgba(41, 178, 246, 0.10);
    --kz-mark-veil: rgba(231, 236, 242, 0.05);
    --kz-grain-opacity: 0.05;
    --kz-select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa7b6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");

    color-scheme: dark;
}

/* No stored choice and no ?theme= override: follow the OS. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --kz-accent: var(--kz-c-brand-400);
        --kz-accent-ui: var(--kz-c-brand-400);
        --kz-accent-text: var(--kz-c-brand-300);
        --kz-accent-hover: var(--kz-c-brand-300);
        --kz-accent-text-hover: var(--kz-c-brand-200);
        --kz-on-accent: var(--kz-c-brand-ink);

        --kz-bg: var(--kz-c-slate-950);
        --kz-surface: var(--kz-c-slate-900);
        --kz-surface-2: var(--kz-c-slate-850);

        --kz-text: var(--kz-c-slate-d-500);
        --kz-text-muted: var(--kz-c-slate-d-400);
        --kz-text-subtle: var(--kz-c-slate-d-350);

        --kz-border: #2c3743;
        --kz-border-strong: var(--kz-c-slate-d-300);

        --kz-link: var(--kz-accent-text);

        --kz-danger: var(--kz-c-red-300);
        --kz-ok: var(--kz-c-green-300);
        --kz-warn: var(--kz-c-amber-300);

        --kz-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
        --kz-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45), 0 2px 6px rgba(0, 0, 0, 0.3);
        --kz-shadow-md: 0 1px 2px rgba(0, 0, 0, 0.5), 0 6px 18px rgba(0, 0, 0, 0.38);
        --kz-shadow-lg: 0 2px 6px rgba(0, 0, 0, 0.5), 0 16px 40px rgba(0, 0, 0, 0.5);
        --kz-shadow-xl: 0 4px 10px rgba(0, 0, 0, 0.55), 0 28px 64px rgba(0, 0, 0, 0.6);

        --kz-header-bg: rgba(14, 19, 25, 0.82);
        --kz-hero-glow: rgba(41, 178, 246, 0.10);
        --kz-mark-veil: rgba(231, 236, 242, 0.05);
        --kz-grain-opacity: 0.05;
        --kz-select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa7b6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");

        color-scheme: dark;
    }
}

/* ==========================================================================
   LAYER 3 — COMPONENT TOKENS
   Read the semantic layer, never a primitive. Theme-independent by
   construction: they inherit whatever the semantic layer currently resolves
   to, so they are declared once and never repeated per theme.
   ========================================================================== */
:root {
    /* The canonical radius alias. Geometry, not colour — it lives here rather
       than in the semantic colour blocks precisely because it must NOT be
       restated per theme. */
    --kz-radius: var(--kz-radius-md);

    /* button */
    --kz-btn-radius: var(--kz-radius-sm);
    --kz-btn-padding: 0.66rem 1.3rem;
    --kz-btn-weight: 600;
    --kz-btn-press: 0.985;

    /* card */
    --kz-card-bg: var(--kz-surface);
    --kz-card-border: var(--kz-border);
    --kz-card-radius: var(--kz-radius-lg);
    --kz-card-padding: var(--kz-space-6);
    --kz-card-shadow: var(--kz-shadow-sm);

    /* form field */
    --kz-field-bg: var(--kz-surface);
    --kz-field-border: var(--kz-border-strong);
    --kz-field-radius: var(--kz-radius-sm);
    --kz-field-padding: 0.6rem 0.8rem;

    /* focus ring — one definition, so everything focusable in the product
       gets the same visible indicator */
    --kz-ring-width: 2px;
    --kz-ring-offset: 2px;
    --kz-ring-color: var(--kz-accent-ui);

    /* layout */
    --kz-container: 70rem;
    --kz-container-wide: 82rem;
}

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

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
}

body {
    margin: 0;
    font-family: var(--kz-font-body);
    font-size: var(--kz-text-base);
    line-height: var(--kz-leading-relaxed);
    color: var(--kz-text);
    background: var(--kz-bg);
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow-wrap: break-word;
    /* Plex Sans ships proper kerning and contextual alternates; both are on by
       default, but naming them stops a future reset from switching them off. */
    font-kerning: normal;
    font-variant-ligatures: contextual;
    -webkit-font-smoothing: antialiased;
}

img,
svg {
    max-width: 100%;
    vertical-align: middle;
}

/* ---------- headings ----------
   TWO FACES, ONE VOICE. Aldrich is a wide display face with exactly one
   weight, so it is spent where a brand should be heard — the page title and
   the section headline — and nowhere else. h3/h4 are structural rather than
   expressive and sit in Plex Sans, which has real 500/600 masters and holds
   up at small sizes. Asking Aldrich for a semibold produces a synthesised
   fake bold, which is what the portal rendered for every heading before this. */
h1,
h2,
h3,
h4 {
    margin: 0 0 0.6em;
    text-wrap: balance;
}

h1,
h2 {
    font-family: var(--kz-font-brand);
    font-weight: 400;
    line-height: var(--kz-leading-tight);
    letter-spacing: var(--kz-tracking-display);
}

h3,
h4 {
    font-family: var(--kz-font-body);
    font-weight: 600;
    line-height: var(--kz-leading-snug);
    letter-spacing: var(--kz-tracking-tight);
}

h1 {
    font-size: var(--kz-text-display-lg);
}

h2 {
    font-size: var(--kz-text-display-md);
}

h3 {
    font-size: var(--kz-text-lg);
}

h4 {
    font-size: var(--kz-text-base);
}

/* `pretty` fixes the last-line orphan that `balance` cannot. The MEASURE is
   set per context rather than globally (see .prose, .page-head, .section__head
   below): a paragraph in a table cell or a stat card has to fill its box, and
   a blanket max-width on `p` would quietly break those. */
p {
    margin: 0 0 1em;
    text-wrap: pretty;
}

/* Numbers stacked in a column must not jitter as they change. */
.tabular,
time,
td,
th {
    font-variant-numeric: tabular-nums;
}

a {
    color: var(--kz-link);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

/* One focus indicator for the whole product, drawn in the 3:1 accent rather
   than the brand fill — a ring is a non-text mark that has to be seen. */
:focus-visible {
    outline: var(--kz-ring-width) solid var(--kz-ring-color);
    outline-offset: var(--kz-ring-offset);
    border-radius: var(--kz-radius-xs);
}

::selection {
    background: var(--kz-accent);
    color: var(--kz-on-accent);
}

code {
    font-family: var(--kz-font-mono);
    font-size: 0.9em;
    background: var(--kz-surface-2);
    padding: 0.1em 0.4em;
    border-radius: var(--kz-radius-xs);
}

/* ---------- page grain ----------
   A flat vector plane reads as sterile. One fixed, non-interactive layer of
   fractal noise gives the whole page a surface without costing a request: the
   texture is an inline SVG data URI, which the CSP permits (img-src 'self'
   data:). Fixed positioning keeps it out of the body's flex flow — a static
   ::before would become a flex item and push the header down. */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: var(--kz-z-base);
    pointer-events: none;
    opacity: var(--kz-grain-opacity);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Print has its own surface. */
@media print {
    body::before {
        display: none;
    }
}

.container {
    /* rem, not px: the wide-screen root-font scaling (end of this file) then
       widens the layout and the type together. 70rem = 1120px at 16px root. */
    width: min(100% - 2.5rem, var(--kz-container));
    margin-inline: auto;
}

.container--wide {
    width: min(100% - 2.5rem, var(--kz-container-wide));
}

main {
    flex: 1 0 auto;
    position: relative;
    z-index: var(--kz-z-raised);
}

.skip-link {
    position: absolute;
    left: -999px;
    top: 0;
    z-index: var(--kz-z-skip);
    background: var(--kz-surface);
    color: var(--kz-text);
    padding: 0.7rem 1.1rem;
    border: 1px solid var(--kz-border-strong);
    border-radius: 0 0 var(--kz-radius-md) 0;
    box-shadow: var(--kz-shadow-lg);
    font-weight: 600;
}

.skip-link:focus {
    left: 0;
}

/* Screen-reader-only content (hidden headings, link hints).

   The belt-and-braces recipe, and every line of it earns its place: `clip` is
   deprecated but still what older engines honour, `clip-path` is what current
   ones use, and the negative margin plus zeroed padding/border stop a 1px box
   from contributing any layout of its own.

   It is still ABSOLUTELY POSITIONED, which means it anchors to the nearest
   positioned ancestor — or, if there is none, to the document. Inside a
   horizontally scrolled table that put the span at its static position ~660px
   along, OUTSIDE the scroller, and the whole page went sideways. Hence the
   companion rule on .portal-table-wrap. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The one small-caps label style in the system. Mono, because every other
   piece of machine-adjacent text on this site is mono, and wide tracking,
   because caps at 12px need the air. */
.eyebrow {
    font-family: var(--kz-font-mono);
    font-weight: 500;
    font-size: var(--kz-text-xs);
    letter-spacing: var(--kz-tracking-eyebrow);
    text-transform: uppercase;
    color: var(--kz-accent-text);
    margin: 0 0 0.9rem;
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

/* A short rule after the label, so the eyebrow reads as a section marker
   rather than a stray line of small text. */
.eyebrow::after {
    content: "";
    flex: 0 0 2rem;
    height: 1px;
    background: currentColor;
    opacity: 0.45;
}

.muted {
    color: var(--kz-text-muted);
}

/* ---------- header ---------- */
.site-header {
    position: sticky;
    top: 0;
    z-index: var(--kz-z-header);
    background: var(--kz-header-bg);
    backdrop-filter: blur(14px) saturate(1.4);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    border-bottom: 1px solid var(--kz-border);
}

.site-header__inner {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    min-height: 4.25rem;
    position: relative;
}

/* brand lockup: inline SVG chevrons + HTML text (page fonts apply, CSP-safe) */
.brand {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    color: var(--kz-text);
    text-decoration: none;
    line-height: 1;
}

.brand:hover {
    text-decoration: none;
}

.brand__chevrons {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    color: var(--kz-accent);
}

/* The mark's own gesture: the chevrons ease apart as the wordmark is
   approached. It is the only motion in the header and the only place the
   brand is allowed to be expressive.

   It moves the two chevrons with `transform`, NOT the container's `gap`.
   Animating gap re-lays-out the lockup on every frame — it widens the flex
   box, pushes the "K6" between the chevrons and shifts the "KZYX SOLUTIONS"
   wordmark beside it — on the most-hovered element on the site, and in
   contradiction of this sheet's own rule that no layout property is animated.
   A transform costs nothing and moves only what is meant to move. */
.brand__chev {
    transition: transform var(--kz-dur-slow) var(--kz-ease-out);
}

.brand:hover .brand__chev:first-child {
    transform: translateX(-0.09rem);
}

.brand:hover .brand__chev:last-child {
    transform: translateX(0.09rem);
}

.brand__chev {
    width: 0.62rem;
    height: 1.45rem;
    stroke: currentColor;
    stroke-width: 10;
    fill: none;
}

.brand__k6 {
    font-family: var(--kz-font-brand);
    font-size: 1.35rem;
    letter-spacing: 0.04em;
    transform: translateY(0.05em);
}

.brand__word {
    font-family: var(--kz-font-mono);
    font-weight: 500;
    font-size: 0.66rem;
    letter-spacing: 0.26em;
    color: var(--kz-text-muted);
    text-transform: uppercase;
    margin-left: 0.35rem;
    transform: translateY(0.12em);
}

.site-nav {
    display: flex;
    align-items: center;
    gap: 0.15rem;
    margin-left: auto;
}

.site-nav a {
    position: relative;
    display: inline-block;
    padding: 0.45rem 0.8rem;
    border-radius: var(--kz-radius-sm);
    color: var(--kz-text-muted);
    font-size: var(--kz-text-md);
    font-weight: 500;
    transition: color var(--kz-dur-fast) var(--kz-ease-standard),
                background-color var(--kz-dur-fast) var(--kz-ease-standard);
}

.site-nav a:hover {
    background: var(--kz-surface-2);
    color: var(--kz-text);
    text-decoration: none;
}

/* The same mark the current page carries, drawn in on approach — so hovering
   previews what "you are here" will look like instead of just tinting. */
.site-nav a:not([aria-current="page"])::after {
    content: "";
    position: absolute;
    left: 0.8rem;
    right: 0.8rem;
    bottom: 0.15rem;
    height: 2px;
    border-radius: var(--kz-radius-pill);
    background: var(--kz-accent-ui);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--kz-dur-slow) var(--kz-ease-out);
}

.site-nav a:not([aria-current="page"]):hover::after {
    transform: scaleX(1);
}

/* Where you are is stated twice — colour AND a mark under the label — so the
   answer does not depend on distinguishing two blues. */
.site-nav a[aria-current="page"] {
    color: var(--kz-text);
    font-weight: 600;
}

.site-nav a[aria-current="page"]::after {
    content: "";
    position: absolute;
    left: 0.8rem;
    right: 0.8rem;
    bottom: 0.15rem;
    height: 2px;
    border-radius: var(--kz-radius-pill);
    background: var(--kz-accent-ui);
}

.header-tools {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.lang-switch {
    display: inline-flex;
    align-items: center;
    font-family: var(--kz-font-mono);
    font-size: var(--kz-text-xs);
    border: 1px solid var(--kz-border);
    border-radius: var(--kz-radius-sm);
    background: var(--kz-surface);
    overflow: hidden;
}

.lang-switch a,
.lang-switch span {
    padding: 0.34rem 0.55rem;
    color: var(--kz-text-muted);
    transition: background-color var(--kz-dur-fast) var(--kz-ease-standard),
                color var(--kz-dur-fast) var(--kz-ease-standard);
}

.lang-switch a:hover {
    background: var(--kz-surface-2);
    color: var(--kz-text);
    text-decoration: none;
}

.lang-switch [aria-current="true"] {
    background: var(--kz-accent);
    color: var(--kz-on-accent);
    font-weight: 500;
}

.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 2.5rem = 40px, and the header row gives it 8px of vertical air on each
       side, which is what carries the row to the 44px touch minimum. */
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    border: 1px solid var(--kz-border);
    border-radius: var(--kz-radius-sm);
    background: var(--kz-surface);
    color: var(--kz-text-muted);
    cursor: pointer;
    transition: background-color var(--kz-dur-fast) var(--kz-ease-standard),
                color var(--kz-dur-fast) var(--kz-ease-standard),
                border-color var(--kz-dur-fast) var(--kz-ease-standard),
                transform var(--kz-dur-fast) var(--kz-ease-standard);
}

.icon-btn:hover {
    background: var(--kz-surface-2);
    color: var(--kz-text);
    border-color: var(--kz-border-strong);
}

.icon-btn:active {
    transform: scale(var(--kz-btn-press));
}

.icon-btn svg {
    width: 1.15rem;
    height: 1.15rem;
}

/* The theme control is ONE glyph — a disc filled on one half — and the filled
   half always points at the theme the button would move you to. Rotating it
   half a turn IS the state change, so there is no second SVG to keep in sync,
   and it sidesteps the sun/moon pair every product ships. */
.theme-icon {
    transition: transform var(--kz-dur-slow) var(--kz-ease-standard);
}

[data-theme="dark"] .theme-icon {
    transform: rotate(180deg);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .theme-icon {
        transform: rotate(180deg);
    }
}

.login-link {
    font-size: var(--kz-text-sm);
    color: var(--kz-text-muted);
    padding: 0.4rem 0.6rem;
    border-radius: var(--kz-radius-sm);
}

.login-link:hover {
    background: var(--kz-surface-2);
    text-decoration: none;
}

/* Logout is a POST form; make the wrapper layout-transparent and reset the
   button so it looks and sits like the adjacent .login-link anchors. */
.auth-form {
    display: contents;
}

button.login-link {
    background: none;
    border: 0;
    cursor: pointer;
    font: inherit;
    font-size: var(--kz-text-sm);
}

.footer-logout {
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    font: inherit;
    color: inherit;
    text-align: left;
}

.footer-logout:hover {
    text-decoration: underline;
}

.nav-toggle {
    display: none;
}

/* ---------- buttons ----------
   Three tiers, not two. A page whose only options are "filled" and "outlined"
   has to shout twice on every row; the third tier is a plain text action that
   carries no box at all. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: var(--kz-btn-padding);
    border-radius: var(--kz-btn-radius);
    border: 1px solid transparent;
    font: inherit;
    font-weight: var(--kz-btn-weight);
    font-size: var(--kz-text-md);
    letter-spacing: var(--kz-tracking-tight);
    cursor: pointer;
    text-decoration: none;
    /* transform and colour only — no layout property is animated anywhere in
       this sheet, so nothing here can cause a reflow mid-interaction. */
    transition: background-color var(--kz-dur-fast) var(--kz-ease-standard),
                border-color var(--kz-dur-fast) var(--kz-ease-standard),
                color var(--kz-dur-fast) var(--kz-ease-standard),
                box-shadow var(--kz-dur-fast) var(--kz-ease-standard),
                transform var(--kz-dur-fast) var(--kz-ease-standard);
}

.btn:hover {
    text-decoration: none;
}

/* Press feedback is a scale, not a nudge downward: a 1px translate on a
   button that sits in a row makes the whole row look like it flinched. */
.btn:active {
    transform: scale(var(--kz-btn-press));
}

.btn--primary {
    background: var(--kz-accent);
    color: var(--kz-on-accent);
    box-shadow: var(--kz-shadow-xs);
}

/* A 1px rise plus a deeper shadow: the primary action lifts toward the cursor.
   It is the one button on the page that gets this — a whole row of lifting
   buttons is a row of buttons nobody can rank. */
.btn--primary:hover {
    background: var(--kz-accent-hover);
    color: var(--kz-on-accent);
    transform: translateY(-1px);
    box-shadow: var(--kz-shadow-md);
}

/* The press still wins over the hover lift, so a click reads as a press
   rather than as a button that keeps floating. */
.btn--primary:active {
    transform: scale(var(--kz-btn-press));
}

.btn--ghost {
    border-color: var(--kz-border-strong);
    color: var(--kz-text);
    background: var(--kz-surface);
}

.btn--ghost:hover {
    background: var(--kz-surface-2);
    border-color: var(--kz-text-muted);
    color: var(--kz-text);
}

/* Tertiary: a verb with an arrow, no chrome. For the third action in a group,
   and for anywhere a full button would over-claim. */
.btn--link {
    padding-inline: 0;
    background: none;
    border: 0;
    color: var(--kz-accent-text);
    text-underline-offset: 0.25em;
}

.btn--link:hover {
    color: var(--kz-accent-text);
    text-decoration: underline;
}

/* The arrow leans in the direction the link goes when you reach for it. It is
   a 4px move on a low-frequency element — enough to register as an answer,
   not enough to be a performance. */
.btn__arrow {
    display: inline-flex;
    transition: transform var(--kz-dur-base) var(--kz-ease-out);
}

.btn__arrow svg {
    width: 1.05rem;
    height: 1.05rem;
}

.btn--link:hover .btn__arrow,
.btn--link:focus-visible .btn__arrow {
    transform: translateX(0.25rem);
}

/* ---------- sections ---------- */
.section {
    padding-block: clamp(3.25rem, 2rem + 4.5vw, 6rem);
}

/* A tinted band, not a different world. The step is one surface level, which
   is enough to separate two sections without the page looking as though a
   dark screenshot were pasted into it. */
.section--tint {
    background: var(--kz-surface);
    border-block: 1px solid var(--kz-border);
}

.section__head {
    max-width: 46rem;
    margin-bottom: clamp(2rem, 1.4rem + 1.6vw, 3rem);
}

.section__head p {
    color: var(--kz-text-muted);
    font-size: var(--kz-text-lg);
    max-width: 60ch;
    margin-bottom: 0;
}

/* ---------- hero ----------
   Layered ground, from back to front: a fine engineering grid, radially
   masked so it fades out rather than ending on an edge; the brand glow; the
   page background. The chevron mark sits above all three. Everything is a
   gradient — no image request, nothing to 404. */
.hero {
    position: relative;
    overflow: hidden;
    padding-block: clamp(4.5rem, 3rem + 7vw, 8.5rem);
    border-bottom: 1px solid var(--kz-border);
    background:
        radial-gradient(60rem 32rem at 82% -18%, var(--kz-hero-glow), transparent 62%),
        var(--kz-bg);
}

.hero::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image:
        linear-gradient(to right, var(--kz-mark-veil) 1px, transparent 1px),
        linear-gradient(to bottom, var(--kz-mark-veil) 1px, transparent 1px);
    background-size: 4.5rem 4.5rem;
    /* Masked from the top-left corner outward, so the grid reads as a drafting
       surface the content sits on rather than as wallpaper. */
    /* Fades out toward the bottom and the right rather than radiating from
       one corner — a corner patch reads as something that failed to paint. */
    -webkit-mask-image: linear-gradient(160deg, #000 0%, rgba(0, 0, 0, 0.45) 45%, transparent 78%);
    mask-image: linear-gradient(160deg, #000 0%, rgba(0, 0, 0, 0.45) 45%, transparent 78%);
}

/* Bled far enough that the SECOND chevron is mostly off-canvas rather than cut
   through its own vertex: a mark sliced across the middle of a stroke reads as
   something that failed to render. The clamp keeps the bleed proportional so
   the cut lands in the same place at every width. */
.hero__mark {
    position: absolute;
    right: clamp(-13rem, -7vw, -5rem);
    top: 50%;
    translate: 0 -50%;
    width: clamp(20rem, 40vw, 34rem);
    color: var(--kz-mark-veil);
    pointer-events: none;
}

.hero__inner {
    position: relative;
    z-index: var(--kz-z-raised);
    max-width: 46rem;
}

.hero h1 {
    margin-bottom: 0.42em;
}

.hero__lead {
    font-size: clamp(1.08rem, 1rem + 0.6vw, 1.3rem);
    line-height: var(--kz-leading-normal);
    color: var(--kz-text-muted);
    max-width: 40rem;
}

.hero__cta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.85rem 1.4rem;
    margin-top: 2.25rem;
}

/* ---------- grids ---------- */
.grid {
    display: grid;
    gap: 1.25rem;
}

.grid--3 {
    grid-template-columns: repeat(3, 1fr);
}

.grid--2 {
    grid-template-columns: repeat(2, 1fr);
}

@media (max-width: 900px) {
    .grid--3 {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 620px) {
    .grid--2,
    .grid--3 {
        grid-template-columns: 1fr;
    }
}

/* ---------- value rail ----------
   Three peers, so none of them is promoted into a "featured" tile. What
   replaces the row of identical bordered boxes is the treatment — a hairline
   with an accent tick on it, an index, an icon and no card chrome at all —
   NOT a change of altitude.

   An earlier cut staggered the three columns down the page. It was wrong twice
   over: a staircase needs a reason to descend, and three peers have none, so it
   read as a misalignment rather than a composition — and it left a large empty
   triangle under the first column. Making the offset bigger, which was the
   first attempt at a fix, only made it more conspicuously arbitrary. Peers are
   set as peers. */
.value-rail {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem 2.5rem;
}

.value {
    position: relative;
    padding-top: 1.6rem;
    border-top: 1px solid var(--kz-border);
}

.value::before {
    /* The accent segment sits ON the hairline, so the rule looks continuous
       and the colour looks like part of it rather than an added decoration. */
    content: "";
    position: absolute;
    top: -1px;
    left: 0;
    width: 2.75rem;
    height: 2px;
    background: var(--kz-accent-ui);
    border-radius: var(--kz-radius-pill);
}

/* The index rides on the same line as the icon: two stacked fragments before
   the title made the block start twice. */
.value__index {
    position: absolute;
    top: 1.6rem;
    right: 0;
    font-family: var(--kz-font-mono);
    font-size: var(--kz-text-xs);
    font-weight: 500;
    letter-spacing: var(--kz-tracking-label);
    color: var(--kz-text-subtle);
}

.value h3 {
    margin-bottom: 0.45em;
}

.value p {
    color: var(--kz-text-muted);
    margin-bottom: 0;
}

.value .card__icon {
    margin-bottom: 1.1rem;
    transition: background-color var(--kz-dur-base) var(--kz-ease-standard),
                color var(--kz-dur-base) var(--kz-ease-standard);
}

/* The accent tick on the rule extends when the block is approached. The
   response is spent on the brand mark rather than on the block — a text column
   that lifts or tints on hover is motion for its own sake, but a mark that
   answers you is the page acknowledging where you are. Transform only, from
   the left, so the rule it sits on does not move. */
.value::before {
    transform-origin: left center;
    transition: transform var(--kz-dur-slow) var(--kz-ease-out);
}

.value:hover::before {
    transform: scaleX(1.9);
}

.value:hover .card__icon {
    background: color-mix(in srgb, var(--kz-accent) 22%, transparent);
}

@media (max-width: 900px) {
    .value-rail {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 620px) {
    .value-rail {
        grid-template-columns: 1fr;
        gap: 1.75rem;
    }
}

/* ---------- spec table ----------
   Peer items that each carry a list, presented as one instrument panel split
   by hairlines instead of as separate floating cards. One border, one shadow,
   one object — which is also what the content is. */
.spec {
    display: grid;
    grid-template-columns: repeat(var(--spec-cols, 3), 1fr);
    background: var(--kz-surface);
    border: 1px solid var(--kz-border);
    border-radius: var(--kz-radius-lg);
    box-shadow: var(--kz-shadow-sm);
    overflow: hidden;
}

.spec--2 {
    --spec-cols: 2;
}

.spec__cell {
    padding: clamp(1.5rem, 1.2rem + 1vw, 2.15rem);
    /* Hairlines between cells only: the container already draws the outside,
       and doubling them makes a 2px seam. */
    border-inline-start: 1px solid var(--kz-border);
    transition: background-color var(--kz-dur-base) var(--kz-ease-standard);
}

.spec__cell .card__icon {
    transition: transform var(--kz-dur-slow) var(--kz-ease-out),
                background-color var(--kz-dur-slow) var(--kz-ease-standard);
}

.spec__cell:hover .card__icon {
    transform: translateY(-2px);
    background: color-mix(in srgb, var(--kz-accent) 22%, transparent);
}

.spec__cell:first-child {
    border-inline-start: 0;
}

.spec__cell:hover {
    background: color-mix(in srgb, var(--kz-accent) 4%, transparent);
}

.spec__cell h3 {
    margin-bottom: 0.45em;
}

.spec__cell > p {
    color: var(--kz-text-muted);
}

.spec__cell ul {
    list-style: none;
    color: var(--kz-text-muted);
    font-size: var(--kz-text-md);
    margin: 1.1rem 0 0;
    padding: 0;
    display: grid;
    gap: 0.5rem;
}

/* Marker drawn as a chevron rather than a bullet — the brand mark is a
   chevron, so the list inherits it instead of a generic dot. */
.spec__cell li {
    position: relative;
    padding-left: 1.1rem;
}

.spec__cell li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.55em;
    width: 0.3rem;
    height: 0.3rem;
    border-right: 1.5px solid var(--kz-accent-ui);
    border-top: 1.5px solid var(--kz-accent-ui);
    transform: rotate(45deg);
}

@media (max-width: 900px) {
    .spec {
        grid-template-columns: 1fr;
    }

    .spec__cell {
        border-inline-start: 0;
        border-top: 1px solid var(--kz-border);
    }

    .spec__cell:first-child {
        border-top: 0;
    }
}

/* ---------- cards ---------- */
.card {
    background: var(--kz-card-bg);
    border: 1px solid var(--kz-card-border);
    border-radius: var(--kz-card-radius);
    padding: var(--kz-card-padding);
    box-shadow: var(--kz-card-shadow);
}

.card h3 {
    margin-bottom: 0.4em;
}

/* Demoted card heading (correct outline level, h3 appearance — W-PUB-11). */
.card h2.card-heading {
    font-family: var(--kz-font-body);
    font-weight: 600;
    font-size: var(--kz-text-lg);
    line-height: var(--kz-leading-snug);
    letter-spacing: var(--kz-tracking-tight);
    margin-bottom: 0.4em;
}

.card p {
    color: var(--kz-text-muted);
    margin-bottom: 0;
}

.card__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    /* Tighter than its container's radius: an inner corner that matches its
       parent's looks like a mistake, one step tighter looks nested. */
    border-radius: var(--kz-radius-md);
    background: color-mix(in srgb, var(--kz-accent) 13%, transparent);
    color: var(--kz-accent-text);
    margin-bottom: 1rem;
}

.card__icon svg {
    width: 1.4rem;
    height: 1.4rem;
}

.card ul {
    color: var(--kz-text-muted);
    margin: 0.75rem 0 0;
    padding-left: 1.2rem;
}

.card ul li {
    margin-bottom: 0.35rem;
}

.card ul li::marker {
    color: var(--kz-accent-text);
}

/* ---------- process steps ----------
   A sequence, drawn as one. The rail is a single line the markers sit on, so
   the four steps read as stages of one process rather than four unrelated
   boxes that happen to be numbered. */
/* An <ol>: the order is the meaning, so it is in the markup, not only in the
   generated numbers. The native marker is dropped because ::before draws a
   designed one. */
.steps {
    counter-reset: step;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1.75rem;
    list-style: none;
    margin: 0;
    padding: 0;
}

.step {
    position: relative;
    padding-top: 3.25rem;
}

/* The rail. Drawn per step and butted together across the gap rather than as
   one absolute element, so it reflows with the grid.

   The overhang is exactly the grid gap, and it must be dropped for any step
   that ENDS A ROW — there is no next step beside it, so the rail would run
   past the container instead of into a gap. Missing the two-column case cost
   8px of sideways page scroll at a 768px viewport (measured: 776px of content
   in a 768px window), which is the kind of defect that only ever shows up on
   a tablet. */
.step::after {
    content: "";
    position: absolute;
    top: 0.72rem;
    left: 0;
    right: -1.75rem;
    height: 1px;
    background: var(--kz-border);
}

/* The last step has nothing after it, so its rail dissolves instead of
   stopping dead in empty space. */
.step:last-child::after {
    right: 0;
    background: linear-gradient(to right, var(--kz-border) 30%, transparent);
}

.step::before {
    counter-increment: step;
    content: "0" counter(step);
    position: absolute;
    top: 0;
    left: 0;
    z-index: var(--kz-z-raised);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.5rem;
    height: 1.45rem;
    padding-inline: 0.55rem;
    border-radius: var(--kz-radius-pill);
    background: var(--kz-surface);
    border: 1px solid var(--kz-border);
    font-family: var(--kz-font-mono);
    font-weight: 500;
    font-size: var(--kz-text-xs);
    letter-spacing: var(--kz-tracking-label);
    color: var(--kz-accent-text);
}

.step h3 {
    font-size: var(--kz-text-base);
    margin-bottom: 0.35em;
}

.step p {
    color: var(--kz-text-muted);
    font-size: var(--kz-text-md);
    margin: 0;
}

@media (max-width: 900px) {
    .steps {
        grid-template-columns: repeat(2, 1fr);
    }

    /* Two columns: every even step ends a row. */
    .step:nth-child(2n)::after {
        right: 0;
    }
}

@media (max-width: 620px) {
    .steps {
        grid-template-columns: 1fr;
        gap: 1.5rem;
    }

    .step::after {
        right: 0;
    }
}

/* ---------- signals / fact strip ---------- */
.signals {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1.75rem;
}

@media (max-width: 900px) {
    .signals {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 480px) {
    .signals {
        grid-template-columns: 1fr;
    }
}

/* The rule is drawn as a pseudo-element rather than a border so it can scale:
   a border cannot be animated without relaying out the text beside it. */
.signal {
    position: relative;
    padding-left: 1.1rem;
}

.signal::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 2px;
    background: var(--kz-accent-ui);
    /* Scales rightward from the rule, not outward from its middle. */
    transform-origin: left center;
    transition: transform var(--kz-dur-slow) var(--kz-ease-out);
}

.signal:hover::before {
    transform: scaleX(2);
}

.signal h3 {
    font-family: var(--kz-font-mono);
    font-weight: 500;
    font-size: var(--kz-text-md);
    letter-spacing: var(--kz-tracking-normal);
    margin-bottom: 0.3em;
}

.signal p {
    color: var(--kz-text-muted);
    font-size: var(--kz-text-md);
    margin: 0;
}

/* ---------- CTA banner ----------
   The one place on the page allowed to be a surface rather than a plane: a
   brand wash out of the top-right corner and an inner hairline that reads as a
   lit edge. A bled brand mark was tried here and cut — clipped by the
   container it read as two faint diagonal scratches, and an ambiguous mark is
   worse than none. */
.cta-banner {
    position: relative;
    overflow: hidden;
    border-radius: var(--kz-radius-xl);
    padding: clamp(2rem, 1.5rem + 3vw, 3.5rem);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem 2rem;
    /* A light source in the corner, not a gradient panel. At 16% over a 38rem
       radius this read as a white-to-blue wash across half the card, which is
       the one place the design brushed against the generic "blue gradient"
       look; pulled in and down to a tint it reads as illumination. */
    background:
        radial-gradient(22rem 14rem at 100% 0%, color-mix(in srgb, var(--kz-accent) 11%, transparent), transparent 72%),
        var(--kz-surface);
    /* Two rings, not a border: an outer hairline for the edge and an inset
       highlight for the lip. This is what makes it read as a raised object. */
    box-shadow:
        inset 0 0 0 1px var(--kz-border),
        inset 0 1px 0 0 color-mix(in srgb, var(--kz-surface) 70%, #fff),
        var(--kz-shadow-md);
}

.cta-banner h2 {
    margin-bottom: 0.25em;
}

.cta-banner p {
    color: var(--kz-text-muted);
    max-width: 52ch;
    margin: 0;
}

/* ---------- forms ---------- */
.form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.1rem;
}

@media (max-width: 620px) {
    .form-grid {
        grid-template-columns: 1fr;
    }
}

.form-field {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.form-field--full {
    grid-column: 1 / -1;
}

.form-field label {
    font-weight: 600;
    font-size: var(--kz-text-md);
}

.form-field input[type="text"],
.form-field input[type="email"],
.form-field textarea {
    font: inherit;
    color: var(--kz-text);
    background: var(--kz-field-bg);
    /* A control's own boundary is a non-text mark that carries meaning, so it
       spends the 3:1 border rather than the hairline used between panels. */
    border: 1px solid var(--kz-field-border);
    border-radius: var(--kz-field-radius);
    padding: var(--kz-field-padding);
    width: 100%;
    transition: border-color var(--kz-dur-fast) var(--kz-ease-standard),
                box-shadow var(--kz-dur-fast) var(--kz-ease-standard);
}

.form-field textarea {
    resize: vertical;
    min-height: 9rem;
}

.form-field input:hover:not(:focus),
.form-field textarea:hover:not(:focus) {
    border-color: var(--kz-text-muted);
}

.form-field input:focus,
.form-field textarea:focus {
    outline: var(--kz-ring-width) solid var(--kz-ring-color);
    outline-offset: 0;
    border-color: var(--kz-ring-color);
}

.form-field .field-hint {
    font-weight: 400;
    color: var(--kz-text-muted);
}

/* .form-field is a stretch column so inputs fill it. A button is not an input:
   left it, at its own width, or the primary action reads as a banner. */
.form-field > .btn {
    align-self: flex-start;
}

.field-validation-error {
    color: var(--kz-danger);
    font-size: var(--kz-text-sm);
}

.input-validation-error {
    border-color: var(--kz-danger) !important;
    /* Colour alone does not carry an error state; the ring doubles it so the
       field is still obviously wrong without red/green discrimination. */
    box-shadow: 0 0 0 1px var(--kz-danger);
}

.validation-summary-errors {
    border: 1px solid var(--kz-danger);
    border-left-width: 3px;
    border-radius: var(--kz-radius-sm);
    background: color-mix(in srgb, var(--kz-danger) 8%, var(--kz-surface));
    color: var(--kz-danger);
    padding: 0.85rem 1.1rem;
    margin-bottom: 1.25rem;
}

.validation-summary-errors ul {
    margin: 0;
    padding-left: 1.2rem;
}

.validation-summary-valid {
    display: none;
}

.consent-row {
    display: flex;
    gap: 0.6rem;
    align-items: flex-start;
}

.consent-row input[type="checkbox"] {
    width: 1.15rem;
    height: 1.15rem;
    margin-top: 0.2rem;
    accent-color: var(--kz-accent-ui);
    flex-shrink: 0;
}

.consent-row label {
    font-weight: 400;
    font-size: var(--kz-text-md);
    color: var(--kz-text-muted);
}

/* honeypot — visually removed, still submittable by naive bots */
.hp-field {
    position: absolute !important;
    left: -6000px !important;
    top: auto !important;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.form-note {
    font-size: var(--kz-text-sm);
    color: var(--kz-text-muted);
    margin-top: 0.75rem;
    margin-bottom: 0;
}

.alert {
    border-radius: var(--kz-radius-sm);
    padding: 0.9rem 1.1rem;
    margin-bottom: 1.5rem;
    border: 1px solid;
    border-left-width: 3px;
}

.alert--success {
    border-color: var(--kz-ok);
    background: color-mix(in srgb, var(--kz-ok) 10%, var(--kz-surface));
    color: var(--kz-ok);
}

/* ---------- contact layout ---------- */
.contact-layout {
    display: grid;
    grid-template-columns: minmax(16rem, 2fr) 3fr;
    gap: 1.5rem;
    align-items: start;
}

/* The form is four times the height of the panel beside it, so the panel
   follows you down instead of leaving a column of nothing next to the fields
   you are filling in. Sticky, not fixed: it stops at the end of the section. */
@media (min-width: 821px) {
    .contact-info {
        position: sticky;
        top: 5.5rem;
    }
}

@media (max-width: 820px) {
    .contact-layout {
        grid-template-columns: 1fr;
    }
}

.contact-info .contact-mail {
    font-family: var(--kz-font-mono);
    font-size: var(--kz-text-md);
    word-break: break-all;
}

.contact-info dl {
    margin: 1.25rem 0 0;
}

.contact-info dt {
    font-family: var(--kz-font-mono);
    font-weight: 500;
    font-size: var(--kz-text-2xs);
    letter-spacing: var(--kz-tracking-label);
    text-transform: uppercase;
    color: var(--kz-text-subtle);
}

.contact-info dd {
    margin: 0.2rem 0 1rem;
    color: var(--kz-text-muted);
    font-size: var(--kz-text-md);
}

/* ---------- prose (legal pages, about) ---------- */
.prose {
    max-width: 48rem;
}

/* ~64ch. The container is 48rem wide for the headings and lists to work
   against, but running body copy that full width crosses 80 characters. */
.prose p,
.prose li {
    max-width: 64ch;
}

.prose h2 {
    margin-top: 2.2em;
    font-size: var(--kz-text-display-sm);
}

.prose h3 {
    margin-top: 1.6em;
}

.prose ul {
    padding-left: 1.3rem;
}

.prose li {
    margin-bottom: 0.4rem;
}

.prose li::marker {
    color: var(--kz-accent-text);
}

.prose address {
    font-style: normal;
}

/* legal placeholders that MUST be completed before go-live */
.placeholder {
    background: color-mix(in srgb, #e6a700 25%, var(--kz-surface));
    border: 1px dashed #b98a00;
    border-radius: var(--kz-radius-xs);
    padding: 0 0.35rem;
    font-family: var(--kz-font-mono);
    font-size: 0.9em;
    color: var(--kz-text);
}

.notice {
    border: 1px solid var(--kz-border);
    border-left: 3px solid var(--kz-accent-ui);
    border-radius: var(--kz-radius-sm);
    background: var(--kz-surface);
    padding: 0.95rem 1.15rem;
    margin-bottom: 1.75rem;
    color: var(--kz-text-muted);
    font-size: var(--kz-text-md);
}

.notice p:last-child {
    margin-bottom: 0;
}

/* ---------- brand ground ----------
   The treatment the home hero established — glow, drafting grid, one bled
   chevron — extracted so every page that opens on it renders the SAME thing.
   Rendered by Pages/Shared/_BrandGround.cshtml; see that file for the markup
   and why it is one chevron rather than the lockup's pair.

   It paints no opaque background, so it composes over whatever --kz-bg the page
   already has, and it is inert: decorative, aria-hidden, pointer-events none,
   and behind everything at the base layer. */
.kz-ground {
    position: absolute;
    inset: 0;
    z-index: var(--kz-z-base);
    overflow: hidden;
    pointer-events: none;
    background: radial-gradient(42rem 20rem at 88% -30%, var(--kz-hero-glow), transparent 62%);
}

/* For a standalone full-page layout (sign-in, /share, /setup) there is no
   positioned parent to fill, so the ground covers the viewport instead — and
   everything scales up with it. The band's values are tuned for a strip a few
   hundred pixels tall; at viewport height the same mark and glow are lost. */
.kz-ground--fixed {
    position: fixed;
    background: radial-gradient(58rem 34rem at 50% -18%, var(--kz-hero-glow), transparent 66%);
}

.kz-ground--fixed::before {
    background-size: 5.5rem 5.5rem;
    -webkit-mask-image: radial-gradient(70rem 48rem at 50% 12%, #000 0%, transparent 78%);
    mask-image: radial-gradient(70rem 48rem at 50% 12%, #000 0%, transparent 78%);
}

/* The mark scales with the page. One glyph, still on the trailing edge — the
   card these layouts centre is narrow, so there is room beside it without the
   mark competing with the content. */
.kz-ground--fixed .kz-ground__mark {
    width: clamp(11rem, 20vw, 18rem);
    right: clamp(-7rem, -5vw, -3rem);
}

.kz-ground::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(to right, var(--kz-mark-veil) 1px, transparent 1px),
        linear-gradient(to bottom, var(--kz-mark-veil) 1px, transparent 1px);
    background-size: 4.5rem 4.5rem;
    /* Fades toward the bottom-right rather than ending on an edge. */
    -webkit-mask-image: linear-gradient(160deg, #000 0%, rgba(0, 0, 0, 0.4) 50%, transparent 82%);
    mask-image: linear-gradient(160deg, #000 0%, rgba(0, 0, 0, 0.4) 50%, transparent 82%);
}

.kz-ground__mark {
    position: absolute;
    right: clamp(-4rem, -2.5vw, -1.5rem);
    top: 50%;
    translate: 0 -50%;
    width: clamp(5rem, 9vw, 7.5rem);
    color: var(--kz-mark-veil);
}

/* ---------- page band (subpages) ----------
   The ground at roughly half the hero's height, with a rule under it. Every
   page opens the same way, which is what makes five subpages feel like one
   site rather than five documents that share a header. */
.page-band {
    position: relative;
    isolation: isolate;
    border-bottom: 1px solid var(--kz-border);
    background: var(--kz-bg);
}

.page-head {
    position: relative;
    z-index: var(--kz-z-raised);
    padding-block: clamp(2.75rem, 2rem + 3vw, 4.5rem);
}

/* Standalone layouts put their content in normal flow over a fixed ground; it
   only needs to win the stacking order.

   :not(.kz-ground) is load-bearing — the ground is itself a direct child of
   these bodies, and without the exclusion this rule overrode its
   `position: fixed` with `relative`, which collapsed an inset: 0 element to
   0×0. It was still in the DOM, still had its background declared, and painted
   absolutely nothing. Measured, not spotted by eye. */
.kz-share > *:not(.kz-ground),
.kz-setup > *:not(.kz-ground),
.portal-login-body > *:not(.kz-ground) {
    position: relative;
    z-index: var(--kz-z-raised);
}

.page-head h1 {
    margin-bottom: 0.4em;
}

.page-head p {
    color: var(--kz-text-muted);
    font-size: var(--kz-text-lg);
    max-width: 56ch;
    margin-bottom: 0;
}

/* The band arrives on the same staggered beat as the hero, so navigating
   between pages feels like one product rather than a page reload. */
@media (prefers-reduced-motion: no-preference) {
    .page-head .eyebrow,
    .page-head h1,
    .page-head p {
        animation: kz-enter var(--kz-dur-enter) var(--kz-ease-out) backwards;
    }

    .page-head h1 {
        animation-delay: 70ms;
    }

    .page-head p {
        animation-delay: 140ms;
    }

    .kz-ground__mark {
        animation: kz-veil 1.1s var(--kz-ease-out) 200ms backwards;
    }
}

@supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
        .kz-ground__mark {
            animation: kz-veil 1.1s var(--kz-ease-out) 200ms backwards,
                       kz-drift-far linear both;
            /* scroll(root block), for the same reason as the hero: .kz-ground
               sets overflow: hidden, which makes it a scroll container — and
               one that never scrolls, so a bare scroll() would bind the drift
               to a timeline that never advances. */
            animation-timeline: auto, scroll(root block);
            animation-range: normal, 0 60vh;
        }

        .kz-ground::before {
            animation: kz-drift-near linear both;
            animation-timeline: scroll(root block);
            animation-range: 0 60vh;
        }
    }
}

/* The section that follows a band does not need its own top padding — the band
   has already given the page its opening breath. */
.page-band + .section {
    padding-block-start: clamp(2.5rem, 1.75rem + 3vw, 4rem);
}

/* ---------- footer ---------- */
.site-footer {
    margin-top: auto;
    position: relative;
    z-index: var(--kz-z-raised);
    background: var(--kz-surface);
    border-top: 1px solid var(--kz-border);
    padding-block: 3rem 1.5rem;
    font-size: var(--kz-text-md);
}

.site-footer__grid {
    display: grid;
    grid-template-columns: 2fr 1fr 1fr;
    gap: 2rem;
    margin-bottom: 2rem;
}

@media (max-width: 720px) {
    .site-footer__grid {
        grid-template-columns: 1fr;
        gap: 1.75rem;
    }
}

.site-footer h2 {
    font-family: var(--kz-font-mono);
    font-weight: 500;
    font-size: var(--kz-text-2xs);
    letter-spacing: var(--kz-tracking-label);
    text-transform: uppercase;
    color: var(--kz-text-subtle);
    margin-bottom: 0.9rem;
}

.site-footer ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

.site-footer li {
    margin-bottom: 0.45rem;
}

.site-footer a {
    color: var(--kz-text-muted);
    transition: color var(--kz-dur-fast) var(--kz-ease-standard);
}

.site-footer a:hover {
    color: var(--kz-accent-text);
}

.site-footer .footer-tagline {
    color: var(--kz-text-muted);
    max-width: 30ch;
    margin-top: 0.9rem;
}

.footer-mail {
    font-family: var(--kz-font-mono);
    font-size: var(--kz-text-sm);
}

.site-footer__bottom {
    border-top: 1px solid var(--kz-border);
    padding-top: 1.25rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.5rem;
    justify-content: space-between;
    color: var(--kz-text-subtle);
    font-size: var(--kz-text-sm);
}

/* ---------- error page ----------
   A dead end is still a page of this site, and these two (the 404/500 and the
   expired-QR 410) were the only public pages with no ground under them at all.
   The band fills the content area rather than sitting as a header, because
   there is no content below it to separate from. */
.error-band {
    position: relative;
    isolation: isolate;
    flex: 1 0 auto;
    display: flex;
    align-items: center;
    background: var(--kz-bg);
}

.error-band > .container {
    position: relative;
    z-index: var(--kz-z-raised);
    width: min(100% - 2.5rem, var(--kz-container));
}

.error-card {
    max-width: 36rem;
    margin: clamp(3rem, 8vh, 6rem) auto;
    text-align: center;
}

.error-card h1 {
    font-size: var(--kz-text-display-md);
    margin-bottom: 0.5em;
}

.error-card p {
    margin-inline: auto;
    max-width: 46ch;
}

/* The status number is the fastest way to say which wall you hit; it is set
   large, in mono, and deliberately quiet so the sentence under it still leads. */
.error-code {
    display: block;
    font-family: var(--kz-font-mono);
    font-weight: 500;
    font-size: clamp(3.5rem, 2rem + 7vw, 6rem);
    line-height: 1;
    letter-spacing: var(--kz-tracking-display);
    color: var(--kz-border-strong);
    margin-bottom: 0.6rem;
}

.error-card .brand__chevrons {
    justify-content: center;
    margin-bottom: 1.25rem;
}

.error-card .brand__chev {
    width: 1rem;
    height: 2.2rem;
}

.error-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.4rem;
    justify-content: center;
    align-items: center;
    margin-top: 1.75rem;
}

/* ---------- mobile navigation ----------
   Breakpoint at 880px: below it the DE nav strings + brand + header tools do
   not reliably fit one row (W-PUB-06), so the nav collapses into the panel. */
@media (max-width: 880px) {
    .nav-toggle {
        display: inline-flex;
        margin-left: auto;
    }

    .site-nav {
        display: none;
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        flex-direction: column;
        align-items: stretch;
        gap: 0.15rem;
        background: var(--kz-surface);
        border-bottom: 1px solid var(--kz-border);
        box-shadow: var(--kz-shadow-lg);
        padding: 0.75rem 1.25rem 1rem;
        margin-left: 0;
    }

    .site-nav.is-open {
        display: flex;
        /* The panel is the one element on the public site that arrives rather
           than appears: it comes from the header edge it is attached to. */
        animation: kz-panel-in var(--kz-dur-slow) var(--kz-ease-out) both;
        transform-origin: top center;
    }

    .site-nav a {
        padding: 0.7rem 0.8rem;
    }

    .site-nav a[aria-current="page"]::after {
        left: 0.8rem;
        right: auto;
        top: 0.7rem;
        bottom: 0.7rem;
        width: 2px;
        height: auto;
    }

    .brand__word {
        display: none;
    }

    .site-nav .login-link {
        display: block;
        border-top: 1px solid var(--kz-border);
        border-radius: 0;
        margin-top: 0.35rem;
        padding-top: 0.85rem;
        color: var(--kz-text-muted);
        font-size: var(--kz-text-sm);
    }
}

@keyframes kz-panel-in {
    from {
        opacity: 0;
        transform: translateY(-0.5rem);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Auth links render once inside #site-nav (see _AuthLinks.cshtml); on desktop
   they sit at the end of the nav row in the compact login-link style. */
/* The sign-in link is a real destination, not a footnote. It was set two steps
   quieter than its neighbours — smaller AND more muted — which made the only
   account entry point on the public site read as disabled. It keeps a quieter
   weight than the nav proper, but at the nav's size and colour, separated by a
   rule rather than by being faded out. */
@media (min-width: 881px) {
    .site-nav a.login-link {
        color: var(--kz-text-muted);
        font-size: var(--kz-text-md);
        margin-left: 0.5rem;
        padding-left: 1rem;
        border-left: 1px solid var(--kz-border);
        border-radius: 0;
    }

    /* No text-decoration here. The nav's drawn hover mark already underlines
       this link, and the two together rendered as a DOUBLE underline on
       "Dashboard" — the one nav item that is both a .login-link and eligible
       for the mark. One underline, and it is the designed one. */
    .site-nav a.login-link:hover {
        color: var(--kz-text);
        background: none;
    }

    /* The mark spans the label, not the separator rule and its padding. */
    .site-nav a.login-link:not([aria-current="page"])::after {
        left: 1rem;
        right: 0;
    }
}

/* ---------- scroll reveal ----------
   Scroll-driven CSS, so there is no observer, no script and nothing to run on
   the main thread. Three guards, all of them load-bearing:
     1. @supports — a browser without view() timelines never sees the rule, so
        it renders the final state. Content is never hidden by a feature that
        is not there.
     2. The range ends early in the element's entry, so a block finishes well
        before it is comfortably readable. This is orientation, not a curtain.
     3. reduced-motion drops the whole thing.

   ON THE OPACITY. An earlier cut animated position only, on the argument that
   a fade could leave a block invisible. That was an over-correction: what it
   actually protected against is a full-page CAPTURE — a tool that renders the
   whole document without ever scrolling it — and not any state a person can be
   in. An inactive timeline resolves to the base style, which is visible, and
   that was verified in the browser rather than assumed: on a viewport taller
   than the document every one of these elements computes to opacity 1. The
   price of the safer version was that nobody could perceive it, which is its
   own kind of failure. Position, opacity and a touch of blur, then — the three
   together are what make an element read as arriving rather than as sliding.

   The groups stagger. Three peers revealing in unison read as one object
   moving; a beat between them reads as a list arriving. */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        .section__head,
        .reveal {
            animation: kz-rise linear both;
            animation-timeline: view();
            animation-range: entry 0% entry 26%;
        }

        /* A section head leads its own section, so it arrives first and the
           items under it follow. */
        .reveal:nth-child(2) {
            animation-range: entry 4% entry 30%;
        }

        .reveal:nth-child(3) {
            animation-range: entry 8% entry 34%;
        }
    }
}

@keyframes kz-rise {
    from {
        opacity: 0;
        transform: translateY(1.75rem);
        filter: blur(6px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
        filter: blur(0);
    }
}

/* ---------- hero entrance ----------
   The page arrives once per visit, which is exactly the frequency at which
   expressive motion earns its place — and the hero is the only thing on the
   site that gets it. Time-based, not scroll-driven: an entrance has to run
   whether or not there is anything to scroll.

   The base state of every one of these elements is visible. The animation
   lives entirely inside the no-preference branch and fills BACKWARDS, so the
   only way to reach the hidden state is for the animation to actually be
   running. Nothing here can strand content.

   The order is the reading order — label, headline, promise, actions — with a
   beat between each, so the hero assembles in the sequence you would read it
   in rather than appearing all at once. */
@media (prefers-reduced-motion: no-preference) {
    .hero .eyebrow,
    .hero h1,
    .hero__lead,
    .hero__cta {
        animation: kz-enter var(--kz-dur-enter) var(--kz-ease-out) backwards;
    }

    .hero h1 {
        animation-delay: 70ms;
    }

    .hero__lead {
        animation-delay: 140ms;
    }

    .hero__cta {
        animation-delay: 210ms;
    }

    /* The mark is scenery: it comes in last and only fades. A large shape
       translating behind the headline would pull the eye off it. */
    .hero__mark {
        animation: kz-veil 1.1s var(--kz-ease-out) 260ms backwards;
    }
}

@keyframes kz-enter {
    from {
        opacity: 0;
        transform: translateY(1.25rem);
        filter: blur(8px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
        filter: blur(0);
    }
}

@keyframes kz-veil {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

/* ---------- hero parallax ----------
   The mark and the drafting grid drift at different rates as the hero leaves,
   which is what gives the band depth instead of the flatness of a printed
   sheet. Scroll-driven, so it moves exactly as fast as the scroll and stops
   when the scroll stops; and confined to the first screen, so it costs nothing
   for the rest of the page.

   .hero__mark is centred with the `translate` property rather than `transform`,
   so the drift and the centring do not collide. */
@supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
        .hero__mark {
            animation: kz-veil 1.1s var(--kz-ease-out) 260ms backwards,
                       kz-drift-far linear both;
            /* scroll(root block), NOT the bare scroll(). The bare form binds to
               the nearest scroll CONTAINER, and .hero sets overflow: hidden —
               which makes it one, and one that never scrolls. The timeline was
               therefore permanently inactive and the drift silently did
               nothing. Measured, not assumed: the mark computed transform:none
               at every scroll position until this was named explicitly. */
            animation-timeline: auto, scroll(root block);
            animation-range: normal, 0 70vh;
        }

        .hero::before {
            animation: kz-drift-near linear both;
            animation-timeline: scroll(root block);
            animation-range: 0 70vh;
        }
    }
}

@keyframes kz-drift-far {
    to {
        transform: translateY(-2.5rem);
    }
}

@keyframes kz-drift-near {
    to {
        transform: translateY(-1rem);
    }
}

/* ---------- motion ---------- */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
    }
}

/* ---------- FAQ (native <details>/<summary>, zero JS — W-PUB-26) ---------- */
/* Full container width, so the disclosure chevrons park on the same right edge
   as the panels and the CTA above them — three different right edges on one
   page is what made this section look pasted in.

   The measure belongs to the ANSWER only. Putting a max-width on .faq__question
   as well capped the whole flex ROW, so the chevron stopped at the end of the
   text box and floated in the middle of a full-width rule — which looked more
   broken than the misalignment it was meant to fix. A question is a single-line
   heading; it does not need a measure. */
.faq {
    border-top: 1px solid var(--kz-border);
}

.faq__answer p {
    max-width: 62ch;
}

/* Rows on a ruled sheet rather than a stack of floating cards: an FAQ is a
   list, and a list does not need nine shadows to say so. */
.faq__item {
    border-bottom: 1px solid var(--kz-border);
}

.faq__question {
    cursor: pointer;
    padding: 1.15rem 0.25rem;
    font-weight: 600;
    list-style: none;
    display: flex;
    /* The chevron is pushed to the container edge while the question text keeps
       its measure — hence max-width on the text and auto margin on the mark. */
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    transition: color var(--kz-dur-fast) var(--kz-ease-standard);
}

.faq__question:hover {
    color: var(--kz-accent-text);
}

/* Hide the native marker in both engines, then draw our own chevron. */
.faq__question::-webkit-details-marker {
    display: none;
}

/* A real chevron built from two borders — same shape as the brand mark, and
   it can rotate, which a "+"/"−" character swap cannot. */
.faq__question::after {
    content: "";
    flex: none;
    margin-left: auto;
    width: 0.5rem;
    height: 0.5rem;
    margin-right: 0.35rem;
    border-right: 2px solid var(--kz-accent-ui);
    border-bottom: 2px solid var(--kz-accent-ui);
    transform: rotate(45deg) translate(-0.1rem, -0.1rem);
    transition: transform var(--kz-dur-base) var(--kz-ease-standard);
}

.faq__item[open] > .faq__question::after {
    transform: rotate(225deg) translate(-0.1rem, -0.1rem);
}

.faq__question:focus-visible {
    outline: var(--kz-ring-width) solid var(--kz-ring-color);
    outline-offset: -2px;
    border-radius: var(--kz-radius-sm);
}

.faq__answer {
    padding: 0 3rem 1.3rem 0.25rem;
    color: var(--kz-text-muted);
}

.faq__answer p {
    margin: 0;
}

/* THE ACCORDION ACTUALLY OPENS.
   A <details> toggles instantly by default, which is the single most-clicked
   interaction on the public site behaving like a light switch. ::details-content
   plus interpolate-size makes it a real disclosure with no JavaScript at all.

   interpolate-size is scoped to .faq rather than :root: it changes how EVERY
   auto-to-length transition in its subtree behaves, and that is not a licence
   worth granting the whole document for one component.

   Guarded, and the guard matters — without ::details-content support the panel
   simply appears, which is the behaviour we had before. Opening is 260ms and
   closing 180ms: the content you asked for deserves the time, the content you
   dismissed does not. */
@supports (interpolate-size: allow-keywords) and (selector(::details-content)) {
    @media (prefers-reduced-motion: no-preference) {
        .faq {
            interpolate-size: allow-keywords;
        }

        .faq__item::details-content {
            block-size: 0;
            overflow: hidden;
            opacity: 0;
            transition: block-size var(--kz-dur-base) var(--kz-ease-standard),
                        opacity var(--kz-dur-fast) var(--kz-ease-standard),
                        content-visibility var(--kz-dur-base) allow-discrete;
        }

        .faq__item[open]::details-content {
            block-size: auto;
            opacity: 1;
            transition-duration: var(--kz-dur-slow), var(--kz-dur-base), var(--kz-dur-slow);
        }
    }
}

/* ---------- project vignettes (W-PUB-23 — placeholder content) ---------- */
.case__meta {
    font-family: var(--kz-font-mono);
    font-size: var(--kz-text-xs);
    letter-spacing: var(--kz-tracking-normal);
    color: var(--kz-text-muted);
    margin-bottom: 0.5rem;
}

.case {
    margin: 0.75rem 0 0;
}

.case dt {
    font-family: var(--kz-font-mono);
    font-size: var(--kz-text-2xs);
    text-transform: uppercase;
    letter-spacing: var(--kz-tracking-label);
    font-weight: 500;
    color: var(--kz-accent-text);
    margin-top: 0.7rem;
}

.case dd {
    margin: 0.2rem 0 0;
    color: var(--kz-text-muted);
    font-size: var(--kz-text-md);
}

/* ---------- print (legal pages are the ones people archive — W-PUB-27) ---------- */
@media print {
    /* Chrome/Edge honour this; it drops the dark theme's backgrounds for paper. */
    :root {
        color-scheme: light;
    }

    .site-header,
    .site-footer,
    .skip-link,
    .hero__mark,
    .nav-toggle,
    #theme-toggle,
    .lang-switch,
    .cta-banner {
        display: none !important;
    }

    body {
        background: #fff;
        color: #000;
    }

    main,
    .section,
    .page-head {
        padding: 0;
        margin: 0;
    }

    .container {
        max-width: none;
        padding: 0;
    }

    .prose {
        max-width: none;
    }

    /* Keep headings with the text they introduce. */
    h1, h2, h3 {
        page-break-after: avoid;
        break-after: avoid;
    }

    p, li, address {
        page-break-inside: avoid;
        break-inside: avoid;
    }

    a {
        color: #000;
        text-decoration: underline;
    }

    /* A printed page has no hyperlinks — spell the target out. Skip in-page
       and mailto/tel links, whose text already carries the address. */
    .prose a[href^="http"]::after {
        content: " (" attr(href) ")";
        font-size: 0.85em;
        word-break: break-all;
    }

    /* Unfilled legal placeholders must stay obvious on paper too, where the
       amber highlight may not print. */
    .placeholder {
        background: transparent;
        border: 1px dashed #000;
        color: #000;
    }

    .notice {
        border: 1px solid #000;
        background: transparent;
    }

    /* Nothing on paper is revealed by scrolling. */
    .section__head,
    .reveal {
        animation: none !important;
        opacity: 1 !important;
        transform: none !important;
    }
}

/* ---------- wide-screen scaling ----------
   The design is rem-based throughout (type, spacing, and the content
   containers), so stepping the root font up on large viewports scales the
   whole layout proportionally instead of leaving a small island of 16px UI
   in an ocean of empty space. Steps, not vw-fluid: predictable rendering at
   common monitor widths, and no drift on odd window sizes. Loaded by both
   the public site and the portal (site.css is included everywhere). */
@media (min-width: 1920px) {
    html {
        font-size: 17px;
    }
}

@media (min-width: 2560px) {
    html {
        font-size: 19px;
    }
}

@media (min-width: 3400px) {
    html {
        font-size: 21px;
    }
}

/* ---------- form busy overlay (contact form) ----------
   Server-rendered inside the form, unhidden by contact-validation.js while
   the submission (two SMTP sends) is in flight. */
.form-busyable {
    position: relative;
}

.form-busy {
    position: absolute;
    inset: 0;
    z-index: var(--kz-z-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    background: color-mix(in srgb, var(--kz-bg) 78%, transparent);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    border-radius: var(--kz-radius-md);
    font-weight: 600;
}

.form-busy[hidden] {
    display: none;
}

/* The one looping animation in the product, and it exists because a
   determinate progress value is not available for an SMTP round trip. */
.form-busy__spinner {
    width: 1.4rem;
    height: 1.4rem;
    border-radius: 50%;
    border: 2.5px solid var(--kz-border);
    border-top-color: var(--kz-accent-ui);
    animation: kz-spin 0.8s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
    .form-busy__spinner {
        /* Slowed rather than stopped: it is a liveness indicator, and a frozen
           one would say the opposite of what it is there to say.

           BOTH properties have to be reclaimed. The blanket rule above sets
           `animation-iteration-count: 1 !important` as well as the duration,
           and an !important declaration only loses to another !important
           declaration for the SAME property — so overriding the duration alone
           left the spinner making exactly one slow turn and then stopping dead,
           mid-request, which is precisely the failure this rule exists to
           prevent. */
        animation-duration: 2.4s !important;
        animation-iteration-count: infinite !important;
    }
}
