/* ============================================================================
   KIIT Event Gallery (FRS) — application design system
   ----------------------------------------------------------------------------
   The only stylesheet the application loads. There is no CSS framework behind
   it: everything a page uses is defined here.

   Layer order, and it matters — later layers refine earlier ones:
     1. tokens       the whole visual language as custom properties. The ONLY
                     place a literal colour may appear is a `:root` block here.
     2. base         element-level reset.
     3. foundation   the grid, the spacing/display utilities, and the base
                     components (button, form control, card, table, modal…).
                     This replaced Bootstrap 5.3 — see the note on that block.
     4. components   the named pieces this app is made of: .stat-card,
                     .photo-grid, .drop-zone, .viewer, .queue…  These refine
                     the foundation, so they come after it.
     5. mobile       `max-width` queries only, including `.data-table--stack`.

   Theme resolution order (last one wins):
     :root                                          -> light
     @media (prefers-color-scheme: dark) :root      -> dark, unless [data-theme="light"]
     :root[data-theme="dark"]                       -> dark, always
   ui.js writes `data-theme` onto <html>.
   ========================================================================== */

/* ---------------------------------------------------------------- tokens -- */
:root {
    color-scheme: light;

    /* Brand ---------------------------------------------------------------- */
    --rgb-primary: 21 128 61;
    --rgb-primary-strong: 22 101 52;
    --rgb-secondary: 4 120 87;
    --rgb-accent: 180 83 9;

    --c-primary: rgb(var(--rgb-primary));
    --c-primary-hover: rgb(var(--rgb-primary-strong));
    --c-primary-soft: rgb(var(--rgb-primary) / 0.10);
    --c-primary-soft-strong: rgb(var(--rgb-primary) / 0.18);
    --c-on-primary: #ffffff;

    --c-secondary: rgb(var(--rgb-secondary));
    --c-secondary-hover: #065f46;
    --c-secondary-soft: rgb(var(--rgb-secondary) / 0.12);
    --c-on-secondary: #ffffff;

    --c-accent: rgb(var(--rgb-accent));
    --c-accent-soft: rgb(var(--rgb-accent) / 0.14);

    /* Surfaces ------------------------------------------------------------- */
    --c-bg: #f3faf5;
    /* Triple as well as hex: the sticky bars sit over scrolling content and
       need the surface at partial alpha, which a hex cannot express. */
    --rgb-surface: 255 255 255;
    --c-surface: rgb(var(--rgb-surface));
    --c-surface-raised: #ffffff;
    --c-surface-sunken: #e8f4ec;
    --c-surface-inverse: #0f2417;
    --c-overlay: rgb(15 18 34 / 0.48);

    /* Borders -------------------------------------------------------------- */
    --c-border: #e2e5ee;
    --c-border-strong: #cbd0dd;
    --c-border-focus: rgb(var(--rgb-primary) / 0.55);

    /* Text ----------------------------------------------------------------- */
    --c-text: #1a1d29;
    --c-text-muted: #5b6478;
    --c-text-subtle: #8a92a6;
    --c-text-inverse: #f7f8fc;
    --c-link: rgb(var(--rgb-primary));

    /* Status --------------------------------------------------------------- */
    --rgb-success: 22 163 74;
    --rgb-warning: 202 138 4;
    --rgb-danger: 220 38 38;
    --rgb-info: 2 132 199;

    --c-success: rgb(var(--rgb-success));
    --c-success-soft: rgb(var(--rgb-success) / 0.12);
    --c-warning: rgb(var(--rgb-warning));
    --c-warning-soft: rgb(var(--rgb-warning) / 0.14);
    --c-danger: rgb(var(--rgb-danger));
    --c-danger-soft: rgb(var(--rgb-danger) / 0.12);
    --c-info: rgb(var(--rgb-info));
    --c-info-soft: rgb(var(--rgb-info) / 0.12);
    --c-on-status: #ffffff;

    /* Skeleton / shimmer ---------------------------------------------------- */
    --c-skeleton-base: #e6e9f1;
    --c-skeleton-sheen: #f4f6fb;

    /* Shadows (colour kept as a triple so alpha can vary) -------------------- */
    --rgb-shadow: 22 27 45;
    --sh-xs: 0 1px 2px rgb(var(--rgb-shadow) / 0.06);
    --sh-sm: 0 1px 3px rgb(var(--rgb-shadow) / 0.08), 0 1px 2px rgb(var(--rgb-shadow) / 0.04);
    --sh-md: 0 4px 12px rgb(var(--rgb-shadow) / 0.10), 0 2px 4px rgb(var(--rgb-shadow) / 0.05);
    --sh-lg: 0 12px 28px rgb(var(--rgb-shadow) / 0.13), 0 4px 10px rgb(var(--rgb-shadow) / 0.06);
    --sh-xl: 0 24px 56px rgb(var(--rgb-shadow) / 0.18);
    --sh-focus: 0 0 0 3px rgb(var(--rgb-primary) / 0.30);

    /* Spacing scale (4px base) ---------------------------------------------- */
    --sp-0: 0;
    --sp-1: 0.25rem;
    --sp-2: 0.5rem;
    --sp-3: 0.75rem;
    --sp-4: 1rem;
    --sp-5: 1.25rem;
    --sp-6: 1.5rem;
    --sp-8: 2rem;
    --sp-10: 2.5rem;
    --sp-12: 3rem;
    --sp-16: 4rem;
    --sp-20: 5rem;

    /* Radius ---------------------------------------------------------------- */
    --r-xs: 4px;
    --r-sm: 6px;
    --r-md: 10px;
    --r-lg: 14px;
    --r-xl: 20px;
    --r-2xl: 28px;
    --r-pill: 999px;

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

    --fs-2xs: 0.6875rem;
    --fs-xs: 0.75rem;
    --fs-sm: 0.875rem;
    --fs-md: 1rem;
    --fs-lg: 1.125rem;
    --fs-xl: 1.375rem;
    --fs-2xl: 1.75rem;
    --fs-3xl: 2.25rem;
    --fs-hero: clamp(2.1rem, 1.2rem + 3.6vw, 3.6rem);

    --fw-normal: 400;
    --fw-medium: 500;
    --fw-semibold: 600;
    --fw-bold: 700;

    --lh-tight: 1.2;
    --lh-snug: 1.4;
    --lh-normal: 1.6;

    --tracking-wide: 0.04em;

    /* Layout ---------------------------------------------------------------- */
    /* 284, not 264: the brand row carries two marks and a rule since the
       university's logo joined this system's, and at 264 the name arrived as
       "KIIT Event ..." — measured at 144px of text in a 125px box. The
       content column is capped at --content-max anyway, so the twenty pixels
       come out of margin, not out of the page. */
    --sidebar-w: 284px;
    --topbar-h: 62px;
    --content-max: 1320px;
    --tile-radius: var(--r-lg);
    --hit-target: 44px;

    /* Motion ---------------------------------------------------------------- */
    /*
       Control heights. Everything a person clicks or types into is pinned to
       one of these, so a button beside a select beside a segmented control
       lines up without anybody adding up padding and border by hand.

       44px is the default because that is the smallest comfortable touch
       target; the small size is for toolbars, the large for a primary action
       a page is built around.
    */
    --control-h-sm: 2.25rem;
    --control-h: 2.75rem;
    --control-h-lg: 3.25rem;

    --dur-fast: 120ms;
    --dur: 200ms;
    --dur-slow: 320ms;
    --ease: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    /* Overshoots and settles. For things that arrive -- a tick, a card,
       a tab indicator -- where a linear stop reads as a jump cut. */
    --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

    /* Stacking -------------------------------------------------------------- */
    --z-sidebar: 1035;
    --z-backdrop: 1030;
    --z-topbar: 1025;
    --z-modal: 1055;
    --z-toast: 1090;

    /* --- photo scrims --------------------------------------------------------- */
    /*
       Ink and scrim for text laid over a photograph — a tile caption, a
       confidence badge, the viewer's chrome. Deliberately the same in both
       themes: the backdrop is the image, not the page, so these do not follow
       light/dark. Tokens rather than literals so the "no hex outside :root"
       rule holds without exception.
    */
    --rgb-scrim: 0 0 0;
    --c-on-scrim: #ffffff;

    /* --- form-control glyphs ------------------------------------------------ */
    /*
       Inline SVG data URIs rather than an icon font, because a <select>
       chevron and a checkbox tick have to render before Font Awesome has
       loaded — otherwise every form control is momentarily blank. The chevron
       carries a literal colour, so it is redefined per theme below; the tick
       and the switch knob sit on the primary fill in both themes, and the
       close glyph is used as a mask so its own colour never shows.
    */
    --icon-chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236b7d70' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6l5 5 5-5'/%3E%3C/svg%3E");
    --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ffffff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.5 3.5L13 4.5'/%3E%3C/svg%3E");
    --icon-switch-knob: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='%23ffffff'/%3E%3C/svg%3E");
    --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 4l8 8M12 4l-8 8'/%3E%3C/svg%3E");

    /* --- chart palette -----------------------------------------------------
       Series colours are NOT the brand green. Validated with the dataviz
       palette checker: brand green + orange fails protanopia separation
       (delta-E 3.7, floor is 8), i.e. red-blind readers cannot tell the two
       series apart. Blue + orange passes every check (protan delta-E 24.7).
       The brand green stays in the UI chrome, where it is never load-bearing
       for meaning.

       Categorical: identity (which series). Fixed order, never cycled.
       Sequential: ordered magnitude (confidence high/medium/low), one hue,
       more-prominent = higher. Light end clears the surface at 2.07:1.       */
    --chart-cat-1: #2a78d6;
    --chart-cat-2: #eb6834;
    --chart-cat-3: #1baf7a;
    --chart-cat-4: #eda100;

    --chart-seq-high: #14532d;
    --chart-seq-mid: #1f9d4e;
    --chart-seq-low: #5ec97f;

    --chart-grid: #e6ece8;
    --chart-axis: #b9c6bd;
    --chart-ink: #1b2a20;
    --chart-ink-muted: #6b7d70;
    --chart-surface: #ffffff;
}

/* Dark palette. Declared twice on purpose: once behind the OS preference
   (skipped when the user explicitly chose light) and once behind the explicit
   attribute, so the in-app toggle overrides the OS in BOTH directions. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;

        --rgb-primary: 74 222 128;
        --rgb-primary-strong: 134 239 172;
        --rgb-secondary: 52 211 153;
        --rgb-accent: 251 191 36;

        --c-primary-hover: rgb(var(--rgb-primary-strong));
        --c-primary-soft: rgb(var(--rgb-primary) / 0.16);
        --c-primary-soft-strong: rgb(var(--rgb-primary) / 0.26);
        --c-on-primary: #07240f;

        --c-secondary-hover: #6ee7b7;
        --c-secondary-soft: rgb(var(--rgb-secondary) / 0.16);
        --c-on-secondary: #042f22;

        --c-bg: #0b120d;
        --rgb-surface: 17 27 20;
        --c-surface: rgb(var(--rgb-surface));
        --c-surface-raised: #17251b;
        --c-surface-sunken: #0d1710;
        --c-surface-inverse: #eef6f0;
        --c-overlay: rgb(3 5 12 / 0.66);

        --c-border: #243425;
        --c-border-strong: #33482f;
        --c-border-focus: rgb(var(--rgb-primary) / 0.65);

        --c-text: #e6f0e8;
        --c-text-muted: #a2b3a6;
        --c-text-subtle: #74856f;
        --c-text-inverse: #0d1710;

        --rgb-success: 52 199 110;
        --rgb-warning: 234 179 8;
        --rgb-danger: 248 113 113;
        --rgb-info: 56 189 248;
        --c-on-status: #0b1018;

        --c-skeleton-base: #1d2a1f;
        --c-skeleton-sheen: #27372a;

        --rgb-shadow: 0 0 0;
        --sh-xs: 0 1px 2px rgb(var(--rgb-shadow) / 0.4);
        --sh-sm: 0 1px 3px rgb(var(--rgb-shadow) / 0.45), 0 1px 2px rgb(var(--rgb-shadow) / 0.3);
        --sh-md: 0 4px 14px rgb(var(--rgb-shadow) / 0.5);
        --sh-lg: 0 14px 34px rgb(var(--rgb-shadow) / 0.55);
        --sh-xl: 0 28px 64px rgb(var(--rgb-shadow) / 0.6);
        --sh-focus: 0 0 0 3px rgb(var(--rgb-primary) / 0.4);

        /* Chart palette, re-stepped for the dark surface rather than flipped:
           both the categorical pair and the ordinal ramp were validated
           against #111b14 separately. */
        --chart-cat-1: #3987e5;
        --chart-cat-2: #d95926;
        --chart-cat-3: #199e70;
        --chart-cat-4: #c98500;

        --chart-seq-high: #86efac;
        --chart-seq-mid: #22c55e;
        --chart-seq-low: #166534;

        --chart-grid: #243425;
        --chart-axis: #3a4d3e;
        --chart-ink: #e6f0e8;
        --chart-ink-muted: #a2b3a6;
        --chart-surface: #111b14;
    }
}

:root[data-theme="dark"] {
    color-scheme: dark;

    --rgb-primary: 74 222 128;
    --rgb-primary-strong: 134 239 172;
    --rgb-secondary: 52 211 153;
    --rgb-accent: 251 191 36;

    --c-primary-hover: rgb(var(--rgb-primary-strong));
    --c-primary-soft: rgb(var(--rgb-primary) / 0.16);
    --c-primary-soft-strong: rgb(var(--rgb-primary) / 0.26);
    --c-on-primary: #07240f;

    --c-secondary-hover: #6ee7b7;
    --c-secondary-soft: rgb(var(--rgb-secondary) / 0.16);
    --c-on-secondary: #042f22;

    --c-bg: #0b120d;
    --rgb-surface: 17 27 20;
    --c-surface: rgb(var(--rgb-surface));
    --c-surface-raised: #17251b;
    --c-surface-sunken: #0d1710;
    --c-surface-inverse: #eef6f0;
    --c-overlay: rgb(3 5 12 / 0.66);

    --c-border: #243425;
    --c-border-strong: #33482f;
    --c-border-focus: rgb(var(--rgb-primary) / 0.65);

    --c-text: #e6f0e8;
    --c-text-muted: #a2b3a6;
    --c-text-subtle: #74856f;
    --c-text-inverse: #0d1710;

    --rgb-success: 52 199 110;
    --rgb-warning: 234 179 8;
    --rgb-danger: 248 113 113;
    --rgb-info: 56 189 248;
    --c-on-status: #0b1018;

    --c-skeleton-base: #1d2a1f;
    --c-skeleton-sheen: #27372a;

    --rgb-shadow: 0 0 0;
    --sh-xs: 0 1px 2px rgb(var(--rgb-shadow) / 0.4);
    --sh-sm: 0 1px 3px rgb(var(--rgb-shadow) / 0.45), 0 1px 2px rgb(var(--rgb-shadow) / 0.3);
    --sh-md: 0 4px 14px rgb(var(--rgb-shadow) / 0.5);
    --sh-lg: 0 14px 34px rgb(var(--rgb-shadow) / 0.55);
    --sh-xl: 0 28px 64px rgb(var(--rgb-shadow) / 0.6);
    --sh-focus: 0 0 0 3px rgb(var(--rgb-primary) / 0.4);
    /* Chart palette, re-stepped for the dark surface rather than flipped:
       both the categorical pair and the ordinal ramp were validated
       against #111b14 separately. */
    --chart-cat-1: #3987e5;
    --chart-cat-2: #d95926;
    --chart-cat-3: #199e70;
    --chart-cat-4: #c98500;

    --chart-seq-high: #86efac;
    --chart-seq-mid: #22c55e;
    --chart-seq-low: #166534;

    --chart-grid: #243425;
    --chart-axis: #3a4d3e;
    --chart-ink: #e6f0e8;
    --chart-ink-muted: #a2b3a6;
    --chart-surface: #111b14;
}

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

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

body {
    margin: 0;
    background-color: var(--c-bg);
    color: var(--c-text);
    font-family: var(--font-sans);
    font-size: var(--fs-md);
    line-height: var(--lh-normal);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    min-height: 100vh;
}

h1, h2, h3, h4, h5, h6 {
    color: var(--c-text);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
    letter-spacing: -0.011em;
}

/* The underline treatment itself is in the base layer; see the note there. */
a {
    color: var(--c-link);
}

code, kbd, pre, samp {
    font-family: var(--font-mono);
}

hr {
    border: 0;
    border-top: 1px solid var(--c-border);
    opacity: 1;
    margin: var(--sp-6) 0;
}

/* Visible, consistent focus for keyboard users on every interactive thing. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--c-border-focus);
    outline-offset: 2px;
    border-radius: var(--r-xs);
}

::selection {
    background: var(--c-primary-soft-strong);
    color: var(--c-text);
}

.text-muted-2 { color: var(--c-text-muted) !important; }
.text-subtle { color: var(--c-text-subtle) !important; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.numeric { font-variant-numeric: tabular-nums; }

.skip-link {
    position: absolute;
    left: var(--sp-4);
    top: -100px;
    z-index: calc(var(--z-toast) + 1);
    padding: var(--sp-2) var(--sp-4);
    background: var(--c-primary);
    color: var(--c-on-primary);
    border-radius: var(--r-sm);
    font-weight: var(--fw-semibold);
    transition: top var(--dur) var(--ease);
}

.skip-link:focus {
    top: var(--sp-4);
    color: var(--c-on-primary);
}

/* ============================================== foundation layer === */
/*
   Everything Bootstrap used to supply, implemented here.

   This replaces the old "bootstrap component bridge", which remapped ~390
   lines of `--bs-*` custom properties onto our tokens. That bridge existed
   only because Bootstrap bakes literal colours into per-component properties;
   with Bootstrap gone the indirection goes with it and these rules set our
   tokens directly.

   Scope is deliberately exactly what the application uses — measured with
   scratchpad/classaudit.py, not guessed. Re-run that script after adding
   markup: it prints every class used but not defined here, which is how an
   unstyled `.col-lg-7` gets caught before it ships.

   Class names match Bootstrap's on purpose. Renaming ~200 grid and utility
   uses across 25 pages and 29 modules would be a large diff with no benefit,
   and the names are a reasonable vocabulary in their own right.
*/

/* ---- reset ------------------------------------------------------------- */

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

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--fs-md);
    line-height: 1.5;
    color: var(--c-text);
    background-color: var(--c-bg);
    -webkit-text-size-adjust: 100%;
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
    margin: 0 0 var(--sp-3);
    font-weight: var(--fw-semibold);
    line-height: 1.25;
    color: var(--c-text);
}

h1 { font-size: var(--fs-3xl); }
h2 { font-size: var(--fs-2xl); }
h3 { font-size: var(--fs-xl); }
h4 { font-size: var(--fs-lg); }
h5 { font-size: var(--fs-md); }
h6 { font-size: var(--fs-sm); }

/* Heading *classes*, so a semantically-correct <h2> can look like an h6. */
.h1 { font-size: var(--fs-3xl); font-weight: var(--fw-semibold); line-height: 1.25; }
.h2 { font-size: var(--fs-2xl); font-weight: var(--fw-semibold); line-height: 1.25; }
.h3 { font-size: var(--fs-xl); font-weight: var(--fw-semibold); line-height: 1.3; }
.h4 { font-size: var(--fs-lg); font-weight: var(--fw-semibold); line-height: 1.3; }
.h5 { font-size: var(--fs-md); font-weight: var(--fw-semibold); line-height: 1.4; }
.h6 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); line-height: 1.4; }

p { margin: 0 0 var(--sp-4); }

/*
   Links never use the browser's underline.

   `text-decoration: underline` appears at full width the instant you hover,
   sits hard against the descenders, and decorates every line of a link that
   wraps. This draws a 2px rule that grows from the left instead: a
   background-image, so it animates without touching layout, and it is set on
   `background-size` which transitions where `text-decoration` cannot.

   Defined once, on the element, so every link in the application gets it and
   nothing has to opt in.
*/
a {
    color: var(--c-link);
    text-decoration: none;
    background-image: linear-gradient(currentColor, currentColor);
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 0% 2px;
    /* Paint the rule just below the baseline rather than through descenders. */
    padding-bottom: 1px;
    transition: background-size var(--dur) var(--ease), color var(--dur-fast) var(--ease);
}

a:hover,
a:focus-visible {
    color: var(--c-primary-hover);
    background-size: 100% 2px;
}

/*
   Anything that is a link only in markup -- a button, a card, a nav item, a
   tile -- opts out. The rule belongs to text you read, not to a surface you
   press.
*/
a.btn,
a.btn-icon,
a.app-nav__link,
a.app-tabbar__link,
a.app-sidebar__brand,
a.app-topbar__brand,
a.auth-brand,
a.landing-brand,
a.photo-tile,
a.stat-card,
a.skip-link,
.pagination a,
.chip-row a {
    background-image: none;
    padding-bottom: 0;
}

img,
svg,
canvas,
video {
    max-width: 100%;
}

img,
svg {
    vertical-align: middle;
}

ul, ol { margin: 0 0 var(--sp-4); padding-left: var(--sp-6); }
dl, dd { margin: 0; }

/*
   `figure` has a UA default of `margin: 1em 40px`, and every photo tile and the
   photo viewer are figures. `.photo-tile` never reset it — it relied on
   Bootstrap's reboot — so without this the whole gallery grid is indented 40px
   on each side and the tiles no longer line up with their container.
*/
figure { margin: 0; }
figcaption { font-size: var(--fs-sm); color: var(--c-text-muted); }

small { font-size: var(--fs-sm); }

b, strong { font-weight: var(--fw-semibold); }

sub, sup { position: relative; font-size: 0.75em; line-height: 0; vertical-align: baseline; }
sub { bottom: -0.25em; }
sup { top: -0.5em; }

summary { cursor: pointer; }

/* A disabled fieldset should look disabled, not just behave that way. */
fieldset:disabled { opacity: 0.6; }

hr {
    height: 0;
    margin: var(--sp-5) 0;
    border: 0;
    border-top: 1px solid var(--c-border);
    opacity: 1;
}

code, pre, kbd, samp {
    font-family: var(--font-mono);
    font-size: 0.9em;
}

code { color: var(--c-primary); }
pre { margin: 0 0 var(--sp-4); overflow: auto; }

button,
input,
select,
textarea {
    font: inherit;
    color: inherit;
    margin: 0;
}

button { cursor: pointer; }

table { border-collapse: collapse; }

caption {
    caption-side: bottom;
    padding: var(--sp-2) 0;
    text-align: left;
    color: var(--c-text-muted);
}

fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
legend { padding: 0; }

/*
   The `hidden` attribute must actually hide.

   It is only a UA-stylesheet rule, so ANY author `display` beats it — and this
   app hides things that are also `.toolbar { display: flex }` or
   `.card { display: flex }`. Bootstrap's reboot carried this rule; without it
   every `el.hidden = true` in ui.js and the page modules silently does
   nothing, which is how the bulk-action bar showed up with nothing selected.
*/
[hidden] {
    display: none !important;
}

/* A single visible focus treatment for everything interactive. */
:focus-visible {
    outline: 2px solid var(--c-primary);
    outline-offset: 2px;
}

/* ---- the grid ---------------------------------------------------------- */
/*
   A 12-column flex grid with a gutter set by `g-*` on the row. Gutters are
   applied as padding on the columns and cancelled by a negative margin on the
   row, which is what keeps a column's background flush with the card edge
   while the gap between columns stays even.
*/

.row {
    --gutter-x: var(--sp-4);
    --gutter-y: var(--sp-4);
    display: flex;
    flex-wrap: wrap;
    margin-right: calc(var(--gutter-x) / -2);
    margin-left: calc(var(--gutter-x) / -2);
    margin-top: calc(var(--gutter-y) * -1);
}

.row > * {
    flex: 0 0 auto;
    width: 100%;
    max-width: 100%;
    padding-right: calc(var(--gutter-x) / 2);
    padding-left: calc(var(--gutter-x) / 2);
    margin-top: var(--gutter-y);
}

.g-0 { --gutter-x: 0; --gutter-y: 0; }
.g-1 { --gutter-x: var(--sp-1); --gutter-y: var(--sp-1); }
.g-2 { --gutter-x: var(--sp-2); --gutter-y: var(--sp-2); }
.g-3 { --gutter-x: var(--sp-3); --gutter-y: var(--sp-3); }
.g-4 { --gutter-x: var(--sp-4); --gutter-y: var(--sp-4); }
.g-5 { --gutter-x: var(--sp-6); --gutter-y: var(--sp-6); }

.col { flex: 1 0 0%; }
.col-auto { flex: 0 0 auto; width: auto; }

.col-1  { width: 8.33333333%; }
.col-2  { width: 16.66666667%; }
.col-3  { width: 25%; }
.col-4  { width: 33.33333333%; }
.col-5  { width: 41.66666667%; }
.col-6  { width: 50%; }
.col-7  { width: 58.33333333%; }
.col-8  { width: 66.66666667%; }
.col-9  { width: 75%; }
.col-10 { width: 83.33333333%; }
.col-11 { width: 91.66666667%; }
.col-12 { width: 100%; }

/*
   Breakpoints, min-width and mobile-first, matching the values the rest of
   this stylesheet already uses. `sm` is 576px rather than Bootstrap's 576px
   min-width convention off-by-one so that the existing `max-width: 576px`
   mobile queries and these meet without a one-pixel gap.
*/
@media (min-width: 577px) {
    .col-sm-1  { width: 8.33333333%; }
    .col-sm-2  { width: 16.66666667%; }
    .col-sm-3  { width: 25%; }
    .col-sm-4  { width: 33.33333333%; }
    .col-sm-5  { width: 41.66666667%; }
    .col-sm-6  { width: 50%; }
    .col-sm-7  { width: 58.33333333%; }
    .col-sm-8  { width: 66.66666667%; }
    .col-sm-9  { width: 75%; }
    .col-sm-10 { width: 83.33333333%; }
    .col-sm-11 { width: 91.66666667%; }
    .col-sm-12 { width: 100%; }
    .col-sm-auto { flex: 0 0 auto; width: auto; }
    .d-sm-inline-flex { display: inline-flex !important; }
    .d-sm-none { display: none !important; }
    .d-sm-block { display: block !important; }
    .d-sm-flex { display: flex !important; }
}

@media (min-width: 769px) {
    .col-md-1  { width: 8.33333333%; }
    .col-md-2  { width: 16.66666667%; }
    .col-md-3  { width: 25%; }
    .col-md-4  { width: 33.33333333%; }
    .col-md-5  { width: 41.66666667%; }
    .col-md-6  { width: 50%; }
    .col-md-7  { width: 58.33333333%; }
    .col-md-8  { width: 66.66666667%; }
    .col-md-9  { width: 75%; }
    .col-md-10 { width: 83.33333333%; }
    .col-md-11 { width: 91.66666667%; }
    .col-md-12 { width: 100%; }
    .col-md-auto { flex: 0 0 auto; width: auto; }
    .d-md-none { display: none !important; }
    .d-md-block { display: block !important; }
    .d-md-flex { display: flex !important; }
}

@media (min-width: 993px) {
    .col-lg-1  { width: 8.33333333%; }
    .col-lg-2  { width: 16.66666667%; }
    .col-lg-3  { width: 25%; }
    .col-lg-4  { width: 33.33333333%; }
    .col-lg-5  { width: 41.66666667%; }
    .col-lg-6  { width: 50%; }
    .col-lg-7  { width: 58.33333333%; }
    .col-lg-8  { width: 66.66666667%; }
    .col-lg-9  { width: 75%; }
    .col-lg-10 { width: 83.33333333%; }
    .col-lg-11 { width: 91.66666667%; }
    .col-lg-12 { width: 100%; }
    .col-lg-auto { flex: 0 0 auto; width: auto; }
    .d-lg-none { display: none !important; }
    .d-lg-block { display: block !important; }
    .d-lg-flex { display: flex !important; }
}

@media (min-width: 1201px) {
    .col-xl-1  { width: 8.33333333%; }
    .col-xl-2  { width: 16.66666667%; }
    .col-xl-3  { width: 25%; }
    .col-xl-4  { width: 33.33333333%; }
    .col-xl-5  { width: 41.66666667%; }
    .col-xl-6  { width: 50%; }
    .col-xl-7  { width: 58.33333333%; }
    .col-xl-8  { width: 66.66666667%; }
    .col-xl-9  { width: 75%; }
    .col-xl-10 { width: 83.33333333%; }
    .col-xl-11 { width: 91.66666667%; }
    .col-xl-12 { width: 100%; }
    .col-xl-auto { flex: 0 0 auto; width: auto; }
}

@media (min-width: 1400px) {
    .col-xxl-1  { width: 8.33333333%; }
    .col-xxl-2  { width: 16.66666667%; }
    .col-xxl-3  { width: 25%; }
    .col-xxl-4  { width: 33.33333333%; }
    .col-xxl-5  { width: 41.66666667%; }
    .col-xxl-6  { width: 50%; }
    .col-xxl-7  { width: 58.33333333%; }
    .col-xxl-8  { width: 66.66666667%; }
    .col-xxl-9  { width: 75%; }
    .col-xxl-10 { width: 83.33333333%; }
    .col-xxl-11 { width: 91.66666667%; }
    .col-xxl-12 { width: 100%; }
    .col-xxl-auto { flex: 0 0 auto; width: auto; }
}

/* ---- display, flex and alignment utilities ----------------------------- */

.d-none { display: none !important; }
.d-block { display: block !important; }
.d-inline { display: inline !important; }
.d-inline-block { display: inline-block !important; }
.d-inline-flex { display: inline-flex !important; }
.d-flex { display: flex !important; }
.d-grid { display: grid !important; }

.flex-row { flex-direction: row !important; }
.flex-column { flex-direction: column !important; }
.flex-wrap { flex-wrap: wrap !important; }
.flex-nowrap { flex-wrap: nowrap !important; }
.flex-fill { flex: 1 1 auto !important; }
.flex-grow-1 { flex-grow: 1 !important; }
.flex-shrink-0 { flex-shrink: 0 !important; }

.justify-content-start { justify-content: flex-start !important; }
.justify-content-end { justify-content: flex-end !important; }
.justify-content-center { justify-content: center !important; }
.justify-content-between { justify-content: space-between !important; }
.justify-content-around { justify-content: space-around !important; }

.align-items-start { align-items: flex-start !important; }
.align-items-end { align-items: flex-end !important; }
.align-items-center { align-items: center !important; }
.align-items-baseline { align-items: baseline !important; }
.align-items-stretch { align-items: stretch !important; }

.align-self-start { align-self: flex-start !important; }
.align-self-center { align-self: center !important; }
.align-self-end { align-self: flex-end !important; }

/* Table-cell vertical alignment, not flexbox. */
.align-middle { vertical-align: middle !important; }
.align-top { vertical-align: top !important; }
.align-bottom { vertical-align: bottom !important; }

.gap-0 { gap: 0 !important; }
.gap-1 { gap: var(--sp-1) !important; }
.gap-2 { gap: var(--sp-2) !important; }
.gap-3 { gap: var(--sp-3) !important; }
.gap-4 { gap: var(--sp-4) !important; }
.gap-5 { gap: var(--sp-6) !important; }

.position-relative { position: relative !important; }
.position-absolute { position: absolute !important; }
.overflow-auto { overflow: auto !important; }
.overflow-hidden { overflow: hidden !important; }

/* ---- spacing utilities -------------------------------------------------- */
/*
   Steps 0-5 map onto the spacing tokens, so a `mb-3` here is the same rhythm
   as a component's own padding. `s`/`e` are start/end (logical), matching the
   `ms-auto` idiom used for pushing an element to the right of a flex row.
*/

.m-0 { margin: 0 !important; }
.m-1 { margin: var(--sp-1) !important; }
.m-2 { margin: var(--sp-2) !important; }
.m-3 { margin: var(--sp-3) !important; }
.m-4 { margin: var(--sp-4) !important; }
.m-5 { margin: var(--sp-6) !important; }
.m-auto { margin: auto !important; }

.mt-0 { margin-top: 0 !important; }
.mt-1 { margin-top: var(--sp-1) !important; }
.mt-2 { margin-top: var(--sp-2) !important; }
.mt-3 { margin-top: var(--sp-3) !important; }
.mt-4 { margin-top: var(--sp-4) !important; }
.mt-5 { margin-top: var(--sp-6) !important; }
.mt-auto { margin-top: auto !important; }

.mb-0 { margin-bottom: 0 !important; }
.mb-1 { margin-bottom: var(--sp-1) !important; }
.mb-2 { margin-bottom: var(--sp-2) !important; }
.mb-3 { margin-bottom: var(--sp-3) !important; }
.mb-4 { margin-bottom: var(--sp-4) !important; }
.mb-5 { margin-bottom: var(--sp-6) !important; }
.mb-auto { margin-bottom: auto !important; }

.ms-0 { margin-left: 0 !important; }
.ms-1 { margin-left: var(--sp-1) !important; }
.ms-2 { margin-left: var(--sp-2) !important; }
.ms-3 { margin-left: var(--sp-3) !important; }
.ms-4 { margin-left: var(--sp-4) !important; }
.ms-auto { margin-left: auto !important; }

.me-0 { margin-right: 0 !important; }
.me-1 { margin-right: var(--sp-1) !important; }
.me-2 { margin-right: var(--sp-2) !important; }
.me-3 { margin-right: var(--sp-3) !important; }
.me-4 { margin-right: var(--sp-4) !important; }
.me-auto { margin-right: auto !important; }

.mx-0 { margin-right: 0 !important; margin-left: 0 !important; }
.mx-2 { margin-right: var(--sp-2) !important; margin-left: var(--sp-2) !important; }
.mx-auto { margin-right: auto !important; margin-left: auto !important; }

.my-0 { margin-top: 0 !important; margin-bottom: 0 !important; }
.my-2 { margin-top: var(--sp-2) !important; margin-bottom: var(--sp-2) !important; }
.my-3 { margin-top: var(--sp-3) !important; margin-bottom: var(--sp-3) !important; }
.my-4 { margin-top: var(--sp-4) !important; margin-bottom: var(--sp-4) !important; }
.my-5 { margin-top: var(--sp-6) !important; margin-bottom: var(--sp-6) !important; }

.p-0 { padding: 0 !important; }
.p-1 { padding: var(--sp-1) !important; }
.p-2 { padding: var(--sp-2) !important; }
.p-3 { padding: var(--sp-3) !important; }
.p-4 { padding: var(--sp-4) !important; }
.p-5 { padding: var(--sp-6) !important; }

.pt-0 { padding-top: 0 !important; }
.pt-2 { padding-top: var(--sp-2) !important; }
.pt-3 { padding-top: var(--sp-3) !important; }
.pb-0 { padding-bottom: 0 !important; }
.pb-2 { padding-bottom: var(--sp-2) !important; }
.pb-3 { padding-bottom: var(--sp-3) !important; }
.px-0 { padding-right: 0 !important; padding-left: 0 !important; }
.px-3 { padding-right: var(--sp-3) !important; padding-left: var(--sp-3) !important; }
.py-0 { padding-top: 0 !important; padding-bottom: 0 !important; }
.py-2 { padding-top: var(--sp-2) !important; padding-bottom: var(--sp-2) !important; }
.py-3 { padding-top: var(--sp-3) !important; padding-bottom: var(--sp-3) !important; }

/* ---- sizing ------------------------------------------------------------- */

.w-25 { width: 25% !important; }
.w-50 { width: 50% !important; }
.w-75 { width: 75% !important; }
.w-100 { width: 100% !important; }
.w-auto { width: auto !important; }
.h-100 { height: 100% !important; }
.h-auto { height: auto !important; }
.mw-100 { max-width: 100% !important; }

/* ---- text --------------------------------------------------------------- */

.small { font-size: var(--fs-sm); }
.fw-normal { font-weight: var(--fw-normal) !important; }
.fw-medium { font-weight: var(--fw-medium) !important; }
.fw-semibold { font-weight: var(--fw-semibold) !important; }
.fw-bold { font-weight: var(--fw-bold) !important; }
.fst-italic { font-style: italic !important; }

.text-start { text-align: left !important; }
.text-center { text-align: center !important; }
.text-end { text-align: right !important; }
.text-nowrap { white-space: nowrap !important; }
.text-truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.text-break { word-break: break-word; overflow-wrap: anywhere; }
.text-uppercase { text-transform: uppercase !important; }
.text-decoration-none { text-decoration: none !important; }

.list-unstyled { padding-left: 0; list-style: none; }

/* Screen-reader-only, for a label that must exist but not be seen. */
.visually-hidden {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip-path: inset(50%) !important;
    white-space: nowrap !important;
    border: 0 !important;
}

/* ---- buttons ------------------------------------------------------------ */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-5);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    line-height: 1.25;
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
    vertical-align: middle;
    background-color: transparent;
    border: 1px solid transparent;
    border-radius: var(--r-md);
    cursor: pointer;
    user-select: none;
    transition: background-color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease);
}

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

.btn:disabled,
.btn.disabled,
.btn[aria-disabled="true"] {
    opacity: 0.55;
    pointer-events: none;
    cursor: default;
}

/* An <a> cannot be :disabled, so links use the class. */
a.disabled {
    opacity: 0.55;
    pointer-events: none;
}

/*
   Every control is one of three heights. `min-height` rather than `height` so
   a button whose label wraps grows instead of clipping -- the floor is what
   makes a row align, not a ceiling.
*/
.btn,
.btn-icon {
    min-height: var(--control-h);
}

.btn-sm {
    min-height: var(--control-h-sm);
    /* No vertical padding: the button is a centred flex box, so its height is
       the scale's and the padding would only fight it. */
    padding: 0 var(--sp-3);
    font-size: var(--fs-xs);
    border-radius: var(--r-sm);
}

/*
   The large button. Previously this existed only inside the max-width:576px
   block, so every "Start search" / "Create my account" on a laptop rendered at
   the default size -- the emphasis the markup asked for simply was not there.
*/
.btn-lg {
    min-height: var(--control-h-lg);
    padding: 0 1.5rem;
    font-size: var(--fs-md);
    border-radius: var(--r-md);
}

.btn-group {
    display: inline-flex;
    vertical-align: middle;
}

.btn-group > .btn { border-radius: 0; }
.btn-group > .btn:first-child { border-start-start-radius: var(--r-md); border-end-start-radius: var(--r-md); }
.btn-group > .btn:last-child { border-start-end-radius: var(--r-md); border-end-end-radius: var(--r-md); }
.btn-group > .btn + .btn { margin-left: -1px; }

.btn-group-sm > .btn {
    padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-xs);
}

/*
   The dialog close control. Its glyph is drawn with a mask rather than a text
   "×" so it keeps its size and weight independent of the font, and inherits
   colour from `currentColor` in both themes.
*/
.btn-close {
    display: inline-block;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    border: 0;
    border-radius: var(--r-sm);
    /* The glyph is a mask over a currentColor fill, so it follows the theme
       and keeps its weight independent of the font. */
    background-color: currentColor;
    color: var(--c-text-muted);
    opacity: 0.8;
    cursor: pointer;
    -webkit-mask: var(--icon-close) center / 0.85rem 0.85rem no-repeat;
    mask: var(--icon-close) center / 0.85rem 0.85rem no-repeat;
}

.btn-close:hover { opacity: 1; }

/* ---- forms -------------------------------------------------------------- */

.form-control,
.form-select {
    display: block;
    width: 100%;
    padding: var(--sp-3) var(--sp-4);
    font-size: var(--fs-sm);
    line-height: 1.5;
    color: var(--c-text);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--r-md);
    transition: border-color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease);
    appearance: none;
}

.form-select {
    /* Room for the chevron, which is drawn as a background image. */
    padding-right: var(--sp-8);
    background-image: var(--icon-chevron-down);
    background-repeat: no-repeat;
    background-position: right var(--sp-4) center;
    background-size: 0.7rem;
}

.form-control:focus,
.form-select:focus {
    outline: 0;
    border-color: var(--c-primary);
    box-shadow: 0 0 0 3px var(--c-primary-soft);
}

.form-control::placeholder { color: var(--c-text-subtle); opacity: 1; }

.form-control:disabled,
.form-select:disabled,
.form-control[readonly] {
    background-color: var(--c-surface-sunken);
    color: var(--c-text-muted);
    cursor: not-allowed;
}

/*
   A single-line field gets a fixed height, which is what lets it match a
   button exactly. A textarea and a file input are excluded: one has to grow
   with its content, and the other renders a browser-drawn button inside
   itself that a fixed height clips.
*/
input.form-control:not([type="file"]),
select.form-select {
    height: var(--control-h);
    padding-block: 0;
}

.form-control-sm,
.form-select-sm {
    padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-xs);
    border-radius: var(--r-sm);
}

input.form-control-sm:not([type="file"]),
select.form-select-sm {
    height: var(--control-h-sm);
    padding-block: 0;
}

.form-select-sm {
    padding-right: var(--sp-6);
    background-position: right var(--sp-3) center;
    background-size: 0.6rem;
}

/* The matching large size. The registration wizard asks one question per
   screen, and a default-height field under a 28px heading reads as an
   afterthought rather than the thing to fill in. */
.form-control-lg,
.form-select-lg {
    padding: 0.72rem var(--sp-4);
    font-size: var(--fs-md);
    border-radius: var(--r-md);
}

input.form-control-lg:not([type="file"]),
select.form-select-lg {
    height: var(--control-h-lg);
    padding-block: 0;
}

textarea.form-control { min-height: 4.5rem; resize: vertical; }

.form-control[type="file"] { padding: var(--sp-2) var(--sp-3); cursor: pointer; }

.form-control[type="color"] { padding: var(--sp-1); height: 2.5rem; }

/* Invalid state, set by ui.applyFieldErrors(). */
.form-control.is-invalid,
.form-select.is-invalid {
    border-color: var(--c-danger);
}

.form-control.is-invalid:focus,
.form-select.is-invalid:focus {
    box-shadow: 0 0 0 3px rgb(var(--rgb-danger) / 0.18);
}

.invalid-feedback { display: none; }

.is-invalid ~ .invalid-feedback,
.was-validated :invalid ~ .invalid-feedback {
    display: block;
}

/* Checkboxes, radios and the switch variant. */
.form-check {
    display: block;
    position: relative;
    padding-left: 1.85rem;
    margin-bottom: var(--sp-2);
}

.form-check-input {
    position: absolute;
    left: 0;
    top: 0.15rem;
    width: 1.15rem;
    height: 1.15rem;
    margin: 0;
    vertical-align: top;
    background-color: var(--c-surface);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--r-xs);
    appearance: none;
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease);
}

.form-check-input[type="radio"] { border-radius: 50%; }

.form-check-input:checked {
    background-color: var(--c-primary);
    border-color: var(--c-primary);
    background-image: var(--icon-check);
    background-repeat: no-repeat;
    background-position: center;
    background-size: 0.75rem;
}

.form-check-input[type="radio"]:checked {
    background-image: none;
    box-shadow: inset 0 0 0 0.22rem var(--c-surface);
}

.form-check-input:focus-visible {
    outline: 0;
    border-color: var(--c-primary);
    box-shadow: 0 0 0 3px var(--c-primary-soft);
}

.form-check-input:disabled { opacity: 0.5; cursor: not-allowed; }

.form-check-label { cursor: pointer; }

.form-check-input:disabled ~ .form-check-label { opacity: 0.6; cursor: not-allowed; }

.form-switch { padding-left: 3.1rem; }

.form-switch .form-check-input {
    width: 2.4rem;
    height: 1.3rem;
    border-radius: var(--r-pill);
    background-image: var(--icon-switch-knob);
    background-repeat: no-repeat;
    background-position: left center;
    background-size: 1.05rem;
    transition: background-position var(--dur-fast) var(--ease),
                background-color var(--dur-fast) var(--ease);
}

.form-switch .form-check-input:checked {
    background-position: right center;
    background-image: var(--icon-switch-knob);
    background-color: var(--c-primary);
    border-color: var(--c-primary);
}

/*
   An input with a button welded to its right edge — used by the password
   reveal control. The button keeps the field's height and the pair shares one
   focus ring, so it reads as a single control.
*/
.input-group {
    display: flex;
    align-items: stretch;
    width: 100%;
}

.input-group > .form-control,
.input-group > .form-select {
    position: relative;
    flex: 1 1 auto;
    width: 1%;
    min-width: 0;
}

.input-group > :not(:last-child) {
    border-start-end-radius: 0;
    border-end-end-radius: 0;
}

.input-group > :not(:first-child) {
    margin-left: -1px;
    border-start-start-radius: 0;
    border-end-start-radius: 0;
}

.input-group > .form-control:focus,
.input-group > .form-select:focus,
.input-group > .btn:focus-visible {
    /* Raised so the focus ring is not clipped by the neighbour's border. */
    z-index: 2;
}

.input-group-text {
    display: flex;
    align-items: center;
    padding: var(--sp-3) var(--sp-4);
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
    background-color: var(--c-surface-sunken);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--r-md);
    white-space: nowrap;
}

/* ---- card --------------------------------------------------------------- */

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-xs);
}

.card-body { flex: 1 1 auto; padding: var(--sp-5); }
.card-header {
    padding: var(--sp-4) var(--sp-5);
    border-bottom: 1px solid var(--c-border);
    background-color: var(--c-surface-sunken);
}
.card-footer {
    padding: var(--sp-4) var(--sp-5);
    border-top: 1px solid var(--c-border);
}
.card-title { margin-bottom: var(--sp-3); }
.card-text:last-child { margin-bottom: 0; }

/* ---- tables ------------------------------------------------------------- */

.table {
    width: 100%;
    margin-bottom: var(--sp-4);
    color: var(--c-text);
    vertical-align: top;
    border-color: var(--c-border);
}

.table > thead { vertical-align: bottom; }

.table > :not(caption) > * > * {
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--c-border);
}

.table > thead th {
    font-weight: var(--fw-semibold);
    text-align: left;
}

.table-sm > :not(caption) > * > * {
    padding: var(--sp-2) var(--sp-3);
}

.table-hover > tbody > tr:hover > * {
    background-color: var(--c-primary-soft);
}

.table-active > * {
    background-color: var(--c-primary-soft);
}

.table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ---- alerts ------------------------------------------------------------- */

.alert {
    position: relative;
    padding: var(--sp-4) var(--sp-5);
    margin-bottom: var(--sp-4);
    border: 1px solid transparent;
    border-radius: var(--r-md);
    font-size: var(--fs-sm);
}

.alert p:last-child { margin-bottom: 0; }
.alert code { color: inherit; }

/* ---- badges ------------------------------------------------------------- */

.badge {
    display: inline-block;
    padding: 0.32em 0.6em;
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    line-height: 1;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    border-radius: var(--r-sm);
}

/* ---- progress ----------------------------------------------------------- */

.progress {
    display: flex;
    height: 0.5rem;
    overflow: hidden;
    background-color: var(--c-surface-sunken);
    border-radius: var(--r-pill);
}

.progress-bar {
    display: flex;
    flex-direction: column;
    justify-content: center;
    overflow: hidden;
    color: var(--c-on-primary);
    text-align: center;
    white-space: nowrap;
    background-color: var(--c-primary);
    transition: width var(--dur) var(--ease);
}

/* ---- nav and tabs ------------------------------------------------------- */

.nav {
    display: flex;
    flex-wrap: wrap;
    padding-left: 0;
    margin-bottom: 0;
    list-style: none;
}

.nav-item { margin-bottom: -1px; }

.nav-tabs {
    gap: var(--sp-1);
    border-bottom: 1px solid var(--c-border);
}

.nav-tabs .nav-link {
    padding: var(--sp-3) var(--sp-4);
    background: none;
    border: 1px solid transparent;
    border-bottom: 2px solid transparent;
    border-radius: var(--r-sm) var(--r-sm) 0 0;
    color: var(--c-text-muted);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    cursor: pointer;
}

.nav-tabs .nav-link:hover {
    color: var(--c-text);
    background-color: var(--c-surface-sunken);
    text-decoration: none;
}

.nav-tabs .nav-link.active {
    color: var(--c-primary);
    border-bottom-color: var(--c-primary);
    background-color: transparent;
}

.nav-pills { gap: var(--sp-2); }

.nav-pills .nav-link {
    padding: var(--sp-2) var(--sp-4);
    border: 0;
    border-radius: var(--r-pill);
    background: var(--c-surface-sunken);
    color: var(--c-text-muted);
    font-size: var(--fs-sm);
    cursor: pointer;
}

.nav-pills .nav-link.active {
    background: var(--c-primary);
    color: var(--c-on-primary);
}

/* ---- modal -------------------------------------------------------------- */
/*
   Behaviour is ui.js's own (show/hide, backdrop click, Escape, focus) — there
   is no Bootstrap JS to drive it. These rules only need to describe the two
   states, so `.fade`/`.show` are a plain opacity transition rather than
   Bootstrap's staged animation.
*/

.modal {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: none;
    overflow-x: hidden;
    overflow-y: auto;
    outline: 0;
}

.modal.show { display: block; }

.modal.fade .modal-dialog {
    transform: translateY(-1rem);
    transition: transform var(--dur) var(--ease);
}

.modal.show .modal-dialog { transform: none; }

.modal-dialog {
    position: relative;
    width: auto;
    max-width: 32rem;
    margin: var(--sp-5) auto;
    pointer-events: none;
}

.modal-dialog-centered {
    display: flex;
    align-items: center;
    min-height: calc(100% - var(--sp-5) * 2);
}

.modal-sm { max-width: 22rem; }
.modal-lg { max-width: 48rem; }
.modal-xl { max-width: 64rem; }

.modal-content {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-lg);
    pointer-events: auto;
    outline: 0;
}

.modal-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-4) var(--sp-5);
    border-bottom: 1px solid var(--c-border);
}

.modal-title { margin: 0; }

.modal-body {
    position: relative;
    flex: 1 1 auto;
    padding: var(--sp-5);
}

.modal-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--sp-2);
    padding: var(--sp-4) var(--sp-5);
    border-top: 1px solid var(--c-border);
}

.modal-dialog-scrollable { height: calc(100% - var(--sp-5) * 2); }
.modal-dialog-scrollable .modal-content { max-height: 100%; overflow: hidden; }
.modal-dialog-scrollable .modal-body { overflow-y: auto; }

.modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-modal) - 1);
    background-color: rgb(var(--rgb-scrim) / 0.5);
    opacity: 0;
    transition: opacity var(--dur) var(--ease);
}

.modal-backdrop.show { opacity: 1; }

/* Stop the page scrolling behind an open dialog. */
body.modal-open { overflow: hidden; }

@media (max-width: 576px) {
    .modal-dialog {
        margin: var(--sp-3);
        max-width: none;
    }

    .modal-dialog-centered { min-height: calc(100% - var(--sp-3) * 2); }
    .modal-body { padding: var(--sp-4); }
    .modal-header,
    .modal-footer { padding: var(--sp-3) var(--sp-4); }

    /* Buttons stack and fill, so they are reachable with a thumb. */
    .modal-footer > .btn { flex: 1 1 auto; }
}

/* ---- colour variants ---------------------------------------------------- */
/*
   The semantic variants of the components above.

   These used to live in the Bootstrap bridge as `--bs-btn-bg` style remaps —
   one indirection per component, because Bootstrap compiled literal colours
   into per-component properties. Setting the property directly is both
   shorter and the thing that actually happens.

   Every value is a token, so the dark palette applies with no second copy.
*/

/* Buttons. `.btn-brand`, `.btn-surface`, `.btn-brand-soft` and `.btn-icon`
   are this app's own and live in the components layer below. */
.btn-primary {
    color: var(--c-on-primary);
    background-color: var(--c-primary);
    border-color: var(--c-primary);
}

.btn-primary:hover,
.btn-primary:focus-visible {
    color: var(--c-on-primary);
    background-color: var(--c-primary-hover);
    border-color: var(--c-primary-hover);
}

.btn-secondary {
    color: var(--c-on-secondary);
    background-color: var(--c-secondary);
    border-color: var(--c-secondary);
}

.btn-secondary:hover,
.btn-secondary:focus-visible {
    color: var(--c-on-secondary);
    background-color: var(--c-secondary-hover);
    border-color: var(--c-secondary-hover);
}

.btn-danger {
    color: var(--c-on-status);
    background-color: var(--c-danger);
    border-color: var(--c-danger);
}

.btn-danger:hover,
.btn-danger:focus-visible {
    color: var(--c-on-status);
    background-color: var(--c-danger);
    border-color: var(--c-danger);
    filter: brightness(0.92);
}

.btn-success {
    color: var(--c-on-status);
    background-color: var(--c-success);
    border-color: var(--c-success);
}

.btn-warning {
    color: var(--c-on-status);
    background-color: var(--c-warning);
    border-color: var(--c-warning);
}

.btn-outline-primary {
    color: var(--c-primary);
    border-color: var(--c-primary);
    background-color: transparent;
}

.btn-outline-primary:hover,
.btn-outline-primary:focus-visible {
    color: var(--c-on-primary);
    background-color: var(--c-primary);
}

.btn-outline-secondary {
    color: var(--c-text);
    border-color: var(--c-border-strong);
    background-color: var(--c-surface);
}

.btn-outline-secondary:hover,
.btn-outline-secondary:focus-visible {
    color: var(--c-text);
    background-color: var(--c-surface-sunken);
    border-color: var(--c-text-subtle);
}

.btn-outline-danger {
    color: var(--c-danger);
    border-color: var(--c-danger);
    background-color: transparent;
}

.btn-outline-danger:hover,
.btn-outline-danger:focus-visible {
    color: var(--c-on-status);
    background-color: var(--c-danger);
}

.btn-link {
    color: var(--c-link);
    border-color: transparent;
    background-color: transparent;
    padding-left: 0;
    padding-right: 0;
    min-height: 0;
}

/* A <button> styled as a link gets the same drawn rule. */
.btn-link {
    background-image: linear-gradient(currentColor, currentColor);
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 0% 2px;
    transition: background-size var(--dur) var(--ease), color var(--dur-fast) var(--ease);
}

.btn-link:hover,
.btn-link:focus-visible {
    color: var(--c-primary-hover);
    background-size: 100% 2px;
}

/*
   Alerts. The tinted background carries the meaning; the left border is the
   redundant cue, so the four are distinguishable without relying on hue —
   which is the whole point for a red/green pair.
*/
.alert-success {
    color: var(--c-text);
    background-color: var(--c-success-soft);
    border-color: transparent;
    border-left: 3px solid var(--c-success);
}

.alert-warning {
    color: var(--c-text);
    background-color: var(--c-warning-soft);
    border-color: transparent;
    border-left: 3px solid var(--c-warning);
}

.alert-danger {
    color: var(--c-text);
    background-color: var(--c-danger-soft);
    border-color: transparent;
    border-left: 3px solid var(--c-danger);
}

.alert-info {
    color: var(--c-text);
    background-color: var(--c-info-soft);
    border-color: transparent;
    border-left: 3px solid var(--c-info);
}

.alert-primary {
    color: var(--c-text);
    background-color: var(--c-primary-soft);
    border-color: transparent;
    border-left: 3px solid var(--c-primary);
}

/* Solid badges. `--c-on-status` is the one ink that stays legible on all of
   these fills in both themes. */
.text-bg-primary { color: var(--c-on-primary); background-color: var(--c-primary); }
.text-bg-secondary { color: var(--c-text); background-color: var(--c-surface-sunken); }
.text-bg-success { color: var(--c-on-status); background-color: var(--c-success); }
.text-bg-warning { color: var(--c-on-status); background-color: var(--c-warning); }
.text-bg-danger { color: var(--c-on-status); background-color: var(--c-danger); }
.text-bg-info { color: var(--c-on-status); background-color: var(--c-info); }

/* Text colours. */
.text-muted { color: var(--c-text-muted) !important; }
.text-subtle { color: var(--c-text-subtle) !important; }
.text-primary { color: var(--c-primary) !important; }
.text-success { color: var(--c-success) !important; }
.text-warning { color: var(--c-warning) !important; }
.text-danger { color: var(--c-danger) !important; }
.text-info { color: var(--c-info) !important; }
.text-body { color: var(--c-text) !important; }

/* Surfaces. */
.bg-body-tertiary { background-color: var(--c-surface-sunken) !important; }
.bg-surface { background-color: var(--c-surface) !important; }

/* A quiet panel for an aside that is information, not a warning. */
.bg-notice {
    background-color: var(--c-primary-soft);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    padding: var(--sp-4);
}

.border { border: 1px solid var(--c-border) !important; }
.border-0 { border: 0 !important; }
.border-top { border-top: 1px solid var(--c-border) !important; }
.border-bottom { border-bottom: 1px solid var(--c-border) !important; }
.rounded { border-radius: var(--r-md) !important; }
.rounded-pill { border-radius: var(--r-pill) !important; }

/* ---- spinners ----------------------------------------------------------- */

.spinner-border {
    display: inline-block;
    width: 1.25rem;
    height: 1.25rem;
    vertical-align: -0.125em;
    border: 2px solid currentcolor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: spinner-border 0.75s linear infinite;
}

.spinner-border-sm { width: 0.9rem; height: 0.9rem; border-width: 1.5px; }

@keyframes spinner-border {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .spinner-border { animation-duration: 1.5s; }
}


/* -------------------------------------------------------------- buttons --- */
.btn {
    font-weight: var(--fw-medium);
    /* Height comes from the control scale in the foundation layer. */
    transition: background-color var(--dur) var(--ease),
                border-color var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease),
                transform var(--dur-fast) var(--ease);
}

.btn:active {
    transform: translateY(1px);
}

.btn:focus-visible {
    box-shadow: var(--sh-focus);
}

.btn-brand {
    background-color: var(--c-primary);
    border: 1px solid var(--c-primary);
    color: var(--c-on-primary);
}

.btn-brand:hover,
.btn-brand:focus {
    background-color: var(--c-primary-hover);
    border-color: var(--c-primary-hover);
    color: var(--c-on-primary);
}

.btn-brand-soft {
    background-color: var(--c-primary-soft);
    border: 1px solid transparent;
    color: var(--c-primary);
}

.btn-brand-soft:hover,
.btn-brand-soft:focus {
    background-color: var(--c-primary-soft-strong);
    color: var(--c-primary);
}

.btn-surface {
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    color: var(--c-text);
}

.btn-surface:hover,
.btn-surface:focus {
    background-color: var(--c-surface-sunken);
    border-color: var(--c-border-strong);
    color: var(--c-text);
}

.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--hit-target);
    height: var(--hit-target);
    min-height: var(--hit-target);
    padding: 0;
    border-radius: var(--r-md);
    border: 1px solid transparent;
    background: transparent;
    color: var(--c-text-muted);
}

.btn-icon:hover,
.btn-icon:focus {
    background: var(--c-surface-sunken);
    color: var(--c-text);
}

/* ---------------------------------------------------------------- cards --- */
.card,
.surface-card {
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    color: var(--c-text);
    box-shadow: var(--sh-xs);
}

.surface-card {
    padding: var(--sp-5);
}

.surface-card--raised {
    background-color: var(--c-surface-raised);
    box-shadow: var(--sh-md);
}

.section-title {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin: 0 0 var(--sp-4);
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
}

.section-title .fa-solid,
.section-title .fa-regular {
    color: var(--c-primary);
}

.eyebrow {
    display: inline-block;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-text-subtle);
}

/* ================================================================ SHELL === */
.app-shell {
    display: flex;
    min-height: 100vh;
    background-color: var(--c-bg);
}

.app-sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: var(--z-sidebar);
    width: var(--sidebar-w);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-4) var(--sp-3);
    background-color: var(--c-surface);
    border-right: 1px solid var(--c-border);
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateX(-100%);
    transition: transform var(--dur-slow) var(--ease-out);
}

.app-sidebar.is-open {
    transform: translateX(0);
    box-shadow: var(--sh-xl);
}

.app-sidebar__brand {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3) var(--sp-4);
    color: var(--c-text);
    text-decoration: none;
    font-weight: var(--fw-bold);
    font-size: var(--fs-lg);
    letter-spacing: -0.02em;
}

.app-sidebar__brand:hover {
    color: var(--c-text);
}

.app-sidebar__section {
    padding: var(--sp-4) var(--sp-3) var(--sp-1);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-text-subtle);
}

.app-nav {
    display: flex;
    flex-direction: column;
    gap: 2px;
    list-style: none;
    margin: 0;
    padding: 0;
}

.app-nav__link {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    min-height: var(--hit-target);
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--r-md);
    color: var(--c-text-muted);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    text-decoration: none;
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.app-nav__link i {
    width: 1.25rem;
    text-align: center;
    font-size: var(--fs-md);
    flex: 0 0 auto;
}

.app-nav__link:hover {
    background-color: var(--c-surface-sunken);
    color: var(--c-text);
}

.app-nav__link.is-active {
    background-color: var(--c-primary-soft);
    color: var(--c-primary);
    font-weight: var(--fw-semibold);
}

.app-nav__badge {
    margin-left: auto;
    padding: 0 var(--sp-2);
    border-radius: var(--r-pill);
    background: var(--c-danger);
    color: var(--c-on-status);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
    line-height: 1.6;
}

.app-sidebar__footer {
    margin-top: auto;
    padding-top: var(--sp-4);
    border-top: 1px solid var(--c-border);
    font-size: var(--fs-xs);
    color: var(--c-text-subtle);
}

.app-sidebar__backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--z-backdrop);
    background: var(--c-overlay);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur) var(--ease), visibility var(--dur) var(--ease);
}

.app-sidebar__backdrop.is-open {
    opacity: 1;
    visibility: visible;
}

.app-body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
}

.app-topbar {
    position: sticky;
    top: 0;
    z-index: var(--z-topbar);
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    min-height: var(--topbar-h);
    padding: var(--sp-2) var(--sp-4);
    background-color: var(--c-surface);
    border-bottom: 1px solid var(--c-border);
}

.app-topbar__title {
    margin: 0;
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-topbar__subtitle {
    display: block;
    font-size: var(--fs-xs);
    font-weight: var(--fw-normal);
    color: var(--c-text-muted);
}

.app-topbar__spacer {
    flex: 1 1 auto;
}

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

.app-avatar {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: var(--r-pill);
    background: var(--c-primary-soft);
    color: var(--c-primary);
    font-size: var(--fs-sm);
    font-weight: var(--fw-bold);
    flex: 0 0 auto;
}

.app-identity {
    display: none;
    line-height: 1.25;
    text-align: right;
}

.app-identity__name {
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
}

.app-identity__meta {
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
}

.app-main {
    flex: 1 1 auto;
    width: 100%;
    max-width: var(--content-max);
    margin: 0 auto;
    padding: var(--sp-5) var(--sp-4) var(--sp-16);
}

@media (min-width: 992px) {
    .app-sidebar {
        transform: none;
        box-shadow: none;
    }

    .app-sidebar__backdrop {
        display: none;
    }

    .app-body {
        margin-left: var(--sidebar-w);
    }

    .app-identity {
        display: block;
    }

    .app-main {
        padding: var(--sp-8) var(--sp-8) var(--sp-20);
    }

    .app-sidebar__toggle {
        display: none !important;
    }
}

/* Lock scroll behind the off-canvas drawer on small screens. */
body.has-drawer-open {
    overflow: hidden;
}

/* ============================================================ stat cards == */
.stat-grid {
    display: grid;
    gap: var(--sp-4);
    grid-template-columns: 1fr;
}

@media (min-width: 577px) {
    .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 993px) {
    .stat-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .stat-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.stat-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-5);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-xs);
    overflow: hidden;
    transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.stat-card::after {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: var(--c-primary);
    opacity: 0.85;
}

.stat-card--success::after { background: var(--c-success); }
.stat-card--warning::after { background: var(--c-warning); }
.stat-card--danger::after  { background: var(--c-danger); }
.stat-card--info::after    { background: var(--c-info); }

.stat-card:hover {
    box-shadow: var(--sh-md);
    transform: translateY(-2px);
}

.stat-card__head {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}

.stat-card__icon {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border-radius: var(--r-md);
    background: var(--c-primary-soft);
    color: var(--c-primary);
    font-size: var(--fs-md);
    flex: 0 0 auto;
}

.stat-card--success .stat-card__icon { background: var(--c-success-soft); color: var(--c-success); }
.stat-card--warning .stat-card__icon { background: var(--c-warning-soft); color: var(--c-warning); }
.stat-card--danger  .stat-card__icon { background: var(--c-danger-soft);  color: var(--c-danger); }
.stat-card--info    .stat-card__icon { background: var(--c-info-soft);    color: var(--c-info); }

.stat-card__label {
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-text-muted);
}

.stat-card__value {
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    line-height: var(--lh-tight);
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
}

.stat-card__delta {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
}

.stat-card__delta--up { color: var(--c-success); }
.stat-card__delta--down { color: var(--c-danger); }

/* ============================================================ photo grid == */
/* Breakpoints follow CONTRACT §8: <=576, 577-992, >=993. */
.photo-grid {
    display: grid;
    gap: var(--sp-3);
    grid-template-columns: repeat(2, minmax(0, 1fr));
    margin: 0;
    padding: 0;
    list-style: none;
}

@media (min-width: 577px) {
    .photo-grid {
        gap: var(--sp-4);
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (min-width: 993px) {
    .photo-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* Larger cells for detail-heavy contexts: 1 / 2 / 3. */
.photo-grid--wide { grid-template-columns: minmax(0, 1fr); }

@media (min-width: 577px) {
    .photo-grid--wide { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 993px) {
    .photo-grid--wide { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.photo-tile {
    position: relative;
    display: block;
    aspect-ratio: 4 / 3;
    border-radius: var(--tile-radius);
    overflow: hidden;
    background-color: var(--c-surface-sunken);
    border: 1px solid var(--c-border);
    box-shadow: var(--sh-xs);
    isolation: isolate;
    transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.photo-tile:hover,
.photo-tile:focus-within {
    box-shadow: var(--sh-lg);
    transform: translateY(-2px);
}

.photo-tile__img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform var(--dur-slow) var(--ease-out);
    background-color: var(--c-surface-sunken);
}

.photo-tile:hover .photo-tile__img,
.photo-tile:focus-within .photo-tile__img {
    transform: scale(1.05);
}

/* Scrim + action row: hidden until hover / keyboard focus / coarse pointer. */
.photo-tile__overlay {
    position: absolute;
    inset: auto 0 0 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--sp-2);
    padding: var(--sp-6) var(--sp-3) var(--sp-3);
    background: linear-gradient(to top, var(--c-overlay), transparent);
    opacity: 0;
    transform: translateY(8px);
    transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.photo-tile:hover .photo-tile__overlay,
.photo-tile:focus-within .photo-tile__overlay {
    opacity: 1;
    transform: translateY(0);
}

.photo-tile__action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--hit-target);
    height: var(--hit-target);
    border: 0;
    border-radius: var(--r-md);
    background: var(--c-surface);
    color: var(--c-text);
    font-size: var(--fs-sm);
    cursor: pointer;
    box-shadow: var(--sh-sm);
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.photo-tile__action:hover,
.photo-tile__action:focus {
    background: var(--c-primary);
    color: var(--c-on-primary);
}

.photo-tile__badge {
    position: absolute;
    top: var(--sp-2);
    left: var(--sp-2);
    z-index: 1;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 2px var(--sp-2);
    border-radius: var(--r-pill);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
    font-variant-numeric: tabular-nums;
    backdrop-filter: blur(6px);
    box-shadow: var(--sh-xs);
}

.photo-tile__select {
    position: absolute;
    top: var(--sp-2);
    right: var(--sp-2);
    z-index: 1;
    width: 20px;
    height: 20px;
    cursor: pointer;
}

.photo-tile__caption {
    position: absolute;
    inset: auto 0 0 0;
    padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-xs);
    color: var(--c-text-inverse);
    background: linear-gradient(to top, var(--c-overlay), transparent);
    pointer-events: none;
}

/* Touch devices never fire hover — keep the controls permanently visible. */
@media (hover: none) {
    .photo-tile__overlay {
        opacity: 1;
        transform: none;
    }
}

/* ======================================================== confidence tag == */
.badge-confidence-high,
.badge-confidence-medium,
.badge-confidence-low {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 2px var(--sp-2);
    border-radius: var(--r-pill);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
    letter-spacing: 0.01em;
    font-variant-numeric: tabular-nums;
    border: 1px solid transparent;
}

.badge-confidence-high {
    background: var(--c-success-soft);
    color: var(--c-success);
    border-color: rgb(var(--rgb-success) / 0.35);
}

.badge-confidence-medium {
    background: var(--c-warning-soft);
    color: var(--c-warning);
    border-color: rgb(var(--rgb-warning) / 0.35);
}

.badge-confidence-low {
    background: var(--c-danger-soft);
    color: var(--c-danger);
    border-color: rgb(var(--rgb-danger) / 0.35);
}

.badge-soft {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 2px var(--sp-2);
    border-radius: var(--r-pill);
    background: var(--c-surface-sunken);
    color: var(--c-text-muted);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    border: 1px solid var(--c-border);
}

.badge-soft--success { background: var(--c-success-soft); color: var(--c-success); border-color: rgb(var(--rgb-success) / 0.3); }
.badge-soft--warning { background: var(--c-warning-soft); color: var(--c-warning); border-color: rgb(var(--rgb-warning) / 0.3); }
.badge-soft--danger  { background: var(--c-danger-soft);  color: var(--c-danger);  border-color: rgb(var(--rgb-danger) / 0.3); }
.badge-soft--info    { background: var(--c-info-soft);    color: var(--c-info);    border-color: rgb(var(--rgb-info) / 0.3); }
.badge-soft--brand   { background: var(--c-primary-soft); color: var(--c-primary); border-color: rgb(var(--rgb-primary) / 0.3); }

/* ========================================================= progress panel = */
.progress-panel {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    padding: var(--sp-5);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-sm);
}

.progress-panel__head {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
}

.progress-panel__title {
    margin: 0;
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
}

.progress-panel__pct {
    margin-left: auto;
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    font-variant-numeric: tabular-nums;
    color: var(--c-primary);
}

.progress-panel__track {
    position: relative;
    height: 10px;
    border-radius: var(--r-pill);
    background: var(--c-surface-sunken);
    overflow: hidden;
}

.progress-panel__fill {
    height: 100%;
    width: 0;
    border-radius: var(--r-pill);
    background: var(--c-primary);
    transition: width var(--dur-slow) var(--ease-out), background-color var(--dur) var(--ease);
}

.progress-panel__track--indeterminate .progress-panel__fill {
    width: 35% !important;
    animation: convo-indeterminate 1.4s var(--ease) infinite;
}

.progress-panel.is-done .progress-panel__fill { background: var(--c-success); }
.progress-panel.is-done .progress-panel__pct { color: var(--c-success); }
.progress-panel.is-failed .progress-panel__fill { background: var(--c-danger); }
.progress-panel.is-failed .progress-panel__pct { color: var(--c-danger); }

.progress-panel__label {
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
    min-height: 1.4em;
}

.progress-panel__stats {
    display: grid;
    gap: var(--sp-3);
    grid-template-columns: repeat(2, minmax(0, 1fr));
    padding-top: var(--sp-3);
    border-top: 1px solid var(--c-border);
}

@media (min-width: 577px) {
    .progress-panel__stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.progress-panel__stat-label {
    display: block;
    font-size: var(--fs-2xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-subtle);
}

.progress-panel__stat-value {
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
}

@keyframes convo-indeterminate {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(300%); }
}

/* ============================================================ empty state = */
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--sp-3);
    padding: var(--sp-12) var(--sp-5);
    text-align: center;
    border: 1px dashed var(--c-border-strong);
    border-radius: var(--r-lg);
    background-color: var(--c-surface);
    color: var(--c-text-muted);
}

.empty-state__icon {
    display: grid;
    place-items: center;
    width: 64px;
    height: 64px;
    border-radius: var(--r-pill);
    background: var(--c-primary-soft);
    color: var(--c-primary);
    font-size: var(--fs-2xl);
}

.empty-state__title {
    margin: 0;
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    color: var(--c-text);
}

.empty-state__text {
    margin: 0;
    max-width: 46ch;
    font-size: var(--fs-sm);
}

/* =============================================================== skeleton = */
.skeleton {
    position: relative;
    display: block;
    height: 1em;
    border-radius: var(--r-sm);
    background-color: var(--c-skeleton-base);
    background-image: linear-gradient(
        90deg,
        var(--c-skeleton-base) 0%,
        var(--c-skeleton-sheen) 50%,
        var(--c-skeleton-base) 100%
    );
    background-size: 200% 100%;
    animation: convo-shimmer 1.35s linear infinite;
    overflow: hidden;
}

/* A single placeholder line, for one piece of text that has not arrived --
   the event name in the registration header, say. `.skeleton` itself is a
   block; this is the inline form. */
.skeleton-line {
    display: inline-block;
    width: 62%;
    height: 0.9em;
    vertical-align: middle;
    border-radius: var(--r-sm);
    background-color: var(--c-skeleton-base);
    background-image: linear-gradient(
        90deg,
        var(--c-skeleton-base) 0%,
        var(--c-skeleton-sheen) 50%,
        var(--c-skeleton-base) 100%
    );
    background-size: 200% 100%;
    animation: convo-shimmer 1.35s linear infinite;
}

.skeleton--text { height: 0.85em; margin-block: 0.35em; }
.skeleton--title { height: 1.4em; width: 45%; }
.skeleton--line-70 { width: 70%; }
.skeleton--line-50 { width: 50%; }
.skeleton--block { height: 120px; border-radius: var(--r-md); }
.skeleton--tile { aspect-ratio: 4 / 3; height: auto; border-radius: var(--tile-radius); }
.skeleton--circle { width: 40px; height: 40px; border-radius: var(--r-pill); }

.skeleton-stack {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}

@keyframes convo-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* ============================================================= data table = */
.data-table {
    position: relative;
    width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-xs);
}

.data-table > table {
    width: 100%;
    min-width: 720px;
    margin: 0;
    border-collapse: separate;
    border-spacing: 0;
    font-size: var(--fs-sm);
    color: var(--c-text);
}

.data-table thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: var(--sp-3) var(--sp-4);
    background-color: var(--c-surface-sunken);
    border-bottom: 1px solid var(--c-border);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-text-muted);
    text-align: left;
    white-space: nowrap;
}

.data-table tbody td {
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--c-border);
    vertical-align: middle;
}

.data-table tbody tr:last-child td {
    border-bottom: 0;
}

.data-table tbody tr:hover td {
    background-color: var(--c-primary-soft);
}

.data-table th.is-sortable {
    cursor: pointer;
    user-select: none;
}

.data-table th.is-sortable:hover {
    color: var(--c-text);
}

.data-table__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--c-border);
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.table-numeric { text-align: right; font-variant-numeric: tabular-nums; }

/* ================================================================ pager === */
.pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--sp-3);
    margin-top: var(--sp-4);
}

.pager__info {
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.pager__list {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    list-style: none;
    margin: 0;
    padding: 0;
    flex-wrap: wrap;
}

.pager__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--hit-target);
    height: var(--hit-target);
    padding: 0 var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background: var(--c-surface);
    color: var(--c-text);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.pager__btn:hover:not(:disabled) {
    background: var(--c-surface-sunken);
    border-color: var(--c-border-strong);
}

.pager__btn.is-active {
    background: var(--c-primary);
    border-color: var(--c-primary);
    color: var(--c-on-primary);
}

.pager__btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.pager__gap {
    padding: 0 var(--sp-1);
    color: var(--c-text-subtle);
}

/* ============================================================== drop zone = */
.drop-zone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--sp-3);
    min-height: 220px;
    padding: var(--sp-8) var(--sp-5);
    text-align: center;
    border: 2px dashed var(--c-border-strong);
    border-radius: var(--r-lg);
    background-color: var(--c-surface);
    color: var(--c-text-muted);
    cursor: pointer;
    transition: border-color var(--dur) var(--ease),
                background-color var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease);
}

.drop-zone:hover,
.drop-zone:focus-visible {
    border-color: var(--c-primary);
    background-color: var(--c-primary-soft);
}

.drop-zone.is-dragover {
    border-color: var(--c-primary);
    border-style: solid;
    background-color: var(--c-primary-soft-strong);
    box-shadow: var(--sh-focus);
}

.drop-zone.is-invalid {
    border-color: var(--c-danger);
    background-color: var(--c-danger-soft);
    color: var(--c-danger);
}

.drop-zone__icon {
    display: grid;
    place-items: center;
    width: 60px;
    height: 60px;
    border-radius: var(--r-pill);
    background: var(--c-primary-soft);
    color: var(--c-primary);
    font-size: var(--fs-xl);
}

.drop-zone__title {
    margin: 0;
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--c-text);
}

.drop-zone__hint {
    margin: 0;
    font-size: var(--fs-xs);
    color: var(--c-text-subtle);
}

.drop-zone input[type="file"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}

.drop-zone__preview {
    display: grid;
    gap: var(--sp-2);
    grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
    width: 100%;
    margin-top: var(--sp-3);
}

/* ============================================================== toasts ==== */
.toast-stack {
    position: fixed;
    z-index: var(--z-toast);
    right: var(--sp-4);
    bottom: var(--sp-4);
    left: var(--sp-4);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-2);
    pointer-events: none;
}

@media (min-width: 577px) {
    .toast-stack {
        left: auto;
        top: calc(var(--topbar-h) + var(--sp-3));
        bottom: auto;
        width: min(380px, calc(100vw - var(--sp-8)));
        align-items: flex-end;
    }
}

.app-toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    width: 100%;
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--c-border);
    border-left: 3px solid var(--c-primary);
    border-radius: var(--r-md);
    background-color: var(--c-surface-raised);
    color: var(--c-text);
    box-shadow: var(--sh-lg);
    font-size: var(--fs-sm);
    animation: convo-toast-in var(--dur-slow) var(--ease-out);
}

.app-toast.is-leaving {
    animation: convo-toast-out var(--dur) var(--ease) forwards;
}

.app-toast__icon { flex: 0 0 auto; margin-top: 2px; color: var(--c-primary); }
.app-toast__body { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

.app-toast__close {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--c-text-subtle);
    cursor: pointer;
    line-height: 1;
}

.app-toast__close:hover { background: var(--c-surface-sunken); color: var(--c-text); }

.app-toast--success { border-left-color: var(--c-success); }
.app-toast--success .app-toast__icon { color: var(--c-success); }
.app-toast--error   { border-left-color: var(--c-danger); }
.app-toast--error .app-toast__icon { color: var(--c-danger); }
.app-toast--warning { border-left-color: var(--c-warning); }
.app-toast--warning .app-toast__icon { color: var(--c-warning); }
.app-toast--info    { border-left-color: var(--c-info); }
.app-toast--info .app-toast__icon { color: var(--c-info); }

@keyframes convo-toast-in {
    from { opacity: 0; transform: translateY(12px) scale(0.98); }
    to   { opacity: 1; transform: none; }
}

@keyframes convo-toast-out {
    to { opacity: 0; transform: translateY(-8px) scale(0.98); }
}

/* ============================================================== forms ===== */
.form-label {
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    color: var(--c-text);
    margin-bottom: var(--sp-1);
}

.form-control,
.form-select {
    /* Height comes from the control scale in the foundation layer. */
    border-radius: var(--r-md);
    border-color: var(--c-border-strong);
    background-color: var(--c-surface);
    color: var(--c-text);
    font-size: var(--fs-sm);
}

.form-control::placeholder { color: var(--c-text-subtle); }

.form-control:focus,
.form-select:focus {
    background-color: var(--c-surface);
    color: var(--c-text);
    border-color: var(--c-primary);
    box-shadow: var(--sh-focus);
}

.form-text { color: var(--c-text-subtle); font-size: var(--fs-xs); }

.form-control.is-invalid,
.form-select.is-invalid {
    border-color: var(--c-danger);
}

.invalid-feedback { color: var(--c-danger); font-size: var(--fs-xs); }

.auth-page {
    display: grid;
    place-items: center;
    min-height: 100vh;
    padding: var(--sp-6) var(--sp-4);
    background:
        radial-gradient(1100px 480px at 50% -12%, var(--c-primary-soft), transparent 70%),
        var(--c-bg);
}

.auth-card {
    width: 100%;
    max-width: 420px;
    padding: var(--sp-8) var(--sp-6);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-xl);
    box-shadow: var(--sh-lg);
}

/* Auth card internals -- shared by the student, camera and admin sign-in pages. */
.auth-card__head {
    margin-bottom: var(--sp-5);
    text-align: center;
}

.auth-card__head h1 {
    margin: 0 0 var(--sp-2);
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    letter-spacing: -0.02em;
    color: var(--c-text);
}

.auth-brand {
    display: inline-flex;
    gap: var(--sp-2);
    align-items: center;
    margin-bottom: var(--sp-4);
    font-weight: var(--fw-semibold);
    color: var(--c-primary);
    text-decoration: none;
}

.auth-brand:hover,
.auth-brand:focus-visible {
    color: var(--c-primary-hover);
    text-decoration: none;
}

.auth-brand i {
    font-size: 1.15em;
}

.auth-card__sub {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.auth-card__foot {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    align-items: center;
    justify-content: center;
    margin-top: var(--sp-5);
    font-size: var(--fs-sm);
}

.auth-card__foot a,
.auth-card__foot .btn-link {
    color: var(--c-link);
}

.auth-sep {
    color: var(--c-text-subtle);
}

.auth-roles {
    margin: var(--sp-5) 0 0;
    padding-top: var(--sp-4);
    border-top: 1px solid var(--c-border);
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
    text-align: center;
}

/* These are signposts to the other sign-in pages, so they carry the rule at
   rest -- they need to look like links before anybody hovers them. */
.auth-roles a {
    color: var(--c-text-muted);
    background-size: 100% 1px;
    opacity: 0.85;
}

.auth-roles a:hover,
.auth-roles a:focus-visible {
    color: var(--c-primary);
    background-size: 100% 2px;
    opacity: 1;
}

/* Dense variant of the photo grid, for dashboard previews. */
.photo-grid--compact {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-2);
}

@media (min-width: 993px) {
    .photo-grid--compact {
        grid-template-columns: repeat(6, minmax(0, 1fr));
    }
}

/* Label/value pairs in side panels. */
.kv-list {
    display: grid;
    gap: var(--sp-3);
    margin: 0;
}

.kv-list > div {
    display: flex;
    gap: var(--sp-3);
    align-items: baseline;
    justify-content: space-between;
}

.kv-list dt {
    font-size: var(--fs-sm);
    font-weight: var(--fw-normal);
    color: var(--c-text-muted);
}

.kv-list dd {
    margin: 0;
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    text-align: right;
    color: var(--c-text);
}

/* Wider auth card, for the registration form's two-column layout. */
.auth-card--wide {
    max-width: 760px;
}

/* Required-field marker. */
.req {
    color: var(--c-danger);
    font-weight: var(--fw-bold);
}

/* Photo requirement checklist on the registration form. */
.photo-rules {
    display: grid;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.photo-rules li {
    display: flex;
    gap: var(--sp-2);
    align-items: baseline;
}

.photo-rules .fa-check {
    color: var(--c-success);
}

.photo-rules .fa-xmark {
    color: var(--c-danger);
}

/* Square preview of the chosen profile photo. */
.photo-preview {
    position: relative;
    aspect-ratio: 1;
    /* Capped and centred: the column is `col-lg-5`, so below 993px it goes
       full width and an uncapped square preview became as tall as the viewport
       is wide. A head-and-shoulders photo needs nothing like that. */
    max-width: 22rem;
    margin-inline: auto;
    overflow: hidden;
    background-color: var(--c-surface-sunken);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
}

.photo-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.photo-preview__placeholder {
    display: grid;
    gap: var(--sp-2);
    place-content: center;
    height: 100%;
    color: var(--c-text-subtle);
    font-size: var(--fs-sm);
    text-align: center;
}

.photo-preview__placeholder i {
    font-size: 2rem;
}

/* A drop zone that already holds a file recedes visually. */
.drop-zone.is-filled {
    border-style: solid;
    background-color: var(--c-surface);
}

/* ============================================================ misc bits === */
.chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
}

/* A badge that is also a filter button. It keeps the badge's own colour so the
   chip still reads as "12 archived", and gains a pressed state so the active
   filter is not colour-alone. */
.chip-toggle {
    border: 1px solid transparent;
    cursor: pointer;
    font: inherit;
    font-size: var(--fs-xs);
    transition: box-shadow var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease);
}

.chip-toggle:hover { border-color: currentColor; }

.chip-toggle[aria-pressed="true"] {
    border-color: currentColor;
    box-shadow: inset 0 0 0 1px currentColor;
    font-weight: var(--fw-semibold);
}

.chip-toggle[aria-pressed="true"]::after {
    content: " ×";
    font-weight: var(--fw-bold);
}

.divider-text {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    color: var(--c-text-subtle);
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}

.divider-text::before,
.divider-text::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--c-border);
}

.chart-box {
    position: relative;
    width: 100%;
    height: 280px;
}

.log-stream {
    max-height: 320px;
    overflow-y: auto;
    padding: var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background-color: var(--c-surface-sunken);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    line-height: 1.75;
    color: var(--c-text-muted);
}

.log-stream__row--error { color: var(--c-danger); }
.log-stream__row--ok { color: var(--c-success); }

/* SweetAlert2 theming through our tokens. */
.swal2-popup {
    background: var(--c-surface-raised) !important;
    color: var(--c-text) !important;
    border-radius: var(--r-lg) !important;
    border: 1px solid var(--c-border) !important;
    font-family: var(--font-sans) !important;
}

.swal2-title,
.swal2-html-container { color: var(--c-text) !important; }

.swal2-confirm.btn-brand,
.swal2-styled.swal2-confirm {
    background-color: var(--c-primary) !important;
    color: var(--c-on-primary) !important;
    border-radius: var(--r-md) !important;
}

.swal2-styled.swal2-cancel {
    background-color: var(--c-surface-sunken) !important;
    color: var(--c-text) !important;
    border-radius: var(--r-md) !important;
}

.swal2-styled.swal2-deny {
    background-color: var(--c-danger) !important;
    color: var(--c-on-status) !important;
    border-radius: var(--r-md) !important;
}

.swal2-input,
.swal2-textarea {
    background: var(--c-surface) !important;
    color: var(--c-text) !important;
    border: 1px solid var(--c-border-strong) !important;
    border-radius: var(--r-md) !important;
}

/* Bootstrap surfaces that would otherwise stay white in dark mode. */
.modal-content,
.dropdown-menu,
.offcanvas,
.list-group-item {
    background-color: var(--c-surface-raised);
    color: var(--c-text);
    border-color: var(--c-border);
}

.modal-header,
.modal-footer { border-color: var(--c-border); }

.dropdown-item { color: var(--c-text); border-radius: var(--r-sm); }
.dropdown-item:hover,
.dropdown-item:focus { background-color: var(--c-primary-soft); color: var(--c-primary); }
.dropdown-divider { border-color: var(--c-border); }

.nav-tabs { border-color: var(--c-border); }
.nav-tabs .nav-link { color: var(--c-text-muted); border-radius: var(--r-md) var(--r-md) 0 0; }
.nav-tabs .nav-link.active {
    background-color: var(--c-surface);
    border-color: var(--c-border) var(--c-border) var(--c-surface);
    color: var(--c-primary);
}

/* ========================================================= landing page === */
.landing-nav {
    position: sticky;
    top: 0;
    z-index: var(--z-topbar);
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
    background-color: var(--c-surface);
    border-bottom: 1px solid var(--c-border);
}

.landing-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
    font-weight: var(--fw-bold);
    font-size: var(--fs-lg);
    letter-spacing: -0.02em;
    color: var(--c-text);
    text-decoration: none;
}

.landing-brand:hover { color: var(--c-text); }

/*
   On a phone the brand is the longest single thing in the header, and next to
   a sign-in button it wrapped onto four lines and shoved the whole bar down.
   Below 480px it drops "(FRS)", keeps the mark, and stays on one line — the
   full name is still the page <title> and the footer heading.
*/
@media (max-width: 480px) {
    .landing-brand {
        /* Measured: "KIIT Event Gallery" needs 128px at --fs-md and the bar
           only spares ~120px next to the mark, the sign-in button and the
           theme toggle. One step down fits it without truncation. */
        font-size: var(--fs-sm);
        gap: var(--sp-2);
        min-width: 0;
    }

    /* Drop the parenthetical, keep the name whole. Truncating to
       "KIIT Event…" reads worse than simply not showing "(FRS)" — the full
       name is still the page <title> and the footer heading. */
    .landing-brand__suffix { display: none; }

    .landing-brand__text {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .landing-nav { padding: var(--sp-3) 0; }

    /*
       Measured at 375px: the bar's content box is 319px, the brand needs 152px
       and the sign-in group 173px, with a 12px gap — 18px over. Tightening the
       gutter and the gap on a phone returns 20px, which is what lets the full
       name sit on one line instead of ellipsising to "KIIT Event …".
    */
    .landing-nav .landing-wrap {
        padding-inline: var(--sp-2);
        gap: var(--sp-2);
    }

    .landing-nav .btn { padding-inline: var(--sp-3); }
    .landing-nav .btn i { display: none; }
}

.landing-hero {
    position: relative;
    overflow: hidden;
    padding: var(--sp-16) var(--sp-4) var(--sp-12);
    background:
        radial-gradient(900px 420px at 15% -10%, var(--c-primary-soft-strong), transparent 65%),
        radial-gradient(760px 380px at 92% 8%, var(--c-secondary-soft), transparent 60%),
        var(--c-bg);
}

@media (min-width: 993px) {
    .landing-hero { padding: var(--sp-20) var(--sp-8) var(--sp-16); }
}

.landing-wrap {
    width: 100%;
    max-width: var(--content-max);
    margin: 0 auto;
    padding-inline: var(--sp-4);
}

.landing-hero__title {
    font-size: var(--fs-hero);
    font-weight: var(--fw-bold);
    letter-spacing: -0.03em;
    line-height: 1.05;
    margin: 0 0 var(--sp-4);
}

.landing-hero__lead {
    max-width: 58ch;
    margin: 0 0 var(--sp-6);
    font-size: var(--fs-lg);
    color: var(--c-text-muted);
}

.landing-section {
    padding: var(--sp-12) 0;
}

.landing-section--alt {
    background-color: var(--c-surface);
    border-block: 1px solid var(--c-border);
}

.landing-grid {
    display: grid;
    gap: var(--sp-4);
    grid-template-columns: 1fr;
}

@media (min-width: 577px) {
    .landing-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 993px) {
    .landing-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.step-card {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-5);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-xs);
    height: 100%;
}

.landing-section--alt .step-card { background-color: var(--c-surface-raised); }

.step-card__num {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    border-radius: var(--r-pill);
    background: var(--c-primary-soft);
    color: var(--c-primary);
    font-weight: var(--fw-bold);
    font-variant-numeric: tabular-nums;
}

.step-card__title {
    margin: 0;
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
}

.step-card__text {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.role-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    height: 100%;
    padding: var(--sp-6) var(--sp-5);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-xl);
    box-shadow: var(--sh-sm);
    text-decoration: none;
    color: var(--c-text);
    transition: transform var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease),
                border-color var(--dur) var(--ease);
}

.role-card:hover,
.role-card:focus-visible {
    transform: translateY(-4px);
    box-shadow: var(--sh-lg);
    border-color: var(--c-primary);
    color: var(--c-text);
}

.role-card__icon {
    display: grid;
    place-items: center;
    width: 52px;
    height: 52px;
    border-radius: var(--r-lg);
    background: var(--c-primary-soft);
    color: var(--c-primary);
    font-size: var(--fs-xl);
}

/* The student card is the primary route into the app, so it gets the brand
   colour. It had no rule at all, which left the main call to action as the
   only one of the three without an accent. */
.role-card--student .role-card__icon { background: var(--c-primary-soft); color: var(--c-primary); }
.role-card--camera .role-card__icon { background: var(--c-secondary-soft); color: var(--c-secondary); }
.role-card--admin .role-card__icon { background: var(--c-accent-soft); color: var(--c-accent); }

.role-card__title {
    margin: 0;
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
}

.role-card__text {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
    flex: 1 1 auto;
}

.role-card__cta {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--c-primary);
}

.role-card:hover .role-card__cta i {
    transform: translateX(3px);
}

.role-card__cta i {
    transition: transform var(--dur) var(--ease);
}

.event-card {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    padding: var(--sp-5);
    height: 100%;
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-xs);
}

.landing-section--alt .event-card { background-color: var(--c-surface-raised); }

.event-card__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-3);
}

.event-card__name {
    margin: 0;
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
}

.event-card__year {
    font-size: var(--fs-xs);
    color: var(--c-text-subtle);
    font-variant-numeric: tabular-nums;
}

.event-card__meta {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.event-card__meta div {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
}

.event-card__meta dt {
    flex: 0 0 auto;
    font-weight: var(--fw-medium);
    color: var(--c-text-subtle);
}

.event-card__meta dd { margin: 0; }

.landing-footer {
    padding: var(--sp-10) 0 var(--sp-8);
    background-color: var(--c-surface);
    border-top: 1px solid var(--c-border);
    color: var(--c-text-muted);
    font-size: var(--fs-sm);
}

.landing-footer a { color: var(--c-text-muted); }
.landing-footer a:hover,
.landing-footer a:focus-visible { color: var(--c-primary); }

.footer-grid {
    display: grid;
    gap: var(--sp-6);
    grid-template-columns: 1fr;
}

@media (min-width: 769px) {
    .footer-grid { grid-template-columns: 2fr 1fr 1fr; }
}

.footer-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}

.footer-heading {
    margin: 0 0 var(--sp-3);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-text-subtle);
}

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

    *,
    *::before,
    *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }

    .skeleton {
        animation: none;
        background-image: none;
    }

    .photo-tile__overlay {
        opacity: 1;
        transform: none;
    }

    .photo-tile:hover,
    .stat-card:hover,
    .role-card:hover {
        transform: none;
    }
}

/* ================================================== print ================ */
@media print {
    .app-sidebar,
    .app-topbar,
    .toast-stack,
    .pager,
    .landing-nav {
        display: none !important;
    }

    .app-body { margin-left: 0; }
    .app-main { padding: 0; }
    body { background: var(--c-surface); }
}

/* ================================================= motion layer === */
/*
   Entrance and emphasis animation, shared by every page.

   Two rules govern everything here:

   1. **Motion is opt-in per element**, via a class, never a blanket
      `* { transition: all }`. A global transition makes every future layout
      change animate whether or not that reads as intentional.
   2. **Everything degrades to nothing** under `prefers-reduced-motion`. The
      block at the end of this section is not decoration — an entrance
      animation that cannot be turned off makes the app unusable for someone
      with a vestibular disorder.

   Entrances are also written so the element is VISIBLE if the animation never
   runs: they animate `from` a hidden state rather than setting `opacity: 0` in
   the base rule. A JS error that stops a class being added must not leave the
   page blank.
*/

@keyframes rise-in {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
}

@keyframes fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes pop-in {
    from { opacity: 0; transform: scale(0.96); }
    to   { opacity: 1; transform: none; }
}

/*
   A staggered entrance for a group.

   `--stagger-index` is set per child (inline, by the page or by ui.js) and
   multiplies the step, so a row of four stat cards arrives left to right
   instead of all at once. Capped in JS at a dozen or so: past that the last
   card would appear a noticeable pause after the first.
*/
.reveal {
    animation: rise-in 460ms var(--ease) both;
    animation-delay: calc(var(--stagger-index, 0) * 60ms);
}

.reveal--fade { animation-name: fade-in; }
.reveal--pop { animation-name: pop-in; }

/* An element that should animate only once it scrolls into view. */
.reveal-on-scroll {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity 520ms var(--ease), transform 520ms var(--ease);
}

.reveal-on-scroll.is-visible {
    opacity: 1;
    transform: none;
}

/* Numbers that count up need tabular figures or the width jitters. */
.is-counting { font-variant-numeric: tabular-nums; }

/*
   A soft pulse for something the user is waiting on — a queued job, a pending
   state. Slow and low-contrast on purpose: it should read as "alive", not as
   an alert demanding attention.
*/
@keyframes soft-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}

.is-pending { animation: soft-pulse 2s var(--ease) infinite; }

/* A brief highlight when a value changes under the reader's eyes. */
@keyframes flash-change {
    from { background-color: var(--c-primary-soft-strong); }
    to   { background-color: transparent; }
}

.just-changed {
    animation: flash-change 1.1s var(--ease);
    border-radius: var(--r-sm);
}

@media (prefers-reduced-motion: reduce) {
    .reveal,
    .reveal--fade,
    .reveal--pop {
        animation: none;
    }

    .reveal-on-scroll {
        opacity: 1;
        transform: none;
        transition: none;
    }

    .is-pending,
    .just-changed {
        animation: none;
    }
}

/* ==================================================== face scanner === */
/*
   The visual for a running photo search.

   It shows the student's OWN registration photo with a sweep passing over it,
   because that is literally what the server is doing: comparing that face
   against every photograph in the event. A generic spinner would be equally
   truthful about "something is happening" and tell them nothing about what.

   The whole thing is `aria-hidden`; the real status is announced from the
   live-region label beneath it, so a screen reader gets the stage and the
   percentage without a description of the animation.
*/

.scanner {
    display: grid;
    gap: var(--sp-4);
    justify-items: center;
    padding: var(--sp-5) var(--sp-4);
    text-align: center;
}

.scanner__stage {
    position: relative;
    width: min(15rem, 62vw);
    aspect-ratio: 1;
    border-radius: var(--r-xl);
    overflow: hidden;
    background-color: var(--c-surface-sunken);
    border: 1px solid var(--c-border);
    isolation: isolate;
}

.scanner__face {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    filter: saturate(0.85) contrast(1.05);
}

/* Shown when there is no photo to scan. */
.scanner__placeholder {
    display: grid;
    place-content: center;
    height: 100%;
    color: var(--c-text-subtle);
    font-size: 2.5rem;
}

/* The sweep. */
.scanner__sweep {
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: 32%;
    z-index: 2;
    background: linear-gradient(
        to bottom,
        transparent,
        rgb(var(--rgb-primary) / 0.30) 44%,
        rgb(var(--rgb-primary) / 0.62) 50%,
        rgb(var(--rgb-primary) / 0.30) 56%,
        transparent
    );
    animation: scanner-sweep 2.1s ease-in-out infinite;
}

@keyframes scanner-sweep {
    0%   { transform: translateY(-110%); }
    50%  { transform: translateY(320%); }
    100% { transform: translateY(-110%); }
}

/* Corner brackets — the "locked on" cue. */
.scanner__reticle {
    position: absolute;
    inset: 12%;
    z-index: 3;
    border-radius: var(--r-sm);
    background:
        linear-gradient(var(--c-primary), var(--c-primary)) 0 0 / 16px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 0 / 2px 16px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 0 / 16px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 0 / 2px 16px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 100% / 16px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 100% / 2px 16px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 100% / 16px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 100% / 2px 16px no-repeat;
    animation: scanner-reticle 2.8s var(--ease) infinite;
}

@keyframes scanner-reticle {
    0%, 100% { inset: 12%; opacity: 0.9; }
    50%      { inset: 18%; opacity: 0.5; }
}

/* An expanding ring, one per "pass". */
.scanner__pulse {
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
    box-shadow: 0 0 0 0 rgb(var(--rgb-primary) / 0.45);
    animation: scanner-pulse 2.1s ease-out infinite;
}

@keyframes scanner-pulse {
    0%   { box-shadow: 0 0 0 0 rgb(var(--rgb-primary) / 0.45); }
    70%  { box-shadow: 0 0 0 18px rgb(var(--rgb-primary) / 0); }
    100% { box-shadow: 0 0 0 0 rgb(var(--rgb-primary) / 0); }
}

.scanner__readout {
    display: grid;
    gap: var(--sp-1);
    max-width: 34ch;
}

.scanner__percent {
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
    color: var(--c-primary);
}

.scanner__label {
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
    min-height: 1.4em;
}

/* A thin determinate bar under the readout. */
.scanner__track {
    width: min(22rem, 80vw);
    height: 4px;
    border-radius: var(--r-pill);
    background-color: var(--c-surface-sunken);
    overflow: hidden;
}

.scanner__fill {
    height: 100%;
    width: 0;
    border-radius: inherit;
    background: linear-gradient(90deg, var(--c-primary), var(--c-secondary, var(--c-primary)));
    transition: width var(--dur) var(--ease);
}

.scanner__stats {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--sp-4);
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
}

.scanner__stats b {
    display: block;
    font-size: var(--fs-lg);
    font-variant-numeric: tabular-nums;
    color: var(--c-text);
}

/* Finished and failed states stop the motion and recolour the readout. */
.scanner.is-done .scanner__sweep,
.scanner.is-done .scanner__pulse,
.scanner.is-failed .scanner__sweep,
.scanner.is-failed .scanner__pulse {
    display: none;
}

.scanner.is-done .scanner__reticle,
.scanner.is-failed .scanner__reticle {
    animation: none;
    inset: 12%;
}

.scanner.is-done .scanner__percent { color: var(--c-success); }
.scanner.is-failed .scanner__percent { color: var(--c-danger); }
.scanner.is-failed .scanner__reticle { background-image: none; box-shadow: inset 0 0 0 2px var(--c-danger); }

@media (prefers-reduced-motion: reduce) {
    .scanner__sweep {
        animation: none;
        top: 34%;
        opacity: 0.5;
    }

    .scanner__reticle,
    .scanner__pulse {
        animation: none;
    }
}

/* ============================================== hero video === */
/*
   The landing hero's looping clip.

   It is decorative, so it carries `aria-hidden` and is out of the tab order —
   everything it shows is stated in the prose beside it. What it is NOT allowed
   to do is move when the visitor has asked for less motion, or play with no
   way to stop it, so there is a real pause control and a reduced-motion rule
   that stops it before the first frame.
*/

.hero-video {
    position: relative;
    margin: 0;
    border-radius: var(--r-xl);
    background-color: var(--c-surface-sunken);
    border: 1px solid var(--c-border);
    box-shadow: var(--sh-lg);
    overflow: hidden;
    isolation: isolate;
}

.hero-video__media {
    display: block;
    width: 100%;
    /*
       The frame follows the source. `--hero-ratio` is set from the video's own
       intrinsic dimensions once metadata arrives (see index.html), clamped to
       something that fits beside the hero text — the supplied clip is 9:16, and
       an unclamped 9:16 card would be ~1000px tall on a desktop. Until then
       this default holds the space so the card does not jump.
    */
    aspect-ratio: var(--hero-ratio, 3 / 4);
    /* Below the `lg` breakpoint the column goes full width, and a 3:4 card at
       880px is 1170px tall — taller than the viewport. The cap bounds it; the
       `cover` fit trims rather than distorts. */
    max-height: 32rem;
    object-fit: cover;
    background-color: var(--c-surface-sunken);
}

/*
   A soft brand tint over the footage, so a clip shot under any lighting still
   sits inside the green/white palette instead of fighting it.
*/
.hero-video::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background:
        linear-gradient(to top, rgb(var(--rgb-scrim) / 0.55), transparent 45%),
        linear-gradient(120deg, rgb(var(--rgb-primary) / 0.18), transparent 60%);
}

/* The reticle: a corner-bracket frame, the visual shorthand for "detecting". */
.hero-video__frame {
    position: absolute;
    inset: var(--sp-4);
    z-index: 2;
    pointer-events: none;
    border-radius: var(--r-md);
    /* Drawn as four corner brackets rather than a full box: a complete
       rectangle reads as a border, brackets read as a viewfinder. */
    background:
        linear-gradient(var(--c-primary), var(--c-primary)) 0 0 / 18px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 0 / 2px 18px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 0 / 18px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 0 / 2px 18px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 100% / 18px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 100% / 2px 18px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 100% / 18px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 100% / 2px 18px no-repeat;
    opacity: 0.85;
    animation: hero-reticle 4s var(--ease) infinite;
}

@keyframes hero-reticle {
    0%, 100% { inset: var(--sp-4); opacity: 0.85; }
    50%      { inset: var(--sp-5); opacity: 0.45; }
}

/* A scan sweep travelling down the frame. */
.hero-video__scanline {
    position: absolute;
    inset-inline: 0;
    top: 0;
    z-index: 2;
    height: 28%;
    pointer-events: none;
    background: linear-gradient(
        to bottom,
        transparent,
        rgb(var(--rgb-primary) / 0.22) 45%,
        rgb(var(--rgb-primary) / 0.45) 50%,
        rgb(var(--rgb-primary) / 0.22) 55%,
        transparent
    );
    animation: hero-scan 3.6s linear infinite;
}

@keyframes hero-scan {
    from { transform: translateY(-100%); }
    to   { transform: translateY(400%); }
}

/* The confidence chip, echoing what the gallery actually shows. */
.hero-video__tag {
    position: absolute;
    top: var(--sp-5);
    left: var(--sp-5);
    z-index: 3;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    animation: hero-tag-in 640ms var(--ease) 900ms both;
}

@keyframes hero-tag-in {
    from { opacity: 0; transform: translateY(-6px) scale(0.94); }
    to   { opacity: 1; transform: none; }
}

.hero-video__toggle {
    position: absolute;
    top: var(--sp-4);
    right: var(--sp-4);
    z-index: 3;
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 1px solid rgb(var(--rgb-scrim) / 0.25);
    border-radius: 50%;
    background-color: rgb(var(--rgb-scrim) / 0.45);
    color: var(--c-on-scrim);
    font-size: var(--fs-xs);
    backdrop-filter: blur(4px);
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease),
                transform var(--dur-fast) var(--ease);
}

.hero-video__toggle:hover { background-color: rgb(var(--rgb-scrim) / 0.7); }
.hero-video__toggle:active { transform: scale(0.94); }

.hero-video__caption {
    position: absolute;
    inset: auto 0 0 0;
    z-index: 3;
    padding: var(--sp-4) var(--sp-5);
    color: var(--c-on-scrim);
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    text-shadow: 0 1px 2px rgb(var(--rgb-scrim) / 0.6);
}

/* Paused: the sweep and the reticle stop with the footage. */
.hero-video.is-paused .hero-video__scanline,
.hero-video.is-paused .hero-video__frame {
    animation-play-state: paused;
}

@media (max-width: 992px) {
    .hero-video__caption { font-size: var(--fs-xs); padding: var(--sp-3) var(--sp-4); }
    .hero-video__frame { inset: var(--sp-3); }
    @keyframes hero-reticle {
        0%, 100% { inset: var(--sp-3); opacity: 0.85; }
        50%      { inset: var(--sp-4); opacity: 0.45; }
    }
}

/*
   Reduced motion: nothing moves. The JS also pauses the video itself, because
   a CSS rule cannot stop a <video> from playing.
*/
@media (prefers-reduced-motion: reduce) {
    .hero-video__scanline,
    .hero-video__frame,
    .hero-video__tag {
        animation: none;
    }

    .hero-video__scanline { opacity: 0.25; }
}

/* ============================================================= toolbar === */
/* Filter/sort/action strip above a list or grid. */
.toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    align-items: center;
    justify-content: space-between;
    padding: var(--sp-3) var(--sp-4);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
}

.toolbar__stats {
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

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

/* The active filter in a segmented button group. */
.btn-group .btn.is-active {
    background-color: var(--c-primary);
    border-color: var(--c-primary);
    color: var(--c-on-primary);
}

@media (max-width: 576px) {
    .toolbar {
        padding: var(--sp-3);
    }

    .toolbar,
    .toolbar__actions {
        /* Stacked and full width beats four unhittable slivers. */
        flex-direction: column;
        align-items: stretch;
    }

    .toolbar__actions > .btn-group,
    .toolbar__actions > .form-select,
    .toolbar__actions > .btn {
        width: 100%;
    }

    .toolbar__actions > .form-select {
        /* Overrides the w-auto the markup uses on wider screens. */
        width: 100% !important;
    }
}

/* Hover/focus actions on a photo tile. */
.photo-tile__actions {
    position: absolute;
    inset: auto 0 0 0;
    display: flex;
    gap: var(--sp-2);
    justify-content: center;
    padding: var(--sp-2);
    margin: 0;
    background: linear-gradient(to top, rgb(var(--rgb-scrim) / 0.55), transparent);
    opacity: 0;
    transition: opacity var(--dur) var(--ease);
}

.photo-tile:hover .photo-tile__actions,
.photo-tile:focus-within .photo-tile__actions {
    opacity: 1;
}

/*
   Touch devices have no hover, so the actions would be unreachable. Show them
   permanently there rather than requiring a long-press nobody will discover.
*/
@media (hover: none) {
    .photo-tile__actions {
        opacity: 1;
    }
}

/* "cap, glasses" hint explaining a lower score. */
.photo-tile__note {
    position: absolute;
    inset: auto auto var(--sp-2) var(--sp-2);
    padding: 2px 6px;
    border-radius: var(--r-sm);
    background-color: var(--c-overlay);
    color: var(--c-on-scrim);
    font-size: var(--fs-2xs);
    pointer-events: none;
}

.photo-tile:hover .photo-tile__note,
.photo-tile:focus-within .photo-tile__note {
    /* Gets in the way of the action buttons once they appear. */
    opacity: 0;
}

/* ============================================================== viewer === */
.viewer-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    align-items: center;
    justify-content: space-between;
}

.viewer-bar__position {
    font-size: var(--fs-sm);
    font-variant-numeric: tabular-nums;
    color: var(--c-text-muted);
}

.viewer-bar__nav {
    display: flex;
    gap: var(--sp-2);
}

.viewer {
    display: grid;
    place-items: center;
    margin: 0;
    padding: var(--sp-3);
    background-color: var(--c-surface-sunken);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    overflow: hidden;
}

.viewer__img {
    max-width: 100%;
    /*
       Capped against the viewport, not the container: a 6000px convocation
       photograph would otherwise push the details panel off the screen.
    */
    max-height: min(72vh, 900px);
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--r-md);
    background-color: var(--c-surface);
}

@media (max-width: 576px) {
    .viewer {
        padding: var(--sp-2);
        margin-inline: calc(-1 * var(--sp-1));
    }

    .viewer__img {
        max-height: 60vh;
    }

    .viewer-bar {
        gap: var(--sp-2);
    }

    /* Bigger targets than a 32px chevron for thumb navigation. */
    .viewer-bar__nav .btn {
        min-width: 56px;
    }
}

/* =========================================================== stage log === */
/* Append-only history of what a background job is doing. */
.stage-log {
    display: grid;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--fs-sm);
}

.stage-log__item {
    display: flex;
    gap: var(--sp-3);
    align-items: baseline;
    padding-bottom: var(--sp-2);
    border-bottom: 1px solid var(--c-border);
}

.stage-log__item:last-child {
    border-bottom: 0;
    padding-bottom: 0;
    font-weight: var(--fw-medium);
}

.stage-log__time {
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
    font-size: var(--fs-xs);
    color: var(--c-text-subtle);
}

/* Photo-quality badge row on the profile page. */
.quality-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    align-items: center;
}

.quality-row__score {
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

/* ======================================================== method list === */
/* Available/unavailable upload methods, and similar capability lists. */
.method-list {
    display: grid;
    gap: var(--sp-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

.method-list__item {
    display: flex;
    gap: var(--sp-3);
    align-items: flex-start;
    font-size: var(--fs-sm);
}

.method-list__item > i {
    margin-top: 3px;
    color: var(--c-success);
}

.method-list__item.is-off {
    color: var(--c-text-muted);
}

.method-list__item.is-off > i {
    color: var(--c-text-subtle);
}

/* ====================================================== upload queue === */
/* One row per file in a batch upload, with its own outcome. */
.queue {
    display: grid;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    list-style: none;
    max-height: 420px;
    overflow-y: auto;
}

.queue__item {
    display: grid;
    grid-template-columns: 40px 1fr auto;
    gap: var(--sp-3);
    align-items: center;
    padding: var(--sp-2) var(--sp-3);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    font-size: var(--fs-sm);
}

.queue__thumb {
    width: 40px;
    height: 40px;
    object-fit: cover;
    border-radius: var(--r-sm);
    background-color: var(--c-surface-sunken);
}

.queue__name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.queue__meta {
    display: block;
    font-size: var(--fs-2xs);
    color: var(--c-text-muted);
}

.queue__state {
    flex: 0 0 auto;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
}

.queue__item.is-stored  { border-color: rgb(var(--rgb-success) / 0.45); }
.queue__item.is-stored  .queue__state { color: var(--c-success); }
.queue__item.is-skipped .queue__state { color: var(--c-warning); }
.queue__item.is-failed  { border-color: rgb(var(--rgb-danger) / 0.45); }
.queue__item.is-failed  .queue__state { color: var(--c-danger); }

@media (max-width: 576px) {
    .queue__item {
        grid-template-columns: 32px 1fr;
        grid-template-areas: "thumb name" ". state";
        row-gap: var(--sp-1);
    }

    .queue__thumb { grid-area: thumb; width: 32px; height: 32px; }
    .queue__name  { grid-area: name; }
    .queue__state { grid-area: state; }
}

/* Taller drop zone, for the dedicated batch-upload page. */
.drop-zone--tall {
    padding: var(--sp-8) var(--sp-5);
}

/* A drop zone that cannot accept anything right now. */
.drop-zone.is-disabled {
    opacity: 0.55;
    cursor: not-allowed;
    background-color: var(--c-surface-sunken);
}

.drop-zone.is-disabled:hover {
    border-color: var(--c-border);
}

@media (max-width: 576px) {
    .drop-zone--tall {
        padding: var(--sp-5) var(--sp-3);
    }
}

/* Filename/size caption under a tile in the operator's history. */
.photo-tile__caption {
    position: absolute;
    inset: auto 0 0 0;
    padding: var(--sp-2);
    margin: 0;
    background: linear-gradient(to top, rgb(var(--rgb-scrim) / 0.7), transparent);
    color: var(--c-on-scrim);
    font-size: var(--fs-2xs);
    line-height: var(--lh-snug);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Stands in for a photo whose file was cleared after the event. */
.photo-tile__purged {
    display: grid;
    gap: var(--sp-1);
    place-content: center;
    width: 100%;
    aspect-ratio: 1;
    background-color: var(--c-surface-sunken);
    color: var(--c-text-subtle);
    font-size: var(--fs-2xs);
    text-align: center;
}

.photo-tile__purged i {
    font-size: 1.5rem;
}

/* =============================================================== charts === */
/*
   Charts need an explicit height: Chart.js with maintainAspectRatio:false
   fills its parent, and a parent with no height collapses to zero.
*/
.chart-wrap {
    position: relative;
    height: 300px;
}

.chart-wrap--short {
    height: 220px;
}

@media (max-width: 576px) {
    .chart-wrap {
        height: 240px;
    }

    .chart-wrap--short {
        height: 200px;
    }
}

/* A stat card that is also a link to its detail screen. */
.stat-card--link {
    text-decoration: none;
    color: inherit;
}

.stat-card--link:hover {
    text-decoration: none;
    color: inherit;
    border-color: var(--c-border-strong);
}

.stat-card--link:focus-visible {
    outline: 2px solid var(--c-border-focus);
    outline-offset: 2px;
}

/* ============================================ mobile refinements === */
/*
   Mobile-specific corrections. Everything is inside a max-width query, so the
   desktop layout above is untouched.
*/

/* ---- iOS zoom-on-focus ---- */
/*
   Safari on iOS zooms the viewport whenever a focused field's font-size is
   under 16px, and never zooms back out. Forcing 16px on the controls is the
   only reliable fix; 'maximum-scale=1' would work too but breaks pinch-zoom
   for users who need it.
*/
@media (max-width: 576px) {
    .form-control,
    .form-select,
    .input-group-text,
    textarea,
    input[type="text"],
    input[type="email"],
    input[type="password"],
    input[type="search"],
    input[type="tel"],
    input[type="number"],
    input[type="date"],
    input[type="datetime-local"] {
        font-size: 16px;
    }
}

@media (max-width: 576px) {
    /* ---- auth pages ---- */
    .auth-page {
        padding: var(--sp-3) var(--sp-3) var(--sp-6);
        place-items: start center;
    }

    .auth-card,
    .auth-card--wide {
        max-width: 100%;
        padding: var(--sp-5) var(--sp-4);
        border-radius: var(--r-lg);
    }

    .auth-card__head {
        margin-bottom: var(--sp-4);
    }

    .auth-card__head h1 {
        font-size: var(--fs-xl);
    }

    .auth-brand {
        margin-bottom: var(--sp-3);
        font-size: var(--fs-sm);
    }

    /* ---- topbar ---- */
    /* Two short lines beat one wrapped to three. */
    .app-topbar {
        gap: var(--sp-2);
        padding: var(--sp-2) var(--sp-3);
    }

    .app-topbar__title {
        font-size: var(--fs-md);
        line-height: var(--lh-snug);
    }

    .app-topbar__subtitle {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: 46vw;
    }

    /* ---- stat cards: two up, compact ---- */
    /*
       Four single-column cards pushed everything else below the fold. Two
       columns keeps the whole row of numbers glanceable, which is the only
       reason these cards exist.
    */
    .stat-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--sp-3);
    }

    .stat-card {
        padding: var(--sp-3);
        gap: var(--sp-1);
    }

    .stat-card__head {
        gap: var(--sp-2);
    }

    .stat-card__icon {
        width: 30px;
        height: 30px;
        font-size: var(--fs-sm);
    }

    .stat-card__label {
        font-size: var(--fs-2xs);
        letter-spacing: 0;
    }

    .stat-card__value {
        font-size: var(--fs-xl);
    }

    .stat-card__delta {
        font-size: var(--fs-2xs);
    }

    /* A card that needs the full width can opt out. */
    .stat-grid > .stat-card--wide {
        grid-column: 1 / -1;
    }

    /* ---- content density ---- */
    .app-main {
        padding: var(--sp-3);
    }

    .card-body {
        padding: var(--sp-4) var(--sp-3);
    }

    h1, .h1 { font-size: var(--fs-2xl); }
    h2, .h2 { font-size: var(--fs-xl); }

    /* Alerts carry a lot of copy on this app; tighten them up. */
    .alert {
        padding: var(--sp-3);
        font-size: var(--fs-sm);
    }

    /* ---- actions ---- */
    /*
       A row of buttons at the bottom of a card should stack and fill the width
       rather than shrink into unhittable slivers.
    */
    .btn-lg {
        padding: 0.7rem 1rem;
        font-size: var(--fs-md);
    }

    .action-row {
        display: grid;
        gap: var(--sp-2);
    }

    .action-row > .btn {
        width: 100%;
    }

    /*
       Every interactive target clears the 44px touch minimum.

       For controls that is one line: the small size stops being small where
       fingers are involved. Patching each selector is how the four different
       heights got here in the first place.
    */
    :root {
        --control-h-sm: var(--control-h);
    }

    /* These two are not on the control scale but are still things you tap. */
    .page-link,
    .app-nav__link {
        min-height: var(--hit-target);
    }

    /* ---- tables ---- */
    /* Wide admin tables scroll inside themselves; the page never scrolls sideways. */
    .data-table,
    .table-responsive {
        margin-inline: calc(-1 * var(--sp-3));
        padding-inline: var(--sp-3);
    }

    /* ---- photo grids ---- */
    .photo-grid {
        gap: var(--sp-2);
    }

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

    /* The accuracy badge must stay readable on a small tile. */
    .photo-tile__badge {
        font-size: var(--fs-2xs);
        padding: 2px 6px;
    }

    /* ---- registration photo step ---- */
    .drop-zone {
        padding: var(--sp-4) var(--sp-3);
    }

    .drop-zone__icon {
        font-size: 1.75rem;
    }

    /* Preview beside a 375px-wide drop zone would be unusably small; cap it
       and centre it instead of letting the grid squeeze it. */
    .photo-preview {
        max-width: 200px;
        margin-inline: auto;
    }

    /* ---- key/value panels ---- */
    .kv-list > div {
        flex-direction: column;
        gap: 2px;
        align-items: flex-start;
    }

    .kv-list dd {
        text-align: left;
    }

    /* ---- progress ---- */
    .progress-panel {
        padding: var(--sp-3);
    }
}

/* Very narrow phones (iPhone SE and similar). */
@media (max-width: 380px) {
    .stat-grid {
        grid-template-columns: 1fr;
    }

    .app-topbar__subtitle {
        display: none;
    }
}

/* Landscape phones: the sticky topbar eats too much of a 375px-tall viewport. */
@media (max-height: 480px) and (orientation: landscape) {
    .app-topbar {
        position: static;
    }

    .auth-page {
        min-height: auto;
        padding-block: var(--sp-4);
    }
}

/* Respect a notch / rounded corners. */
@supports (padding: max(0px)) {
    @media (max-width: 576px) {
        .app-topbar {
            padding-left: max(var(--sp-3), env(safe-area-inset-left));
            padding-right: max(var(--sp-3), env(safe-area-inset-right));
        }

        .app-main {
            padding-left: max(var(--sp-3), env(safe-area-inset-left));
            padding-right: max(var(--sp-3), env(safe-area-inset-right));
            padding-bottom: max(var(--sp-6), env(safe-area-inset-bottom));
        }
    }
}

/* ===================================== stacked tables on phones === */

/*
 * A data table that becomes a list of cards below 700px.
 *
 * `.data-table` alone gives a wide table its own horizontal scroller, which is
 * correct but poor on a phone: an eight-column registration row means the
 * reader swipes sideways to find out whether a student has a face photo. Opt a
 * table into `.data-table--stack` and each row becomes a block whose cells
 * carry their own label, so nothing scrolls and nothing is dropped.
 *
 * Every `<td>` in such a table MUST have `data-label`, because the header row
 * is hidden and the label is the only thing naming the value.
 */
@media (max-width: 700px) {
    .data-table--stack {
        overflow-x: visible;
        border: 0;
        background: transparent;
    }

    .data-table--stack table,
    .data-table--stack tbody,
    .data-table--stack tr,
    .data-table--stack td {
        display: block;
        width: 100%;
    }

    /*
     * The base rule gives `.data-table > table` a `min-width: 720px` so wide
     * tables scroll rather than squash. That min-width has to go here, or every
     * stacked "card" stays 720px wide and its values sit off the right edge
     * where only the labels are visible.
     */
    .data-table--stack > table {
        min-width: 0;
        max-width: 100%;
    }

    /* Cell padding and rules come from the card, not from the table. */
    .data-table--stack tbody td,
    .data-table--stack tbody tr:last-child td {
        border-bottom: 0;
    }

    .data-table--stack tbody tr:hover td {
        background-color: transparent;
    }

    .data-table--stack thead {
        /* Hidden visually; the per-cell labels replace it. */
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .data-table--stack tr {
        margin-bottom: var(--sp-3);
        padding: var(--sp-3) var(--sp-4);
        background-color: var(--c-surface);
        border: 1px solid var(--c-border);
        border-radius: var(--r-lg);
        box-shadow: var(--sh-xs);
    }

    .data-table--stack tr:last-child { margin-bottom: 0; }

    .data-table--stack td {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: var(--sp-3);
        padding: var(--sp-1) 0;
        border: 0;
        text-align: right;
    }

    .data-table--stack td::before {
        content: attr(data-label);
        flex: 0 0 auto;
        font-size: var(--fs-xs);
        font-weight: var(--fw-medium);
        color: var(--c-text-subtle);
        text-align: left;
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }

    /* A cell with no label is the row's own heading: give it the full width. */
    .data-table--stack td:not([data-label]) {
        display: block;
        text-align: left;
        padding-bottom: var(--sp-2);
        margin-bottom: var(--sp-2);
        border-bottom: 1px solid var(--c-border);
        font-weight: var(--fw-semibold);
    }

    .data-table--stack td:not([data-label])::before { content: none; }

    /* Action buttons get a row of their own rather than a cramped column. */
    .data-table--stack td[data-label="Actions"] {
        display: block;
        text-align: left;
        padding-top: var(--sp-3);
    }

    .data-table--stack td[data-label="Actions"]::before { display: none; }

    .data-table--stack .table-numeric { text-align: right; }

    /* The row-select checkbox belongs beside the heading, not on its own line. */
    .data-table--stack td:has(> .form-check-input[type="checkbox"]) {
        display: inline-flex;
        width: auto;
        padding-right: var(--sp-2);
    }

    .data-table--stack td:has(> .form-check-input[type="checkbox"])::before {
        display: none;
    }
}

/* ============================================== share link ========== */
/*
   An address somebody is going to copy, print or put behind a QR code. It is
   shown in full rather than as "Copy link", because a person sending it to
   three thousand graduands wants to see what they are sending.
*/

.share-link {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-3);
    background-color: var(--c-surface-sunken);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
}

.share-link__label {
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    color: var(--c-text-muted);
    white-space: nowrap;
}

.share-link__url {
    flex: 1 1 260px;
    min-width: 0;
    font-size: var(--fs-sm);
    color: var(--c-primary);
    background: none;
    padding: 0;
    /* Breaks anywhere rather than forcing the card sideways on a phone. */
    overflow-wrap: anywhere;
}

/* =========================================== upload destinations ===== */
/*
   Where an uploaded photograph is kept, and what is already kept.

   The unavailable option stays on screen, dimmed, with its reason. An option
   that is simply absent when S3 is off is indistinguishable from an option
   that never existed, and the reason ("S3_ENABLED is off in .env") is also
   the fix.
*/

.destinations {
    border: 0;
    padding: 0;
    margin: 0;
}

.destination {
    display: flex;
    gap: var(--sp-3);
    align-items: flex-start;
    padding: var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    cursor: pointer;
    transition: border-color var(--dur-fast) var(--ease),
                background-color var(--dur-fast) var(--ease);
}

.destination + .destination {
    margin-top: var(--sp-2);
}

.destination:hover:not(.is-unavailable) {
    border-color: var(--c-primary);
}

.destination:has(input:checked) {
    border-color: var(--c-primary);
    background-color: var(--c-primary-soft);
}

/* :has() is well supported now, but a browser without it must still show the
   choice clearly -- the radio itself is visible and does the work. */
.destination input {
    margin-top: 3px;
    flex: 0 0 auto;
    accent-color: var(--c-primary);
}

.destination__body {
    display: grid;
    gap: 2px;
    min-width: 0;
}

.destination__label {
    font-weight: var(--fw-semibold);
}

.destination__detail {
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
    line-height: 1.5;
}

.destination__reason {
    font-size: var(--fs-xs);
    color: var(--c-warning);
    margin-top: 2px;
}

.destination.is-unavailable {
    cursor: not-allowed;
    opacity: 0.72;
    background-color: var(--c-surface-sunken);
}

/* The always-complete count above a table that may be a sample. With a
   thousand files the rows are capped; these numbers never are. */
.queue-tally {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    margin-bottom: var(--sp-3);
    font-size: var(--fs-sm);
}

.queue-tally__item {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
}

/* ---------------------------------------------- stored photograph list -- */

.stored-name {
    /* A filename is the widest thing in this table and the least useful when
       it forces a horizontal scrollbar. */
    max-width: 38ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 700px) {
    /* The stacked card form has the full width to itself. */
    .stored-name {
        max-width: none;
        white-space: normal;
        overflow-wrap: anywhere;
    }
}

/* A toolbar that is a section heading plus controls, not a page-wide bar. */
.toolbar--tight {
    margin-bottom: 0;
    padding: 0;
    background: none;
    border: 0;
}

/* The two halves of the landing footer's last line: the legal note, and who
   built it. Side by side on a desktop, stacked on a phone. */
.footer-bottom {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2);
    font-size: var(--fs-xs);
}

.footer-credit {
    color: var(--c-text-subtle);
    white-space: nowrap;
}

/* ============================================== rating strip ========= */
/*
   Five stars, small, at the end of a page. It is the least important thing on
   a screen of somebody's photographs, and it is sized to say so -- the version
   of this that interrupts with a modal is a toll gate on the thing they came
   for.
*/
.rating {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    border: 1px dashed var(--c-border);
    border-radius: var(--r-pill);
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
}

.rating__label {
    white-space: nowrap;
}

.rating__stars {
    display: inline-flex;
    gap: 1px;
}

.rating__star {
    /* Not a .btn: a 44px target per star would be a 220px control for the
       smallest question on the page. Still 28px, which is hittable. */
    width: 28px;
    height: 28px;
    display: grid;
    place-items: center;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--c-border-strong);
    font-size: 14px;
    cursor: pointer;
    transition: color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease-spring);
}

.rating__star:hover,
.rating__star:focus-visible {
    transform: scale(1.18);
}

.rating__star.is-lit {
    color: var(--c-warning);
}

.rating__stars.is-chosen .rating__star {
    cursor: default;
    transform: none;
}

.rating__thanks {
    color: var(--c-success);
    font-weight: var(--fw-semibold);
}

@media (prefers-reduced-motion: reduce) {
    .rating__star,
    .rating__star:hover,
    .rating__star:focus-visible {
        transition: none;
        transform: none;
    }
}

/* ==================================== rating, the admin's view ====== */

.rating-summary {
    padding-bottom: var(--sp-3);
    border-bottom: 1px solid var(--c-border);
    margin-bottom: var(--sp-3);
}

.rating-summary__score {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
}

.rating-summary__score strong {
    font-size: var(--fs-3xl);
    font-weight: var(--fw-bold);
    line-height: 1;
    letter-spacing: -0.02em;
}

.rating-summary__outof {
    color: var(--c-text-subtle);
    font-size: var(--fs-sm);
}

.rating-summary__stars {
    margin-left: auto;
    color: var(--c-border-strong);
    font-size: var(--fs-sm);
    letter-spacing: 1px;
}

.rating-summary__stars .is-lit { color: var(--c-warning); }

.rating-summary__meta {
    margin: var(--sp-2) 0 0;
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
}

/* Five rows, so no axis and no chart library. */
.rating-bars {
    display: grid;
    gap: 6px;
    margin: 0;
}

.rating-bars__row {
    display: grid;
    grid-template-columns: 3.2em 1fr;
    align-items: center;
    gap: var(--sp-2);
}

.rating-bars__row dt {
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
    white-space: nowrap;
}

.rating-bars__row dd {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin: 0;
    min-width: 0;
}

.rating-bars__track {
    flex: 1 1 auto;
    height: 8px;
    border-radius: var(--r-pill);
    background-color: var(--c-surface-sunken);
    overflow: hidden;
}

.rating-bars__fill {
    display: block;
    height: 100%;
    border-radius: inherit;
    background-color: var(--c-warning);
}

.rating-bars__count {
    min-width: 2.5em;
    text-align: right;
    font-size: var(--fs-xs);
    font-variant-numeric: tabular-nums;
    color: var(--c-text-muted);
}

/* ======================================== segmented control ========= */

.segmented {
    display: inline-flex;
    align-items: stretch;
    min-height: var(--control-h-sm);
    padding: 3px;
    gap: 2px;
    border-radius: var(--r-md);
    background-color: var(--c-surface-sunken);
    border: 1px solid var(--c-border);
}

.segmented__item {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 12px;
    border: 0;
    border-radius: calc(var(--r-md) - 3px);
    background: none;
    color: var(--c-text-muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    white-space: nowrap;
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease);
}

.segmented__item:hover { color: var(--c-text); }

.segmented__item.is-active {
    background-color: var(--c-surface);
    color: var(--c-primary);
    box-shadow: var(--sh-xs);
}

/* The gallery toolbar wraps onto its own line on a phone, where four segments
   plus a sort and a download button will not sit beside a count. */
@media (max-width: 767.98px) {
    .toolbar--gallery {
        display: grid;
        gap: var(--sp-3);
    }

    .toolbar--gallery .toolbar__actions {
        display: grid;
        gap: var(--sp-2);
    }

    .segmented {
        /* Each segment gets an equal share rather than sizing to its label,
           so the row does not end with one very wide "Worth a look". */
        display: grid;
        grid-template-columns: repeat(4, 1fr);
    }

    .segmented__item {
        padding-inline: 4px;
    }
}

/* ============================================ the leading action ===== */
/*
   The one thing a page is for, given its own shape rather than a card among
   cards. Tinted, so it reads as the place to start on a screen of readouts.
*/
.hero-action {
    display: grid;
    gap: var(--sp-3);
    padding: var(--sp-5);
    border-radius: var(--r-lg);
    border: 1px solid var(--c-primary-soft-strong);
    background:
        radial-gradient(120% 140% at 100% 0%, var(--c-primary-soft), transparent 62%),
        var(--c-surface);
}

.hero-action__title {
    margin: 0 0 var(--sp-1);
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    letter-spacing: -0.01em;
}

.hero-action__lead {
    margin: 0;
    color: var(--c-text-muted);
    font-size: var(--fs-sm);
    line-height: 1.55;
}

.hero-action__buttons {
    display: grid;
    gap: var(--sp-2);
}

/* min-width:0 so a long hint wraps instead of widening the grid column. */
.hero-action__text { min-width: 0; }

.hero-action__progress { margin-top: var(--sp-2); }

@media (min-width: 576px) {
    /* Text and action side by side once there is room for the button not to
       span the card. */
    .hero-action {
        grid-template-columns: 1fr auto;
        align-items: center;
        gap: var(--sp-4);
    }

    .hero-action__buttons {
        grid-auto-flow: column;
        align-items: center;
    }

    /* The progress panel spans both columns when it appears. */
    .hero-action__progress { grid-column: 1 / -1; }
}

/* A quiet link in a panel header: it is a route onward, not the heading. */
.panel__link {
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
    white-space: nowrap;
}

.panel__link:hover,
.panel__link:focus-visible { color: var(--c-primary); }

/* =================================================== panels ========== */

/* Vertical rhythm for a page made of sections, so each one does not have to
   remember its own bottom margin. */
.stack-lg {
    display: grid;
    gap: var(--sp-4);
}

.panel {
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-xs);
    overflow: hidden;
}

.panel__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    background-color: var(--c-surface-sunken);
    border-bottom: 1px solid var(--c-border);
}

.panel__title {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin: 0;
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.01em;
}

.panel__icon {
    color: var(--c-primary);
    font-size: 0.95em;
}

/* A fact about the section, not an action. Muted so it reads as a label. */
.panel__badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px 10px;
    border-radius: var(--r-pill);
    background-color: var(--c-surface);
    border: 1px solid var(--c-border);
    color: var(--c-text-subtle);
    font-size: var(--fs-2xs);
    white-space: nowrap;
}

.panel__body {
    padding: var(--sp-4);
}

.panel__note {
    margin: var(--sp-4) 0 0;
    padding-top: var(--sp-3);
    border-top: 1px dashed var(--c-border);
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
}

/* ---------------------------------------------------------- notices ---- */
/*
   Standing context, not an interruption. `.alert` is loud on purpose and is
   right at the top of a page; these sit inside a panel and explain a rule that
   is always true, so they are quieter and carry their icon in a gutter rather
   than inline with the text.
*/
.notice {
    display: flex;
    gap: var(--sp-3);
    padding: var(--sp-3);
    border-radius: var(--r-md);
    border: 1px solid var(--c-border);
    background-color: var(--c-surface-sunken);
    font-size: var(--fs-sm);
    line-height: 1.55;
}

.notice__icon {
    flex: 0 0 auto;
    margin-top: 2px;
    font-size: 1.05em;
    color: var(--c-text-muted);
}

.notice--good {
    border-color: rgb(var(--rgb-success) / 0.32);
    background-color: var(--c-success-soft);
}

.notice--good .notice__icon { color: var(--c-success); }

.notice--warn {
    border-color: rgb(var(--rgb-warning) / 0.35);
    background-color: var(--c-warning-soft);
}

.notice--warn .notice__icon { color: var(--c-warning); }

.notice--info {
    border-color: rgb(var(--rgb-info) / 0.3);
    background-color: var(--c-info-soft);
}

.notice--info .notice__icon { color: var(--c-info); }

.notice--locked .notice__icon { color: var(--c-text-subtle); }

/* ------------------------------------------------------ photo panel ---- */

.photo-panel {
    display: grid;
    gap: var(--sp-4);
    align-items: start;
}

@media (min-width: 769px) {
    /* The photograph is a fixed, modest column; the controls take the rest.
       A square preview that grows with the viewport is a portrait the size of
       a poster. */
    .photo-panel {
        grid-template-columns: minmax(180px, 240px) 1fr;
        gap: var(--sp-5);
    }
}

.photo-panel__figure { min-width: 0; }
.photo-panel__action { min-width: 0; }

/* ============================================== the search button ==== */

.btn-search {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    /* The gradient is wider than the button so it has somewhere to travel. */
    background: linear-gradient(110deg,
        var(--c-primary) 0%,
        var(--c-secondary) 45%,
        var(--c-primary) 100%);
    background-size: 220% 100%;
    background-position: 0% 0;
    border: 0;
    color: var(--c-on-primary);
    font-weight: var(--fw-semibold);
    box-shadow: 0 6px 18px -6px rgb(var(--rgb-primary) / 0.55);
    transition: transform 160ms var(--ease-spring),
                box-shadow 160ms var(--ease),
                background-position 600ms var(--ease);
}

/* The sweep. Slow and low-contrast: this is a button drawing attention to
   itself on a quiet page, not a thing demanding to be pressed. */
.btn-search::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(100deg,
        transparent 35%,
        rgb(255 255 255 / 0.28) 50%,
        transparent 65%);
    transform: translateX(-120%);
    animation: search-sweep 3.6s var(--ease) infinite;
}

@keyframes search-sweep {
    /* Two thirds of the cycle is nothing happening. */
    0%, 55% { transform: translateX(-120%); }
    85%     { transform: translateX(120%); }
    100%    { transform: translateX(120%); }
}

.btn-search:hover,
.btn-search:focus-visible {
    background-position: 100% 0;
    transform: translateY(-2px);
    box-shadow: 0 12px 26px -8px rgb(var(--rgb-primary) / 0.65);
    color: var(--c-on-primary);
}

.btn-search:active {
    transform: translateY(1px) scale(0.985);
    box-shadow: 0 2px 8px -4px rgb(var(--rgb-primary) / 0.6);
    transition-duration: 60ms;
}

.btn-search:disabled,
.btn-search.is-busy {
    transform: none;
    box-shadow: none;
    opacity: 0.72;
}

.btn-search:disabled::before,
.btn-search.is-busy::before {
    animation: none;
    opacity: 0;
}

/* The magnifier looks over the button when you hover it. */
.btn-search__icon {
    display: inline-block;
    margin-right: var(--sp-2);
    transition: transform 420ms var(--ease-spring);
}

.btn-search:hover .btn-search__icon,
.btn-search:focus-visible .btn-search__icon {
    animation: search-scan 1.1s var(--ease) infinite;
}

@keyframes search-scan {
    0%, 100% { transform: translateX(0) rotate(0deg); }
    25%      { transform: translateX(-3px) rotate(-14deg); }
    60%      { transform: translateX(3px) rotate(12deg); }
}

@media (prefers-reduced-motion: reduce) {
    /* The rule still appears, it just does not grow into place. */
    a,
    .btn-link {
        transition: none;
    }

    .btn-search,
    .btn-search::before,
    .btn-search__icon {
        animation: none;
        transition: none;
    }

    .btn-search:hover,
    .btn-search:active,
    .btn-search:focus-visible {
        transform: none;
    }

    .success-flash,
    .success-flash__card,
    .success-flash__ring,
    .success-flash__check,
    .app-tabbar__link.has-unseen .app-tabbar__icon,
    .app-nav__link.has-unseen > i {
        animation: none;
    }

    /* The tick still has to be a tick, not an unfinished stroke. */
    .success-flash__ring,
    .success-flash__check {
        stroke-dashoffset: 0;
    }
}

/* ========================================== success handoff ========== */
/*
   The beat between "it worked" and the next screen.

   Without it a finished search simply becomes a different page, and the
   student never sees the result of the thing they waited for. The tick draws
   itself: a stroke-dashoffset animation, so it reads as a confirmation being
   made rather than an image appearing.
*/

.success-flash {
    position: fixed;
    inset: 0;
    z-index: 1200;
    display: grid;
    place-items: center;
    background-color: rgb(var(--rgb-scrim) / 0.55);
    backdrop-filter: blur(4px);
    animation: fade-in 180ms var(--ease) both;
    cursor: pointer;
}

.success-flash__card {
    display: grid;
    justify-items: center;
    gap: var(--sp-2);
    padding: var(--sp-6);
    border-radius: var(--r-xl);
    background-color: var(--c-surface);
    box-shadow: var(--sh-lg);
    text-align: center;
    animation: success-card-in 320ms var(--ease-spring) both;
}

@keyframes success-card-in {
    from { opacity: 0; transform: scale(0.9) translateY(8px); }
    to   { opacity: 1; transform: none; }
}

.success-flash__tick {
    display: grid;
    place-items: center;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background-color: var(--c-primary-soft);
}

.success-flash__ring {
    stroke: var(--c-primary);
    stroke-width: 3;
    stroke-dasharray: 145;
    stroke-dashoffset: 145;
    animation: success-ring 460ms var(--ease-out) 60ms forwards;
}

.success-flash__check {
    stroke: var(--c-primary);
    stroke-width: 4.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: 44;
    stroke-dashoffset: 44;
    animation: success-check 320ms var(--ease-out) 420ms forwards;
}

@keyframes success-ring  { to { stroke-dashoffset: 0; } }
@keyframes success-check { to { stroke-dashoffset: 0; } }

.success-flash__title {
    margin: 0;
    font-size: var(--fs-lg);
    font-weight: var(--fw-bold);
}

.success-flash__sub {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

/* ============================================ the gallery is waiting == */
/*
   A tab that glows now and then, not continuously: a permanent glow is
   decoration the eye stops seeing, and it would pulse for as long as the
   student never opened their gallery. It stops the moment they do.
*/

.app-tabbar__link.has-unseen .app-tabbar__icon,
.app-nav__link.has-unseen > i {
    animation: tab-glow 2.8s var(--ease) infinite;
}

@keyframes tab-glow {
    /* Most of the cycle is rest. The signal is the two beats. */
    0%, 62%, 100% { box-shadow: 0 0 0 0 rgb(var(--rgb-primary) / 0); color: inherit; }
    70%  { box-shadow: 0 0 0 5px rgb(var(--rgb-primary) / 0.22); color: var(--c-primary); }
    78%  { box-shadow: 0 0 0 0 rgb(var(--rgb-primary) / 0);      color: var(--c-primary); }
    86%  { box-shadow: 0 0 0 5px rgb(var(--rgb-primary) / 0.22); color: var(--c-primary); }
    94%  { box-shadow: 0 0 0 0 rgb(var(--rgb-primary) / 0);      color: inherit; }
}

/* The count itself, for the tab bar, where there is room for it. */
.app-tabbar__icon[data-unseen]::after {
    content: attr(data-unseen);
    position: absolute;
    top: -3px;
    right: 2px;
    min-width: 15px;
    padding: 0 4px;
    border-radius: var(--r-pill);
    background-color: var(--c-primary);
    color: var(--c-on-primary);
    font-size: 9px;
    font-weight: var(--fw-bold);
    line-height: 15px;
}

.app-tabbar__icon[data-unseen] {
    overflow: visible;
}

/* ============================================ sidebar footer ========= */

.app-sidebar__signout {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    width: 100%;
    padding: var(--sp-2) var(--sp-3);
    margin-bottom: var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background: none;
    color: var(--c-text-muted);
    font-size: var(--fs-sm);
    cursor: pointer;
    transition: color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease),
                background-color var(--dur-fast) var(--ease);
}

.app-sidebar__signout:hover,
.app-sidebar__signout:focus-visible {
    color: var(--c-danger);
    border-color: var(--c-danger);
    background-color: var(--c-danger-soft);
}

.app-sidebar__who {
    font-size: var(--fs-2xs);
    color: var(--c-text-subtle);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-sidebar__credit {
    margin-top: var(--sp-1);
    font-size: var(--fs-2xs);
    color: var(--c-text-subtle);
}

/* ============================================== app feel ============= */
/*
   What separates a page from an application on a phone: the primary action is
   reachable and full width, things you press say so, and content arrives
   rather than appearing. None of it survives prefers-reduced-motion.
*/

.action-card {
    /* The one card on the dashboard that is a control rather than a readout. */
    border-color: var(--c-primary-soft-strong);
}

.action-card__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
}

.action-card__text { min-width: 0; }

.action-card__buttons {
    display: flex;
    gap: var(--sp-2);
}

@media (max-width: 575.98px) {
    .action-card__row { display: grid; }

    /* A 120px button floating right of a paragraph is a desktop shape. On a
       phone the action is the card, so it spans it. */
    .action-card__buttons { display: grid; }
    .action-card__buttons .btn { width: 100%; }
}

/*
   Press states.

   A touch device has no hover, so without this a tap that begins a page load
   or a request looks exactly like a tap that missed.
*/
@media (hover: none) {
    .btn:active,
    .stat-card:active,
    .photo-tile:active {
        transform: scale(0.975);
    }

    .btn,
    .stat-card,
    .photo-tile {
        transition: transform var(--dur-fast) var(--ease);
    }
}

@media (prefers-reduced-motion: reduce) {
    .btn:active,
    .stat-card:active,
    .photo-tile:active {
        transform: none;
    }
}

/* ================================================= brand + tab bar === */
/*
   The university mark, and the navigation that replaces the drawer on a phone.

   Both exist for the same reason: below 992px the sidebar folds into a drawer,
   and a drawer takes the application's identity and its navigation off screen
   at the same time. The mark goes into the bar; the four things a student does
   go into a bottom row where a thumb already is.
*/

.brand-mark {
    width: 36px;
    height: 36px;
    border-radius: var(--r-md);
    flex: 0 0 auto;
    display: block;
}

/* ---------------------------------------------------- the brand lockup - */

/* Two marks on one row: the university's, a hairline, then this system's.
   One custom property sizes both, so a context sets `--brand-h` and never
   touches the images. Height is the shared dimension and width follows the
   aspect of each file — a 4382x3327 wordmark and a 1254² icon cannot share a
   box, and forcing them into one is exactly what makes a pair of logos look
   amateur. */
.brand-lockup {
    --brand-h: 32px;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    flex: 0 0 auto;
    /* The images are inline-level children; without this the line box adds a
       descender's worth of space under the row. */
    line-height: 0;
}

.brand-lockup__kiit,
.brand-lockup__frs {
    height: var(--brand-h);
    width: auto;
    display: block;
    /* Overrides the global `img { max-width: 100% }`: inside a narrow flex
       parent that rule can shrink the wordmark's width without its height,
       which distorts it. */
    max-width: none;
}

/* Matched on what the eye measures, not on the box.
   The university's gear fills its trimmed file edge to edge, while the face
   mark is a circle inscribed in a square with a soft glow around it — so at
   the same box height the circle reads noticeably smaller. The multiplier
   brings the circle's diameter up to the gear's height. */
.brand-lockup__frs {
    height: calc(var(--brand-h) * 1.14);
}

.brand-lockup__rule {
    flex: 0 0 auto;
    width: 1px;
    height: calc(var(--brand-h) * 0.66);
    background: var(--c-border);
}

/* The auth card is the first thing a graduand sees and the only thing on the
   screen, so the lockup is allowed to be the largest it gets. */
.auth-brand__logo {
    --brand-h: 38px;
}

.landing-brand__logo {
    --brand-h: 34px;
}

/* One mark only, where there is no room for two: the phone tab bar and
   anywhere a caller asks for it explicitly. */
.brand-lockup--mark-only .brand-lockup__kiit,
.brand-lockup--mark-only .brand-lockup__rule {
    display: none;
}

/* ------------------------------------------------------ sidebar brand - */

.app-sidebar__brand {
    display: flex;
    align-items: center;
    /* Tighter than a normal row: the sidebar is 264px and now carries two
       marks, a rule and two lines of text. Measured — at sp-3 and a 30px
       lockup the name needed 144px and had 111, so "KIIT Event Gallery"
       arrived as "KIIT Event ...". */
    gap: var(--sp-2);
}

.app-sidebar__logo {
    --brand-h: 26px;
}

.app-sidebar__brandtext {
    display: grid;
    min-width: 0;
}

.app-sidebar__brandname {
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-sidebar__brandarea {
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
    line-height: 1.3;
}

/* ------------------------------------------------------- topbar brand - */

.app-topbar__brand {
    display: none;
    align-items: center;
    flex: 0 0 auto;
}

.app-topbar__logo {
    --brand-h: 28px;
}

.app-topbar__heading {
    min-width: 0;
}

/* --------------------------------------------------------- tab bar ---- */

.app-tabbar {
    display: none;
    position: sticky;
    bottom: 0;
    z-index: 30;
    /* Respects the home-indicator inset on an iPhone; 0 everywhere else. */
    padding-bottom: env(safe-area-inset-bottom, 0);
    background-color: rgb(var(--rgb-surface) / 0.92);
    backdrop-filter: blur(12px);
    border-top: 1px solid var(--c-border);
}

.app-tabbar__link {
    flex: 1 1 0;
    display: grid;
    justify-items: center;
    gap: 3px;
    padding: 8px 2px 7px;
    min-height: 54px;
    color: var(--c-text-muted);
    text-decoration: none;
    font-size: 11px;
    line-height: 1.2;
    -webkit-tap-highlight-color: transparent;
}

.app-tabbar__icon {
    position: relative;
    display: grid;
    place-items: center;
    width: 42px;
    height: 26px;
    border-radius: var(--r-pill);
    font-size: 16px;
    transition: background-color var(--dur) var(--ease),
                color var(--dur) var(--ease),
                transform var(--dur) var(--ease-spring);
}

.app-tabbar__link.is-active {
    color: var(--c-primary);
    font-weight: 600;
}

.app-tabbar__link.is-active .app-tabbar__icon {
    background-color: var(--c-primary-soft);
    color: var(--c-primary);
    transform: translateY(-1px);
}

/* The press state is the whole point on a touch device: without it a tap that
   starts a page load looks like a tap that did nothing. */
.app-tabbar__link:active .app-tabbar__icon {
    transform: scale(0.9);
}

.app-tabbar__label {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

@media (max-width: 991.98px) {
    .app-topbar__brand { display: flex; }

    .app-tabbar { display: flex; }

    /* The bar is sticky inside the scrolling column, so the last card needs
       room to clear it -- otherwise it sits permanently underneath. */
    .app-shell.has-tabbar .app-main {
        padding-bottom: calc(var(--sp-5) + 54px);
    }

    /*
       Where there is a tab bar, it is the navigation. The drawer held the same
       four links behind a hamburger, so the two together asked the reader to
       decide which one to use before they could go anywhere.
    */
    .app-shell.has-tabbar .app-sidebar,
    .app-shell.has-tabbar .app-sidebar__backdrop,
    .app-shell.has-tabbar .app-sidebar__toggle {
        display: none !important;
    }

    /* The identity block does not fit a phone bar. Sign-out stays, because
       wherever the tab bar replaced the drawer it took the drawer's copy with
       it -- an admin has no tab bar and keeps the drawer, but a camera
       operator has one and would otherwise have no way out. */
    .app-identity { display: none; }

    .app-topbar__signout { display: inline-flex; }
}

/*
   Above the drawer breakpoint the sidebar is the navigation, so the tab bar is
   the duplicate and it goes. The rule lives outside the media query above
   because `.app-tabbar` is `display: none` by default -- this is only here to
   say so out loud, next to its opposite.
*/
@media (min-width: 992px) {
    .app-tabbar { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .app-tabbar__icon { transition: none; }
    .app-tabbar__link:active .app-tabbar__icon { transform: none; }
}

/* ============================================ registration polish === */
/*
   Registration is four steps, and these are the pieces that make it read like
   an app rather than a form: a stepper that says where you are, a code field
   that behaves like the one in every other app, and a strength meter that
   advises instead of refusing.
*/

.auth-page--wizard {
    align-items: flex-start;
    padding-block: var(--sp-5);
}

.auth-card--wizard {
    width: 100%;
    /* `.auth-card` caps at 420px, and max-width beats width -- so the wizard's
       wider card has to raise the cap, not just ask for more width. */
    max-width: 560px;
    position: relative;
    overflow: hidden;
    padding-bottom: var(--sp-5);
}

.auth-card--wizard::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: linear-gradient(90deg, var(--c-primary), var(--c-secondary), var(--c-primary));
    background-size: 200% 100%;
    animation: auth-accent 6s linear infinite;
}

@keyframes auth-accent {
    from { background-position: 0% 0; }
    to   { background-position: 200% 0; }
}

/* ---------------------------------------------------------- head ------ */

.wizard__head {
    display: grid;
    gap: var(--sp-3);
    margin-bottom: var(--sp-4);
}

/* The brand is two lines here, not one: `.auth-brand` lays its children out
   inline, so without this the name and the descriptor run together as
   "KIIT Event GalleryConvocation photography". */
.auth-brand__text {
    display: grid;
    text-align: left;
    line-height: 1.25;
}

.auth-brand__sub {
    display: block;
    font-size: var(--fs-xs);
    font-weight: var(--fw-normal);
    color: var(--c-text-muted);
}

.wizard__event {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
    min-height: 1.25em;
}

.wizard__title {
    margin: 0 0 var(--sp-2);
    font-size: var(--fs-2xl);
    font-weight: 700;
    letter-spacing: -0.01em;
    line-height: 1.2;
}

.wizard__lead {
    margin: 0 0 var(--sp-4);
    color: var(--c-text-muted);
    font-size: var(--fs-sm);
    line-height: 1.6;
}

/* ---------------------------------------------------------- stepper --- */
/*
   Four bullets on a rail. The rail is drawn by each step's ::before rather
   than as a separate element, so the markup stays an ordered list and a
   screen reader hears "1 of 4" instead of a decoration.
*/

.stepper {
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
}

.stepper__step {
    position: relative;
    flex: 1 1 0;
    display: grid;
    justify-items: center;
    gap: 6px;
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
    transition: color var(--dur) var(--ease);
}

.stepper__step:not(:first-child)::before {
    content: "";
    position: absolute;
    top: 13px;
    right: 50%;
    left: -50%;
    height: 2px;
    background-color: var(--c-border);
    transition: background-color var(--dur) var(--ease);
}

.stepper__step.is-done::before,
.stepper__step.is-current::before {
    background-color: var(--c-primary);
}

.stepper__bullet {
    position: relative;
    z-index: 1;
    width: 26px;
    height: 26px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background-color: var(--c-surface-sunken);
    border: 2px solid var(--c-border);
    font-size: 12px;
    font-weight: 700;
    line-height: 1;
    transition: background-color var(--dur) var(--ease),
                border-color var(--dur) var(--ease),
                color var(--dur) var(--ease),
                transform var(--dur) var(--ease);
}

.stepper__step.is-current { color: var(--c-text); }

.stepper__step.is-current .stepper__bullet {
    background-color: var(--c-primary);
    border-color: var(--c-primary);
    color: var(--c-on-primary);
    transform: scale(1.12);
}

.stepper__step.is-done .stepper__bullet {
    background-color: var(--c-primary-soft);
    border-color: var(--c-primary);
    color: var(--c-primary);
}

.stepper__step.is-done .stepper__bullet {
    /* fa-check, so a finished step reads as finished and not just as its own
       number in a different colour. */
    font-size: 0;
}

.stepper__step.is-done .stepper__bullet::after {
    content: "\2713";
    font-size: 13px;
    font-weight: 700;
}

.stepper__label {
    white-space: nowrap;
}

/* ---------------------------------------------------------- steps ----- */

/* ---------------------------------------------------------- actions --- */

.wizard__actions {
    display: flex;
    gap: var(--sp-2);
    margin-top: var(--sp-5);
}

.wizard__next {
    flex: 1 1 auto;
}

/* ------------------------------------------------------- code entry --- */

.code-panel {
    margin-top: var(--sp-4);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--c-border);
    /* This one keeps a fade: it appears in place, below a button that is still
       on screen, so without it the panel simply blinks into existence. */
    animation: fade-in 200ms var(--ease) both;
}

.code-panel__sent {
    margin: 0 0 var(--sp-3);
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.code-panel__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-top: var(--sp-2);
    font-size: var(--fs-xs);
}

.code-panel__hint {
    color: var(--c-text-muted);
    text-align: right;
}

.code-panel__hint.is-dev {
    color: var(--c-warning);
    font-weight: 600;
}

.code-input {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: var(--sp-2);
}

.code-input__box {
    width: 100%;
    /* Tall enough to be a comfortable target, square-ish at every width. */
    aspect-ratio: 3 / 4;
    max-height: 64px;
    padding: 0;
    text-align: center;
    font-size: var(--fs-xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--c-text);
    background-color: var(--c-surface-sunken);
    border: 2px solid var(--c-border);
    border-radius: var(--r-md);
    transition: border-color var(--dur) var(--ease),
                background-color var(--dur) var(--ease),
                transform var(--dur) var(--ease);
}

.code-input__box:focus {
    outline: none;
    border-color: var(--c-primary);
    background-color: var(--c-surface);
    transform: translateY(-2px);
}

.code-input.is-filled .code-input__box {
    border-color: var(--c-primary);
}

/* A confirmed address is still readable, just no longer editable. */
.is-locked .form-control[readonly] {
    background-color: var(--c-surface-sunken);
    color: var(--c-text-muted);
}

/* --------------------------------------------------- password meter --- */

.pw-meter {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: var(--sp-2);
}

.pw-meter__track {
    flex: 1 1 auto;
    height: 5px;
    border-radius: var(--r-pill);
    background-color: var(--c-surface-sunken);
    overflow: hidden;
}

.pw-meter__fill {
    height: 100%;
    width: 0;
    border-radius: inherit;
    background-color: var(--c-text-muted);
    transition: width 320ms var(--ease), background-color 320ms var(--ease);
}

.pw-meter__label {
    font-size: var(--fs-xs);
    font-weight: 600;
    min-width: 3.5em;
    text-align: right;
}

/* Advice, not a verdict: even "weak" is accepted, so it is amber and not red.
   A red bar beside a button that works anyway teaches people to ignore red. */
.pw-meter__fill[data-level="weak"],
.pw-meter__label[data-level="weak"]   { background-color: var(--c-warning); color: var(--c-warning); }
.pw-meter__fill[data-level="fair"],
.pw-meter__label[data-level="fair"]   { background-color: var(--c-secondary); color: var(--c-secondary); }
.pw-meter__fill[data-level="good"],
.pw-meter__label[data-level="good"]   { background-color: var(--c-primary); color: var(--c-primary); }
.pw-meter__fill[data-level="strong"],
.pw-meter__label[data-level="strong"] { background-color: var(--c-success); color: var(--c-success); }

.pw-meter__label[data-level] { background-color: transparent; }

/* ------------------------------------------------------- photo step --- */

.photo-step {
    display: grid;
    gap: var(--sp-4);
}

.photo-step__preview {
    justify-self: center;
    width: min(200px, 60%);
}

/* min-width:0 is load-bearing: a grid item defaults to min-content width, so
   the long hint line under the drop zone would push the column wider than the
   card on a narrow phone. */
.photo-step__controls {
    min-width: 0;
}

/* -------------------------------------------------------- finished ---- */

.wizard__done {
    text-align: center;
    display: grid;
    justify-items: center;
    gap: var(--sp-3);
    padding: var(--sp-5) 0;
}

.wizard__tick {
    width: 64px;
    height: 64px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background-color: var(--c-primary-soft);
    color: var(--c-primary);
    font-size: 28px;
    animation: wizard-tick 480ms var(--ease-spring) both;
}

@keyframes wizard-tick {
    from { opacity: 0; transform: scale(0.4); }
    to   { opacity: 1; transform: scale(1); }
}

.wizard__redirect {
    margin: 0;
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
}

/*
   The face-detection pass over the chosen photo.

   Registration is the only moment the student waits on the face engine, and it
   is a wait that can end in rejection, so it gets a visual that says what is
   being examined rather than a generic spinner on the button.
*/
.photo-preview__scan {
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    background-color: rgb(var(--rgb-scrim) / 0.25);
}

.photo-preview__sweep {
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: 34%;
    background: linear-gradient(
        to bottom,
        transparent,
        rgb(var(--rgb-primary) / 0.35) 45%,
        rgb(var(--rgb-primary) / 0.7) 50%,
        rgb(var(--rgb-primary) / 0.35) 55%,
        transparent
    );
    animation: scanner-sweep 1.6s ease-in-out infinite;
}

.photo-preview__reticle {
    position: absolute;
    inset: 14%;
    border-radius: var(--r-sm);
    background:
        linear-gradient(var(--c-primary), var(--c-primary)) 0 0 / 14px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 0 / 2px 14px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 0 / 14px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 0 / 2px 14px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 100% / 14px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 0 100% / 2px 14px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 100% / 14px 2px no-repeat,
        linear-gradient(var(--c-primary), var(--c-primary)) 100% 100% / 2px 14px no-repeat;
    animation: scanner-reticle 2.2s var(--ease) infinite;
}

/*
   The photo checklist ticks itself off.

   The four rules are not decoration — a photo that breaks them produces no
   encoding and the student silently gets nothing. `.is-met` is applied by the
   page once a photo passes the client-side checks it can actually verify.
*/
.photo-rules li {
    transition: color var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

.photo-rules li.is-met {
    color: var(--c-success);
}

.photo-rules li.is-met > i::before {
    /* fa-circle-check, so a met rule reads differently from an instruction. */
    content: "\f058";
}

@media (prefers-reduced-motion: reduce) {
    .photo-preview__sweep,
    .photo-preview__reticle,
    .stepper__step,
    .code-input__box,
    .pw-meter__fill,
    .wizard__tick {
        animation: none;
        transition: none;
    }
}
