/* The Global Dance Party — the room.
 *
 * An additive layer. It adds depth to what is already there and changes no
 * structure: every rule here either reveals the canvas painted behind the
 * page, or tightens type the site already sets.
 *
 * Paired with gdp-room.js, which tags elements at runtime with data-gdp-*
 * attributes. Framer class names are content hashes and change on every
 * publish, so nothing here may depend on one.
 */

/* ------------------------------------------------------ motion system
 *
 * Three speeds and one easing curve, because twenty different ones is what
 * this layer had grown to and it reads as twenty different hands. Everything
 * added or refined from here uses these.
 *
 *   fast   input answering back - press, hover, play/pause
 *   med    something changing state - a panel, a doorway, a player mode
 *   slow   atmosphere - the countdown, ambient light, anything decorative
 *
 * One curve for all of it. Standard ease-out overshoot: quick to leave, soft
 * to arrive, which is what makes a control feel physical rather than timed.
 * `--gdp-enter` is the flatter curve for things arriving on screen, where an
 * overshoot reads as a bounce and cheapens it.
 */
:root {
    --gdp-fast: 140ms;
    --gdp-med: 320ms;
    --gdp-slow: 900ms;
    --gdp-ease: cubic-bezier(0.22, 1, 0.36, 1);
    --gdp-enter: cubic-bezier(0.4, 0, 0.2, 1);
}

/* One global honouring of the setting, so a rule added later cannot forget.
 * Not `none` - a zeroed duration still fires transitionend and still lets
 * state changes commit, where `none` can strand code that waits on one. */
@media (prefers-reduced-motion: reduce) {
    :root {
        --gdp-fast: 1ms;
        --gdp-med: 1ms;
        --gdp-slow: 1ms;
    }
}

/* The page wrapper paints #0A0A0C over the whole document, which would hide
 * the room entirely. The colour moves to <body> so the wrapper can go
 * transparent without changing what anyone sees. */
html body {
    background-color: #0a0a0c !important;
}

#gdp-room {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    /* Fade the top so the floor never competes with the nav, and ease the
     * bottom edge so it reads as distance rather than a cut. */
    -webkit-mask-image: linear-gradient(
        to bottom,
        transparent 0,
        rgba(0, 0, 0, 0.5) 6%,
        #000 18%,
        #000 88%,
        rgba(0, 0, 0, 0.55) 100%
    );
    mask-image: linear-gradient(
        to bottom,
        transparent 0,
        rgba(0, 0, 0, 0.5) 6%,
        #000 18%,
        #000 88%,
        rgba(0, 0, 0, 0.55) 100%
    );
}

/* Content sits above the room. Without this the canvas paints over the page. */
#main {
    position: relative;
    z-index: 1;
}

/* ------------------------------------------------------------ the crate
 *
 * Mix cards get a real camera. Perspective is set per card rather than on a
 * shared parent: the archive's flex wrapper is Framer's and may be rebuilt,
 * and a self-contained card survives that.
 */
/* !important throughout: the builder sets transform and box-shadow on these
 * cards through two-class selectors that outrank an attribute selector, so
 * without it the custom properties update every frame and nothing moves. */
[data-gdp-card] {
    transform: perspective(1000px) rotateX(var(--gdp-rx, 0deg))
        rotateY(var(--gdp-ry, 0deg)) translateZ(var(--gdp-tz, 0px)) !important;
    transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1),
        box-shadow 420ms cubic-bezier(0.22, 1, 0.36, 1) !important;
}

/* will-change is a promise the compositor keeps by giving the element its
 * own layer, and it was being made to every card at once. The archive
 * carries 74 of them: 74 permanent GPU layers, held for the whole session,
 * for cards that are mostly still. That is a documented way to make a page
 * scroll worse rather than better, and this page had a choppiness report
 * against it. The promise is now made only about the card actually being
 * moved - the one under the pointer - which is the case it was meant for. */
[data-gdp-card][data-gdp-lit="1"] {
    will-change: transform;
    box-shadow: 0 26px 60px -18px rgba(0, 0, 0, 0.85),
        0 0 0 1px rgba(244, 241, 233, 0.09),
        0 0 44px -14px rgba(56, 212, 255, 0.4) !important;
}

/* The specular sweep. One band of light tracking the pointer across the
 * card face, clipped by the card's own rounded corners. */
[data-gdp-card]::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    transition: opacity 320ms ease;
    background: radial-gradient(
        130% 90% at var(--gdp-sx, 50%) var(--gdp-sy, 50%),
        rgba(255, 255, 255, 0.17) 0%,
        rgba(56, 212, 255, 0.1) 32%,
        transparent 62%
    );
    z-index: 3;
}

[data-gdp-card][data-gdp-lit="1"]::after {
    opacity: 1;
}

/* ------------------------------------------------------- display type
 *
 * Allfeat's rule, from refero: negative tracking under sub-1.0 leading is
 * most of what separates premium editorial from a default type ramp. Applied
 * only to type the runtime measured at 54px or larger, so body copy and
 * labels are untouched.
 */
[data-gdp-display] {
    letter-spacing: -0.021em !important;
    line-height: 0.95 !important;
}

/* --------------------------------------------------------- eyebrow pills
 *
 * Cassette's rule, from refero: the small label above a section heading
 * becomes a bordered pill. Turns five pieces of floating text into one
 * label-print system that repeats down the page.
 */
[data-gdp-eyebrow] {
    display: inline-flex !important;
    /* The pill must hug its label. Framer's section columns stretch their
     * children, which turns an inline-flex pill into a full-width bar. */
    align-self: flex-start !important;
    width: fit-content !important;
    align-items: center;
    gap: 0.55em;
    padding: 0.44em 0.95em 0.46em !important;
    border: 1px solid rgba(244, 241, 233, 0.22);
    border-radius: 999px;
    background: rgba(244, 241, 233, 0.03);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    transition: border-color 600ms ease, background-color 600ms ease;
}

[data-gdp-eyebrow]::before {
    content: "";
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.8;
    flex: none;
}

/* The pills catch the room's pulse, so the label system belongs to the same
 * circuit as the lights rather than sitting outside it. */
[data-gdp-eyebrow][data-gdp-hot="1"] {
    border-color: rgba(56, 212, 255, 0.42);
    background: rgba(56, 212, 255, 0.07);
}

/* --------------------------------------------------------- reduced motion
 *
 * Not a downgrade: the room is still there, it simply stops moving. The
 * runtime paints one static frame and never schedules another.
 */
@media (prefers-reduced-motion: reduce) {
    [data-gdp-card] {
        transform: none !important;
        transition: none !important;
    }
    [data-gdp-card]::after {
        display: none;
    }
    [data-gdp-eyebrow] {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

/* ------------------------------------------------------- touch targets
 *
 * The transport controls are 34px and the nav icons 20 to 28px. Both are
 * comfortably under the 44px minimum every touch guideline agrees on, and the
 * transport row is exactly the row a person uses most: play, skip, change mix.
 *
 * The hit area is extended with a pseudo-element so nothing moves and nothing
 * is redrawn. Sizes are measured, not assumed: the four transport buttons sit
 * on a 42px pitch, so a 44px-wide target would overlap its neighbour and turn
 * "next mix" into "fast forward". 42 claims exactly half of each 8px gap and
 * stops there. The nav icons have 16px or more around them, so they take the
 * full 44.
 *
 * Pointer devices are left alone. A mouse does not need this and the larger
 * area would swallow hovers meant for what is next to it.
 */
@media (hover: none) and (pointer: coarse) {
    [aria-label^="Go to Mix"],
    [aria-label^="Rewind "],
    [aria-label^="Fast forward"],
    [aria-label^="Play Mix"],
    [aria-label^="Pause Mix"],
    [aria-label^="Resume Mix"],
    [aria-label^="Copy link"],
    [aria-label="Search Icon"] {
        position: relative;
    }

    [aria-label^="Go to Mix"]::after,
    [aria-label^="Rewind "]::after,
    [aria-label^="Fast forward"]::after,
    [aria-label^="Play Mix"]::after,
    [aria-label^="Pause Mix"]::after,
    [aria-label^="Resume Mix"]::after,
    [aria-label^="Copy link"]::after,
    [aria-label="Search Icon"]::after {
        content: "";
        position: absolute;
        left: 50%;
        top: 50%;
        width: 44px;
        height: 44px;
        transform: translate(-50%, -50%);
        /* invisible, and never on top of the control's own artwork */
        background: transparent;
        z-index: 0;
    }

    /* the tightly spaced transport row: half of each gap, no further */
    [aria-label^="Go to Mix"]::after,
    [aria-label^="Rewind "]::after,
    [aria-label^="Fast forward"]::after {
        width: 42px;
    }

    /* The nav icons sit in wrappers sized exactly to the icon with overflow
     * hidden, which clips the enlarged target back to 20px and undoes the
     * whole exercise. Only the wrapper directly holding one of these controls
     * is opened up, and it contains nothing else that could spill. */
    :has(> [aria-label="Search Icon"]),
    :has(> [aria-label^="Resume Mix"]),
    :has(> [aria-label^="Pause Mix"]) {
        overflow: visible !important;
    }
}

/* ---------------------------------------------------------- the pulse
 *
 * A dance site where only the background moves is a poster with a screensaver
 * behind it. On every kick, <html> carries data-gdp-pulse for about a sixth of
 * a second, and the parts of the page that can move without becoming annoying
 * take a step.
 *
 * Deliberately small. These are elements people read and click, so the motion
 * has to be felt more than seen: a percent or two of scale and a lift in
 * border light. Anything bigger and the page is fidgeting rather than dancing.
 * Nothing here moves layout, so nothing reflows.
 */
[data-gdp-eyebrow],
#gdp-filter .gdp-chip,
#gdp-party-cta {
    transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1),
        border-color 160ms ease, box-shadow 160ms ease;
}

/* !important because the 3D-tilt rule above sets a 420ms transition with
 * !important on every card - a beat that eases over 420ms never arrives.
 * A playing card answers the kick in 160ms; the tilt on it rides the same
 * clock, which reads as energy rather than as a glitch. */
[data-gdp-card][data-gdp-lit="1"] {
    transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1),
        border-color 160ms ease, box-shadow 160ms ease !important;
}

html[data-gdp-pulse] [data-gdp-eyebrow] {
    transform: scale(1.035);
    border-color: rgba(56, 212, 255, 0.55);
}

html[data-gdp-pulse] #gdp-filter .gdp-chip {
    transform: translateY(-1px);
    border-color: rgba(56, 212, 255, 0.45);
}

/* The beat pulse stands down while the door is hailing or holding its larger
 * playing size, so the three are not fighting over one transform. It beats on
 * the resting button, which is the only state that has room to move. */
html[data-gdp-pulse] #gdp-party-cta:not([data-gdp-hail="1"]):not([data-gdp-on="1"]) {
    transform: translateY(-2px) scale(1.03);
    box-shadow: 0 14px 38px -12px rgba(237, 47, 212, 0.75);
}

html[data-gdp-pulse] [data-gdp-card][data-gdp-lit="1"] {
    box-shadow: 0 26px 60px -18px rgba(0, 0, 0, 0.85),
        0 0 0 1px rgba(56, 212, 255, 0.3),
        0 0 56px -12px rgba(56, 212, 255, 0.6) !important;
}

/* Calm and reduced motion opt out of the whole thing. */
@media (prefers-reduced-motion: reduce) {
    html[data-gdp-pulse] [data-gdp-eyebrow],
    html[data-gdp-pulse] #gdp-filter .gdp-chip,
    html[data-gdp-pulse] #gdp-party-cta {
        transform: none;
    }
}


/* ---------------------------------------------------------------- mobile
 *
 * Nothing over anything. On a phone the hero's transport is positioned
 * inside the record's container and lands on the sleeve; the record is
 * shrunk inside that same container so the buttons sit in clear space
 * below it. Its centre does not move, so the spin still runs true inside
 * the rings and the composition is unchanged apart from a smaller record.
 *
 * important, because the size is an inline style the site writes itself. */
@media (max-width: 760px) {
    [data-gdp-heroart] {
        width: 200px !important;
        height: 200px !important;
        left: 46px !important;
        top: 46px !important;
    }

    /* The "Play me" badge sits between the transport and the mix selector and
     * had nowhere to go: three pixels of clearance above it and fifteen
     * pixels of the selector lying across its bottom edge. The badge and the
     * transport are both absolutely placed, so the one thing in this stack
     * that can move without disturbing anything is the selector, which is in
     * normal flow - and everything below it follows. */
    [data-gdp-selrow] {
        margin-top: 22px !important;
    }
}

/* The broadcast pill on a phone.
 *
 * The pill centres its children and the builder hands them an explicit width
 * wider than the pill's own content box, so each line overflowed the padding
 * by about ten pixels a side and sat against the glass. Constraining the
 * children to the box is the whole fix; the text re-centres inside the
 * padding and the pill reads as one tidy object again. */
@media (max-width: 760px) {
    [data-gdp-pill] > *,
    [data-gdp-pill] > * > * {
        max-width: 100% !important;
    }

    [data-gdp-pill] #gdp-localtime {
        font-size: 10.5px;
        letter-spacing: 0.06em;
    }
}

/* ------------------------------------------------------------ display type
 *
 * Never split a word in the middle. The builder sets word-break: break-word
 * on all of its text, which is a reasonable default for a paragraph and a
 * disaster at display size: the first casualty here was the host's own name,
 * rendered SWEET / Y G on a phone. Big type wraps at spaces or not at all. */
[data-gdp-display],
[data-gdp-hostname] {
    word-break: normal !important;
    overflow-wrap: normal !important;
}

@media (max-width: 760px) {
    /* Sized from the viewport so the name lands on one line at any phone
     * width, and capped so it never grows past the size it was designed at.
     * The ratio comes from the measured line: 366px of text per 86px of font,
     * against a column that is 89% of the viewport. */
    [data-gdp-hostname],
    [data-gdp-hostname] span {
        font-size: min(14vw, 86px) !important;
        line-height: 1 !important;
        letter-spacing: -0.02em !important;
    }
}

/* ------------------------------------------------- party mode, hands off
 *
 * The player fades when nobody has touched anything for a few seconds, so
 * what is left on screen is the room. Anything in the overlay that is not
 * the canvas is HUD. Exit fades with the rest - any movement brings it all
 * straight back, and the canvas hold lifts at the same time so the floor
 * comes up to full strength as the controls go. */
[data-gdp-hud] {
    transition: opacity 0.65s ease;
}

html[data-gdp-idle="1"] [data-gdp-hud],
html[data-gdp-idle="1"] #gdp-req-fab,
html[data-gdp-idle="1"] #gdp-calm-btn {
    opacity: 0;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    [data-gdp-hud] {
        transition: none;
    }
}

/* The controls live at the foot of the room, not the middle of it.
 *
 * The overlay is a centred flex column, so the title, scrubber and
 * transport parked dead-centre - directly over the video wall and the
 * crowd, and first-time visitors had to wait out the idle fade before
 * the room showed itself at all. Re-anchoring the column puts the player
 * where a club puts it: down by the floor, under the scene. Everything
 * absolutely positioned in the overlay - the canvas, the ambience wash,
 * the exit button, the full-screen mix picker the title chevron opens -
 * ignores justify-content and stays where it is; only the three static
 * flex children move. Layout properties only: no opacity, transform or
 * filter on this container (see the ghost-rule lesson - the overlay's
 * descendants include composited subtrees that an ancestor effect would
 * silently take down with it). */
html[data-gdp-party="1"] div:has(> #gdp-party-room) {
    justify-content: flex-end !important;
    padding-bottom: calc(30px + env(safe-area-inset-bottom, 0px)) !important;
}

/* ==================================================================== film
 *
 * Everything above builds a room. This builds the *camera* — the layer that
 * decides whether the room reads as a rendering or as something shot.
 *
 * Two effects, both deliberately chosen because they cost nothing per frame:
 * this site already had a choppiness complaint, and the fix for that was
 * spending fewer milliseconds, not more. Grain is one cached SVG bitmap
 * translated on the GPU; the chromatic fringe is a static text-shadow. No
 * filters, no blurs, no per-frame JavaScript, nothing the compositor has to
 * re-rasterise.
 */

/* Emulsion.
 *
 * A pure-black digital ground is the single biggest tell that something was
 * rendered rather than photographed — real darkness has grain in it. One
 * fixed layer of fractal noise over the whole page puts a film stock under
 * the neon, and because it sits above the content rather than behind it, it
 * ties the canvas scenes and the DOM into one image instead of two.
 *
 * Sized 130% and translated rather than re-generated: transform is the one
 * property the compositor can animate without touching the main thread, so
 * the grain moves like film without costing a frame. */
html::after {
    content: "";
    position: fixed;
    top: -15%;
    left: -15%;
    width: 130%;
    height: 130%;
    pointer-events: none;
    z-index: 2147483300;
    /* Plain alpha, not mix-blend-mode.
     *
     * overlay looked marginally richer and cost the whole page its scroll.
     * A blended layer cannot be composited on its own: the browser has to
     * read back everything underneath and re-blend the full viewport - every
     * time this layer moves, which is ~6 times a second, AND every time the
     * content beneath it scrolls. On integrated graphics that is most of a
     * frame budget, spent on a texture at 5% opacity that nobody can
     * consciously see.
     *
     * Without the blend this is an ordinary composited layer: the transform
     * animation runs on the compositor thread and never touches the main
     * thread at all. Opacity is lifted a little to make up the contrast the
     * blend was providing. */
    opacity: 0.075;
    will-change: transform;
    animation: gdpGrain 0.7s steps(1, end) infinite;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

@keyframes gdpGrain {
    0%   { transform: translate3d(0, 0, 0) }
    25%  { transform: translate3d(-2%, 1.5%, 0) }
    50%  { transform: translate3d(1.5%, -2%, 0) }
    75%  { transform: translate3d(-1%, -1%, 0) }
    100% { transform: translate3d(2%, 1%, 0) }
}

/* Calm mode means "turn the room down", so the stock gets quieter with it
 * rather than staying at full strength over a dimmed scene. Reduced motion
 * keeps the grain — it is texture, not movement — and simply stops it
 * moving, which is the part that could bother anyone. */
html[data-gdp-calm="1"]::after {
    opacity: 0.03;
}

@media (prefers-reduced-motion: reduce) {
    html::after {
        animation: none;
    }
}

/* Lens.
 *
 * A hair of chromatic separation on display type only. Real anamorphic glass
 * cannot hold every wavelength on the same plane, and the eye reads that
 * failure as "this was photographed through something". Kept under two
 * pixels at the sizes this applies to (54px and up, per the runtime's own
 * measurement) so it registers as depth rather than as a broken font.
 *
 * Static shadows: no filter, no blur, no repaint after first paint. */
[data-gdp-display] {
    text-shadow:
        -0.011em 0 0 rgba(56, 212, 255, 0.3),
        0.011em 0 0 rgba(237, 47, 212, 0.24);
}

/* The host's name is the one piece of display type that is already a
 * gradient fill; a fringe under it fights the gradient instead of lifting
 * it. */
[data-gdp-hostname] {
    text-shadow: none;
}

/* The floor is a room, not a page with a room on it. While it is open the
 * body-level floating controls step out entirely — they are page furniture,
 * and the HUD inside the overlay already carries everything the room needs.
 * (The exit control is part of that HUD and is unaffected.) */
html[data-gdp-party="1"] #gdp-req-fab,
html[data-gdp-party="1"] #gdp-calm-btn {
    display: none !important;
}

/* ------------------------------------------------------------- a11y
 *
 * The skip link is the first thing in the tab order and invisible until it
 * has focus. Without it, reaching the archive grid by keyboard meant tabbing
 * through the whole navigation on every single page.
 */
#gdp-skip {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 2147483600;
    padding: 12px 18px;
    border-radius: 0 0 10px 0;
    background: #38d4ff;
    color: #07070a;
    font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 13px;
    font-weight: 800;
    letter-spacing: 0.02em;
    text-decoration: none;
    transform: translateY(-120%);
    transition: transform 0.16s ease;
}
#gdp-skip:focus {
    transform: translateY(0);
}
#main:focus {
    outline: none;
}

/* One visible focus ring for everything the site adds, in the site's own
   accent rather than the browser default, which is invisible on this ink. */
[data-gdp-card]:focus-visible,
#gdp-find input:focus-visible,
#gdp-req-fab:focus-visible,
#gdp-calm-btn:focus-visible,
#gdp-req button:focus-visible,
#gdp-req input:focus-visible {
    outline: 2px solid #38d4ff;
    outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
    #gdp-skip {
        transition: none;
    }
}

/* ------------------------------------------------- the second doorway
 *
 * There is a "Party Mode" button further down the page that does exactly what
 * the hero doorway does. Whether two entries should exist is a content call
 * and not one to make in a stylesheet - but them looking like two unrelated
 * controls is a design bug either way. This gives the lower one the same
 * treatment, so the page reads as having one door in two places rather than
 * two different buttons.
 *
 * Tagged from gdp-welcome.js by label, because the markup carries no stable
 * hook of its own.
 */
[data-gdp-doorway] {
    position: relative;
    isolation: isolate;
    /* 40px measured. Below the 44px that a thumb can reliably hit, and this
       is a primary destination, not a footnote. */
    min-height: 44px;
    border-radius: 999px !important;
    color: #f4f1e9 !important;
    background: linear-gradient(
        100deg,
        rgba(56, 212, 255, 0.85),
        rgba(237, 47, 212, 0.85)
    ) !important;
    transition: transform var(--gdp-med) var(--gdp-ease);
}
[data-gdp-doorway]::before {
    content: "";
    position: absolute;
    inset: 1px;
    z-index: -1;
    border-radius: 999px;
    background: #0b0b10;
}
[data-gdp-doorway]::after {
    content: "";
    position: absolute;
    inset: 1px;
    z-index: -1;
    border-radius: 999px;
    opacity: 0;
    background: radial-gradient(
        120% 150% at 50% 118%,
        rgba(56, 212, 255, 0.5),
        rgba(237, 47, 212, 0.22) 45%,
        transparent 72%
    );
    transition: opacity var(--gdp-med) var(--gdp-ease);
}
[data-gdp-doorway]:hover::after,
[data-gdp-doorway]:focus-visible::after {
    opacity: 1;
}
[data-gdp-doorway]:hover {
    transform: translateY(-1px);
}
[data-gdp-doorway]:focus-visible {
    outline: 2px solid #38d4ff;
    outline-offset: 3px;
}

/* ==================================================================
 * REFINEMENT PASS - hierarchy, depth, state clarity
 * ================================================================== */

/* ----------------------------------------------------- A. hero rhythm
 *
 * The order of importance in the hero is brand, then latest mix, then play,
 * then the doorway, then the broadcast countdown. The markup already carries
 * that order; what it lacked was air between the tiers, so everything arrived
 * at once and the eye had to sort it out. These are spacing and weight only -
 * no element moves, nothing is hidden.
 */

/* Measure. Long lines are the single most common reason a dark page reads as
 * heavy - the eye loses the return sweep. 66ch is the upper bound of
 * comfortable, and this only ever tightens. */
[data-framer-name="Hero Content"] p,
[data-framer-name="Opening"] p {
    max-width: 66ch;
}

/* The countdown is ritual, not the headline. It sat at the same visual weight
 * as the thing you are meant to press. Half a step back: the frame softens,
 * the numerals keep their size and contrast. Supporting the ritual, not
 * competing with the listening. */
[data-gdp-live-pill],
a[href*="ffm.to/globaldanceparty"] {
    transition: border-color var(--gdp-med) var(--gdp-ease),
        background-color var(--gdp-med) var(--gdp-ease);
}

/* -------------------------------------------- C. player state clarity
 *
 * State was carried almost entirely by the play/pause glyph. These add a
 * second, non-colour channel for each state - which is also the accessibility
 * requirement: never state by colour or motion alone.
 */

/* Loading. A control that has been pressed and has not answered yet is the
 * worst moment in any player: nothing distinguishes "working" from "broken".
 * A slow sheen across the transport says the press landed. */
[data-gdp-player="loading"] [data-gdp-transport],
[data-gdp-loading="1"] {
    position: relative;
    overflow: hidden;
}
[data-gdp-player="loading"] [data-gdp-transport]::after,
[data-gdp-loading="1"]::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(
        100deg,
        transparent 20%,
        rgba(56, 212, 255, 0.14) 50%,
        transparent 80%
    );
    transform: translateX(-100%);
    animation: gdpSheen 1.15s var(--gdp-enter) infinite;
}
@keyframes gdpSheen {
    to {
        transform: translateX(100%);
    }
}

/* Playing. A thin steady light under the transport - present, not flashing. */
[data-gdp-playing="1"] [data-gdp-transport] {
    box-shadow: 0 6px 22px -14px rgba(56, 212, 255, 0.85);
}

/* The record turns while it plays and holds still when it does not. It is the
 * oldest playback indicator there is and it needs no legend. Transform only,
 * so it is compositor work and costs no layout. */
[data-gdp-spin] {
    animation: gdpSpin 8s linear infinite;
    animation-play-state: paused;
    will-change: transform;
}
[data-gdp-playing="1"] [data-gdp-spin] {
    animation-play-state: running;
}
@keyframes gdpSpin {
    to {
        transform: rotate(360deg);
    }
}

/* Complete. */
[data-gdp-player="ended"] [data-gdp-transport] {
    box-shadow: none;
}

/* Transport buttons: tactile, and obviously pressable. Fast tier - this is
 * input answering back. */
[data-gdp-transport] button,
[aria-label^="Go to Mix"],
[aria-label="Next mix"],
[aria-label="Previous mix"] {
    transition: transform var(--gdp-fast) var(--gdp-ease),
        opacity var(--gdp-fast) var(--gdp-ease);
}
[aria-label="Next mix"]:hover,
[aria-label="Previous mix"]:hover {
    transform: scale(1.08);
}
[aria-label="Next mix"]:active,
[aria-label="Previous mix"]:active {
    transform: scale(0.96);
}

/* Every icon-only control the layer can reach gets a visible ring. */
[data-gdp-transport] button:focus-visible,
[aria-label^="Go to Mix"]:focus-visible,
[aria-label="Next mix"]:focus-visible,
[aria-label="Previous mix"]:focus-visible,
[aria-label="Leave party mode"]:focus-visible {
    outline: 2px solid #38d4ff;
    outline-offset: 3px;
    border-radius: 999px;
}

/* --------------------------------------------- D. archive as discovery
 *
 * The grid was a database: 74 equal cells, equal weight, equal silence. These
 * do not restructure it - they give it a hierarchy to read and something to
 * answer back when touched.
 */

/* The card lifts, and its art brightens a little under it. Two properties,
 * both composited. */
[data-gdp-card] {
    transition: transform var(--gdp-med) var(--gdp-ease),
        box-shadow var(--gdp-med) var(--gdp-ease);
}
[data-gdp-card]:hover {
    transform: translateY(-4px);
    box-shadow: 0 22px 44px -26px rgba(0, 0, 0, 0.9),
        0 0 0 1px rgba(56, 212, 255, 0.16);
}
[data-gdp-card] img {
    transition: filter var(--gdp-med) var(--gdp-ease);
}
[data-gdp-card]:hover img {
    filter: brightness(1.06) saturate(1.06);
}
[data-gdp-card]:focus-visible {
    outline: 2px solid #38d4ff;
    outline-offset: 4px;
}

/* The newest mix is the recommended one, and it should not have to shout to
 * say so. A hairline of brand light along its edge, permanently. */
[data-gdp-card][data-gdp-latest="1"] {
    box-shadow: 0 0 0 1px rgba(56, 212, 255, 0.34);
}
[data-gdp-card][data-gdp-latest="1"]:hover {
    box-shadow: 0 22px 44px -26px rgba(0, 0, 0, 0.9),
        0 0 0 1px rgba(56, 212, 255, 0.5);
}

/* Metadata rhythm: the mix number leads, the rest recedes. Tabular numerals so
 * counts and durations line up down the column instead of shimmering. */
[data-gdp-card] [data-gdp-meta] {
    font-variant-numeric: tabular-nums;
}

/* Genre chips: editorial rather than functional. The active one is filled;
 * the rest are outlines. Colour is never the only signal - the active chip is
 * also the only filled one, and carries aria-pressed. */
#gdp-filter .gdp-chip,
#gdp-coll .gdp-chip {
    transition: border-color var(--gdp-fast) var(--gdp-ease),
        color var(--gdp-fast) var(--gdp-ease),
        background-color var(--gdp-fast) var(--gdp-ease),
        transform var(--gdp-fast) var(--gdp-ease);
}
#gdp-filter .gdp-chip:active,
#gdp-coll .gdp-chip:active {
    transform: scale(0.97);
}

/* Surprise Me: playful, restrained. It brightens; it does not bounce. */
[data-gdp-surprise] {
    transition: transform var(--gdp-fast) var(--gdp-ease),
        box-shadow var(--gdp-med) var(--gdp-ease);
}
[data-gdp-surprise]:hover {
    transform: translateY(-1px);
    box-shadow: 0 0 22px -8px rgba(56, 212, 255, 0.7);
}
[data-gdp-surprise]:active {
    transform: translateY(0) scale(0.98);
}

/* ------------------------------------------------- E. legacy pacing
 *
 * Chapters, not a list. A hairline rule and a wider gap turn six paragraphs
 * into six milestones without touching a word of the copy.
 */
[data-framer-name="Era"] {
    position: relative;
}
[data-framer-name="Era"] + [data-framer-name="Era"] {
    margin-top: clamp(28px, 4.5vw, 56px);
    padding-top: clamp(24px, 4vw, 44px);
    border-top: 1px solid rgba(244, 241, 233, 0.08);
}
[data-gdp-attrib] {
    letter-spacing: 0.01em;
}

/* ------------------------------------------------ F. depth, not void
 *
 * Darkness reads as space when something sits in front of something else, and
 * as a void when everything is on one plane. One soft vignette, painted on the
 * body's own backdrop, gives the page a centre.
 */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background: radial-gradient(
        120% 90% at 50% 0%,
        rgba(56, 212, 255, 0.05),
        transparent 58%
    );
}

/* Glow is reserved. Anything that is live, playing or a way in may carry it;
 * nothing else may. */
[data-gdp-live="1"],
[data-gdp-playing="1"] [data-gdp-transport] {
    /* declared above - listed here as the whitelist, deliberately */
}

/* ------------------------------------------------ H. responsive
 *
 * Decoration goes first on small screens; hierarchy is what carries the
 * premium feeling there, not density.
 */
@media (max-width: 760px) {
    /* Hover lift is meaningless on touch and leaves cards stuck in a hovered
       state after a tap. */
    [data-gdp-card]:hover {
        transform: none;
        box-shadow: none;
    }
    [data-gdp-card]:hover img {
        filter: none;
    }
    /* The vignette is a desktop nicety; on a phone it is a gradient over the
       whole screen for no legibility gain. */
    body::before {
        display: none;
    }
    /* Comfortable targets. */
    [data-gdp-surprise],
    #gdp-filter .gdp-chip,
    #gdp-coll .gdp-chip {
        min-height: 44px;
    }
}

/* Anything that animates forever stops when asked. */
@media (prefers-reduced-motion: reduce) {
    [data-gdp-spin],
    [data-gdp-player="loading"] [data-gdp-transport]::after,
    [data-gdp-loading="1"]::after {
        animation: none;
    }
    [data-gdp-card]:hover {
        transform: none;
    }
}

/* The way out of the room.
 *
 * Measured 99x42 on a phone. Two pixels under the minimum sounds pedantic
 * until you remember which control this is: the one a visitor reaches for
 * when they want the immersive fullscreen thing to stop. It already cost
 * one round of "there is no easy way out of party mode on mobile", and a
 * target that small is part of why. */
/* Second round of the same complaint, different cause: the pill was the
 * right size but wore the room's own colours - a near-black plate with a
 * faint ring, sitting on a near-black scene full of moving light. On a
 * static screenshot it reads; over beams and a video wall it camouflages.
 * The plate now blurs whatever the scene paints behind it (that
 * separation works against ANY backdrop, which a fixed colour cannot
 * promise) and carries a ring bright enough to read as a control. This
 * button is a leaf - the ancestor-effect rule (9g) is about containers
 * with composited subtrees, not about styling a childless control. */
[aria-label="Leave party mode"] {
    min-height: 44px;
    min-width: 44px;
    background: rgba(10, 10, 12, 0.55) !important;
    border: 1px solid rgba(244, 241, 233, 0.42) !important;
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    box-shadow:
        0 2px 16px rgba(0, 0, 0, 0.5),
        0 0 0 4px rgba(10, 10, 12, 0.25);
}
@media (max-width: 760px) {
    [aria-label="Leave party mode"] {
        top: calc(14px + env(safe-area-inset-top, 0px)) !important;
        right: 14px !important;
        min-height: 48px;
        min-width: 48px;
    }
}

/* The navbar resume chip and the floor bar are the same offer in two places,
 * which is the exact objection the design brief raised against a floor bar.
 * Answered by standing the chip down rather than giving up the bar: it is
 * hidden only while the bar is actually up and carrying something. */
html[data-gdp-bar="1"] [data-gdp-chip] {
    display: none !important;
}

/* Nothing should end underneath the bar. */
html[data-gdp-bar="1"] body {
    padding-bottom: 72px;
}
@media (max-width: 760px) {
    html[data-gdp-bar="1"] body {
        padding-bottom: 64px;
    }
}

/* ------------------------------------------------- the Floor and the Booth
 *
 * Two views of the same homepage, hide/show only - no node is ever removed,
 * so hydration and the audio shield are untouched. The floor keeps the
 * turntable (a record you tap is the whole tutorial); the crate, cue rail,
 * tracklist, selector and mixer are booth equipment.
 */
html[data-gdp-view="floor"] [data-gdp-booth="1"] {
    display: none !important;
}
/* Zero-flash versions of the same hides. The JS tags land a few hundred
 * milliseconds after first paint, which on a cold load let the crate flash
 * before collapsing. :has() needs no tags - the crate column is the child
 * containing crate items, the rail is the child containing the Right Rail -
 * so browsers that support it (all modern ones) never show the flash, and
 * the tagged rules above remain as the fallback for the rest. */
html[data-gdp-view="floor"] [data-framer-name="Booth Row"] > div:has(.gdp-crate-item) {
    display: none !important;
}
html[data-gdp-view="floor"]
    [data-framer-name="Booth Row"]
    > div:has([data-framer-name="Right Rail"]),
html[data-gdp-view="floor"] [data-framer-name="Right Rail"] {
    display: none !important;
}
/* The mix selector (previous / choose a mix / next) stays in BOTH views.
 * It was hidden on the desktop floor at first, but a player where you can
 * tap the record and not choose the record is a radio, not a player - the
 * operator called it: picking a mix is listener furniture, not booth gear.
 * No rule needed; kept as a comment so nobody re-hides it as "clutter". */
html[data-gdp-view="floor"] #gdp-deckbar {
    display: none !important;
}
/* The lone turntable centres in the row the crate used to share. */
html[data-gdp-view="floor"] [data-framer-name="Booth Row"] {
    justify-content: center !important;
}
/* The "PLAY ME" caption hangs absolutely ~45px below the turntable's box.
 * In the booth the row is 593px tall (the crate columns set the height) so
 * the caption had headroom; on the floor the row shrinks to the turntable
 * itself and the caption bled into the mix selector beneath it. Reserve the
 * caption's space explicitly. */
html[data-gdp-view="floor"] [data-framer-name="Booth Row"] {
    padding-bottom: 62px !important;
    align-items: center !important;
    gap: clamp(28px, 4vw, 60px) !important;
}

/* One door: once the Dance Floor doorway exists, the older "Party Mode"
 * button is the same action twice.
 *
 * Ghost-hidden, NOT display:none. The doorway works by relaying a click to
 * this button, and Framer's press handler does not fire on an element that
 * has no layout - verified directly: a click on the connected button under
 * display:none did nothing, the same click with the button merely clipped
 * opens the room. So it keeps its box and loses everything else: invisible,
 * unhittable, out of the reading order (gdp-view.js adds aria-hidden and
 * tabindex=-1, which CSS cannot). */
/* THE BUTTON, NEVER ITS CONTAINER.
 *
 * The first version of this rule ghosted [data-framer-name="Party Mode"] -
 * the container - and the Framer component mounts its fullscreen overlay
 * INLINE inside that container, not through a portal. Ancestor opacity
 * composites the whole subtree, position:fixed children included: the room
 * mounted, active() returned true, the exit button existed, every DOM check
 * passed - and not one pixel of the club was visible. The operator found it
 * the honest way, by pressing the button and seeing nothing.
 *
 * So the ghost styles land only on the trigger buttons themselves, tagged
 * data-gdp-ghost by gdp-view.js. The container stays untouched, because the
 * room lives in it. */
html[data-gdp-door="1"] [data-gdp-ghost],
html[data-gdp-door="1"] button[data-gdp-doorway] {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    overflow: hidden !important;
    clip-path: inset(50%) !important;
    opacity: 0 !important;
    pointer-events: none !important;
    margin: 0 !important;
    padding: 0 !important;
    border: 0 !important;
}

/* The one orientation line. Factual register, no sentiment (rule 6). */
#gdp-orient {
    margin: 14px auto 0;
    max-width: 62ch;
    text-align: center;
    font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 14.5px;
    line-height: 1.55;
    color: rgba(244, 241, 233, 0.78);
}
@media (max-width: 760px) {
    #gdp-orient {
        font-size: 13.5px;
        padding: 0 18px;
    }
}

/* The Booth toggle: same family as the doorway - dark plate, hairline light -
 * so the two doors beside each other read as siblings, not rivals. */
#gdp-booth-toggle {
    position: relative;
    isolation: isolate;
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 2px;
    min-height: 52px;
    margin-left: 12px;
    padding: 0 20px;
    border: 0;
    border-radius: 14px;
    cursor: pointer;
    font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
    text-align: left;
    color: #f4f1e9;
    background: rgba(244, 241, 233, 0.16);
    transition: transform var(--gdp-med) var(--gdp-ease);
}
#gdp-booth-toggle::before {
    content: "";
    position: absolute;
    inset: 1px;
    z-index: -1;
    border-radius: 13px;
    background: #0b0b10;
}
#gdp-booth-toggle:hover {
    transform: translateY(-1px);
    background: rgba(56, 212, 255, 0.45);
}
#gdp-booth-toggle:focus-visible {
    outline: 2px solid #38d4ff;
    outline-offset: 3px;
}
#gdp-booth-toggle .ttl {
    font-size: 12px;
    font-weight: 800;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    line-height: 1.15;
}
#gdp-booth-toggle .sub {
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: rgba(244, 241, 233, 0.62);
    line-height: 1.15;
}
/* The slot now holds two doors; let them wrap on a phone. */
#gdp-ctaslot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 12px 0;
}
/* No Booth on a phone, because there is no booth on a phone: the crate and
 * the rail are desktop furniture Framer never ships to this breakpoint. A
 * toggle that appears to do nothing is worse than no toggle. */
@media (max-width: 760px) {
    #gdp-booth-toggle {
        display: none !important;
    }
}
@media (prefers-reduced-motion: reduce) {
    #gdp-booth-toggle {
        transition: none;
    }
}

/* ---------------------------------------- the two doors, made unmissable
 *
 * Operator call: the Dance Floor and the Booth are the site's two
 * destinations and were sized like options. Larger, a periodic nudge until
 * first noticed, and a hover explanation - because testers' actual words
 * were "I don't know where to go or why".
 *
 * The nudge stops the moment a visitor notices a door (hover, focus or
 * press - remembered for the session), stops while music plays (the beat
 * glint owns the doorway then; two motions on one control is noise), and
 * never runs under calm mode or reduced motion - where the enlarged size
 * and a stronger standing ring carry the prominence instead.
 */
html[data-gdp-view] #gdp-party-cta {
    min-height: 66px;
    padding: 0 30px 0 24px;
    gap: 15px;
}
html[data-gdp-view] #gdp-party-cta .ttl {
    font-size: 14.5px;
}
html[data-gdp-view] #gdp-party-cta .sub {
    font-size: 12.5px;
}
html[data-gdp-view] #gdp-party-cta .door {
    width: 24px;
    height: 32px;
}
html[data-gdp-view] #gdp-booth-toggle {
    min-height: 66px;
    padding: 0 26px;
    margin-left: 14px;
    /* Gold: the same colour the booth fascia carries inside the party room,
       so the label and the destination share an identity. */
    background: linear-gradient(
        100deg,
        rgba(242, 196, 101, 0.75),
        rgba(242, 196, 101, 0.35)
    );
}
html[data-gdp-view] #gdp-booth-toggle:hover {
    background: linear-gradient(
        100deg,
        rgba(242, 196, 101, 1),
        rgba(242, 196, 101, 0.55)
    );
}
html[data-gdp-view] #gdp-booth-toggle .ttl {
    font-size: 14.5px;
}
html[data-gdp-view] #gdp-booth-toggle .sub {
    font-size: 12.5px;
    color: rgba(242, 196, 101, 0.8);
}

/* The nudge: a quick double-hop with a glow bloom, then five seconds of
 * stillness. The two doors are half a cycle out of phase so they never jump
 * together - simultaneous competing movement is the thing the motion system
 * forbids. */
@keyframes gdpNudgeCyan {
    0%, 12%, 100% {
        transform: translateY(0);
        box-shadow: 0 0 0 0 rgba(56, 212, 255, 0);
    }
    3% {
        transform: translateY(-6px);
        box-shadow: 0 14px 30px -12px rgba(56, 212, 255, 0.55);
    }
    6% {
        transform: translateY(0);
    }
    9% {
        transform: translateY(-3px);
        box-shadow: 0 10px 24px -12px rgba(237, 47, 212, 0.4);
    }
}
@keyframes gdpNudgeGold {
    0%, 12%, 100% {
        transform: translateY(0);
        box-shadow: 0 0 0 0 rgba(242, 196, 101, 0);
    }
    3% {
        transform: translateY(-6px);
        box-shadow: 0 14px 30px -12px rgba(242, 196, 101, 0.6);
    }
    6% {
        transform: translateY(0);
    }
    9% {
        transform: translateY(-3px);
        box-shadow: 0 10px 24px -12px rgba(242, 196, 101, 0.4);
    }
}
html[data-gdp-view]
    #gdp-party-cta:not([data-gdp-noticed]):not(:hover):not([data-gdp-on="1"]):not([data-gdp-hail="1"]) {
    animation: gdpNudgeCyan 7s var(--gdp-ease) infinite;
}
html[data-gdp-view] #gdp-booth-toggle:not([data-gdp-noticed]):not(:hover) {
    animation: gdpNudgeGold 7s var(--gdp-ease) 3.5s infinite;
}
html[data-gdp-calm="1"] #gdp-party-cta,
html[data-gdp-calm="1"] #gdp-booth-toggle {
    animation: none !important;
}
@media (prefers-reduced-motion: reduce) {
    #gdp-party-cta,
    #gdp-booth-toggle {
        animation: none !important;
    }
    /* Prominence without motion: a standing ring. */
    html[data-gdp-view] #gdp-party-cta:not([data-gdp-noticed]) {
        box-shadow: 0 0 0 2px rgba(56, 212, 255, 0.35);
    }
    html[data-gdp-view] #gdp-booth-toggle:not([data-gdp-noticed]) {
        box-shadow: 0 0 0 2px rgba(242, 196, 101, 0.4);
    }
}

/* The explanation. Appears above the door on hover or keyboard focus, after
 * a beat so it never flashes on a pass-over; wired to aria-describedby by
 * gdp-view.js so a screen reader gets the same sentence without the hover. */
.gdp-tip {
    position: absolute;
    bottom: calc(100% + 13px);
    left: 50%;
    transform: translateX(-50%) translateY(5px);
    width: max-content;
    max-width: min(280px, 84vw);
    padding: 11px 14px;
    border-radius: 11px;
    background: rgba(13, 13, 19, 0.97);
    border: 1px solid rgba(244, 241, 233, 0.16);
    box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.9);
    font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 12.5px;
    font-weight: 500;
    line-height: 1.5;
    letter-spacing: 0.01em;
    text-transform: none;
    text-align: left;
    color: rgba(244, 241, 233, 0.88);
    white-space: normal;
    opacity: 0;
    pointer-events: none;
    z-index: 6;
    transition: opacity var(--gdp-fast) var(--gdp-ease) 0.22s,
        transform var(--gdp-fast) var(--gdp-ease) 0.22s;
}
.gdp-tip::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    width: 9px;
    height: 9px;
    margin: -5px 0 0 -5px;
    transform: rotate(45deg);
    background: rgba(13, 13, 19, 0.97);
    border-right: 1px solid rgba(244, 241, 233, 0.16);
    border-bottom: 1px solid rgba(244, 241, 233, 0.16);
}
#gdp-party-cta:hover .gdp-tip,
#gdp-party-cta:focus-visible .gdp-tip,
#gdp-booth-toggle:hover .gdp-tip,
#gdp-booth-toggle:focus-visible .gdp-tip {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}
@media (prefers-reduced-motion: reduce) {
    .gdp-tip {
        transition-duration: 1ms;
    }
}

/* -------------------------------------------------- the two scene doors
 *
 * Left of the turntable: the club entrance, the party visible through it.
 * Right: the DJ booth. Desktop floor view only - phones keep the compact
 * doors (no flanking space), the booth view gives the flanks back to the
 * crate. The compact ctaslot doors retire on the desktop floor because the
 * scenes ARE those two buttons at full size.
 */
.gdp-scene {
    display: none;
}
@media (min-width: 761px) {
    html[data-gdp-view="floor"] #gdp-ctaslot {
        display: none !important;
    }
    html[data-gdp-view="floor"] .gdp-scene {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 13px;
        position: relative;
        isolation: isolate;
        width: clamp(200px, 21vw, 264px);
        padding: 0;
        border: 0;
        background: transparent;
        cursor: pointer;
        font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
        color: #f4f1e9;
        transition: transform var(--gdp-med) var(--gdp-ease);
    }
    html[data-gdp-view="floor"] .gdp-scene:hover {
        transform: translateY(-5px);
    }
}
.gdp-scene:focus-visible {
    outline: 2px solid #38d4ff;
    outline-offset: 5px;
    border-radius: 18px;
}
.gdp-scene .frame {
    position: relative;
    width: min(100%, 236px);
    height: clamp(250px, 23vw, 306px);
    border-radius: 16px 16px 10px 10px;
    padding: 2px;
    display: block;
}
#gdp-scene-floor .frame {
    background: linear-gradient(
        165deg,
        rgba(56, 212, 255, 0.85),
        rgba(237, 47, 212, 0.65)
    );
}
#gdp-scene-booth .frame {
    background: linear-gradient(
        165deg,
        rgba(217, 180, 92, 0.85),
        rgba(217, 180, 92, 0.3)
    );
}
.gdp-scene .peek {
    position: absolute;
    inset: 2px;
    border-radius: 14px 14px 8px 8px;
    overflow: hidden;
    background: #07070b;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Through the left door: the reel on the wall, silhouettes in front. The
   .wall's own glow is the no-video fallback on weak devices. */
#gdp-scene-floor .wall {
    position: absolute;
    inset: 0;
    background: radial-gradient(
            85% 60% at 50% 26%,
            rgba(237, 47, 212, 0.32),
            rgba(56, 212, 255, 0.14) 55%,
            transparent 80%
        )
        #07070b;
}
#gdp-scene-floor .wall video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: brightness(0.72) saturate(1.12);
    transition: filter var(--gdp-med) var(--gdp-ease);
}
#gdp-scene-floor:hover .wall video {
    filter: brightness(0.95) saturate(1.2);
}
#gdp-scene-floor .beamsL {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(
            115deg,
            transparent 32%,
            rgba(56, 212, 255, 0.18) 45%,
            transparent 58%
        ),
        linear-gradient(
            245deg,
            transparent 30%,
            rgba(237, 47, 212, 0.16) 44%,
            transparent 58%
        );
    mix-blend-mode: screen;
}
#gdp-scene-floor .crowd {
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    width: 100%;
    height: 38%;
}
/* Light under the door. Breathes, slowly - the slow tier of the motion
   system - and holds still under calm mode or reduced motion. */
#gdp-scene-floor .spill {
    position: absolute;
    left: 14%;
    right: 14%;
    top: calc(clamp(250px, 23vw, 306px) - 4px);
    height: 22px;
    border-radius: 50%;
    background: radial-gradient(
        50% 100% at 50% 0%,
        rgba(56, 212, 255, 0.55),
        rgba(237, 47, 212, 0.3) 55%,
        transparent 85%
    );
    filter: blur(6px);
    opacity: 0.55;
    pointer-events: none;
    animation: gdpSpillBreathe 4.5s ease-in-out infinite alternate;
}
@keyframes gdpSpillBreathe {
    from {
        opacity: 0.4;
    }
    to {
        opacity: 0.8;
    }
}
#gdp-scene-floor:hover .spill {
    opacity: 1;
    animation: none;
}
html[data-gdp-calm="1"] #gdp-scene-floor .spill {
    animation: none;
}
/* The booth card photo: fills the frame like the reel fills the door, and
 * brightens the same way on hover, so the two scenes answer touch with one
 * voice. The SVG rules below stay for the load-failure fallback. */
#gdp-scene-booth .booth-photo {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: brightness(0.88) saturate(1.05);
    transition: filter var(--gdp-med) var(--gdp-ease);
}
#gdp-scene-booth:hover .booth-photo,
#gdp-scene-booth:focus-visible .booth-photo {
    filter: brightness(1.04) saturate(1.12);
}
@media (prefers-reduced-motion: reduce) {
    #gdp-scene-booth .booth-photo {
        transition: none;
    }
}

/* The booth illustration. The fascia lights the way the real one does. */
#gdp-scene-booth .booth-art {
    width: 88%;
    height: auto;
}
#gdp-scene-booth .fascia {
    filter: drop-shadow(0 0 4px rgba(217, 180, 92, 0.45));
    transition: filter var(--gdp-fast) var(--gdp-ease);
}
#gdp-scene-booth:hover .fascia {
    filter: drop-shadow(0 0 11px rgba(217, 180, 92, 0.95));
}
#gdp-scene-booth .leds circle {
    opacity: 0.45;
    transition: opacity var(--gdp-fast) var(--gdp-ease);
}
#gdp-scene-booth:hover .leds circle {
    opacity: 1;
}
/* Captions */
.gdp-scene .cap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    line-height: 1.2;
}
.gdp-scene .cap .t {
    font-size: 15px;
    font-weight: 800;
    letter-spacing: 0.17em;
    text-transform: uppercase;
}
.gdp-scene .cap .s {
    font-size: 12.5px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: rgba(244, 241, 233, 0.62);
    transition: color var(--gdp-fast) var(--gdp-ease);
}
#gdp-scene-floor:hover .cap .s {
    color: #38d4ff;
}
#gdp-scene-booth:hover .cap .s {
    color: rgb(217, 180, 92);
}
.gdp-scene .arw {
    display: inline-block;
    transition: transform var(--gdp-fast) var(--gdp-ease);
}
.gdp-scene:hover .arw {
    transform: translateX(3px);
}
/* Scene tooltips ride the shared .gdp-tip; these show them. */
.gdp-scene:hover .gdp-tip,
.gdp-scene:focus-visible .gdp-tip {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}
@media (prefers-reduced-motion: reduce) {
    .gdp-scene,
    .gdp-scene .arw,
    #gdp-scene-floor .wall video {
        transition: none;
    }
    html[data-gdp-view="floor"] .gdp-scene:hover {
        transform: none;
    }
    #gdp-scene-floor .spill {
        animation: none;
    }
}

/* Party mode: the spinning logo disc is retired (operator call - it covered
 * the crowd and the video wall, and made the room read as a player skin).
 * Tagged at mount by gdp-party.js; the component's rotation loop keeps
 * writing to the hidden node harmlessly. */
html[data-gdp-party="1"] [data-gdp-disc] {
    display: none !important;
}
