/*
 * MoonHavenRP - website
 *
 * The MoonHaven glass design language, ported from the in-game UI
 * (mh_multicharacter / loadscreen web/style.css) to the browser.
 *
 * What carries over: the palette, the Outfit + Alex Brush typography, the
 * brand gradient, layered-gradient glass, gradient ring borders, pill buttons.
 *
 * What deliberately differs: this is a browser, not the game's CEF, so real
 * backdrop-filter, scroll-driven reveals and parallax are all available and
 * used. Sizing is fluid (clamp) rather than viewport-height rem, because a
 * web page is read on phones as well as ultrawides.
 */

/* ==================================================================
   1. Tokens
   ================================================================== */

:root {
    /* Channel tokens. Every veil, hairline and shadow below is mixed from
       these two, so a theme flips them once instead of in thirty separate
       declarations. */
    --w: 255, 255, 255;
    --k: 0, 0, 0;

    /* Palette. Shared with Mooniez Store, value for value, so the two sites
       are one family - and pulled a step off pure black on purpose: near-white
       type on near-black bleeds at the edges, which is the most common reason
       a dark site is tiring to read. */
    --ink:            #100D1A;
    --ink-2:          #171423;
    --accent:         #A78BFA;   /* cyberpunk purple */
    --secondary:      #EC4899;   /* neon pink */
    --highlight:      #7DD3FC;   /* moonlight blue */
    --text:           #E9E7F2;
    /* The raised text colour: what a hover or an active control turns.
       Paper redefines it - white on paper is the bug it replaces. */
    --hi:             #FFFFFF;
    /* The light that follows the cursor across a button. Paper
       redefines it: --w is ink there, and a black smudge is not a
       glow. */
    --btn-glow:  rgba(255, 255, 255, 0.30);
    --text-muted:     #BCBCC9;
    --text-dim:       #8A8A98;

    --grad-brand:     linear-gradient(100deg, #8B5CF6 0%, #EC4899 52%, #A78BFA 100%);
    /* The fill that sits inside a gradient-rimmed pill. Near-opaque on purpose:
       a real gradient border is painted to the border box with the fill painted
       to the padding box, and only an opaque fill hides the gradient everywhere
       except the rim. It is the avatar bubble's recipe, which is the one that
       reads right - a masked rim comes out thin and washed by comparison. */
    --pill-base:      linear-gradient(rgba(24, 17, 44, 0.95), rgba(14, 10, 26, 0.95));
    --grad-brand-4:   linear-gradient(100deg, #8B5CF6, #EC4899, #A78BFA, #8B5CF6);

    --glow-purple:    rgba(167, 139, 250, 0.45);
    --glow-pink:      rgba(244, 114, 182, 0.35);

    /* Glass */
    /* The glass tint. Deliberately light: this is the density the cards had
       part-way through the reveal, which is the look that was wanted - the
       aurora reading through the panel rather than being shut out by it.
       Only the tint is lighter; the blur and the text are untouched, so the
       copy stays as crisp as it was. */
    --panel:          rgba(18, 13, 32, 0.25);
    --panel-soft:     rgba(var(--w), 0.045);
    --panel-border:   rgba(var(--w), 0.13);
    --panel-border-2: rgba(var(--w), 0.26);
    --panel-shadow:   0 1.5rem 3.5rem rgba(var(--k), 0.45);

    /* The two sheets the page is held between. The header and the footer are
       the same material and must stay that way, so they share one recipe
       instead of each carrying its own hand-mixed rgba. --sheet-2 is the
       thicker version the header takes once content is passing under it. */
    --sheet:          linear-gradient(180deg, rgba(16, 12, 28, 0.62), rgba(8, 6, 15, 0.48));
    /* The footer wears the same sheet, mirrored: the dense end faces the
       outside of the page so header and footer bookend the content
       instead of both leaning the same way. It is a token because it was
       a literal, and a literal cannot follow the theme - which is why
       the footer stayed a dark band under ink in daylight. */
    --sheet-foot:     linear-gradient(0deg, rgba(16, 12, 28, 0.62), rgba(8, 6, 15, 0.48));
    --sheet-2:        linear-gradient(180deg, rgba(14, 10, 24, 0.78), rgba(8, 6, 15, 0.66));
    --sheet-blur:     blur(26px) saturate(160%);
    --sheet-pop:      linear-gradient(180deg, rgba(22, 17, 38, 0.97), rgba(12, 9, 22, 0.96));
    --sheet-tip:      rgba(16, 12, 26, 0.98);
    --sheet-menu:     rgba(13, 10, 24, 0.93);
    --sheet-veil:     rgba(4, 3, 9, 0.88);
    --blur-lite:      blur(14px) saturate(130%);

    /* The other gradient-rimmed fills, one token each for the same reason. */
    --pill-avatar:    linear-gradient(rgba(30, 22, 52, 0.95), rgba(14, 10, 26, 0.95));
    --pill-flat:      linear-gradient(rgba(6, 4, 12, 0.92), rgba(6, 4, 12, 0.92));
    --pill-soft:      linear-gradient(rgba(20, 14, 36, 0.95), rgba(12, 9, 22, 0.95));

    /* The sky, layer by layer, so daylight can change the weather without
       touching a line of markup. */
    --sky-wash:       #1A1030;
    --sky-violet:     rgba(139, 92, 246, 0.16);
    --sky-pink:       rgba(236, 72, 153, 0.12);
    --aurora-violet:  rgba(139, 92, 246, 0.30);
    --aurora-pink:    rgba(236, 72, 153, 0.22);
    --aurora-blue:    rgba(96, 165, 250, 0.16);
    --star-opacity:   0.85;
    --moon-face:      radial-gradient(circle at 34% 32%, #FFF7FD 0%, #E9D8FF 35%, #B79CF5 70%, #7C5CD6 100%);
    --moon-glow:      0 0 4rem rgba(167, 139, 250, 0.55),
                      0 0 10rem rgba(236, 72, 153, 0.28),
                      inset -1.2rem -1.2rem 3rem rgba(40, 20, 80, 0.55);
    --grain-opacity:  0.05;

    /* Shape & motion */
    --r-lg: 1.5rem;
    --r-md: 1rem;
    --r-sm: 0.7rem;
    --ease: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --speed: 0.28s;

    /* Rhythm */
    --wrap: 76rem;
    --gutter: clamp(1.25rem, 4vw, 3rem);
    --section-y: clamp(4.5rem, 11vh, 9rem);
}

/* ==================================================================
   Daylight
   Same city, different hour. The channel tokens invert, the surfaces
   become paper instead of glass over black, and the brand gradient
   darkens a step so it reads ON light rather than glowing over dark.
   The attribute is ours - `data-theme` belongs to the browser and to
   whatever is hosting the page, and neither should move this.
   ================================================================== */

html[data-mh="light"] {
/* Daylight takes the wishlist's lilac, which the owner already likes
   there. White is invisible on a pale button and ink reads as a
   shadow, which is the bug this replaces. */
html[data-mh="light"] { --btn-glow: rgba(167, 139, 250, 0.32); }
    color-scheme: light;

    --w: 24, 18, 40;
    --k: 40, 30, 70;

    --ink:            #F4F2FA;
    --ink-2:          #EAE6F5;
    --accent:         #6D28D9;
    --secondary:      #DB2777;
    --highlight:      #0EA5E9;
    --text:           #1B1630;
    --hi:             #241D3D;
    --text-muted:     #4B4462;
    --text-dim:       #6E667F;

    --grad-brand:     linear-gradient(100deg, #7C3AED 0%, #DB2777 52%, #8B5CF6 100%);
    --grad-brand-4:   linear-gradient(100deg, #7C3AED, #DB2777, #8B5CF6, #7C3AED);

    --glow-purple:    rgba(124, 58, 237, 0.28);
    --glow-pink:      rgba(219, 39, 119, 0.20);

    --panel:          rgba(255, 255, 255, 0.62);
    --panel-soft:     rgba(255, 255, 255, 0.5);
    --panel-border:   rgba(var(--w), 0.16);
    --panel-border-2: rgba(var(--w), 0.30);
    --panel-shadow:   0 1rem 2.4rem rgba(var(--k), 0.10);

    --sheet:          linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(248, 246, 253, 0.62));
    --sheet-foot:     linear-gradient(0deg, rgba(255, 255, 255, 0.8), rgba(248, 246, 253, 0.62));
    --sheet-2:        linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(246, 244, 252, 0.82));
    --sheet-pop:      linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 246, 253, 0.97));
    --sheet-tip:      rgba(27, 22, 48, 0.96);
    --sheet-menu:     rgba(252, 250, 255, 0.96);
    --sheet-veil:     rgba(244, 242, 250, 0.9);

    --pill-base:      linear-gradient(#FFFFFF, #F3EEFD);
    --pill-avatar:    linear-gradient(#FFFFFF, #F0E9FC);
    --pill-flat:      linear-gradient(#FFFFFF, #FFFFFF);
    --pill-soft:      linear-gradient(#FFFFFF, #F4F0FE);

    --sky-wash:       #E1D6F8;
    --sky-violet:     rgba(139, 92, 246, 0.10);
    --sky-pink:       rgba(236, 72, 153, 0.09);
    --aurora-violet:  rgba(167, 139, 250, 0.20);
    --aurora-pink:    rgba(244, 114, 182, 0.16);
    --aurora-blue:    rgba(125, 211, 252, 0.20);
    --star-opacity:   0;
    --moon-face:      radial-gradient(circle at 34% 32%, #FFFFFF 0%, #F7F0FF 30%, #DCCBF6 66%, #B49CE6 100%);
    --moon-glow:      0 0 2.2rem rgba(255, 255, 255, 0.55),
                      0 0 5rem rgba(167, 139, 250, 0.28),
                      inset -1.1rem -1.1rem 2.8rem rgba(116, 92, 176, 0.30);
    --grain-opacity:  0.04;
}

/* White on the brand gradient stays white - it sits on color, not on paper. */
html[data-mh="light"] .btn--primary { color: #fff; }

/* The grain is an overlay blend; on a light ground that adds dirt rather than
   texture, and multiply keeps it as a paper tooth instead. */
html[data-mh="light"] .sky__grain { mix-blend-mode: multiply; }

/* The big outline numeral behind each step is the brand gradient at low
   opacity. On black that is a faint glow; on paper the same value is ink. */
html[data-mh="light"] .step::after { opacity: 0.16; }

/* The scrollbar is a neon bar against black. On paper it is the loudest
   thing on the page. */
html[data-mh="light"] ::-webkit-scrollbar-thumb { background: linear-gradient(180deg, #8B5CF6, #C026A3); }
html[data-mh="light"] ::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, #7C3AED, #A21C87); }

/* Tooltips keep their dark sheet in both themes - a small dark chip over
   paper reads better than a pale one, and it matches the store. */
html[data-mh="light"] [data-tip]::after { color: #fff; }

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

* { margin: 0; padding: 0; }

html {
    /* The page is dark by design. Without saying so, Chrome's automatic dark
       mode treats it as a light page and darkens it a second time, which
       flattens the whole backdrop to grey. */
    color-scheme: dark;
    /* Reserve the scrollbar channel on every page. Without it a short page
       has no scrollbar and a long one does, so the whole layout jumps
       sideways by the scrollbar width as you move between them - the other
       way a multi-page site shifts on a click. */
    scrollbar-gutter: stable;
    /* The night color lives here, on the root, so it is what paints the
       canvas. Body is left transparent so the fixed sky layer above the
       canvas stays visible - see the note on body below. Painting it here
       also means a page always has night behind it, never bare grey. */
    background: var(--ink);
    scroll-behavior: smooth;
    /* Anchored jumps clear the fixed navigation bar. */
    scroll-padding-top: 6.5rem;
    -webkit-text-size-adjust: 100%;
}

body {
    min-height: 100%;
    font-family: 'Outfit', system-ui, -apple-system, sans-serif;
    font-size: clamp(1rem, 0.95rem + 0.2vw, 1.125rem);
    line-height: 1.65;
    color: var(--text);
    /* Transparent on purpose. <html> carries the night color and paints the
       canvas; the moment <body> paints one too it becomes an opaque box at
       z-index 0, sitting directly on top of the z-index:-1 sky and hiding
       the aurora, the moon and the whole starfield. */
    background: transparent;
    overflow-x: hidden;
    overflow-wrap: break-word;
    -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }

::selection { background: rgba(236, 72, 153, 0.35); color: #fff; }

/* Gradient scrollbar, as in the in-game menus. */
::-webkit-scrollbar { width: 0.65rem; }
::-webkit-scrollbar-track { background: rgba(var(--w), 0.03); }
::-webkit-scrollbar-thumb {
    border-radius: 999px;
    background: linear-gradient(180deg, #8B5CF6, #EC4899);
}
::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, #A78BFA, #F472B6); }

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 0.4rem;
}

.skip {
    position: fixed;
    top: -100%;
    left: 50%;
    transform: translateX(-50%);
    z-index: 200;
    padding: 0.75rem 1.5rem;
    background: var(--ink-2);
    border: 1px solid var(--panel-border-2);
    border-radius: 0 0 var(--r-md) var(--r-md);
}
.skip:focus { top: 0; }

/* ==================================================================
   2. Sky backdrop
   ================================================================== */

.sky {
    position: fixed;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
    background:
        radial-gradient(120% 80% at 50% -10%, var(--sky-wash) 0%, transparent 60%),
        radial-gradient(90% 60% at 85% 15%, var(--sky-violet) 0%, transparent 65%),
        radial-gradient(70% 50% at 10% 80%, var(--sky-pink) 0%, transparent 70%),
        var(--ink);
}

/* Slow aurora wash. Two oversized blurred blobs drifting past each other. */
.sky__aurora {
    position: absolute;
    inset: -30%;
    background:
        radial-gradient(35% 45% at 25% 30%, var(--aurora-violet), transparent 70%),
        radial-gradient(30% 40% at 75% 60%, var(--aurora-pink), transparent 70%),
        radial-gradient(28% 34% at 55% 85%, var(--aurora-blue), transparent 70%);
    filter: blur(60px);
    /* No will-change. An infinite transform animation is promoted by the
       browser anyway, and a permanently promoted layer carrying a 60px blur
       is one of the things that makes the compositor start dropping work. */
    animation: auroraDrift 42s var(--ease) infinite alternate;
}

@keyframes auroraDrift {
    0%   { transform: translate3d(-3%, 0, 0) scale(1); }
    50%  { transform: translate3d(3%, 2%, 0) scale(1.08); }
    100% { transform: translate3d(-2%, -3%, 0) scale(1.03); }
}

.sky__stars {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: var(--star-opacity);
    transition: opacity 0.6s var(--ease);
}

/* The moon. A soft disc with a rim light and a wide halo - the logo's crescent
   is the mark, this is the atmosphere behind it. */
.moon {
    z-index: -1;
    position: absolute;
    top: clamp(4rem, 12vh, 10rem);
    right: clamp(3rem, 12vw, 14rem);
    width: clamp(7rem, 14vw, 15rem);
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--moon-face);
    /* A crescent, cut the way the logo's is: a second disc punched out of
       the top right, leaving the lit edge opening to the lower left. A
       straight-edged terminator would be the astronomically correct half
       moon and the wrong shape for this brand. */
    -webkit-mask-image: radial-gradient(circle at 68% 30%, transparent 0 46%, #000 47%);
            mask-image: radial-gradient(circle at 68% 30%, transparent 0 46%, #000 47%);
    /* drop-shadow, not box-shadow: a box-shadow is cast by the border box and
       would ring the whole circle, including the half that is no longer
       there. A filter follows the masked shape. */
    filter: drop-shadow(0 0 2.2rem rgba(167, 139, 250, 0.5))
            drop-shadow(0 0 5rem rgba(236, 72, 153, 0.22));
    animation: moonFloat 18s ease-in-out infinite;
}

@keyframes moonFloat {
    0%, 100% { transform: translateY(0) }
    50%      { transform: translateY(-1.4rem) }
}

/* Motes: slow specks rising through the whole page, carried over from the
   shop so both sites move the same way. The star field twinkles in place and
   this drifts - together they read as depth rather than as one flat layer.

   Each mote is one element with its own position, size, drift and duration set
   inline, so the field never looks like a repeating pattern. Twelve is enough
   at this size; more only costs frames. */
.sky__motes { position: absolute; inset: 0; overflow: hidden; }

.mote {
    position: absolute;
    border-radius: 50%;
    background: var(--mote, rgba(var(--w), 0.75));
    opacity: 0;
    animation: moteRise var(--dur, 26s) linear var(--delay, 0s) infinite;
    will-change: transform, opacity;
}
@keyframes moteRise {
    0%   { transform: translate3d(0, 12vh, 0) scale(0.6); opacity: 0; }
    12%  { opacity: 0.75; }
    82%  { opacity: 0.55; }
    100% { transform: translate3d(var(--drift, 3vw), -108vh, 0) scale(1); opacity: 0; }
}
/* White specks on a pale page are dirt, the same reason the stars go out. */
html[data-mh="light"] .sky__motes { display: none; }

/* Film grain. Kills the banding that big soft gradients produce on cheap
   panels and gives the whole page a little texture. */
.sky__grain {
    position: absolute;
    inset: 0;
    /* Deliberately NO mix-blend-mode. Overlay is only a request; when the
       compositor declines it the element falls back to normal blending, and
       mid-grey noise over a near-black page is a flat grey wash across the
       whole site. Low opacity kills the gradient banding just as well and
       cannot fail into something ugly. */
    opacity: var(--grain-opacity);
    pointer-events: none;
    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'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ==================================================================
   3. Layout primitives
   ================================================================== */

.wrap {
    width: 100%;
    max-width: var(--wrap);
    margin-inline: auto;
    padding-inline: var(--gutter);
}

section { position: relative; }

.section { padding-block: var(--section-y); }

.section__head {
    max-width: 46rem;
    margin-inline: auto;
    margin-bottom: clamp(2.5rem, 6vh, 4rem);
    text-align: center;
}

/* Everything that wears the brand gradient as its border. One recipe, the same
   one the avatar bubbles have always used: the fill paints to the padding box,
   the gradient to the border box, and the border itself is transparent so the
   gradient IS the edge. Each element only has to name its own fill. */
/* .btn.btn--ghost, not .btn--ghost: `.btn { border: 0 }` is further down the
   file and would win the tie, leaving no border area for the gradient to paint
   into - the rim simply vanished. */
.btn.btn--ghost,
.chip,
.status-chip,
.social {
    border: 1.5px solid transparent;
    background: var(--pill-base) padding-box, var(--grad-brand) border-box;
}

.glass {
    position: relative;
    background:
        linear-gradient(180deg, rgba(var(--w), 0.055), rgba(var(--w), 0.015)),
        var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: var(--r-lg);
    box-shadow: var(--panel-shadow);
    /* No backdrop-filter, deliberately. A blurred backdrop cannot be sampled
       while the element's transform is animating - Chrome puts it on its own
       compositing layer and the filter has nothing to read - so the card slid
       in unfrosted and then snapped to frosted the instant the slide landed.
       That jump is the thing being removed. A plain translucent tint looks
       the same at every point of the animation, and the see-through look is
       the one that was wanted anyway. */
}

/* The inner rim that gives the glass its edge. Restated per element rather
   than inherited, so children never paint over it. */
.glass::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    box-shadow: inset 0 1px 0 rgba(var(--w), 0.12);
}

/* ==================================================================
   4. Typography
   ================================================================== */

.cursive {
    font-family: 'Alex Brush', cursive;
    font-weight: 400;
    /* Alex Brush sits small and low next to Outfit - lift and enlarge it so
       the two read as one word. */
    font-size: 1.35em;
    line-height: 0;
    padding-right: 0.04em;
}

.h1, .h2, .h3 {
    font-weight: 700;
    line-height: 1.12;
    letter-spacing: -0.02em;
}

.h1 { font-size: clamp(2.5rem, 1.6rem + 4.2vw, 5.25rem); }
.h2 { font-size: clamp(1.9rem, 1.3rem + 2.4vw, 3.25rem); }
.h3 { font-size: clamp(1.15rem, 1rem + 0.6vw, 1.5rem); font-weight: 600; }

/* The animated brand gradient. Every heading carries it now, plus the two
   brand names, so a title never has to remember to wrap itself in .grad
   (owner, 2026-08-24). The .grad class stays for the pieces that are not
   headings - stat numbers, and any span that wants it on its own. */
/* Team names are marked up as paragraphs, not headings - they are labels on a
   card, not document structure - so they have to be named here to get the
   gradient the headings get (owner, 2026-08-26). */
.grad,
h1, h2, h3,
.h1, .h2, .h3,
.brand__name,
.foot__name,
.maker__name,
.member__name {
    background: var(--grad-brand-4);
    background-size: 300% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    animation: titleGradientShift 9s linear infinite;
}

@keyframes titleGradientShift {
    to { background-position: 300% 0; }
}

.lede {
    font-size: clamp(1.05rem, 1rem + 0.4vw, 1.3rem);
    color: var(--text-muted);
    font-weight: 300;
}

.eyebrow {
    display: inline-block;
    margin-bottom: 1rem;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--accent);
}

.muted { color: var(--text-muted); }

/* ==================================================================
   5. Buttons & chips
   ================================================================== */

.btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    padding: 0.9rem 2rem;
    border: 0;
    border-radius: 999px;
    overflow: hidden;
    font: inherit;
    font-weight: 600;
    font-size: 1rem;
    cursor: pointer;
    white-space: nowrap;
    transition: transform var(--speed) var(--ease),
                box-shadow var(--speed) var(--ease),
                background-color var(--speed) var(--ease),
                border-color var(--speed) var(--ease);
}

/* The same pointer-tracked glow the cards use, at button scale. main.js feeds
   --mx / --my; without JS the light simply sits in the middle and the button
   still behaves. */
.btn::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s var(--ease);
    background: radial-gradient(
        5.5rem circle at var(--mx, 50%) var(--my, 50%),
        var(--btn-glow),
        transparent 60%
    );
}
.btn:hover::before { opacity: 1; }

/* A ghost button sits on the dark glass, so white would wash it out - it
   takes the brand purple instead. */
.btn--ghost::before {
    background: radial-gradient(
        6.5rem circle at var(--mx, 50%) var(--my, 50%),
        rgba(167, 139, 250, 0.32),
        transparent 62%
    );
}

/* A small button is a smaller surface, so the light gets tighter with it. */
.btn--sm { padding: 0.6rem 1.35rem; font-size: 0.92rem; }
.btn--sm::before {
    background: radial-gradient(
        3.5rem circle at var(--mx, 50%) var(--my, 50%),
        var(--btn-glow),
        transparent 60%
    );
}
.btn--ghost.btn--sm::before {
    background: radial-gradient(
        4rem circle at var(--mx, 50%) var(--my, 50%),
        rgba(167, 139, 250, 0.32),
        transparent 62%
    );
}

.btn--primary {
    position: relative;
    color: #fff;
    border: 1.5px solid transparent;
    /* No rim on the filled button - a lighter tint of its own fill just reads
       as a white outline. The border stays 1.5px and transparent so this
       button is exactly as tall as the outlined ones beside it, and
       background-origin: border-box makes the gradient span the border ring
       too. Without that the ring shows the gradient REPEATING, and the tail
       of it is pink - which looks like a pink border and is not one. */
    background: linear-gradient(120deg, #8B5CF6, #EC4899);
    background-origin: border-box;
    background-size: 200% 100%;
    box-shadow: 0 0.6rem 1.6rem rgba(139, 92, 246, 0.35);
}
.btn--primary:hover {
    background-position: 100% 0;
    transform: translateY(-2px);
    box-shadow: 0 0.9rem 2.2rem rgba(236, 72, 153, 0.45);
}
.btn--primary:active { transform: translateY(0); }

/* Sheen that sweeps across on hover and back again on the way out. A
   transition rather than a keyframe animation: an animation only plays
   forwards, and when :hover ends it is simply removed, so the light snapped
   off instead of retreating. A transition runs in both directions. At rest
   and at the end of the sweep the band sits outside the button, so it needs
   no opacity of its own. */
.btn--primary::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: linear-gradient(105deg, transparent 35%, rgba(var(--w), 0.35) 50%, transparent 65%);
    background-size: 250% 100%;
    background-position: 150% 0;
    pointer-events: none;
    transition: background-position 0.85s var(--ease-out);
}
.btn--primary:hover::after { background-position: -80% 0; }

/* Fill and rim come from the gradient-border rule up top. Hover lifts the fill
   a touch; the gradient layer has to be restated or the shorthand drops it. */
.btn--ghost { color: var(--text); }
.btn.btn--ghost:hover {
    background:
        linear-gradient(rgba(var(--w), 0.06), rgba(var(--w), 0.06)) padding-box,
        var(--pill-base) padding-box,
        var(--grad-brand) border-box;
    transform: translateY(-2px);
}

.btn-row {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    justify-content: center;
    margin-top: 2rem;
}

.chip {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.4rem 0.95rem;
    border-radius: 999px;
    font-size: 0.85rem;
    font-weight: 500;
    color: var(--text-muted);
    transition: color var(--speed) var(--ease),
                border-color var(--speed) var(--ease),
                background-color var(--speed) var(--ease);
}
a.chip:hover {
    color: var(--text);
    background:
        linear-gradient(rgba(167, 139, 250, 0.14), rgba(167, 139, 250, 0.14)) padding-box,
        var(--pill-base) padding-box,
        var(--grad-brand) border-box;
}

/* ==================================================================
   6. Navigation
   ================================================================== */

.nav {
    position: fixed;
    inset: 0 0 auto 0;
    z-index: 100;
    /* A sheet you can see the page move behind, from the first pixel - not a
       bar that appears once you scroll. The footer is the same material. */
    background: var(--sheet);
    border-bottom: 1px solid var(--panel-border);
    backdrop-filter: var(--sheet-blur);
    -webkit-backdrop-filter: var(--sheet-blur);
    box-shadow: inset 0 -1px 0 rgba(var(--w), 0.05);
    transition: background 0.35s var(--ease), box-shadow 0.35s var(--ease);
}

/* Deliberately no .is-stuck appearance. The sheet is the same whether you
   have scrolled or not - the bar should not change under you. The class is
   still set by the script; nothing styles it any more. */

.nav__inner {
    width: 100%;
    /* Deliberately NOT --wrap. That is the measure for reading a column of
       text; this bar has to hold a wordmark, six links, the status chip and
       a button, and squeezing all of it into a reading width is what pushed
       the first link on top of the title. */
    max-width: 94rem;
    margin-inline: auto;
    padding: 0.85rem var(--gutter);
    display: flex;
    align-items: center;
    gap: clamp(0.9rem, 2vw, 1.75rem);
}

.brand {
    display: flex;
    align-items: center;
    gap: 0.85rem;
}

.brand__logo {
    width: clamp(2.6rem, 3.4vw, 3.25rem);
    height: auto;
    filter: drop-shadow(0 0 0.7rem var(--glow-purple));
    transition: transform 0.5s var(--ease-out);
}
.brand:hover .brand__logo { transform: rotate(-6deg) scale(1.06); }

.brand__text { display: flex; flex-direction: column; line-height: 1.15; }

/* No margin-right: auto any more - the links centre themselves instead of
   being shoved against the right edge. */
.brand { flex: none; }

/* The six links as one group, centred in whatever space is left between the
   brand and the status chip. On phones it dissolves into the column with
   display: contents, so the stacked menu is unaffected. */
.nav__set {
    display: flex;
    align-items: center;
    gap: clamp(0.6rem, 1.4vw, 1.6rem);
    margin-inline: auto;
}

.brand__name {
    font-size: clamp(1.15rem, 1rem + 0.55vw, 1.5rem);
    font-weight: 700;
    letter-spacing: -0.01em;
    white-space: nowrap;
}

.brand__tag {
    font-size: 0.68rem;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-dim);
    /* One line. It is a strapline under a name, and broken across two rows it
       stops reading as one. */
    white-space: nowrap;
}

.nav__links {
    flex: 1;
    display: flex;
    align-items: center;
    gap: clamp(0.6rem, 1.4vw, 1.6rem);
}
.nav__links a:not(.btn):not(.status-chip) { white-space: nowrap; }

/* Icons in both menus, treated the way the store treats them: dim until you
   go near them, then brand pink with a light behind. A thin glyph at this
   size reads as a hairline, so it gets a glow rather than a heavier weight -
   the thinness is the point. */
.nav__links a:not(.btn):not(.status-chip) > i {
    /* Fixed box, not glyph-sized. Font Awesome is a webfont: before it loads
       the glyph is zero-wide, and when it lands every label shifts right to
       make room. Reserving the space means nothing moves. */
    flex: none;
    width: 1.05rem;
    text-align: center;
    color: var(--text-dim);
    font-size: 0.92em;
    transition: color var(--speed) var(--ease), filter var(--speed) var(--ease);
}
.nav__links a:not(.btn):not(.status-chip):hover > i,
.nav__links a.is-active:not(.btn):not(.status-chip) > i {
    color: var(--secondary);
    filter: drop-shadow(0 0 0.45rem rgba(236, 72, 153, 0.6));
}

.nav__links a:not(.btn):not(.status-chip) {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.42rem;
    padding: 0.4rem 0.15rem;
    font-size: 0.98rem;
    font-weight: 500;
    color: var(--text-muted);
    transition: color var(--speed) var(--ease);
}
.nav__links a:not(.btn):not(.status-chip)::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 2px;
    border-radius: 2px;
    background: var(--grad-brand);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--speed) var(--ease-out);
}
/* The :not() chain above carries its own specificity, so the active rules must
   repeat it - written as a bare `.is-active` they lose to the base rule and the
   underline stays collapsed on the page you are actually on. */
.nav__links a:not(.btn):not(.status-chip):hover,
.nav__links a.is-active:not(.btn):not(.status-chip) { color: var(--text); }

.nav__links a:not(.btn):not(.status-chip):hover::after,
.nav__links a.is-active:not(.btn):not(.status-chip)::after { transform: scaleX(1); }

.nav__toggle {
    display: none;
    flex-direction: column;
    justify-content: center;
    gap: 0.32rem;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0.6rem;
    border: 1px solid var(--panel-border);
    border-radius: var(--r-sm);
    background: rgba(var(--w), 0.04);
    cursor: pointer;
}
.nav__toggle span {
    display: block;
    height: 2px;
    border-radius: 2px;
    background: var(--text);
    transition: transform 0.3s var(--ease), opacity 0.2s var(--ease);
}
.nav__toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(0.42rem) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-0.42rem) rotate(-45deg); }


/* Readable by a screen reader, invisible on screen. Not display:none -
   that would hide it from the reader too, which is the whole point of it. */
.sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* ---- Theme switch ----
   Two of them, one per breakpoint: last in the bar on a wide screen, beside
   the menu button on a narrow one. Only ever one is on screen and both are
   wired to the same handler, so they cannot disagree. */
.themeswitch {
    position: relative;
    display: inline-grid;
    place-items: center;
    width: 2.4rem;
    height: 2.4rem;
    flex: none;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    background: rgba(var(--w), 0.05);
    color: var(--text-muted);
    font-size: 0.95rem;
    cursor: pointer;
    transition: color var(--speed) var(--ease), border-color var(--speed) var(--ease),
                background-color var(--speed) var(--ease), transform var(--speed) var(--ease);
}
.themeswitch:hover {
    color: var(--text);
    border-color: var(--secondary);
    background: rgba(var(--w), 0.10);
    transform: translateY(-1px);
}
.themeswitch i {
    grid-area: 1 / 1;
    transition: opacity 0.25s var(--ease), transform 0.4s var(--ease-out);
}
.themeswitch .fa-sun { opacity: 0; transform: rotate(-90deg) scale(0.6); }
html[data-mh="light"] .themeswitch .fa-moon { opacity: 0; transform: rotate(90deg) scale(0.6); }
html[data-mh="light"] .themeswitch .fa-sun { opacity: 1; transform: none; }

.themeswitch--mast { display: none; }

/* ---- Live status chip ---- */

.status-chip {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.42rem 0.9rem;
    border-radius: 999px;
    font-size: 0.85rem;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    white-space: nowrap;
    /* Reserved width, and this is not cosmetic. The links are right-aligned
       as a group (.brand takes margin-right: auto), so the group's left edge
       is decided by the total width of everything after it. This chip starts
       as 'Checking...' and a second later becomes 'Offline', 'Status soon'
       or a player count - and every one of those resized the chip and slid
       the whole menu sideways. Wide enough here for the longest of them, so
       the text can change without anything moving. */
    min-width: 9.75rem;
    justify-content: center;
    transition: border-color var(--speed) var(--ease), color var(--speed) var(--ease);
}
.status-chip:hover { color: var(--text); }

.status-chip__dot {
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    background: var(--text-dim);
    flex: none;
}

.status-chip.is-online { color: var(--text); border-color: rgba(74, 222, 128, 0.35); }
.status-chip.is-online .status-chip__dot {
    background: #4ADE80;
    box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.6);
    animation: pulseDot 2.4s var(--ease) infinite;
}
@keyframes pulseDot {
    70%  { box-shadow: 0 0 0 0.55rem rgba(74, 222, 128, 0); }
    100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
}

.status-chip.is-offline { border-color: rgba(248, 113, 113, 0.3); }
.status-chip.is-offline .status-chip__dot { background: #F87171; }

/* ==================================================================
   7. Hero
   ================================================================== */

.hero {
    position: relative;
    min-height: min(100svh, 60rem);
    display: grid;
    place-items: center;
    padding-block: clamp(9rem, 18vh, 12rem) clamp(4rem, 10vh, 7rem);
    text-align: center;
}

.hero__inner {
    width: 100%;
    max-width: 58rem;
    margin-inline: auto;
    padding-inline: var(--gutter);
}

.hero__logo {
    width: clamp(6.5rem, 13vw, 10.5rem);
    margin: 0 auto 1.75rem;
    filter: drop-shadow(0 0 1.4rem var(--glow-purple))
            drop-shadow(0 0 3rem var(--glow-pink));
    animation: logoPulse 6s ease-in-out infinite;
}
@keyframes logoPulse {
    0%, 100% { transform: scale(1);    filter: drop-shadow(0 0 1.4rem var(--glow-purple)) drop-shadow(0 0 3rem var(--glow-pink)); }
    50%      { transform: scale(1.035); filter: drop-shadow(0 0 2rem var(--glow-purple)) drop-shadow(0 0 4.2rem var(--glow-pink)); }
}

.hero__title { margin-bottom: 1.25rem; }

.hero__lede {
    max-width: 40rem;
    margin: 0 auto;
}

.hero__meta {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.6rem;
    margin-top: 2rem;
}

/* Scroll hint at the bottom of the hero. */
/* A button, not a label: it was only ever a hint, and a hint you can press
   is strictly better. Kept looking exactly as it did - no border, no fill -
   so it still reads as a cue rather than as a control competing with the two
   real buttons above it. */
.hero__scroll {
    position: absolute;
    left: 50%;
    bottom: clamp(1.5rem, 4vh, 3rem);
    transform: translateX(-50%);
    display: grid;
    justify-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0.9rem;
    border: 0;
    background: none;
    font: inherit;
    font-size: 0.72rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--text-dim);
    cursor: pointer;
    transition: color var(--speed) var(--ease);
}
.hero__scroll:hover { color: var(--text); }
.hero__scroll:hover span { background: linear-gradient(180deg, var(--secondary), transparent); }
.hero__scroll span {
    display: block;
    width: 1px;
    height: 2.75rem;
    background: linear-gradient(180deg, var(--accent), transparent);
    animation: scrollHint 2.2s var(--ease) infinite;
    transform-origin: top;
}
@keyframes scrollHint {
    0%       { transform: scaleY(0);   opacity: 0; }
    35%      { transform: scaleY(1);   opacity: 1; }
    100%     { transform: scaleY(1);   opacity: 0; transform-origin: bottom; }
}


/* ---- Masthead banner ----
   A full-bleed strip of the city under the header, the same shot the
   loadscreen opens on, so arriving at the site looks like arriving at the
   server. It sits BEHIND the fixed header, which is why the header's own
   sheet is translucent - you see the city moving under the menu. */
.banner-head {
    position: relative;
    height: clamp(11rem, 26vh, 20rem);
    margin-bottom: calc(var(--section-y) * -0.35);
    overflow: hidden;
}
.banner-head img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Anchored low: the interesting half of the shot is the skyline, and a
       centred crop puts the header over it. */
    object-position: 50% 62%;
}
/* Fades into the page instead of ending on a hard edge, and darkens under the
   header so the menu stays readable over a bright part of the photograph. */
.banner-head::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
        linear-gradient(180deg, rgba(var(--k), 0.72) 0%, rgba(var(--k), 0.12) 42%, var(--ink) 100%),
        radial-gradient(70% 90% at 50% 0%, rgba(139, 92, 246, 0.22), transparent 70%);
}
html[data-mh="light"] .banner-head::after {
    background:
        linear-gradient(180deg, rgba(20, 14, 34, 0.55) 0%, rgba(20, 14, 34, 0.10) 42%, var(--ink) 100%),
        radial-gradient(70% 90% at 50% 0%, rgba(124, 58, 237, 0.18), transparent 70%);
}

/* ---- Inner pages: the title stands on the picture too ----
   The strip was a band of city with the heading underneath it. Taller now,
   and the page header sits ON it, so every page opens the way the home page
   does. Same ordering trick: the picture goes behind .sky, and .sky gives up
   its opaque fill, so the aurora, the moon and the grain keep painting over
   the photograph rather than being buried by it. */
body.has-banner:not([data-page="home"]) .banner-head {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: clamp(22rem, 46vh, 32rem);
    margin-bottom: 0;
    z-index: -2;
}
body.has-banner:not([data-page="home"]) .sky { background: none; }

body.has-banner:not([data-page="home"]) .pagehead {
    position: relative;
    z-index: 1;
    /* Exactly as tall as the picture behind it, with the heading centred in
       what is left after the fixed header. That is what makes every page
       start its content at the same height: pages whose lede wraps to two
       lines used to push their first section further down than pages whose
       lede fits on one, so Features and Team began in different places. */
    min-height: clamp(22rem, 46vh, 32rem);
    display: grid;
    align-content: center;
    padding-block: clamp(5rem, 11vh, 7rem) clamp(2rem, 5vh, 3rem);
}

/* ---- Home: the picture is the whole first screen ----
   The strip becomes a full-height backdrop with the entire hero standing on
   it - mark, name, lede, buttons, chips and the scroll cue.

   It goes BEHIND .sky rather than in front of it, and .sky gives up its own
   opaque fill here so the photograph shows through. That ordering is the
   whole trick: the aurora, the drifting moon and the grain are children of
   .sky, so they keep painting over the picture instead of being buried by
   it. <html> still carries the night colour, so the page below the fold is
   unaffected by the sky going transparent. */
body[data-page="home"] .banner-head {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 100svh;
    margin-bottom: 0;
    z-index: -2;
}
body[data-page="home"] .sky { background: none; }

/* Lighter over the picture than the strip version: the hero's own text needs
   to read, but burying the city defeats the point of putting it there. */
body[data-page="home"] .banner-head::after {
    background:
        linear-gradient(180deg, rgba(var(--k), 0.62) 0%, rgba(var(--k), 0.28) 38%, rgba(var(--k), 0.55) 78%, var(--ink) 100%),
        radial-gradient(75% 60% at 50% 40%, rgba(139, 92, 246, 0.20), transparent 72%);
}
html[data-mh="light"] body[data-page="home"] .banner-head::after {
    background:
        linear-gradient(180deg, rgba(20, 14, 34, 0.42) 0%, rgba(20, 14, 34, 0.16) 38%, rgba(244, 242, 250, 0.55) 80%, var(--ink) 100%),
        radial-gradient(75% 60% at 50% 40%, rgba(124, 58, 237, 0.16), transparent 72%);
}

/* A full screen, and the hero already centres itself inside whatever it is
   given. The top padding clears the fixed header. */
body[data-page="home"] .hero {
    position: relative;
    z-index: 1;
    min-height: 100svh;
    padding-block: clamp(7rem, 16vh, 10rem) clamp(4rem, 9vh, 6rem);
}

/* Over a photograph the title had nothing to separate it from the city
   behind. text-shadow cannot help here - the letters are a clipped gradient
   with a transparent fill, so there is no glyph colour to cast a shadow from.
   drop-shadow works on the rendered shape instead, which is why it is a
   filter and not a shadow. */
body[data-page="home"] .hero__title,
body.has-banner:not([data-page="home"]) .pagehead h1 {
    filter: drop-shadow(0 0 1.4rem rgba(167, 139, 250, 0.55))
            drop-shadow(0 0 3.5rem rgba(236, 72, 153, 0.35));
}

/* The lede is ordinary text, so it can take a real shadow - and it gets the
   full text colour rather than the muted one, because muted grey over a busy
   photograph is the thing that was hard to read. */
body[data-page="home"] .hero__lede,
body.has-banner:not([data-page="home"]) .pagehead .lede {
    color: var(--text);
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.65), 0 0 1.5rem rgba(0, 0, 0, 0.55);
}
/* The eyebrow is small and coloured, which is the worst combination over a
   photograph, so it gets the shadow as well. */
body.has-banner:not([data-page="home"]) .pagehead .eyebrow {
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
html[data-mh="light"] body[data-page="home"] .hero__lede,
html[data-mh="light"] body.has-banner:not([data-page="home"]) .pagehead .lede {
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 1.5rem rgba(0, 0, 0, 0.4);
    color: #fff;
}

/* And a soft pool of shade under the whole block, so every part of it has
   something to sit on wherever the photograph happens to be bright. */
body[data-page="home"] .hero__inner,
body.has-banner:not([data-page="home"]) .pagehead .wrap { position: relative; }
body[data-page="home"] .hero__inner::before,
body.has-banner:not([data-page="home"]) .pagehead .wrap::before {
    content: '';
    position: absolute;
    inset: -10% -8%;
    z-index: -1;
    pointer-events: none;
    background: radial-gradient(58% 54% at 50% 50%, rgba(0, 0, 0, 0.55), transparent 76%);
}

/* Every other page keeps the strip. */
body.has-banner:not([data-page="home"]) .hero { padding-block: clamp(3rem, 8vh, 6rem) clamp(4rem, 10vh, 7rem); }

/* ---- Page header, for every page that is not home ---- */

.pagehead {
    padding-block: clamp(9rem, 20vh, 13rem) clamp(2rem, 5vh, 3.5rem);
    text-align: center;
}
.pagehead .lede { max-width: 42rem; margin-inline: auto; margin-top: 1rem; }

/* ==================================================================
   8. Stats strip
   ================================================================== */

.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: clamp(0.75rem, 2vw, 1.25rem);
    padding: clamp(1.5rem, 3vw, 2.25rem);
    text-align: center;
}

.stat__value {
    display: block;
    font-size: clamp(1.9rem, 1.4rem + 1.9vw, 3rem);
    font-weight: 700;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

.stat__label {
    display: block;
    margin-top: 0.35rem;
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--text-dim);
}

/* ==================================================================
   9. Cards
   ================================================================== */

.grid {
    display: grid;
    gap: clamp(1rem, 2.2vw, 1.6rem);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
}

.card {
    position: relative;
    padding: clamp(1.5rem, 2.6vw, 2rem);
    border-radius: var(--r-lg);
    overflow: hidden;
    transition: transform 0.45s var(--ease-out),
                box-shadow 0.45s var(--ease-out),
                border-color 0.45s var(--ease-out);
}

/* Cards do not react to the pointer (owner, 2026-08-26). Hover states belong
   to buttons and links here - things you can press. A panel that lifts,
   brightens its edge and lights up under the cursor reads as pressable when
   it is not, and with a page full of them the whole surface twitches as the
   mouse crosses it. The transitions stay declared: nothing animates while
   there is no second state to animate to, and they are what a theme switch
   fades through. */


/* Font Awesome glyphs take currentColor, so the icon finally tints with the
   brand accent instead of sitting there as a full-color emoji. */
.card__icon i, .rule__icon i, .chip i { color: var(--accent); }

/* Every mention of LGBTQ+, and the flag beside it. Works on a Font Awesome
   glyph as well as on text - both are painted type, so both take a clipped
   background. Listed with the icon contexts as well as bare, or the colour
   rule above would win where it is more specific. */
.rainbow,
.card__icon i.rainbow,
.chip i.rainbow {
    background: linear-gradient(95deg, #F2685E 0%, #F5B057 22%, #E8CF4A 42%, #63C97F 62%, #5E9CE8 80%, #B681DE 100%);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
}
.chip i { font-size: 0.9em; margin-right: 0.1rem; }

.card__icon {
    display: grid;
    place-items: center;
    width: 3.4rem;
    height: 3.4rem;
    margin-bottom: 1.15rem;
    font-size: 1.35rem;
    line-height: 1;
    border-radius: 50%;
    /* Gradient ring: the fill paints to the padding box, the ring to the
       border box, so the border itself carries the brand gradient. */
    background:
        var(--pill-base) padding-box,
        var(--grad-brand) border-box;
    border: 1.5px solid transparent;
    box-shadow: 0 0 1.5rem rgba(139, 92, 246, 0.25);
    transition: transform 0.45s var(--ease-out), box-shadow 0.45s var(--ease-out);
}

.card__title { margin-bottom: 0.55rem; }

.card__body {
    font-size: 0.98rem;
    font-weight: 300;
    color: var(--text-muted);
}

/* ---- Feature group headings on the Features page ---- */

.group { margin-top: clamp(3rem, 7vh, 5rem); }
.group:first-of-type { margin-top: 0; }

.group__head {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    margin-bottom: clamp(1.25rem, 3vh, 2rem);
}
.group__head h2 {
    font-size: clamp(1.35rem, 1.1rem + 1vw, 1.9rem);
    font-weight: 600;
    white-space: nowrap;
}
.group__head::after {
    content: '';
    flex: 1;
    height: 1px;
    background: linear-gradient(90deg, var(--panel-border-2), transparent);
}

/* ==================================================================
   10. Rules
   ================================================================== */

.rules { display: grid; gap: clamp(1rem, 2vw, 1.5rem); }

.rule {
    padding: clamp(1.5rem, 3vw, 2.25rem);
    border-radius: var(--r-lg);
}

.rule__head {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1.25rem;
    padding-bottom: 1.25rem;
    border-bottom: 1px solid var(--panel-border);
}

.rule__icon {
    display: grid;
    place-items: center;
    width: 2.9rem;
    height: 2.9rem;
    flex: none;
    font-size: 1.3rem;
    border-radius: 50%;
    background:
        var(--pill-base) padding-box,
        var(--grad-brand) border-box;
    border: 1.5px solid transparent;
}

.rule ul { list-style: none; display: grid; gap: 0.85rem; }

.rule li {
    position: relative;
    padding-left: 1.85rem;
    font-weight: 300;
    color: var(--text-muted);
}
.rule li::before {
    content: '';
    position: absolute;
    left: 0.35rem;
    top: 0.72em;
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: var(--grad-brand);
    box-shadow: 0 0 0.5rem var(--glow-purple);
}

/* ==================================================================
   11. Join steps & FAQ
   ================================================================== */

/* One row, whatever the count. `auto-fit` with a 17rem minimum was fitting
   three across and dropping the fourth onto its own line on the Join page,
   which read as a step apart from the others. Flowing into columns instead
   keeps every step on one line and lets them share the width equally - three
   on the home page, four on Join, and any number later without a new rule. */
.steps {
    display: grid;
    gap: clamp(1rem, 2.2vw, 1.5rem);
    counter-reset: step;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
}
/* Below this a four-across row would be columns of two words, so it goes back
   to wrapping rows. */
@media (max-width: 68rem) {
    .steps { grid-auto-flow: row; grid-auto-columns: auto; grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 34rem) {
    .steps { grid-template-columns: 1fr; }
}

/* A column, so the button can be pushed to the bottom. The cards are grid
   items and already stretch to the tallest in the row, so once each button
   sits at the bottom of its own card they all line up - however much or little
   text sits above them. */
.step {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: clamp(1.5rem, 2.6vw, 2rem);
    padding-top: 2.75rem;
    border-radius: var(--r-lg);
    counter-increment: step;
}
/* align-items: flex-start above keeps the button its own width rather than
   stretching it across the card; the text still needs the full width back. */
.step h3, .step p { align-self: stretch; }

.step::before {
    content: counter(step, decimal-leading-zero);
    position: absolute;
    top: clamp(1.1rem, 2vw, 1.5rem);
    right: clamp(1.25rem, 2.4vw, 1.75rem);
    font-size: clamp(2.5rem, 2rem + 2vw, 3.75rem);
    font-weight: 800;
    line-height: 1;
    background: var(--grad-brand);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    opacity: 0.28;
    pointer-events: none;
}

.step h3 { margin-bottom: 0.5rem; }
.step p { font-size: 0.98rem; font-weight: 300; color: var(--text-muted); }
/* auto takes whatever space is left over, which is what lines the buttons up;
   the 1.25rem stays as the minimum for the card with the most text, where
   there is no slack left to take. */
.step .btn { margin-top: auto; }
/* Only where a button actually follows - the cards without one should not
   carry a phantom gap at the bottom. */
.step p:has(+ .btn) { margin-bottom: 1.25rem; }

/* ---- Connect box ---- */

.connect {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1rem;
    padding: clamp(1.25rem, 2.5vw, 1.75rem);
    border-radius: var(--r-md);
}

.connect__code {
    flex: 1 1 14rem;
    min-width: 0;
    padding: 0.85rem 1.2rem;
    border-radius: var(--r-sm);
    border: 1px dashed var(--panel-border-2);
    background: rgba(var(--k), 0.3);
    font-family: ui-monospace, 'Cascadia Code', Consolas, monospace;
    font-size: 1rem;
    color: var(--accent);
    overflow-x: auto;
    white-space: nowrap;
}

/* ---- FAQ ---- */

.faq { display: grid; gap: 0.85rem; }

/* A section heading, sitting in the same column as the questions it opens.
   Extra room above it, none for the first, so the groups read apart without a
   rule between them. */
.faq__section { margin-top: clamp(1.5rem, 3.5vh, 2.5rem); }
.faq__section:first-child { margin-top: 0; }
.faq__section p {
    margin-top: 0.35rem;
    font-size: 0.95rem;
    color: var(--text-muted);
}

.faq details {
    border-radius: var(--r-md);
    overflow: hidden;
    transition: border-color var(--speed) var(--ease);
}
.faq details[open] { border-color: var(--panel-border-2); }

/* The store's question hover: the text is painted with a gradient that is
   mostly flat near-white, with the brand colours parked off to the right.
   Hovering slides the gradient across rather than swapping one colour for
   another, so the colour arrives through the words left to right. */
.faq summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1.15rem clamp(1.25rem, 2.5vw, 1.75rem);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
    /* currentColor for the flat half, not a literal near-white: the resting
       text has to be the theme's own ink on paper. The footer links already
       do this - a #F3F3F8 clipped to the letters is invisible in daylight. */
    background-image: linear-gradient(90deg,
        currentColor 0%, currentColor 42%,
        #8B5CF6 62%, #EC4899 80%, #A78BFA 100%);
    background-size: 240% 100%;
    background-position: 0% 0;
    -webkit-background-clip: text;
            background-clip: text;
    color: var(--text);
    -webkit-text-fill-color: transparent;
    transition: background-position 0.4s var(--ease);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { background-position: 100% 0; }

/* The chevron is drawn from borders, not type, so the transparent text fill
   above cannot reach it - but say so, because it is not obvious. */
.faq summary::after { -webkit-text-fill-color: initial; }

.faq summary::after {
    content: '';
    flex: none;
    width: 0.62rem;
    height: 0.62rem;
    border-right: 2px solid var(--accent);
    border-bottom: 2px solid var(--accent);
    transform: rotate(45deg) translate(-0.15rem, -0.15rem);
    transition: transform 0.3s var(--ease);
}
.faq details[open] summary::after {
    transform: rotate(-135deg) translate(-0.15rem, -0.15rem);
}

.faq__body {
    padding: 0 clamp(1.25rem, 2.5vw, 1.75rem) 1.5rem;
    font-weight: var(--body-weight, 300);
    color: var(--text-muted);
}

/* ==================================================================
   12. Team
   ================================================================== */

.team {
    display: grid;
    gap: clamp(1rem, 2.2vw, 1.6rem);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

.member {
    padding: clamp(1.75rem, 3vw, 2.25rem);
    border-radius: var(--r-lg);
    text-align: center;
    overflow: hidden;
    /* No transition: nothing on this card changes state any more. The pointer
       glow it once had, the lift, the border tint - all gone (owner,
       2026-08-26). Its arrival is animated by the reveal rules, which carry
       their own. */
}

.member__avatar {
    display: grid;
    place-items: center;
    width: clamp(5.5rem, 9vw, 7rem);
    aspect-ratio: 1;
    margin: 0 auto 1.25rem;
    border-radius: 50%;
    overflow: hidden;
    font-size: clamp(1.75rem, 3vw, 2.25rem);
    font-weight: 700;
    color: var(--text);
    background:
        var(--pill-avatar) padding-box,
        var(--grad-brand) border-box;
    border: 2px solid transparent;
    box-shadow: 0 0 1.8rem rgba(139, 92, 246, 0.3);
}
.member__avatar img { width: 100%; height: 100%; object-fit: cover; }

.member__name { font-size: 1.2rem; font-weight: 600; }

.member__role {
    margin-top: 0.2rem;
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    background: var(--grad-brand);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
}

.member__bio {
    margin-top: 0.9rem;
    font-size: 0.95rem;
    font-weight: 300;
    color: var(--text-muted);
}

/* ==================================================================
   13. Gallery
   ================================================================== */

/* Thumbnails, laid out like the store's portfolio. Bigger tiles than before
   - there is no caption competing for the space any more, so the picture can
   have it. */
/* Four across, like the shop's portfolio. auto-fill with a small minimum
   would give four on a wide screen and any number below it; a fixed count
   keeps the row honest and drops to two and then one at the breakpoints
   below, so a thumbnail is never a postage stamp. */
.gallery {
    display: grid;
    gap: clamp(0.75rem, 1.8vw, 1.25rem);
    grid-template-columns: repeat(4, 1fr);
}
/* The site's own breakpoints, in rem like every other query in this file, so
   the gallery reflows in step with the rest of the page instead of at two
   private widths of its own. */
@media (max-width: 68rem) { .gallery { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 30rem) { .gallery { grid-template-columns: 1fr; } }

.shot {
    position: relative;
    border-radius: var(--r-md);
    overflow: hidden;
    /* THIS is what keeps every thumbnail the same size whatever is dropped
       in. The tile fixes the shape; object-fit: cover on the image below
       fills it and crops the overflow, so a tall phone screenshot and a wide
       ultrawide grab both land as the same rectangle. Never set a height on
       the image instead - that stretches them. */
    aspect-ratio: 16 / 10;
    /* A plain edge at rest, the same one the team cards wear (owner,
       2026-08-26); the gradient arrives on hover, from the ring below. The
       fill only shows before the screenshot loads. */
    border: 1px solid var(--panel-border);
    background: var(--pill-flat);
    /* The default arrow, not zoom-in (owner, 2026-08-26). */
    cursor: pointer;
    transition: transform 0.35s var(--ease-out), box-shadow 0.35s var(--ease-out);
}

.shot img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.7s var(--ease-out), filter 0.5s var(--ease);
}


/* Lightbox */
/* Lightbox, the shop's one exactly: a panel with the picture inside it rather
   than a bare image floating on a veil. The panel is what caps the size - the
   picture is fitted into a 16:9 well with object-fit: contain, so a 2400px
   panorama and a tall portrait both land inside the same frame at a size that
   fits the screen, instead of the portrait running off the top and bottom.
   No caption: the gallery is thumbnails only. */
.lightbox {
    position: fixed;
    inset: 0;
    z-index: 400;
    display: grid;
    place-items: center;
    padding: clamp(1rem, 4vw, 3rem);
    background: var(--sheet-veil);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    opacity: 0;
    visibility: hidden;
    /* Belt and braces with visibility: a closed overlay must never sit over
       the page swallowing clicks. */
    pointer-events: none;
    transition: opacity 0.3s var(--ease), visibility 0.3s;
}
.lightbox.is-open { opacity: 1; visibility: visible; pointer-events: auto; }

.lightbox__panel {
    position: relative;
    width: min(58rem, 100%);
    max-height: 100%;
    overflow: hidden;
    border: 1px solid var(--panel-border-2);
    border-radius: var(--r-lg);
    background: var(--sheet-pop);
    box-shadow: 0 2rem 5rem rgba(var(--k), 0.65);
    transform: scale(0.97);
    transition: transform 0.35s var(--ease-out);
}
.lightbox.is-open .lightbox__panel { transform: none; }

.lightbox__shot {
    position: relative;
    aspect-ratio: 16 / 9;
    /* Never taller than the viewport, whatever the aspect ratio works out to -
       this is what stops a picture being cut off on a short window. */
    max-height: calc(100vh - 2 * clamp(1rem, 4vw, 3rem));
    display: grid;
    place-items: center;
    overflow: hidden;
    background:
        radial-gradient(85% 100% at 50% 0%, rgba(var(--w), 0.09), transparent 70%),
        rgba(var(--k), 0.35);
}
/* The gradient edge, drawn as a ring masked out of its own middle rather than
   by swapping the background for a two-layer padding-box/border-box pair. That
   swap cannot be transitioned - background-image does not interpolate between
   a different number of layers - so the gradient would snap in. An opacity
   fade on a ring does, and it leaves the fill and the picture untouched. Same
   idiom as the shop's related-item cards. */
.shot::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 2;
    padding: 1px;
    border-radius: inherit;
    pointer-events: none;
    background: var(--grad-brand);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask-composite: exclude;
    opacity: 0;
    transition: opacity var(--speed) var(--ease);
}
.shot:hover::after { opacity: 1; }

/* Absolutely positioned, and that is load-bearing rather than tidy. As a
   normal grid item at height:100%, a 640x1400 portrait sized the well instead
   of fitting into it - aspect-ratio only proposes a shape, and a grid item
   taller than its track simply wins, so the panel grew to 1965px and ran off
   the screen. Out of flow, the well's height comes from its aspect-ratio and
   max-height alone, and the picture is fitted into whatever that turns out to
   be. */
.lightbox__shot img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.lightbox__close,
.lightbox__step {
    position: absolute;
    z-index: 2;
    display: grid;
    place-items: center;
    width: 2.6rem;
    height: 2.6rem;
    border: 1px solid var(--panel-border-2);
    border-radius: 50%;
    background: var(--sheet-tip);
    color: var(--text-muted);
    font-size: 1.05rem;
    cursor: pointer;
    transition: color var(--speed) var(--ease), border-color var(--speed) var(--ease);
}
.lightbox__close:hover,
.lightbox__step:hover { color: #fff; border-color: var(--secondary); }
.lightbox__close { top: 0.7rem; right: 0.7rem; }
/* Vertically centred on the picture, which is the whole panel here. */
.lightbox__step--prev { top: calc(50% - 1.3rem); left: 0.7rem; }
.lightbox__step--next { top: calc(50% - 1.3rem); right: 0.7rem; }

.lightbox__count {
    position: absolute;
    top: 0.7rem;
    left: 0.7rem;
    z-index: 2;
    padding: 0.25rem 0.65rem;
    border: 1px solid var(--panel-border-2);
    border-radius: 999px;
    background: var(--sheet-tip);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* One picture, nothing to step through. */
.lightbox.is-single .lightbox__step,
.lightbox.is-single .lightbox__count { display: none; }

.empty {
    padding: clamp(3rem, 8vh, 5rem) var(--gutter);
    border-radius: var(--r-lg);
    text-align: center;
}

/* ==================================================================
   13b. Cross-site banner & storefront
   ================================================================== */

/* The doorway to Mooniez Store. Deliberately quieter than a section of
   its own - the RP server stays the subject of the page. */
.banner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: clamp(1.25rem, 3vw, 2.5rem);
    padding: clamp(1.5rem, 3.5vw, 2.5rem);
    border-radius: var(--r-lg);
    overflow: hidden;
}
.banner::after {
    content: '';
    position: absolute;
    top: 50%;
    right: -6rem;
    width: 22rem;
    aspect-ratio: 1;
    transform: translateY(-50%);
    border-radius: 50%;
    background: radial-gradient(closest-side, rgba(236, 72, 153, 0.16), transparent 70%);
    pointer-events: none;
}
.banner__text { flex: 1 1 22rem; position: relative; }
.banner__text .eyebrow { margin-bottom: 0.35rem; }
.banner__text p { margin-top: 0.5rem; font-weight: 300; max-width: 42rem; }
.banner .btn { position: relative; flex: none; }

/* ---- Storefront (Mooniez Store) ---- */

.promises {
    display: grid;
    gap: clamp(0.75rem, 1.8vw, 1.25rem);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

.promise {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: clamp(1.25rem, 2.4vw, 1.75rem);
    border-radius: var(--r-md);
}
.promise__icon { font-size: 1.6rem; line-height: 1.2; flex: none; }
.promise p { margin-top: 0.25rem; font-size: 0.94rem; font-weight: 300; }

.products {
    display: grid;
    gap: clamp(1rem, 2.2vw, 1.6rem);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}

.product {
    display: flex;
    flex-direction: column;
    border-radius: var(--r-lg);
    overflow: hidden;
    transition: transform 0.45s var(--ease-out), border-color 0.45s var(--ease-out);
}

.product__shot {
    position: relative;
    aspect-ratio: 16 / 9;
    display: grid;
    place-items: center;
    background:
        radial-gradient(70% 90% at 50% 0%, rgba(139, 92, 246, 0.22), transparent 70%),
        rgba(var(--k), 0.3);
    border-bottom: 1px solid var(--panel-border);
    overflow: hidden;
}
.product__shot img { width: 100%; height: 100%; object-fit: cover; }
.product__crest {
    width: 42% !important;
    height: auto !important;
    object-fit: contain !important;
    opacity: 0.85;
    filter: drop-shadow(0 0 1.4rem var(--glow-purple));
}

.product__badge {
    position: absolute;
    top: 0.85rem;
    right: 0.85rem;
    padding: 0.3rem 0.8rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    border: 1px solid var(--panel-border-2);
    /* No backdrop-filter: this badge rides on a card that lifts, and a blur
       cannot be sampled while an ancestor's transform is animating - it would
       drop for the length of the lift and snap back. The background is already
       three-quarters opaque, so it was doing very little anyway. */
    background: rgba(10, 7, 20, 0.82);
}
.product__badge.is-available { color: #4ADE80; border-color: rgba(74, 222, 128, 0.4); }
.product__badge.is-soon      { color: var(--accent); border-color: rgba(167, 139, 250, 0.4); }
.product__badge.is-free      { color: var(--highlight); border-color: rgba(96, 165, 250, 0.4); }

.product__body {
    display: flex;
    flex-direction: column;
    flex: 1;
    padding: clamp(1.35rem, 2.6vw, 1.9rem);
}
.product__name { font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; }
.product__tagline { margin-top: 0.4rem; font-weight: 300; color: var(--text-muted); }

.product__tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 1rem; }
.product__tags .chip { font-size: 0.75rem; padding: 0.25rem 0.7rem; }

.product__points {
    list-style: none;
    display: grid;
    gap: 0.5rem;
    margin-top: 1.25rem;
}
.product__points li {
    position: relative;
    padding-left: 1.5rem;
    font-size: 0.92rem;
    font-weight: 300;
    color: var(--text-muted);
}
.product__points li::before {
    content: '';
    position: absolute;
    left: 0.15rem;
    top: 0.55em;
    width: 0.6rem;
    height: 0.3rem;
    border-left: 2px solid var(--accent);
    border-bottom: 2px solid var(--accent);
    transform: rotate(-45deg);
}

.product__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.85rem;
    margin-top: auto;
    padding-top: 1.5rem;
}
.product__price {
    font-size: 1.2rem;
    font-weight: 700;
    background: var(--grad-brand);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
}

.is-disabled { opacity: 0.5; pointer-events: none; }

/* ==================================================================
   14. Call to action & footer
   ================================================================== */

.cta { padding-block: 0 var(--section-y); }

.cta__panel {
    padding: clamp(2.5rem, 6vw, 4.5rem) clamp(1.5rem, 5vw, 4rem);
    border-radius: clamp(1.5rem, 3vw, 2.25rem);
    text-align: center;
    overflow: hidden;
}
.cta__panel .lede { max-width: 36rem; margin: 1rem auto 0; }

/* A soft brand glow rising from the bottom edge of the panel. */
.cta__panel::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: -60%;
    width: 120%;
    aspect-ratio: 2 / 1;
    transform: translateX(-50%);
    border-radius: 50%;
    background: radial-gradient(closest-side, rgba(139, 92, 246, 0.22), transparent 70%);
    pointer-events: none;
}

/* ---- The sign-off in the footer ---- */

.heartbeat {
    color: var(--secondary);
    animation: heartbeat 2.2s ease-in-out infinite;
}
@keyframes heartbeat {
    0%, 100% { transform: scale(1); }
    12%      { transform: scale(1.25); }
    24%      { transform: scale(1); }
    36%      { transform: scale(1.18); }
    48%      { transform: scale(1); }
}

/* The brand gradient drifting through the name, under a soft white sweep. */
.credit {
    font-weight: 700;
    background:
        linear-gradient(110deg, rgba(var(--w), 0) 40%, rgba(var(--w), 0.35) 50%, rgba(var(--w), 0) 60%),
        linear-gradient(to right, #8B5CF6, #EC4899, #A78BFA, #8B5CF6);
    background-size: 220% auto, 200% auto;
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    animation: creditSweep 9s linear infinite;
}
@keyframes creditSweep { to { background-position: -220% 0, -200% 0; } }

@media (prefers-reduced-motion: reduce) {
    .heartbeat, .credit { animation: none; }
}

.foot {
    position: relative;
    border-top: 1px solid var(--panel-border);
    overflow: hidden;
    /* The header's sheet, mirrored - see --sheet-foot. */
    background: var(--sheet-foot);
    backdrop-filter: var(--sheet-blur);
    -webkit-backdrop-filter: var(--sheet-blur);
    box-shadow: inset 0 1px 0 rgba(var(--w), 0.05);
}

/* The same aurora as the sky, slowed right down, so the page closes on the
   note it opened with. Nothing down here should compete with the links. */
.foot::before {
    content: '';
    position: absolute;
    inset: -70% -15% auto -15%;
    height: 220%;
    z-index: 0;
    pointer-events: none;
    background:
        radial-gradient(26% 55% at 18% 50%, rgba(139, 92, 246, 0.26), transparent 70%),
        radial-gradient(22% 50% at 55% 45%, rgba(236, 72, 153, 0.20), transparent 70%),
        radial-gradient(18% 42% at 85% 55%, rgba(96, 165, 250, 0.12), transparent 70%);
    filter: blur(34px);
    animation: auroraDrift 48s var(--ease) infinite alternate;
}
.foot > * { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) { .foot::before { animation: none; } }

.foot__inner {
    display: grid;
    gap: clamp(1.5rem, 4vw, 3rem);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
    padding-block: clamp(2rem, 4.5vh, 3rem);
}

.foot__brand { display: flex; align-items: center; gap: 0.85rem; margin-bottom: 0.9rem; }
.foot__brand img { width: 3rem; flex: none; filter: drop-shadow(0 0 0.8rem var(--glow-purple)); }
.foot__name { font-size: 1.2rem; font-weight: 700; line-height: 1.2; }
.foot__tag {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    /* One line, always. NATAMOONIE is five characters longer than the store's
       NATA, which was enough to push "made with" onto a second line at the
       old size and tracking. Nothing here may wrap - it is a signature. */
    white-space: nowrap;
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-dim);
}
.foot__tag .heartbeat { font-size: 0.85em; }
.foot__blurb { margin-top: 0.85rem; font-size: 0.88rem; font-weight: 400; color: var(--text-muted); }

.foot__col h4 {
    margin-bottom: 0.75rem;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--text-dim);
}
.foot__col a {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.26rem 0;
    font-size: 0.96rem;
    color: var(--text-muted);
    transition: color var(--speed) var(--ease);
}
/* The gradient arrives by sliding, not by swapping. A background-clip:text
   fill cannot be transitioned, so painting the link with a gradient that is
   flat for its first 42% and animating the POSITION gives the colour change
   AND a real transition. currentColor keeps the flat part correct in both
   themes without a second rule. */
.foot__col a {
    background-image: linear-gradient(90deg,
        currentColor 0%, currentColor 42%,
        #8B5CF6 62%, #EC4899 80%, #A78BFA 100%);
    background-size: 240% 100%;
    background-position: 0 0;
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
    transition: background-position 0.4s var(--ease);
}
.foot__col a:hover { background-position: 100% 0; }

/* The icon is a webfont glyph, so the transparent text fill above would erase
   it. Opt it back in and let it change colour on its own. */
.foot__col a i {
    width: 1.05rem;
    text-align: center;
    color: var(--text-dim);
    -webkit-text-fill-color: currentColor;
    transition: color var(--speed) var(--ease);
}
.foot__col a:hover i { color: var(--secondary); }

.foot__social { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 1rem; }

.foot__legal {
    padding: 1.1rem var(--gutter) 2rem;
    border-top: 1px solid rgba(var(--w), 0.05);
    font-size: 0.78rem;
    text-align: center;
    color: var(--text-dim);
}

/* ---- Legal pages ---- */

/* A legal page is a stack of short sections: a readable measure, headings
   that can be scanned, and blanks that are impossible to miss. */
.legal { max-width: 52rem; margin-inline: auto; }
.legal__box { padding: clamp(1.2rem, 2.4vw, 1.7rem); border-radius: var(--r-lg); margin-bottom: clamp(0.9rem, 2vw, 1.35rem); }
.legal__box h2 { font-size: clamp(1.1rem, 1rem + 0.5vw, 1.35rem); font-weight: 600; margin-bottom: 0.7rem; }
.legal__box h3 { font-size: 1rem; font-weight: 600; margin: 1.1rem 0 0.5rem; }
.legal__box p { font-size: 0.96rem; font-weight: var(--body-weight, 400); color: var(--text-muted); margin-bottom: 0.75rem; }
.legal__box p:last-child { margin-bottom: 0; }
/* A link inside a sentence: set in the brand gradient, no underline at rest,
   and a lit one sweeps in from the left on hover. Exactly the store's. */
/* Gradient lettering with an underline that sweeps in from the left.

   Both are the same gradient, laid on the element twice: the first copy is
   clipped to the glyphs - that is the lettering - and the second is a flat bar
   parked along the bottom edge with zero width, grown to full width on hover.
   background-clip takes one value per layer, which is what makes the pair
   possible: `text` for the type, `padding-box` for the bar.

   It has to be a background rather than the two obvious alternatives. An
   absolutely positioned ::after collapses when a link wraps onto a second
   line, which is what hid this underline in the privacy notice.
   text-decoration wraps correctly but cannot be hidden at rest, because with
   background-clip:text the decoration is painted from the text's own
   background and ignores text-decoration-color. box-decoration-break: clone
   gives each line fragment its own bar, so a wrapped link is underlined all
   the way through. */
.faq__body a,
.legal__box p a,
.cookie__text a,
.card__body a,
.rule li a,
.step p a,
.member__bio a,
.foot__blurb a,
.lede a,
.section__head p a {
    background-image: var(--grad-brand), var(--grad-brand);
    -webkit-background-clip: text, padding-box;
            background-clip: text, padding-box;
    background-repeat: no-repeat;
    background-position: 0 0, 0 100%;
    background-size: 100% 100%, 0% 2px;
    color: transparent;
    -webkit-text-fill-color: transparent;
    -webkit-box-decoration-break: clone;
            box-decoration-break: clone;
    text-decoration: none;
    transition: background-size var(--speed) var(--ease-out);
}
.faq__body a:hover,
.legal__box p a:hover,
.cookie__text a:hover,
.card__body a:hover,
.rule li a:hover,
.step p a:hover,
.member__bio a:hover,
.foot__blurb a:hover,
.lede a:hover,
.section__head p a:hover {
    background-size: 100% 100%, 100% 2px;
}

/* A note to the owner rather than to the reader: visibly a task, so it
   cannot be mistaken for part of the policy or quietly left in. */
.fillme {
    padding: 0.05rem 0.4rem;
    border-radius: 0.3rem;
    background: rgba(236, 72, 153, 0.16);
    border: 1px dashed rgba(236, 72, 153, 0.5);
    font-size: 0.9em;
    font-weight: 600;
    color: var(--secondary);
    -webkit-text-fill-color: currentColor;
}



/* ---- Closing notes ----
   Two short blocks that each end in one button. Side by side on anything
   wide enough, stacked below that. Equal height so the two buttons line up
   however unequal the paragraphs are. */
.endnotes {
    display: grid;
    gap: clamp(0.9rem, 2vw, 1.35rem);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
    align-items: stretch;
    margin-top: clamp(3rem, 7vh, 5rem);
}
.endnote {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.7rem;
    padding: clamp(1.3rem, 2.6vw, 1.75rem);
    border-radius: var(--r-lg);
}
.endnote p { font-size: 0.98rem; font-weight: var(--body-weight, 400); color: var(--text-muted); }
.endnote p b { color: var(--text); }
/* Pins the button to the bottom, so both cards' buttons sit on one line. */
.endnote .btn { margin-top: auto; }

/* ---- Founder cards ---- */

/* The store's maker card, brought over: a portrait with the moon hanging off
   the top left, the mark tucked bottom right, and the socials pinned to the
   bottom so two cards side by side line up however long the roles are. */
.makers {
    display: grid;
    gap: clamp(1rem, 2.2vw, 1.6rem);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
    max-width: 44rem;
    margin-inline: auto;
    align-items: stretch;
}

.maker {
    display: flex;
    flex-direction: column;
    padding: clamp(1.3rem, 2.6vw, 1.75rem);
    border-radius: var(--r-lg);
    text-align: center;
    overflow: hidden;
    /* No transition: nothing on this card changes state any more. The pointer
       glow it once had, the lift, the border tint - all gone (owner,
       2026-08-26). Its arrival is animated by the reveal rules, which carry
       their own. */
}

.maker__portrait {
    position: relative;
    aspect-ratio: 885 / 1080;
    margin-bottom: 1.1rem;
    border-radius: var(--r-md);
    overflow: hidden;
    border: 1.5px solid transparent;
    box-shadow: 0 0 1.6rem var(--glow-pink), 0 0 3rem var(--glow-purple);
    background:
        linear-gradient(100deg, rgba(139, 92, 246, 0.30), rgba(236, 72, 153, 0.22), rgba(167, 139, 250, 0.30)) padding-box,
        var(--pill-soft) padding-box,
        var(--grad-brand) border-box;
}
.maker__photo {
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* A whisper of the brand over a black-and-white photograph. */
    filter: saturate(0) contrast(1.04);
}
.maker__portrait::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
        linear-gradient(180deg, transparent 55%, rgba(var(--k), 0.55) 100%),
        radial-gradient(80% 60% at 80% 0%, rgba(236, 72, 153, 0.18), transparent 70%),
        radial-gradient(70% 60% at 10% 100%, rgba(139, 92, 246, 0.20), transparent 70%);
}

/* Standing in for a photograph until there is one: the same frame, filled
   with the brand wash and the mark, so the card is not a grey hole. */
.maker__placeholder {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
}
.maker__placeholder img { width: 45%; opacity: 0.85; filter: drop-shadow(0 0 1rem var(--glow-purple)); }

/* The mark tucked into the corner of the portrait, the way a photographer
   signs a print. */
.maker__badge {
    position: absolute;
    right: 0.7rem;
    bottom: 0.7rem;
    z-index: 2;
    width: 2.4rem;
    opacity: 0.9;
    filter: drop-shadow(0 0 0.6rem var(--glow-pink));
}


.maker__name { font-size: 1.35rem; font-weight: 700; line-height: 1.1; }
.maker__role {
    margin-top: 0.25rem;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    background: var(--grad-brand);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
}

/* margin-top:auto pins the row to the bottom, so two cards with different
   length roles still line their socials up. */
.socials { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; margin-top: auto; padding-top: 1.4rem; }
.social {
    /* The rim pseudo is absolutely positioned, so it is taken out of this grid
       instead of becoming a second grid item next to the icon. */
    position: relative;
    display: grid;
    place-items: center;
    width: 2.6rem;
    height: 2.6rem;
    border-radius: 50%;
    color: var(--text-muted);
    font-size: 0.95rem;
    transition: color var(--speed) var(--ease),
                background var(--speed) var(--ease), transform var(--speed) var(--ease);
}
/* A handle is not a link - it keeps the look but not the cursor, so nobody
   clicks expecting to go somewhere. */
.social--tag { cursor: default; }
/* Unchanged apart from the border, which the gradient rim now owns full time
   instead of only lighting up on hover. */
.social:hover {
    color: var(--hi);
    background:
        linear-gradient(100deg, rgba(139, 92, 246, 0.30), rgba(236, 72, 153, 0.22), rgba(167, 139, 250, 0.30)) padding-box,
        var(--pill-base) padding-box,
        var(--grad-brand) border-box;
    transform: translateY(-2px);
}
.social--todo { opacity: 0.45; }

/* ---- Tooltips ---- */

/* One attribute, one rule, no library: anything with data-tip gets one. */
[data-tip] { position: relative; }
[data-tip]::after {
    content: attr(data-tip);
    position: absolute;
    bottom: calc(100% + 0.5rem);
    left: 50%;
    transform: translate(-50%, 0.3rem);
    padding: 0.35rem 0.65rem;
    /* The wishlist's gradient rim, brought over (owner). Two layers in one
       background: the fill painted to the padding box, the brand gradient to
       the border box, so the 1px ring is the gradient and the middle stays
       the flat sheet colour. A plain border cannot hold a gradient. */
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    background:
        linear-gradient(var(--sheet-tip), var(--sheet-tip)) padding-box,
        var(--grad-brand) border-box;
    box-shadow: 0 0.6rem 1.5rem rgba(var(--k), 0.55);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.02em;
    color: var(--text);
    white-space: nowrap;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.2s var(--ease), transform 0.2s var(--ease-out), visibility 0.2s;
    z-index: 50;
}
[data-tip]:hover::after, [data-tip]:focus-visible::after {
    opacity: 1;
    visibility: visible;
    transform: translate(-50%, 0);
}

/* The nav is fixed to the top of the window, so a label above one of its
   controls opens off the top of the screen - the theme switch had one and
   nobody could read it (owner). Everything in the bar speaks downward. */
header.nav [data-tip]::after {
    bottom: auto;
    top: calc(100% + 0.5rem);
    transform: translate(-50%, -0.3rem);
}

header.nav [data-tip]:hover::after,
header.nav [data-tip]:focus-visible::after { transform: translate(-50%, 0); }
/* Against the left edge there is no room above, so this one speaks sideways. */
.totop[data-tip]::after { bottom: 50%; left: calc(100% + 0.6rem); transform: translate(-0.3rem, 50%); }
.totop[data-tip]:hover::after { transform: translate(0, 50%); }

/* ---- Back to top ---- */

/* Left side, and it only exists once you are far enough down for it to be
   useful - a button that is always there is furniture, not help. */
.totop {
    position: fixed;
    left: clamp(1rem, 3vw, 1.75rem);
    bottom: clamp(1rem, 3vh, 1.75rem);
    z-index: 160;
    display: grid;
    place-items: center;
    width: 2.9rem;
    height: 2.9rem;
    /* Gradient rim, same as the buttons and the notice. */
    border: 1.5px solid transparent;
    border-radius: 50%;
    background: var(--sheet-pop) padding-box, var(--grad-brand) border-box;
    box-shadow: 0 0.75rem 2rem rgba(var(--k), 0.5), inset 0 1px 0 rgba(var(--w), 0.08);
    backdrop-filter: var(--blur-lite);
    -webkit-backdrop-filter: var(--blur-lite);
    color: var(--text-muted);
    font-size: 1.1rem;
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transform: translateY(0.6rem);
    transition: opacity 0.3s var(--ease), transform 0.3s var(--ease-out),
                visibility 0.3s, color var(--speed) var(--ease), border-color var(--speed) var(--ease);
}
.totop.is-up { opacity: 1; visibility: visible; transform: none; }
/* The rim is a background layer, so border-color would paint a flat line over
   it. Brighten instead - and the lift has to be on .is-up, because the base
   transform is what keeps the button tucked away while it is hidden. */
.totop:hover { color: var(--hi); }
.totop.is-up:hover {
    transform: translateY(-2px);
    box-shadow: 0 0.75rem 2rem rgba(var(--k), 0.5), 0 0 1.4rem var(--glow-pink);
}

/* ---- Privacy notice ---- */

/* Bottom right, out of the way of everything, and it never covers the page.
   Nearly opaque rather than a glass sheet: it is a short piece of text people
   are meant to read once and dismiss, not part of the scenery. */
.cookie {
    position: fixed;
    right: clamp(1rem, 3vw, 1.75rem);
    bottom: clamp(1rem, 3vw, 1.75rem);
    z-index: 250;
    display: grid;
    gap: 0.7rem;
    justify-items: start;
    width: min(23rem, calc(100vw - 2rem));
    padding: 1.15rem 1.3rem;
    /* Gradient rim, same recipe as the ghost buttons: transparent border,
       fill to the padding box, gradient to the border box. */
    border: 1.5px solid transparent;
    border-radius: var(--r-md);
    background: var(--sheet-pop) padding-box, var(--grad-brand) border-box;
    box-shadow: 0 1.5rem 3rem rgba(var(--k), 0.55), 0 0 1.8rem var(--glow-pink);
    opacity: 0;
    transform: translateY(0.75rem);
    transition: opacity 0.35s var(--ease), transform 0.35s var(--ease-out);
}
.cookie.is-in { opacity: 1; transform: none; }

/* .cookie sets its own display, which would beat the [hidden] attribute -
   so the attribute has to be spelled out or a dismissed notice stays put. */
.cookie[hidden] { display: none; }

.cookie__title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.95rem;
    font-weight: 600;
}
.cookie__title i { color: var(--secondary); }
.cookie__text { font-size: 0.86rem; line-height: 1.55; color: var(--text-muted); }

/* Only in the notice: the link sits on its own line (owner, 2026-08-26). This
   is a narrow column, so inline it always broke mid-phrase - "What we" on one
   line and "do with data" on the next. Everywhere else links stay inline; this
   is the one place the column is too narrow for that to read.
   fit-content keeps the sweeping underline the width of the words rather than
   the width of the box. The full stop moved inside the link with it, so it
   travels on the same line instead of being left behind as an orphan. */
.cookie__text a {
    display: block;
    width: fit-content;
    margin-top: 0.45rem;
}


/* Layout only in this block. It sits after the shared text-link styling, so
   anything declared here wins - and an always-on underline was exactly what it
   was winning with once. The look stays with the rule above: no underline at
   rest, one sweeping in from the left on hover. */
.cookie .btn { width: auto; }

/* ==================================================================
   15. Scroll reveal
   ================================================================== */

/* The arrival slides with `translate`, not `transform`. They look the same,
   but `transition` is one property: whichever rule wins decides what animates
   for everything on the element. With the reveal owning transform, anything
   that also wanted a hover - the gallery tiles - got the reveal's list and its
   border and shadow snapped instead of fading. Separate properties, one
   transition list, each at its own speed: slow to arrive, quick under the
   pointer. */
.reveal {
    opacity: 0;
    translate: 0 1.75rem;
    transition: opacity 0.75s var(--ease-out), translate 0.75s var(--ease-out),
                transform 0.35s var(--ease-out),
                box-shadow 0.35s var(--ease-out),
                border-color 0.35s var(--ease);
}

/* Glass slides in but does not fade. A frosted panel drawn at half opacity is
   a half-frosted panel - you see the page straight through it, and the frost
   only arrives when the fade finishes. That is the boxes "going less
   see-through after a second". Sliding alone is reveal enough, and the glass
   is at full density from the first frame. */
.glass.reveal,
.glass.reveal-child { opacity: 1; }
.reveal.is-in { opacity: 1; translate: none; }

/* Children of a revealed container come in one after another. The delay is
   set inline by main.js so it survives any number of items. */
.reveal-child {
    opacity: 0;
    translate: 0 1.5rem;
    transition: opacity 0.65s var(--ease-out), translate 0.65s var(--ease-out),
                transform 0.35s var(--ease-out),
                box-shadow 0.35s var(--ease-out),
                border-color 0.35s var(--ease);
}
.reveal-child.is-in { opacity: 1; translate: none; }

/* ==================================================================
   16. Responsive
   ================================================================== */

@media (max-width: 84rem) {
    .brand__tag { display: none; }
    .nav__links .status-chip { display: none; }
    .nav__links { gap: clamp(0.6rem, 1.2vw, 1.4rem); }
}

@media (max-width: 68rem) {
    .nav__toggle { display: flex; }

    /* On a narrow screen the switch leaves the menu and sits beside the menu
       button, where it is reachable without opening anything first. */
    .themeswitch--bar { display: none; }
    .themeswitch--mast {
        display: inline-grid;
        position: absolute;
        top: 50%;
        right: calc(var(--gutter) + 3.4rem);
        transform: translateY(-50%);
    }

    /* The group has no job in a column - its children join the stack. */
    .nav__set { display: contents; }

    .nav__links {
        /* Absolute against .nav, which is the fixed element. It used to be
           fixed with top:100%, which only landed in the right place because
           the backdrop-filter on .nav happens to make it a containing block
           for fixed children - true today, and not something to rely on. */
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        flex-direction: column;
        align-items: stretch;
        gap: 0.25rem;
        padding: 1rem var(--gutter) 1.75rem;
        /* Nearly opaque, with the blur behind it. Enough of the sky comes
           through to still feel like part of the page; not enough for the
           aurora to compete with the words. */
        background: var(--sheet-menu);
        backdrop-filter: blur(24px) saturate(150%);
        -webkit-backdrop-filter: blur(24px) saturate(150%);
        border-bottom: 1px solid var(--panel-border);
        box-shadow: 0 1.5rem 3rem rgba(var(--k), 0.5);
        opacity: 0;
        visibility: hidden;
        /* Belt and braces with visibility: a closed menu must never sit over
           the page swallowing taps. */
        pointer-events: none;
        transform: translateY(-0.75rem);
        transition: opacity 0.3s var(--ease), transform 0.3s var(--ease-out), visibility 0.3s;
    }
    .nav__links.is-open { opacity: 1; visibility: visible; pointer-events: auto; transform: none; }

    .nav__links a:not(.btn):not(.status-chip) {
        display: flex;
        gap: 0.8rem;
        padding: 0.85rem 0.25rem;
        font-size: 1.05rem;
        border-bottom: 1px solid rgba(var(--w), 0.06);
    }
    /* Wider column in the phone menu so the labels line up down the list. */
    .nav__links a:not(.btn):not(.status-chip) > i { width: 1.2rem; font-size: 1em; }
    .nav__links a:not(.btn):not(.status-chip)::after { display: none; }

    /* The column has room again, so both come back. */
    .brand__tag { display: block; }
    .nav__links .status-chip { display: inline-flex; align-self: flex-start; margin-top: 1rem; }
    .nav__links .btn { margin-top: 0.5rem; }

    .foot__inner { text-align: center; }
    .foot__brand { flex-direction: column; }
    .foot__col a { justify-content: center; }
    .foot__social, .foot__reach { justify-content: center; margin-inline: auto; }
}

@media (max-width: 30rem) {
    .brand__tag { display: none; }
    .btn { width: 100%; }
    .btn-row { flex-direction: column; }
    /* Never the header's own buttons - those sit in a row, not a stack. */
    .nav .btn { width: auto; }
}

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }

    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }

    .reveal, .reveal-child { opacity: 1; translate: none; transition: none; }
    .sky__aurora, .moon { animation: none; }
    .mote { animation: none; opacity: 0.4; }
    .sky__stars { display: none; }
}

/* ==================================================================
   17. Raised tiles
   Last in the file on purpose - see the note.
   ================================================================== */

/* The shop's lift, brought over so both sites answer the pointer identically
   (owner, 2026-08-26): five pixels, four tenths of a second, the same border
   tint and the same bloom.

   The resting shadow is written out as an explicit zero rather than left as
   `none` - a shadow cannot interpolate from nothing and would snap in.

   On performance, because this is where hover jank comes from. `will-change`
   and `translateZ(0)` are NOT used here. Both promote an element to its own
   compositing layer, and on a grid of tiles that means dozens of permanent
   layers competing for GPU memory - the documented way to make things slower,
   not faster. The transform promotes itself for the length of the transition,
   which is when it is actually needed. The blurred shadow is the other
   expense, and it is affordable precisely because only one tile is ever
   hovered: one element repaints, not the grid.

   This sits at the end because `transition` is a single property: each tile
   declares its own further up, and the scroll-reveal rules declare one too.
   Whichever comes last decides what animates for the element, so this must be
   last - and must carry the reveal's `opacity` and `translate`, or these tiles
   would pop in instead of arriving. The reveal's 0.75s/0.65s split is
   flattened to 0.7s here, which is not a difference anyone can see. */
/* The team cards are not in this list at all (owner, 2026-08-26): they have no
   hover state of any kind, so they need neither the resting zero-shadow that
   anchors the fade nor a transition beyond the one the reveal gives them. */
.card, .step, .product, .endnote, .shot {
    box-shadow: 0 0 0 rgba(var(--k), 0), 0 0 0 rgba(236, 72, 153, 0);
    transition: opacity 0.7s var(--ease-out), translate 0.7s var(--ease-out),
                transform 0.4s var(--ease-out),
                border-color 0.4s var(--ease),
                box-shadow 0.4s var(--ease);
}
.card, .step, .product, .endnote { border-color: var(--panel-border); }

/* Everything answers with the same glow - the shop's exact values, so an info
   card here feels like an info card there. */
.card:hover, .step:hover, .product:hover, .endnote:hover {
    box-shadow: 0 0.85rem 1.7rem rgba(var(--k), 0.42), 0 0 1.1rem rgba(236, 72, 153, 0.14);
}
/* Gallery thumbnails take neither the lift nor the glow (owner, 2026-08-26) -
   both halves of that shadow are blurred, and on a grid of pictures the halo
   reads as the tile going soft. The gradient ring is the whole hover. Its
   border also goes transparent so the ring becomes the edge rather than
   sitting inside a second one. */
.shot:hover { border-color: transparent; }
/* Only the info cards lift. Gallery thumbnails stay put (owner, 2026-08-26),
   like the shop items and portfolio tiles they sit next to in spirit. */
/* The team page does not lift (owner, 2026-08-26): Nata and Sora's cards and
   the crew below them are portraits, not things you click. They keep the glow
   and the border tint. */
.card:hover, .step:hover, .product:hover, .endnote:hover {
    transform: translate3d(0, -5px, 0);
}
/* Everything except .shot, whose border is deliberately transparent so the
   gradient rim underneath shows through - tinting it would paint over the rim
   and the tile would lose its edge on hover. */
.card:hover, .step:hover, .product:hover, .endnote:hover {
    border-color: rgba(236, 72, 153, 0.45);
}

@media (prefers-reduced-motion: reduce) {
    .card:hover, .step:hover, .product:hover, .endnote:hover {
        transform: none;
    }
}
