        @font-face {
            font-family: 'Neuland';
            src: url('assets/fonts/neuland.otf') format('opentype');
            font-display: block;
        }
        @font-face {
            font-family: 'Relation';
            src: url('assets/fonts/relation.otf') format('opentype');
            font-display: block;
        }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        html, body { width: 100%; height: 100%; }
        body {
            background: #000;
            overflow: hidden;
            font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
            /* touch-action: none disables native double-tap zoom and pinch
               zoom — the JS handlers still receive every touch, they're just
               not interpreted as gestures by the browser. Combined with the
               viewport meta and the gesturestart blocker further down, this
               kills accidental zoom on both Android and iOS Safari. */
            touch-action: none;
            -webkit-user-select: none;
            user-select: none;
        }
        canvas { display: block; touch-action: none; }

        /* Loading interstitial — covers the viewport with the brand backdrop
           until the bottle's first frame has painted (see revealScene() in
           scene.js). Visible by default so it shows the instant the page
           parses, before Three.js boots; faded out and removed once ready. */
        #loader {
            position: fixed;
            inset: 0;
            z-index: 9999;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 1.5em;
            background: #0a0a15;
            transition: opacity 0.6s ease;
        }
        #loader.is-hidden {
            opacity: 0;
            pointer-events: none;
        }
        .loader-mark {
            font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
            font-weight: 300;
            font-size: clamp(22px, 4vw, 38px);
            letter-spacing: 0.2em;
            text-transform: uppercase;
            color: #d8d8e0;
            text-shadow: 0 0 14px rgba(180, 200, 230, 0.18);
        }
        .loader-mark span { color: #ff8c1a; }
        .loader-bar {
            width: clamp(120px, 22vw, 200px);
            height: 2px;
            border-radius: 2px;
            background: rgba(255, 140, 26, 0.15);
            overflow: hidden;
        }
        .loader-bar i {
            display: block;
            width: 40%;
            height: 100%;
            border-radius: 2px;
            background: #ff8c1a;
            animation: loader-slide 1.1s ease-in-out infinite;
        }
        @keyframes loader-slide {
            0%   { transform: translateX(-110%); }
            100% { transform: translateX(360%); }
        }
        @media (prefers-reduced-motion: reduce) {
            .loader-bar i { animation: none; width: 100%; opacity: 0.55; }
        }

        /* Left-side floating menu (KSP-style). Items are absolutely positioned
           via a flex container so adding entries to the menu data array is
           sufficient — no other changes required. */
        #menu {
            position: fixed;
            left: 3.5vw;
            top: 50%;
            transform: translateY(-50%);
            display: flex;
            flex-direction: column;
            /* Items handle their own spacing via margin-bottom so we can
               collapse them individually (gap is per-pair and can't be
               overridden per-child). */
            gap: 0;
            z-index: 10;
            pointer-events: none;
        }
        .menu-item {
            pointer-events: auto;
            font-family: inherit;
            font-weight: 300;
            font-size: clamp(14px, 1.5vw, 20px);
            /* Widens slightly as the cursor nears (see --prox). */
            letter-spacing: calc(0.18em + 0.06em * var(--prox));
            text-transform: uppercase;
            /* --prox is the 0..1 cursor-proximity, written per-frame by ui.js. */
            --prox: 0;
            color: #d8d8e0;
            color: color-mix(in srgb, #d8d8e0, #ffb265 calc(var(--prox) * 100%));
            text-decoration: none;
            background: none;
            border: none;
            padding: 0.2em 0;
            margin-bottom: 1.4em;
            /* Explicit max-height so .leaving can transition height → 0 */
            max-height: 5em;
            text-align: left;
            align-self: flex-start;
            cursor: pointer;
            position: relative;
            white-space: nowrap;
            -webkit-tap-highlight-color: transparent;
            /* Ambient shadow always; the three hot-orange glow layers fade in
               with --prox (their alpha is scaled by it) for the magnetic glow. */
            text-shadow:
                0 0 8px rgba(180, 200, 230, 0.18),
                0 1px 2px rgba(0, 0, 0, 0.6),
                0 0 6px rgba(255, 140, 26, calc(var(--prox) * 0.95)),
                0 0 18px rgba(255, 120, 20, calc(var(--prox) * 0.7)),
                0 0 38px rgba(255, 100, 10, calc(var(--prox) * 0.45));
            /* Independent `scale` keeps `transform` free for the .leaving slide
               and `translate`/`rotate` for the float drift below. */
            scale: calc(1 + 0.05 * var(--prox));
            /* --prox-driven props (colour, letter-spacing, glow, scale) follow
               the cursor every frame — JS smooths them, so they're kept out of
               this transition to avoid trailing the pointer. */
            transition:
                opacity 0.35s ease,
                transform 0.35s ease,
                max-height 0.35s ease,
                margin 0.35s ease,
                padding 0.35s ease;
            animation: menu-float 7s ease-in-out infinite;
            will-change: translate, scale;
        }
        /* The literal last menu-item gets margin-bottom: 0 — :last-of-type
           can't be used here because the items are mixed <a>/<span>/<button>
           tags and the pseudo-class would match each tag's last element. The
           `menu-last` class is added in JS to the final entry instead. */
        .menu-item.menu-last {
            margin-bottom: 0;
        }
        /* Gentle 2D drift — vertical bob + a small horizontal sway and a
           sub-degree rotation give each item the look of floating in space.
           Staggered per item via animation-delay set inline. */
        @keyframes menu-float {
            0%, 100% { translate: 0 0;       rotate: 0deg; }
            25%      { translate: 2px -6px;  rotate: 0.3deg; }
            50%      { translate: 0 -9px;    rotate: 0deg; }
            75%      { translate: -2px -5px; rotate: -0.3deg; }
        }
        .menu-item::before {
            content: '› ';
            display: inline-block;
            /* Caret reveals with proximity rather than only on direct hover. */
            opacity: var(--prox);
            transform: translateX(calc(-0.3em * (1 - var(--prox))));
            color: #ff8c1a;
        }
        .menu-item:hover,
        .menu-item:focus-visible,
        .menu-item:active {
            color: #ffb265;
            letter-spacing: 0.24em;
            outline: none;
            text-shadow:
                0 0 6px rgba(255, 140, 26, 0.95),
                0 0 18px rgba(255, 120, 20, 0.7),
                0 0 38px rgba(255, 100, 10, 0.45);
        }
        .menu-item:hover::before,
        .menu-item:focus-visible::before,
        .menu-item:active::before {
            opacity: 1;
            transform: translateX(0);
        }
        /* Active (open) action item is a Back control: flip the caret to point
           left and keep it visible, so it reads "‹ Back" with no extra arrow. */
        .menu-item.is-remix-active::before {
            content: '‹ ';
            opacity: 1;
            transform: translateX(0);
        }
        /* Respect users who prefer reduced motion — no floating, no surprises. */
        @media (prefers-reduced-motion: reduce) {
            /* No drift and no proximity grow; colour/glow still respond so the
               menu stays legibly interactive. */
            .menu-item { animation: none; scale: 1; }
        }
        .menu-item.disabled {
            cursor: default;
        }
        /* When the remix panel opens, the other menu items slide left, fade
           out, and collapse their height to 0 so the Remix item smoothly
           floats up to the top of the column. */
        .menu-item.leaving {
            opacity: 0;
            transform: translateX(-24px);
            max-height: 0;
            margin-bottom: 0;
            padding-top: 0;
            padding-bottom: 0;
            overflow: hidden;
            pointer-events: none;
            animation: none;
        }
        .menu-item.is-remix-active {
            color: #ffb265;
            text-shadow:
                0 0 6px rgba(255, 140, 26, 0.85),
                0 0 18px rgba(255, 120, 20, 0.55);
            /* Restore the spacing below the now-top item so the panel sits
               below it with breathing room. Overrides :last-of-type's 0. */
            margin-bottom: 1.4em;
        }

        /* Generic panel mechanic — any menu item with an action: 'foo'
           drives a panel below it. Closed: max-height 0 so it contributes
           no layout height (menu stays vertically centred on its items
           alone). Open: max-height transitions to a generous cap so the
           natural content height reveals smoothly. */
        .menu-panel {
            display: flex;
            flex-direction: column;
            gap: 1.0em;
            align-self: flex-start;
            /* Floor at 300px so the remix Upload/Reset row clears the new
               glass padding; cap at 320px so the panel doesn't dominate on
               desktop; prefer 36vw between those so it scales naturally. */
            width: clamp(300px, 36vw, 320px);
            box-sizing: border-box;
            padding: 0 0.85em;
            max-height: 0;
            overflow: hidden;
            opacity: 0;
            pointer-events: none;
            /* Subtle glass-morphism backdrop so the panel reads as its own
               surface against the 3D bottle. Blur softens whatever's behind;
               background + border give a faint frame. Border-radius is
               restrained to avoid feeling like a card. The backdrop is
               always present — opacity 0 hides it cleanly when closed. */
            background: rgba(10, 10, 21, 0.42);
            -webkit-backdrop-filter: blur(12px);
            backdrop-filter: blur(12px);
            border: 1px solid rgba(216, 216, 224, 0.12);
            border-radius: 8px;
            transition:
                opacity 0.35s ease,
                max-height 0.4s ease,
                padding 0.35s ease;
        }
        .menu-panel.open {
            max-height: 80vh;
            opacity: 1;
            pointer-events: auto;
            /* Vertical padding kicks in only when open so the closed panel
               truly collapses to zero — avoids a thin glass strip floating
               where the panel will appear. */
            padding: 0.9em 0.85em;
        }

        /* Talk-to-us panel — static text with the DECT extension. */
        .talk-info {
            font-family: inherit;
            font-weight: 300;
            font-size: 13px;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            line-height: 1.7;
            color: #d8d8e0;
        }
        .talk-info p { margin: 0 0 0.6em 0; }
        .talk-info p:last-child { margin-bottom: 0; }
        .talk-info .dial {
            color: #ffb265;
            font-weight: 500;
            letter-spacing: 0.22em;
            text-shadow: 0 0 10px rgba(255, 140, 26, 0.55);
        }

        /* Find-us compass panel — large rotating arrow + distance readout.
           Heading + bearing are computed in JS and written to --find-angle;
           the wrapper applies the rotation so the SVG can be swapped without
           disturbing the transform. */
        .find-panel-body {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 1em;
            width: 100%;
        }
        /* The stage is the non-rotating container; the arrow-wrap rotates
           inside it. Keeping them separate lets us paint a north marker
           (desktop / static mode) without it spinning along with the arrow.
           Width tracks the panel (clamped) so narrow phones don't get a
           stage that overflows the menu column. */
        .find-arrow-stage {
            position: relative;
            width: min(180px, 70%);
            aspect-ratio: 1;
            margin-top: 0.6em;
        }
        .find-arrow-wrap {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            transform: rotate(var(--find-angle, 0deg));
            /* Linear, not ease, so a rapid stream of small updates doesn't
               bunch into a stuttering catch-up animation. */
            transition: transform 0.4s ease-out, opacity 0.25s ease;
            will-change: transform;
        }
        /* In mobile/live mode the arrow follows fast sensor updates, so the
           transition needs to be short and linear to avoid bunching. */
        .find-arrow-stage.is-live .find-arrow-wrap {
            transition: transform 0.18s linear, opacity 0.25s ease;
        }
        .find-arrow-wrap.is-stale { opacity: 0.35; }
        .find-arrow {
            width: 100%;
            height: 100%;
            fill: #ffb265;
            filter: drop-shadow(0 0 12px rgba(255, 140, 26, 0.55));
        }
        /* North marker for the static (desktop) mode — a flex sibling above
           the stage so it never spills outside the panel's overflow:hidden
           clip. Hidden in live (mobile) mode via the parent's :has()-driven
           visibility rule below. No letter-spacing — on a single character
           it just pads the right side and makes the letter look off-centre. */
        .find-north-mark {
            display: none;
            font-family: inherit;
            font-weight: 500;
            font-size: clamp(26px, 7vw, 34px);
            color: #f0f0f5;
            text-shadow:
                0 0 10px rgba(220, 230, 255, 0.55),
                0 0 22px rgba(180, 200, 230, 0.3);
            line-height: 1;
            pointer-events: none;
        }
        /* Static (desktop / no-sensor) mode: reveal the N above the dial. */
        .find-panel-body:has(.find-arrow-stage.is-static) .find-north-mark {
            display: block;
        }
        .find-readout {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 0.2em;
        }
        .find-distance {
            font-family: inherit;
            font-weight: 400;
            font-size: 28px;
            letter-spacing: 0.08em;
            color: #ffb265;
            text-shadow: 0 0 10px rgba(255, 140, 26, 0.55);
            /* Tabular figures keep digit columns stable as the number ticks. */
            font-feature-settings: "tnum";
            font-variant-numeric: tabular-nums;
        }
        .find-distance-label,
        .find-status {
            font-family: inherit;
            font-weight: 300;
            font-size: 11px;
            letter-spacing: 0.22em;
            text-transform: uppercase;
            color: #b8b8c0;
            text-align: center;
        }
        /* Reserve a row so status copy changes don't pop the layout. */
        .find-status { min-height: 1.2em; }
        .find-map-link,
        .find-enable {
            align-self: stretch;
            text-decoration: none;
        }
        .remix-field {
            display: flex;
            flex-direction: column;
            gap: 0.4em;
        }
        .remix-field label {
            font-family: inherit;
            font-weight: 300;
            font-size: 11px;
            letter-spacing: 0.22em;
            text-transform: uppercase;
            color: #b8b8c0;
        }
        .remix-field input[type="text"] {
            font-family: inherit;
            font-weight: 300;
            font-size: 15px;
            letter-spacing: 0.08em;
            color: #ffb265;
            background: rgba(10, 10, 21, 0.55);
            border: 1px solid rgba(216, 216, 224, 0.25);
            border-radius: 6px;
            padding: 0.55em 0.8em;
            outline: none;
            transition: border-color 0.25s ease, box-shadow 0.25s ease, color 0.25s ease;
            -webkit-appearance: none;
            appearance: none;
        }
        .remix-field input[type="text"]:focus,
        .remix-field input[type="text"]:hover {
            border-color: rgba(255, 140, 26, 0.7);
            box-shadow: 0 0 14px rgba(255, 140, 26, 0.35);
        }
        .remix-file {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            /* justify-content centers the line box, but wrapped text inside
               it still aligns left — center that too. */
            text-align: center;
            font-family: inherit;
            font-weight: 300;
            font-size: 12px;
            letter-spacing: 0.22em;
            text-transform: uppercase;
            color: #d8d8e0;
            background: rgba(10, 10, 21, 0.55);
            border: 1px solid rgba(216, 216, 224, 0.25);
            border-radius: 6px;
            padding: 0.7em 1em;
            cursor: pointer;
            transition: color 0.25s ease, border-color 0.25s ease, text-shadow 0.25s ease;
        }
        .remix-file:hover {
            color: #ffb265;
            border-color: rgba(255, 140, 26, 0.7);
            text-shadow: 0 0 10px rgba(255, 140, 26, 0.55);
        }
        .remix-file input { display: none; }
        .remix-image-row {
            display: flex;
            gap: 0.6em;
        }
        .remix-image-row .remix-file { flex: 1; }
        .remix-reset {
            font-family: inherit;
            font-weight: 300;
            cursor: pointer;
            flex: 0 0 auto;
            text-align: center;
        }

        /* Posterise + threshold controls — out in the open (posterise is the
           default treatment for uploads). */
        .remix-tone {
            border: 1px solid rgba(216, 216, 224, 0.18);
            border-radius: 6px;
            background: rgba(10, 10, 21, 0.35);
            padding: 0.6em 0.9em 0.8em;
            display: flex;
            flex-direction: column;
            gap: 0.45em;
        }
        .remix-slider-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-family: inherit;
            font-weight: 300;
            font-size: 11px;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: #b8b8c0;
        }
        .remix-slider-row span { color: #ffb265; letter-spacing: 0.1em; }
        .remix-check {
            display: flex;
            align-items: center;
            gap: 0.5em;
            font-family: inherit;
            font-weight: 300;
            font-size: 11px;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: #b8b8c0;
            cursor: pointer;
            user-select: none;
        }
        .remix-check input { accent-color: #ffb265; cursor: pointer; }
        .remix-tone input[type="range"] {
            -webkit-appearance: none;
            appearance: none;
            width: 100%;
            height: 4px;
            background: rgba(216, 216, 224, 0.25);
            border-radius: 2px;
            outline: none;
            transition: opacity 0.2s ease;
        }
        .remix-tone input[type="range"]:disabled { opacity: 0.4; cursor: not-allowed; }
        .remix-tone input[type="range"]:disabled + * { opacity: 0.4; }
        .remix-slider-row.disabled { opacity: 0.4; }
        .remix-tone input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 14px;
            height: 14px;
            border-radius: 50%;
            background: #ffb265;
            cursor: pointer;
            box-shadow: 0 0 8px rgba(255, 140, 26, 0.55);
        }
        .remix-tone input[type="range"]::-moz-range-thumb {
            width: 14px;
            height: 14px;
            border: none;
            border-radius: 50%;
            background: #ffb265;
            cursor: pointer;
            box-shadow: 0 0 8px rgba(255, 140, 26, 0.55);
        }

        /* Live preview of the current label composition — the THREE module
           appends the actual texture-backing canvas here so it stays in sync
           with the bottle for free. */
        .remix-preview {
            display: flex;
            justify-content: center;
            margin-top: 0.2em;
        }
        .remix-preview canvas {
            display: block;
            width: 100%;
            max-width: 260px;
            height: auto;
            border-radius: 8px;
            box-shadow: 0 0 18px rgba(0, 0, 0, 0.6);
        }

        /* iOS permission prompt — only visible until motion is enabled or
           confirmed unavailable. Styled to match the menu so it doesn't look
           bolted on. */
        #motion-prompt {
            position: fixed;
            top: 1.4em;
            left: 50%;
            transform: translateX(-50%);
            z-index: 11;
            display: none;
            font-family: 'JetBrains Mono', ui-monospace, monospace;
            font-weight: 300;
            font-size: 13px;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: #d8d8e0;
            background: rgba(10, 10, 21, 0.7);
            border: 1px solid rgba(216, 216, 224, 0.25);
            border-radius: 999px;
            padding: 0.7em 1.4em;
            cursor: pointer;
            -webkit-tap-highlight-color: transparent;
            backdrop-filter: blur(6px);
            -webkit-backdrop-filter: blur(6px);
            transition: color 0.25s ease, border-color 0.25s ease, text-shadow 0.25s ease;
        }
        #motion-prompt:active {
            color: #ffb265;
            border-color: rgba(255, 140, 26, 0.7);
            text-shadow: 0 0 12px rgba(255, 140, 26, 0.7);
        }

        /* Tiny audio toggle, bottom-left. Defaults to "Sound off" — clicking
           it both unmutes and provides the user-gesture browsers require
           before any audio can play, so the first menu hover after that
           plays instantly. */
        #mute-toggle {
            position: fixed;
            bottom: 1.4em;
            left: 1.4em;
            z-index: 11;
            font-family: 'JetBrains Mono', ui-monospace, monospace;
            font-weight: 300;
            font-size: 11px;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: #d8d8e0;
            background: rgba(10, 10, 21, 0.5);
            border: 1px solid rgba(216, 216, 224, 0.2);
            border-radius: 999px;
            padding: 0.55em 1em;
            cursor: pointer;
            -webkit-tap-highlight-color: transparent;
            backdrop-filter: blur(6px);
            -webkit-backdrop-filter: blur(6px);
            opacity: 0.7;
            transition: color 0.25s ease, border-color 0.25s ease,
                        text-shadow 0.25s ease, opacity 0.25s ease;
        }
        #mute-toggle:hover,
        #mute-toggle:focus-visible { opacity: 1; outline: none; }
        #mute-toggle.unmuted {
            color: #ffb265;
            border-color: rgba(255, 140, 26, 0.55);
            text-shadow: 0 0 10px rgba(255, 140, 26, 0.55);
        }
        /* Perf-mode badge — only shown when ?perf=low or ?perf=xlow. Sits
           right of the sound toggle. Display is left to inline `hidden` so
           default-mode users never see it. */
        #perf-badge {
            position: fixed;
            bottom: 1.4em;
            /* `left` is set in JS from the actual mute-toggle width so it
               can't overlap regardless of font/letter-spacing/zoom. */
            left: 0;
            z-index: 11;
            font-family: 'JetBrains Mono', ui-monospace, monospace;
            font-weight: 300;
            font-size: 11px;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: #ffb265;
            background: rgba(10, 10, 21, 0.5);
            border: 1px solid rgba(255, 140, 26, 0.35);
            border-radius: 999px;
            padding: 0.55em 1em;
            backdrop-filter: blur(6px);
            -webkit-backdrop-filter: blur(6px);
            opacity: 0.7;
            pointer-events: none;
            font-variant-numeric: tabular-nums;
        }
        /* Touch-only devices: when the user turns sound on, append a hint
           line inside the button reminding them that iOS/Android route
           Web Audio through the silent-switch path. Shows full-size for a
           few seconds, then smoothly fades and collapses. Desktop never
           sees it. */
        @media (hover: none) and (pointer: coarse) {
            #mute-toggle.unmuted::after {
                content: "ringer must be on";
                display: block;
                font-size: 11px;
                font-style: italic;
                letter-spacing: 0.08em;
                text-transform: none;
                text-shadow: none;
                overflow: hidden;
                animation: hint-cycle 4.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
            }
            @keyframes hint-cycle {
                0%   { opacity: 0; max-height: 0;    margin-top: 0;     transform: translateY(-2px); }
                10%  { opacity: 1; max-height: 2em;  margin-top: 0.4em; transform: translateY(0); }
                75%  { opacity: 1; max-height: 2em;  margin-top: 0.4em; transform: translateY(0); }
                100% { opacity: 0; max-height: 0;    margin-top: 0;     transform: translateY(0); }
            }
        }
