/*
 * Kiosk stylesheet.
 *
 * Primary target: 10.2" iPad (9th gen). Portrait CSS viewport 810×1080,
 * landscape 1080×810. The layout is fluid (grid fractions + dvh) so it
 * also works in Safari (which adds ~80px of chrome) and when the iPad is
 * rotated. The previous stylesheet used fixed heights and a min-height
 * on the keys, which forced the keypad to overflow and clip the bottom
 * row on anything shorter than the ideal viewport.
 *
 * Aesthetic: workshop-instrument — Fluke/DeWalt vibe. Dark panel, tinted
 * neutrals, Funnel Display/Sans for a precise mechanical character.
 * See .impeccable.md for the full brief.
 */

/* ===================================================================
   Tokens
   =================================================================== */

:root {
    /* --- Type --- */
    --font-display: 'Funnel Display', -apple-system, BlinkMacSystemFont,
        'Segoe UI', sans-serif;
    --font-body: 'Funnel Sans', -apple-system, BlinkMacSystemFont,
        'Segoe UI', sans-serif;

    /* Fixed rem scale (kiosk viewport is fixed; product UIs should not
       use fluid type — it makes hit-box regressions nondeterministic). */
    --fs-caption: 0.75rem;     /* 12 */
    --fs-small: 0.875rem;      /* 14 */
    --fs-body: 1.125rem;       /* 18 */
    --fs-label: 1.25rem;       /* 20 */
    --fs-h3: 1.5rem;           /* 24 */
    --fs-h2: 1.875rem;         /* 30 */
    --fs-h1: 2.25rem;          /* 36 */
    --fs-display: 2.75rem;     /* 44 */
    --fs-hero: 3.5rem;         /* 56 */

    /* --- Color (OKLCH, tinted toward brand hue 252°) --- */
    --brand: oklch(0.60 0.18 252);
    --brand-deep: oklch(0.48 0.17 252);
    --brand-bright: oklch(0.72 0.14 252);
    --brand-soft: oklch(0.60 0.18 252 / 0.14);
    --brand-ring: oklch(0.60 0.18 252 / 0.30);

    --bg: oklch(0.17 0.015 252);
    --bg-raised: oklch(0.22 0.016 252);
    --bg-key: oklch(0.27 0.014 252);
    --bg-key-active: oklch(0.38 0.02 252);
    --bg-pill: oklch(0.24 0.015 252);

    --border: oklch(0.32 0.012 252);
    --border-strong: oklch(0.42 0.015 252);

    --text: oklch(0.96 0.004 252);
    --text-soft: oklch(0.84 0.008 252);
    --text-muted: oklch(0.66 0.012 252);
    --text-faint: oklch(0.48 0.012 252);

    --success: oklch(0.74 0.17 152);
    --success-soft: oklch(0.74 0.17 152 / 0.14);
    --warn: oklch(0.80 0.15 80);
    --warn-soft: oklch(0.80 0.15 80 / 0.14);
    --error: oklch(0.66 0.22 25);
    --error-soft: oklch(0.66 0.22 25 / 0.14);

    /* --- Space (4pt scale) --- */
    --space-2xs: 4px;
    --space-xs: 8px;
    --space-sm: 12px;
    --space-md: 16px;
    --space-lg: 20px;
    --space-xl: 28px;
    --space-2xl: 40px;
    --space-3xl: 56px;

    /* --- Radius --- */
    --radius-sm: 10px;
    --radius-md: 16px;
    --radius-lg: 22px;
    --radius-pill: 999px;

    /* --- Motion --- */
    --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
    --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

    /* --- Shadow --- */
    --shadow-card: 0 20px 50px -24px rgba(0, 0, 0, 0.7);
    --shadow-float: 0 30px 70px -24px rgba(0, 0, 0, 0.8),
        0 0 0 1px rgba(255, 255, 255, 0.04);
}

/* ===================================================================
   Reset + page
   =================================================================== */

* {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    padding: 0;
    width: 100%;
    /* dvh tracks the *actual* visible viewport — shrinks when Safari
       chrome appears, grows back when it hides. The vh fallback keeps
       older iPadOS working; dvh wins wherever both are supported. */
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 16px;
    font-feature-settings: 'ss01', 'cv11';  /* alt-1 and stylistic tweaks */
    -webkit-font-smoothing: antialiased;
    -webkit-tap-highlight-color: transparent;
    -webkit-user-select: none;
    user-select: none;
    overscroll-behavior: none;
    touch-action: manipulation;
}

button {
    font-family: inherit;
    border: 0;
    background: none;
    color: inherit;
    padding: 0;
    cursor: pointer;
}

input {
    font-family: inherit;
}

/* Ambient radial glow behind the whole page — a very subtle cue that
   the panel is "on", like the faint backlight bleed of a real device. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    background:
        radial-gradient(
            ellipse at 50% -10%,
            oklch(0.60 0.18 252 / 0.08),
            transparent 55%
        ),
        radial-gradient(
            ellipse at 50% 110%,
            oklch(0.60 0.18 252 / 0.06),
            transparent 50%
        );
    pointer-events: none;
    z-index: 0;
}

/* ===================================================================
   Screen container
   =================================================================== */

.screen {
    position: absolute;
    inset: 0;
    display: none;
    flex-direction: column;
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
        env(safe-area-inset-bottom) env(safe-area-inset-left);
    z-index: 1;
}

/* Default active state (pair screen + any future simple screens). The
   clock screen overrides this below to use a grid. Scoping both under
   `.active` keeps `.screen { display: none }` effective when inactive —
   the earlier version scoped `.clock-screen` to `display: grid`
   unconditionally, which beat `.screen { display: none }` on
   specificity and left the clock screen visible under the pair panel. */
.screen.active {
    display: flex;
}

/* ===================================================================
   Clock screen — grid-first layout
   =================================================================== */

.clock-screen.active {
    /* header → main (fills) → footer. The grid lets us keep the header
       and footer at their natural sizes and give the rest to `main`. */
    display: grid;
    grid-template-rows: auto 1fr auto;
}

/* ---- Header ----
 *
 * Intentionally minimal: just the wall-clock time on the right. Status
 * and build info are not competing for attention — they appear only
 * when they matter (offline pill slides in; double-tap on the clock
 * reveals the admin info panel).
 */

.clock-header {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    padding: var(--space-xs) var(--space-lg);
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border);
    gap: var(--space-sm);
    position: relative;
}

/* Offline/error chip — hidden entirely when the kiosk is online so the
   header reads as "just the time". Appears with a soft slide when the
   connection drops. */
.status-chip {
    display: none;
    align-items: center;
    gap: var(--space-xs);
    padding: 4px var(--space-sm);
    border-radius: var(--radius-pill);
    font-size: var(--fs-small);
    font-weight: 600;
    line-height: 1;
    letter-spacing: 0.02em;
    transition: background 200ms var(--ease-out-expo),
        color 200ms var(--ease-out-expo);
}

.status-chip.offline,
.status-chip.error {
    display: inline-flex;
    animation: chip-in 240ms var(--ease-out-expo) both;
}

.status-chip.offline {
    background: var(--warn-soft);
    color: var(--warn);
    border: 1px solid color-mix(in oklch, var(--warn) 40%, transparent);
}

.status-chip.error {
    background: var(--error-soft);
    color: var(--error);
    border: 1px solid color-mix(in oklch, var(--error) 40%, transparent);
}

@keyframes chip-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}

.online-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: none;
}

.status-chip.offline .online-dot {
    background: var(--warn);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--warn) 30%, transparent);
    animation: pulse 1.4s var(--ease-in-out) infinite;
}

.status-chip.error .online-dot {
    background: var(--error);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--error) 30%, transparent);
}

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

/* Live clock: it's the only thing in the header by default. It's also
   the tap target for the hidden admin gesture (double-tap reveals build
   info). Rendered as a button so it announces correctly to screen
   readers and gets proper focus treatment. */
.live-clock {
    font-family: var(--font-display);
    font-size: var(--fs-h3);
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    color: var(--text);
    letter-spacing: -0.01em;
    line-height: 1;
    padding: 6px var(--space-sm);
    border-radius: var(--radius-sm);
    background: transparent;
    transition: background 120ms var(--ease-out-quart);
}

.live-clock:active {
    background: var(--bg-key);
}

/* Admin info popover — anchored to the clock button. Revealed by a
   double-tap on the clock; auto-dismisses after 6s or on outside tap.
   Keeps diagnostic info available to an admin without putting it in
   front of regular users. */
.admin-info {
    position: absolute;
    top: calc(100% + var(--space-xs));
    right: var(--space-lg);
    min-width: 260px;
    padding: var(--space-md) var(--space-lg);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-float);
    font-size: var(--fs-small);
    color: var(--text-soft);
    z-index: 10;
    opacity: 0;
    transform: translateY(-6px);
    visibility: hidden;
    transition:
        opacity 180ms var(--ease-out-expo),
        transform 180ms var(--ease-out-expo),
        visibility 0s linear 180ms;
    pointer-events: none;
}

.admin-info.show {
    opacity: 1;
    transform: translateY(0);
    visibility: visible;
    pointer-events: auto;
    transition:
        opacity 180ms var(--ease-out-expo),
        transform 180ms var(--ease-out-expo),
        visibility 0s linear 0s;
}

.admin-info-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-lg);
    padding: var(--space-2xs) 0;
}

.admin-info-row + .admin-info-row {
    border-top: 1px solid var(--border);
    margin-top: var(--space-2xs);
    padding-top: var(--space-xs);
}

.admin-info-label {
    font-size: var(--fs-caption);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--text-faint);
    font-weight: 600;
}

.admin-info-value {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    color: var(--text);
    text-align: right;
    word-break: break-all;
}

/* ---- Main: stage + keypad ----
 *
 * Landscape is the primary orientation (iPad mounted sideways in the
 * shop). Side-by-side layout gives the keypad a near-square row/column
 * aspect so the 3×4 keys read as buttons, not as squat bars.
 *
 * Portrait is still supported for a user holding the iPad or installing
 * the kiosk on a device that can't be rotated — stage stacks on top of
 * the keypad. */

.clock-main {
    display: grid;
    grid-template-rows: 1fr;
    grid-template-columns: minmax(320px, 42%) 1fr;
    gap: var(--space-xl);
    padding: var(--space-lg) var(--space-2xl) var(--space-md);
    min-height: 0;
    position: relative;
}

@media (orientation: portrait) {
    .clock-main {
        grid-template-rows: minmax(180px, 34%) 1fr;
        grid-template-columns: 1fr;
        gap: var(--space-lg);
        padding: var(--space-lg) var(--space-xl) var(--space-md);
    }
}

/* ---- Stage: shared cell for display + result overlay ---- */

.stage {
    position: relative;
    min-height: 0;
    min-width: 0;
}

.display,
.result-overlay {
    position: absolute;
    inset: 0;
}

.display {
    background: linear-gradient(
        180deg,
        oklch(0.24 0.02 252) 0%,
        var(--bg-raised) 100%
    );
    border-radius: var(--radius-lg);
    border: 1px solid var(--border);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        var(--shadow-card);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-lg);
    padding: var(--space-xl);
    transition: border-color 220ms var(--ease-out-expo),
        background 220ms var(--ease-out-expo);
}

.display.error {
    border-color: color-mix(in oklch, var(--error) 60%, transparent);
    animation: shake 360ms var(--ease-out-expo);
}

@keyframes shake {
    0%, 100% { transform: translateX(0); }
    15% { transform: translateX(-12px); }
    30% { transform: translateX(10px); }
    45% { transform: translateX(-8px); }
    60% { transform: translateX(6px); }
    80% { transform: translateX(-3px); }
}

.display-prompt {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 500;
    color: var(--text);
    letter-spacing: -0.015em;
    line-height: 1.1;
    /* The <p> stretches to its widest bilingual line; without this the
       shorter English "Enter your PIN" sat left-aligned under the wider
       Spanish subtitle. Centering aligns both stacked languages on the
       same visual axis. */
    text-align: center;
}

.display-sub {
    margin: 0;
    font-size: var(--fs-body);
    color: var(--text-muted);
    min-height: calc(var(--fs-body) * 1.4);
    text-align: center;
    line-height: 1.4;
    max-width: 40ch;
}

.display-sub.warn {
    color: var(--warn);
}

.display-sub.error {
    color: var(--error);
}

/* ---- PIN dots ---- */

.pin-dots {
    display: flex;
    gap: var(--space-xl);
}

.pin-dot {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: transparent;
    border: 3px solid var(--border-strong);
    transition:
        background 260ms var(--ease-out-expo),
        border-color 260ms var(--ease-out-expo),
        box-shadow 260ms var(--ease-out-expo);
}

.pin-dot.filled {
    background: var(--brand);
    border-color: var(--brand);
    box-shadow: 0 0 0 6px var(--brand-ring);
    /* Single pulse when a digit lands. Runs once, no idle movement. */
    animation: pin-land 380ms var(--ease-out-expo);
}

@keyframes pin-land {
    0% { transform: scale(0.8); }
    55% { transform: scale(1.18); }
    100% { transform: scale(1); }
}

/* ---- Result overlay ---- */

.result-overlay {
    border-radius: var(--radius-lg);
    background: linear-gradient(
        160deg,
        var(--brand),
        var(--brand-deep)
    );
    color: oklch(0.98 0 0);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    padding: var(--space-xl);
    opacity: 0;
    visibility: hidden;
    transform: scale(0.94);
    transition:
        opacity 280ms var(--ease-out-expo),
        transform 280ms var(--ease-out-expo),
        visibility 0s linear 280ms;
    pointer-events: none;
    box-shadow: var(--shadow-float);
}

/* Portrait stage is ~34% of viewport height and the result overlay has
   five children (check, name, action, job-chip, time). Tightening the
   padding and gap keeps the column from clipping when the full stack
   renders (clock-OUT with a job code). Landscape stays roomy. */
@media (orientation: portrait) {
    .result-overlay {
        padding: var(--space-lg) var(--space-md);
        gap: var(--space-xs);
    }
}

.result-overlay.show {
    opacity: 1;
    visibility: visible;
    transform: scale(1);
    transition:
        opacity 280ms var(--ease-out-expo),
        transform 280ms var(--ease-out-expo),
        visibility 0s linear 0s;
}

.result-overlay.error {
    background: linear-gradient(
        160deg,
        oklch(0.60 0.22 25),
        oklch(0.42 0.18 25)
    );
}

.result-overlay.warn {
    background: linear-gradient(
        160deg,
        oklch(0.72 0.16 65),
        oklch(0.52 0.14 55)
    );
}

/* Stagger the overlay's internal elements so the reveal feels
   composed rather than all-at-once. Job chip slots in between the
   action line and the time so the reveal order reads top-to-bottom. */
.result-overlay.show .result-check { animation: rise 520ms var(--ease-out-expo) both; }
.result-overlay.show .result-name   { animation: rise 520ms var(--ease-out-expo) 60ms both; }
.result-overlay.show .result-action { animation: rise 520ms var(--ease-out-expo) 120ms both; }
.result-overlay.show .result-job    { animation: rise 520ms var(--ease-out-expo) 160ms both; }
.result-overlay.show .result-time   { animation: rise 520ms var(--ease-out-expo) 200ms both; }

@keyframes rise {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
}

.result-check {
    width: 76px;
    height: 76px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.18);
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.24);
}

.result-check svg {
    width: 44px;
    height: 44px;
    color: #fff;
}

.result-name {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--fs-display);
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.05;
    text-align: center;
}

.result-action {
    margin: 0;
    font-size: var(--fs-label);
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    opacity: 0.92;
    text-align: center;
}

.result-time {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--fs-h3);
    font-variant-numeric: tabular-nums;
    opacity: 0.86;
    letter-spacing: -0.01em;
    /* The <p> shrinks to the width of its wider bilingual line
       ("a las 3:47 PM" > "at 3:47 PM"). Without this, the English
       span sits left-aligned inside the Spanish-wide box and reads
       as visually off-center. Centering aligns both stacked languages
       on the same vertical axis — same fix the prompt uses above. */
    text-align: center;
}

/* ===================================================================
   Keypad
   =================================================================== */

.keypad {
    /* `minmax(0, 1fr)` is the critical fix — it lets rows shrink below
       their content's intrinsic min-size when the container is short.
       Previously `1fr` defaulted to `minmax(auto, 1fr)` which honored
       the min-height on .key and overflowed the bottom row. */
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: repeat(4, minmax(0, 1fr));
    gap: var(--space-sm);
    min-height: 0;
}

.key {
    background: var(--bg-key);
    color: var(--text);
    font-family: var(--font-display);
    font-size: var(--fs-hero);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        0 1px 0 rgba(0, 0, 0, 0.2);
    transition:
        transform 80ms var(--ease-out-quart),
        background 80ms var(--ease-out-quart),
        box-shadow 80ms var(--ease-out-quart),
        border-color 200ms var(--ease-out-quart);
    will-change: transform;
}

.key:active,
.key.pressed {
    background: var(--bg-key-active);
    border-color: var(--border-strong);
    transform: scale(0.965);
    box-shadow:
        inset 0 3px 12px rgba(0, 0, 0, 0.35),
        0 0 0 4px var(--brand-ring);
}

.key-util {
    font-family: var(--font-body);
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text-muted);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    /* Keys are flex containers (align/justify center). When a bilingual
       label lives inside, we need a column direction so English sits on
       top of Spanish instead of beside it. */
    flex-direction: column;
    gap: 2px;
}

.key-util svg {
    width: 40%;
    max-width: 44px;
    height: auto;
    stroke: var(--text-muted);
    transition: stroke 120ms var(--ease-out-quart);
}

.key-util:active svg,
.key-util.pressed svg {
    stroke: var(--text);
}

/* ===================================================================
   Job-code step (clock-OUT flow)
   ===================================================================
   The clock-main area runs in one of two modes: PIN (default, numeric
   keypad) or job-code (alphanumeric keypad). Mode switching is a single
   class on .clock-main; everything else falls out of the cascade.
*/

.display-job,
.keypad-job {
    /* Hidden until the state machine enters JOB_CODE. Using display:none
       instead of .hidden so the swap is atomic — no half-states. */
    display: none;
}

.clock-main.job-mode .display-pin,
.clock-main.job-mode .keypad-pin {
    display: none;
}

.clock-main.job-mode .display-job {
    display: flex;
}

.clock-main.job-mode .keypad-job {
    /* Grid of 7 cols × 6 rows: letters (4 rows), digits (2 rows), util
       keys sharing the last row with confirm/cancel. minmax(0, 1fr) so
       rows shrink below intrinsic min-size on a squeezed viewport — same
       trick as the PIN keypad. */
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    grid-template-rows: repeat(6, minmax(0, 1fr));
    gap: var(--space-xs);
    min-height: 0;
}

.display-greeting {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--fs-label);
    color: var(--brand-bright);
    font-weight: 500;
    letter-spacing: 0.02em;
    line-height: 1.1;
    text-align: center;
    /* Only takes space when populated — when the preview couldn't resolve
       a name, we fall back to an anonymous prompt and skip this line. */
    min-height: 0;
}

.display-greeting:empty {
    display: none;
}

/* Big tabular readout of the typed job code. Sits where the PIN dots do
   on the pin display, so the two modes feel like the same instrument
   switching tasks, not two different screens. Font size is --fs-display
   (not hero) so a 12-char code like "HON31-BUILD24" still fits the
   landscape panel (≈333px inside). Overflow guards against exotic
   hand-typed codes that might still push past the edge. */
.job-code-display {
    display: inline-flex;
    align-items: baseline;
    justify-content: center;
    max-width: 100%;
    padding: var(--space-sm) var(--space-lg);
    min-width: 9ch;
    min-height: calc(var(--fs-display) * 1.35);
    font-family: var(--font-display);
    font-size: var(--fs-display);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.06em;
    color: var(--text);
    background: var(--bg);
    border: 2px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.25);
    line-height: 1;
    overflow: hidden;
}

.job-code-text {
    white-space: nowrap;
}

.job-code-text:empty + .job-code-caret {
    /* When empty, the caret anchors to the left edge so the readout
       reads as "waiting for input" instead of a blank centered box. */
    margin-left: 0;
}

.job-code-text {
    color: var(--text);
}

.job-code-caret {
    display: inline-block;
    width: 3px;
    height: 0.9em;
    margin-left: 4px;
    background: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-ring);
    /* Blinks to signal the line is live; stops pulsing during the
       submit so a stalled network doesn't look like normal idle. */
    animation: caret-blink 1s steps(2, end) infinite;
}

.clock-main.job-mode.submitting .job-code-caret {
    animation: none;
    opacity: 0.4;
}

@keyframes caret-blink {
    0%, 49% { opacity: 1; }
    50%, 100% { opacity: 0; }
}

/* Alphanumeric key styling. Letter + digit keys reuse .key's base
   look (background, border, press feedback). Font drops to --fs-h1
   so 7-wide letters fit without crowding. */
.key-alpha,
.key-num {
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 500;
    letter-spacing: -0.01em;
}

.key-num {
    /* Digits get a hair more emphasis so eye can sort "I" (letter) from
       "1" (digit) — a classic near-miss on shop-floor codes. */
    color: var(--brand-bright);
}

.key-dash {
    /* Single non-alphanumeric key; match the util styling so it reads
       as a separator, not a letter. */
    font-family: var(--font-display);
    font-size: var(--fs-display);
    color: var(--text-muted);
    font-weight: 500;
}

.key-job-confirm {
    /* Confirm is the primary action on this screen. Spans 2 cells to
       read as the deliberate target and uses the brand ramp so it
       visually anchors the bottom-right corner. */
    grid-column: span 2;
    background: var(--brand);
    color: #fff;
    border: 1px solid transparent;
    font-family: var(--font-display);
    font-size: var(--fs-h2);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    flex-direction: column;
    gap: 2px;
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.12),
        0 6px 18px -8px var(--brand-ring);
    transition:
        transform 80ms var(--ease-out-quart),
        background 160ms var(--ease-out-expo),
        box-shadow 160ms var(--ease-out-expo),
        opacity 160ms var(--ease-out-expo);
}

.key-job-confirm:active,
.key-job-confirm.pressed {
    background: var(--brand-deep);
    transform: scale(0.985);
    box-shadow:
        inset 0 3px 10px rgba(0, 0, 0, 0.3),
        0 2px 8px -4px var(--brand-ring);
    border-color: transparent;
}

.key-job-confirm[aria-disabled='true'],
.key-job-confirm:disabled {
    background: var(--bg-key);
    color: var(--text-faint);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
    cursor: default;
    opacity: 0.7;
}

.key-job-confirm[aria-disabled='true']:active,
.key-job-confirm:disabled:active {
    transform: none;
}

.key-job-cancel {
    grid-column: span 2;
}

/* Result-overlay "Job · GPK36" row — only appears on clock-OUT success
   and carries the job code the user just entered. Tabular numerals so
   alpha/digit pairs ("GPK36") align across successive result flashes. */
.result-job {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--fs-h3);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.06em;
    text-align: center;
    color: rgba(255, 255, 255, 0.94);
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-sm);
    padding: 4px var(--space-md);
    background: rgba(0, 0, 0, 0.18);
    border-radius: var(--radius-pill);
    border: 1px solid rgba(255, 255, 255, 0.14);
}

.result-job[hidden] {
    display: none;
}

.result-job .job-label {
    font-size: var(--fs-small);
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    opacity: 0.75;
}

/* Reduced-motion: still blink the caret (it's the readout's "I'm
   listening" cue and is nearly imperceptible at 1 Hz) but drop the
   press scale on confirm — a "primary button" doesn't need motion. */
@media (prefers-reduced-motion: reduce) {
    .job-code-caret {
        animation: none;
        opacity: 0.6;
    }
}

/* ===================================================================
   Footer
   =================================================================== */

.clock-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--space-xs) var(--space-xl);
    background: var(--bg-raised);
    border-top: 1px solid var(--border);
    color: var(--text-muted);
    font-size: var(--fs-caption);
    min-height: 36px;
}

.footer-left {
    font-weight: 500;
    letter-spacing: 0.02em;
}

.footer-left:not(:empty)::before {
    content: '';
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--warn);
    margin-right: var(--space-xs);
    vertical-align: middle;
    box-shadow: 0 0 0 2px color-mix(in oklch, var(--warn) 30%, transparent);
}

.reset-corner {
    display: inline-block;
    width: 48px;
    height: 48px;
    /* Deliberately empty and transparent — hidden admin affordance.
       Sized big enough (48px) to hit reliably from a fingertip without
       a visual target. */
}

/* ===================================================================
   Pair screen
   =================================================================== */

.pair-screen {
    align-items: center;
    justify-content: center;
    background:
        radial-gradient(
            ellipse at 50% 30%,
            oklch(0.22 0.025 252) 0%,
            var(--bg) 70%
        );
}

.pair-panel {
    width: min(640px, 92vw);
    padding: var(--space-3xl) var(--space-2xl) var(--space-2xl);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-float);
    text-align: center;
}

.pair-brand {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-xs);
    font-family: var(--font-display);
    font-weight: 700;
    margin-bottom: var(--space-xl);
    padding-bottom: var(--space-md);
    border-bottom: 1px solid var(--border);
    letter-spacing: -0.01em;
}

.pair-brand .brand-mark {
    font-size: var(--fs-h1);
    color: var(--brand);
    /* Slight optical adjustment — the ampersand-heavy glyph sits
       better with a hair of negative letterspacing. */
    letter-spacing: -0.02em;
}

.pair-brand .brand-wordmark {
    font-size: var(--fs-h3);
    color: var(--text);
    text-transform: uppercase;
    font-weight: 600;
    letter-spacing: 0.08em;
}

.pair-title {
    margin: 0 0 var(--space-sm);
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.1;
}

.pair-subtitle {
    margin: 0 0 var(--space-2xl);
    color: var(--text-muted);
    font-size: var(--fs-body);
    line-height: 1.5;
    max-width: 42ch;
    margin-inline: auto;
}

.pair-subtitle strong {
    color: var(--text-soft);
    font-weight: 600;
}

.pair-input {
    display: block;
    width: 100%;
    padding: var(--space-lg) var(--space-sm);
    font-family: var(--font-display);
    font-size: 3rem;
    font-weight: 500;
    text-align: center;
    letter-spacing: 0.36em;
    text-indent: 0.36em; /* re-centers text given the letterspacing */
    background: var(--bg);
    border: 2px solid var(--border);
    border-radius: var(--radius-md);
    color: var(--text);
    font-variant-numeric: tabular-nums;
    outline: none;
    transition:
        border-color 160ms var(--ease-out-expo),
        box-shadow 160ms var(--ease-out-expo);
}

.pair-input::placeholder {
    color: var(--text-faint);
}

.pair-input:focus {
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-ring);
}

.pair-submit {
    margin-top: var(--space-md);
    width: 100%;
    padding: var(--space-lg);
    font-family: var(--font-display);
    font-size: var(--fs-label);
    font-weight: 600;
    background: var(--brand);
    color: #fff;
    border-radius: var(--radius-md);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    transition:
        background 160ms var(--ease-out-expo),
        transform 80ms var(--ease-out-quart),
        box-shadow 160ms var(--ease-out-expo);
    box-shadow: 0 8px 20px -10px var(--brand-ring);
}

.pair-submit:active {
    background: var(--brand-deep);
    transform: scale(0.99);
    box-shadow: 0 4px 10px -6px var(--brand-ring);
}

.pair-submit:disabled {
    opacity: 0.5;
    cursor: default;
}

.pair-error {
    margin: var(--space-md) 0 0;
    color: var(--error);
    font-size: var(--fs-small);
    min-height: calc(var(--fs-small) * 1.4);
    line-height: 1.4;
}

.pair-error:not(:empty) {
    padding: var(--space-sm) var(--space-md);
    background: var(--error-soft);
    border-radius: var(--radius-sm);
    margin-top: var(--space-md);
}

.pair-version {
    margin: var(--space-xl) 0 0;
    color: var(--text-faint);
    font-size: var(--fs-caption);
    font-variant-numeric: tabular-nums;
}

/* ===================================================================
   Wake lock video (hidden)
   =================================================================== */

.wake-video {
    position: fixed;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
    left: -10px;
    top: -10px;
}

/* ===================================================================
   Bilingual subtitle pattern (English primary + Mexican Spanish)
   ===================================================================
   Everything employee-facing renders both languages. By default the
   English leads and the Spanish sits below it in a smaller, muted
   weight — the classic subtitle treatment. The `.bilingual-inline`
   modifier switches to a side-by-side pair for tight spots (status
   chip, footer badge) where a second line would waste vertical room.
*/

.t-en {
    display: block;
    line-height: 1.15;
}

.t-es {
    display: block;
    margin-top: 0.18em;
    font-family: var(--font-body);
    font-size: 0.58em;
    font-weight: 400;
    letter-spacing: 0.01em;
    text-transform: none;
    color: var(--text-muted);
    line-height: 1.2;
}

/* When the parent already sets a small size (e.g. the keypad's Clear
   button at 1.125rem), a 0.58× subtitle becomes unreadable. Bump the
   ratio for small-font contexts. */
.key-util .t-es,
.footer-left .t-es {
    font-size: 0.78em;
}

/* Inline variant: single-line, mid-dot separator between languages.
   Used in compact status-bar spots where stacking costs too much
   vertical space. */
.bilingual-inline {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-xs);
}

.bilingual-inline .t-en,
.bilingual-inline .t-es {
    display: inline;
    margin: 0;
    line-height: 1;
}

.bilingual-inline .t-en {
    font-size: inherit;
}

.bilingual-inline .t-es {
    font-size: 0.86em;
    color: currentColor;
    opacity: 0.7;
}

.bilingual-inline .t-en:not(:last-child)::after {
    content: '·';
    margin-left: var(--space-xs);
    color: currentColor;
    opacity: 0.5;
}

/* Gradient-filled result overlay: the muted token doesn't read against
   a blue/red/amber ramp. Use alpha on the overlay's own foreground
   colour so the Spanish stays legible on every variant. */
.result-overlay .t-es {
    color: rgba(255, 255, 255, 0.78);
}

/* Sub-line already uses --text-muted for its colour; a second muted
   layer reads as invisible. Restore the primary color for the t-en
   inside .display-sub and keep the Spanish a touch below it. */
.display-sub .t-es {
    color: color-mix(in oklch, var(--text-muted) 70%, var(--text-faint));
}

/* ===================================================================
   Reduced motion — honor the OS preference if an admin ever sets it.
   Keeps functional transitions (press feedback, state changes) but
   removes decorative animations.
   =================================================================== */

@media (prefers-reduced-motion: reduce) {
    .pin-dot.filled,
    .result-overlay.show .result-check,
    .result-overlay.show .result-name,
    .result-overlay.show .result-action,
    .result-overlay.show .result-job,
    .result-overlay.show .result-time {
        animation: none;
    }

    .status-chip.offline .online-dot {
        animation: none;
    }

    .display.error {
        animation: none;
    }

    * {
        transition-duration: 80ms !important;
    }
}
