@charset "UTF-8";

/* ============================================================================
   ⁹²⁹ZX⁹²⁹  —  midnight glass
   ----------------------------------------------------------------------------
   ARCHITECTURE

   Cascade layers, in ascending order of authority. Because a later layer always
   beats an earlier one regardless of specificity, component rules stay flat and
   overrides need no `!important` and no specificity games.

     reset        normalising only
     tokens       the design system: every scale lives here and nowhere else
     base         element defaults (html, body, links, selection, focus)
     atmosphere   the fixed backdrop — aurora, grid, dust, vignette, grain
     layout       stage, card, glass panel
     components   everything inside or floating above the card
     theme        surface modes (data-mode on <html>) — derived from 4 primitives
     state        runtime classes toggled by profile.js
     a11y         user preferences: touch, motion, contrast, forced colours, print

   COLOUR MODEL
   Colours are stored as bare `r, g, b` triples so any rule can pick its own
   alpha via rgba(var(--x), α). Two primitives — --ink-rgb and --surface — plus
   the accent drive everything; a surface mode overrides those and the rest
   follows automatically.
   ========================================================================== */

@layer vendor, reset, tokens, base, atmosphere, layout, components, theme, state, a11y;

/* Font Awesome, pinned to the lowest layer. Loaded here rather than from a
   <link> because an unlayered sheet beats every layered rule no matter how
   specific — which is how the icon font was silently winning over this file.
   index.html preloads the same URL so the request still starts immediately. */
@import url("assets/fontawesome/css/all.min.css") layer(vendor);

/* ==========================================================================
   RESET
   ========================================================================== */

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

    :where(body, h1, h2, h3, p, figure, blockquote, dl, dd) { margin: 0; }

    :where(button, input) {
        padding: 0;
        font: inherit;
        color: inherit;
        border: none;
        background: none;
    }

    :where(img, svg, canvas) { display: block; max-width: 100%; }

    :where(a) { color: inherit; text-decoration: none; }
}

/* ==========================================================================
   TOKENS  —  the single source of truth
   ========================================================================== */

@layer tokens {
    :root {
        /* ── colour primitives ────────────────────────────────────────────
           A surface mode overrides these four (plus a couple of tuning
           knobs); every other colour in the sheet derives from them.      */
        --ink-rgb:       255, 255, 255;   /* text and highlights           */
        --ink-contrast:    6,   7,  12;   /* reads against solid ink       */
        --shadow-rgb:      0,   0,   0;   /* drop shadows                  */
        --surface:      #0a0b11;          /* solid ground of the glass     */
        --bg:           #05060a;          /* the page behind everything    */

        /* ── accent — rewritten by the theme picker ──────────────────────
           --tint glows; --a1..--a3 are the generated aurora palette.

           These are the avatar's own two colours: pfp.png is #000000 and
           #ffffff and nothing else, so the accent is white and the aurora
           greys out rather than tinting. Identical to what accentCSS()
           emits for #ffffff, which is what the picker starts on and what
           Reset returns to — the first paint has to agree with it or the
           page would flash orange before profile.js runs. Pick any other
           accent in the theme panel and it overrides all four.            */
        --tint: 255, 255, 255;
        --a1:   255, 255, 255;
        --a2:   199, 199, 199;
        --a3:   199, 199, 199;

        /* ── derived surfaces ────────────────────────────────────────────
           Panels come from --surface, so a mode never restates them.      */
        --panel-bg:    color-mix(in srgb, var(--surface), transparent 38%);
        --panel-solid: color-mix(in srgb, var(--surface), transparent 6%);

        /* ── derived ink ─────────────────────────────────────────────── */
        --ink:   rgb(var(--ink-rgb));
        --ink-2: rgba(var(--ink-rgb), 0.64);   /* secondary copy          */
        --ink-3: rgba(var(--ink-rgb), 0.40);   /* supporting copy         */
        --ink-4: rgba(var(--ink-rgb), 0.22);   /* whispers                */

        /* ── derived structure ───────────────────────────────────────── */
        --line:   rgba(var(--ink-rgb), 0.09);
        --line-2: rgba(var(--ink-rgb), 0.16);
        --wash:   rgba(var(--ink-rgb), 0.035);  /* faint fill on a surface */
        --wash-2: rgba(var(--ink-rgb), 0.06);

        /* ── atmosphere tuning ───────────────────────────────────────── */
        --vig-rgb:     0, 0, 0;
        --vig-alpha:   0.55;
        --grid-alpha:  0.045;
        --grain-alpha: 0.05;
        --grain-blend: overlay;
        --mote:       255, 255, 255;
        --mote-far:   190, 205, 255;
        /* Applied per blob, not to the container — see .aurora. A little
           tighter than the container blur it replaces, because blurring
           three soft radial gradients one at a time keeps more of them. */
        --aurora-blur: 80px;

        /* ── black hole ───────────────────────────────────────────────
           The disk and the photon ring read from the accent palette, so
           the theme picker recolours the whole thing for free. Only the
           event horizon is fixed: it is the one part that is not light. */
        --bh-size:       min(88vmin, 900px);
        /* Both fractions of --bh-size, and both matched to the rendered
           one: blackhole.js sizes its shadow at 0.41 of this box, so a
           fallback at the old 0.24 read as a different object the moment
           WebGL was missing. */
        --bh-core:       0.41;   /* event horizon, as a fraction of --bh-size */
        --bh-ring:       0.445;  /* photon ring sits just outside the horizon */
        --bh-tilt:       74deg;  /* the shader's camera pitch, near edge-on   */

        /* The disk is an annulus, not a plate. One shape, used as the mask
           for the gas and again for the shading that sits over it — they
           have to agree or the seam shows. */
        --bh-annulus: radial-gradient(circle closest-side,
            transparent 43%, #000 53%, #000 74%, transparent 99%);
        --bh-blur:       clamp(5px, 0.85vmin, 11px);
        --bh-disk-alpha: 0.9;
        --bh-halo-alpha: 0.14;
        /* how hard the shadow sits on the page — near-opaque over ink,
           much lighter over a pale surface, where a black disc behind a
           frosted panel reads as a smudge rather than a hole */
        --bh-shade: 0.55;
        --bh-void:  1;
        /* The rendered one is far brighter than the gradient stack it
           replaces, and the card sits right over its throat — this is what
           keeps it a backdrop rather than something the panel has to fight. */
        --bh-canvas-alpha: 0.72;
        --bh-spin:       46s;

        /* ── type scale ──────────────────────────────────────────────── */
        --font-sans:    'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        --font-display: 'Space Grotesk', var(--font-sans);
        --font-mono:    ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;

        --fs-3xs: 0.55rem;
        --fs-2xs: 0.6rem;
        --fs-xs:  0.66rem;
        --fs-sm:  0.73rem;
        --fs-md:  0.84rem;
        --fs-lg:  1rem;
        /* the two display sizes scale with the viewport instead of stepping
           down at breakpoints */
        --fs-name: clamp(1.58rem, 1.2rem + 1.6vw, 2.05rem);
        --fs-hero: clamp(3rem, 11vw, 5.6rem);

        /* ── tracking scale ──────────────────────────────────────────── */
        --track-hero:  -0.045em;
        --track-tight: -0.035em;
        --track-1:      0.07em;
        --track-2:      0.12em;
        --track-3:      0.18em;
        --track-4:      0.24em;
        --track-5:      0.28em;

        /* ── space scale (4px base) ──────────────────────────────────── */
        --sp-1:  2px;
        --sp-2:  4px;
        --sp-3:  6px;
        --sp-4:  8px;
        --sp-5: 10px;
        --sp-6: 12px;
        --sp-7: 16px;
        --sp-8: 20px;
        --sp-9: 26px;

        /* ── radius scale ────────────────────────────────────────────── */
        --r-1:  6px;
        --r-2:  8px;
        --r-3: 11px;
        --r-4: 14px;
        --r-5: 20px;
        --r-6: clamp(22px, 1.4vw + 18px, 28px);
        --r-pill: 100px;
        --r-full: 50%;

        /* ── motion ──────────────────────────────────────────────────── */
        --dur-1: 0.15s;   /* immediate feedback   */
        --dur-2: 0.25s;   /* small state change   */
        --dur-3: 0.35s;   /* the default          */
        --dur-4: 0.5s;    /* deliberate           */
        --dur-5: 0.8s;    /* entrances            */
        --dur-6: 1.1s;    /* cinematic            */

        --ease:   cubic-bezier(0.22, 1, 0.36, 1);      /* decelerate       */
        --spring: cubic-bezier(0.34, 1.56, 0.64, 1);   /* slight overshoot */
        --ease-io: ease-in-out;

        /* ── elevation ───────────────────────────────────────────────── */
        --edge-hi:  inset 0 1px 0 0 rgba(var(--ink-rgb), 0.12);
        --edge-lo:  inset 0 -1px 0 0 rgba(var(--ink-rgb), 0.04);
        --elev-1:   0 6px 18px -6px rgba(var(--shadow-rgb), 0.6);
        --elev-2:   0 18px 44px -14px rgba(var(--shadow-rgb), 0.85);
        --elev-3:   0 40px 80px -20px rgba(var(--shadow-rgb), 0.75);
        --glow:     0 0 70px -30px rgba(var(--tint), 0.5);

        /* ── stacking order, named so it can be reasoned about ───────── */
        --z-fx:      0;
        --z-stage:   2;
        --z-player: 500;
        --z-mirrors: 500;
        --z-toggle: 600;
        --z-scrim:  650;
        --z-panel:  700;
        --z-enter: 9000;
        --z-cursor: 100000;

        /* ── runtime values written by profile.js ────────────────────── */
        --fill: 70%;  /* volume slider fill */
    }
}

/* ==========================================================================
   BASE
   ========================================================================== */

@layer base {
    html {
        height: 100%;
        -webkit-text-size-adjust: 100%;
        text-size-adjust: 100%;
        -webkit-tap-highlight-color: transparent;
    }

    body {
        min-height: 100%;
        font-family: var(--font-sans);
        color: var(--ink);
        background: var(--bg);
        overflow-x: hidden;
        cursor: none;   /* replaced by .cursor-ring / .cursor-dot */
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-rendering: optimizeLegibility;
    }

    a, button, input { cursor: none; }

    ::selection {
        background: rgba(var(--tint), 0.35);
        color: rgb(var(--ink-contrast));
    }

    ::-webkit-scrollbar { width: 6px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb {
        background: rgba(var(--ink-rgb), 0.1);
        border-radius: var(--r-1);
    }

    :focus-visible {
        outline: 2px solid rgba(var(--tint), 0.8);
        outline-offset: 3px;
        border-radius: var(--r-1);
    }
    :focus:not(:focus-visible) { outline: none; }
}

/* ==========================================================================
   ATMOSPHERE  —  the fixed backdrop, all of it decorative and inert
   ========================================================================== */

@layer atmosphere {
    .fx {
        position: fixed;
        inset: 0;
        z-index: var(--z-fx);
        overflow: hidden;
        pointer-events: none;
    }

    /* — aurora: three slow lights, coloured by the accent palette — */
    /* The blur sits on each blob rather than on this box on purpose: a
       filter here would make the drifting children re-blur the whole
       viewport every frame, where three self-contained blurred layers are
       rasterised once and then only moved. */
    .aurora {
        position: absolute;
        inset: -20%;
        opacity: 0.85;

        & span {
            position: absolute;
            border-radius: var(--r-full);
            filter: blur(var(--aurora-blur));
            will-change: transform;
        }

        & .a1 {
            inset-block-start: -8%;
            inset-inline-start: -6%;
            width: max(46vw, 320px);
            aspect-ratio: 1;
            background: radial-gradient(circle, rgba(var(--a1), 0.42), transparent 68%);
            animation: drift-1 28s var(--ease-io) infinite;
        }

        & .a2 {
            inset-block-end: -12%;
            inset-inline-end: -8%;
            width: max(42vw, 300px);
            aspect-ratio: 1;
            background: radial-gradient(circle, rgba(var(--a2), 0.3), transparent 68%);
            animation: drift-2 34s var(--ease-io) infinite;
        }

        & .a3 {
            inset-block-start: 42%;
            inset-inline-start: 46%;
            width: max(38vw, 260px);
            aspect-ratio: 1;
            background: radial-gradient(circle, rgba(var(--a3), 0.2), transparent 68%);
            animation: drift-3 40s var(--ease-io) infinite;
        }
    }

    @keyframes drift-1 {
        0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
        33%      { transform: translate3d(14vw, 8vh, 0) scale(1.14); }
        66%      { transform: translate3d(-6vw, 14vh, 0) scale(0.94); }
    }
    @keyframes drift-2 {
        0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
        33%      { transform: translate3d(-12vw, -8vh, 0) scale(1.1); }
        66%      { transform: translate3d(8vw, -13vh, 0) scale(0.92); }
    }
    @keyframes drift-3 {
        0%, 100% { transform: translate3d(-50%, -50%, 0) scale(1); }
        50%      { transform: translate3d(-50%, -50%, 0) scale(1.3); }
    }

    /* — black hole ————————————————————————————————————————————————
         Stacked in one grid cell so every part shares a centre. Reading
         outward: a soft lensing halo, the accretion disk tilted almost
         edge-on, the event horizon, the photon ring, and finally the near
         half of the disk again — masked out of the same tilted plane, so
         it sweeps in front of the horizon the way the real thing does.
         Every colour but the horizon comes from the accent palette. */
    .blackhole {
        position: absolute;
        /* Sits off to the right, matching where blackhole.js puts the rendered
           one — 19% of the width in from centre, a touch above the middle. */
        inset-block-start: calc(50% - 5vmin);
        inset-inline-start: 69%;
        width: var(--bh-size);
        aspect-ratio: 1;
        translate: -50% -50%;
        display: grid;
        place-items: center;
        perspective: 1100px;
        opacity: var(--bh-disk-alpha);

        & > * { grid-area: 1 / 1; }
    }

    /* light bent around the hole — a wide, faint accent wash, dark in the
       middle so the shadow already has something to sit in */
    .bh-halo {
        width: 100%;
        aspect-ratio: 1;
        border-radius: var(--r-full);
        background:
            radial-gradient(circle closest-side,
                rgba(0, 0, 0, var(--bh-shade)) 0%,
                rgba(0, 0, 0, calc(var(--bh-shade) * 0.62)) 26%,
                transparent 46%),
            radial-gradient(circle closest-side,
                rgba(var(--a1), var(--bh-halo-alpha)) 0%,
                rgba(var(--a2), calc(var(--bh-halo-alpha) * 0.55)) 34%,
                transparent 72%);
        animation: bh-breathe 12s var(--ease-io) infinite;
    }

    /* the disk's plane: tilted towards edge-on, spun by the child */
    .bh-tilt {
        position: relative;   /* the shading below is placed against this box */
        width: 100%;
        aspect-ratio: 1;
        transform: rotateX(var(--bh-tilt));

        /* The one side of the disk turning towards us is blue-shifted and
           several times brighter than the side turning away — it is what
           stops the ring reading as a flat halo. It belongs to the geometry,
           not to the gas, so it sits here and stays put while the texture
           below it turns. No blend mode: a 3D-transformed parent flattens
           differently across browsers, and plain alpha does the same job. */
        &::after {
            content: '';
            position: absolute;
            inset: 0;
            border-radius: var(--r-full);
            background:
                /* hot side */
                linear-gradient(100deg,
                    rgba(255, 245, 235, 0.5) 0%,
                    rgba(var(--a3), 0.34) 17%,
                    rgba(var(--a1), 0.12) 34%,
                    transparent 52%),
                /* the side turning away, dimmed rather than recoloured */
                linear-gradient(100deg,
                    transparent 55%,
                    rgba(0, 0, 0, 0.3) 78%,
                    rgba(0, 0, 0, 0.5) 100%);
            -webkit-mask-image: var(--bh-annulus);
            mask-image: var(--bh-annulus);
            filter: blur(calc(var(--bh-blur) * 1.6));
        }
    }

    /* keep only the near semicircle for the pass in front of the horizon */
    .bh-near {
        -webkit-mask-image: linear-gradient(to bottom, transparent 49%, #000 56%);
        mask-image: linear-gradient(to bottom, transparent 49%, #000 56%);
    }

    /* Every radial stop here is `closest-side`, so a percentage is a
       fraction of the box's half-width and the disk keeps its proportions
       against --bh-core. The default (farthest-corner) would push all of
       them out by ~1.41 and swallow the horizon. */
    .bh-disk {
        width: 100%;
        height: 100%;
        border-radius: var(--r-full);
        background:
            /* hot inner lip, where the gas is about to go over the edge */
            radial-gradient(circle closest-side,
                transparent 44%,
                rgba(255, 246, 232, 0.42) 49%,
                rgba(var(--a3), 0.52) 54%,
                rgba(var(--a1), 0.4) 62%,
                rgba(var(--a1), 0.12) 74%,
                transparent 88%),
            /* the gas itself: uneven, so the ring is never a clean band */
            conic-gradient(from 200deg,
                rgba(var(--a1), 0.4)    0deg,
                rgba(var(--a3), 0.56)  46deg,
                rgba(var(--a1), 0.62) 104deg,
                rgba(var(--a1), 0.36) 168deg,
                rgba(var(--a3), 0.46) 226deg,
                rgba(var(--a2), 0.26) 290deg,
                rgba(var(--a1), 0.4)  360deg),
            /* the cool outer reaches, which the accent alone never gets to */
            radial-gradient(circle closest-side,
                transparent 58%,
                rgba(var(--a2), 0.2) 76%,
                transparent 96%);
        /* punch out the middle: the disk is an annulus, not a plate */
        -webkit-mask-image: var(--bh-annulus);
        mask-image: var(--bh-annulus);
        filter: blur(var(--bh-blur)) saturate(130%);
        animation: bh-spin var(--bh-spin) linear infinite;
        will-change: transform;
    }

    /* event horizon — the only part that never takes the accent */
    .bh-core {
        width: calc(var(--bh-size) * var(--bh-core));
        aspect-ratio: 1;
        border-radius: var(--r-full);
        background: #000;
        /* blot the disk out as it approaches, rather than cutting it off */
        box-shadow:
            0 0 clamp(10px, 1.8vmin, 26px) clamp(8px, 1.6vmin, 24px) #000,
            0 0 clamp(30px, 5vmin, 70px) clamp(2px, 0.6vmin, 8px) rgba(0, 0, 0, 0.85);
    }

    /* photon ring — the bright outline hugging the horizon */
    .bh-ring {
        position: relative;   /* the two lensed arcs hang off this box */
        width: calc(var(--bh-size) * var(--bh-ring));
        aspect-ratio: 1;
        border-radius: var(--r-full);
        /* Nothing inside the photon ring ever gets out, so nothing painted
           under it is allowed to show through — this is what keeps the
           blurred arcs from washing the shadow grey. */
        background: radial-gradient(circle closest-side,
            rgba(0, 0, 0, var(--bh-void)) 76%,
            rgba(0, 0, 0, calc(var(--bh-void) * 0.7)) 92%,
            transparent 100%);
        border: clamp(1px, 0.22vmin, 2.5px) solid rgba(255, 240, 225, 0.8);
        box-shadow:
            0 0 clamp(8px, 1.3vmin, 18px)  rgba(var(--a3), 0.6),
            0 0 clamp(28px, 5vmin, 74px)   rgba(var(--a1), 0.32),
            inset 0 0 clamp(10px, 1.7vmin, 24px) rgba(var(--a1), 0.45);
        animation: bh-breathe 9s var(--ease-io) infinite;

        /* The far side of the disk, lifted over the top of the shadow and
           folded under the bottom of it. Bright above and below, gone at
           the sides — where the flat disk is already doing the work. */
        &::before {
            content: '';
            position: absolute;
            inset: calc(var(--bh-size) * -0.075);
            border-radius: var(--r-full);
            border: clamp(2px, 0.5vmin, 6px) solid rgba(var(--a1), 0.7);
            -webkit-mask-image: linear-gradient(to bottom,
                #000 0%, transparent 34%, transparent 66%, #000 100%);
            mask-image: linear-gradient(to bottom,
                #000 0%, transparent 34%, transparent 66%, #000 100%);
            filter: blur(clamp(2px, 0.45vmin, 6px));
        }

        /* and the same fold once more, wider and fainter, for the falloff
           between that arc and the flat disk it comes out of */
        &::after {
            content: '';
            position: absolute;
            inset: calc(var(--bh-size) * -0.15);
            border-radius: var(--r-full);
            border: clamp(3px, 0.8vmin, 10px) solid rgba(var(--a2), 0.3);
            -webkit-mask-image: linear-gradient(to bottom,
                #000 0%, transparent 40%, transparent 60%, #000 100%);
            mask-image: linear-gradient(to bottom,
                #000 0%, transparent 40%, transparent 60%, #000 100%);
            filter: blur(clamp(4px, 1vmin, 14px));
        }
    }

    @keyframes bh-spin {
        to { transform: rotate(1turn); }
    }

    @keyframes bh-breathe {
        0%, 100% { opacity: 0.82; }
        50%      { opacity: 1; }
    }

    /* — the real one ————————————————————————————————————————————————
         blackhole.js draws the whole scene — stars, gas, ring, shadow —
         into this one full-bleed canvas. It is stretched from a smaller
         drawing buffer on purpose: the image is nothing but wide, soft
         gradients, so the upscale costs nothing visible and saves a lot
         of fill rate. `bh-live` lands on <html> only once a frame has
         actually been drawn, so the gradient version below covers the
         gap and stays put if WebGL never arrives. */
    .bh-gl {
        position: absolute;
        inset: 0;
        display: block;
        width: 100%;
        height: 100%;
        opacity: var(--bh-canvas-alpha);
    }

    .bh-live .blackhole > * { display: none; }

    /* — fine grid, masked to a soft ellipse so it never meets an edge — */
    .grid {
        position: absolute;
        inset: 0;
        opacity: 0.7;
        background-image:
            linear-gradient(to right,  rgba(var(--ink-rgb), var(--grid-alpha)) 1px, transparent 1px),
            linear-gradient(to bottom, rgba(var(--ink-rgb), var(--grid-alpha)) 1px, transparent 1px);
        background-size: 68px 68px;
        -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000, transparent 85%);
        mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000, transparent 85%);
    }

    /* — drifting dust, drawn by profile.js — */
    #fx-canvas {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
    }

    /* — vignette pulls the eye to the middle — */
    .vignette {
        position: absolute;
        inset: 0;
        background:
            radial-gradient(ellipse 90% 75% at 50% 45%, transparent 40%, rgba(var(--vig-rgb), var(--vig-alpha)) 100%),
            linear-gradient(to bottom,
                rgba(var(--vig-rgb), calc(var(--vig-alpha) * 0.72)),
                transparent 25%, transparent 70%,
                rgba(var(--vig-rgb), calc(var(--vig-alpha) * 0.9)));
    }

    /* — film grain: one tiny inline SVG, nudged around to avoid a static
         pattern reading as a texture map — */
    /* inset only has to cover the largest offset in `grain-shift` (4%).
       It used to be -50%, which blended four viewports' worth of pixels
       against the whole stack behind it to show one. */
    .grain {
        position: absolute;
        inset: -6%;
        opacity: var(--grain-alpha);
        mix-blend-mode: var(--grain-blend);
        will-change: transform;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
        animation: grain-shift 8s steps(6) infinite;
    }

    @keyframes grain-shift {
        0%   { transform: translate(0, 0); }
        16%  { transform: translate(-3%, -2%); }
        33%  { transform: translate(2%, -4%); }
        50%  { transform: translate(-2%, 3%); }
        66%  { transform: translate(3%, 1%); }
        83%  { transform: translate(-1%, -3%); }
        100% { transform: translate(0, 0); }
    }
}

/* ==========================================================================
   LAYOUT  —  stage, card, and the glass panel itself
   ========================================================================== */

@layer layout {
    .stage {
        position: relative;
        z-index: var(--z-stage);
        min-height: 100vh;
        min-height: 100svh;
        display: flex;
        align-items: center;
        justify-content: center;
        padding:
            max(40px, env(safe-area-inset-top))
            max(var(--sp-8), env(safe-area-inset-right))
            max(40px, env(safe-area-inset-bottom))
            max(var(--sp-8), env(safe-area-inset-left));
        perspective: 1400px;

        /* short or landscape screens: start at the top so nothing clips */
        @media (height <= 640px) {
            align-items: flex-start;
            padding-block: var(--sp-9);
        }
    }

    .card {
        position: relative;
        width: 100%;
        max-width: 412px;
        display: flex;
        flex-direction: column;
        align-items: center;
        @media (width >= 1200px) { max-width: 436px; }

        /* light pooled underneath, so the card reads as lifted */
        &::after {
            content: '';
            position: absolute;
            inset-block-end: -46px;
            inset-inline-start: 50%;
            width: 74%;
            height: 90px;
            transform: translateX(-50%);
            background: radial-gradient(ellipse at center, rgba(var(--tint), 0.3), transparent 70%);
            filter: blur(26px);
            opacity: 0.85;
            pointer-events: none;
            z-index: -1;
        }
    }

    @keyframes card-in {
        from { opacity: 0; transform: translateY(30px) scale(0.95); filter: blur(10px); }
        to   { opacity: 1; transform: translateY(0) scale(1);       filter: blur(0); }
    }

    /* — the glass — */
    .panel {
        position: relative;
        width: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: clamp(19px, 1.2vw + 15px, 22px);
        padding:
            clamp(26px, 3.2vw + 14px, 38px)
            clamp(16px, 4.2vw + 2px, 32px)
            clamp(22px, 2.4vw + 13px, 30px);
        border-radius: var(--r-6);
        background:
            linear-gradient(165deg,
                rgba(var(--ink-rgb), 0.075) 0%,
                rgba(var(--ink-rgb), 0.02) 42%,
                rgba(var(--ink-rgb), 0.045) 100%),
            var(--panel-bg);
        backdrop-filter: blur(18px) saturate(150%);
        -webkit-backdrop-filter: blur(18px) saturate(150%);
        box-shadow: var(--edge-hi), var(--edge-lo), var(--elev-3), var(--glow);
        overflow: hidden;
        transition: box-shadow var(--dur-4) var(--ease), transform var(--dur-4) var(--ease);

        /* keep content above the hairline pseudo-element */
        & > * { position: relative; z-index: 1; }

        /* gradient hairline, drawn as a masked ring so the fill stays clean */
        &::after {
            content: '';
            position: absolute;
            inset: 0;
            padding: 1px;
            border-radius: inherit;
            background: linear-gradient(150deg,
                rgba(var(--ink-rgb), 0.55) 0%,
                rgba(var(--ink-rgb), 0.06) 28%,
                rgba(var(--tint), 0.28) 55%,
                rgba(var(--ink-rgb), 0.1) 78%,
                rgba(var(--ink-rgb), 0.35) 100%);
            -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
            pointer-events: none;
        }

    }

    /* Without backdrop-filter the translucent fill would show raw page
       behind it, so fall back to the near-opaque surface. */
    @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
        .panel, .player, .theme-toggle, .tp { background-color: var(--panel-solid); }
    }
}

/* ==========================================================================
   COMPONENTS
   ========================================================================== */

@layer components {

    /* ── cursor ─────────────────────────────────────────────────────── */

    .cursor-ring,
    .cursor-dot {
        position: fixed;
        inset-block-start: 0;
        inset-inline-start: 0;
        z-index: var(--z-cursor);
        border-radius: var(--r-full);
        pointer-events: none;
        will-change: transform;
    }

    .cursor-ring {
        width: 30px;
        height: 30px;
        border: 1px solid rgba(var(--ink-rgb), 0.55);
        /* deliberately no backdrop-filter: this element moves with the
           pointer, and a backdrop read-back per mouse move is the single
           most expensive thing a 30px ring could possibly do */
        transition:
            width var(--dur-2) var(--spring),
            height var(--dur-2) var(--spring),
            border-color var(--dur-2) ease,
            background var(--dur-2) ease,
            opacity var(--dur-2) ease;
    }

    .cursor-dot {
        width: 5px;
        height: 5px;
        background: var(--ink);
        box-shadow: 0 0 10px rgba(var(--ink-rgb), 0.8);
    }

    /* ── enter overlay ──────────────────────────────────────────────── */

    .enter {
        position: fixed;
        inset: 0;
        z-index: var(--z-enter);
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        background: var(--bg);
        cursor: none;
        transition:
            opacity var(--dur-5) var(--ease),
            visibility var(--dur-5),
            transform var(--dur-6) var(--ease),
            filter var(--dur-5) var(--ease);

        /* breathing core light */
        &::before {
            content: '';
            position: absolute;
            width: min(60vmin, 520px);
            aspect-ratio: 1;
            border-radius: var(--r-full);
            background: radial-gradient(circle, rgba(var(--tint), 0.16), transparent 66%);
            animation: breathe 6s var(--ease-io) infinite;
            pointer-events: none;
        }
    }

    @keyframes breathe {
        0%, 100% { transform: scale(1);    opacity: 0.55; }
        50%      { transform: scale(1.22); opacity: 1; }
    }

    .rings {
        position: absolute;
        width: min(78vmin, 620px);
        aspect-ratio: 1;
        pointer-events: none;

        & span {
            position: absolute;
            inset: 0;
            border: 1px solid rgba(var(--ink-rgb), 0.1);
            border-radius: var(--r-full);
            animation: ripple-out 5s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;

            &:nth-child(2) { animation-delay: 1.66s; }
            &:nth-child(3) { animation-delay: 3.32s; }
        }
    }

    @keyframes ripple-out {
        0%   { transform: scale(0.28); opacity: 0; }
        12%  { opacity: 0.9; }
        100% { transform: scale(1);    opacity: 0; }
    }

    .enter-inner {
        position: relative;
        z-index: 2;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: var(--sp-9);
        padding-inline: 24px;
        text-align: center;

        @media (height <= 640px) { gap: var(--sp-7); }
    }

    /* entrances share one keyframe and stagger by delay */
    @keyframes rise-in {
        from { opacity: 0; transform: translateY(26px) scale(0.96); filter: blur(6px); }
        to   { opacity: 1; transform: translateY(0) scale(1);       filter: blur(0); }
    }

    .enter-mark {
        width: clamp(56px, 4vw + 42px, 74px);
        aspect-ratio: 1;
        margin-block-end: -6px;
        border: 1px solid rgba(var(--ink-rgb), 0.18);
        border-radius: var(--r-full);
        object-fit: cover;
        box-shadow:
            0 0 0 6px rgba(var(--ink-rgb), 0.03),
            0 18px 50px rgba(var(--shadow-rgb), 0.7);
        animation: rise-in var(--dur-6) var(--ease) both;
    }

    .enter-name {
        max-width: 100%;
        font-family: var(--font-display);
        font-size: var(--fs-hero);
        font-weight: 700;
        line-height: 0.95;
        letter-spacing: var(--track-hero);
        color: var(--ink);
        background: linear-gradient(180deg,
            rgb(var(--ink-rgb)) 0%,
            rgb(var(--ink-rgb)) 45%,
            rgba(var(--ink-rgb), 0.72) 100%);
        -webkit-background-clip: text;
        background-clip: text;
        -webkit-text-fill-color: transparent;
        word-break: break-word;
        animation: rise-in var(--dur-6) var(--ease) 0.08s both;

        @media (height <= 640px) { font-size: clamp(2.2rem, 9vh, 3.8rem); }
    }

    .enter-tagline {
        margin-block-start: -14px;
        font-size: var(--fs-sm);
        font-weight: 500;
        letter-spacing: clamp(0.18em, 0.6vw, 0.26em);
        text-transform: uppercase;
        color: var(--ink-3);
        animation: rise-in var(--dur-6) var(--ease) 0.18s both;
    }

    .enter-btn {
        position: relative;
        display: inline-flex;
        align-items: center;
        gap: var(--sp-7);
        padding: clamp(13px, 1vw + 9px, 16px) clamp(30px, 3vw + 18px, 38px);
        overflow: hidden;
        border: 1px solid rgba(var(--ink-rgb), 0.22);
        border-radius: var(--r-pill);
        background: rgba(var(--ink-rgb), 0.03);
        font-family: var(--font-display);
        font-size: var(--fs-sm);
        font-weight: 600;
        letter-spacing: var(--track-4);
        text-transform: uppercase;
        color: var(--ink-2);
        animation: rise-in var(--dur-6) var(--ease) 0.3s both;
        transition:
            transform var(--dur-3) var(--spring),
            border-color var(--dur-2) ease,
            color var(--dur-2) ease,
            background var(--dur-2) ease,
            box-shadow var(--dur-3) ease;

        /* light sweeping across the button */
        &::after {
            content: '';
            position: absolute;
            inset: 0;
            background: linear-gradient(100deg, transparent 20%, rgba(var(--ink-rgb), 0.22) 50%, transparent 80%);
            transform: translateX(-120%);
            animation: sweep 3.4s var(--ease-io) 1.4s infinite;
        }

        & i {
            font-size: var(--fs-xs);
            transition: transform var(--dur-3) var(--spring);
        }
    }

    @keyframes sweep {
        0%        { transform: translateX(-120%); }
        45%, 100% { transform: translateX(120%); }
    }

    /* hovering anywhere on the overlay lights the button — the whole
       screen is the target, the button just shows it */
    .enter:hover .enter-btn,
    .enter-btn:hover {
        transform: translateY(-3px);
        border-color: rgba(var(--tint), 0.55);
        background: rgba(var(--tint), 0.08);
        color: var(--ink);
        box-shadow:
            0 0 46px rgba(var(--tint), 0.22),
            0 16px 40px rgba(var(--shadow-rgb), 0.55);
    }
    .enter:hover .enter-btn i { transform: translateX(5px); }

    .enter-hint {
        position: absolute;
        inset-block-end: max(30px, env(safe-area-inset-bottom));
        inset-inline: 0;
        z-index: 2;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: var(--sp-4);
        font-size: var(--fs-2xs);
        letter-spacing: var(--track-3);
        text-transform: uppercase;
        color: var(--ink-4);
        animation: hint-pulse 3s var(--ease-io) infinite;
    }

    @keyframes hint-pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }

    /* ── avatar ─────────────────────────────────────────────────────── */

    .avatar {
        position: relative;
        width: clamp(92px, 6vw + 68px, 118px);
        aspect-ratio: 1;
        flex-shrink: 0;
    }

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

    .avatar-halo {
        position: absolute;
        inset: -16px;
        border-radius: var(--r-full);
        background: conic-gradient(from 0deg,
            rgb(var(--a1)), rgb(var(--a2)), rgb(var(--a3)), rgb(var(--a1)));
        filter: blur(18px);
        opacity: 0.35;
        will-change: transform;
        animation: spin 14s linear infinite;
        transition: opacity var(--dur-4) ease;
    }

    .avatar-ring {
        position: absolute;
        inset: -3px;
        border-radius: var(--r-full);
        background: conic-gradient(from 0deg,
            rgba(var(--ink-rgb), 0.95),
            rgba(var(--a1), 0.75) 25%,
            rgba(var(--ink-rgb), 0.15) 50%,
            rgba(var(--a2), 0.8) 75%,
            rgba(var(--ink-rgb), 0.95));
        animation: spin 9s linear infinite;
        will-change: transform;

        /* punch the middle out so only the rim shows */
        &::before {
            content: '';
            position: absolute;
            inset: 2px;
            border-radius: var(--r-full);
            background: var(--surface);
        }
    }

    .avatar-img {
        position: absolute;
        inset: 4px;
        width: calc(100% - 8px);
        height: calc(100% - 8px);
        z-index: 2;
        border-radius: var(--r-full);
        object-fit: cover;
        transition: transform var(--dur-4) var(--spring);
    }

    /* ── presence colours, shared by the badge and the pill ─────────── */

    .status-badge,
    .status-pill {
        --st: 116, 127, 141;   /* offline */
        --st-glow: 0;

        &.online  { --st: 35, 165, 90;  --st-glow: 0.7; }
        &.idle    { --st: 240, 178, 50; --st-glow: 0.6; }
        &.dnd     { --st: 242, 63, 67;  --st-glow: 0.6; }
    }

    .status-badge {
        position: absolute;
        inset-block-end: 3px;
        inset-inline-end: 3px;
        width: 22px;
        height: 22px;
        z-index: 3;
        border: 3px solid var(--surface);
        border-radius: var(--r-full);
        background: rgb(var(--st));
        box-shadow: 0 0 12px rgba(var(--st), var(--st-glow));
        transition: background var(--dur-3) ease, box-shadow var(--dur-3) ease;

        /* the ping only exists while online */
        &::after {
            content: '';
            position: absolute;
            inset: 0;
            border-radius: var(--r-full);
            opacity: 0;
        }

        &.online::after {
            opacity: 1;
            background: rgba(var(--st), 0.6);
            animation: ping 2.4s cubic-bezier(0, 0, 0.2, 1) infinite;
        }
    }

    @keyframes ping {
        0%        { transform: scale(1);   opacity: 0.7; }
        70%, 100% { transform: scale(2.4); opacity: 0; }
    }

    /* ── identity ───────────────────────────────────────────────────── */

    .identity {
        width: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: var(--sp-4);
        text-align: center;
    }

    .name {
        max-width: 100%;
        font-family: var(--font-display);
        font-size: var(--fs-name);
        font-weight: 700;
        line-height: 1.05;
        letter-spacing: var(--track-tight);
        color: var(--ink);
        background:
            linear-gradient(100deg,
                rgb(var(--ink-rgb)) 20%,
                rgba(var(--ink-rgb), 0.45) 40%,
                rgb(var(--ink-rgb)) 60%)
            0 0 / 220% auto;
        -webkit-background-clip: text;
        background-clip: text;
        -webkit-text-fill-color: transparent;
        word-break: break-word;
        animation: shimmer 6s linear infinite;
    }

    @keyframes shimmer {
        from { background-position: 200% center; }
        to   { background-position: -20% center; }
    }

    /* if the gradient can't be clipped to the glyphs, show plain ink */
    @supports not ((-webkit-background-clip: text) or (background-clip: text)) {
        .name, .enter-name {
            color: var(--ink);
            -webkit-text-fill-color: var(--ink);
        }
    }

    .bio {
        min-height: 1.3em;
        font-size: var(--fs-md);
        font-weight: 400;
        letter-spacing: 0.01em;
        color: var(--ink-3);

        /* typewriter caret */
        &::after {
            content: '';
            display: inline-block;
            width: 2px;
            height: 0.95em;
            margin-inline-start: var(--sp-1);
            vertical-align: -0.12em;
            background: rgba(var(--tint), 0.95);
            box-shadow: 0 0 8px rgba(var(--tint), 0.8);
            animation: caret 1s steps(1) infinite;
        }
    }

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

    .status-pill {
        display: inline-flex;
        align-items: center;
        gap: var(--sp-4);
        margin-block-start: var(--sp-1);
        padding: var(--sp-3) 15px;
        border: 1px solid var(--line);
        border-radius: var(--r-pill);
        background: var(--wash);
        font-size: var(--fs-xs);
        font-weight: 600;
        letter-spacing: var(--track-2);
        text-transform: uppercase;
        color: var(--ink-2);
        transition: border-color var(--dur-3) ease, background var(--dur-3) ease, color var(--dur-3) ease;

        & .dot {
            width: 7px;
            height: 7px;
            flex-shrink: 0;
            border-radius: var(--r-full);
            background: rgb(var(--st));
            box-shadow: 0 0 10px rgba(var(--st), calc(var(--st-glow) + 0.2));
            transition: background var(--dur-3) ease, box-shadow var(--dur-3) ease;
        }

        /* tint the whole pill once a live status arrives */
        &:not(.offline) {
            border-color: rgba(var(--st), 0.32);
            background: rgba(var(--st), 0.08);
            color: rgba(var(--ink-rgb), 0.8);   /* a live status earns full weight */
        }
    }

    /* ── now playing (only rendered live) ───────────────────────────── */

    .now-playing {
        --spotify: 30, 215, 96;

        display: none;
        width: 100%;
        align-items: center;
        gap: var(--sp-6);
        padding: var(--sp-5) var(--sp-7);
        border: 1px solid var(--line);
        border-radius: var(--r-4);
        background: var(--wash);
        animation: rise-in var(--dur-4) var(--ease) both;
    }

    .np-art {
        width: 40px;
        height: 40px;
        flex-shrink: 0;
        border-radius: var(--r-2);
        object-fit: cover;
        background: var(--wash-2);
        box-shadow: 0 4px 14px rgba(var(--shadow-rgb), 0.5);
    }

    .np-meta {
        flex: 1;
        min-width: 0;
        text-align: start;
    }

    .np-label {
        display: flex;
        align-items: center;
        gap: var(--sp-3);
        margin-block-end: var(--sp-1);
        font-size: var(--fs-3xs);
        font-weight: 700;
        letter-spacing: var(--track-3);
        text-transform: uppercase;
        color: rgb(var(--spotify));
    }

    .np-title,
    .np-artist {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
    .np-title  { font-size: var(--fs-md); font-weight: 600; color: var(--ink); }
    .np-artist { font-size: var(--fs-xs); color: var(--ink-3); }

    /* ── equalizers: one keyframe, two sizes ────────────────────────── */

    @keyframes eq { from { transform: scaleY(0.25); } to { transform: scaleY(1); } }

    .np-eq,
    .eq {
        display: flex;
        align-items: flex-end;
        gap: var(--sp-1);
        flex-shrink: 0;

        & i {
            width: 2px;
            border-radius: var(--r-pill);   /* i.e. fully round at this width */
            transform-origin: bottom;
        }
    }

    .np-eq {
        height: 12px;

        & i {
            background: rgb(var(--spotify, 30, 215, 96));
            animation: eq 0.9s var(--ease-io) infinite alternate;
        }
        & i:nth-child(1) { height: 40%;  animation-delay: 0s; }
        & i:nth-child(2) { height: 90%;  animation-delay: 0.2s; }
        & i:nth-child(3) { height: 60%;  animation-delay: 0.4s; }
        & i:nth-child(4) { height: 100%; animation-delay: 0.1s; }
    }

    .eq {
        height: 14px;

        & i {
            height: 30%;
            background: rgba(var(--ink-rgb), 0.35);
            transition: background var(--dur-2) ease;
        }
        & i:nth-child(1) { height: 45%;  animation-delay: 0s; }
        & i:nth-child(2) { height: 100%; animation-delay: 0.18s; }
        & i:nth-child(3) { height: 65%;  animation-delay: 0.36s; }
        & i:nth-child(4) { height: 85%;  animation-delay: 0.09s; }
    }

    /* ── chips ──────────────────────────────────────────────────────── */

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

    .chips {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: var(--sp-3);
    }

    .chip {
        display: inline-flex;
        align-items: center;
        gap: var(--sp-3);
        padding: var(--sp-3) var(--sp-7);
        border: 1px solid var(--line-2);
        border-radius: var(--r-pill);
        background: rgba(var(--ink-rgb), 0.05);
        font-size: var(--fs-xs);
        font-weight: 600;
        letter-spacing: var(--track-1);
        text-transform: uppercase;
        color: var(--ink-2);
        animation: chip-in var(--dur-4) var(--spring) backwards;
        transition:
            transform var(--dur-2) var(--spring),
            background var(--dur-2) ease,
            border-color var(--dur-2) ease,
            color var(--dur-2) ease,
            box-shadow var(--dur-2) ease;

        &:nth-child(1) { animation-delay: 0.3s; }
        &:nth-child(2) { animation-delay: 0.38s; }
        &:nth-child(3) { animation-delay: 0.46s; }
        &:nth-child(4) { animation-delay: 0.54s; }

        &:hover {
            border-color: rgba(var(--tint), 0.55);
            background: rgba(var(--tint), 0.08);
            color: var(--ink);
            box-shadow: 0 10px 26px -12px rgba(var(--tint), 0.5);
        }

        & i { font-size: var(--fs-2xs); color: rgba(var(--tint), 0.95); }
    }

    /* an unequipped server tag drops its chip — inline-flex would otherwise
       win against the attribute */
    .chip[hidden] { display: none; }

    /* ── divider ────────────────────────────────────────────────────── */

    .divider {
        width: 100%;
        display: flex;
        align-items: center;
        gap: var(--sp-6);

        &::before,
        &::after {
            content: '';
            flex: 1;
            height: 1px;
            background: linear-gradient(90deg, transparent, rgba(var(--ink-rgb), 0.22));
        }
        &::after { background: linear-gradient(90deg, rgba(var(--ink-rgb), 0.22), transparent); }

        & span {
            /* offsets the trailing letter-space so the word optically centres */
            padding-inline-start: 0.28em;
            font-size: var(--fs-3xs);
            font-weight: 600;
            letter-spacing: var(--track-5);
            text-transform: uppercase;
            color: var(--ink-4);
        }
    }

    /* ── connect tiles ──────────────────────────────────────────────── */

    .connect {
        width: 100%;
        display: flex;
        gap: clamp(8px, 0.8vw + 5px, 10px);
    }

    .tile {
        /* each brand supplies one rgb triple and two gradient stops;
           every rule below is shared */
        --brand: var(--ink-rgb);
        --brand-1: rgba(var(--ink-rgb), 0.3);
        --brand-2: rgba(var(--ink-rgb), 0.15);

        position: relative;
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: var(--sp-4);
        padding: clamp(14px, 1.4vw + 9px, 16px) var(--sp-4) clamp(11px, 1vw + 8px, 13px);
        border: 1px solid var(--line);
        border-radius: var(--r-4);
        background: rgba(var(--ink-rgb), 0.028);
        user-select: none;
        -webkit-user-select: none;
        animation: chip-in var(--dur-4) var(--spring) backwards;
        transition:
            transform var(--dur-3) var(--spring),
            border-color var(--dur-3) ease,
            box-shadow var(--dur-3) ease;

        &:nth-child(1) { animation-delay: 0.46s; }
        &:nth-child(2) { animation-delay: 0.54s; }
        &:nth-child(3) { animation-delay: 0.62s; }

        &.discord  { --brand: 88, 101, 242; --brand-1: #7b86ff; --brand-2: #4650d4; }
        &.telegram { --brand: 41, 182, 246; --brand-1: #48c6ff; --brand-2: #0277bd; }
        &.session  { --brand: 0, 200, 80;   --brand-1: #2ee07a; --brand-2: #00843d; }

        /* brand wash, rising from the bottom edge */
        &::before {
            content: '';
            position: absolute;
            inset: 0;
            border-radius: inherit;
            background: radial-gradient(120% 90% at 50% 115%, rgba(var(--brand), 0.22), transparent 70%);
            opacity: 0;
            transition: opacity var(--dur-3) ease;
        }

        /* hairline across the top */
        &::after {
            content: '';
            position: absolute;
            inset-block-start: 0;
            inset-inline: 18%;
            height: 1px;
            background: linear-gradient(90deg, transparent, rgba(var(--brand), 0.9), transparent);
            opacity: 0.35;
            transition: opacity var(--dur-3) ease;
        }

        /* never transform the tile itself on hover/press: moving the
           hit target under a stationary cursor makes :hover strobe at
           the edges — only the children move */
        &:hover,
        &:focus-visible {
            border-color: rgba(var(--brand), 0.5);
            box-shadow:
                0 16px 36px -14px rgba(var(--brand), 0.6),
                inset 0 1px 0 0 rgba(255, 255, 255, 0.06);

            &::before { opacity: 1; }
            &::after  { opacity: 0.9; }

            & .tile-icon {
                transform: translateY(-3px) scale(1.07);
                box-shadow: 0 12px 30px -6px rgba(var(--brand), 0.9);
            }
            & .tile-label { color: var(--ink); }
        }

        &:active .tile-icon { transform: translateY(-1px) scale(0.95); }
    }

    .tile-icon {
        position: relative;
        z-index: 1;
        display: flex;
        align-items: center;
        justify-content: center;
        width: clamp(34px, 1.6vw + 28px, 38px);
        aspect-ratio: 1;
        border-radius: var(--r-3);
        background: linear-gradient(140deg, var(--brand-1), var(--brand-2));
        box-shadow: 0 8px 24px -6px rgba(var(--brand), 0.75);
        font-size: clamp(0.92rem, 0.4vw + 0.8rem, 1.02rem);
        color: #fff;   /* always white: it sits on a saturated brand fill */
        transition: transform var(--dur-3) var(--spring), box-shadow var(--dur-3) ease;
    }

    /* Session brand mark — inline SVG, so it inherits the tile's white ink.
       The check sits in the same box and only shows once the ID is copied. */
    .ico-session {
        width: 54%;
        height: auto;
        transition: opacity var(--dur-2) ease, transform var(--dur-2) var(--spring);
    }

    .tile.session .ico-copied {
        position: absolute;
        inset: 0;
        display: grid;
        place-content: center;
        opacity: 0;
        transform: scale(0.6);
        transition: opacity var(--dur-2) ease, transform var(--dur-2) var(--spring);
    }

    /* ── visitor tally ──────────────────────────────────────────────
       Pinned to the panel's top-right corner. The `.panel > *` rule sets
       position: relative on every child, so this needs the extra class to
       win and go absolute. */
    .panel .visitors {
        position: absolute;
        z-index: 2;
        inset-block-start: clamp(10px, 1.2vw + 6px, 14px);
        inset-inline-end: clamp(10px, 1.2vw + 6px, 14px);
        display: flex;
        align-items: center;
        gap: var(--sp-3);
        padding: var(--sp-2) var(--sp-4);
        border: 1px solid rgba(var(--ink-rgb), 0.09);
        border-radius: 999px;
        background: rgba(var(--ink-rgb), 0.045);
        font-size: var(--fs-3xs);
        font-weight: 700;
        letter-spacing: var(--track-2);
        color: var(--ink-3);
        user-select: none;
        transition: color var(--dur-3) ease, border-color var(--dur-3) ease,
                    background var(--dur-3) ease;

        &:hover {
            color: var(--ink-2);
            border-color: rgba(var(--tint), 0.35);
            background: rgba(var(--tint), 0.08);
        }

        & i {
            font-size: var(--fs-3xs);
            color: rgba(var(--tint), 0.9);
        }
    }

    .visitors[hidden] { display: none; }

    /* tabular figures so the pill does not jitter as the number ticks up */
    .visitors-n { font-variant-numeric: tabular-nums; }

    .tile-label {
        position: relative;
        z-index: 1;
        font-size: var(--fs-2xs);
        font-weight: 700;
        letter-spacing: clamp(0.08em, 0.25vw, 0.12em);
        text-transform: uppercase;
        color: var(--ink-3);
        transition: color var(--dur-3) ease;
    }

    .tip {
        position: absolute;
        inset-block-end: calc(100% + 7px);
        inset-inline-start: 50%;
        z-index: 3;
        padding: var(--sp-2) var(--sp-4);
        border-radius: var(--r-1);
        background: rgba(var(--brand), 0.92);
        font-size: var(--fs-3xs);
        font-weight: 800;
        letter-spacing: var(--track-2);
        text-transform: uppercase;
        white-space: nowrap;
        color: #04210f;
        opacity: 0;
        transform: translateX(-50%) translateY(6px) scale(0.85);
        pointer-events: none;
        transition: opacity var(--dur-2) ease, transform var(--dur-3) var(--spring);
    }

    /* ── pgp key ────────────────────────────────────────────────────── */

    .pgp {
        width: 100%;
        display: flex;
        align-items: center;
        gap: var(--sp-5);
        padding: var(--sp-5) var(--sp-6);
        border: 1px solid var(--line);
        border-radius: var(--r-4);
        background: rgba(var(--ink-rgb), 0.028);
        animation: chip-in var(--dur-4) var(--spring) 0.7s backwards;
        transition:
            border-color var(--dur-3) ease,
            background var(--dur-3) ease,
            box-shadow var(--dur-3) ease;

        &:hover,
        &:focus-visible {
            border-color: rgba(var(--tint), 0.5);
            box-shadow: 0 16px 40px -16px rgba(var(--tint), 0.55);

            & .pgp-icon  { transform: scale(1.08) rotate(-8deg); }
            & .pgp-label { color: var(--ink-2); }
            & .pgp-go    { transform: translateX(3px); color: var(--ink-2); }
        }

        &:active .pgp-icon { transform: scale(0.95); }
    }

    .pgp-icon {
        flex: none;
        display: flex;
        align-items: center;
        justify-content: center;
        width: clamp(30px, 1.4vw + 25px, 34px);
        aspect-ratio: 1;
        border-radius: var(--r-3);
        background: linear-gradient(140deg, rgb(var(--a1)), rgb(var(--a2)));
        box-shadow: 0 8px 24px -8px rgba(var(--tint), 0.85);
        font-size: var(--fs-sm);
        color: #07080d;   /* the fill is the lifted accent, so it is always light */
        transition: transform var(--dur-3) var(--spring);
    }

    /* min-width:0 lets the fingerprint wrap instead of stretching the row */
    .pgp-id {
        flex: 1;
        min-width: 0;
        display: flex;
        flex-direction: column;
        gap: var(--sp-2);
    }

    .pgp-label {
        font-size: var(--fs-2xs);
        font-weight: 700;
        letter-spacing: var(--track-2);
        text-transform: uppercase;
        color: var(--ink-3);
        transition: color var(--dur-3) ease;

        & em {
            font-style: normal;
            font-weight: 600;
            color: var(--ink-4);
        }
    }

    .pgp-fp {
        font-family: var(--font-mono);
        font-size: var(--fs-3xs);
        letter-spacing: 0.04em;
        line-height: 1.45;
        word-break: break-all;
        color: rgba(var(--tint), 0.85);
    }

    .pgp-go {
        flex: none;
        font-size: var(--fs-2xs);
        color: var(--ink-4);
        transition: transform var(--dur-3) var(--spring), color var(--dur-3) ease;
    }

    /* ── footer ─────────────────────────────────────────────────────── */

    .foot {
        display: flex;
        align-items: center;
        gap: var(--sp-3);
        margin-block-start: var(--sp-7);
        font-size: var(--fs-2xs);
        letter-spacing: var(--track-3);
        text-transform: uppercase;
        color: var(--ink-4);
        transition: color var(--dur-3) ease;

        &:hover { color: var(--ink-3); }

        & i {
            font-size: var(--fs-2xs);
            color: rgba(var(--tint), 0.9);
            animation: beat 1.8s var(--ease-io) infinite;
        }
    }

    @keyframes beat {
        0%, 100% { transform: scale(1); }
        14%      { transform: scale(1.35); }
        28%      { transform: scale(1); }
        42%      { transform: scale(1.22); }
        56%      { transform: scale(1); }
    }
}

/* ==========================================================================
   COMPONENTS  —  floating chrome (player, theme picker)
   ========================================================================== */

@layer components {

    /* ── music player ───────────────────────────────────────────────── */

    .player {
        position: fixed;
        inset-block-start: max(var(--sp-6), env(safe-area-inset-top));
        inset-inline-end: max(var(--sp-6), env(safe-area-inset-right));
        z-index: var(--z-player);
        display: flex;
        align-items: center;
        gap: clamp(8px, 0.6vw + 6px, 10px);
        padding: var(--sp-4) clamp(12px, 1.2vw + 8px, 16px) var(--sp-4) var(--sp-4);
        border: 1px solid var(--line-2);
        border-radius: var(--r-pill);
        background: var(--panel-bg);
        backdrop-filter: blur(20px) saturate(150%);
        -webkit-backdrop-filter: blur(20px) saturate(150%);
        box-shadow: var(--edge-hi), var(--elev-2);
        transition:
            border-color var(--dur-3) ease,
            box-shadow var(--dur-3) ease,
            transform var(--dur-3) var(--ease);

        @media (width >= 621px) {
            inset-block-start: max(var(--sp-8), env(safe-area-inset-top));
            inset-inline-end: max(var(--sp-8), env(safe-area-inset-right));
        }

        /* over narrow screens it can cross the card, so drop the transparency */
        @media (width <= 620px) { background: var(--panel-solid); }

        &:hover {
            border-color: rgba(var(--ink-rgb), 0.3);
            box-shadow:
                var(--edge-hi),
                0 20px 50px -12px rgba(var(--shadow-rgb), 0.9),
                0 0 40px -12px rgba(var(--tint), 0.5);

            & .vol { opacity: 1; }
        }
    }

    .play-wrap {
        position: relative;
        width: clamp(36px, 1.6vw + 30px, 40px);
        aspect-ratio: 1;
        flex-shrink: 0;
    }

    .play-ring {
        position: absolute;
        inset: 0;
        transform: rotate(-90deg);   /* start the sweep at 12 o'clock */
        pointer-events: none;

        & circle { fill: none; stroke-width: 2; }
        & .ring-bg { stroke: rgba(var(--ink-rgb), 0.12); }
        & .bar {
            stroke: url(#playGrad);
            stroke-linecap: round;
            /* 2πr for r=18.5; profile.js drives the offset */
            stroke-dasharray: 116.24;
            stroke-dashoffset: 116.24;
            transition: stroke-dashoffset var(--dur-2) linear;
        }
    }

    /* the SVG gradient tracks the accent */
    #playGrad {
        & .g1 { stop-color: rgb(var(--a1)); }
        & .g2 { stop-color: rgb(var(--a2)); }
        & .g3 { stop-color: rgb(var(--ink-rgb)); }
    }

    .play-btn {
        position: absolute;
        inset: 4px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: var(--r-full);
        background: linear-gradient(140deg, rgb(var(--ink-rgb)), rgba(var(--ink-rgb), 0.76));
        color: rgb(var(--ink-contrast));
        font-size: var(--fs-sm);
        box-shadow: 0 6px 18px -4px rgba(var(--ink-rgb), 0.45);
        transition: transform var(--dur-2) var(--spring), box-shadow var(--dur-2) ease;

        &:hover  { transform: scale(1.09); box-shadow: 0 8px 24px -4px rgba(var(--ink-rgb), 0.6); }
        &:active { transform: scale(0.94); }

        /* optically centre the play triangle; the pause glyph is symmetric */
        & i { transform: translateX(0.5px); }
    }

    .vol {
        display: flex;
        align-items: center;
        gap: var(--sp-3);
        opacity: 0.45;
        transition: opacity var(--dur-2) ease;

        & i {
            width: 12px;
            flex-shrink: 0;
            font-size: var(--fs-2xs);
            text-align: center;
            color: var(--ink-2);
        }

        & input {
            appearance: none;
            -webkit-appearance: none;
            width: clamp(44px, 3vw + 32px, 52px);
            height: 3px;
            border-radius: var(--r-pill);
            outline: none;
            /* --fill is written by profile.js as the slider moves */
            background: linear-gradient(90deg,
                rgba(var(--ink-rgb), 0.85) var(--fill),
                rgba(var(--ink-rgb), 0.14) var(--fill));
        }

        /* the two thumb pseudo-elements cannot be combined in one rule:
           an unknown selector would invalidate the whole block */
        & input::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 9px;
            height: 9px;
            border-radius: var(--r-full);
            background: #fff;
            box-shadow: 0 0 8px rgba(var(--ink-rgb), 0.7);
            cursor: none;
            transition: transform var(--dur-1) ease;
        }
        & input::-moz-range-thumb {
            width: 9px;
            height: 9px;
            border: none;
            border-radius: var(--r-full);
            background: #fff;
            box-shadow: 0 0 8px rgba(var(--ink-rgb), 0.7);
            cursor: none;
        }
        & input:hover::-webkit-slider-thumb { transform: scale(1.25); }
    }

    /* ── mirrors: clear net ⇄ i2p ⇄ tor ─────────────────────────────── */

    /* A segmented control in the corner opposite the player: one segment per
       network, the one you are on filled and marked, the others a click away.
       It is the only chrome that says the same site exists somewhere else, so
       it reads as one object rather than three loose links. */
    .mirrors {
        position: fixed;
        inset-block-start: max(var(--sp-6), env(safe-area-inset-top));
        inset-inline-start: max(var(--sp-6), env(safe-area-inset-left));
        z-index: var(--z-mirrors);
        display: flex;
        align-items: center;
        gap: var(--sp-1);
        padding: var(--sp-1);
        border: 1px solid var(--line-2);
        border-radius: var(--r-pill);
        background: var(--panel-bg);
        backdrop-filter: blur(20px) saturate(150%);
        -webkit-backdrop-filter: blur(20px) saturate(150%);
        box-shadow: var(--edge-hi), var(--elev-2);

        @media (width >= 621px) {
            inset-block-start: max(var(--sp-8), env(safe-area-inset-top));
            inset-inline-start: max(var(--sp-8), env(safe-area-inset-left));
        }

        /* same reason as the player: on a narrow screen it crosses the card */
        @media (width <= 620px) { background: var(--panel-solid); }
    }

    .mirror {
        position: relative;
        display: flex;
        align-items: center;
        gap: var(--sp-3);
        padding: var(--sp-3) clamp(10px, 0.8vw + 6px, 14px);
        border: 1px solid transparent;
        border-radius: var(--r-pill);
        font-size: var(--fs-3xs);
        font-weight: 700;
        letter-spacing: var(--track-2);
        text-transform: uppercase;
        color: var(--ink-3);
        white-space: nowrap;
        transition:
            color var(--dur-2) ease,
            background var(--dur-2) ease,
            border-color var(--dur-2) ease;

        & i {
            font-size: var(--fs-2xs);
            color: rgba(var(--ink-rgb), 0.45);
            transition: color var(--dur-2) ease;
        }

        &:hover {
            color: var(--ink);
            background: rgba(var(--ink-rgb), 0.07);

            & i { color: rgba(var(--tint), 0.95); }
        }

        &:active { transform: scale(0.97); }
    }

    /* the one you are on: lit, ringed, and out of the tab order — there is
       nowhere for it to take you */
    .mirror[aria-current="page"] {
        color: var(--ink);
        background: rgba(var(--tint), 0.14);
        border-color: rgba(var(--tint), 0.34);
        box-shadow: 0 0 18px -6px rgba(var(--tint), 0.55);
        pointer-events: none;

        & i { color: rgb(var(--tint)); }

        /* the live dot, which is what "you are here" actually looks like */
        &::before {
            content: '';
            width: 5px;
            aspect-ratio: 1;
            border-radius: var(--r-full);
            background: rgb(var(--tint));
            box-shadow: 0 0 8px rgba(var(--tint), 0.9);
            animation: mirror-pulse 2.6s var(--ease-io) infinite;
        }
    }

    @keyframes mirror-pulse {
        0%, 100% { opacity: 1;    transform: scale(1); }
        50%      { opacity: 0.45; transform: scale(0.82); }
    }

    /* Every segment carries "you are here" in its markup, so it has to be
       gone from all of them but the marked one — otherwise a screen reader
       reads it out three times. On that one it stays in the accessibility
       tree but off the screen, where the dot is saying it instead. */
    .mirror-here { display: none; }

    .mirror[aria-current="page"] .mirror-here {
        display: inline;
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* a mirror that does not exist yet: still named, so it reads as a promise
       rather than a broken link, but with nothing to click */
    .mirror.soon {
        color: var(--ink-4);
        pointer-events: none;

        & i { color: rgba(var(--ink-rgb), 0.26); }
    }

    .mirror-soon {
        display: none;
        margin-inline-start: calc(var(--sp-1) * -1);
        padding: 2px var(--sp-4);
        border-radius: var(--r-pill);
        background: rgba(var(--ink-rgb), 0.07);
        font-size: var(--fs-3xs);
        font-weight: 600;
        letter-spacing: 0.02em;
        text-transform: none;
        color: var(--ink-4);
    }

    .mirror.soon .mirror-soon { display: inline-block; }

    /* Three segments do not fit beside the player on a phone, so the labels
       go and the icons carry it — except on the one you are on, which keeps
       its name because that is the whole point of the control. Only once one
       of them is marked, though: with nothing to say where you are, three
       unlabelled icons say nothing either. */
    @media (width <= 520px) {
        .mirrors:has([aria-current="page"]) .mirror:not([aria-current="page"]) .mirror-name { display: none; }
        .mirror.soon .mirror-soon { display: none; }
    }

    /* ── theme picker ───────────────────────────────────────────────── */

    .theme-toggle {
        position: fixed;
        inset-block-end: max(var(--sp-7), env(safe-area-inset-bottom));
        inset-inline-end: max(var(--sp-7), env(safe-area-inset-right));
        z-index: var(--z-toggle);
        display: flex;
        align-items: center;
        justify-content: center;
        width: 46px;
        aspect-ratio: 1;
        border: 1px solid var(--line-2);
        border-radius: var(--r-full);
        /* --panel-solid is 94% opaque, so a backdrop-filter here would pay
           for a read-back of the whole animated backdrop to show 6% of it */
        background: var(--panel-solid);
        font-size: var(--fs-md);
        color: var(--ink-2);
        box-shadow: var(--edge-hi), 0 16px 36px -14px rgba(var(--shadow-rgb), 0.8);
        transition:
            transform var(--dur-3) var(--spring),
            color var(--dur-2) ease,
            border-color var(--dur-2) ease,
            box-shadow var(--dur-3) ease;

        @media (width >= 521px) {
            inset-block-end: max(var(--sp-8), env(safe-area-inset-bottom));
            inset-inline-end: max(var(--sp-8), env(safe-area-inset-right));
        }

        &:hover {
            transform: translateY(-3px) rotate(-18deg);
            color: var(--ink);
            border-color: rgba(var(--tint), 0.55);
            box-shadow:
                var(--edge-hi),
                0 18px 40px -12px rgba(var(--shadow-rgb), 0.9),
                0 0 34px -8px rgba(var(--tint), 0.6);
        }

        &:active { transform: scale(0.93); }
    }

    .tp-scrim {
        position: fixed;
        inset: 0;
        z-index: var(--z-scrim);
        background: rgba(var(--shadow-rgb), 0.45);
        backdrop-filter: blur(3px);
        -webkit-backdrop-filter: blur(3px);
        opacity: 0;
        visibility: hidden;
        transition: opacity var(--dur-3) var(--ease), visibility var(--dur-3);

        /* only phones get a scrim; on desktop the panel sits beside the card */
        @media (width >= 521px) { display: none; }
    }

    .tp {
        position: fixed;
        inset-block-end: calc(max(var(--sp-8), env(safe-area-inset-bottom)) + 58px);
        inset-inline-end: max(var(--sp-8), env(safe-area-inset-right));
        z-index: var(--z-panel);
        width: 332px;
        max-height: min(640px, 100vh - 120px);
        overflow-y: auto;
        padding: var(--sp-7);
        border: 1px solid var(--line-2);
        border-radius: var(--r-5);
        background: var(--panel-solid);   /* opaque enough not to need a blur */
        box-shadow: var(--edge-hi), 0 30px 70px -20px rgba(var(--shadow-rgb), 0.85);
        opacity: 0;
        visibility: hidden;
        transform: translateY(14px) scale(0.96);
        transform-origin: bottom right;
        transition:
            opacity var(--dur-3) var(--ease),
            transform var(--dur-3) var(--spring),
            visibility var(--dur-3);

        /* phones: dock it as a bottom sheet */
        @media (width <= 520px) {
            inset-inline: var(--sp-5);
            inset-block-end: calc(max(14px, env(safe-area-inset-bottom)) + 56px);
            width: auto;
            max-height: 74vh;
            transform-origin: bottom center;
        }

        &::-webkit-scrollbar { width: 4px; }
        &::-webkit-scrollbar-thumb { background: rgba(var(--ink-rgb), 0.14); border-radius: var(--r-1); }
    }

    .tp-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-block-end: var(--sp-7);
    }

    .tp-title {
        display: flex;
        align-items: center;
        gap: var(--sp-4);
        font-family: var(--font-display);
        font-size: var(--fs-sm);
        font-weight: 600;
        letter-spacing: var(--track-3);
        text-transform: uppercase;
        color: var(--ink-2);

        & i { font-size: var(--fs-xs); color: rgba(var(--tint), 0.95); }
    }

    .tp-close {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 26px;
        aspect-ratio: 1;
        border: 1px solid var(--line);
        border-radius: var(--r-2);
        font-size: var(--fs-sm);
        color: var(--ink-3);
        transition: color var(--dur-2) ease, border-color var(--dur-2) ease, background var(--dur-2) ease;

        &:hover {
            color: var(--ink);
            border-color: var(--line-2);
            background: var(--wash-2);
        }
    }

    .tp-sec {
        margin-block-end: var(--sp-7);

        &:last-of-type { margin-block-end: var(--sp-6); }

        & h3 {
            margin-block-end: var(--sp-5);
            font-size: var(--fs-3xs);
            font-weight: 700;
            letter-spacing: var(--track-3);
            text-transform: uppercase;
            color: var(--ink-4);
        }
    }

    /* accent swatches */
    .swatches {
        display: flex;
        flex-wrap: wrap;
        gap: var(--sp-3);
    }

    .sw {
        position: relative;
        width: 30px;
        aspect-ratio: 1;
        border: 1px solid rgba(var(--ink-rgb), 0.18);
        border-radius: var(--r-full);
        background: var(--sw);
        box-shadow: 0 4px 12px -3px var(--sw);
        transition: transform var(--dur-2) var(--spring), box-shadow var(--dur-2) ease;

        &:hover { transform: translateY(-2px) scale(1.08); }

        &[aria-checked='true'] {
            box-shadow:
                0 0 0 2px var(--panel-solid),
                0 0 0 3.5px var(--sw),
                0 6px 16px -4px var(--sw);
        }

        /* tick, drawn from two borders */
        &[aria-checked='true']::after {
            content: '';
            position: absolute;
            inset-inline-start: 50%;
            inset-block-start: 47%;
            width: 9px;
            height: 5px;
            border-inline-start: 2px solid rgba(0, 0, 0, 0.65);
            border-block-end: 2px solid rgba(0, 0, 0, 0.65);
            transform: translate(-50%, -50%) rotate(-45deg);
        }
    }

    .sw-custom {
        position: relative;
        width: 30px;
        aspect-ratio: 1;
        overflow: hidden;
        border: 1px solid rgba(var(--ink-rgb), 0.18);
        border-radius: var(--r-full);
        background: conic-gradient(#ff5f6d, #ffc371, #6ee7b7, #5fd8ff, #8b93ff, #ff6fae, #ff5f6d);
        transition: transform var(--dur-2) var(--spring);

        &:hover { transform: translateY(-2px) scale(1.08); }

        /* the real input covers the swatch but stays invisible */
        & input {
            position: absolute;
            inset: -8px;
            width: calc(100% + 16px);
            height: calc(100% + 16px);
            padding: 0;
            opacity: 0;
        }

        & i {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: var(--fs-2xs);
            color: rgba(0, 0, 0, 0.6);
            pointer-events: none;
        }
    }

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

    .mode {
        display: flex;
        align-items: center;
        gap: var(--sp-4);
        padding: var(--sp-4) var(--sp-5);
        border: 1px solid var(--line);
        border-radius: var(--r-2);
        background: rgba(var(--ink-rgb), 0.03);
        font-size: var(--fs-xs);
        font-weight: 600;
        letter-spacing: 0.04em;
        text-align: start;
        color: var(--ink-3);
        transition:
            transform var(--dur-2) var(--spring),
            border-color var(--dur-2) ease,
            color var(--dur-2) ease,
            background var(--dur-2) ease;

        &:hover {
            transform: translateY(-2px);
            color: var(--ink-2);
            border-color: var(--line-2);
        }

        &[aria-checked='true'] {
            color: var(--ink);
            border-color: rgba(var(--tint), 0.6);
            background: rgba(var(--tint), 0.1);
        }
    }

    .mode-chip {
        width: 18px;
        aspect-ratio: 1;
        flex-shrink: 0;
        border: 1px solid rgba(var(--ink-rgb), 0.16);
        border-radius: var(--r-1);
        background: var(--chip-bg, #05060a);
    }

    /* effect switches */
    .tp-toggles {
        display: flex;
        flex-direction: column;
        gap: 2px;
    }

    .tg {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--sp-5);
        width: 100%;
        padding: var(--sp-4) var(--sp-2);
        border-radius: var(--r-2);
        font-size: var(--fs-sm);
        font-weight: 500;
        text-align: start;
        color: var(--ink-2);
        transition: color var(--dur-2) ease, background var(--dur-2) ease;

        &:hover { color: var(--ink); background: rgba(var(--ink-rgb), 0.04); }

        &[aria-checked='true'] .tg-track {
            background: rgba(var(--tint), 0.55);
            border-color: rgba(var(--tint), 0.7);

            &::after { transform: translateX(14px); background: #fff; }
        }
    }

    .tg-track {
        position: relative;
        width: 32px;
        height: 18px;
        flex-shrink: 0;
        border: 1px solid var(--line);
        border-radius: var(--r-pill);
        background: rgba(var(--ink-rgb), 0.12);
        transition: background var(--dur-2) ease, border-color var(--dur-2) ease;

        &::after {
            content: '';
            position: absolute;
            inset-block-start: 2px;
            inset-inline-start: 2px;
            width: 12px;
            height: 12px;
            border-radius: var(--r-full);
            background: rgba(var(--ink-rgb), 0.5);
            transition: transform var(--dur-2) var(--spring), background var(--dur-2) ease;
        }
    }

    .tp-foot {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--sp-5);
        padding-block-start: var(--sp-6);
        border-block-start: 1px solid var(--line);
    }

    .tp-note {
        font-size: var(--fs-3xs);
        letter-spacing: var(--track-1);
        text-transform: uppercase;
        color: var(--ink-4);
    }

    .tp-reset {
        display: inline-flex;
        align-items: center;
        gap: var(--sp-3);
        padding: var(--sp-3) var(--sp-6);
        border: 1px solid var(--line-2);
        border-radius: var(--r-pill);
        background: rgba(var(--ink-rgb), 0.04);
        font-size: var(--fs-2xs);
        font-weight: 700;
        letter-spacing: var(--track-2);
        text-transform: uppercase;
        color: var(--ink-2);
        transition:
            transform var(--dur-2) var(--spring),
            color var(--dur-2) ease,
            border-color var(--dur-2) ease,
            background var(--dur-2) ease;

        &:hover {
            transform: translateY(-1px);
            color: var(--ink);
            border-color: rgba(var(--tint), 0.5);
            background: rgba(var(--tint), 0.1);
        }

        & i { font-size: var(--fs-3xs); }
    }
}

/* ==========================================================================
   THEME  —  surface modes, set as data-mode on <html> by the picker
   Each mode restates primitives only; panels, ink and structure derive.
   ========================================================================== */

@layer theme {

    /* pure black, for OLED panels */
    :root[data-mode='obsidian'] {
        --bg:         #000000;
        --surface:    #06060a;
        --grid-alpha: 0.035;
        --vig-alpha:  0.45;
    }

    /* softer charcoal — less contrast, more of a "product" feel */
    :root[data-mode='slate'] {
        --bg:         #12141c;
        --surface:    #1b1e29;
        --grid-alpha: 0.05;
        --vig-alpha:  0.42;
        --mote-far:   170, 185, 235;
    }

    /* light mode: ink flips to near-black, but the glass keeps white
       highlights — inverting those would read as dirt, not shine */
    :root[data-mode='daylight'] {
        --ink-rgb:       18,  20,  32;
        --ink-contrast: 255, 255, 255;
        --shadow-rgb:    38,  42,  68;
        --bg:         #e9ebf4;
        --surface:    #ffffff;

        --vig-rgb:      96, 106, 148;
        --vig-alpha:   0.3;
        --grid-alpha:  0.055;
        --grain-alpha: 0.045;
        --grain-blend: multiply;
        --mote:         70,  80, 120;
        --mote-far:    105, 115, 175;

        /* dark ink on a light ground needs more weight than the inverse */
        --ink-2:  rgba(var(--ink-rgb), 0.76);
        --ink-3:  rgba(var(--ink-rgb), 0.58);
        --ink-4:  rgba(var(--ink-rgb), 0.42);
        --line:   rgba(var(--ink-rgb), 0.12);
        --line-2: rgba(var(--ink-rgb), 0.2);

        /* lighting has to be rebuilt rather than derived */
        --edge-hi: inset 0 1px 0 0 rgba(255, 255, 255, 0.9);
        --elev-2:  0 18px 40px -14px rgba(var(--shadow-rgb), 0.3);
        --elev-3:  0 30px 64px -22px rgba(var(--shadow-rgb), 0.28);
        --glow:    0 0 70px -30px rgba(var(--tint), 0.45);

        /* the horizon carries itself against a light ground — the halo
           only has to hint at the lensing */
        --bh-halo-alpha: 0.26;
        --bh-shade: 0.14;
        --bh-disk-alpha: 0.92;
        --bh-canvas-alpha: 1;      /* the shader thins itself out here — let it decide */

        & .aurora { opacity: 0.55; }

        /* Seen through the frosted panel a flat black disc reads as a grey
           smudge, so the horizon fades out at its edge here instead — and
           the fill behind the photon ring, which exists to keep the arcs
           off the shadow, has to fade with it. */
        & .bh-core {
            background: radial-gradient(circle closest-side,
                #000 52%, rgba(0, 0, 0, 0.45) 78%, transparent 100%);
            box-shadow: none;
        }

        & .bh-ring {
            background: radial-gradient(circle closest-side,
                rgba(0, 0, 0, 0.5) 44%,
                rgba(0, 0, 0, 0.2) 74%,
                transparent 96%);
        }

        /* frosted white surfaces read better than translucent ink here */
        & .tile { background: rgba(255, 255, 255, 0.5); }
        & .chip,
        & .status-pill,
        & .now-playing { background: rgba(255, 255, 255, 0.55); }
        & .pgp { background: rgba(255, 255, 255, 0.5); }

        & .panel::after {
            background: linear-gradient(150deg,
                rgba(255, 255, 255, 0.95) 0%,
                rgba(var(--ink-rgb), 0.1) 30%,
                rgba(var(--tint), 0.35) 55%,
                rgba(var(--ink-rgb), 0.08) 78%,
                rgba(255, 255, 255, 0.8) 100%);
        }
        & .enter-mark {
            box-shadow:
                0 0 0 6px rgba(var(--ink-rgb), 0.04),
                0 18px 44px rgba(var(--shadow-rgb), 0.25);
        }

        /* Spotify green fails contrast on white; darken it just here */
        & .np-label { color: #0f9d4c; }

        /* deepen both wordmark gradients so the shimmer never washes out */
        & .name {
            background-image: linear-gradient(100deg,
                rgb(var(--ink-rgb)) 20%,
                rgba(var(--ink-rgb), 0.72) 40%,
                rgb(var(--ink-rgb)) 60%);
        }
        & .enter-name {
            background-image: linear-gradient(180deg,
                rgb(var(--ink-rgb)) 0%,
                rgb(var(--ink-rgb)) 55%,
                rgba(var(--ink-rgb), 0.8) 100%);
        }
    }
}

/* ==========================================================================
   STATE  —  classes toggled at runtime by profile.js
   Later than `components`, so no override here needs !important.
   ========================================================================== */

@layer state {

    /* ── entrances, released once the visitor clicks through ────────── */

    @keyframes player-in {
        from { opacity: 0; transform: translateY(-18px) scale(0.9); }
        to   { opacity: 1; transform: translateY(0) scale(1); }
    }

    /* The enter overlay paints an opaque --bg over the entire backdrop, so
       until it goes every animation under .fx is work nobody can see. Paused
       keyframes hold their first frame and cost nothing to composite; the
       two canvases are held by their own modules, which also skips the
       WebGL pass entirely. */
    body:not(.entered) .fx *,
    body:not(.entered) .fx *::before,
    body:not(.entered) .fx *::after { animation-play-state: paused; }

    body.entered .card   { animation: card-in var(--dur-6) var(--ease) 0.1s both; }
    body.entered .player { animation: player-in var(--dur-5) var(--spring) 0.45s both; }
    body.entered .theme-toggle    { animation: player-in var(--dur-5) var(--spring) 0.6s both; }

    /* a finished animation keeps its final frame applied, so drop it
       once it has played (matches `body.entered .card`, plus one class,
       to win) */
    body.entered .card.settled { animation: none; opacity: 1; }

    /* ── enter overlay dismissal ────────────────────────────────────── */

    .enter.gone {
        opacity: 0;
        visibility: hidden;
        transform: scale(1.08);
        filter: blur(14px);
        pointer-events: none;
    }

    /* stop every loop inside it the moment it starts leaving */
    .enter.gone,
    .enter.gone *,
    .enter.gone *::before,
    .enter.gone *::after { animation: none; }

    /* ── cursor ─────────────────────────────────────────────────────── */

    body.cursor-hot .cursor-ring {
        width: 52px;
        height: 52px;
        border-color: rgba(var(--tint), 0.9);
        background: rgba(var(--tint), 0.1);
    }
    body.cursor-hot .cursor-dot  { opacity: 0; }
    body.cursor-down .cursor-ring { width: 22px; height: 22px; }

    /* ── playback ───────────────────────────────────────────────────── */

    body.playing {
        & .play-btn i    { transform: none; }   /* pause glyph is symmetric */
        & .avatar-halo   { animation-duration: 7s; opacity: 0.5; }
        & .eq i {
            background: rgba(var(--tint), 0.95);
            animation: eq 0.85s var(--ease-io) infinite alternate;
        }
    }

    /* when the Web Audio analyser is driving the bars, JS owns their
       transform — must come after the .playing rule above */
    body.reactive .eq i {
        animation: none;
        transition: transform 0.08s linear;
    }

    /* ── live activity ──────────────────────────────────────────────── */

    .now-playing.live { display: flex; }

    /* ── session id copied ──────────────────────────────────────────── */

    .tile.session.copied {
        border-color: rgba(var(--brand), 0.6);
        box-shadow: 0 14px 40px -14px rgba(var(--brand), 0.9);

        &::before { opacity: 1; }

        & .ico-session { opacity: 0; transform: scale(0.6); }
        & .ico-copied  { opacity: 1; transform: scale(1); }

        & .tip {
            opacity: 1;
            transform: translateX(-50%) translateY(0) scale(1);
        }
    }

    /* ── theme picker open ──────────────────────────────────────────── */

    body.tp-open {
        & .tp {
            opacity: 1;
            visibility: visible;
            transform: translateY(0) scale(1);
        }
        & .tp-scrim { opacity: 1; visibility: visible; }
        & .theme-toggle { transform: translateY(-3px) rotate(90deg); color: var(--ink); }
    }

    /* ── effect switches ────────────────────────────────────────────── */

    .fx-off-aurora .aurora,
    .fx-off-grid   .grid,
    .fx-off-blackhole .blackhole,
    .fx-off-blackhole .bh-gl,
    .fx-off-dust   #fx-canvas,
    .fx-off-grain  .grain,
    .fx-off-cursor .cursor-ring,
    .fx-off-cursor .cursor-dot { display: none; }

    .fx-off-cursor body,
    .fx-off-cursor a,
    .fx-off-cursor button,
    .fx-off-cursor input { cursor: auto; }
}

/* ==========================================================================
   A11Y  —  user preferences win over everything above
   ========================================================================== */

@layer a11y {

    /* ── touch devices: no pointer to track, and a battery to respect ── */

    @media (hover: none) and (pointer: coarse) {
        .cursor-ring,
        .cursor-dot,
        #fx-canvas,
        .grain { display: none; }

        body, a, button, input { cursor: auto; }

        .aurora span { filter: blur(52px); }
        .aurora { opacity: 0.7; }
        .panel  { backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }

        /* one blurred, spinning disk is enough here — drop the front pass */
        .bh-near { display: none; }
    }

    /* ── reduced motion ──────────────────────────────────────────────
       Decorative loops go entirely; colour feedback stays, because it
       carries meaning (hover, focus, copied). Nothing here needs
       !important — this layer already outranks the rest.              */

    @media (prefers-reduced-motion: reduce) {
        *, *::before, *::after {
            animation: none;
            transition-duration: var(--dur-1);
            transition-property: color, background-color, border-color, opacity, box-shadow, fill, stroke;
            scroll-behavior: auto;
        }

        #fx-canvas, .grain { display: none; }

        /* these only look right in motion — freeze them in a good pose */
        .name, .enter-name {
            background: none;
            color: var(--ink);
            -webkit-text-fill-color: var(--ink);
        }
        .status-badge::after { display: none; }
        .enter-btn::after    { display: none; }
    }

    /* ── more contrast ───────────────────────────────────────────────── */

    @media (prefers-contrast: more) {
        :root {
            --ink-2:  rgba(var(--ink-rgb), 0.88);
            --ink-3:  rgba(var(--ink-rgb), 0.72);
            --ink-4:  rgba(var(--ink-rgb), 0.55);
            --line:   rgba(var(--ink-rgb), 0.3);
            --line-2: rgba(var(--ink-rgb), 0.45);
        }

        .aurora { opacity: 0.4; }
        .grain  { display: none; }
        .blackhole, .bh-gl { opacity: 0.45; }
        .panel, .player, .tp, .theme-toggle { background: var(--panel-solid); }
    }

    /* ── forced colours (Windows high contrast) ──────────────────────
       Drop the decoration and let the OS palette through.             */

    @media (forced-colors: active) {
        .fx, .cursor-ring, .cursor-dot { display: none; }
        body { cursor: auto; }

        .panel, .player, .tp, .theme-toggle, .tile, .chip, .status-pill, .now-playing {
            background: Canvas;
            border: 1px solid CanvasText;
            box-shadow: none;
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
        }

        .card::after,
        .panel::before,
        .panel::after,
        .tile::before,
        .tile::after { display: none; }

        .name, .enter-name {
            background: none;
            color: CanvasText;
            -webkit-text-fill-color: CanvasText;
        }
    }

    /* ── print: a legible page rather than a sheet of black ──────────── */

    @media print {
        .fx, .enter, .player, .theme-toggle, .tp, .tp-scrim,
        .cursor-ring, .cursor-dot { display: none; }

        body { background: #fff; color: #000; }
        .stage { min-height: auto; padding: 0; }
        .panel {
            border: 1px solid #ccc;
            background: #fff;
            box-shadow: none;
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
        }
        .name {
            background: none;
            color: #000;
            -webkit-text-fill-color: #000;
        }

        .pgp { border-color: #ccc; background: #fff; }
    }
}

/* ==========================================================================
   HELD  —  the page before it knows its own colour
   Unlayered on purpose: every rule above sits inside an @layer, so these win
   outright without needing !important, whatever mode is set.
   ========================================================================== */

/* The accent is read off the avatar, so until the live profile lands there is
   no honest colour to paint. The card itself is complete and readable the
   whole time — this only holds back the parts that exist to carry the accent,
   and they fade in rather than snap once the colour is known.

   Only ever set while the accent is automatic: a visitor who picked their own
   colour is waiting on nothing, and the <head> script lifts this immediately
   for them. */
:root.profile-pending {
    --tint: var(--ink-rgb);
    --a1:   var(--ink-rgb);
    --a2:   var(--ink-rgb);
    --a3:   var(--ink-rgb);
    /* not `none`: --glow sits in a box-shadow list, and one bad entry drops
       the whole declaration */
    --glow: 0 0 0 0 transparent;
}

.aurora,
.avatar-halo { transition: opacity var(--dur-6) ease; }

:root.profile-pending .aurora,
:root.profile-pending .avatar-halo { opacity: 0; }

/* the reveal is a fade, not a jump */
@media (prefers-reduced-motion: reduce) {
    .aurora, .avatar-halo { transition: none; }
}
