/* =========================================================================
   Nikster. Mobile-first.
   Blue #0A3EFD = idle / scan / logo. Magenta #FF00CC = playing.
   Green = QR lock. Red = QR miss.
   Font: Doppio One at /DoppioOne-Regular.woff2 (splash % uses sans-serif
   on purpose so the number does not jump when the face arrives).
   ========================================================================= */

/* ----- Self-hosted font ----- */
@font-face {
    font-family: 'Doppio One';
    src: url('/DoppioOne-Regular.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

/* ----- Reset & base ----- */
*,
*::before,
*::after { box-sizing: border-box; margin: 0; padding: 0; }

html { height: 100%; }

body {
    min-height: 100%;
    font-family: 'Doppio One', system-ui, Arial, sans-serif;
    background: #0d0f1c;   /* #0e0e14 + a breath of logo blue #0A3EFD */
    color: #f2f2f7;
    display: flex;
    flex-direction: column;
    align-items: center;
    overflow-x: hidden;
    position: relative;
    /* room for the docked scan button */
    padding-bottom: 8rem;
    /* the room warms up when a song plays (class from app.js) */
    transition: background-color 1s ease;
}

/* Playing wash. Keep this quiet — it sits under slashes + notes. */
body.music-playing {
    background: #250d26;   /* #0e0e14 blended 10% with #FF00CC */
}

.bottom-fade {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    height: 11rem;
    pointer-events: none;
    z-index: 9;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.92) 0%, rgba(0, 0, 0, 0.45) 42%, transparent 100%);
}

/* Top radial. Idle = logo blue. .music-playing flips it magenta. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    background:
        radial-gradient(ellipse 80% 50% at 50% -10%,
            rgba(76, 133, 249, 0.18),
            transparent 70%);
    pointer-events: none;
    transition: background 1s ease;
}

body.music-playing::before {
    background:
        radial-gradient(ellipse 80% 50% at 50% -10%,
            rgba(255, 0, 204, 0.14),
            transparent 70%);
}

/* Viewport flash on QR hit/miss. Animate ::after, never body
   (that would fade the whole page). */
body::after {
    content: '';
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 50;             /* above the docked button */
    opacity: 0;
}

body.screen-locked::after {
    animation: screen-flash-green 0.45s ease-out 1;  /* 450ms = LOCK_FLASH_MS */
    box-shadow: inset 0 0 130px rgba(46, 204, 113, 0.4);
    border: 3px solid rgba(46, 204, 113, 0.85);
}

body.screen-rejected::after {
    animation: screen-flash-red 0.35s ease-out 1;   /* 350ms = REJECT_FLASH_MS */
    box-shadow: inset 0 0 130px rgba(255, 59, 48, 0.4);
    border: 3px solid rgba(255, 59, 48, 0.85);
}

@keyframes screen-flash-green {
    0%   { opacity: 0; }
    40%  { opacity: 1; }
    100% { opacity: 0; }
}

@keyframes screen-flash-red {
    0%   { opacity: 0; }
    40%  { opacity: 1; }
    100% { opacity: 0; }
}

/* Under notes, above body wash. Always mounted so idle↔play does not rebuild.
   Tile is 256²; duration 48s = one tile per cycle. */
.bg-pattern {
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 0;
    opacity: 0.55;
    background-image: url('slashes.png');
    background-repeat: repeat;
    background-size: 256px 256px;
    animation: pattern-drift 48s linear infinite;
}

@keyframes pattern-drift {
    from { background-position: 0 0; }
    to   { background-position: 0 256px; }
}

/* Drift notes (spans from app.js). z-index 0 on purpose: negative z on a
   fixed child of a scrolling body can vanish behind the body background.
   .idle = opacity 0 + animation-play-state paused (CPU). */
.bg-notes {
    position: fixed;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    z-index: 0;
    transition: opacity 0.8s ease;
}

.bg-notes.idle {
    opacity: 0;
}

.bg-notes.idle .note {
    animation-play-state: paused;
}

.note {
    position: absolute;
    /* Off-screen park sized for the 52–132px notes. */
    bottom: -180px;
    color: #0A3EFD;
    user-select: none;
    animation-name: note-drift;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
}

/* Drift end: the FULL viewport height plus fixed top clearance, so a note
   always exits the screen completely before the loop teleports it back to
   the bottom park — regardless of viewport size. */
@keyframes note-drift {
    0% {
        transform: translateY(0) rotate(-8deg);
    }
    50% {
        transform: translateY(-55vh) translateX(2vw) rotate(6deg);
    }
    100% {
        transform: translateY(calc(-100vh - 260px)) translateX(-1vw) rotate(-8deg);
    }
}

/* ----- Header ----- */
.header {
    width: 100%;
    flex-shrink: 0;
    padding: 1.5rem 1rem 2.6rem;
    padding-top: calc(1.5rem + env(safe-area-inset-top, 0px));
    text-align: center;
    position: relative;
    z-index: 1;
}

.lang-switch {
    position: absolute;
    top: calc(0.85rem + env(safe-area-inset-top, 0px));
    right: 0.4rem;
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    z-index: 6;
    pointer-events: auto;
}

.lang-btn {
    appearance: none;
    border: 0;
    background: transparent;
    font-size: 1.85rem;
    line-height: 1;
    padding: 0.45rem 0.4rem;
    min-width: 48px;
    min-height: 48px;
    cursor: pointer;
    opacity: 0.45;
    filter: grayscale(0.4);
    -webkit-tap-highlight-color: transparent;
}

.lang-btn.is-active {
    opacity: 1;
    filter: none
        drop-shadow(1px 0 0 #000)
        drop-shadow(-1px 0 0 #000)
        drop-shadow(0 1px 0 #000)
        drop-shadow(0 -1px 0 #000);
}

.logo-link {
    display: inline-block;
    text-decoration: none;
}

.logo {
    max-width: 55vw;
    transition: filter 1s ease;
    filter:
        drop-shadow(0 0 8px rgba(10, 62, 253, 0.4))
        drop-shadow(0 0 22px rgba(10, 62, 253, 0.22))
        drop-shadow(0 0 40px rgba(124, 58, 237, 0.16));
}

body.music-playing .logo {
    filter:
        drop-shadow(0 0 8px rgba(255, 0, 204, 0.45))
        drop-shadow(0 0 24px rgba(255, 0, 204, 0.26))
        drop-shadow(0 0 44px rgba(255, 0, 204, 0.14));
}

/* ----- Main column ----- */
.main {
    width: 100%;
    max-width: 420px;
    flex: 1;
    padding: 0 1rem 1rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    z-index: 1;
}

/* Camera or player only. Status sits under the header, outside this box.
   margin:auto so the view centers between header and the fixed scan button. */
.stage {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: auto 0;    /* the robust centering: soaks up free space above & below */
}

/* Plate stays. JS fades #statusText only. Width matches the scan button.
   1px #000 halo = relief against notes. min-height holds two lines. */
.status {
    position: relative;
    z-index: 1;
    flex-shrink: 0;
    box-sizing: border-box;
    width: calc(100% - 2rem);
    max-width: 400px;
    min-height: 2.8em;
    height: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 2rem auto 0.5rem;
    padding: 0.7rem 1.15rem;
    text-align: center;
    font-size: 1.39rem;
    line-height: 1.4;
    color: #cfcfe0;
    overflow-wrap: anywhere;
    transform-origin: 50% 50%;
    background: rgba(0, 0, 0, 0.5);
    border-radius: 1.1rem;
    text-shadow:
        1px 0 0 #000,
        -1px 0 0 #000,
        0 1px 0 #000,
        0 -1px 0 #000;
}

.status-text {
    display: block;
    width: 100%;
    transform-origin: 50% 50%;
}

/* Phase 1 — outgoing message fades out, swift (text only; plate stays) */
.status-text.status-fade {
    animation: status-fade-out 0.15s ease forwards;
}

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

/* Phase 2 — incoming message pops in with a modest overshoot ("boop") */
.status-text.status-pop {
    animation: status-pop-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes status-pop-in {
    0% {
        opacity: 0;
        transform: scale(0.85);
    }
    55% {
        opacity: 1;
        transform: scale(1.10);
    }
    100% {
        opacity: 1;
        transform: scale(1);
    }
}

/* ----- Views ----- */
.view {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.25rem;
}

.view[hidden] { display: none; }

/* ----- Buttons ----- */
.btn {
    font-family: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
}

/* ----- Docked scan button — plastic 3D, pinned to the bottom ----- */
.btn-primary {
    position: fixed;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: calc(100% - 2rem);
    max-width: 400px;
    margin-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
    padding: 1.1rem 1.4rem;
    font-size: 1.72rem;
    letter-spacing: 0.02em;
    color: #ffffff;
    border: none;
    border-radius: 1.25rem;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    /* plastic: glossy top-to-bottom face */
    overflow: visible;
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.28) 0%,
            rgba(255, 255, 255, 0.05) 42%,
            rgba(0, 0, 0, 0.18) 100%),
        linear-gradient(135deg, #0A3EFD 0%, #3D2AE8 100%);
    box-shadow:
        0 8px 28px rgba(10, 62, 253, 0.45),
        0 2px 4px rgba(0, 0, 0, 0.45),
        0 1px 0 rgba(255, 255, 255, 0.35) inset;
    text-shadow:
        1px 0 0 #000,
        -1px 0 0 #000,
        0 1px 0 #000,
        0 -1px 0 #000;
    transition: transform 0.08s ease, box-shadow 0.08s ease;
}

/* Logo-blue sonar: ready-to-scan pulse every 3s. */
.btn-primary::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    box-shadow:
        0 0 0 0 rgba(10, 62, 253, 0.55),
        0 0 0 0 rgba(10, 62, 253, 0.0);
    animation: scan-sonar 3s ease-out infinite;
}

body.needs-tap .btn-primary::before {
    animation: none;
}

.scan-btn-face {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
}

.scan-ico {
    width: 1.28em;
    height: 1.28em;
    flex-shrink: 0;
    display: block;
}

.scan-ico-close { display: none; }

.btn-primary.is-scanning .scan-ico-qr { display: none; }
.btn-primary.is-scanning .scan-ico-close { display: block; }

@keyframes scan-sonar {
    0%, 12% {
        box-shadow:
            0 0 0 0 rgba(10, 62, 253, 0.0),
            inset 0 0 0 0 rgba(10, 62, 253, 0.0);
    }
    22% {
        box-shadow:
            0 0 18px 4px rgba(10, 62, 253, 0.5),
            inset 0 0 18px rgba(10, 62, 253, 0.22);
    }
    38% {
        box-shadow:
            0 0 0 16px rgba(10, 62, 253, 0.0),
            inset 0 0 10px rgba(10, 62, 253, 0.08);
    }
    55% {
        box-shadow:
            0 0 18px 4px rgba(10, 62, 253, 0.5),
            inset 0 0 18px rgba(10, 62, 253, 0.22);
    }
    78%, 100% {
        box-shadow:
            0 0 0 22px rgba(10, 62, 253, 0.0),
            inset 0 0 0 0 rgba(10, 62, 253, 0.0);
    }
}

.btn-primary:not(:disabled):active {
    transform: translateX(-50%) translateY(3px) scale(0.98);
    box-shadow:
        0 3px 12px rgba(124, 58, 237, 0.4),
        0 1px 2px rgba(0, 0, 0, 0.4),
        0 1px 0 rgba(255, 255, 255, 0.25) inset;
}

/* ----- Round control buttons — play/pause & restart (plastic 3D) -----
   Base size = the RESTART button (the small sibling, sits LEFT of play).
   The play/pause hero overrides its own size below.
   Press feedback works two ways:
     - :active as the baseline;
     - .pressed as the deterministic driver (manual pointerdown/up in
       app.js) — :active alone gets swallowed on touch when the same tap
       churns the DOM. Both set the same visuals. */
.btn-round {
    width: 76px;
    height: 76px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    border: 3px solid rgba(123, 138, 175, 0.5);
    color: #f2f2f7;
    /* plastic face */
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.07) 0%,
            rgba(255, 255, 255, 0.0) 40%,
            rgba(0, 0, 0, 0.22) 100%),
        #232331;
    box-shadow:
        0 8px 28px rgba(0, 0, 0, 0.65),
        0 0 36px rgba(0, 0, 0, 0.5);
    /* spring on release: same overshoot family as the status boop */
    transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
                box-shadow 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
                background 1s ease,
                border-color 1s ease;
}

.btn-round:not(:disabled):active,
.btn-round.pressed {
    transform: translateY(3px) scale(0.92);
    box-shadow:
        0 2px 8px rgba(0, 0, 0, 0.4);
    transition-duration: 0.08s;   /* press in fast, spring back slow */
}

/* Hero. Idle = dark + static black glow (binds to the bottom fade).
   .is-playing = magenta rim + glow-pulse. ::after bloom is opacity 0
   unless .is-playing — otherwise idle still leaks magenta. */
#playPauseBtn {
    position: relative;
    z-index: 1;
    width: 140px;
    height: 140px;
    color: #ffffff;
    border: 6px solid rgba(123, 138, 175, 0.5);
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.07) 0%,
            rgba(255, 255, 255, 0.0) 40%,
            rgba(0, 0, 0, 0.22) 100%),
        #232331;
    box-shadow:
        0 10px 36px rgba(0, 0, 0, 0.7),
        0 0 48px rgba(0, 0, 0, 0.55);
}

#playPauseBtn:not(.is-playing)::after {
    opacity: 0 !important;
    animation: none !important;
}

#playPauseBtn.needs-tap {
    box-shadow:
        0 0 28px 8px rgba(10, 62, 253, 0.7),
        0 0 64px 16px rgba(10, 62, 253, 0.4),
        0 8px 26px rgba(0, 0, 0, 0.45);
}

#playPauseBtn.needs-tap::before {
    content: '';
    position: absolute;
    inset: -8px;
    border-radius: inherit;
    pointer-events: none;
    animation: play-sonar 1.8s ease-out infinite;
}

@keyframes play-sonar {
    0%, 8% {
        box-shadow:
            0 0 0 0 rgba(10, 62, 253, 0.0),
            0 0 0 0 rgba(10, 62, 253, 0.0),
            inset 0 0 0 0 rgba(10, 62, 253, 0.0);
    }
    22% {
        box-shadow:
            0 0 0 14px rgba(10, 62, 253, 0.55),
            0 0 42px 12px rgba(10, 62, 253, 0.7),
            inset 0 0 28px rgba(10, 62, 253, 0.45);
    }
    40% {
        box-shadow:
            0 0 0 28px rgba(10, 62, 253, 0.0),
            0 0 18px 4px rgba(10, 62, 253, 0.2),
            inset 0 0 10px rgba(10, 62, 253, 0.1);
    }
    58% {
        box-shadow:
            0 0 0 14px rgba(10, 62, 253, 0.55),
            0 0 42px 12px rgba(10, 62, 253, 0.7),
            inset 0 0 28px rgba(10, 62, 253, 0.45);
    }
    80%, 100% {
        box-shadow:
            0 0 0 36px rgba(10, 62, 253, 0.0),
            0 0 0 0 rgba(10, 62, 253, 0.0),
            inset 0 0 0 0 rgba(10, 62, 253, 0.0);
    }
}

/* Bloom behind the face. z-index -1 inside the button's stacking context. */
#playPauseBtn::after {
    content: '';
    position: absolute;
    inset: -60px;                            /* MUCH bigger than the button */
    border-radius: 50%;
    pointer-events: none;
    z-index: -1;
    background: radial-gradient(circle,
        rgba(255, 0, 204, 0.55),
        rgba(255, 0, 204, 0.18) 60%,
        transparent 75%);
    opacity: 0;
    transition: opacity 0.6s ease;
}

/* Playing state: very dark magenta-nearing-black face. */
#playPauseBtn.is-playing {
    border-color: rgba(255, 0, 204, 0.85);
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.14) 0%,
            rgba(255, 0, 204, 0.04) 45%,
            rgba(0, 0, 0, 0.45) 100%),
        linear-gradient(160deg, #2a0718, #17040f);
    animation: glow-pulse 1s ease-in-out infinite;   /* double bass */
}

#playPauseBtn.is-playing::after {
    animation: glow-bloom 2s ease-in-out infinite;
}

@keyframes glow-bloom {
    0%, 100% { opacity: 0.4; }
    50%      { opacity: 0.8; }
}

/* Two equal pairs per second. Valley stays high so it breathes, not pumps. */
@keyframes glow-pulse {
    0% {
        box-shadow:
            0 8px 44px rgba(255, 0, 204, 0.45),
            0 0 70px rgba(255, 0, 204, 0.2),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.25) inset;
    }
    12% {
        box-shadow:
            0 8px 70px rgba(255, 0, 204, 0.8),
            0 0 110px rgba(255, 0, 204, 0.4),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.35) inset;
    }
    22% {
        box-shadow:
            0 8px 52px rgba(255, 0, 204, 0.55),
            0 0 82px rgba(255, 0, 204, 0.26),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.28) inset;
    }
    32% {
        box-shadow:
            0 8px 70px rgba(255, 0, 204, 0.8),
            0 0 110px rgba(255, 0, 204, 0.4),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.35) inset;
    }
    50% {
        box-shadow:
            0 8px 44px rgba(255, 0, 204, 0.45),
            0 0 70px rgba(255, 0, 204, 0.2),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.25) inset;
    }
    62% {
        box-shadow:
            0 8px 70px rgba(255, 0, 204, 0.8),
            0 0 110px rgba(255, 0, 204, 0.4),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.35) inset;
    }
    72% {
        box-shadow:
            0 8px 52px rgba(255, 0, 204, 0.55),
            0 0 82px rgba(255, 0, 204, 0.26),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.25) inset;
    }
    82% {
        box-shadow:
            0 8px 70px rgba(255, 0, 204, 0.8),
            0 0 110px rgba(255, 0, 204, 0.4),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.35) inset;
    }
    100% {
        box-shadow:
            0 8px 44px rgba(255, 0, 204, 0.45),
            0 0 70px rgba(255, 0, 204, 0.2),
            0 2px 6px rgba(0, 0, 0, 0.5),
            0 1px 0 rgba(255, 255, 255, 0.25) inset;
    }
}

.btn-primary:focus-visible,
.btn-round:focus-visible {
    outline: 2px solid #FF00CC;
    outline-offset: 3px;
}

/* Restart button joins the dark-while-playing club — same near-black
   magenta face as the hero, with a faint magenta rim so it doesn't go
   muddy against the darkened face. */
body.music-playing #restartBtn {
    border-color: rgba(255, 0, 204, 0.4);
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.14) 0%,
            rgba(255, 0, 204, 0.03) 45%,
            rgba(0, 0, 0, 0.4) 100%),
        linear-gradient(160deg, #24061a, #140310);
}

/* Controls: symmetric grid so the PLAY button is dead-centre on the page
   axis, with the smaller restart button parked to its left. The empty
   right-hand column (same 1fr width) is what keeps play perfectly centred.
   Position: relative anchors the play-aura note field below. */
.controls {
    position: relative;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    width: 100%;
    margin-top: 1rem;
}

#restartBtn {
    grid-column: 1;
    justify-self: end;
    margin-right: 1.75rem;
    position: relative;
    z-index: 1;
    width: 64px;
    height: 64px;
}

#restartBtn .icon {
    width: 47px;
    height: 47px;
}

.icon-restart.spin {
    animation: rewind-spin 0.5s cubic-bezier(0.22, 0.8, 0.3, 1);
}

@keyframes rewind-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

#playPauseBtn {
    grid-column: 2;
}

/* ----- Play-aura: notes that fade in/out around the play button -----
   Active STRICTLY while a song is playing (managed by app.js). Anchored to
   .controls: a zero-size origin at the play button's centre; JS places each
   note at a random angle/radius around it. Notes sit BELOW the play button,
   the restart button and the status line (z-index 0 < their 1) so they
   drift behind them. All notes share the magenta of the play button's glow
   — only their OPACITY varies.
   Full life cycle: fade in (~0.3s) → gentle bob (0.8s) → fade out (~0.3s)
   → respawn elsewhere. Short lives, same dance.
   The bob runs at 5.76s per cycle (×1.8 slow) so the per-life movement
   delta stays small and the swarm floats serenely instead of jittering. */
.aura-notes {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 0;
    height: 0;
    z-index: 0;
    pointer-events: none;
}

.aura-note {
    position: absolute;
    color: #FF00CC;
    user-select: none;
    pointer-events: none;
    opacity: 0;
    transform: translate(-50%, -50%);   /* centre the glyph on its spot */
    transition: opacity 0.3s ease;
}

.aura-note.visible {
    opacity: var(--aura-opacity, 0.55);
    animation: aura-bob 5.76s ease-in-out infinite;
}

@keyframes aura-bob {
    0% {
        translate: 0 0;
        rotate: -6deg;
    }
    50% {
        translate: 0 -10px;
        rotate: 6deg;
    }
    100% {
        translate: 0 0;
        rotate: -6deg;
    }
}

/* ----- Icons ----- */
.icon {
    width: 62%;
    height: 62%;
}

.icon-pause rect { fill: currentColor; }
.icon-play polygon { fill: currentColor; }

/* Restart icon: arc is stroked (NOT filled), arrowhead is filled (NOT stroked) */
.icon-restart path {
    fill: none;
    stroke: currentColor;
    stroke-width: 7;
    stroke-linecap: round;
}
.icon-restart polygon {
    fill: currentColor;
    stroke: none;
}

#playPauseBtn .icon {
    position: absolute;
    width: 62%;
    height: 62%;
    transition: transform 0.28s cubic-bezier(0.22, 0.8, 0.3, 1);
}

#playPauseBtn .icon-play  { transform: scale(1); }
#playPauseBtn .icon-pause { transform: scale(0); }
#playPauseBtn.is-playing .icon-play  { transform: scale(0); }
#playPauseBtn.is-playing .icon-pause { transform: scale(1); }

/* Camera. ::after on <video> does not paint (replaced element).
   Hide .cam-wrap, never the video alone — empty black box otherwise.
   Live = logo-blue 3px border, no pulse. Verdicts freeze the frame:
     .cam-locked 450ms green / .cam-rejected 350ms red + 2s JS cooldown.
   body.screen-* flashes the viewport in the same colour. */
.cam-wrap {
    width: 100%;
    max-width: 320px;
    aspect-ratio: 1 / 1;
    border-radius: 0.75rem;
    border: 3px solid rgba(123, 138, 175, 0.5);
    background: #000;
    margin-top: 1.25rem;
    position: relative;
    overflow: hidden;
    transition: border-color 0.15s ease;
}

.cam-wrap[hidden] { display: none; }

.camera {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Colour wash over the frozen frame — opacity flipped by the states below */
.cam-tint {
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.1s ease;
}

/* --- Live preview: logo-blue 3px border. That's it. --- */
.cam-wrap.is-scanning {
    border-color: rgba(10, 62, 253, 0.75);
}

/* --- Touch acknowledgement ring: born at the tap point, expands softly,
       fades away. Purely visual. --- */
.cam-tap {
    position: absolute;
    width: 64px;
    height: 64px;
    border: 2px solid rgba(255, 255, 255, 0.9);
    border-radius: 50%;
    pointer-events: none;
    transform: translate(-50%, -50%) scale(0.45);
    opacity: 0;
    animation: cam-tap-ring 0.45s ease-out forwards;
}

@keyframes cam-tap-ring {
    0% {
        transform: translate(-50%, -50%) scale(0.45);
        opacity: 0.9;
    }
    70% {
        transform: translate(-50%, -50%) scale(1);
        opacity: 0.5;
    }
    100% {
        transform: translate(-50%, -50%) scale(1.05);
        opacity: 0;
    }
}

/* --- Lock-on: green tint + one quick green pulse (450ms = LOCK_FLASH_MS) --- */
.cam-wrap.cam-locked {
    border-color: rgba(46, 204, 113, 0.9);
    animation: cam-glow-lock 0.45s ease-out 1;
}

.cam-wrap.cam-locked .cam-tint {
    background: rgba(46, 204, 113, 0.28);
    opacity: 1;
}

@keyframes cam-glow-lock {
    0% {
        box-shadow:
            0 6px 18px rgba(0, 0, 0, 0.5);
    }
    45% {
        box-shadow:
            0 0 46px rgba(46, 204, 113, 0.95),
            0 0 90px rgba(46, 204, 113, 0.45),
            0 6px 18px rgba(0, 0, 0, 0.5);
    }
    100% {
        box-shadow:
            0 0 30px rgba(46, 204, 113, 0.6),
            0 6px 18px rgba(0, 0, 0, 0.5);
    }
}

/* --- Rejection: red tint + one short red pulse (350ms = REJECT_FLASH_MS).
     The 2s cooldown afterwards is JS-managed: the visual silence simply
     means no further animation classes are applied during that window. --- */
.cam-wrap.cam-rejected {
    border-color: rgba(255, 59, 48, 0.9);
    animation: cam-glow-reject 0.35s ease-out 1;
}

.cam-wrap.cam-rejected .cam-tint {
    background: rgba(255, 59, 48, 0.3);
    opacity: 1;
}

@keyframes cam-glow-reject {
    0% {
        box-shadow:
            0 6px 18px rgba(0, 0, 0, 0.5);
    }
    50% {
        box-shadow:
            0 0 42px rgba(255, 59, 48, 0.9),
            0 6px 18px rgba(0, 0, 0, 0.5);
    }
    100% {
        box-shadow:
            0 0 20px rgba(255, 59, 48, 0.5),
            0 6px 18px rgba(0, 0, 0, 0.5);
    }
}

#canvas[hidden] { display: none; }

/* ----- Desktop polish (debugging comfort, nothing special) ----- */
@media (min-width: 600px) {
    .btn-primary { font-size: 1.92rem; }
    #playPauseBtn { width: 160px; height: 160px; }
    .btn-round { width: 84px; height: 84px; }
    .cam-wrap { max-width: 420px; }
}

.veil {
    position: fixed;
    inset: 0;
    z-index: 70;
    background: #05060c;
    pointer-events: none;
    opacity: 1;
    transition: opacity 0.55s ease;
}

.veil[hidden] { display: none; }
.veil.is-out { opacity: 0; }

/* Splash on / only. /<uid> skips it.
   Ghost logo 10% + left clip-path fill = the bar.
   Notes are just magenta glyphs (no mask / mix-blend — Firefox mobile
   rendered those as a black screen). Veil (z 70) hides the SPA until
   splash (z 80) finishes, then veil fades. */
.splash {
    position: fixed;
    inset: 0;
    z-index: 80;
    pointer-events: none;
}

.splash[hidden] { display: none; }

.splash-layer {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.4rem;
    transition: opacity 0.85s ease;
}

.splash.is-cutting .splash-layer {
    opacity: 0;
}

.splash-wash {
    position: absolute;
    inset: 0;
    background: #0A3EFD;
    opacity: 0.35;
}

.splash-logo-wrap {
    position: relative;
    z-index: 1;
    width: min(72vw, 340px);
}

.splash-logo {
    display: block;
    width: 100%;
    height: auto;
}

.splash-logo-ghost {
    opacity: 0.1;
}

.splash-logo-fill {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    clip-path: inset(0 100% 0 0);
}

.splash-pct {
    position: relative;
    z-index: 2;
    margin: 0;
    font-family: sans-serif;
    font-size: 1.72rem;
    letter-spacing: 0.02em;
    color: #ffffff;
    text-shadow:
        1px 0 0 #000,
        -1px 0 0 #000,
        0 1px 0 #000,
        0 -1px 0 #000;
}

.splash-cutouts {
    position: absolute;
    inset: 0;
    z-index: 6;
    overflow: hidden;
}

.splash-note {
    position: absolute;
    left: var(--x);
    top: var(--y);
    color: #FF00CC;
    font-size: clamp(28px, 8vw, 72px);
    line-height: 1;
    transform: translate(-50%, -50%) scale(0.2) rotate(var(--r));
    opacity: 0;
}

.splash.is-cutting .splash-note {
    animation: note-woosh 0.9s cubic-bezier(0.16, 0.7, 0.2, 1) forwards;
    animation-delay: var(--d, 0s);
}

@keyframes note-woosh {
    0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.15) rotate(var(--r)); }
    28%  { opacity: 1; }
    100% { opacity: 0; transform: translate(-50%, -50%) scale(3.2) rotate(var(--r)); }
}

/* logo does not jiggle; fill is clip-path only */