/* ============================================================================
   InCity Development — store stylesheet
   Tebex: Template -> Assets -> style.css

   Colour and type tokens live in the theme (theme/theme.css). This file is
   structure: the shell, the components, the grid.

   The design brief in one line: it has to be recognisably the same house as the
   ic_ scripts, and it has to be a better thing than an in-game panel.

   Where it follows the scripts
     - Rajdhani for anything that names, labels or counts
     - violet on controls and edges, never as a large field
     - the two-part elevation: black drop + violet bloom
     - the same easing curve, so hovers feel like the game UI

   Where it deliberately does not
     - a body face the scripts never needed, because a product page is prose
     - backdrop-filter on the header. FiveM's CEF build renders it as a solid
       black block, so the scripts ban it outright. A browser composites it
       properly, and this is exactly the kind of thing the web gets to do that
       the game does not.
     - page-scale atmosphere: an aurora wash and a grain layer. On a 1180px
       panel that would be noise; on a full page it is depth.
   ============================================================================ */

/* --- design tokens -------------------------------------------------------- */
/* ===== REMOVE THIS BLOCK IF THE THEME SECTION EVER SERVES store.css =====

   This is theme/theme.css, duplicated here because the Appearance -> Themes
   layer is not reaching the page. Without these every var() below resolves to
   nothing: white ground, serif type, and .ramp headings that set
   color: transparent over a gradient that is not there, so words disappear
   mid-sentence.

   theme/theme.css remains the master. While this block exists it loads AFTER
   store.css and will win, so editing the palette in Appearance will appear to
   do nothing — that is the cost, and the reason for the shouting above.
   ======================================================================== */

:root {
    /* --- type ------------------------------------------------------------ */
    /* Rajdhani carries the brand: headings, captions, numbers, buttons. It is
       condensed and technical — perfect for a label, punishing for a paragraph.
       So body copy gets a neutral system face instead. That split is the main
       thing separating this from the script UIs, where Rajdhani does all jobs. */
    --font-display: "Rajdhani", "Segoe UI", Roboto, sans-serif;
    --font-body: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-mono: ui-monospace, "Cascadia Mono", Consolas, monospace;

    /* --- ink ------------------------------------------------------------- */
    /* rgb(10, 8, 16) is the scripts' "colour everything is darkened towards".
       The page goes one step deeper so panels can sit ON something. */
    --ink: 10 8 16;
    --bg: #07060c;
    --bg-2: #0a0810;

    --surface-1: #100e18;
    --surface-2: #16131f;
    --surface-3: #1d1929;

    /* --- violet ---------------------------------------------------------- */
    --violet: #8b5cf6;
    --violet-rgb: 139 92 246;
    --violet-lit: #a78bfa;
    --violet-pale: #c9bcff;
    --violet-deep: #7518f4;

    /* The logo's own gradient ramp. Reused for wordmarks and hairlines so the
       mark and the page are demonstrably the same artwork. */
    --brand-ramp: linear-gradient(100deg,
        #8424f5 0%, #963af1 18%, #ae66ef 40%,
        #c99cf1 62%, #e5d7f6 84%, #f1edf8 100%);

    /* --- text ------------------------------------------------------------ */
    --text: #f1f5f9;
    --text-dim: rgb(255 255 255 / 0.72);
    --text-faint: rgb(255 255 255 / 0.55);

    /* --- status ---------------------------------------------------------- */
    --good: #9ff5b0;
    --warn: #fcd34d;
    --bad: #ff9c8f;

    /* --- sale -------------------------------------------------------------- */
    /* A discount gets its own colour rather than borrowing --bad. They used to
       be the same token, which meant "20% off" was drawn in the exact red this
       store uses for errors and delete links.
       The fix was never to make the badge cool. This palette is violet from top
       to bottom, so the one warm thing on the page is the one thing that cannot
       be missed — and a saturated rose is far enough round the wheel from the
       accent to shout without fighting it. */
    --sale: #f4365c;
    --sale-rgb: 244 54 92;
    --sale-lit: #ff5a7a;
    --sale-deep: #d21444;

    /* --- lines ----------------------------------------------------------- */
    --hair: rgb(167 139 250 / 0.22);
    --hair-soft: rgb(255 255 255 / 0.09);
    --border: rgb(var(--violet-rgb) / 0.18);
    --border-lit: rgb(var(--violet-rgb) / 0.45);

    /* --- elevation -------------------------------------------------------- */
    /* Same two-part recipe as the script panels: a deep black drop that seats
       the object, plus a violet bloom that ties it to the accent. */
    --shadow-card: 0 18px 50px -22px rgb(0 0 0 / 0.85);
    --shadow-panel:
        0 25px 80px -10px rgb(0 0 0 / 0.7),
        0 0 40px -10px rgb(var(--violet-rgb) / 0.25),
        inset 0 1px 0 rgb(255 255 255 / 0.04);
    --shadow-lift:
        0 30px 70px -25px rgb(0 0 0 / 0.9),
        0 0 50px -18px rgb(var(--violet-rgb) / 0.4);
    --glow: 0 0 0 1px rgb(var(--violet-rgb) / 0.35), 0 0 32px -8px rgb(var(--violet-rgb) / 0.55);

    /* --- geometry --------------------------------------------------------- */
    /* Scripts use 12px panels / 9px tiles. On a page those read as small, so
       everything moves up one step and keeps the same ratio between them. */
    --r-sm: 8px;
    --r: 12px;
    --r-lg: 16px;
    --r-xl: 22px;

    /* --- motion ----------------------------------------------------------- */
    /* Straight from the scripts' <Transition name="win">. */
    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-in: cubic-bezier(0.5, 0, 0.9, 0.4);
    --t-fast: 0.16s;
    --t: 0.28s;

    /* --- layout ----------------------------------------------------------- */
    --wrap: 1240px;
    --header-h: 74px;
}

/* --- fonts ---------------------------------------------------------------- */
/* The four Rajdhani faces used to be declared here against .woff2 assets. They
   now live in fonts.css, embedded as data URIs, because Tebex has no font
   asset type — the uploader takes TWIG, CSS, SASS, JAVASCRIPT and IMAGE only.
   Still self-hosted, still the exact files the ic_ scripts ship; the delivery
   changed, not the decision. layout.html loads fonts.css before this file. */

/* --- reset ---------------------------------------------------------------- */

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

/* The browser's own [hidden] rule is `display: none` at element specificity, so
   any class here that sets a display wins over it. Several things on this store
   ship hidden and are revealed by script — the adoption line, the filter bar,
   the Discord count — and every one of them would otherwise render empty. */
[hidden] { display: none !important; }

html { scroll-behavior: smooth; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 15px;
    line-height: 1.65;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    overflow-x: hidden;
}

img, svg, video { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }

::selection { background: rgb(var(--violet-rgb) / 0.35); color: #fff; }

/* Scrollbar, same treatment as the script panels. */
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--bg-2); }
::-webkit-scrollbar-thumb {
    background: rgb(var(--violet-rgb) / 0.25);
    border: 3px solid var(--bg-2);
    border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover { background: rgb(var(--violet-rgb) / 0.45); }

:focus-visible {
    outline: 2px solid var(--violet-lit);
    outline-offset: 2px;
    border-radius: var(--r-sm);
}

/* --- page atmosphere ------------------------------------------------------ */
/* Two fixed layers behind everything. The aurora is anchored to the top of the
   viewport rather than the document, so it reads as light in the room instead
   of a graphic that scrolls away. Both are pointer-events:none and sit at
   negative z-index, so nothing has to know they exist. */

body::before {
    content: "";
    position: fixed;
    inset: -20% -10% auto -10%;
    height: 90vh;
    z-index: -2;
    pointer-events: none;
    background:
        radial-gradient(50% 55% at 18% 0%, rgb(117 24 244 / 0.20), transparent 70%),
        radial-gradient(45% 50% at 82% 8%, rgb(139 92 246 / 0.14), transparent 72%),
        radial-gradient(60% 40% at 50% 0%, rgb(196 154 239 / 0.06), transparent 80%);
    filter: blur(10px);
}

/* Film grain. Kept very low — it should be felt, not seen. */
body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0.035;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* --- typography ----------------------------------------------------------- */

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 28px; }

/* The family's small uppercase label, straight from the scripts. */
.cap {
    font-family: var(--font-display);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: 1.7px;
    text-transform: uppercase;
    color: var(--text-faint);
}
.cap--hi { color: var(--violet-pale); }

.num { font-family: var(--font-display); font-variant-numeric: tabular-nums; font-weight: 700; }

h1, h2, h3, h4 {
    font-family: var(--font-display);
    font-weight: 700;
    line-height: 1.12;
    letter-spacing: -0.005em;
    margin: 0;
}

.display {
    font-size: clamp(38px, 5.6vw, 66px);
    letter-spacing: -0.015em;
    line-height: 1.04;
}

/* The wordmark treatment: the logo's own ramp, clipped to the glyphs. Used
   sparingly — one phrase per page at most, or it stops meaning anything. */
.ramp {
    background: var(--brand-ramp);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.page-title { font-size: clamp(28px, 3.4vw, 42px); margin-bottom: 10px; }
.lede { font-size: 17px; color: var(--text-dim); max-width: 62ch; }

/* Section heading: eyebrow, title, and a hairline that runs to the edge. */
.section { margin-top: 76px; }
.section__head { display: flex; align-items: center; gap: 18px; margin-bottom: 26px; flex-wrap: wrap; }
.section__head h2 { font-size: clamp(22px, 2.4vw, 30px); white-space: nowrap; }
.section__rule {
    flex: 1;
    height: 1px;
    background: linear-gradient(90deg, var(--hair), transparent);
}

/* Store-authored HTML (package descriptions, custom pages). */
.rich-text { color: var(--text-dim); }
.rich-text > * + * { margin-top: 14px; }
.rich-text h2, .rich-text h3 { color: var(--text); margin-top: 26px; }
.rich-text h2 { font-size: 24px; }
.rich-text h3 { font-size: 19px; }
.rich-text strong { color: var(--text); font-weight: 600; }
/* :not(.btn) so a button pasted into a CMS page or a package description keeps
   its own colour — this selector is 0,1,1 and outranks .btn--primary (0,1,0). */
.rich-text a:not(.btn) { color: var(--violet-pale); text-decoration: underline; text-underline-offset: 3px; }
.rich-text ul, .rich-text ol { padding-left: 22px; }
/* Tebex gives a package exactly one image, so any further screenshots live in
   the description HTML. They get the same frame as the main shot. */
.rich-text img {
    width: 100%;
    border-radius: var(--r);
    border: 1px solid var(--border);
    box-shadow: var(--shadow-card);
    margin-top: 20px;
}
.rich-text li + li { margin-top: 6px; }
.rich-text code {
    font-family: var(--font-mono);
    font-size: 0.88em;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 1px 6px;
    color: var(--violet-pale);
}
.rich-text--sm { font-size: 14px; }

.muted { color: var(--text-faint); }
.empty {
    color: var(--text-faint);
    padding: 44px 0;
    text-align: center;
    font-family: var(--font-display);
    font-size: 17px;
}

/* --- header --------------------------------------------------------------- */

.header {
    position: sticky;
    top: 0;
    z-index: 40;
    height: var(--header-h);
    background: rgb(var(--ink) / 0.72);
    /* The FiveM-forbidden line. */
    backdrop-filter: blur(20px) saturate(1.4);
    -webkit-backdrop-filter: blur(20px) saturate(1.4);
    border-bottom: 1px solid var(--hair-soft);
}
.header::after {
    /* A violet filament along the bottom edge, brightest under the logo. */
    content: "";
    position: absolute;
    inset: auto 0 -1px 0;
    height: 1px;
    background: linear-gradient(90deg,
        transparent, rgb(var(--violet-rgb) / 0.5) 12%, rgb(var(--violet-rgb) / 0.12) 45%, transparent 80%);
}
/* Brand left, nav dead-centre, help and basket right. The two flanking groups
   both flex:1 so the rail is centred against the page, not against whatever the
   right-hand cluster happens to weigh this week. */
.header__inner { display: flex; align-items: center; gap: 24px; height: 100%; }

.brand { display: flex; align-items: center; flex-shrink: 0; }
.brand img { height: 30px; width: auto; }

/* Flat links with an underline marker, not a pill rail.
   A rounded track around the links made the nav read as a widget sitting on the
   header — one more object competing with the mark and the basket. Without it
   the header is three things at three weights: mark, navigation, account. */
.nav { display: flex; align-items: center; gap: 4px; }

.nav__link {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: var(--header-h);
    padding: 0 14px;
    font-family: var(--font-display);
    font-size: 15.5px;
    font-weight: 600;
    letter-spacing: 0.4px;
    color: var(--text-faint);
    white-space: nowrap;
    transition: color var(--t-fast) var(--ease-out);
}
.nav__link .i { width: 16px; height: 16px; opacity: 0.7; transition: opacity var(--t-fast) var(--ease-out); }
.nav__link .i--ext { width: 11px; height: 11px; opacity: 0.4; margin-left: -3px; }
.nav__link:hover { color: var(--text); }
.nav__link:hover .i { opacity: 1; }

/* The marker: a violet bar on the header's own bottom edge. It grows from the
   centre so switching pages reads as the indicator moving rather than two
   separate things blinking. */
.nav__link::after {
    content: "";
    position: absolute;
    left: 12px;
    right: 12px;
    bottom: 0;
    height: 2px;
    border-radius: 2px 2px 0 0;
    background: var(--violet-lit);
    box-shadow: 0 0 12px -1px rgb(var(--violet-rgb) / 0.8);
    transform: scaleX(0);
    transition: transform var(--t) var(--ease-out);
}
.nav__link:hover::after { transform: scaleX(0.35); }
.nav__link.is-active { color: var(--text); }
.nav__link.is-active .i { opacity: 1; color: var(--violet-pale); }
.nav__link.is-active::after { transform: scaleX(1); }

.header__actions { display: flex; align-items: center; gap: 10px; flex: 1; justify-content: flex-end; }
.brand { flex: 1; }

.user-chip {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    height: 38px;
    padding: 0 15px;
    border-radius: 999px;
    border: 1px solid var(--hair-soft);
    background: rgb(255 255 255 / 0.028);
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 15px;
    letter-spacing: 0.3px;
    color: var(--text-dim);
    transition: color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out),
        background var(--t-fast) var(--ease-out);
}
.user-chip--action:hover { color: var(--text); border-color: var(--border); background: rgb(255 255 255 / 0.05); }
.user-chip__dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--good);
    box-shadow: 0 0 0 3px rgb(159 245 176 / 0.14);
}
.user-chip .i { width: 16px; height: 16px; opacity: 0.7; }

/* Icon only, with the count pinned to the corner. "Basket" printed next to a
   cart glyph was the same word twice. */
.cart {
    position: relative;
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border-radius: 999px;
    border: 1px solid var(--hair-soft);
    background: rgb(255 255 255 / 0.028);
    color: var(--text-dim);
    transition: color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out),
        background var(--t-fast) var(--ease-out);
}
.cart:hover { color: var(--text); border-color: var(--border-lit); background: rgb(var(--violet-rgb) / 0.1); }
.cart__count {
    position: absolute;
    top: -3px;
    right: -3px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    display: grid;
    place-items: center;
    border-radius: 999px;
    background: var(--violet);
    border: 2px solid var(--bg);
    color: #fff;
    font-family: var(--font-display);
    font-size: 11.5px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* --- icons ---------------------------------------------------------------- */
/* One rule for every inline SVG in the store: stroked, currentColor, no fill.
   Individual icons never carry their own colours, so a link's colour change
   takes the glyph with it. */
.i {
    width: 17px;
    height: 17px;
    flex-shrink: 0;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.i--xs { width: 13px; height: 13px; stroke-width: 2; }

.icon-btn {
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: var(--r-sm);
    border: 1px solid var(--hair-soft);
    color: var(--text-faint);
    transition: color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out),
        border-color var(--t-fast) var(--ease-out);
}
.icon-btn:hover:not(:disabled) { color: var(--text); background: rgb(255 255 255 / 0.05); border-color: var(--border); }
.icon-btn:disabled { opacity: 0.35; cursor: default; }

/* Text link with a leading or trailing glyph. Used for Docs, Sign in, View all. */
.link-ext {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 0.3px;
    color: var(--text-faint);
    transition: color var(--t-fast) var(--ease-out);
}
.link-ext:hover { color: var(--text); }

.basket-btn .badge { margin-left: -2px; }

/* --- buttons -------------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    padding: 10px 18px;
    border-radius: var(--r-sm);
    border: 1px solid transparent;
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 700;
    letter-spacing: 0.6px;
    white-space: nowrap;
    transition:
        transform var(--t-fast) var(--ease-out),
        box-shadow var(--t) var(--ease-out),
        background var(--t-fast) var(--ease-out),
        border-color var(--t-fast) var(--ease-out);
}
.btn:active { transform: translateY(1px); }

/* The primary button is pale, not violet.
   A saturated violet slab had to compete with a UI that is already violet — the
   accent, the borders, the active pill — so the one control that must win was
   fighting everything around it. Taking it off the hue entirely settles that: it
   is the brightest thing on the page by luminance rather than by saturation, and
   violet goes back to meaning "accent" everywhere else.
   It is also where the logo's own gradient ends, so it is still brand colour —
   just the far end of the ramp. */
/* One page, one primary. Used for the single action a page exists to produce:
   Browse scripts on the homepage, Add to basket on a product.
   Hover changes brightness only — the old one warmed towards violet on hover,
   which made the button look like it was changing its mind. */
.btn--primary {
    background: linear-gradient(180deg, #f4f2fa, #ddd8ec);
    color: #16111f;
    border-color: rgb(255 255 255 / 0.5);
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.9), 0 2px 10px -5px rgb(0 0 0 / 0.7);
}
.btn--primary:hover {
    background: linear-gradient(180deg, #fbfaff, #e6e2f3);
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 1), 0 5px 16px -6px rgb(0 0 0 / 0.8);
}

/* The paying moment, and the only button in the store that looks like this.
   A basket is the one screen where the main action deserves weight rather than
   restraint — it is the last thing anyone does here, and a pale slab sitting in
   a pale panel was not carrying it. */
.btn--checkout {
    background: linear-gradient(180deg, #8257ea, #5f2fc4);
    color: #fff;
    border-color: rgb(255 255 255 / 0.14);
    box-shadow:
        inset 0 1px 0 rgb(255 255 255 / 0.22),
        0 10px 26px -14px rgb(var(--violet-rgb) / 0.9);
}
.btn--checkout:hover {
    background: linear-gradient(180deg, #9068f2, #6b39d4);
    box-shadow:
        inset 0 1px 0 rgb(255 255 255 / 0.28),
        0 14px 30px -14px rgb(var(--violet-rgb) / 1);
}
.btn--checkout .i { opacity: 0.85; }

/* Icon-only, square. Used as the card's affordance instead of a worded button:
   the card is already a link, so the control only has to point. */
.btn--icon { padding: 0; width: 36px; height: 36px; }
.btn .i { width: 16px; height: 16px; }

.btn--ghost {
    background: rgb(var(--violet-rgb) / 0.07);
    border-color: var(--border);
    color: var(--text);
}
.btn--ghost:hover { background: rgb(var(--violet-rgb) / 0.14); border-color: var(--border-lit); }

.btn--quiet { color: var(--text-faint); padding-inline: 12px; }
.btn--quiet:hover { color: var(--text); background: rgb(255 255 255 / 0.05); }

.btn--disabled {
    background: var(--surface-2);
    border-color: var(--hair-soft);
    color: var(--text-faint);
    cursor: not-allowed;
}
.btn--disabled:active { transform: none; }

.btn--sm { padding: 7px 13px; font-size: 14px; }
.btn--lg { padding: 14px 26px; font-size: 16.5px; }
.btn--block { width: 100%; }

.badge {
    min-width: 21px;
    padding: 0 6px;
    border-radius: 999px;
    background: rgb(0 0 0 / 0.28);
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

/* Small pill used for framework tags, versions, statuses. */
.chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 25px;
    padding: 0 11px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: rgb(var(--violet-rgb) / 0.08);
    font-family: var(--font-display);
    font-size: 12.5px;
    font-weight: 600;
    letter-spacing: 0.5px;
    color: var(--violet-pale);
    white-space: nowrap;
}
.chip--plain { background: rgb(255 255 255 / 0.04); border-color: var(--hair-soft); color: var(--text-faint); }
.chip--good { background: rgb(159 245 176 / 0.10); border-color: rgb(159 245 176 / 0.3); color: var(--good); }

/* Sits on a screenshot next to the live-server count, so it is built the same
   way that one is: dark translucent ground, hairline, small type. The only
   thing separating the two is what the hairline and the glyph are — rose and a
   tag here, green and a dot there — which is enough to tell a price from a
   statistic without either of them raising its voice.

   It used to be a solid rose pill with an inner highlight and a coloured drop
   shadow. That read as a supermarket sticker on artwork the rest of the store
   treats carefully, and it was the only saturated warm object on the site. The
   number does not need help being noticed; it needs to be legible and to mean
   "sale" rather than "server". White text does the first, the rose hairline and
   the tag do the second.

   The struck-through price under the description still does the arithmetic —
   this only has to flag that there is some. */
.chip--sale {
    background: rgb(var(--ink) / 0.78);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border-color: rgb(var(--sale-rgb) / 0.5);
    color: #fff;
    font-weight: 700;
    letter-spacing: 0.7px;
    text-transform: uppercase;
}
.chip--sale .i { width: 12px; height: 12px; color: var(--sale-lit); }

/* --- layout --------------------------------------------------------------- */

.shell { min-height: calc(100vh - var(--header-h)); padding-bottom: 90px; }

/* Off-screen until focused. */
/* Present in the document, absent from the page. For headings that a screen
   reader needs and a sighted reader does not. */
.u-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.skip {
    position: fixed;
    top: 12px;
    left: 12px;
    z-index: 100;
    padding: 10px 18px;
    border-radius: var(--r-sm);
    background: var(--surface-2);
    border: 1px solid var(--border-lit);
    font-family: var(--font-display);
    font-weight: 700;
    transform: translateY(-160%);
    transition: transform var(--t) var(--ease-out);
}
.skip:focus { transform: translateY(0); }

.cat-head { margin-bottom: 26px; }
/* Full width, so the category line runs the same measure as the strip and the
   grid under it rather than stopping two thirds of the way across. .lede caps
   itself at 62ch for the hero, where it sits beside an image and needs to; at
   0,2,0 this beats that without touching the hero. The leading goes up with
   the measure — a longer line needs more help getting back to the next one. */
.cat-head__text { max-width: none; }
.cat-head .lede { max-width: none; line-height: 1.75; }

/* Said once at the top of the catalogue instead of repeated on every card.
   No panel around it: four short facts read on the way past do not need the
   weight of a bordered component. A hairline above and below is enough to make
   them a set. */
/* Equal cells across the full width, the same shape the homepage .stats strip
   makes. Left-aligned it left a third of the row empty and the dividers landed
   wherever the words happened to end; on a grid they land on the quarters and
   the strip reads as one object rather than four things pushed to one side. */
.assure {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    margin: 0 0 30px;
    padding: 0;
    list-style: none;
    border-top: 1px solid var(--hair-soft);
    border-bottom: 1px solid var(--hair-soft);
}
.assure li {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    padding: 15px 18px;
    font-family: var(--font-display);
    font-size: 14.5px;
    font-weight: 600;
    letter-spacing: 0.3px;
    color: var(--text-dim);
    text-align: center;
}
.assure li + li { border-left: 1px solid var(--hair-soft); }
.assure .i { color: var(--violet-pale); opacity: 0.9; }
/* On a product page the strip is the last thing before the story, and .assure's
   catalogue margin would stack with the chapter block's own top padding. */
.assure--product { margin-bottom: 0; }

/* The one item on the strip that goes somewhere. Deliberately the same size
   and colour as the three claims beside it — a coloured link among them would
   read as the important one rather than the checkable one, and the arrow is
   enough to say it moves.

   currentColor on the arrow, because .assure .i paints icons violet and that
   is the strip's leading glyph, not a trailing hint. */
.assure__docs {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    color: inherit;
    transition: color var(--t-fast) var(--ease-out);
}
.assure__docs .i--ext { width: 11px; height: 11px; color: currentColor; opacity: 0.45; }
.assure__docs:hover { color: var(--text); }
.assure__docs:hover .i--ext { opacity: 1; }
.assure__docs:focus-visible { outline: 2px solid var(--violet-lit); outline-offset: 3px; border-radius: 3px; }

.product__head { margin-bottom: 26px; display: grid; gap: 14px; }
.product__head .page-title { margin: 0; }
.product__tags { display: flex; flex-wrap: wrap; gap: 8px; }

.checkout { width: 100%; }
.prose-card { padding: 34px; margin-top: 22px; max-width: 860px; }

.breadcrumb {
    display: flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.4px;
    color: var(--text-faint);
    margin-bottom: 18px;
}
.breadcrumb a:hover { color: var(--violet-pale); }

/* --- surfaces ------------------------------------------------------------- */

.panel {
    background:
        radial-gradient(140% 80% at 85% -10%, rgb(var(--violet-rgb) / 0.07), transparent 55%),
        linear-gradient(160deg, var(--surface-2), var(--surface-1));
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-card);
}

/* --- hero ----------------------------------------------------------------- */

/* The first screen belongs to the hero. Without a floor on its height the
   Scripts heading crept above the fold on a laptop, so the page opened on the
   catalogue rather than on what the store is.

   svh rather than vh because mobile browsers count their own chrome into vh,
   which pushes the stat strip off the bottom on exactly the devices that can
   least afford it.

   padding-block, not the shorthand: .hero also carries .wrap, and a `padding`
   shorthand here wiped out the wrap's horizontal padding. Above 1240px the
   wrap's max-width hid it, so the hero only touched the screen edge between
   720px and 1240px — the window sizes least likely to get looked at. */
.hero {
    position: relative;
    min-height: calc(100svh - var(--header-h));
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 52px;
    padding-block: 56px 40px;
}
.hero__grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.86fr);
    gap: 60px;
    align-items: center;
}
.hero__eyebrow { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
.hero__eyebrow::after {
    content: "";
    width: 60px;
    height: 1px;
    background: linear-gradient(90deg, var(--hair), transparent);
}
.hero h1 { max-width: 17ch; margin-bottom: 22px; }
.hero .lede { font-size: 18px; }
.hero__cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }

/* The hero's product shot. Given the full panel treatment — violet bloom and
   all — because it is the first piece of the actual product anyone sees. */
.hero__showcase {
    position: relative;
    display: block;
    aspect-ratio: 16 / 10;
    border-radius: var(--r-xl);
    border: 1px solid var(--border);
    overflow: hidden;
    background: var(--surface-2);
    box-shadow: var(--shadow-panel);
    transition: border-color var(--t) var(--ease-out), box-shadow var(--t) var(--ease-out);
}
.hero__showcase img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s var(--ease-out); }
/* The frame stays put on hover. A panel this size lifting off the page was
   more movement than a hover deserves; the image easing forward inside a fixed
   frame says the same thing without shoving the layout. */
.hero__showcase:hover { border-color: var(--border-lit); box-shadow: var(--shadow-lift); }
.hero__showcase:hover img { transform: scale(1.03); }

/* Flush to the bottom edge rather than inset: an inset bar leaves a sliver of
   screenshot beneath it, and on a busy frame that thin strip reads as a glitch.
   A scrim above the bar carries the image into it instead of cutting it. */
.hero__showcase-tag {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    padding: 13px 20px;
    border-top: 1px solid var(--hair);
    border-radius: 0 0 calc(var(--r-xl) - 1px) calc(var(--r-xl) - 1px);
    background: rgb(var(--ink) / 0.9);
}
.hero__showcase-tag::before {
    content: "";
    position: absolute;
    inset: auto 0 100% 0;
    height: 90px;
    pointer-events: none;
    background: linear-gradient(180deg, transparent, rgb(var(--ink) / 0.55));
}

/* Stat strip under the hero — the numbers are the argument. */
.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 1px;
    /* The hero is a flex column now and owns the spacing. */
    margin-top: 0;
    background: var(--hair-soft);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    overflow: hidden;
}
.stat { position: relative; background: var(--surface-1); padding: 22px 24px; }

/* A live figure earns a pulse; a static one does not get to pretend. */
.stat.is-live .stat__n { color: var(--good); }
.stat.is-live::before {
    content: "";
    position: absolute;
    top: 22px;
    right: 22px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--good);
    box-shadow: 0 0 0 3px rgb(159 245 176 / 0.16);
    animation: breathe 2.6s ease-in-out infinite;
}
.stat__n {
    font-family: var(--font-display);
    font-size: 32px;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    margin-bottom: 7px;
}

/* --- script cards --------------------------------------------------------- */

/* 268px rather than 300px so a four-script catalogue lands as one row on a
   desktop instead of three plus an orphan. */
.grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
    gap: 20px;
}

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    background: linear-gradient(160deg, var(--surface-2), var(--surface-1));
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    overflow: hidden;
    box-shadow: var(--shadow-card);
    transition:
        border-color var(--t) var(--ease-out),
        box-shadow var(--t) var(--ease-out);
}
/* A hairline of light along the top edge — the thing that stops a dark card
   from looking like a hole in the page. On hover it takes the accent, which is
   the card's only colour change and enough of one. */
.card::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 1px;
    z-index: 2;
    background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.10), transparent);
    transition: background var(--t) var(--ease-out);
}
.card:hover::before {
    background: linear-gradient(90deg, transparent, rgb(var(--violet-rgb) / 0.9), transparent);
}
.card:hover { border-color: var(--border-lit); box-shadow: var(--shadow-lift); }

/* The whole card is the click target: one empty anchor stretched over it, as a
   direct child of .card so inset:0 resolves against the card itself. The old
   version put the stretch on the title link, but .card__name is absolutely
   positioned, so it only ever covered the words. */
.card__link { position: absolute; inset: 0; z-index: 2; border-radius: inherit; }
.card__link:focus-visible { outline: 2px solid var(--violet-lit); outline-offset: 3px; }
/* Above the stretched link, so store-authored links in a description still work. */
.card__desc a { position: relative; z-index: 3; }

/* The screenshot is most of the card. 16:10 rather than 16:9 gives the name
   somewhere to sit without crowding the picture it sits on. */
.card__media {
    position: relative;
    aspect-ratio: 16 / 10;
    background:
        radial-gradient(90% 90% at 50% 0%, rgb(var(--violet-rgb) / 0.16), transparent 70%),
        var(--surface-3);
    overflow: hidden;
}
.card__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.7s var(--ease-out); }
.card:hover .card__media img { transform: scale(1.055); }

/* Tall and heavy, because the name is set on top of it. A GTA screenshot can be
   a bright sky or a lit interior at exactly the height the title lands. */
.card__scrim {
    position: absolute;
    inset: 22% 0 0 0;
    pointer-events: none;
    background: linear-gradient(180deg,
        transparent,
        rgb(var(--ink) / 0.28) 38%,
        rgb(var(--ink) / 0.72) 70%,
        rgb(var(--ink) / 0.95));
}

/* Fallback when a package has no image: the mark, quietly. The scrim stays —
   the name needs the same footing here as anywhere else. */
.card__media--empty { display: flex; align-items: center; justify-content: center; }
.card__media--empty img { width: 62px; height: auto; opacity: 0.2; object-fit: contain; margin-bottom: 18px; }

.card__badges {
    position: absolute;
    inset: 12px 12px auto;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 6px;
    z-index: 3;
    pointer-events: none;
}
.card__badges-l, .card__badges-r { display: flex; gap: 6px; flex-wrap: wrap; }
.card__badges-r { justify-content: flex-end; }
/* On a screenshot every badge needs its own ground. The sale chip is no longer
   an exception — it is built from the same ingredients now. */
.card__badges .chip { background: rgb(var(--ink) / 0.78); backdrop-filter: blur(10px); }
.card__badges .chip { height: 24px; padding: 0 10px; font-size: 12px; }

/* Pushed to the far end so the discount and the live count never jostle. */
.chip--live {
    margin-left: auto;
    border-color: rgb(159 245 176 / 0.28);
    color: var(--good);
    font-weight: 600;
}
.chip--live strong { font-weight: 700; font-variant-numeric: tabular-nums; }

.card__name {
    position: absolute;
    left: 18px;
    right: 18px;
    bottom: 14px;
    z-index: 2;
    font-size: 21px;
    line-height: 1.15;
    /* The two-layer shadow the scripts use for text on a game frame: a tight
       dark edge for the glyph, a wide soft one to lift it off the picture. */
    text-shadow: 0 1px 2px rgb(0 0 0 / 0.95), 0 2px 18px rgb(0 0 0 / 0.8);
}
.card__name { transition: color var(--t-fast) var(--ease-out); }
.card:hover .card__name { color: var(--violet-pale); }

.card__body { display: flex; flex-direction: column; gap: 14px; padding: 16px 18px 17px; flex: 1; }
.card__desc {
    font-size: 14px;
    line-height: 1.55;
    color: var(--text-dim);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
/* A product that carries a one-line summary shows exactly that on its card, so
   the catalogue and the product header say the same sentence instead of the
   card quoting the first three lines of a chapter.

   Written as "everything after the summary" rather than "only the summary" on
   purpose: a product with no summary yet matches nothing here and keeps showing
   its description, which is what it should do until one is written. */
/* Class as well as attribute: Tebex strips data-* from panel content, so on
   the live store [data-summary] matches nothing and every chapter after the
   summary stayed in the card — the clamp just cut the sentence off. */
.card__desc [data-summary] ~ *,
.card__desc .pd-summary ~ * { display: none; }
/* .pd-summary is sized for the product header; inside a card it takes the
   card's own type so one tile does not shout louder than the other three. */
.card__desc [data-summary],
.card__desc .pd-summary { margin: 0; font-size: inherit; line-height: inherit; max-width: none; }
/* Just the price now. The whole card is the control, so it needed no mark of
   its own; still pushed to the far end so a short description does not float
   the price into the middle of the card. */
/* A rule above the money. The description used to run straight into the price,
   which read as one block of text ending in a number; with a button down there
   too the row is a set of controls rather than the last line of a paragraph, and
   it should look like one. Inset by the body padding rather than bled to the
   card edge, so it separates the content without cutting the card in half. */
.card__foot {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: auto;
    padding-top: 13px;
    border-top: 1px solid var(--hair-soft);
}
/* Pushed to the far corner, and lifted above the stretched card link — without
   the z-index the card would swallow every click meant for it and send people to
   the product page, which is the one thing this button exists to skip. */
.card__buy {
    position: relative;
    z-index: 3;
    margin-left: auto;
    flex-shrink: 0;
}
/* In flight, and then a beat of green to say it landed. The button does not
   move or resize in either state — it sits in a grid of four cards, and a
   control that changes size on click nudges everything around it. */
.card__buy.is-busy { opacity: 0.55; pointer-events: none; }
.card__buy.is-added { background: var(--good); border-color: var(--good); }

/* --- rail (horizontal catalogue) ------------------------------------------ */
/* A wrapped grid leaves a half-empty second row the moment the count is not a
   multiple of three — four scripts in a three-wide grid is one card sitting
   alone under three. A rail is right at any count, and the cut-off card at the
   edge is itself the affordance that says there is more. */
.rail {
    position: relative;
    margin: 0 -28px;
    padding: 4px 28px;
    /* The right edge fades so the clipped card reads as continuing rather than
       as a rendering mistake. */
    -webkit-mask-image: linear-gradient(90deg, #000 0 92%, transparent 100%);
    mask-image: linear-gradient(90deg, #000 0 92%, transparent 100%);
}
.rail--end {
    -webkit-mask-image: none;
    mask-image: none;
}
.rail__track {
    display: flex;
    gap: 20px;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scroll-behavior: smooth;
    scrollbar-width: none;
    padding-bottom: 4px;
    cursor: grab;
}
.rail__track::-webkit-scrollbar { display: none; }
.rail__track.is-dragging { cursor: grabbing; scroll-behavior: auto; scroll-snap-type: none; }
.rail__track.is-dragging a { pointer-events: none; }

/* Cards are sized as a fraction of the view, not a fixed width. A fixed 316px
   against a 1184px row left 140px of scroll — just enough to clip the last card
   and not enough to feel like a rail, which reads as a bug rather than a
   design. Four per view means today's catalogue lands as one clean row, and the
   fifth script turns it into a real rail without anyone touching the CSS. */
.rail__track > .card {
    flex: 0 0 calc((100% - (var(--per-view) - 1) * 20px) / var(--per-view));
    scroll-snap-align: start;
}
.rail { --per-view: 4; }

.rail__nav { display: flex; gap: 6px; }
.rail__nav .icon-btn { width: 32px; height: 32px; }
.rail__nav .i { width: 15px; height: 15px; }

.prose { max-width: 68ch; }

.pullquote {
    position: relative;
    margin: 44px 0 0;
    padding: 34px 0 0 34px;
    max-width: 46ch;
}
/* An oversized quote mark set in the display face, low enough to be texture. */
.pullquote::before {
    content: "\201C";
    position: absolute;
    top: -6px;
    left: -8px;
    font-family: var(--font-display);
    font-size: 110px;
    line-height: 1;
    color: rgb(var(--violet-rgb) / 0.16);
    pointer-events: none;
}
.pullquote p {
    margin: 0 0 14px;
    font-family: var(--font-display);
    font-size: clamp(22px, 2.4vw, 30px);
    font-weight: 600;
    line-height: 1.28;
    letter-spacing: -0.005em;
    color: var(--text);
}
.pullquote cite {
    font-style: normal;
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 1.4px;
    text-transform: uppercase;
    color: var(--violet-pale);
}

/* --- price ---------------------------------------------------------------- */

.price { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.price__now { font-family: var(--font-display); font-size: 21px; font-weight: 700; font-variant-numeric: tabular-nums; }
.price__was { font-size: 14px; color: var(--text-faint); text-decoration: line-through; }
.price--lg .price__now { font-size: 38px; }
.price--free .price__now { color: var(--good); }

/* Sits beside the price rather than on the screenshot. Deep and flat: no
   gradient, no glow, no neon. Next to the number it modifies it does not have
   to compete for attention, so it can afford to look expensive instead of
   loud.

   Four treatments are kept below so the choice can be made by looking rather
   than by describing. Swap the class on .price__off to try another. */
.price__off {
    padding: 3px 7px;
    border-radius: 5px;
    background: var(--sale-deep);
    color: #fff;
    font-family: var(--font-display);
    font-size: 12.5px;
    font-weight: 700;
    letter-spacing: 0.3px;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* A — nothing at all. The struck price already says it. */
.price__off--none { display: none; }

/* B — the figure in colour, no container. */
.price__off--text { padding: 0; background: none; color: var(--sale-lit); font-size: 13.5px; }

/* C — tinted, with a hairline. */
.price__off--tint {
    background: rgb(var(--sale-rgb) / 0.14);
    box-shadow: inset 0 0 0 1px rgb(var(--sale-rgb) / 0.4);
    color: var(--sale-lit);
}

/* D — solid, but in the accent rather than a second hue. */
.price__off--violet { background: rgb(var(--violet-rgb) / 0.2); box-shadow: inset 0 0 0 1px rgb(var(--violet-rgb) / 0.4); color: var(--violet-pale); }

/* The struck-through original stays quiet on purpose — it is context, not the
   offer. The saving is what gets the colour. */
.save {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-top: 8px;
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 700;
    letter-spacing: 0.3px;
    color: var(--sale-lit);
}
.save .i { width: 15px; height: 15px; }

/* --- package detail ------------------------------------------------------- */

/* Areas rather than source order, so the buy box can follow the shot on a phone
   without being dragged below the whole feature list. */
.product {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    grid-template-areas:
        "media buy"
        "body  buy";
    gap: 34px;
    align-items: start;
}
.product__media { grid-area: media; }
.product__body { grid-area: body; margin-top: -6px; }
.buybox { grid-area: buy; }

.shot {
    aspect-ratio: 16 / 9;
    border-radius: var(--r-lg);
    border: 1px solid var(--border);
    overflow: hidden;
    background:
        radial-gradient(80% 80% at 50% 0%, rgb(var(--violet-rgb) / 0.16), transparent 70%),
        var(--surface-2);
    box-shadow: var(--shadow-panel);
    margin-bottom: 30px;
}
.shot img, .shot iframe { width: 100%; height: 100%; border: 0; display: block; }
.shot img { object-fit: cover; }
.shot--empty { display: flex; align-items: center; justify-content: center; }
.shot--empty img { width: 110px; height: auto; opacity: 0.2; object-fit: contain; }

/* --- gallery -------------------------------------------------------------- */
/* Built at runtime from the package image plus whatever the description holds
   (see scripts.js). Absent when there is only one frame to show. */
.gallery__thumbs {
    display: flex;
    gap: 10px;
    margin-top: 12px;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: 2px;
}
.gallery__thumbs::-webkit-scrollbar { display: none; }

.gallery__thumb {
    position: relative;
    flex: 0 0 96px;
    aspect-ratio: 16 / 10;
    padding: 0;
    border-radius: var(--r-sm);
    border: 1px solid var(--hair-soft);
    background: var(--surface-3);
    overflow: hidden;
    opacity: 0.55;
    transition: opacity var(--t) var(--ease-out), border-color var(--t) var(--ease-out),
        transform var(--t) var(--ease-out);
}
.gallery__thumb img { width: 100%; height: 100%; object-fit: cover; }
.gallery__thumb:hover { opacity: 0.85; }
.gallery__thumb.is-active {
    opacity: 1;
    border-color: var(--violet-lit);
    box-shadow: 0 0 0 1px rgb(var(--violet-rgb) / 0.4);
}

.gallery__thumb--video { display: grid; place-items: center; }
.gallery__play {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    background: rgb(var(--violet-rgb) / 0.22);
    border: 1px solid var(--border-lit);
}
.gallery__play svg { width: 13px; height: 13px; fill: var(--violet-pale); margin-left: 2px; }

/* Buy box: follows you down the page, because the price is the one control
   that must never be more than a glance away. */
.buybox {
    position: sticky;
    top: calc(var(--header-h) + 24px);
    padding: 24px;
    display: grid;
    gap: 16px;
}
.buybox__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.buybox__meta { display: grid; gap: 9px; padding-top: 16px; border-top: 1px solid var(--hair-soft); font-size: 14px; }
.buybox__meta > div { display: flex; justify-content: space-between; gap: 14px; }
.buybox__meta dt, .buybox__meta span:first-child { color: var(--text-faint); }

.spec { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
.spec li { display: flex; gap: 11px; align-items: flex-start; font-size: 14.5px; color: var(--text-dim); }
.spec li::before {
    content: "";
    flex-shrink: 0;
    width: 15px;
    height: 15px;
    margin-top: 4px;
    border-radius: 4px;
    background: rgb(var(--violet-rgb) / 0.18);
    border: 1px solid var(--border-lit);
    /* A tick, drawn rather than shipped as an icon font. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.9 6.1-6.6' fill='none' stroke='%23c9bcff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    background-size: 13px;
}

/* --- feature blocks (homepage) -------------------------------------------- */

.feature { padding: 26px; display: grid; gap: 11px; align-content: start; }
.feature__icon {
    width: 42px;
    height: 42px;
    display: grid;
    place-items: center;
    border-radius: var(--r-sm);
    background: rgb(var(--violet-rgb) / 0.14);
    border: 1px solid var(--border);
    margin-bottom: 5px;
}
.feature__icon svg { width: 20px; height: 20px; stroke: var(--violet-pale); fill: none; stroke-width: 1.7; }
.feature h3 { font-size: 18px; }
.feature p { margin: 0; font-size: 14.5px; color: var(--text-dim); }

/* --- basket / checkout ---------------------------------------------------- */

/* --- basket lines --------------------------------------------------------- */
/* A grid, not a table. Every row shares one column track, so thumbnails,
   steppers and prices line up down the panel instead of each cell sizing itself
   to whatever it happens to contain. */
.lines { padding: 0; overflow: hidden; }

.lines__head,
.lines__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 92px 132px 40px;
    align-items: center;
    gap: 20px;
    padding: 0 22px;
}
.lines__head { padding-block: 16px 12px; }
.lines__row { padding-block: 16px; border-top: 1px solid var(--hair-soft); transition: background var(--t-fast) var(--ease-out); }
.lines__row:hover { background: rgb(255 255 255 / 0.02); }

.lines__item { display: flex; align-items: center; gap: 15px; min-width: 0; }
.lines__thumb {
    width: 56px;
    height: 56px;
    flex-shrink: 0;
    border-radius: var(--r-sm);
    object-fit: cover;
    background: var(--surface-3);
    border: 1px solid var(--border);
}
.lines__thumb--empty { display: grid; place-items: center; }
.lines__thumb--empty img { width: 26px; height: auto; opacity: 0.3; }
.lines__text { display: grid; gap: 3px; min-width: 0; }
.lines__name {
    font-family: var(--font-display);
    font-size: 17px;
    font-weight: 600;
    transition: color var(--t-fast) var(--ease-out);
}
.lines__name:hover { color: var(--violet-pale); }
.lines__meta { font-size: 12.5px; color: var(--text-faint); }

/* Scoped so it outranks the generic input[type="number"] rule further down,
   which would otherwise stretch it to the column width. */
.lines .qty {
    width: 100%;
    padding: 8px 10px;
    text-align: center;
    font-family: var(--font-display);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    /* The browser's own stepper is two grey chevrons in a font that belongs to
       no other control on the page. The field is three characters wide; typing
       is the interaction. */
    -moz-appearance: textfield;
    appearance: textfield;
}
.lines .qty::-webkit-outer-spin-button,
.lines .qty::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Quantity and price are columns, so the heading and the value under it share
   one centre line. They did not: the quantity box fills its column and centres
   the number inside it, while the word above sat at the column left edge, and
   the price text sat 37px left of its own heading. The column boxes were always
   aligned — only the text inside them was not.

   justify-content rather than text-align for the price, because .price is a flex
   row (it can carry a struck-through original beside the current figure), and
   scoped to .lines so the product page and the featured module keep theirs. */
.lines__head .cap:nth-child(2),
.lines__head .cap:nth-child(3) { text-align: center; }
.lines .price { justify-content: center; }
/* While the new quantity is on its way to Tebex. */
.lines.is-updating { opacity: 0.55; pointer-events: none; transition: opacity var(--t-fast) var(--ease-out); }

.lines__x {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: var(--r-sm);
    color: var(--text-faint);
    transition: color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out);
}
.lines__x:hover { color: var(--bad); background: rgb(255 156 143 / 0.12); }

/* Both secondary actions on one bar inside the panel, rather than a button
   floating in the gap underneath it. */
.lines__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 13px 22px;
    border-top: 1px solid var(--hair-soft);
    background: rgb(var(--ink) / 0.35);
}

/* --- checkout layout ------------------------------------------------------ */
.checkout__grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    gap: 20px;
    align-items: start;
    margin-top: 20px;
}
/* Basket and coupon, one under the other and the same width — a discount box
   narrower or wider than the thing it discounts reads as belonging to something
   else. */
.checkout__main { display: grid; gap: 20px; align-content: start; }

/* The money, kept beside the items rather than under them, and it travels with
   the reader: on a long basket the total and the pay button should never be
   somewhere you have to scroll back to. Its own scrollbar only if the summary
   ever grows taller than the window. */
.checkout__rail {
    position: sticky;
    top: calc(var(--header-h) + 20px);
    max-height: calc(100svh - var(--header-h) - 40px);
    overflow-y: auto;
    scrollbar-width: none;
}
.checkout__rail::-webkit-scrollbar { display: none; }

.summary { padding: 22px 24px; display: grid; gap: 14px; align-content: start; }
.summary__line {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    font-size: 14px;
    color: var(--text-dim);
}
.summary__line--tax span:last-child, .summary__line--good span:last-child { color: var(--text-faint); font-size: 13px; }
.summary__line--good span:first-child { color: var(--good); }
.summary__total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    padding-top: 15px;
    border-top: 1px solid var(--hair-soft);
}
.summary__n {
    font-family: var(--font-display);
    font-size: 30px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.summary__note { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--text-faint); }

.empty-state { padding: 64px 30px; display: grid; justify-items: center; gap: 18px; text-align: center; }
.empty-state__icon {
    width: 46px; height: 46px; fill: none; stroke: var(--text-faint);
    stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; opacity: 0.5;
}
.empty-state .empty { padding: 0; }

/* --- redeem --------------------------------------------------------------- */
/* Coupons and gift cards share one endpoint on Tebex, so they share one field
   here. Asking the customer to classify the code they are holding would be a
   question with no consequence. */
.redeem { padding: 22px 24px; display: grid; gap: 20px; align-content: start; }
.redeem__form { margin: 0; display: grid; gap: 8px; }
.redeem__input { display: flex; gap: 8px; }
.redeem__input input { flex: 1; min-width: 0; }
.redeem__applied {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding-top: 16px;
    border-top: 1px solid var(--hair-soft);
}
.chip__x {
    display: inline-grid;
    place-items: center;
    margin: -2px -3px -2px 2px;
    padding: 3px;
    border-radius: 50%;
    opacity: 0.65;
}
.chip__x:hover { opacity: 1; background: rgb(255 255 255 / 0.12); }

/* --- forms ---------------------------------------------------------------- */

.field { display: grid; gap: 7px; }
.field > .cap { margin-bottom: -1px; }
.input, input[type="text"], input[type="number"], input[type="email"],
input[type="search"], select, textarea {
    width: 100%;
    padding: 11px 14px;
    background: rgb(var(--ink) / 0.6);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    color: var(--text);
    font: inherit;
    transition: border-color var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out);
}
input::placeholder, textarea::placeholder { color: rgb(255 255 255 / 0.32); }
/* Safari draws its own control furniture inside a search field. */
input[type="search"] { -webkit-appearance: none; appearance: none; }
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--border-lit);
    box-shadow: 0 0 0 3px rgb(var(--violet-rgb) / 0.15);
}
select { appearance: none; cursor: pointer; padding-right: 38px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%23c9bcff' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 13px center; background-size: 15px;
}

.form-card { padding: 30px; display: grid; gap: 20px; max-width: 480px; }
/* The verification step reads as one short sequence — context, product, why,
   who, buy — so it gets a little more width and noticeably more air between
   those parts than the base card, which holds a single question. */
/* width as well as max-width: the card is a flex item in a centred row, so
   without a definite width it sizes to its content and the max never applies —
   the panel was drawing itself as wide as its longest line happened to be. */
.form-card--wide { width: 100%; max-width: 680px; padding: 34px 36px; gap: 26px; }

/* --- sidebar modules ------------------------------------------------------ */

/* One band at the end of the page rather than a rail beside the catalogue.
   Whatever the panel has enabled lands here — one module or eight — and the
   heading only exists when there is something under it (see layout.html). */
.modules { margin-top: 90px; }
.modules__head { display: flex; align-items: center; gap: 18px; margin-bottom: 20px; }
.modules__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(262px, 1fr));
    gap: 16px;
    align-items: start;
}

.module { padding: 18px 20px; display: grid; gap: 12px; align-content: start; }
.module__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.module__row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: 14px; }
.module__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; }
.module__list li { display: grid; grid-template-columns: 1fr auto; gap: 3px 12px; font-size: 13.5px; }
.module__who { font-family: var(--font-display); font-weight: 600; font-size: 15px; }
.module__what { color: var(--text-faint); font-size: 12.5px; grid-column: 1; }
.module__amt { font-family: var(--font-display); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--violet-pale); grid-row: 1; grid-column: 2; }
.module__when { color: var(--text-faint); font-size: 12px; grid-column: 2; text-align: right; }
.module__avatar { width: 42px; height: 42px; border-radius: var(--r-sm); border: 1px solid var(--border); }
.module__form { display: flex; gap: 8px; }
.module__cta { display: flex; align-items: center; gap: 12px; }

.status { display: flex; align-items: center; gap: 9px; font-family: var(--font-display); font-weight: 600; }
.status__dot { width: 9px; height: 9px; border-radius: 50%; background: var(--text-faint); flex-shrink: 0; }
.status--on .status__dot {
    background: var(--good);
    box-shadow: 0 0 0 4px rgb(159 245 176 / 0.15);
    animation: breathe 2.6s ease-in-out infinite;
}
.status--off .status__dot { background: var(--bad); }
@keyframes breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

.bar { height: 7px; background: rgb(var(--ink) / 0.8); border-radius: 999px; overflow: hidden; border: 1px solid var(--hair-soft); }
.bar__fill {
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--violet-deep), var(--violet-lit));
    box-shadow: 0 0 14px -2px rgb(var(--violet-rgb) / 0.8);
    transition: width 0.6s var(--ease-out);
}
/* Both layers have to be restated: setting background-image alone would replace
   the fill's violet gradient and leave stripes floating on nothing. */
.bar--striped .bar__fill {
    background-image:
        linear-gradient(120deg,
            rgb(255 255 255 / 0.16) 25%, transparent 25%, transparent 50%,
            rgb(255 255 255 / 0.16) 50%, rgb(255 255 255 / 0.16) 75%, transparent 75%),
        linear-gradient(90deg, var(--violet-deep), var(--violet-lit));
    background-size: 16px 16px, 100% 100%;
}
.bar--animated .bar__fill { animation: track 1s linear infinite; }
@keyframes track { to { background-position: 16px 0, 0 0; } }

.mono { font-family: var(--font-mono); font-size: 13px; color: var(--text-dim); }

/* --- toast ---------------------------------------------------------------- */

.toast {
    position: fixed;
    top: calc(var(--header-h) + 18px);
    right: 24px;
    z-index: 60;
    max-width: 380px;
    padding: 14px 20px;
    border-radius: var(--r);
    border: 1px solid var(--border);
    border-left: 3px solid var(--violet);
    background: rgb(var(--ink) / 0.94);
    backdrop-filter: blur(14px);
    box-shadow: var(--shadow-panel);
    font-size: 14.5px;
    animation: toast-in 0.34s var(--ease-out) both;
}
.toast--success { border-left-color: var(--good); }
.toast--error { border-left-color: var(--bad); }
@keyframes toast-in { from { opacity: 0; transform: translateY(-12px) scale(0.97); } }

/* --- footer --------------------------------------------------------------- */

/* No padding-bottom of its own: the row inside already carries 18px, and the
   two together made the gap under the mark twice the gap above it. Tebex own
   legal footer sits directly beneath this, so there is nothing below to clear. */
.footer { border-top: 1px solid var(--hair-soft); margin-top: 90px; }
.footer__inner {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 24px;
    padding-block: 18px;
}
.footer__mark img { height: 24px; width: auto; opacity: 0.8; transition: opacity var(--t-fast) var(--ease-out); }
.footer__mark:hover img { opacity: 1; }

.footer__social { display: flex; gap: 8px; justify-self: end; }
.footer__social .icon-btn { width: 34px; height: 34px; border-radius: 999px; }

.footer__legal {
    margin: 0;
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--text-faint);
    text-align: center;
    text-wrap: balance;
}

/* Narrow enough that three across stops working: the mark and the buttons sit
   on one line, the sentence takes the one below. */
@media (max-width: 720px) {
    /* 36px a side is a fifth of a phone screen gone before any content. The
       card keeps its vertical rhythm and gives the width back. */
    .form-card--wide { padding: 26px 22px; gap: 22px; }


    .footer__inner {
        grid-template-columns: auto auto;
        justify-content: space-between;
        gap: 14px 24px;
    }
    /* padding-block again for the same reason as .hero and .header__inner:
       .footer__inner is a .wrap, and a later breakpoint sets the padding
       SHORTHAND on .wrap, which resets the block padding to zero and drops the
       mark and the buttons onto the border. Two classes rather than one so the
       rule wins on specificity instead of on source order, which is the thing
       that broke it here: same weight, and the .wrap rule simply comes later. */
    .footer .footer__inner { padding-block: 18px; }
    .footer__legal { grid-column: 1 / -1; grid-row: 2; }
    .footer__social { grid-column: 2; grid-row: 1; }
}

/* Tebex's mandatory merchant-of-record footer. It must not be hidden — doing so
   breaks the terms and gets the store pulled — but it does take these. */
:root {
    --tebex-legal-footer-background-color: #07060c;
    --tebex-legal-footer-text-color: rgb(255 255 255 / 0.55);
    --tebex-legal-footer-border-color: rgb(255 255 255 / 0.09);
    --tebex-legal-footer-max-width: 1240px;
    --tebex-legal-footer-logo-color: rgb(255 255 255 / 0.55);
}

/* --- hero side ------------------------------------------------------------ */
.hero__side { display: grid; gap: 26px; align-content: center; }

/* The quote sits under the showcase rather than in the prose column: the left
   side is already carrying the headline and the pitch, and this is the line the
   studio would want read last. */
.pullquote--hero { margin: 0; padding: 0 0 0 26px; max-width: none; }
.pullquote--hero::before { font-size: 76px; top: -18px; left: -6px; }
.pullquote--hero p { font-size: clamp(18px, 1.7vw, 21px); margin-bottom: 10px; }
.pullquote--hero cite { font-size: 12px; }

.nav__count {
    padding: 1px 7px;
    border-radius: 999px;
    background: rgb(var(--violet-rgb) / 0.16);
    color: var(--violet-pale);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

/* --- adoption (heartbeat.re) ---------------------------------------------- */
/* Never rendered empty: the element ships hidden and only scripts.js reveals
   it, and only when the number is above the floor. */
.adopt {
    display: flex;
    align-items: center;
    gap: 9px;
    margin: 0;
    font-family: var(--font-display);
    font-size: 13.5px;
    font-weight: 600;
    letter-spacing: 0.3px;
    color: var(--text-faint);
}
.adopt strong { color: var(--good); font-weight: 700; font-variant-numeric: tabular-nums; }
.adopt__dot {
    width: 6px;
    height: 6px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--good);
    box-shadow: 0 0 0 3px rgb(159 245 176 / 0.16);
    animation: breathe 2.6s ease-in-out infinite;
}
.adopt--lg {
    align-items: flex-start;
    padding: 13px 15px;
    border-radius: var(--r-sm);
    border: 1px solid rgb(159 245 176 / 0.22);
    background: rgb(159 245 176 / 0.06);
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--text-dim);
}
.adopt--lg .adopt__dot { margin-top: 6px; }

/* --- catalogue filters ---------------------------------------------------- */
.filters {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    margin-bottom: 22px;
}
/* The search takes everything left over, which puts the sort control hard
   against the right edge without either of them needing a margin to get there.
   It was capped at 380px to leave room for a result count that no longer
   exists, and a half-width search bar beside a gap read as unfinished.

   Both controls are pinned to one height. Left to themselves the input came out
   at 49px from its padding and the segmented control at 45px from its own, and
   two controls in a row that miss each other by four pixels look like a
   mistake rather than a choice. */
.filters { --control-h: 46px; }
.filters__search { position: relative; flex: 1 1 260px; display: flex; align-items: center; }
.filters__search input { height: var(--control-h); padding-top: 0; padding-bottom: 0; }
.filters .seg { flex-shrink: 0; height: var(--control-h); align-items: center; }
.filters__search .i { position: absolute; left: 13px; color: var(--text-faint); pointer-events: none; }
.filters__search input { padding-left: 40px; padding-right: 40px; }
.filters__kbd {
    position: absolute;
    right: 11px;
    padding: 1px 7px;
    border-radius: 5px;
    border: 1px solid var(--hair-soft);
    background: rgb(255 255 255 / 0.04);
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-faint);
    pointer-events: none;
}

/* Segmented control. A native <select> is the one element that would look like
   it came from a different website. */
.seg {
    display: flex;
    gap: 2px;
    padding: 3px;
    border-radius: var(--r-sm);
    border: 1px solid var(--hair-soft);
    background: rgb(var(--ink) / 0.5);
}
.seg__btn {
    padding: 7px 14px;
    border-radius: 6px;
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.3px;
    color: var(--text-faint);
    white-space: nowrap;
    transition: color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out);
}
.seg__btn:hover { color: var(--text); background: rgb(255 255 255 / 0.05); }
.seg__btn.is-active {
    color: var(--violet-pale);
    background: rgb(var(--violet-rgb) / 0.16);
    box-shadow: inset 0 0 0 1px rgb(var(--violet-rgb) / 0.26);
}

/* --- lightbox -------------------------------------------------------------- */
/* The cursor is the whole affordance. A magnifier badge or a caption bar over
   the corner would sit on top of the one thing the panel exists to show, and
   the pointer already says it in every image viewer anyone has used.

   The ring goes on the container with an offset, because the container clips
   its own overflow and an inset shadow on the image would be cut off by it. */
.pd__media--zoom { cursor: zoom-in; }
.pd__media--zoom:focus-visible {
    outline: 2px solid var(--violet-lit);
    outline-offset: 3px;
}

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 90;
    display: none;
    align-items: center;
    justify-content: center;
    /* Named, because the image below has to know it: its height ceiling is the
       viewport less this inset, and the two drifting apart is what let the frame
       grow taller than the picture inside it. */
    --lb-inset: 72px;
    padding: var(--lb-inset) 76px;
    background: rgb(4 3 8 / 0.94);
    backdrop-filter: blur(6px);
}
.lightbox.is-open { display: flex; animation: lb-in 0.22s var(--ease-out); }
@keyframes lb-in { from { opacity: 0; } }

.lightbox__bar {
    position: absolute;
    top: 18px;
    left: 22px;
    right: 22px;
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.lightbox__frame {
    max-width: 100%;
    max-height: 100%;
    display: flex;
    /* Not the default stretch. As a flex line the image was being pulled to the
       frame height and held at its own width, so a 16:9 shot sat in a 2.1:1 box
       — object-fit kept the picture honest and the radius and shadow wrapped the
       empty bands either side of it. The frame has to hug the picture. */
    align-items: center;
    justify-content: center;
    border-radius: var(--r);
    overflow: hidden;
    box-shadow: var(--shadow-panel);
}
.lightbox__frame img {
    display: block;
    max-width: 100%;
    max-height: calc(100svh - var(--lb-inset) * 2);
    width: auto;
    height: auto;
}
.lightbox__frame iframe { width: min(1100px, 88vw); aspect-ratio: 16 / 9; height: auto; border: 0; }

.lightbox__nav { position: absolute; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; }
.lightbox__nav--prev { left: 18px; }
.lightbox__nav--next { right: 18px; }

/* --- reveal ---------------------------------------------------------------- */
/* The class only ever adds movement. Without JavaScript, without an
   IntersectionObserver, or under reduced motion it is never applied and
   everything is simply visible. */
.reveal { opacity: 0; transform: translateY(14px); }
.reveal.is-in {
    opacity: 1;
    transform: none;
    transition: opacity 0.5s var(--ease-out), transform 0.55s var(--ease-out);
}

/* --- custom pages ---------------------------------------------------------- */
.cms { padding-bottom: 20px; }
.cms__head { margin-bottom: 30px; }
.cms__body > * { max-width: 70ch; }
/* Breaks the measure on purpose — structure, not prose. */
.cms__body > .stats { max-width: none; }
.cms__body > h2 { margin-top: 62px; margin-bottom: 4px; font-size: clamp(23px, 2.6vw, 31px); }
.cms__body > .stats { margin: 34px 0 8px; }
/* .cms__body > * is 0,2,0 and was overriding .lede's own measure with 70ch. */
.cms__body > .lede { max-width: 62ch; }

/* ==========================================================================
   ABOUT PAGE — CHAPTERS (design candidate)
   ========================================================================== */
/* Seven chapters, one on screen at a time, moved through horizontally.

   The page is a reading surface, not a slideshow, so the chrome is two text
   buttons and a hairline. No dots, no counters, no cards, no frame — the only
   thing that says "there is more" is the name of the chapter you are about to
   read, which is the most useful thing it could say.

   Composition varies only where the content asks for it: chapter 02 splits the
   two of us, chapter 06 sets the figures. Everything else is two columns of
   editorial text under its title, because a chapter has to fit a screen and a
   single 1184px line would be unreadable anyway. */

.cms__body > .ab-chapters { max-width: none; margin-top: 0; }

/* The About page has to start where the homepage starts, and .hero does that
   with a mechanism rather than a number: min-height of a full viewport below
   the header, contents centred inside it. Its eyebrow therefore sits 145px
   below the header on a 950px screen and 390px below on a 1440p one.

   A fixed offset here matched at one height and drifted 245px at the other,
   which is what made moving between the two pages feel like moving between
   two sites. The chapter block now uses the same recipe, so the two track
   each other at every viewport height instead of at one.

   105px is what sits above it inside the wrap: 32 of .wrap padding, the
   breadcrumb's 23 and its 18 of margin, plus 32 of breathing room at the
   bottom so the block never runs to the very edge. */
.ab-chapters.is-ready {
    min-height: calc(100svh - var(--header-h) - 105px);
    display: flex;
    flex-direction: column;
    justify-content: center;
}
.cms:has(.ab-chapters) .cms__body { margin-top: 0; }
.cms:has(.ab-chapters) .cms__head { display: none; }

/* Until scripts.js says otherwise every chapter is simply in the page, in
   order. Nothing is hidden by CSS alone — without JavaScript this reads as a
   normal long About page rather than as one chapter and six missing ones. */
.ab-chapters .ch + .ch { margin-top: 46px; }

.ab-chapters.is-ready .ch { display: none; }
.ab-chapters.is-ready .ch.is-active { display: block; }
.ab-chapters.is-ready .ch + .ch { margin-top: 0; }

/* One screen, and the controls are welded to the bottom of it. The height is
   measured by scripts.js into --ch-h rather than assumed, because what sits
   above the block (header, breadcrumb) is not fixed. The fallback keeps the
   block sane if that measurement never runs.

   overflow-y on the stage is a safety valve, not the plan: the type scale
   below is tuned so a chapter fits without it. But a visitor on a short laptop
   with a long chapter must still be able to reach Next, and a stage that
   scrolls inside its own box always can — the nav cannot be pushed off screen
   by content, ever. */
.ab-chapters.is-ready .ch__stage {
    /* Set by scripts.js to the height of the tallest chapter, capped at what is
       actually left on screen. Tallest-chapter rather than full-height is what
       keeps the nav still between chapters without opening a void under a short
       one; the cap is what keeps the nav on screen on a laptop. */
    flex: 0 0 auto;
    height: var(--ch-stage, auto);
    overflow-y: auto;
    /* No visible bar. The scale below is tuned so a chapter fits and this never
       has to move; when a very short window forces it, it stays invisible
       rather than putting a native scrollbar through the middle of the page.
       Same treatment .nav and .rail already get. */
    scrollbar-width: none;
    -ms-overflow-style: none;
}
.ab-chapters.is-ready .ch__stage::-webkit-scrollbar { display: none; }

/* On a short window the longest chapter can still need a few lines more than
   the screen has. Hiding the scrollbar without saying so would hide the fact
   that there is more, so the last 44px of the stage fade out — the reader gets
   the same cue the catalogue rail gives at the end of its track. Only applied
   when the stage is actually clipped, which on a normal desktop is never. */
.ab-chapters.is-clipped .ch__stage {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 44px), transparent);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 44px), transparent);
}
/* Clear of the stage, not welded to it. The stage is as tall as the LONGEST
   chapter, so on that one the last line of copy ends exactly at its floor —
   with no margin the rule above the controls landed hard against the text.
   scripts.js counts this margin when it works out how much room is left, so
   the gap never costs the nav its place on screen. */
.ab-chapters.is-ready .ch__nav { margin-top: min(38px, 4vh); }


/* --- chapter ------------------------------------------------------------- */

/* The same mark the homepage hero opens with, down to the 60px rule: label,
   12px gap, then a short hairline fading out to the right. Not flex:1 — a
   rule stretched across 1184px fades to nothing almost immediately and reads
   as a faint smear rather than the deliberate tick the hero makes. Paired
   with .cap--hi on the label, this is the gesture that makes arriving here
   from the homepage feel like the same site. */
.ab-chapters .ch__eyebrow {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 min(11px, 1.3vh);
}
.ab-chapters .ch__eyebrow::after {
    content: "";
    width: 60px;
    height: 1px;
    background: linear-gradient(90deg, var(--hair), transparent);
}

/* 0,2,1 — beats .rich-text h2 (0,1,1) outright rather than on source order. */
/* One line, across the page. The size is driven by vw rather than a fixed
   clamp ceiling because the constraint is not "how big should this be" but
   "the longest of the seven headings has to fit on one line at every width".
   nowrap enforces it; the vw term keeps the promise.

   The 60px ceiling is the binding constraint, not taste: .wrap stops at 1240
   (1184 of content), so past a ~1300px viewport the column stops growing while
   vw does not. At 2560px, 3.45vw is 84px, and the longest heading then measures
   1570px against 1184 of column — five of the seven overflowed. At 60px it
   measures 1121px and fits with 63px to spare. Both nowrap and the vw term are
   dropped below 900px, where one line across a phone is not worth wanting. */
.ab-chapters .ch h2.ch__title {
    margin: 0;
    white-space: nowrap;
    font-family: var(--font-display);
    font-size: min(3.45vw, 60px, 6vh);
    font-weight: 700;
    line-height: 1.04;
    letter-spacing: -0.018em;
    color: var(--text);
}

/* The one paragraph that sits at full measure under the title: it is the
   chapter's thesis, so it is set larger and brighter than the columns below,
   and it is the only thing between the heading and the body. */
.ab-chapters .ch__intro {
    margin: min(18px, 2vh) 0 0;
    font-size: min(clamp(16.5px, 1.5vw, 19px), 2.4vh);
    line-height: 1.55;
    color: var(--text);
}

/* Two columns, because the alternative at this width is a 140-character line.
   The rule above them is the same hairline the homepage uses beside a section
   heading, so the break between the thesis and the body is a mark the visitor
   has already seen. */
.ab-chapters .ch__cols {
    margin-top: min(18px, 2vh);
    padding-top: min(16px, 1.8vh);
    border-top: 1px solid var(--hair-soft);
    columns: 1;
}
.ab-chapters .ch__cols p {
    margin: 0 0 min(13px, 1.5vh);
    font-size: min(16px, 1.95vh);
    /* A full-width line is a long line, so the leading goes up with the measure
       — the same adjustment the rest of the store makes when text runs the
       width of the page. */
    line-height: 1.78;
    color: var(--text-dim);
}
.ab-chapters .ch__cols p:last-child { margin-bottom: 0; }
.ab-chapters .ch__cols strong { color: var(--text); font-weight: 600; }
.ab-chapters .ch__cols em { font-style: normal; color: var(--violet-pale); }

/* The chapter's last word. A step up in brightness and size, and a short tick
   above it — the same gesture as .section__rule, not a quote glyph or a box. */
.ab-chapters .ch__close {
    margin: min(20px, 2.2vh) 0 0;
    padding-top: min(16px, 1.8vh);
    font-size: min(clamp(16px, 1.5vw, 18px), 2.2vh);
    line-height: 1.5;
    color: var(--text);
}
.ab-chapters .ch__close::before {
    content: "";
    display: block;
    width: 34px;
    height: 1px;
    margin-bottom: min(14px, 1.6vh);
    background: linear-gradient(90deg, var(--hair), transparent);
}


/* --- chapter 02: the two of us ------------------------------------------- */
/* Two people, so two columns — an explicit grid rather than flowed text,
   because which half a sentence lands in matters here and nowhere else. */
.ab-chapters .ch__split {
    margin-top: min(18px, 2vh);
    padding-top: min(16px, 1.8vh);
    border-top: 1px solid var(--hair-soft);
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: min(26px, 3vh) 58px;
}
.ab-chapters .ch__who h3 {
    margin: 0;
    font-family: var(--font-display);
    font-size: min(25px, 3.1vh);
    font-weight: 700;
    line-height: 1.1;
    color: var(--text);
}
.ab-chapters .ch__figure {
    margin: 4px 0 min(10px, 1.2vh);
    padding-bottom: min(9px, 1.1vh);
    border-bottom: 1px solid var(--hair-soft);
    font-family: var(--font-display);
    font-size: 14.5px;
    font-weight: 600;
    color: var(--text-faint);
}
.ab-chapters .ch__figure .num { font-size: 20px; color: var(--violet-pale); margin-right: 5px; }
/* Chapter 02 is the longest in the set — two people, three paragraphs each —
   and it is the only one that ever runs past the stage. It gets its own,
   slightly denser setting so the other six are not shrunk to solve it. */
.ab-chapters .ch__who p {
    margin: 0 0 min(11px, 1.3vh);
    font-size: min(15.5px, 1.9vh);
    line-height: 1.6;
    color: var(--text-dim);
}
.ab-chapters .ch__who p:last-child { margin-bottom: 0; }
/* --- chapter 06: the figures --------------------------------------------- */
/* The numbers are the argument in this chapter, so they are set as numbers
   rather than buried in a sentence. No tiles: a row of figures on a hairline. */
.ab-chapters .ch__figs {
    margin-top: min(20px, 2.2vh);
    padding-top: min(18px, 2vh);
    border-top: 1px solid var(--hair-soft);
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 22px;
}
.ab-chapters .ch__fig { display: grid; gap: 5px; align-content: start; }
.ab-chapters .ch__fig-n {
    font-family: var(--font-display);
    font-size: min(clamp(24px, 2.6vw, 34px), 4.6vh);
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
    color: var(--text);
}
/* The figures already sit on their own rule; a second one directly under them
   would box them in. */
.ab-chapters .ch__figs + .ch__cols { border-top: 0; padding-top: 0; }

/* Going straight through to the basket. Brief, but the card should look like
   it is doing something rather than sitting inert for a beat. */
.form-card.is-continuing { opacity: 0.5; pointer-events: none; transition: opacity var(--t) var(--ease-out); }
/* Except the identity. It is the one control on this card whose whole job is
   to say "not me", and the moment the page is committing to a purchase is
   exactly the wrong moment to turn it off. */
.form-card.is-continuing .discord-id { pointer-events: auto; opacity: 1; }

/* --- discord identity, options page --------------------------------------- */
/* Built from the same measurements as .field's inputs — 11px/14px, --ink at
   60%, a --border hairline, --r-sm, and the violet focus ring — so the row
   reads as another control in the form rather than a widget bolted onto it.
   Blurple appears once, on the mark. */
.discord-id {
    display: flex;
    align-items: center;
    gap: 11px;
    width: 100%;
    padding: 11px 14px;
    background: rgb(var(--ink) / 0.6);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    color: var(--text);
    font: inherit;
    text-decoration: none;
    transition: border-color var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out);
}
a.discord-id:hover,
a.discord-id:focus-visible {
    outline: none;
    border-color: var(--border-lit);
    box-shadow: 0 0 0 3px rgb(var(--violet-rgb) / 0.15);
}

.discord-id__mark { width: 19px; height: 19px; flex-shrink: 0; fill: #5865f2; }
.discord-id__label { flex: 1; min-width: 0; font-size: 15px; }
.discord-id__end {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: 0.45;
}

/* Connected reads as settled: the name carries weight and the tick is the one
   green thing on the page, so it is unmistakable at a glance. */
.discord-id.is-connected { cursor: default; }
/* Recalled from this browser rather than rendered by Tebex, so it is still a
   link — clicking it signs in as somebody else. */
.discord-id--recalled { cursor: pointer; }
.discord-id--recalled:hover {
    border-color: var(--border-lit);
    box-shadow: 0 0 0 3px rgb(var(--violet-rgb) / 0.15);
}
.discord-id.is-connected .discord-id__label { font-weight: 600; }
.discord-id.is-connected .discord-id__end { stroke: var(--good); opacity: 1; }

/* Connected is a statement, not a field. It sits a little taller than the
   inputs it was modelled on and says the word out loud, so nothing about it
   invites a customer to fill it in again. */
.discord-id.is-connected { padding-block: 13px; }
.discord-id__state { display: inline-flex; align-items: center; gap: 8px; flex-shrink: 0; }
.discord-id__verified {
    display: none;
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    color: var(--good);
}
.discord-id.is-connected .discord-id__verified { display: inline; }
/* A long username shortens rather than shoving the verdict off the row. */
.discord-id__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* One line, and it belongs to the row below it rather than to the form. */
.verify-note {
    margin: 0 0 2px;
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--text-dim);
}

/* The purchase cannot be fulfilled without the Discord identity, so the button
   that starts it is not offered until that is done — a disabled control the
   customer cannot explain is worse than no control.

   :has() rather than a Twig flag because a "set" tag inside a for-loop does
   not escape the loop's scope, and the condition lives inside exactly such a
   loop. (Written without braces on purpose: Tebex parses assets as Twig, so
   Twig tag syntax in a CSS comment is a syntax error, not a comment.)
   Where :has() is unsupported the button simply shows, which is where we were. */
.form-card form:has(.discord-id:not(.is-connected)) button[type="submit"] { display: none; }
.discord-gate { display: none; }
.form-card form:has(.discord-id:not(.is-connected)) .discord-gate {
    display: block;
    margin: 0;
    font-size: 13.5px;
    color: var(--text-faint);
    text-align: center;
}

/* --- navigation ---------------------------------------------------------- */
/* Two text buttons and a hairline between them. The buttons say only what they
   do; the rule reports how much is left and is easy to ignore. Nothing here
   competes with the chapter above it. */
.ab-chapters .ch__nav {
    display: none;
    margin-top: 26px;
    padding-top: min(14px, 1.6vh);
    border-top: 1px solid var(--hair-soft);
    align-items: center;
    gap: 24px;
}
.ab-chapters.is-ready .ch__nav { display: flex; }

.ab-chapters .ch__nav .ch__step {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 9px 2px;
    border: 0;
    background: none;
    cursor: pointer;
    /* These are anchors on the About page — <button> does not survive the
       panel sanitiser — so the link defaults have to be undone. */
    text-decoration: none;
    font-family: var(--font-display);
    font-size: 14.5px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--text-faint);
    transition: color var(--t-fast) var(--ease-out);
}
.ab-chapters .ch__nav .ch__step:hover:not(:disabled):not(.is-disabled) { color: var(--text); }
.ab-chapters .ch__nav .ch__step:disabled,
.ab-chapters .ch__nav .ch__step.is-disabled { opacity: 0.32; cursor: default; pointer-events: none; }
.ab-chapters .ch__nav .ch__step .i { width: 17px; height: 17px; }
.ab-chapters .ch__nav .ch__step--next { margin-left: auto; }

/* On the last chapter Next has nowhere to go — the story is over. So there it
   is replaced by the thing the reader would actually want next, in the same
   place and the same type as every other control on the page.

   It is not a .btn. This design has no filled objects anywhere: seven chapters
   of type and hairlines, and a near-white slab was the single heaviest thing
   on it — which is why it read fine in the hero, where it sits beside an
   image and a second button, and wrong here. Violet-pale marks it as the one
   live control in the row.

   Placed after .ch__step on purpose: both this and .ch__step are two classes
   deep, so the one further down the sheet wins. Above it, display:none lost
   to .ch__step's inline-flex and the CTA showed on all seven. */
.ab-chapters .ch__nav .ch__step--cta { display: none; }
.ab-chapters.is-last .ch__nav .ch__step--next { display: none; }
/* 0,3,0 — beats .rich-text a:not(.btn) (0,2,1), which would otherwise
   underline it and repaint it. */
.ab-chapters.is-last .ch__nav .ch__step--cta {
    display: inline-flex;
    margin-left: auto;
    color: var(--violet-pale);
    text-decoration: none;
}
.ab-chapters.is-last .ch__nav .ch__step--cta:hover { color: var(--violet-lit); }

/* 120px of hairline, not a bar across the page. It reports position without
   asking to be read. */
.ab-chapters .ch__track {
    width: 120px;
    height: 2px;
    border-radius: 2px;
    background: rgb(255 255 255 / 0.1);
    overflow: hidden;
}
.ab-chapters .ch__fill {
    display: block;
    height: 100%;
    border-radius: 2px;
    background: var(--brand-ramp);
    transition: width var(--t) var(--ease-out);
}

/* --- the move between chapters ------------------------------------------- */
/* Only the arriving chapter animates. The leaving one is simply gone, which
   keeps the stage a single element with no absolute positioning and no height
   to reconcile. 18px is a nudge in the direction of travel, not a slide.
   The sheet's prefers-reduced-motion block already switches this off. */
@keyframes ch-in-right { from { opacity: 0; transform: translateX(18px); } }
@keyframes ch-in-left  { from { opacity: 0; transform: translateX(-18px); } }
.ab-chapters .ch.is-in-right { animation: ch-in-right 0.34s var(--ease-out); }
.ab-chapters .ch.is-in-left  { animation: ch-in-left  0.34s var(--ease-out); }

/* ==========================================================================
   PRODUCT PAGE — CHAPTERS (design candidate)
   ========================================================================== */
/* The About page's grammar applied to a product, where media carries as much
   weight as the prose: one feature on screen at a time, title first, the
   screenshot beside it at a size you can actually read an interface in.

   Deliberately its own namespace rather than a refactor of .ab-chapters. The
   About page is signed off; the shared chrome here (eyebrow, title, nav) is a
   few dozen lines of duplication, and that is a smaller price than reaching
   into a page that is finished. The JavaScript IS shared — scripts.js drives
   any [data-chapters] root — so the behaviour cannot drift between them.

   Nothing here is Diving-specific. Chapters come from the package description,
   so a product with three of them renders three. */

/* --- purchase bar --------------------------------------------------------- */
/* Name, price, one button. The old buy box carried a spec list, a licence row,
   documentation and Discord links and an adoption figure; none of that is what
   somebody is deciding on in the first three seconds, and all of it was taking
   up the space the product itself should have.

   It is sticky from the start rather than a second bar that appears later: one
   element, two states, so the control never moves out from under the cursor. */
.pbar {
    position: sticky;
    top: var(--header-h);
    z-index: 40;
    margin-bottom: 6px;
    border-bottom: 1px solid transparent;
    transition: padding var(--t) var(--ease-out), background var(--t) var(--ease-out),
                border-color var(--t) var(--ease-out);
}
.pbar__inner {
    display: flex;
    align-items: center;
    gap: 24px;
    flex-wrap: wrap;
    padding-block: 22px 20px;
    transition: padding var(--t) var(--ease-out);
}

/* A stack, not a row: the name with its proposition under it, and the same
   17px gap the catalogue leaves between its own title and description. */
.pbar__id { display: grid; gap: 17px; min-width: 0; }
.pbar__name {
    margin: 0;
    font-family: var(--font-display);
    font-size: clamp(26px, 2.6vw, 34px);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.015em;
    color: var(--text);
    transition: font-size var(--t) var(--ease-out);
}
/* The line that connects this page to the catalogue, where every product
   already carries one. Body face, not a second heading — it is a sentence about
   the product, and setting it in Rajdhani would make it compete with the name
   directly above it. Capped well short of the page so it stays one line on a
   normal desktop and never runs under the price block. */
.pd-summary {
    margin: 0;
    max-width: 720px;
    font-size: 17px;
    line-height: 1.75;
    color: var(--text-dim);
}

/* Price and button on the top line; what the discount is worth on the line
   under the price. Two levels, and the button lands beside the number rather
   than beside the whole group.

   A grid, not a flex row, because that is what puts the button on the price's
   line while the saving hangs below it without a margin invented to hold
   either in place. .pbar__price stays in the markup — the saving belongs to
   the price, not to the row — but goes display: contents so the grid can place
   all three against one another. */
.pbar__buy {
    display: grid;
    grid-template-columns: auto auto;
    align-items: center;
    column-gap: 26px;
    /* Nothing: the button is taller than the price's line box, and the slack
       it leaves under that line is the gap. Add to it and the saving drifts
       away from the number it belongs to. */
    row-gap: 0;
    margin-left: auto;
}
.pbar__price { display: contents; }
.pbar__buy .price { grid-column: 1; grid-row: 1; justify-self: end; row-gap: 0; gap: 11px; }
.pbar__buy .price__now { color: var(--text); }
.pbar__buy .btn { grid-column: 2; grid-row: 1; }

/* Commercial rather than editorial: the line box is closed up so the number
   reads as a figure on a shelf edge and not as a heading. */
.pbar .price--lg .price__now { font-size: clamp(26px, 2.4vw, 32px); line-height: 1.04; }

/* The currency code is the least of the three facts — the symbol in front of
   the number has already said it. Small, quiet, on the price's own baseline,
   and far enough off the last digit that it does not read as part of it. */
.price__code {
    font-family: var(--font-display);
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: 0.09em;
    color: var(--text-faint);
}

/* The saving, as money and then as a percentage. Text, not a badge: by this
   point the visitor is deciding rather than browsing, and the catalogue's
   outlined pill is a discovery device — it labels a card you are scanning
   past. Here the useful thing is the number itself.

   The amount takes the accent because it is what persuades; the percentage is
   corroboration and stays in ordinary body colour, quieter than the price it
   sits under and never louder than it. */
.pbar__save {
    grid-column: 1;
    grid-row: 2;
    justify-self: end;
    display: flex;
    align-items: baseline;
    gap: 7px;
    margin: 0;
    font-size: 13.5px;
    white-space: nowrap;
}
.pbar__save-amt { font-weight: 600; color: var(--violet-lit); }
.pbar__save-sep { color: rgb(var(--violet-rgb) / 0.55); }
.pbar__save-pct { color: var(--text-dim); }

/* Pinned: the same bar, quieter and thinner, reading as a shelf under the
   header rather than a card floating over the page. The blur is the header's
   own recipe — this is meant to look like the header grew a row, not like a
   purchase widget followed you down. */
.pbar.is-pinned {
    background: rgb(10 8 16 / 0.82);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    border-bottom-color: var(--hair-soft);
}
.pbar.is-pinned .pbar__inner { padding-block: 11px; }
.pbar.is-pinned .pbar__name { font-size: 19px; }
/* Everything that is not needed to buy. The proposition and the saving are
   for a first read; once the reader is in the chapters the bar is a control,
   and a control that keeps advertising is in the way. Name, price, currency,
   button — the collapsed row is for somebody who has already decided. */
.pbar.is-pinned .pbar__summary { display: none; }
.pbar.is-pinned .pbar__save { display: none; }
.pbar.is-pinned .price--lg .price__now { font-size: 21px; }

/* --- the story ------------------------------------------------------------ */
/* No margin between chapters: only one is ever on screen, and the stage
   carries .rich-text, which hands every top-level block a top margin. The
   script measures offsetHeight, which does not see margins — so 14px of
   inherited spacing was overflowing a stage sized to fit exactly. */
.pd-chapters .pd { display: none; margin: 0; }
.pd-chapters .pd.is-active { display: block; }
/* Without JavaScript every chapter is simply in the page in order — a long
   product page, which is the correct thing to degrade to. */
.pd-chapters:not(.is-ready) .pd { display: block; }
.pd-chapters:not(.is-ready) .pd + .pd { margin-top: 54px; }

/* Top-aligned rather than centred. About can centre because only 73px sits
   above it; this page carries a breadcrumb, a purchase bar and a trust strip,
   so centring the remainder could only ever push the chapter lower. */
.pd-chapters.is-ready {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    /* Enough to separate the story from the purchase area, not enough to make
       the chapter look like it belongs to a different page. */
    padding-top: min(44px, 3.4vh);
}
.pd-chapters.is-ready .ch__stage {
    flex: 0 0 auto;
    height: var(--ch-stage, auto);
    overflow-y: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
}
.pd-chapters.is-ready .ch__stage::-webkit-scrollbar { display: none; }

/* --- chapter -------------------------------------------------------------- */
/* The same opening mark the homepage hero and the About chapters make. */
.pd-chapters .ch__eyebrow {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 min(11px, 1.3vh);
}
.pd-chapters .ch__eyebrow::after {
    content: "";
    width: 60px;
    height: 1px;
    background: linear-gradient(90deg, var(--hair), transparent);
}

/* 0,2,1 — beats .rich-text h2 (0,1,1) outright rather than on source order,
   which matters because the chapters arrive inside .rich-text. */
.pd-chapters .pd h2.ch__title {
    margin: 0;
    white-space: nowrap;
    font-family: var(--font-display);
    font-size: min(2.9vw, 46px, 5.2vh);
    font-weight: 700;
    line-height: 1.06;
    letter-spacing: -0.018em;
    color: var(--text);
}

/* Prose left, screenshot right. The text column is deliberately the smaller
   half: the copy is three or four lines and the screenshot is the argument.
   0.72/1.28 of 1128 gives roughly 406px of text — about 48 characters, which
   is short but correct for a caption-length paragraph — and 722px of media,
   enough to read a FiveM interface at 16:9.

   --ch-media is a ceiling the script sets when the chapter is taller than the
   screen it is on. The media track stops growing at that width and the prose
   column, being the only fr track left, takes back what it released — so the
   row stays full width and no void opens between the two. Unset, which is
   every screen with room to spare and every visitor without the script, the
   pair falls back to the ratio it was designed at.

   1fr/1.778fr rather than 0.72/1.28 — the same proportion, but summing above 1.
   Flex factors that add up to less than one are treated as a fraction of the
   leftover rather than a share of it, so once the media track is a fixed width
   the prose track would take only 72% of what it released and leave the rest as
   a void off the right edge. */
.pd__grid {
    margin-top: min(22px, 2.4vh);
    padding-top: min(20px, 2.2vh);
    border-top: 1px solid var(--hair-soft);
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, var(--ch-media, 1.778fr));
    column-gap: 54px;
    align-items: start;
}

.pd__text { display: grid; gap: min(13px, 1.5vh); align-content: start; }
.pd__text p {
    margin: 0;
    font-size: min(16px, 1.95vh);
    line-height: 1.7;
    color: var(--text-dim);
}
.pd__text strong { color: var(--text); font-weight: 600; }

/* --- media ---------------------------------------------------------------- */
/* A hairline and a radius, and that is all. No frame, no shadow, no browser
   chrome: the screenshot is evidence, and anything drawn around it competes
   with the interface it is meant to be showing. aspect-ratio rather than a
   height, so the row is the same height on every chapter and the nav below it
   never moves as you page through. */
.pd__media {
    margin: 0;
    border-radius: var(--r-lg);
    border: 1px solid var(--hair-soft);
    overflow: hidden;
    background: var(--surface-1);
    aspect-ratio: 16 / 9;
}
/* The reset matters: the stage carries .rich-text so a product without
   chapters still renders its description as prose, and .rich-text img brings a
   frame with it — margin-top: 20px, a radius and a card shadow. Inside a 16:9
   box with overflow:hidden that pushed every screenshot 20px down, showed the
   container edge above it and clipped the bottom. The iframe escaped because
   .rich-text has no rule for one. */
.pd__media img,
.pd__media iframe {
    display: block;
    margin: 0;
    border-radius: 0;
    box-shadow: none;
    width: 100%;
    height: 100%;
    border: 0;
    object-fit: cover;
}

/* --- details, below the story --------------------------------------------- */
/* Reference material: the things that are true whether or not you have seen a
   screenshot. It sits under the chapters and scrolls normally, because this is
   the part somebody reads once, before buying, rather than pages through.

   A label/value list, not tiles and not a spec checklist — the two things the
   old buy box did. The label column is Rajdhani at .cap size so it reads as
   structure, and the value carries the sentence. */
/* Close enough that the rule and the DETAILS label land at or just below the
   fold on a normal screen, which is the whole affordance: a visitor who can see
   a hairline and a label has been told there is more, without a scroll arrow, a
   chevron or a read-on widget. */
.pd-details {
    margin-top: 38px;
    padding-top: 30px;
    border-top: 1px solid var(--hair-soft);
    padding-bottom: 20px;
}
.pd-details__title {
    margin: 0 0 30px;
    font-family: var(--font-display);
    font-size: clamp(24px, 2.4vw, 34px);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.015em;
    color: var(--text);
}

/* One short paragraph between the title and the table: what the report covers,
   without claiming more than it lists. */
.pd-details__lede {
    margin: -18px 0 30px;
    max-width: 74ch;
    font-size: 16px;
    line-height: 1.7;
    color: var(--text-dim);
}

/* A report, not a matrix. Rows on hairlines, category on the left, the actual
   resource names on the right — no ticks, no empty columns for things this does
   not integrate with, and nothing that has to be read left-to-right to be
   understood. A server owner scanning for "does it do ox_inventory" finds it
   without reading a single label. */
.pd-compat {
    margin: 0;
    display: grid;
    grid-template-columns: 168px minmax(0, 1fr);
    column-gap: 40px;
}
.pd-compat dt {
    padding: 15px 0;
    border-top: 1px solid var(--hair-soft);
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 1.6px;
    text-transform: uppercase;
    color: var(--violet-pale);
}
.pd-compat dd {
    margin: 0;
    padding: 11px 0 12px;
    border-top: 1px solid var(--hair-soft);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 7px;
}

/* The store already sets code this way inside .rich-text; these rows are not
   inside it, so the treatment is restated rather than a new one invented. */
.pd-compat code {
    font-family: var(--font-mono);
    font-size: 13px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 4px 9px;
    color: var(--text);
}
.pd-compat__more { margin: 26px 0 0; }

/* --- navigation ----------------------------------------------------------- */
/* The About page's controls, unchanged in spirit: two text buttons and a short
   hairline. On the last chapter Next has nowhere to go, so it becomes the only
   thing worth doing next — Add to basket — in the same place and the same type,
   which is also the second purchase point on the page without adding a widget
   to carry it. */
.pd-chapters .ch__nav {
    display: none;
    margin-top: min(34px, 3.6vh);
    padding-top: min(14px, 1.6vh);
    border-top: 1px solid var(--hair-soft);
    align-items: center;
    gap: 24px;
    flex: 0 0 auto;
}
.pd-chapters.is-ready .ch__nav { display: flex; }

.pd-chapters .ch__nav .ch__step {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 9px 2px;
    border: 0;
    background: none;
    cursor: pointer;
    /* These are anchors on the About page — <button> does not survive the
       panel sanitiser — so the link defaults have to be undone. */
    text-decoration: none;
    font-family: var(--font-display);
    font-size: 14.5px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--text-faint);
    transition: color var(--t-fast) var(--ease-out);
}
.pd-chapters .ch__nav .ch__step:hover:not(:disabled):not(.is-disabled) { color: var(--text); }
.pd-chapters .ch__nav .ch__step:disabled,
.pd-chapters .ch__nav .ch__step.is-disabled { opacity: 0.32; cursor: default; pointer-events: none; }
.pd-chapters .ch__nav .ch__step .i { width: 17px; height: 17px; }
.pd-chapters .ch__nav .ch__step--next { margin-left: auto; }

.pd-chapters .ch__track {
    width: 120px;
    height: 2px;
    border-radius: 2px;
    background: rgb(255 255 255 / 0.1);
    overflow: hidden;
}
.pd-chapters .ch__fill {
    display: block;
    height: 100%;
    border-radius: 2px;
    background: var(--brand-ramp);
    transition: width var(--t) var(--ease-out);
}

/* The last chapter has no next one, and nothing takes that place: a product
   page sells from the header and the sticky bar, not from the end of a
   paragraph. (About still ends on a call to action — it has somewhere to send
   people, and those rules are its own.) */
.pd-chapters.is-last .ch__nav .ch__step--next { display: none; }

/* Only the arriving chapter animates; the leaving one is simply gone. Same
   restrained nudge in the direction of travel as About. */
.pd-chapters .pd.is-in-right { animation: ch-in-right 0.34s var(--ease-out); }
.pd-chapters .pd.is-in-left  { animation: ch-in-left  0.34s var(--ease-out); }

/* --- responsive ----------------------------------------------------------- */

@media (max-width: 900px) {
    .checkout__grid { grid-template-columns: minmax(0, 1fr); }
    .checkout__rail { position: static; }
}

@media (max-width: 1080px) {
    .product {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "media" "buy" "body";
        gap: 28px;
    }
    .buybox { position: static; }
    .product__body { margin-top: 0; }
}

@media (max-width: 1180px) { .rail { --per-view: 3; } }
@media (max-width: 880px) { .rail { --per-view: 2; } }

@media (max-width: 980px) {
    .hero__grid { grid-template-columns: minmax(0, 1fr); gap: 40px; }
    /* The rail keeps its own width; the header stops trying to centre it. */
    .header__inner { gap: 16px; }
    .basket-btn__label { display: none; }
}

@media (max-width: 860px) {
    /* Ragged when it wraps — four items never divide evenly into a phone. */
    .assure { grid-template-columns: minmax(0, 1fr); }
    .assure li { justify-content: flex-start; text-align: left; padding: 12px 0; }
    .assure li + li { border-left: 0; border-top: 1px solid var(--hair-soft); }
}

@media (max-width: 720px) {
    .wrap { padding: 0 18px; }

    /* 76px a side leaves a phone about 240px of picture. The close button still
       needs its corner, so the room comes off the sides.

       This lives here, after the base rule, and not with the other narrow
       overrides further up: same weight, so the one further down the sheet is
       the one that applies. Up there it was silently losing. */
    .lightbox { --lb-inset: 64px; padding: var(--lb-inset) 12px; }
    .lightbox__bar { top: 14px; left: 14px; right: 14px; gap: 14px; }

    /* Five sort options do not fit a phone in one row — measured at 391px the
       control ran past the viewport and took the page with it. It gets its own
       row and scrolls sideways, the same way .nav does, rather than wrapping:
       a segmented control broken across two lines stops reading as one thing. */
    .filters .seg {
        flex: 1 1 100%;
        overflow-x: auto;
        scrollbar-width: none;
        -ms-overflow-style: none;
    }
    .filters .seg::-webkit-scrollbar { display: none; }

    /* The rail drops to its own row under the brand rather than into a
       hamburger. A couple of links do not deserve a drawer, and a store's
       categories should not be one tap further away than they have to be. */
    .header { height: auto; }
    /* padding-block for the same reason as .hero — .header__inner is a .wrap,
       and the shorthand would drop its side padding onto the screen edge. */
    .header__inner { flex-wrap: wrap; gap: 12px; padding-block: 12px; }
    .brand img { height: 26px; }
    .nav {
        order: 3;
        flex-basis: 100%;
        /* Hug the pills instead of stretching the rail across the viewport —
           an empty half-width pill track reads as a broken control. */
        width: max-content;
        max-width: 100%;
        overflow-x: auto;
        scrollbar-width: none;
    }
    .nav::-webkit-scrollbar { display: none; }
    /* The desktop link is as tall as the header so its marker can sit on the
       header's own edge. Once the header wraps, that height leaves the marker
       floating in space below the words. */
    .nav__link { flex-shrink: 0; height: auto; padding: 7px 12px 9px; }
    .nav__link::after { left: 12px; right: 12px; border-radius: 2px; }

    /* A partial card on a phone: the peek is the only affordance there is room
       for, and it says "swipe" better than any arrow would. */
    .rail { margin: 0 -18px; padding: 4px 18px; --per-view: 1.15; }

    .hero { padding-top: 34px; }
    .hero__grid { gap: 30px; }
    .stat { padding: 18px; }
    .stat__n { font-size: 24px; }
    .grid { grid-template-columns: 1fr; }
    /* The column header row is meaningless once the row wraps, and the price
       moves under the name rather than fighting the stepper for width. */
    .lines__head { display: none; }
    .lines__row {
        grid-template-columns: minmax(0, 1fr) 78px;
        grid-template-areas: "item qty" "price qty";
        gap: 6px 14px;
        padding-inline: 16px;
    }
    .lines__item { grid-area: item; }
    .lines .qty { grid-area: qty; align-self: center; }
    /* Back to the left here. There are no headings on this layout to line up
       with — the price is indented to sit under the script name instead, and
       the desktop centring would push it off that line. */
    .lines__row .price { grid-area: price; padding-left: 71px; justify-content: flex-start; }
    .lines__x { position: absolute; right: 12px; top: 12px; }
    .lines__row { position: relative; }
    .lines__foot { padding-inline: 16px; }
    .toast { left: 18px; right: 18px; max-width: none; }
}

/* --- about: chapters, tablet --------------------------------------------- */
@media (max-width: 900px) {
    /* One line across a phone is not worth wanting: the title wraps like any
       other and takes its normal size back. */
    /* One line across a phone is not worth wanting: the title wraps like any
       other heading and takes a normal size back. */
    .ab-chapters .ch h2.ch__title { white-space: normal; font-size: min(clamp(28px, 3.4vw, 46px), 6vh); }
    .ab-chapters .ch__split { grid-template-columns: minmax(0, 1fr); gap: 28px; }
    .ab-chapters .ch__figs { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 24px; }
    .ab-chapters .ch h2.ch__title { max-width: none; }
    /* The chapter flows down the page here rather than into a viewport-height
       box with its own scrollbar. One tall column is what a phone is for, and
       the nav travelling with the content is better than a nested scroller. */
    .ab-chapters.is-ready { display: block; min-height: 0; }
    .ab-chapters.is-ready .ch__stage { overflow-y: visible; }
    .ab-chapters.is-ready .ch__nav { margin-top: 30px; }
}

/* --- about: chapters, phone ---------------------------------------------- */
@media (max-width: 560px) {
    .ab-chapters .ch__intro { font-size: 16.5px; }
    .ab-chapters .ch__cols p { font-size: 15px; }
    .ab-chapters .ch__nav { gap: 12px; margin-top: 34px; }
    .ab-chapters .ch__track { width: 64px; }
}

/* --- product chapters, tablet --------------------------------------------- */
@media (max-width: 900px) {
    /* Text above the screenshot rather than beside it. Half of 844px is about
       360px, and the screenshot stops being inspectable long before the prose
       stops being readable — so the media takes the full width and the copy
       introduces it. */
    .pd__grid { grid-template-columns: minmax(0, 1fr); row-gap: 20px; }
    .pd-chapters .pd h2.ch__title { white-space: normal; font-size: min(clamp(26px, 3.4vw, 40px), 6vh); }

    /* The chapter flows down the page here instead of into a viewport-height
       box with its own scroller. One tall column is what a phone is for. */
    .pd-chapters.is-ready { display: block; min-height: 0; }
    .pd-chapters.is-ready .ch__stage { overflow-y: visible; height: auto; }
    .pd-chapters .ch__nav { margin-top: 28px; }
    /* The label column stops being a column: 168px beside a phone-width row of
       resource names leaves the names nowhere to go. */
    .pd-compat { grid-template-columns: minmax(0, 1fr); }
    .pd-compat dt { padding-bottom: 8px; }
    .pd-compat dd { padding-top: 0; padding-bottom: 18px; border-top: 0; }
    .pd-details__lede { margin-top: -10px; }
    .pd-details { margin-top: 40px; padding-top: 28px; }

    /* The bar keeps its two states but loses the name at the small size — on a
       phone the page title is right there and the control is what matters. */
    .pbar__inner { padding-block: 16px 14px; gap: 14px; }
    .pbar__buy { column-gap: 18px; }
    .pd-summary { font-size: 16px; }
}

/* --- product chapters, phone ---------------------------------------------- */
@media (max-width: 560px) {
    .pd-chapters .pd h2.ch__title { font-size: clamp(24px, 7vw, 30px); }
    .pd__text p { font-size: 15px; }
    .pd-chapters .ch__nav { gap: 12px; }
    .pd-chapters .ch__track { width: 56px; }

    /* Name and buy on their own rows: side by side at this width the price and
       the button squeeze the name to two characters and an ellipsis. */
    .pbar__inner { flex-direction: column; align-items: stretch; gap: 12px; }
    /* One column, read in order: what it costs, what that saves, then the
       button across the full width. Side by side, a 26px number and a button
       leave neither of them room. .pbar__price becomes a box again, which
       re-parents the price and the saving and makes their placement on the
       desktop grid inert — only the button needs putting back. */
    .pbar__buy {
        grid-template-columns: minmax(0, 1fr);
        margin-left: 0;
        row-gap: 14px;
    }
    .pbar__price { display: grid; justify-items: start; gap: 5px; }
    .pbar__buy .price,
    .pbar__save { justify-self: start; }
    .pbar__buy .btn { grid-column: 1; grid-row: auto; justify-content: center; }
    .pbar .price--lg .price__now { font-size: 26px; }
    .pd-summary { font-size: 15.5px; }
    .pbar.is-pinned .pbar__name { font-size: 17px; }
    /* Pinned on a phone the name goes entirely — the row is a control, and the
       chapter behind it needs the height more than the label does. */
    .pbar.is-pinned .pbar__id { display: none; }
    .pbar.is-pinned .price--lg .price__now { font-size: 19px; }
}

/* --- motion preferences --------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
    }
}
