/*
 * Tłumacz EPUB — design system.
 *
 * Every colour, radius, type step, shadow and duration is a custom property
 * declared once in :root and overridden once in the dark-mode block. Nothing
 * below those two blocks may contain a literal colour — that is what keeps
 * the contrast test (tests/test_frontend_assets.py) able to prove the whole
 * palette, in both schemes, from the tokens alone.
 *
 * Tailwind supplies layout utilities only (flex, grid, gap, padding, sr-only).
 * Colour, type and every component live here.
 */

/* ======================================================== 1. tokens (light) */

:root {
    color-scheme: light dark;

    /* --- content layer ---------------------------------------------------- */
    --bg-base: #FFFFFF;
    --bg-grouped: #F5F5F7;
    --bg-elevated: #FFFFFF;
    --bg-toolbar: rgba(251, 251, 253, 0.72);
    --bg-toolbar-solid: #FBFBFD;

    /* --- labels ----------------------------------------------------------- */
    --label: #1D1D1F;
    --label-secondary: #515154;
    --label-tertiary: #6E6E73;
    --label-quaternary: #86868B;     /* non-text only: disabled, decoration */

    /* --- separators & control outlines ------------------------------------ */
    --separator: #D2D2D7;
    --separator-opaque: #C6C6C8;
    --border-control: #86868B;

    /* --- fills ------------------------------------------------------------ */
    /* --fill-tertiary .. --fill-primary are drawn on a white card, where a
       light tint is enough. --fill-on-grouped is for a fill that has to read
       as a SHAPE wherever it lands — on the page, on a card or on the
       toolbar: the segmented track, a count badge, a glyph tile. At 1.03:1
       --fill-tertiary simply vanishes on --bg-grouped. */
    --fill-tertiary: #F2F2F4;        /* text-bearing */
    --fill-secondary: #E8E8ED;       /* non-text */
    --fill-primary: #DEDEE3;         /* non-text */
    --fill-on-grouped: #D6D6DC;      /* shape fill; 1.33:1 on the page */
    --fill-on-grouped-hover: #C7C7CE;
    --fill-on-grouped-active: #BBBBC4;

    /* --- accent ----------------------------------------------------------- */
    /* Hover and press go DARKER in both schemes. Apple's web hover (#0077ED)
       is lighter than the rest state, which drops white-on-blue to 4.32:1 —
       below AA — so the ladder is inverted here and kept identical in dark
       mode, where a lighter hover is impossible at AA over a #0071E3 rest. */
    --accent: #0071E3;
    --accent-hover: #0068D0;
    --accent-active: #005BBB;
    --accent-text: #0066CC;
    --on-accent: #FFFFFF;

    /* --- semantic status: text and glyphs only ---------------------------- */
    --green: #00792C;
    --orange: #B25000;
    --red: #D70015;
    --red-hover: #BF0013;
    --on-status: #FFFFFF;

    /* --- derived roles ---------------------------------------------------- */
    --ring: var(--accent);
    --progress-fill: var(--accent);
    /* The drawn edge for prefers-contrast: more. A derived role, so the
       dark block inherits it from --label-secondary without a second
       declaration, and the contrast tests can still resolve it. */
    --border-contrast: var(--label-secondary);
    /* Apple's switch green. A switch track is a non-text object and the
       state is carried by the knob position, so --green (a text colour,
       picked for 5.57:1 on white and therefore a dark bottle green) is the
       wrong tool. The hairline keeps the control's boundary at 3:1. */
    --switch-on: #34C759;
    --switch-on-border: #248A3D;
    --seg-selected-bg: var(--bg-base);
    --knob: #FFFFFF;
    --scrollbar-thumb: rgba(0, 0, 0, 0.22);
    --scrollbar-thumb-hover: rgba(0, 0, 0, 0.36);

    /* --- elevation -------------------------------------------------------- */
    --shadow-toast: 0 8px 28px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.08);
    --shadow-seg: 0 1px 2px rgba(0, 0, 0, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
    --shadow-knob: 0 3px 8px rgba(0, 0, 0, 0.15), 0 1px 1px rgba(0, 0, 0, 0.16);

    /* --- hairline --------------------------------------------------------- */
    --hairline: 1px;

    /* --- type ------------------------------------------------------------- */
    --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
        "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
        "Liberation Mono", monospace;

    /* --- spacing (4px rhythm) --------------------------------------------- */
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 20px;
    --sp-6: 24px;
    --sp-8: 32px;
    --sp-10: 40px;
    --sp-12: 48px;
    --sp-16: 64px;

    /* --- radii ------------------------------------------------------------ */
    --r-xs: 6px;
    --r-sm: 8px;
    --r-md: 10px;
    --r-lg: 12px;
    --r-xl: 18px;
    --r-full: 980px;

    /* --- motion ----------------------------------------------------------- */
    /* --dur-tick is for a value the user is already watching arrive: a
       transition longer than the gap between two updates shows the
       previous number, not the current one. */
    --dur-tick: 100ms;
    --dur-fast: 150ms;
    --dur-base: 250ms;
    --dur-slow: 350ms;
    --ease-out: cubic-bezier(0.00, 0.00, 0.20, 1.00);
    --ease-in: cubic-bezier(0.40, 0.00, 1.00, 1.00);
    --ease-standard: cubic-bezier(0.40, 0.00, 0.20, 1.00);
    --ease-spring: cubic-bezier(0.34, 1.20, 0.64, 1.00);

    /* --- layout ----------------------------------------------------------- */
    /* The two sticky bars. Both the bars themselves and the scroll padding
       that keeps a focused control out from under them are sized from
       these, so they cannot drift apart. */
    --toolbar-h: 48px;
    /* The bar is one .btn-cta (44px) between two var(--sp-3) paddings. Both
       were guesses before — 72px here and 116px on a phone against a bar
       that measures 68 — and the toast dock that reads this landed on
       "Zapisz ustawienia". */
    --save-bar-h: 68px;
    --col-page: 980px;
    --col-form: 692px;
    --col-login: 420px;
    --gutter: var(--sp-4);
    --blur-toolbar: saturate(180%) blur(20px);
}

/* ========================================================= 2. tokens (dark) */

@media (prefers-color-scheme: dark) {
    :root {
        --bg-base: #000000;
        --bg-grouped: #000000;
        --bg-elevated: #1C1C1E;
        --bg-toolbar: rgba(22, 22, 23, 0.72);
        --bg-toolbar-solid: #161617;

        --label: #F5F5F7;
        --label-secondary: #AEAEB2;
        --label-tertiary: #98989D;
        --label-quaternary: #8E8E93;

        --separator: #38383A;
        --separator-opaque: #48484A;
        --border-control: #79797D;

        --fill-tertiary: #2C2C2E;
        --fill-secondary: #3A3A3C;
        --fill-primary: #48484A;
        --fill-on-grouped: #3A3A3C;
        --fill-on-grouped-hover: #48484A;
        --fill-on-grouped-active: #545456;

        --accent: #0071E3;
        --accent-hover: #0068D0;
        --accent-active: #005BBB;
        --accent-text: #2997FF;
        --on-accent: #FFFFFF;

        --green: #30D158;
        --orange: #FF9230;
        --red: #FF6165;
        --red-hover: #FF8A8D;
        --on-status: #000000;

        --ring: var(--accent-text);
        --progress-fill: #0A84FF;
        --switch-on: #30D158;
        --switch-on-border: #1E8F3C;
        --seg-selected-bg: #636366;
        --knob: #FFFFFF;
        --scrollbar-thumb: rgba(255, 255, 255, 0.22);
        --scrollbar-thumb-hover: rgba(255, 255, 255, 0.36);

        --shadow-toast: 0 8px 28px rgba(0, 0, 0, 0.60), 0 1px 3px rgba(0, 0, 0, 0.40);
        --shadow-seg: none;
        --shadow-knob: 0 3px 8px rgba(0, 0, 0, 0.45), 0 1px 1px rgba(0, 0, 0, 0.35);
    }
}

@media (min-resolution: 2dppx) {
    :root { --hairline: 0.5px; }
}

@media (min-width: 600px) {
    :root { --gutter: var(--sp-5); }
}

@media (min-width: 768px) {
    :root { --gutter: var(--sp-6); }
}

/* ========================================================== 3. base & reset */

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

/* The hidden attribute always wins over display utilities such as .flex. */
[hidden] { display: none !important; }

html {
    -webkit-text-size-adjust: 100%;
    /* WCAG 2.2 SC 2.4.11: keyboard focus must not land under the sticky
       toolbar or the sticky save bar. scroll-padding shrinks the
       scrollport, so the browser scrolls the focused control clear of
       both instead of treating "behind the bar" as visible. */
    scroll-padding-top: calc(var(--toolbar-h) + var(--sp-2));
    scroll-padding-bottom: calc(var(--save-bar-h) + var(--sp-2));
}

body {
    margin: 0;
    min-height: 100dvh;
    background-color: var(--bg-grouped);
    color: var(--label);
    font-family: var(--font-sans);
    font-size: 1.0625rem;
    line-height: 1.2941;
    letter-spacing: -0.026em;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, p, ul, ol, figure, fieldset, legend {
    margin: 0;
    padding: 0;
}

ul, ol { list-style: none; }

fieldset { border: 0; }

strong { font-weight: 600; }

a { color: var(--accent-text); text-decoration: none; }
a:hover { text-decoration: underline; }

button { font-family: inherit; }

img, svg { display: block; }

::selection { background: color-mix(in srgb, var(--accent) 24%, transparent); }

/* ============================================================ 4. type scale */

/* Tracking closes as the text grows into body size and opens again above
   it, but never past zero. Apple's published SF table does turn positive
   at Title 1 and Large Title, because on an Apple device those sizes are
   set in the SF Pro *Display* cut, which is drawn tighter and needs the
   space added back. This stack falls through to Segoe UI, Roboto and
   DejaVu on every other machine, and none of them has a display cut: the
   positive values simply set the two largest lines on the page wider than
   the body copy under them. */
.t-large-title { font-size: 2.125rem; line-height: 1.2059; letter-spacing: -0.018em; font-weight: 700; }
.t-title1      { font-size: 1.75rem; line-height: 1.2143; letter-spacing: -0.020em; font-weight: 700; }
.t-title2      { font-size: 1.375rem; line-height: 1.2727; letter-spacing: -0.022em; font-weight: 700; }
.t-title3      { font-size: 1.25rem; line-height: 1.25; letter-spacing: -0.023em; font-weight: 600; }
.t-headline    { font-size: 1.0625rem; line-height: 1.2941; letter-spacing: -0.026em; font-weight: 600; }
.t-body        { font-size: 1.0625rem; line-height: 1.2941; letter-spacing: -0.026em; font-weight: 400; }
.t-callout     { font-size: 1rem; line-height: 1.3125; letter-spacing: -0.020em; font-weight: 400; }
.t-subheadline { font-size: 0.9375rem; line-height: 1.3333; letter-spacing: -0.016em; font-weight: 400; }
.t-footnote    { font-size: 0.8125rem; line-height: 1.3846; letter-spacing: -0.006em; font-weight: 400; }
.t-caption1    { font-size: 0.75rem; line-height: 1.3333; letter-spacing: 0; font-weight: 400; }
.t-caption2    { font-size: 0.6875rem; line-height: 1.1818; letter-spacing: 0.006em; font-weight: 400; }

.t-em { font-weight: 600; }

@media (max-width: 639px) {
    .t-large-title { font-size: 1.75rem; line-height: 1.2143; letter-spacing: -0.020em; }
    .t-title1 { font-size: 1.5rem; line-height: 1.25; letter-spacing: -0.021em; }
}

/* Label roles — the only way to colour text. */
.text-label { color: var(--label); }
.text-secondary { color: var(--label-secondary); }
.text-tertiary { color: var(--label-tertiary); }
.text-accent { color: var(--accent-text); }

.glyph-secondary { color: var(--label-secondary); }
.glyph-tertiary { color: var(--label-tertiary); }
.glyph-accent { color: var(--accent-text); }
.glyph-red { color: var(--red); }
.glyph-green { color: var(--green); }
.glyph-orange { color: var(--orange); }

.nums { font-variant-numeric: tabular-nums; }
.wrap-anywhere { overflow-wrap: anywhere; }

/* ================================================================ 5. icons */

.svg-sprite {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

.icon {
    display: inline-block;
    flex-shrink: 0;
    vertical-align: middle;
    fill: none;
    stroke: currentColor;
}

/* The house style is an outline; a solid mark has to say so, because
   .icon's fill/stroke beat the symbol's own attributes. */
.icon-solid {
    fill: currentColor;
    stroke: none;
}

.icon-14 { width: 14px; height: 14px; }
.icon-16 { width: 16px; height: 16px; }
.icon-18 { width: 18px; height: 18px; }
.icon-20 { width: 20px; height: 20px; }
.icon-24 { width: 24px; height: 24px; }
.icon-28 { width: 28px; height: 28px; }
.icon-48 { width: 48px; height: 48px; }

/* ================================================================ 6. focus */

:focus-visible {
    outline: 3px solid var(--ring);
    outline-offset: 2px;
}

button:focus:not(:focus-visible),
a:focus:not(:focus-visible),
summary:focus:not(:focus-visible) { outline: none; }

.skip-link {
    position: absolute;
    left: var(--sp-4);
    top: -5rem;
    z-index: 60;
    padding: var(--sp-2) var(--sp-4);
    border-radius: var(--r-sm);
    background: var(--bg-elevated);
    color: var(--label);
    font-size: 0.9375rem;
    line-height: 1.3333;
    font-weight: 600;
    box-shadow: var(--shadow-toast);
}

.skip-link:focus { top: var(--sp-4); text-decoration: none; }

/* ============================================================ 7. scrollbar */

::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: var(--r-full); }
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }

/* =============================================================== 8. layout */

.page {
    width: 100%;
    max-width: calc(var(--col-page) + 2 * var(--gutter));
    margin-inline: auto;
    padding-inline: var(--gutter);
}

.main {
    padding-block: var(--sp-6) var(--sp-12);
}

@media (min-width: 768px) {
    .main { padding-block: var(--sp-10) var(--sp-16); }
}

/* A sticky element stops at the end of its containing block, so the page's
   own bottom padding is dead space *under* the save bar: scrolled to the
   end of /settings the bar released and floated 48-64px off the viewport
   edge while the viewport-fixed toast dock stayed where it was, and the
   error toast came down on the bar and on "Zapisz ustawienia". No fixed
   offset can track a sticky element, so remove what it slides away from:
   the bar's own padding is the bottom margin of the page. */
body:has(.save-bar) .main { padding-block-end: 0; }

.form-column {
    width: 100%;
    max-width: var(--col-form);
    margin-inline: auto;
}

.stack-sections > * + * { margin-top: var(--sp-8); }

@media (min-width: 768px) {
    .stack-sections > * + * { margin-top: var(--sp-12); }
}

.stack-groups > * + * { margin-top: var(--sp-6); }

@media (min-width: 768px) {
    .stack-groups > * + * { margin-top: var(--sp-8); }
}

/* ============================================================== 9. toolbar */

.toolbar {
    position: sticky;
    top: 0;
    z-index: 30;
    background: var(--bg-toolbar);
    backdrop-filter: var(--blur-toolbar);
    -webkit-backdrop-filter: var(--blur-toolbar);
    box-shadow: 0 var(--hairline) 0 0 var(--separator);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .toolbar { background: var(--bg-toolbar-solid); }
}

.toolbar-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    min-height: var(--toolbar-h);
}

.wordmark {
    position: relative;
    display: inline-flex;
    align-items: center;
    min-height: 32px;
    padding-inline: var(--sp-1);
    margin-inline-start: calc(-1 * var(--sp-1));
    border-radius: var(--r-sm);
    color: var(--label);
    font-size: 1.0625rem;
    line-height: 1.2941;
    letter-spacing: -0.026em;
    font-weight: 600;
}

.wordmark::after {
    content: '';
    position: absolute;
    inset: -6px;
}

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

/* Press feedback, colour only. Hover is gated behind a fine pointer, as
   it should be, which left the biggest targets on the page mute on a
   touch screen: the reference wants the highlight on pointer-down, not
   on release. Nothing here moves, so none of it needs a
   prefers-reduced-motion exception. */
.wordmark:active { color: var(--accent-text); }

.nav-actions {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
}

.nav-link {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: 32px;
    padding-inline: 10px;
    border-radius: var(--r-sm);
    background: transparent;
    border: 0;
    color: var(--label-secondary);
    font-size: 0.9375rem;
    line-height: 1.3333;
    letter-spacing: -0.016em;
    font-weight: 400;
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease-standard),
                color var(--dur-fast) var(--ease-standard);
}

.nav-link:hover { text-decoration: none; }

.nav-link:active { background: var(--fill-on-grouped-active); color: var(--label); }

.nav-link::after {
    content: '';
    position: absolute;
    inset: -6px;
}

.nav-link[aria-current="page"] {
    color: var(--label);
    font-weight: 600;
}

.logout-form { display: contents; }

@media (hover: hover) and (pointer: fine) {
    .wordmark:hover { color: var(--accent-text); }
    .nav-link:hover { background: var(--fill-on-grouped); color: var(--label); }
}

@media (max-width: 639px) {
    .nav-label {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border-width: 0;
    }
    .nav-link { padding-inline: var(--sp-2); }
}

/* ================================================== 10. ALLOW_NO_AUTH banner */

.auth-banner {
    position: relative;
    z-index: 40;
    background: var(--red);
    color: var(--on-status);
    padding-block: var(--sp-3);
}

.auth-banner-inner {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
}

.auth-banner .icon { margin-top: 1px; }

/* ============================================================== 11. buttons */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    position: relative;
    min-height: 32px;
    padding-inline: var(--sp-4);
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    font-size: 0.9375rem;
    line-height: 1.3333;
    letter-spacing: -0.016em;
    font-weight: 600;
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    user-select: none;
    transition: background-color var(--dur-fast) var(--ease-standard),
                color var(--dur-fast) var(--ease-standard),
                transform var(--dur-fast) var(--ease-standard);
}

/* A 44x44 hit area around a control that is visually smaller (HIG 44pt,
   WCAG 2.5.8's floor is 24). The inset is per size: 32px + 2x6 = 44 here,
   28px + 2x8 = 44 on .btn-sm below. */
.btn::after {
    content: '';
    position: absolute;
    inset: -6px;
}

.btn:hover { text-decoration: none; }
.btn:active { transform: scale(0.97); }

.btn-sm {
    min-height: 28px;
    padding-inline: var(--sp-3);
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
}

.btn-sm::after { inset: -8px; }

.btn-lg {
    min-height: 44px;
    padding-inline: var(--sp-6);
    border-radius: var(--r-md);
    font-size: 1.0625rem;
    line-height: 1.2941;
    letter-spacing: -0.026em;
}

.btn-cta {
    min-height: 44px;
    width: 100%;
    padding-inline: var(--sp-6);
    border-radius: var(--r-full);
    font-size: 1.0625rem;
    line-height: 1.2941;
    letter-spacing: -0.026em;
}

.btn-primary { background: var(--accent); color: var(--on-accent); }
/* --fill-secondary, not --fill-tertiary: a secondary capsule now carries
   the main action of a file row ("Rozpocznij", "Pobierz"), and
   --fill-tertiary is 1.12:1 on a white card — too faint to read as a
   control rather than as a tinted label. */
.btn-secondary { background: var(--fill-on-grouped); color: var(--label); }
.btn-plain { background: transparent; color: var(--accent-text); }

/* A destructive row action is not an alarm: six red "Usuń" down the right
   edge of a list turn the column into a warning stripe and make the one
   thing the user came to do harder to find. It rests in --label-secondary
   at body weight and turns red the moment it is aimed at. */
.btn-danger {
    background: transparent;
    color: var(--label-secondary);
    font-weight: 400;
}

.btn-danger:focus-visible { color: var(--red); }

.btn-primary:active { background: var(--accent-active); }
.btn-secondary:active { background: var(--fill-on-grouped-active); }
.btn-plain:active { background: var(--fill-secondary); }
.btn-danger:active { background: var(--fill-secondary); color: var(--red-hover); }

@media (hover: hover) and (pointer: fine) {
    .btn-primary:hover { background: var(--accent-hover); }
    .btn-secondary:hover { background: var(--fill-on-grouped-hover); }
    .btn-plain:hover { background: var(--fill-tertiary); }
    .btn-danger:hover { background: var(--fill-tertiary); color: var(--red); }
}

.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    padding: 0;
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--label-secondary);
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease-standard),
                color var(--dur-fast) var(--ease-standard);
}

.btn-icon::after {
    content: '';
    position: absolute;
    inset: -6px;
}

/* An inline text action ("zmień" on the model line) is 34x18 of ink and
   would otherwise have no target at all beyond its own glyphs. */
.inline-action { position: relative; }

.inline-action::after {
    content: '';
    position: absolute;
    inset: -13px -8px;
}

.btn-icon:active { background: var(--fill-secondary); }

@media (hover: hover) and (pointer: fine) {
    .btn-icon:hover { background: var(--fill-tertiary); color: var(--label); }
}

.btn:disabled,
.btn[aria-disabled="true"],
.btn-icon:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    transform: none;
}

/* =========================================================== 12. group list */

.group { display: block; }

/* Section-level prose — group headers, hints, the model line, the legend —
   all sit on the column edge, flush with the card below. A 16px indent on
   some of them and none on others puts two captions three lines apart on
   two different edges. */
.group-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    padding: 0 0 var(--sp-2);
}

.group-title {
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    font-weight: 400;
    color: var(--label-tertiary);
}

.group-note {
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-tertiary);
    font-variant-numeric: tabular-nums;
}

.group-action { margin-inline-start: auto; }

.group-hint {
    padding: 0 0 var(--sp-2);
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-secondary);
}

.group-list {
    background: var(--bg-elevated);
    border-radius: var(--r-lg);
    overflow: hidden;
    list-style: none;
    margin: 0;
    padding: 0;
}

@media (min-width: 768px) {
    .group-list { border-radius: var(--r-xl); }
}

.group-row {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    min-height: 56px;
    padding: var(--sp-3) var(--sp-4);
}

.group-row + .group-row::before {
    content: '';
    position: absolute;
    inset-inline-start: var(--sp-4);
    inset-inline-end: 0;
    top: 0;
    height: var(--hairline);
    background: var(--separator);
}

.row-glyph { margin-top: 1px; }

.row-main {
    flex: 1 1 auto;
    min-width: 0;
}

.row-main > * + * { margin-top: var(--sp-1); }

.row-title {
    font-size: 1.0625rem;
    line-height: 1.2941;
    letter-spacing: -0.026em;
    font-weight: 600;
    color: var(--label);
    overflow-wrap: anywhere;
}

.row-meta {
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-secondary);
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}

/* --sp-4, not --sp-2: two .btn-sm halos grow 8px towards each other, and
   where they overlap the browser hands the whole overlap to one of them —
   which is how "Rozpocznij" measured 39px instead of 44. */
.row-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-4);
    flex-shrink: 0;
}

/* Keeps "Pobierz" away from the trash so a misclick cannot delete a paid
   translation. */
.action-gap { width: var(--sp-3); }

/* A row arrives and leaves; it does not blink in and out. Rows are keyed
   file id + status, so a job finishing destroys its node in one panel and
   builds it in another on the same poll tick — measured as an 876px
   teleport. Both ends are opacity plus a 6px lift, the two properties the
   compositor can carry on its own.

   The entrance only runs once the list carries data-ready="true", which
   app.js sets on the pass AFTER the one that filled it: a page that merely
   loads must not cascade its whole queue in. Feedback is for a change. */
.file-row { --dur-exit: var(--dur-base); }

/* :not(.is-leaving) is load-bearing, not decoration: this selector is one
   step more specific than .file-row.is-leaving, so without it the entrance
   would go on pinning opacity 1 and the exit would never render — the same
   way `animation: … both` used to beat the toast's exit. */
.group-list[data-ready="true"] > .file-row:not(.is-leaving) {
    opacity: 1;
    transform: translateY(0);
    transition: opacity var(--dur-base) var(--ease-out),
                transform var(--dur-base) var(--ease-out);
}

@starting-style {
    .group-list[data-ready="true"] > .file-row:not(.is-leaving) {
        opacity: 0;
        transform: translateY(6px);
    }
}

/* Out along the path it came in, on the inverse curve. It is on its way
   out, so it stops taking clicks the moment it starts leaving. */
.file-row.is-leaving {
    opacity: 0;
    transform: translateY(6px);
    transition: opacity var(--dur-exit) var(--ease-in),
                transform var(--dur-exit) var(--ease-in);
    pointer-events: none;
}

@media (max-width: 599px) {
    .file-row {
        display: grid;
        grid-template-columns: 20px 1fr;
        column-gap: var(--sp-3);
        align-items: start;
    }
    .file-row .row-actions {
        grid-column: 2;
        width: 100%;
        margin-top: var(--sp-3);
    }
    .group-action { width: 100%; margin-inline-start: 0; }
}

.group-sublist {
    margin-top: var(--sp-2);
    padding: var(--sp-3);
    border-radius: var(--r-sm);
    background: var(--fill-tertiary);
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-secondary);
}

/* ============================================================= 13. progress */

.progress-track {
    position: relative;
    width: 100%;
    height: 4px;
    border-radius: var(--r-full);
    background: var(--fill-secondary);
    overflow: hidden;
}

.progress-fill {
    width: 0;
    height: 100%;
    border-radius: inherit;
    background: var(--progress-fill);
    transition: width var(--dur-base) var(--ease-out);
}

/* The job bar above is retargeted once per 2 s poll, and easing between
   two known points is exactly right for it. The upload bar is driven by
   XHR progress events many times a second, and a 250 ms ease-out means
   the bar shows where the upload was a quarter of a second ago. Short and
   linear, so it reads 1:1 with the bytes. */
.progress-fill-direct { transition: width var(--dur-tick) linear; }

.spinner {
    display: inline-block;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    border: 2px solid var(--fill-secondary);
    border-top-color: var(--label-secondary);
    border-radius: 50%;
    animation: spin 800ms linear infinite;
}

.status-dot {
    width: 8px;
    height: 8px;
    flex-shrink: 0;
    border-radius: var(--r-full);
    background: currentColor;
}

/* The one thing actually happening on the page keeps its pulse. */
.is-live { animation: pulse 1600ms var(--ease-standard) infinite; }

.count-badge:empty { display: none; }

.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    min-height: 20px;
    padding-inline: var(--sp-2);
    border-radius: var(--r-full);
    background: var(--fill-on-grouped);
    color: var(--label-secondary);
    font-size: 0.75rem;
    line-height: 1.3333;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ========================================================== 14. disclosures */

/* inline-flex, not flex: a block-level summary is as wide as its
   container, so the focus ring framed the whole 980px row for a 120px
   control. */
details > summary {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: 32px;
    list-style: none;
    cursor: pointer;
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--accent-text);
    border-radius: var(--r-sm);
}

details > summary::-webkit-details-marker { display: none; }

.disclosure-chevron {
    color: var(--label-tertiary);
    transition: transform var(--dur-fast) var(--ease-standard);
}

details[open] > summary .disclosure-chevron { transform: rotate(180deg); }

@media (hover: hover) and (pointer: fine) {
    details > summary:hover {
        background: var(--fill-tertiary);
        margin-inline: calc(-1 * var(--sp-2));
        padding-inline: var(--sp-2);
    }
}

/* Tint only: the hover rule widens its own box, and a box that grows
   under the finger is a jump, not feedback. */
details > summary:active { background: var(--fill-tertiary); }

.error-full {
    margin: var(--sp-2) 0 0;
    max-height: 240px;
    overflow: auto;
    padding: var(--sp-3);
    padding-inline-end: var(--sp-10);
    border-radius: var(--r-sm);
    background: var(--fill-tertiary);
    color: var(--label-secondary);
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    line-height: 1.3846;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.error-block { position: relative; }

.error-copy {
    position: absolute;
    top: var(--sp-2);
    inset-inline-end: var(--sp-1);
}

/* ================================================== 15. fields and controls */

.field-row > * + * { margin-top: var(--sp-1); }

.field-label {
    display: block;
    font-size: 0.9375rem;
    line-height: 1.3333;
    letter-spacing: -0.016em;
    font-weight: 600;
    color: var(--label);
}

.help {
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-secondary);
}

.help:empty { display: none; }

.field-error {
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--red);
}

.field {
    width: 100%;
    min-height: 36px;
    padding-inline: var(--sp-3);
    border: 1px solid var(--border-control);
    border-radius: var(--r-md);
    background: var(--bg-base);
    color: var(--label);
    font-family: inherit;
    font-size: 1.0625rem;
    line-height: 1.2941;
    letter-spacing: -0.026em;
    transition: border-color var(--dur-fast) var(--ease-standard),
                box-shadow var(--dur-fast) var(--ease-out);
}

.field-lg { min-height: 44px; }

.field::placeholder { color: var(--label-tertiary); opacity: 1; }

.field:focus,
.field:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent);
}

.field[aria-invalid="true"] { border-color: var(--red); }

.field:disabled,
.field[readonly] {
    background: var(--fill-tertiary);
    color: var(--label-tertiary);
    opacity: 0.6;
    cursor: not-allowed;
}

@media (hover: hover) and (pointer: fine) {
    .field:hover:not(:focus):not(:disabled) { border-color: var(--label-quaternary); }
}

/* Touch sizing. .field was already lifted here; the provider segmented
   control — the primary control of /settings — and the switch row were
   left at 28px and 35px. */
@media (max-width: 639px) {
    .field { min-height: 44px; }
}

.field-group {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

.field-num { max-width: 9rem; }

.field-suffix {
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-tertiary);
    white-space: nowrap;
}

.select-field {
    appearance: none;
    -webkit-appearance: none;
    padding-inline-end: 34px;
    background-image: url("chevron-down.svg");
    background-position: right var(--sp-3) center;
    background-repeat: no-repeat;
    background-size: 12px 12px;
    text-overflow: ellipsis;
}

@media (prefers-color-scheme: dark) {
    .select-field { background-image: url("chevron-down-dark.svg"); }
}

select option,
select optgroup {
    background: var(--bg-elevated);
    color: var(--label);
}

.key-status {
    margin-top: var(--sp-2);
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: 18px;
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-secondary);
}

.key-status:empty { display: none; }

/* The glyph carries the state, the message stays in the ordinary secondary
   label: a whole sentence set in --green is the one thing on the page that
   would signal by colour alone. */
.status-glyph {
    flex-shrink: 0;
    margin-top: 1px;
}

.model-info {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
}

/* ================================================== 16. segmented control */

/* Full width of the form column, as the spec asks: 400px inside 692px is
   neither content width nor column width. */
.segmented {
    width: 100%;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 2px;
    min-height: 32px;
    padding: 2px;
    border-radius: 9px;
    background: var(--fill-on-grouped);
}

.seg-input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

.seg-label {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 28px;
    border-radius: 7px;
    color: var(--label-secondary);
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    font-weight: 500;
    cursor: pointer;
    user-select: none;
    transition: background-color var(--dur-fast) var(--ease-standard),
                color var(--dur-fast) var(--ease-standard),
                transform var(--dur-fast) var(--ease-standard);
}

.seg-label:active { transform: scale(0.98); }

.seg-input:checked + .seg-label {
    background: var(--seg-selected-bg);
    color: var(--label);
    font-weight: 600;
    box-shadow: var(--shadow-seg);
}

.seg-input:focus-visible + .seg-label {
    outline: 3px solid var(--ring);
    outline-offset: 2px;
}

@media (hover: hover) and (pointer: fine) {
    .seg-label:hover { color: var(--label); }
}

/* Touch sizing, after the base rules so it is the later declaration.
   .field is lifted in section 15; these two were left at 28px and 35px —
   and the segmented control is the primary control of /settings. */
@media (max-width: 639px) {
    .seg-label { min-height: 44px; }
    .switch-row { min-height: 44px; }
}

/* ============================================================== 17. switch */

.switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    cursor: pointer;
}

.switch-input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

.switch {
    position: relative;
    display: inline-block;
    width: 51px;
    height: 31px;
    flex-shrink: 0;
    border-radius: var(--r-full);
    background: var(--fill-secondary);
    box-shadow: inset 0 0 0 var(--hairline) var(--border-control);
    transition: background-color var(--dur-base) var(--ease-standard),
                box-shadow var(--dur-base) var(--ease-standard);
}

.switch-knob {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 27px;
    height: 27px;
    border-radius: 50%;
    background: var(--knob);
    box-shadow: var(--shadow-knob);
    transition: transform var(--dur-base) var(--ease-spring);
}

.switch-input:checked + .switch {
    background: var(--switch-on);
    box-shadow: inset 0 0 0 var(--hairline) var(--switch-on-border);
}

.switch-input:checked + .switch .switch-knob { transform: translateX(20px); }

.switch-input:focus-visible + .switch {
    outline: 3px solid var(--ring);
    outline-offset: 2px;
}

.switch-input:disabled + .switch { opacity: 0.45; }

@media (hover: hover) and (pointer: fine) {
    .switch-row:hover .switch-input:not(:checked) + .switch { background: var(--fill-primary); }
}

/* ========================================================= 18. upload zone */

.upload-zone {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: var(--sp-4);
    min-height: 132px;
    padding: var(--sp-8) var(--sp-5);
    border: 1px dashed var(--border-control);
    border-radius: var(--r-xl);
    background: transparent;
    cursor: pointer;
    transition: border-color var(--dur-fast) var(--ease-standard),
                background-color var(--dur-fast) var(--ease-standard);
}

@media (max-width: 599px) {
    .upload-zone { min-height: 100px; padding: var(--sp-5) var(--sp-4); }
}

.upload-zone:focus-within {
    outline: 3px solid var(--ring);
    outline-offset: 2px;
}

.upload-zone.drag-over {
    border-color: var(--accent);
    border-style: solid;
    background: color-mix(in srgb, var(--accent) 6%, transparent);
}

.upload-glyph { color: var(--label-secondary); transition: color var(--dur-fast) var(--ease-standard); }

@media (hover: hover) and (pointer: fine) {
    .upload-zone:hover { border-color: var(--accent); background: var(--fill-tertiary); }
    .upload-zone:hover .upload-glyph { color: var(--accent-text); }
}

/* The dropzone is the largest target on the dashboard and, when the queue
   is empty, its empty state too. */
.upload-zone:active { border-color: var(--accent); background: var(--fill-tertiary); }
.upload-zone:active .upload-glyph { color: var(--accent-text); }

.upload-text { min-width: 0; text-align: start; }

/* When the queue is empty the dropzone is the empty state: it grows and
   stacks, so the message sits under the prompt rather than beside it. */
.upload-empty { max-width: 44ch; }

.upload-zone.is-empty {
    flex-direction: column;
    min-height: 220px;
    gap: var(--sp-3);
}

.upload-zone.is-empty .upload-text { text-align: center; }

/* Touch screens cannot drag files in: describe tapping instead. */
.touch-only { display: none; }

@media (hover: none) and (pointer: coarse) {
    .touch-only { display: inline; }
    .pointer-only { display: none; }
}

/* ========================================================== 19. page parts */

.model-line {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-1) var(--sp-2);
    padding-inline: 0;
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-secondary);
}

.loading-state {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding-block: var(--sp-12);
    font-size: 0.9375rem;
    line-height: 1.3333;
    letter-spacing: -0.016em;
    color: var(--label-secondary);
}

.legend-list {
    display: grid;
    gap: var(--sp-2);
    margin-top: var(--sp-3);
    padding-inline: 0;
}

@media (min-width: 600px) {
    .legend-list { grid-template-columns: 1fr 1fr; gap: var(--sp-2) var(--sp-6); }
}

.legend-item {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-secondary);
}

.group-glyph { flex-shrink: 0; }
.legend-glyph { flex-shrink: 0; margin-top: 1px; }

/* =========================================================== 20. login page */

.login-main {
    display: grid;
    place-items: center;
    min-height: calc(100dvh - var(--sp-12));
    padding: var(--sp-6) var(--gutter);
}

.login-column {
    width: 100%;
    max-width: var(--col-login);
    text-align: center;
}

.login-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: var(--r-lg);
    background: var(--fill-on-grouped);
    color: var(--label);
}

.login-title { margin-top: var(--sp-6); }
.login-subtitle { margin-top: var(--sp-2); }
/* The column is centred, but a message beside its glyph is not. */
.login-flashes { margin-top: var(--sp-8); text-align: start; }
.login-form { margin-top: var(--sp-6); text-align: start; }
.login-form .btn-cta { margin-top: var(--sp-6); }
.login-help { margin-top: var(--sp-8); }

/* ======================================================== 21. settings page */

.section-head { margin-bottom: var(--sp-3); }
.section-head > * + * { margin-top: var(--sp-1); }

.settings-row { display: block; }

.settings-row .field-label { margin-bottom: var(--sp-1); }
.settings-row .help { margin-top: var(--sp-1); }
.settings-row .field-error { margin-top: var(--sp-1); }

@media (min-width: 600px) {
    .settings-row {
        display: grid;
        grid-template-columns: minmax(180px, 34%) 1fr;
        align-items: start;
        column-gap: var(--sp-4);
    }
    /* (36px field - 20px label line) / 2: the label sits on the field's baseline. */
    .settings-row .field-label { margin-bottom: 0; padding-top: var(--sp-2); }
    .settings-row .settings-control { grid-column: 2; }
    .settings-row .settings-span { grid-column: 1 / -1; }
}

.settings-control > * + * { margin-top: var(--sp-2); }

.save-bar {
    position: sticky;
    bottom: 0;
    z-index: 20;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    margin-top: var(--sp-8);
    padding: var(--sp-3) var(--sp-4);
    background: var(--bg-toolbar);
    backdrop-filter: var(--blur-toolbar);
    -webkit-backdrop-filter: var(--blur-toolbar);
    box-shadow: 0 calc(-1 * var(--hairline)) 0 0 var(--separator);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .save-bar { background: var(--bg-toolbar-solid); }
}

.save-bar .btn-cta { width: auto; min-width: 160px; margin-inline-start: auto; }

.dirty-indicator {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: 0.8125rem;
    line-height: 1.3846;
    letter-spacing: -0.006em;
    color: var(--label-secondary);
}

.dirty-indicator .status-dot { color: var(--orange); }

@media (max-width: 599px) {
    .save-bar { flex-direction: column; align-items: stretch; }
    .save-bar .btn-cta { width: 100%; }

    /* Stacked, the indicator sits above the full-width button — but only
       once there is something to indicate. Clean, the bar is still 68px;
       dirty it grows by the indicator's own line box (13px x 1.3846 = 18)
       plus the column gap. A single number could not be right in both
       states, so :has() asks which one we are in. */
    :root:has(.dirty-indicator:not([hidden])) { --save-bar-h: 98px; }
}

.provider-panel[data-active="false"] { display: none; }

/* ============================================================== 22. toasts */

.toast-region {
    position: fixed;
    z-index: 50;
    inset-block-end: var(--sp-6);
    inset-inline-end: var(--sp-6);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    width: min(380px, calc(100vw - 2 * var(--sp-4)));
    pointer-events: none;
}

/* A page with a sticky save bar docks its toasts above it. Landing on the
   bar puts one light translucent surface on another — the reference rules
   that out on legibility alone — and it covers "Zapisz ustawienia", the
   one control the page exists for. :has() keeps it in the stylesheet:
   nothing has to tell a script which pages have a bar. */
body:has(.save-bar) .toast-region {
    inset-block-end: calc(var(--save-bar-h) + var(--sp-3));
}

/* Full width on a phone, and always at the bottom: pinned to the top it
   covers the sticky toolbar, and its close button lands on "Wyloguj". */
@media (max-width: 639px) {
    .toast-region {
        inset-block-start: auto;
        inset-block-end: calc(var(--sp-3) + env(safe-area-inset-bottom, 0px));
        inset-inline: var(--sp-4);
        width: auto;
    }

    body:has(.save-bar) .toast-region {
        inset-block-end: calc(var(--save-bar-h) + var(--sp-3) + env(safe-area-inset-bottom, 0px));
    }
}

/* Each live region is a lane inside the dock; an empty lane must not add a
   gap to the stack. */
.toast-lane {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}

.toast-lane:empty { display: none; }

.toast {
    /* One source of truth for the exit: the transitions below and the
       safety-net timer in common.js both read this. */
    --dur-exit: var(--dur-slow);
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-radius: var(--r-lg);
    background: var(--bg-toolbar);
    backdrop-filter: var(--blur-toolbar);
    -webkit-backdrop-filter: var(--blur-toolbar);
    box-shadow: var(--shadow-toast);
    color: var(--label);
    font-size: 0.9375rem;
    line-height: 1.3333;
    letter-spacing: -0.016em;
    pointer-events: auto;
    opacity: 1;
    transform: translateY(0);
    transition: opacity var(--dur-exit) var(--ease-out),
                transform var(--dur-exit) var(--ease-out);
}

/* The entrance is a transition, not a keyframe animation. `animation: …
   both` keeps the last frame applied for ever and a running animation
   outranks a normal author declaration, so .toast.is-leaving below never
   rendered — the toast vanished instead of leaving. A transition can also
   be interrupted and retargeted at any frame; a keyframe cannot. */
@starting-style {
    .toast {
        opacity: 0;
        transform: translateY(8px);
    }
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .toast { background: var(--bg-toolbar-solid); }
}

.toast-error .toast-glyph { color: var(--red); }
.toast-success .toast-glyph { color: var(--green); }
.toast-info .toast-glyph { color: var(--accent-text); }

.toast-glyph { margin-top: 1px; }

/* A message listing several refused files keeps its line breaks. */
.toast-text {
    white-space: pre-line;
    overflow-wrap: anywhere;
}

.toast .btn-icon {
    width: 28px;
    height: 28px;
    margin: -2px calc(-1 * var(--sp-2)) -2px 0;
}

.toast .btn-icon::after { inset: -8px; }

/* Mirrors the entrance path: out the way it came in, on the inverse curve. */
.toast.is-leaving {
    opacity: 0;
    transform: translateY(8px);
    transition: opacity var(--dur-exit) var(--ease-in),
                transform var(--dur-exit) var(--ease-in);
}

.toast-close-all {
    align-self: flex-end;
    pointer-events: auto;
}

/* =========================================================== 23. animations */

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

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

/* ================================================== 24. reduced motion */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    /* A frozen spinner reads as a hang, so it keeps turning, slowly. */
    .spinner {
        animation-duration: 2s !important;
        animation-iteration-count: infinite !important;
    }

    /* A pulsing mark adds nothing once motion is off; keep it fully opaque. */
    .is-live { animation: none !important; opacity: 1; }

    .btn:active,
    .seg-label:active { transform: none; }
}

/* ============================== 25. transparency & contrast preferences */

/* The reference asks for three independent signals; reduced motion above
   was the only one answered. Both blocks here only ever raise contrast,
   so neither can pull a pair below its floor. */

@media (prefers-reduced-transparency: reduce) {
    .toolbar,
    .save-bar,
    .toast {
        background: var(--bg-toolbar-solid);
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

@media (prefers-contrast: more) {
    .toolbar,
    .save-bar,
    .toast {
        background: var(--bg-toolbar-solid);
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }

    /* A surface told apart only by a fill gets a drawn edge. An inset
       shadow takes no space, so nothing reflows when the setting is on. */
    .group-list,
    .field,
    .btn-secondary {
        box-shadow: inset 0 0 0 1px var(--border-contrast);
    }

    /* A focused field replaces its box-shadow with the ring, so its edge
       is carried by the border it already has. */
    .field { border-color: var(--border-contrast); }
}
