/* GladHub storefront design system.
 * Shared type, surfaces, controls and responsive layouts.
 * Theme and FX sections are generated; edit their source builders.
 */

/* ── the typeface ─────────────────────────────────────────────────────────
 *
 * KANIT. Self-hosted, Thai and Latin, two weights, four files, 65 KB total.
 *
 * This replaced Chakra Petch, which the owner did not like and was right not
 * to: Chakra is a squared technical face with clipped corners and flat
 * terminals, and a whole interface set in it reads as hard and machined. The
 * reference sites the owner pointed at are all set in smooth geometric sans,
 * and none of them are angular.
 *
 * Kanit is geometric and smooth where Chakra is cut. Round bowls, even stroke,
 * open counters, and it holds up in uppercase for labels without going stiff.
 * The reason it wins over the obvious Latin geometrics is that IT HAS REAL
 * THAI. This site is read in three languages, and a Latin-only display face
 * means every Thai heading silently falls back to the system font, so half the
 * audience never sees the typography at all. Kanit's Thai is loopless and
 * drawn to match its Latin, by the same foundry, in one release.
 *
 * Self-hosted rather than from a CDN because the CSP forbids third-party font
 * origins, and because a webfont that arrives late is a layout shift on the
 * page where somebody is reading a price. Files taken from the Google Fonts
 * API's own pre-subset woff2, Kanit v17, SIL Open Font License.
 *
 * It carries display, labels and numbers only. Running text stays on the
 * system stack, which renders Thai better on every device this audience uses
 * and costs nothing. The split is deliberate: a display face at 15px for
 * paragraphs would be style at the price of reading. */
@font-face {
  font-family: "Kanit"; font-style: normal; font-weight: 300; font-display: swap;
  src: url("fonts/kanit-latin-300.v1.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Kanit"; font-style: normal; font-weight: 300; font-display: swap;
  src: url("fonts/kanit-thai-300.v1.woff2") format("woff2");
  unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+25CC;
}
@font-face {
  font-family: "Kanit"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("fonts/kanit-latin-400.v1.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Kanit"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("fonts/kanit-thai-400.v1.woff2") format("woff2");
  unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+25CC;
}
@font-face {
  font-family: "Kanit"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("fonts/kanit-latin-600.v1.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Kanit"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("fonts/kanit-thai-600.v1.woff2") format("woff2");
  unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+25CC;
}

/* The Thai faces do NOT claim U+200C-200D, and that is the one change from
 * the ranges Google Fonts ships. Those two are the zero-width joiners inside
 * emoji sequences such as the chef in "Run a Restaurant!", and the desktop game
 * wall writes catalogue names in this family. A browser downloads every face
 * whose range covers a character on the page, so one emoji in one game name
 * fetched the Thai file for every English visitor on a laptop. The Latin faces
 * still cover both through U+2000-206F, and a Thai page loads its face from the
 * Thai letters themselves. */

/* Shared tokens. Restrained violet surfaces, a consistent radius scale,
 * and short ease-out transitions. All themes retain these layout values. */

:root {
  /* ground and surfaces.
   *
   * The page is a VIOLET TO BLACK GRADIENT, not a flat near-black. Owner
   * instruction, and it is what makes the brand legible in the chrome itself
   * rather than only in the accent: the document opens in deep violet where the
   * header and the hero are, and settles to black by the time the reader is in
   * a list of cards, so the artwork down there has a neutral ground to sit on.
   *
   * Both ends are tokens, and BOTH are measured by the contrast gate. A
   * gradient whose light end nothing checks is how text ends up legal on paper
   * and illegal on screen. */
  --void: #08060f;
  --deep: #191038;
  --sunk: #0c0918;
  --surf: #12101f;
  --raise: #191527;
  --hair: rgba(196, 190, 226, 0.09);
  --hair-2: rgba(196, 190, 226, 0.17);
  --hair-lit: color-mix(in srgb, var(--vio-txt) 50%, transparent);

  /* ink */
  --ink: #eeecf5;
  --mute: #9d99ae;
  --faint: #9691a8;
  /* Icons at rest. Its own token on purpose: an icon is not text and must not
     inherit a muted text colour, which is what made every glyph on the live
     site the same grey as the label beside it. */
  --glyph: #cbc6e2;

  --vio: #8b5cf6;
  --vio-txt: #b39dff;
  --ion: #46c6ff;
  --ok: #45dfa0;
  --warn: #ffc857;
  --bad: #ff7a90;

  /* The one gradient, used on four elements in the whole system: the active
     navigation rail, the top edge of a subject panel, the best-value plan, and
     the scroll progress line. Two hues, not three: a third makes a rainbow. */
  --holo: linear-gradient(90deg, #8b5cf6, #46c6ff);

  /* The primary control. Deep enough that WHITE ink measures 5.7:1 on the
     lightest point, which is what lets every glyph on this site stay light. */
  --btn-fill: linear-gradient(180deg, #7c3aed, #6423c9);
  --btn-fill-hi: linear-gradient(180deg, #8b4cf8, #6f2ad8);

  --bloom: color-mix(in srgb, var(--vio) 28%, transparent);
  --rim: rgba(255, 255, 255, 0.07);
  --rim-hi: rgba(255, 255, 255, 0.18);
  --shadow: 0 1px 0 rgba(0, 0, 0, 0.5), 0 18px 40px -30px rgba(0, 0, 0, 0.9);
  --shadow-lift: 0 1px 0 rgba(0, 0, 0, 0.5), 0 28px 60px -34px rgba(0, 0, 0, 0.95);

  /* type */
  --disp: "Kanit", system-ui, -apple-system, "Segoe UI", sans-serif;
  --face: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "SF Mono", "Segoe UI Mono", monospace;

  /* space, strictly on a 4px grid. Nothing between the steps, so two things
     that should line up cannot miss by three pixels. */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 20px;
  --s6: 24px; --s8: 32px; --s10: 40px; --s13: 52px; --s16: 64px;

  /* Radius: controls, grouped controls, surfaces, artwork. */
  --r1: 6px; --r2: 8px; --r3: 12px; --r4: 14px; --pill: 999px;

  /* motion. One curve: leaves immediately, lands soft, never overshoots. */
  --out: cubic-bezier(0.22, 0.61, 0.24, 1);
  --spring: cubic-bezier(0.22, 0.61, 0.24, 1);
  --tint: 0.14s;
  --move: 0.24s;
  --enter: 0.42s;
  --slow: 0.7s;

  /* THE THEME'S OWN CHARACTER. Three values, and every preset overrides them.
   * See the THEME SIGNATURES section at the foot of this sheet for what they
   * buy: `--fx-op` dims the whole ambient layer in one place, and the other two
   * are the tempo of the only two lit moments a page always has, so Cyberpunk
   * can snap where Sakura eases without a second element anywhere. */
  --fx-op: 1;
  --fx-sweep: 0.62s;
  --fx-edge: 13s;
  /* How much of the black floor at the bottom of the ground layer a theme
     keeps. Four themes put their signature down there and have to see it. */
  --amb-floor: 1;

  color-scheme: dark;
}

/* ── reset ──────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }

html {
  background: var(--void);
  scrollbar-color: rgba(120, 108, 168, 0.45) transparent;
  /* The header is sticky, so anything scrolled to by an anchor or by
     scrollIntoView would otherwise land underneath it. Set once on the root
     rather than per target, which is where it gets forgotten. */
  scroll-padding-top: 76px;
  /* NOT `scroll-behavior: smooth`. That property applies to every scroll the
     document performs, including the position a browser restores when the back
     button is pressed, which turns an instant return into a two second crawl up
     the page. The shell asks for smooth per click instead, on in-page links
     only, and honours prefers-reduced-motion while it does. */
}

/* The sprite is a definition, not a picture. It has to stay in the document for
   every `<use>` to resolve, so it cannot be `display: none`, and it must take no
   space and catch no clicks. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; pointer-events: none; }
body {
  margin: 0; min-height: 100vh;
  display: flex; flex-direction: column;
  background: var(--void); color: var(--ink);
  font: 400 15px/1.68 var(--face);
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}
::selection { background: var(--vio); color: #fff; }
img, svg, canvas { max-width: 100%; }
/* Every link eases. This one declaration is most of the finding:  had no
   transition at all, so every link in the design snapped between two colours
   while everything around it moved, which reads as a bug rather than as
   restraint. It also covers the breadcrumb and the section links, because
   those are anchors too and were only listed separately by the audit. */
a { color: var(--vio-txt); text-decoration: none; transition: color var(--tint) ease; }
a:hover { color: #fff; }
/* A link INSIDE a sentence is underlined. Colour alone was the only thing
   telling "weao.xyz" or "Open a ticket on Discord" apart from the words around
   them, which fails anybody who does not see that violet as different (WCAG
   1.4.1). Navigation, buttons and card covers are not running text and keep
   their own treatment; this reaches only links whose parent is prose. */
:is(p, li, small, dd) a:not(.btn), .msg a:not(.btn) {
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
}
:is(p, li, small, dd) a:not(.btn):hover, .msg a:not(.btn):hover { text-decoration-color: currentColor; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--vio); outline-offset: 3px; border-radius: 3px; }

/* The skip link. First in every document, off screen until it has focus, and
   then the first thing on screen: above the header, which is sticky and would
   otherwise cover it. Transform and opacity only, so arriving costs no layout. */
.skip {
  position: fixed; top: var(--s3); left: var(--s3); z-index: 300;
  padding: 10px 16px; border-radius: var(--r2);
  background: var(--raise); color: var(--ink);
  border: 1px solid var(--hair-lit); box-shadow: var(--shadow-lift);
  font: 400 14px/1.2 var(--disp);
  opacity: 0; transform: translateY(calc(-100% - var(--s4))); pointer-events: none;
  transition: transform var(--move) var(--out), opacity var(--tint) ease;
}
.skip:focus { opacity: 1; transform: none; pointer-events: auto; }
.skip:hover { color: #fff; }

/* Visually hidden, still read. For a heading a page needs for its outline and
   has no visible place for, which is the catalogue's case. */
.vh {
  position: absolute !important; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* WEIGHT.
 *
 * The whole scale came down a step, because the owner said it read as heavy and
 * it did: Kanit carries more stroke at a given number than most sans faces, so
 * 700 here is nearer another face's 800 and a page of it is loud.
 *
 * Three weights now instead of two, which is what makes the reduction possible
 * without flattening anything: 600 for headings, 400 for labels and controls,
 * 300 for anything large enough that thin strokes still hold. Dropping 700 to
 * 600 and 500 to 400 with only two files would have left headings and labels
 * the same weight, and hierarchy would have gone with the heaviness.
 *
 * 300 is used only above about 30px. Below that it thins out against a dark
 * ground, which is the direction light-on-dark already errs in. */
h1, h2, h3, h4 { margin: 0; font-family: var(--disp); font-weight: 600; line-height: 1.16; letter-spacing: 0.002em; }
/* Bigger, and one step back up in weight.
 *
 * 300 was the answer to "too heavy" and it overshot: at 300 a page heading is a
 * whisper, and the owner read it as small rather than as light, which is what
 * happens when weight drops far enough that the strokes stop holding the size.
 * 400 at a larger size is the shape that was actually wanted, heavier without
 * going back to the slab. The rest of the scale stays where it is. */
h1 { font-size: clamp(35px, 6vw, 62px); font-weight: 400; letter-spacing: -0.012em; text-wrap: balance; }
h2 { font-size: clamp(22px, 3.1vw, 29px); text-wrap: balance; }
h3 { font-size: 18px; }
p { margin: 0 0 var(--s3); }

/* Digits that change width while a counter runs read as broken. */
.num, .amt, .fig, td.n, time { font-variant-numeric: tabular-nums; }

/* The ONE uppercase role in the system. Everything a person reads as language
   is sentence case; capitals mean "this is a label, not a sentence". */
.lbl {
  font: 400 10.5px/1 var(--disp);
  letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--faint);
}
.eyebrow {
  display: inline-flex; align-items: center; gap: var(--s3);
  font: 400 11px/1 var(--disp);
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--vio-txt);
}
.eyebrow::before { content: ""; width: 20px; height: 1px; background: var(--holo); }

/* ── the ground ───────────────────────────────────────────────────────────
 *
 * One fixed layer. Its structure never moves; what the active theme paints on
 * top of it is the THEME SIGNATURES section at the foot of this sheet.
 *
 * The original theme adds a desktop-only composition of actual game artwork.
 * Its cards sit behind a dark reading veil and move using transforms only.
 * Seasonal themes retain their own backgrounds. The grid remains opt-in.
 *
 * ## `.amb`, and why it is not `.field` any more
 *
 * It was `.field` from the day this design was written, and `uv-reuse.css:49`
 * has declared `.field { display: grid; position: relative; margin-bottom: … }`
 * since the day before that, because `checkout.js` writes `<label class="field">`
 * around the coupon box and the sheet had to style it.
 *
 * Two different things with one name, and the stylesheet that loses is the one
 * that loads first. So this layer resolved to `position: relative` at ZERO
 * HEIGHT on every page, its four absolutely positioned children collapsed with
 * it, and the violet-to-black ground, the hairline grid and the watermark were
 * all absent from the live site from the 2026-08-05 cutover onward. Nothing
 * failed: the contrast gate reads tokens, the icon gate reads the sprite, and
 * the page-weight gate weighs bytes. A background that is simply not painted is
 * invisible to all three, and the page still looks deliberate because near
 * black on near black looks like a choice.
 *
 * The fix is the name. A layout utility and a form control cannot share one,
 * and scoping this to `body > .field` would have kept the collision alive for
 * whoever writes the next `.field`.
 */
.amb { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.scriptfield {
  --wall-image: none;
  display: none;
}
/**
 * The hairline grid, OFF unless a theme asks for it.
 *
 * `opacity: 0` here is the whole point of the rule and it is the second thing
 * this has been. It used to default to full strength, and every one of the ten
 * themes turned it down: 0.5, 0.5, 0.4, 0.22, 0, 0.45, 0.55, 0.35, 0.18, and
 * cyberpunk redrew it in its own accent. `original` was the only preset with no
 * override, so the DEFAULT was the only place on the site showing this grid at
 * a hundred percent, which is backwards twice over: it is the quietest theme
 * and it is the one most people see.
 *
 * The owner's report was "i dont like the Grid bg", looking at `original`, and
 * they had good reason to. The layer this paints on was dead in production from
 * the v2 cutover on 2026-08-05 until it was repaired on 2026-08-13, so for eight
 * days the site had no grid at all. What came back was not a change nobody
 * asked for, it was a change nobody asked for AT FULL STRENGTH.
 *
 * Off by default keeps the grid opt-in. Seasonal themes declare what they need;
 * the original now uses the desktop game wall without a grid underneath it.
 */
.amb .grid {
  position: absolute; inset: -1px;
  opacity: 0;
  background-image:
    linear-gradient(90deg, color-mix(in srgb, var(--vio-txt) 6%, transparent) 0 1px, transparent 1px 32px),
    linear-gradient(0deg, color-mix(in srgb, var(--vio-txt) 6%, transparent) 0 1px, transparent 1px 32px);
  background-size: 32px 32px;
  -webkit-mask-image: linear-gradient(180deg, #fff, transparent 62%);
          mask-image: linear-gradient(180deg, #fff, transparent 62%);
}

/**
 * THE VIOLET TO BLACK GRADIENT, which is the theme.
 *
 * Three layers, and the order is the whole trick. A single linear violet-to-
 * black fill is what a gradient generator produces: a flat band with a visible
 * seam where it stops. Real depth comes from one linear pass for the overall
 * fall, plus two off-centre radial pools that keep the light from being
 * symmetrical, plus a black vignette at the bottom so the fall lands rather
 * than fading out mid-page.
 *
 * It is on a FIXED layer, so it belongs to the window and not to the document:
 * scrolling moves the content through the light instead of dragging the light
 * along with it, which is what stops a long catalogue page from having the same
 * purple wash repeated at every scroll position.
 */
.amb .wash {
  position: absolute; inset: 0;
  background:
    /* the pools, off centre on purpose */
    radial-gradient(90% 55% at 18% -8%, color-mix(in srgb, var(--vio) 12%, transparent), transparent 62%),
    radial-gradient(70% 46% at 96% 4%, color-mix(in srgb, var(--ion) 4%, transparent), transparent 60%),
    /* the fall */
    linear-gradient(178deg, var(--deep) 0%, color-mix(in srgb, var(--deep) 45%, var(--void)) 26%, var(--void) 62%, var(--void) 100%);
}
/**
 * The floor. Without it the fall finishes around two thirds down and the last
 * third reads as a different, flatter page.
 *
 * It is a pseudo-element, so it is the LAST child of this layer and paints over
 * every real one. That is right for the wash and wrong for a theme whose
 * signature lives along the bottom of the window: at full strength it took the
 * cyberpunk floor grid, the halloween fog and the summer caustics down to
 * nothing, and all three looked like rules that had failed to apply. Hence the
 * token. It only ever DARKENS toward black, so turning it down moves the ground
 * back toward `--void`, which is the colour every contrast pair is measured
 * against: no theme can weaken it into an illegible page.
 */
.amb::after {
  content: ""; position: absolute; inset: 0; opacity: var(--amb-floor);
  background: linear-gradient(180deg, transparent 52%, rgba(4, 3, 9, 0.85) 100%);
}

/* The real mark, very large and very faint, anchored to the top right and fixed
 * so it does not scroll.
 *
 * The site owns a logo and the preview was wearing a generic gem glyph, which
 * was the first thing the owner asked about. This is the same file the live
 * header uses, at 4.5% opacity, which is enough to read as the brand's paper
 * and not enough to compete with anything placed on it. `background-image`
 * rather than an `<img>`: it is decoration, it must never be announced, and it
 * must never be draggable.
 */
.amb .watermark {
  position: absolute;
  top: -80px; right: -120px;
  width: min(720px, 62vw); aspect-ratio: 1;
  background: url("/site-watermark.v1.webp") no-repeat center / contain;
  opacity: 0.045;
  /* Faded out toward the bottom left so it never reaches the reading column. */
  -webkit-mask-image: radial-gradient(72% 72% at 72% 26%, #fff, transparent 78%);
          mask-image: radial-gradient(72% 72% at 72% 26%, #fff, transparent 78%);
}
@media (max-width: 780px) { .amb .watermark { display: none; } }

/* ── layout ─────────────────────────────────────────────────────────────── */

.top, main, .foot { position: relative; z-index: 1; }
main { flex: 1 0 auto; width: 100%; min-width: 0; max-width: 1240px; margin-inline: auto; padding-inline: var(--s6); }
.wide { max-width: 1320px; }

/* A section: a rule, a number, a title. The number is real information (this is
   the third block on the page) rather than decoration, which is why it is only
   used where the blocks are genuinely a sequence. */
.sect { padding: var(--s16) 0 0; }
.sect-h { display: flex; align-items: baseline; gap: var(--s4); margin-bottom: var(--s3); }
.sect-h .rule { flex: 1; height: 1px; align-self: center; background: linear-gradient(90deg, var(--hair-2), transparent); }
/* 24px tall at least, which is the WCAG minimum for a target. At 12px/1 the
   link was 12 pixels high: "See every game" was a 92x12 strip to hit. The
   padding takes the room, so the words do not move. */
.sect-h .more { display: inline-flex; align-items: center; min-height: 24px; padding-block: 6px;
  font: 400 12px/1 var(--disp); letter-spacing: 0.05em; color: var(--vio-txt); white-space: nowrap; }
.sect-h .more:hover { color: #fff; }
.lead { margin: 0 0 var(--s6); color: var(--mute); font-size: 14px; max-width: 66ch; }

/* ── shared shapes, moved from uv-reuse.css ───────────────────────────────
 *
 * These were in the reuse sheet because the pages that first used them were
 * the reused-script pages. By 2026-09-27 the home page drew .code, the shop
 * drew .fineprint, .more-wrap and .tick, the catalogue drew .tagrow and
 * .docacts and the terms drew .kicker, so every one of those pages paid for a
 * second render-blocking stylesheet to style six elements. Measured with
 * browser coverage: 99 to 100 per cent of that sheet went unused on them.
 *
 * They live here now, in the order they had there, which is the order that
 * decides a tie between two of them (.mt then .fineprint: a fine print line
 * keeps its zero margin, exactly as before). */
.pad { padding: clamp(18px, 2.4vw, 32px); }
.mt { margin-top: var(--s5); }
.dim { color: var(--mute); font-size: 13.5px; }
.fineprint { margin: 0; color: var(--mute); font-size: 12px; line-height: 1.7; overflow-wrap: anywhere; }
.two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--s6); align-items: start; }
.two > * { min-width: 0; }
@media (max-width: 900px) { .two { grid-template-columns: 1fr; } }
.more-wrap { display: flex; justify-content: center; padding: var(--s6) 0 0; }
.tagrow { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s4); }
.docacts { display: flex; gap: var(--s3); flex-wrap: wrap; margin: var(--s5) 0 var(--s8); }

/* The panels those pages build with `.pane`, and the two column split. */
.cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); gap: var(--s6); align-items: start; }
.cols > * { min-width: 0; }
@media (max-width: 900px) { .cols { grid-template-columns: 1fr; } }
.pane {
  padding: clamp(18px, 2.4vw, 32px); border-radius: var(--r4); min-width: 0;
  background: var(--surf);
  border: 1px solid var(--hair); box-shadow: inset 0 1px 0 var(--rim);
}
.pane h3 { display: flex; align-items: center; gap: var(--s2); margin-bottom: var(--s3); }
.hint { color: var(--mute); font-size: 14px; line-height: 1.7; margin: 0 0 var(--s5); max-width: 60ch; overflow-wrap: anywhere; }
.helpline { margin-top: var(--s5); padding-top: var(--s4); border-top: 1px solid var(--hair);
  color: var(--mute); font-size: 12.5px; line-height: 1.65; }
.kicker { font: 400 11px/1 var(--disp); letter-spacing: 0.16em; text-transform: uppercase; color: var(--vio-txt); }

.code {
  font: 500 13px/1.75 var(--mono); color: var(--mute);
  padding: var(--s4); margin: 0 0 var(--s3); border-radius: var(--r2);
  background: var(--sunk); border: 1px solid var(--hair);
  overflow-x: auto; white-space: pre; max-width: 100%;
}
.code b { color: var(--ink); font-weight: 500; }
.code u { text-decoration: none; color: var(--ion); }

/* 44 and 22 are both even, which is what keeps the glyph on whole pixels: a
   centred fixed-size child has to differ from its box by an even number or the
   browser lands it on a half pixel and the icon reads as slightly off. That
   has been the cause of "icons ไม่ตรง" three separate times. */
.tick {
  display: grid; place-items: center; width: 44px; height: 44px; border-radius: var(--pill);
  color: var(--ok);
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--ok) 38%, transparent);
  box-shadow: 0 0 30px -12px var(--ok), inset 0 1px 0 var(--rim);
}
.tick .ic { width: 22px; height: 22px; }

/* ── panel ──────────────────────────────────────────────────────────────── */

.panel {
  position: relative; border-radius: var(--r4);
  background: linear-gradient(180deg, var(--raise), var(--surf) 42%, var(--sunk));
  border: 1px solid var(--hair);
  box-shadow: inset 0 1px 0 var(--rim), var(--shadow);
  overflow: hidden;
}
/* LIGHT 1, and one of only five: a single holographic hairline along the top of
   a panel that is the subject of its section. Masked at both ends so it fades
   rather than stopping, because a line with two hard ends reads as a border. */
.panel.lit::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px; z-index: 4;
  background: var(--holo); opacity: 0.7; pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg, transparent, #fff 16%, #fff 84%, transparent);
          mask-image: linear-gradient(90deg, transparent, #fff 16%, #fff 84%, transparent);
}

/* ── icon tiles ───────────────────────────────────────────────────────────
 *
 * The device the owner pointed at: a small rounded plate with a hairline and an
 * inner light, holding one 1.6px glyph. It is what turns a loose icon into a
 * designed object, and it is the difference between the payment dialog reading
 * as a list of links and reading as a product.
 *
 * The glyph inherits `--glyph` and NEVER the page's ground colour. */
/**
 * `.tile.tile`, doubled on purpose.
 *
 * A tile is `<span class="tile">`, and a component rule as ordinary as
 * `.catcard span { display: block }` matches it at (0,1,1) against this rule's
 * (0,1,0), from later in the sheet, so it won twice over. The tile stopped
 * being a grid, `place-items: center` stopped meaning anything, and the glyph
 * fell to the top left of its box. That is indistinguishable from an icon that
 * is not centred, and it was reported as one three times.
 *
 * The selector that caused it is fixed, but fixing one selector only fixes one
 * selector. Repeating the class costs nothing, reads as deliberate, and lifts
 * this to (0,2,0), which no `.component type` rule can reach. A layout utility
 * has to beat the component it is used inside, or it is not a utility.
 */
.tile.tile {
  width: 40px; height: 40px; flex: none; display: grid; place-items: center;
  border-radius: var(--r2);
  background: linear-gradient(180deg, color-mix(in srgb, var(--vio) 14%, transparent), color-mix(in srgb, var(--vio) 2%, transparent));
  border: 1px solid var(--hair-2);
  box-shadow: inset 0 1px 0 var(--rim);
  color: var(--vio-txt);
  transition: transform var(--move) var(--spring), border-color var(--tint) ease,
              box-shadow var(--move) var(--out), color var(--tint) ease;
}
/* 20, not 19. A 19px glyph centred in a 40px box lands on a HALF PIXEL: the
   browser rounds it to one side and the icon sits a pixel off centre, which is
   exactly the "icons are not straight" the owner kept pointing at. Every
   icon-in-a-box pair in this system now differs by an even number, and
   check-uv-preview.mjs fails the build if a new one does not. */
.tile .ic { width: 20px; height: 20px; }
.tile.sm { width: 32px; height: 32px; border-radius: var(--r1); }
.tile.sm .ic { width: 16px; height: 16px; }
.tile.lg { width: 48px; height: 48px; }
.tile.lg .ic { width: 22px; height: 22px; }
.tile.ion {
  color: var(--ion); border-color: color-mix(in srgb, var(--ion) 26%, transparent);
  background: linear-gradient(180deg, color-mix(in srgb, var(--ion) 13%, transparent), color-mix(in srgb, var(--ion) 2%, transparent));
}
.tile.plain { color: var(--glyph); background: var(--sunk); border-color: var(--hair); }

/* ── icons ────────────────────────────────────────────────────────────────
 *
 * Light at rest, accent when engaged, dark never. The glow is held for the
 * moment of interaction rather than being always on, because a permanent glow
 * is the strongest cheap-looking signal available. */
.ic {
  width: 17px; height: 17px; display: block; flex: none;
  color: var(--glyph);
  transition: color var(--tint) ease, transform var(--move) var(--spring),
              filter var(--tint) ease;
}
.ic-lead { margin-right: -2px; }
/* The trailing arrow keeps its space at all times and only fades and slides, so
   nothing relaid out and the label never moves under the pointer. It comes from
   the RIGHT, out of the control's own padding, so it never draws on top of the
   word it follows. */
.ic-arw { opacity: 0; transform: translateX(7px); }

/* ── buttons ──────────────────────────────────────────────────────────────
 *
 * Three roles and no more: the thing to press, the alternative, and the quiet
 * one that is really a link. */
.btn {
  position: relative; overflow: hidden; isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 44px; padding: 12px 20px; border-radius: var(--r2); border: 1px solid transparent;
  font: 400 14px/1.2 var(--disp); letter-spacing: 0.015em; text-transform: none;
  white-space: nowrap;
  transition: transform var(--tint) var(--spring), box-shadow var(--move) var(--out),
              border-color var(--tint) ease, color var(--tint) ease,
              background-color var(--tint) ease, opacity var(--tint) ease;
}
/* Down fast, back slow. A press should register before the finger lifts, and
   0.14s in both directions makes a control feel spongy; the release is where
   the softness belongs. */
.btn:active { transform: scale(0.972); transition-duration: 0.06s; }
.btn:hover .ic-arw, .btn:focus-visible .ic-arw { opacity: 1; transform: none; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; transform: none; }
.btn-sm { padding: 10px 14px; font-size: 13px; }
.btn-lg { min-height: 52px; padding: 15px 24px; font-size: 15px; }
.btn-w { width: 100%; }

/**
 * Where the finger landed, briefly.
 *
 * The shell writes the pointer position into --px / --py and adds `.pressed`;
 * this fades a soft highlight out from that exact point over a third of a
 * second and removes itself. It is not the material ripple, an expanding filled
 * circle that needs its own element per press and is unmistakably somebody
 * else's design language. A highlight that starts under the finger says the
 * same thing, costs one pseudo-element, and belongs on a dark surface.
 */
@keyframes uv-press {
  from { opacity: 0.85; transform: scale(0.4); }
  to { opacity: 0; transform: scale(1); }
}
.btn::before, .chips button::before, .expick::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: 0; border-radius: inherit;
  background: radial-gradient(circle at var(--px, 50%) var(--py, 50%),
              rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.06) 42%, transparent 62%);
}
.btn.pressed::before, .chips button.pressed::before, .expick.pressed::before {
  animation: uv-press 0.34s var(--out) forwards;
}
/* The label has to stay above the flash, or a press dims the words it is
   acknowledging. */
.btn > *, .chips button > *, .expick > * { position: relative; z-index: 1; }

.btn-p {
  color: #fff; background: var(--btn-fill);
  box-shadow: inset 0 1px 0 var(--rim-hi), 0 12px 30px -18px rgba(88, 28, 200, 0.9);
}
.btn-p .ic { color: #fff; }
/* One sweep of light across the face per hover, behind the label on its own
   layer so it lights the control rather than washing out the words. */
.btn-p::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(102deg, transparent 38%, rgba(255, 255, 255, 0.3) 50%, transparent 62%);
  transform: translateX(-130%);
  transition: transform 0.62s var(--out);
}
/* Rises a pixel and lights the surface under it. One pixel is the whole
   movement: a control that jumps four looks like it is trying to escape, and
   the glow does the work of saying it is live. */
.btn-p:hover {
  background: var(--btn-fill-hi);
  transform: translateY(-1px);
  box-shadow: inset 0 1px 0 var(--rim-hi), 0 0 0 1px var(--hair-lit),
              0 0 34px -6px var(--bloom), 0 20px 40px -20px rgba(0, 0, 0, 0.9);
}
.btn-p:hover::after { transform: translateX(130%); }
.btn-p:active { transform: translateY(0) scale(0.972); }

/**
 * The secondary control takes a LIT RIM rather than a brighter border.
 *
 * A single border colour cannot be a gradient, so the rim is a gradient
 * painted into the padding box and masked to the one pixel edge. It costs one
 * pseudo-element and it is what stops the alternative reading as a disabled
 * version of the primary: the two are then different in kind, one filled and
 * one outlined in the same two hues, rather than different in brightness.
 */
.btn-s {
  color: var(--ink); border-color: var(--hair-2);
  background: rgba(18, 16, 31, 0.72);
}
.btn-s::after {
  content: ""; position: absolute; inset: -1px; z-index: -1; border-radius: inherit;
  padding: 1px; background: var(--holo);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  opacity: 0; transition: opacity var(--move) var(--out);
}
.btn-s:hover {
  color: #fff; border-color: transparent;
  background: rgba(30, 25, 50, 0.85);
  transform: translateY(-1px);
  box-shadow: 0 0 30px -14px var(--bloom);
}
.btn-s:hover::after { opacity: 1; }
.btn-s:hover .ic { color: var(--vio-txt); }
.btn-s:active { transform: translateY(0) scale(0.972); }

.btn-q {
  padding: 10px 12px; color: var(--vio-txt);
  font: 600 13px/1 var(--disp); letter-spacing: 0.02em; text-transform: none;
}
.btn-q:hover { color: #fff; }
.btn-q:hover .ic { color: #fff; }

/* The leading glyph turns very slightly toward the label it introduces. Two
   degrees, on every button in the system at once, which is the sort of thing
   that is only noticed when it is taken away. */
.btn .ic-lead { transition: transform var(--move) var(--out), color var(--tint) ease; }
.btn:hover .ic-lead { transform: translateY(-1px); }

/* ── tags ─────────────────────────────────────────────────────────────────
 *
 * A pill with a lit dot. The dot is what makes a tag read as a STATE rather
 * than as a word in a box, and it is one shape carrying four meanings by
 * colour, so the vocabulary is learned once and used everywhere. */
.tag {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 9px; border-radius: var(--r1);
  border: 1px solid var(--hair-2); background: rgba(10, 9, 18, 0.7);
  font: 400 10px/1 var(--disp); letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--mute); white-space: nowrap;
  transition: border-color var(--tint) ease, color var(--tint) ease;
}
.tag::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%; flex: none;
  background: currentColor; box-shadow: 0 0 8px currentColor;
}
.tag[data-t="hot"] { color: var(--vio-txt); border-color: color-mix(in srgb, var(--vio-txt) 34%, transparent); }
.tag[data-t="new"] { color: var(--ok); border-color: rgba(69, 223, 160, 0.3); }
.tag[data-t="sale"] { color: var(--warn); border-color: rgba(255, 200, 87, 0.3); }
.tag[data-t="limited"] { color: var(--ion); border-color: color-mix(in srgb, var(--ion) 34%, transparent); }
/* The catalogue has always carried a free/paid category and the site rendered a
   price and a buy button on all of it, so every visitor read the whole library
   as paid. `free` takes --ok because it is the good news. `paid` takes --warn
   rather than the violet of "hot": gold reads as premium, and only four of the
   fifty-two working scripts wear it, so it should not look like a warning about
   the other forty-eight. */
.tag[data-t="free"] { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 34%, transparent); }
.tag[data-t="paid"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 34%, transparent); }
/* No dot where the tag is a quantity rather than a state. */
.tag.flat::before { display: none; }
.tag.flat { font-family: var(--mono); letter-spacing: 0.04em; text-transform: none; font-size: 11px; }

/* A status that is genuinely live, and the only thing on a page allowed to
   move on its own. */
.live { display: inline-flex; align-items: center; gap: 7px; color: var(--ok);
  font: 400 10.5px/1 var(--disp); letter-spacing: 0.14em; text-transform: uppercase; }
.live::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%; flex: none;
  background: currentColor; box-shadow: 0 0 9px currentColor;
  animation: uv-pulse 2.4s var(--out) infinite;
}
.live[data-s="maintenance"] { color: var(--warn); }
.live[data-s="discontinued"], .live[data-s="coming-soon"], .live[data-s="in-development"] { color: var(--faint); }
.live[data-s="maintenance"]::before, .live[data-s="discontinued"]::before,
.live[data-s="coming-soon"]::before, .live[data-s="in-development"]::before { animation: none; }
@keyframes uv-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

/* ── fields ─────────────────────────────────────────────────────────────── */

.f { display: grid; gap: 7px; }
.f > span, .f > label { font: 400 10.5px/1 var(--disp); letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute); }
.f input {
  width: 100%; padding: 12px 14px; border-radius: var(--r1);
  border: 1px solid var(--hair); background: var(--sunk); color: var(--ink);
  font: 500 13.5px/1.2 var(--mono); letter-spacing: 0.04em;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
  transition: border-color var(--tint) ease, box-shadow var(--move) var(--out);
}
.f input::placeholder { color: var(--faint); letter-spacing: 0; font-family: var(--face); }
.f input:focus {
  outline: none; border-color: var(--vio);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--vio) 16%, transparent), 0 0 26px -12px var(--bloom);
}

/* ── the search pill ────────────────────────────────────────────────────── */

.seek {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 8px 10px 8px 13px; min-width: 0;
  border: 1px solid var(--hair); border-radius: var(--r2);
  background: var(--sunk); color: var(--faint); font-size: 13px;
  transition: border-color var(--tint) ease, box-shadow var(--move) var(--out), color var(--tint) ease;
}
.seek:hover, .seek:focus-within { border-color: var(--hair-2); color: var(--mute); box-shadow: 0 0 22px -14px var(--bloom); }
.seek input {
  border: 0; background: none; color: var(--ink); font: inherit; width: 15ch; min-width: 0; padding: 0;
}
/* The ring is KEPT and shaped to the control it belongs to, rather than being
   removed. The first version of this set `outline: none` and lit the pill
   instead, and the keyboard gate refused it: a focus style that lives on a
   different element is a promise the rule cannot make, and a control a keyboard
   user cannot find is not a control they have.
   Rounding the input to the pill makes the ring trace the shape somebody sees,
   so keeping it costs the design nothing. */
.seek input { border-radius: var(--r1); }
.seek:focus-within {
  border-color: var(--vio);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--vio) 18%, transparent), 0 0 24px -12px var(--bloom);
}
.seek input::placeholder { color: var(--faint); }
kbd {
  font: 400 10px/1 var(--disp); letter-spacing: 0.06em; color: var(--faint);
  border: 1px solid var(--hair-2); border-bottom-width: 2px; border-radius: 6px;
  padding: 4px 6px; background: var(--sunk); white-space: nowrap;
}

/* ══ HEADER ═══════════════════════════════════════════════════════════════ */

/**
 * The header answers the page rather than sitting on it.
 *
 * At the very top of the document it is transparent and borderless, so the hero
 * runs to the top edge of the window and the first thing seen is the page and
 * not the chrome. The moment anything scrolls, `data-lift` flips and it becomes
 * a solid blurred bar with a hairline under it. Both states are the same
 * element and the change is background, border and shadow only, so it costs no
 * layout and cannot shift what is beside it.
 */
/**
 * THE HEADER
 *
 * Three states off one scroll listener, and no progress bar. The bar is gone at
 * the owner's request and they were right about it: it was a two pixel gradient
 * that moved constantly, in the one element that should be the calmest thing on
 * the page, reporting a number nobody needs.
 *
 *   at the top       transparent and borderless, so the hero runs to the edge
 *                    of the window and the first thing seen is the page
 *   scrolled         a solid blurred bar with a lit rule under it
 *   scrolling down   gone, translated out by its own height
 *
 * The third is the one that earns its keep. Reading down a catalogue, the
 * header is in the way; reaching for it means scrolling up, and that gesture is
 * what brings it back, instantly. It is `translateY` and nothing else: the
 * element keeps its size, so the page beneath it never reflows and never jumps.
 */
.top {
  display: flex; align-items: center; gap: var(--s4);
  min-height: 76px;
  padding: var(--s3) max(var(--s6), calc((100% - 1240px) / 2 + var(--s6)));
  border-bottom: 1px solid transparent;
  background: rgba(8, 6, 15, 0);
  position: sticky; top: 0; z-index: 40; isolation: isolate;
  transition:
    transform 0.36s var(--out),
    background-color var(--move) var(--out),
    border-color var(--move) var(--out),
    box-shadow var(--move) var(--out),
    backdrop-filter var(--move) var(--out);
}
.top[data-lift="1"] {
  border-bottom-color: var(--hair);
  background: rgba(8, 6, 15, 0.8);
  backdrop-filter: blur(22px) saturate(1.4);
  box-shadow: 0 14px 40px -32px rgba(0, 0, 0, 1);
}
/* No `data-lift` at all means the shell never ran. A header stuck transparent
   over scrolled content is unreadable, so the fallback is the solid state: the
   attribute is what turns it OFF, never what turns it on. */
.top:not([data-lift]) {
  border-bottom-color: var(--hair);
  background: rgba(8, 6, 15, 0.8);
  backdrop-filter: blur(22px) saturate(1.4);
}
/* Out of the way while reading downward. Not applied while the drawer is open,
   because a header that leaves under an open dialog takes its close button with
   it. */
.top[data-away="1"] { transform: translateY(-100%); }

/* The lit rule under the bar, in place of the progress line. It is the same
   two hues and it does not move: it fades up with the bar and that is all. */
.top[data-lift="1"]::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--vio) 55%, transparent) 22%,
              color-mix(in srgb, var(--ion) 40%, transparent) 62%, transparent);
}
/* The violet the header sits in, bleeding down from the top of the window.
   Behind the bar rather than on it, so it reads as the page continuing up
   past the chrome instead of as a shadow the header casts. */
.top::before {
  content: ""; position: absolute; inset: -60px 0 auto 0; height: 160px; z-index: -1;
  background: radial-gradient(60% 100% at 50% 0%, color-mix(in srgb, var(--vio) 20%, transparent), transparent 72%);
  opacity: 1; pointer-events: none;
  transition: opacity var(--move) var(--out);
}
.top[data-lift="1"]::before { opacity: 0; }

.mk { display: inline-flex; align-items: center; gap: 9px; position: relative; }
/* The site's own mark. It is a lit glyph on transparency, so it needs no plate
   behind it and gets none: a logo in a rounded tile is a placeholder, and this
   one already carries its own glow. */
.mk .mkimg {
  width: 30px; height: 30px; display: block; flex: none;
  filter: drop-shadow(0 0 12px rgba(124, 92, 246, 0.45));
  transition: transform 0.5s var(--out), filter var(--move) var(--out);
}
.mk:hover .mkimg { transform: scale(1.08) rotate(-8deg); filter: drop-shadow(0 0 20px color-mix(in srgb, var(--vio) 85%, transparent)); }
.mk:active .mkimg { transform: scale(0.96) rotate(0deg); transition-duration: 0.08s; }
.mk b {
  font: 600 17px/1 var(--disp); letter-spacing: 0.03em; color: var(--ink);
  transition: color var(--tint) ease, letter-spacing 0.5s var(--out);
}
/* The wordmark opens up very slightly under the pointer. 0.055em is about a
   pixel and a half across the word: felt rather than seen, which is the point.
   It is the only place in the system that animates letter-spacing, and it is
   affordable here because the mark is the leftmost thing in the bar, so the
   reflow it causes has nothing to its left to disturb. */
.mk:hover b { color: #fff; letter-spacing: 0.055em; }
.mk b i { font-style: normal; color: var(--vio-txt); transition: color var(--tint) ease; }
.mk:hover b i { color: var(--ion); }
.foot .mk .mkimg { width: 26px; height: 26px; }

.nav { position: relative; display: flex; gap: 2px; }
.nav a {
  position: relative; z-index: 1; isolation: isolate;
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 44px; padding: 9px 10px; border-radius: var(--r2); color: var(--mute);
  font: 400 12.5px/1 var(--disp); letter-spacing: 0.015em; text-transform: none;
  transition: color var(--tint) ease;
}
/* The hover surface. A violet wash that grows from the middle rather than
   appearing at full width, so moving along the bar reads as one continuous
   gesture instead of six separate blocks switching on and off. */
.nav a::before {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
  background: linear-gradient(180deg, color-mix(in srgb, var(--vio) 16%, transparent), color-mix(in srgb, var(--vio) 5%, transparent));
  opacity: 0; transform: scaleX(0.7); transform-origin: center;
  transition: opacity var(--tint) ease, transform var(--move) var(--out);
}
.nav a:hover::before, .nav a:focus-visible::before { opacity: 1; transform: none; }
.nav a .ic { width: 15px; height: 15px; transition: color var(--tint) ease, transform var(--move) var(--out); }
.nav a:hover, .nav a:focus-visible { color: #fff; }
.nav a:hover .ic { color: var(--vio-txt); transform: translateY(-2px) scale(1.06); }
.nav a[aria-current] { color: var(--ink); }
.nav a[aria-current] .ic { color: var(--vio-txt); }

/**
 * Signed in, said in the chrome rather than only on the Account page.
 *
 * A live dot beside the Account entry, drawn from the state the shell resolves
 * once and shares. It is deliberately small: the point is that the answer is
 * available at a glance from anywhere, not that it should be loud.
 *
 * A REAL ELEMENT rather than `::after` on the link. The current-page rail is
 * already an `::after` on `.nav a[aria-current]`, and a second rule claiming the
 * same pseudo-element would silently replace the rail with a dot on the one
 * page where both apply. Two meanings, two elements.
 *
 * Only `="in"` shows it. Unknown and signed-out are the same absence, because a
 * dot meaning "we have not checked yet" is worse than no dot at all.
 */
.livedot {
  width: 6px; height: 6px; border-radius: 50%; flex: none;
  background: var(--ok); box-shadow: 0 0 8px var(--ok);
  margin-left: 1px;
  transform: scale(0); opacity: 0;
  transition: transform var(--move) var(--out), opacity var(--tint) ease;
}
html[data-session="in"] .livedot { transform: none; opacity: 1; }

/**
 * ONE mark under the navigation, which slides to whatever the pointer is over
 * and returns to the current page on the way out.
 *
 * The obvious build is a `::after` on each link, fading in on hover. That gives
 * six marks that blink independently and it is what the bar had. One shared
 * mark that travels is the difference between a menu that reacts and a menu
 * that feels connected, and it is the single clearest piece of motion here.
 *
 * A fixed 100px base scaled by transform, never `width` and `left`. Those are
 * layout properties: animating them relayouts the whole header on every frame
 * of every hover. `transform` and `opacity` are the two the compositor owns.
 */
.navink {
  position: absolute; z-index: 0; bottom: 3px; left: 0;
  width: 100px; height: 2px; transform-origin: left center;
  background: var(--holo); box-shadow: 0 0 10px var(--bloom);
  opacity: 0; pointer-events: none;
  transition: transform var(--move) var(--out), opacity var(--tint) ease;
}
/* With no shell there is no ink, so the current page still has to be marked by
   something. This static rail is that something, and it is switched off the
   moment the shell says it has taken over. The script sets `data-ink`, rather
   than the stylesheet trying to read the inline style the script writes, which
   would depend on how the browser serialises it. */
.nav a[aria-current]::after {
  content: ""; position: absolute; left: 12px; right: 12px; bottom: 3px; height: 2px;
  background: var(--holo); box-shadow: 0 0 10px var(--bloom);
}
.nav[data-ink="1"] a[aria-current]::after { display: none; }

.top .grow { flex: 1; }
@media (max-width: 1080px) { .nav, .top .seek { display: none; } }

/* the language menu */
.lang { position: relative; }
.langbtn {
  display: inline-flex; align-items: center; gap: 6px; padding: 8px 11px;
  border: 1px solid var(--hair); border-radius: var(--r1);
  color: var(--mute); font: 400 11.5px/1 var(--disp); letter-spacing: 0.1em;
  transition: color var(--tint) ease, border-color var(--tint) ease, background-color var(--tint) ease;
}
.langbtn:hover, .langbtn[aria-expanded="true"] { color: var(--ink); border-color: var(--hair-2); background: color-mix(in srgb, var(--vio) 8%, transparent); }
.langbtn .ic-caret { width: 12px; height: 12px; transition: transform var(--move) var(--out); }
.langbtn[aria-expanded="true"] .ic-caret { transform: rotate(180deg); }
.langmenu {
  position: absolute; top: calc(100% + 7px); right: 0; z-index: 60; min-width: 170px;
  margin: 0; padding: var(--s1); list-style: none;
  background: var(--raise); border: 1px solid var(--hair-2); border-radius: var(--r2);
  box-shadow: var(--shadow-lift), inset 0 1px 0 var(--rim);
  opacity: 0; visibility: hidden; transform: translateY(-7px) scale(0.985); transform-origin: top right;
  transition: opacity var(--move) var(--out), transform var(--move) var(--out), visibility 0s linear var(--move);
}
.lang[data-open="1"] .langmenu {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity var(--move) var(--out), transform var(--move) var(--spring), visibility 0s;
}
.langmenu li {
  display: flex; align-items: center; gap: var(--s3); padding: 9px 12px; border-radius: var(--r1);
  color: var(--mute); font-size: 13.5px; cursor: pointer;
  transition: background-color var(--tint) ease, color var(--tint) ease;
}
.langmenu li:hover { background: color-mix(in srgb, var(--vio) 10%, transparent); color: var(--ink); }
.langmenu li[aria-selected="true"] { color: var(--vio-txt); }
.langmenu li i {
  width: 10px; height: 10px; flex: none; opacity: 0;
  border-left: 2px solid var(--vio); border-bottom: 2px solid var(--vio);
  transform: rotate(-45deg) translate(1px, -2px);
}
.langmenu li[aria-selected="true"] i { opacity: 1; }
.langmenu li span { margin-left: auto; color: var(--faint); font: 400 10px/1 var(--disp); letter-spacing: 0.1em; }

/* the burger and the sheet */
.burger {
  display: none; width: 38px; height: 38px; place-items: center;
  border: 1px solid var(--hair); border-radius: var(--r1); color: var(--ink);
  transition: border-color var(--tint) ease, background-color var(--tint) ease;
}
.burger:hover { border-color: var(--hair-lit); background: color-mix(in srgb, var(--vio) 8%, transparent); }
.burger i { display: block; width: 16px; height: 1.5px; background: currentColor; position: relative;
  transition: background-color var(--tint) ease; }
.burger i::before, .burger i::after {
  content: ""; position: absolute; left: 0; width: 100%; height: 1.5px; background: currentColor;
  transition: transform var(--move) var(--spring);
}
.burger i::before { top: -5px; } .burger i::after { top: 5px; }
.burger[aria-expanded="true"] i { background: transparent; }
.burger[aria-expanded="true"] i::before { transform: translateY(5px) rotate(45deg); }
.burger[aria-expanded="true"] i::after { transform: translateY(-5px) rotate(-45deg); }
@media (max-width: 1080px) { .burger { display: grid; } }

/* ── the header on a small phone ────────────────────────────────────────────
 *
 * At 320px the header was 340px wide and EVERY page on the site scrolled
 * sideways. 48px of padding leaves 272px, and the mark, the language button,
 * "Get a key" and the burger want closer to four hundred between them. It is
 * the kind of defect that never shows up on a desktop and never shows up in a
 * source check: the page simply drifts left under a thumb.
 *
 * Below 560px the header keeps only what a phone header is for: who you are
 * with, and the way to everything else. The two it drops are both in the sheet,
 * where the call to action is a full-width button rather than the fourth thing
 * in a crowded row. */
@media (max-width: 560px) {
  .top { gap: var(--s3); padding-left: var(--s4); padding-right: var(--s4); }
  .top > .lang, .top > .btn { display: none; }
}

/**
 * A drawer down the right edge, not a full-screen takeover.
 *
 * It used to cover the whole window and fade in, which is the cheapest possible
 * version: the page vanishes, a flat sheet appears, and there is nothing to
 * press to get back except the button you came from. A panel that slides in
 * over a darkened page keeps the reader's place visible the whole time, gives
 * them a large obvious target to dismiss it, and is the movement they already
 * expect from every application they use.
 *
 * The panel travels on `translateX` and the scrim on `opacity`, so the whole
 * thing runs on the compositor and stays smooth on a phone.
 */
.scrim {
  position: fixed; inset: 0; z-index: 85;
  background: rgba(4, 3, 9, 0.66);
  backdrop-filter: blur(5px);
  opacity: 0; visibility: hidden;
  transition: opacity var(--move) var(--out), visibility 0s linear var(--move);
}
.scrim[data-open="1"] {
  opacity: 1; visibility: visible;
  transition: opacity var(--move) var(--out), visibility 0s;
}

.sheet {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 90;
  width: min(378px, 88vw); padding: var(--s3) var(--s6) var(--s6);
  background: var(--sunk);
  border-left: 1px solid var(--hair-2);
  box-shadow: -30px 0 70px -40px rgba(0, 0, 0, 1);
  display: flex; flex-direction: column; overflow-y: auto;
  visibility: hidden; transform: translateX(100%);
  transition: transform var(--move) var(--out), visibility 0s linear var(--move);
}
.sheet[data-open="1"] {
  visibility: visible; transform: none;
  transition: transform 0.34s var(--out), visibility 0s;
}
.sheet .shead { display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  padding-bottom: var(--s5); border-bottom: 1px solid var(--hair); margin-bottom: var(--s2); }
.sheet .shead .burger { display: grid; }
.sheet a {
  position: relative; display: flex; align-items: center; gap: var(--s4);
  padding: var(--s4) 0 var(--s4) var(--s3);
  border-bottom: 1px solid var(--hair); color: var(--ink);
  font: 600 19px/1 var(--disp); letter-spacing: 0.02em;
  opacity: 0; transform: translateX(14px);
  transition: color var(--tint) ease, transform var(--move) var(--out);
}
.sheet[data-open="1"] a {
  opacity: 1; transform: none;
  transition: opacity var(--enter) ease, transform var(--enter) var(--out),
              color var(--tint) ease;
}
.sheet a .ic { width: 19px; height: 19px; transition: color var(--tint) ease; }
.sheet a::before {
  content: ""; position: absolute; left: 0; top: 12px; bottom: 12px; width: 2px; border-radius: 2px;
  background: var(--holo); box-shadow: 0 0 10px var(--bloom);
  transform: scaleY(0); transform-origin: center; transition: transform var(--move) var(--out);
}
/* The rail grows and the row shifts, but the shift is a transform rather than
   `padding-left`, so hovering a menu item does not relayout the drawer. */
.sheet a:hover, .sheet a:focus-visible { color: #fff; transform: translateX(4px); }
.sheet a:hover .ic { color: var(--vio-txt); }
.sheet a:hover::before, .sheet a[aria-current]::before { transform: scaleY(1); }
.sheet a[aria-current] { color: var(--vio-txt); }
.sheet a[aria-current] .ic { color: var(--vio-txt); }
/* The items arrive one after another behind the panel. 40ms apart: close enough
   to read as one movement, far enough apart to be a cascade rather than a
   flicker. The last one lands at 0.28s, which is when the panel itself stops. */
.sheet[data-open="1"] a:nth-of-type(1) { transition-delay: 0.04s; }
.sheet[data-open="1"] a:nth-of-type(2) { transition-delay: 0.08s; }
.sheet[data-open="1"] a:nth-of-type(3) { transition-delay: 0.12s; }
.sheet[data-open="1"] a:nth-of-type(4) { transition-delay: 0.16s; }
.sheet[data-open="1"] a:nth-of-type(5) { transition-delay: 0.2s; }
.sheet[data-open="1"] a:nth-of-type(6) { transition-delay: 0.24s; }
.sheet[data-open="1"] a:nth-of-type(7) { transition-delay: 0.28s; }
.sheet .sfoot { margin-top: auto; padding-top: var(--s6); display: grid; gap: var(--s3); }

/* ══ FOOTER ═══════════════════════════════════════════════════════════════ */

.foot {
  margin-top: var(--s16); border-top: 1px solid var(--hair);
  background: rgba(10, 9, 18, 0.7);
}
/* Two rows on one grid rather than one flex line that wraps unpredictably: the
   mark and the navigation on top, the badge and the small print underneath,
   separated by a hairline. Wrapping decided the arrangement before, so at some
   widths the copyright landed between two links. */
.foot .inner {
  max-width: 1240px; margin-inline: auto; padding: var(--s8) var(--s6);
  display: grid; gap: var(--s5);
  grid-template-columns: auto 1fr;
  align-items: center;
}
.foot nav { display: flex; gap: var(--s5); flex-wrap: wrap; justify-content: flex-end; }
.foot a { color: var(--mute); font-size: 13px; display: inline-flex; align-items: center; gap: 7px;
  transition: color var(--tint) ease; }
.foot a:hover { color: var(--ink); }
.foot a .ic { width: 14px; height: 14px; transition: color var(--tint) ease, transform var(--move) var(--out); }
.foot a:hover .ic { color: var(--vio-txt); transform: translateY(-1px); }
.foot small { color: var(--faint); font-size: 11.5px; text-align: right; }
.foot .badge {
  display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px;
  border: 1px solid var(--hair-2); border-radius: var(--r1);
  font: 400 10px/1 var(--disp); letter-spacing: 0.14em; text-transform: uppercase; color: var(--warn);
}
/* An element rather than a pseudo-element, matching the dot in the preview bar
   at the top of the page. That one keeps its own rule further down the sheet
   because it is a different size; giving them one shared rule here would have
   been quietly overridden by it and left this dot at the wrong scale. */
.foot .badge .dot {
  width: 5px; height: 5px; border-radius: 50%; flex: none;
  background: currentColor; box-shadow: 0 0 8px currentColor;
}
/* The rule that separates the identity from the small print: ONE line, the
   full width of the grid, in a row of its own. It used to be a border under
   the mark and a second border under the navigation, which left a gap where
   the column gap fell and sat a pixel apart, because one of the two paddings
   had been trimmed by a pixel to line them up and did not. A pseudo-element is
   a grid item like any other, so it can span both columns. */
.foot .inner::before {
  content: ""; grid-row: 2; grid-column: 1 / -1;
  height: 1px; background: var(--hair);
}
.foot .badge, .foot small { grid-row: 3; }
.foot .badge { justify-self: start; }
@media (max-width: 720px) {
  .foot .inner { grid-template-columns: 1fr; }
  .foot nav { justify-content: flex-start; }
  .foot small { text-align: left; }
  /* Stacked: the mark, the links, then the rule, then the small print. */
  .foot .inner::before { grid-row: 3; }
  .foot .badge, .foot small { grid-row: 4; }
}

/* ══ ARRIVAL ══════════════════════════════════════════════════════════════
 *
 * NOTHING IS HIDDEN WAITING FOR JAVASCRIPT. The resting state of every section
 * is the finished one; the shell adds the class `uv-io` to <html> only once its
 * observer exists, and only then does anything start hidden. Block the script,
 * fail its request, hit a parse error, and the page is simply already there.
 * The live site learned this the expensive way with a seven hundred pixel hole
 * on its home page. */
.uv-io [data-rise] > * { opacity: 0; transform: translateY(8px); }
.uv-io [data-rise].seen > * {
  opacity: 1; transform: none;
  transition: opacity var(--enter) var(--out), transform var(--enter) var(--out);
}
.uv-io [data-rise].seen > :nth-child(2) { transition-delay: 0.06s; }
.uv-io [data-rise].seen > :nth-child(3) { transition-delay: 0.12s; }
.uv-io [data-rise].seen > :nth-child(4) { transition-delay: 0.18s; }
.uv-io [data-rise].seen > :nth-child(n+5) { transition-delay: 0.24s; }
/* A grid stagger runs on the children of the grid itself, capped at eight so a
   catalogue of sixty does not have a card arriving four seconds late. */
.uv-io [data-rise-kids] > * { opacity: 0; transform: translateY(8px); }
.uv-io [data-rise-kids].seen > * { opacity: 1; transform: none; transition: opacity var(--enter) var(--out), transform var(--enter) var(--out); }
.uv-io [data-rise-kids].seen > :nth-child(1) { transition-delay: 0.02s; }
.uv-io [data-rise-kids].seen > :nth-child(2) { transition-delay: 0.06s; }
.uv-io [data-rise-kids].seen > :nth-child(3) { transition-delay: 0.10s; }
.uv-io [data-rise-kids].seen > :nth-child(4) { transition-delay: 0.14s; }
.uv-io [data-rise-kids].seen > :nth-child(5) { transition-delay: 0.18s; }
.uv-io [data-rise-kids].seen > :nth-child(6) { transition-delay: 0.22s; }
.uv-io [data-rise-kids].seen > :nth-child(7) { transition-delay: 0.26s; }
.uv-io [data-rise-kids].seen > :nth-child(n+8) { transition-delay: 0.3s; }

/* ══ HOME ═════════════════════════════════════════════════════════════════ */

.home-intro {
  display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  align-items: center; gap: var(--s10) var(--s13);
  padding: var(--s16) 0 var(--s8); position: relative;
}
.hero { position: relative; padding: 0; min-width: 0; }
.hero-copy { min-width: 0; }
.hero h1 { margin: var(--s6) 0; font-size: clamp(44px, 5.6vw, 62px); line-height: var(--hero-lh, 1.08); letter-spacing: -0.025em; }
/* LIGHT 4: the phrase the page is about. A bloom that fades out before it
   reaches any edge plus a line of light that draws itself, and nothing that has
   a corner. The first version of this on the live site was a filled slab and it
   read as exactly what it was. */
.hero h1 .lit { position: relative; color: var(--vio-txt); }
.hero h1 .lit::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0.01em; height: 2px;
  background: var(--holo); box-shadow: 0 0 16px var(--bloom);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--slow) var(--out) 0.25s;
}
.home-intro.seen .hero h1 .lit::after, html:not(.uv-io) .hero h1 .lit::after { transform: scaleX(1); }
/* THAI HEADINGS NEED THE ROOM THAI TYPE TAKES.
 *
 * The headings are set at 1.08 to 1.16, which suits Latin capitals and x-height
 * and nothing that stacks above and below them. Thai places tone marks and
 * vowels over and under the base line, so at 1.08 the second line's marks rose
 * into the first, and on the home page the underline under the lit word ran
 * straight through the tone marks of the line below it. Thai gets its own line
 * height, and the underline hangs a little lower inside the extra room.
 *
 * The home heading's value travels as `--hero-lh` rather than as a line-height
 * here, because the desktop default look sets that heading's line-height again
 * from a more specific selector (see the default theme block further down),
 * and a plain declaration here lost to it at every width above a tablet. */
:lang(th) :is(h1, h2, h3) { line-height: 1.34; }
:lang(th) .hero h1 {
  --hero-lh: 1.42; line-height: var(--hero-lh);
}
:lang(th) .hero h1 .lit::after { bottom: -0.06em; }
/* Chinese has no marks above the line, but every character fills the whole em
   square, so at 1.08 the home heading's two lines all but touched and the
   underline ran across the tops of the characters under it. Less room than
   Thai needs, and the same lowered underline. */
:lang(zh) .hero h1 {
  --hero-lh: 1.3; line-height: var(--hero-lh);
}
:lang(zh) .hero h1 .lit::after { bottom: -0.06em; }
.hero .sub { color: var(--mute); font-size: 16px; line-height: 1.8; max-width: 43ch; margin: 0 0 var(--s8); }
.hero .cta { display: flex; gap: var(--s3); flex-wrap: wrap; }
/* The price line under the two actions. Quiet by design: the muted colour for
   the words and the ink colour for the figure, so the price reads first and
   nothing competes with the buttons above it. The height is held while it is
   still empty, so a figure arriving from the network shifts nothing. */
.hero-price { min-height: 1.7em; margin: var(--s4) 0 0; color: var(--mute); font-size: 13.5px; line-height: 1.7; }
.hero-price b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
.home-intro .trust {
  grid-column: 1 / -1; display: flex; justify-content: space-between; gap: var(--s5); flex-wrap: wrap;
  margin: var(--s3) 0 0; padding: var(--s6) 0 0; border-top: 1px solid var(--hair);
  color: var(--mute); font-size: 12.5px;
}
.home-intro .trust span { display: inline-flex; align-items: center; gap: 7px; }
.home-intro .trust .ic { width: 14px; height: 14px; color: var(--ok); }

/* On a phone the four reassurances wrap to four lines with a 20px gap between
   them, and that is a hundred and fifty pixels of supporting detail in a hero
   that has to leave room for the page underneath it. Two columns and a tighter
   rhythm say the same four things in half the height. */
@media (max-width: 560px) {
  .hero { padding: 0; }
  .home-intro .trust {
    display: grid; grid-template-columns: 1fr 1fr;
    gap: var(--s2) var(--s3); margin-top: var(--s6);
  }
  .home-intro .trust span { font-size: 11.5px; line-height: 1.3; align-items: flex-start; }
  .home-intro .trust .ic { flex: none; margin-top: 1px; }
}

/* the figures, as a bento rather than a row of numbers */
.bento { display: grid; grid-template-columns: 1fr 1fr 1.35fr; gap: var(--s3); margin-top: var(--s4); }
.stat-sk { min-height: 160px; border-radius: var(--r3); }
.stat {
  position: relative; min-width: 0; padding: var(--s6); border-radius: var(--r3);
  background: var(--surf); border: 1px solid var(--hair);
  box-shadow: inset 0 1px 0 var(--rim);
  transition: border-color var(--tint) ease, transform var(--move) var(--out), background-color var(--tint) ease;
}
.stat:hover { border-color: var(--hair-2); background: var(--raise); }
.bento > .stat { min-height: 160px; }
.stat .lbl { display: block; margin-bottom: var(--s2); }
.stat .fig { display: block; margin-top: var(--s5); font: 400 clamp(28px, 3vw, 38px)/1 var(--disp); letter-spacing: -0.025em; }
.stat .fig small { font-size: 0.5em; color: var(--mute); font-weight: 500; margin-left: 6px; letter-spacing: 0.06em; }
.stat .stat-note { display: inline-flex; align-items: center; flex-wrap: wrap; max-width: 100%; gap: 6px; margin-top: var(--s4); font: 400 11.5px/1.5 var(--disp); color: var(--mute); text-transform: none; letter-spacing: 0; }
.stat-note [data-home-retry] { flex-basis: 100%; justify-content: flex-start; padding-inline: 0; white-space: normal; }
.stat .stat-note .ic { width: 13px; height: 13px; color: var(--glyph); }
.stat.chart { display: block; height: auto; padding-right: 42%; }
.stat.chart .lbl { white-space: nowrap; }
.stat.chart canvas { position: absolute; right: var(--s5); bottom: var(--s6); width: 38%; height: 56px; }
.stat .live { position: absolute; top: var(--s6); right: var(--s5); font-size: 9px; }
@media (max-width: 900px) {
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat.chart, .stat-sk:last-child { grid-column: 1 / -1; }
}
@media (max-width: 520px) {
  .stat { padding: var(--s4); }
  /* The Live mark sits on the label's line, so it follows the padding down
     with it. At 24px against a 16px label it hung half a line low. */
  .stat .live { top: var(--s4); right: var(--s4); }
  .bento > .stat { min-height: 144px; }
  .stat .lbl { line-height: 1.45; font-size: 10px; letter-spacing: 0.08em; }
  .stat .stat-note { align-items: flex-start; font-size: 11px; }
  .stat .stat-note .ic { margin-top: 2px; }
}

/* A static composition of existing game artwork. Only a deliberate hover moves it. */
.hero-library {
  display: block; position: relative; min-width: 0; color: var(--ink);
  border: 1px solid var(--hair-2); border-radius: var(--r4);
  background: linear-gradient(145deg, var(--raise), var(--sunk));
  box-shadow: var(--shadow); overflow: hidden;
}
.library-top { display: flex; justify-content: space-between; align-items: center; padding: var(--s5) var(--s6); color: var(--mute); font: 400 12px/1.4 var(--disp); letter-spacing: 0.035em; }
.library-top .ic { width: 16px; height: 16px; }
.library-art { position: relative; height: 290px; margin: 0 var(--s4); isolation: isolate; }
.library-cover { position: absolute; overflow: hidden; width: 47%; height: 86%; top: 6%; border: 1px solid var(--hair-2); border-radius: var(--r2); background: var(--sunk); box-shadow: var(--shadow-lift); transition: transform var(--slow) var(--out); }
.library-cover img { width: 100%; height: 100%; object-fit: cover; }
.library-cover::after { content: ""; position: absolute; inset: 35% 0 0; background: linear-gradient(transparent, var(--void)); }
.library-cover span { position: absolute; bottom: var(--s4); left: var(--s4); right: var(--s3); z-index: 1; color: var(--ink); font: 600 17px/1.2 var(--disp); }
.library-cover-back { left: 2%; transform: rotate(-11deg) translateY(8px); }
.library-cover-front { left: 27%; top: 0; height: 96%; z-index: 2; transform: rotate(-2deg); }
.library-cover-side { right: 1%; transform: rotate(12deg) translateY(10px); }
.library-caption { display: flex; align-items: center; justify-content: space-between; gap: var(--s4); padding: var(--s6); border-top: 1px solid var(--hair); }
.library-caption small { display: block; color: var(--mute); font-size: 11px; margin-bottom: var(--s2); }
.library-caption b { font: 400 23px/1.25 var(--disp); letter-spacing: -0.01em; }
.library-arrow { display: grid; place-items: center; flex: none; width: 42px; height: 42px; border-radius: 50%; border: 1px solid var(--hair-2); transition: background-color var(--move) ease, transform var(--move) var(--out); }
.hero-library:focus-visible { outline-color: var(--vio-txt); outline-offset: 5px; }
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .hero-library:hover .library-cover-front { transform: rotate(0) translateY(-5px); }
  .hero-library:hover .library-cover-back { transform: rotate(-14deg) translate(-4px, 3px); }
  .hero-library:hover .library-cover-side { transform: rotate(15deg) translate(4px, 4px); }
  .hero-library:hover .library-arrow { transform: translateX(3px); background: var(--raise); }
}
@media (max-width: 900px) {
  .home-intro { gap: var(--s8); }
  .hero h1 { font-size: clamp(40px, 6vw, 60px); }
  .library-art { height: 250px; }
  .library-caption { padding: var(--s5); }
  .library-caption b { font-size: 20px; }
}
@media (max-width: 720px) {
  .home-intro { grid-template-columns: minmax(0, 1fr); padding-top: var(--s10); gap: var(--s8); }
  .hero h1 { font-size: clamp(44px, 10.5vw, 62px); max-width: 12ch; }
  .hero .sub { max-width: 48ch; }
  .library-art { height: clamp(220px, 59vw, 360px); }
  .library-cover { width: 43%; }
  .library-cover-front { left: 28%; }
  .library-cover-back { left: 5%; }
  .library-cover-side { right: 4%; }
  .home-intro .trust { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); margin: 0; }
}
@media (max-width: 560px) {
  .hero .cta { display: grid; grid-template-columns: minmax(0, 1fr); }
  .hero .cta .btn { width: 100%; }
}
@media (max-width: 380px) {
  .library-caption { padding: var(--s4); }
  .library-caption b { font-size: 19px; }
  .library-top { padding: var(--s4); }
}
[data-page-hidden="true"] .amb *,
[data-page-hidden="true"] .amb *::before,
[data-page-hidden="true"] .amb *::after { animation-play-state: paused !important; }

/* the three steps */
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s3); }
.step {
  padding: var(--s5); border-radius: var(--r3); background: var(--surf);
  border: 1px solid var(--hair); box-shadow: inset 0 1px 0 var(--rim);
  transition: border-color var(--tint) ease, transform var(--move) var(--out);
}
.step:hover { border-color: var(--hair-2); background: var(--raise); }
.step:hover .tile { transform: translateY(-2px) scale(1.05); border-color: var(--hair-lit); }
.step .n { font: 600 11px/1 var(--disp); letter-spacing: 0.16em; color: var(--faint); }
.step h3 { margin: var(--s3) 0 var(--s2); font-size: 16px; }
.step p { margin: 0; color: var(--mute); font-size: 13.5px; }
.step .tile { margin-bottom: var(--s4); }
@media (max-width: 820px) { .steps { grid-template-columns: 1fr; } }

/* the loader line, which is the product in one line of text */
.loaderbox { padding: var(--s6); display: grid; gap: var(--s4); }
.loaderbox .code {
  font: 500 13px/1.7 var(--mono); color: var(--mute);
  padding: var(--s4); border-radius: var(--r2); background: var(--sunk);
  border: 1px solid var(--hair); overflow-x: auto; white-space: pre; margin: 0;
}
.loaderbox .code b { color: var(--ink); font-weight: 500; }
.loaderbox .code u { text-decoration: none; color: var(--ion); }
.loaderbox .row { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: center; }

/* ══ CARD GRID (shop, home rail) ══════════════════════════════════════════
 *
 * The card is a poster. The art is the whole surface, the chrome is a hairline,
 * and everything written on it sits on a scrim that is part of the picture
 * rather than in a panel underneath it.
 *
 * Interaction is four things moving together, all of them transform or opacity:
 * the card lifts, the art pushes in, a spotlight tracks the pointer across the
 * surface, and a holographic rim fades up on the border. The spotlight is what
 * makes it feel like a physical object under a light rather than a div that
 * grew a shadow.
 */
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(248px, 100%), 1fr)); gap: var(--s4); }
.card {
  position: relative; aspect-ratio: 1; border-radius: var(--r4); overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end;
  background: var(--sunk); border: 1px solid var(--hair);
  transition: transform var(--move) var(--out), box-shadow var(--move) var(--out), border-color var(--tint) ease;
}
.card:hover { transform: translateY(-2px); border-color: var(--hair-2); box-shadow: var(--shadow-lift); }
.card .art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  filter: saturate(0.96) brightness(0.84);
  transition: transform 0.7s var(--out), filter var(--move) ease; }
.card:hover .art { transform: scale(1.06); filter: none; }
.card .art.none { display: grid; place-items: center; font: 600 46px/1 var(--disp); color: var(--faint); background: var(--surf); }
.card .veil {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(7,6,13,.5) 0%, transparent 26%, rgba(7,6,13,.55) 56%, rgba(7,6,13,.95) 100%);
  transition: opacity var(--move) ease;
}
.card:hover .veil { opacity: 0.92; }
/* The pointer spotlight. `--cx/--cy` are written by the shell on pointermove
   and default to the middle, so a touch device gets a still highlight rather
   than nothing at all. */
.card .glare {
  position: absolute; inset: 0; z-index: 2; pointer-events: none; opacity: 0;
  background: radial-gradient(260px circle at var(--cx, 50%) var(--cy, 50%), color-mix(in srgb, var(--vio-txt) 16%, transparent), transparent 62%);
  transition: opacity var(--move) ease;
}
.card:hover .glare { opacity: 1; }
/**
 * LIGHT 3 on a card: the rim, and it FOLLOWS THE POINTER around the border.
 *
 * The border is drawn by masking a background down to a one pixel frame, which
 * is what makes it follow the radius exactly where a box-shadow would not. The
 * trick is then that the background can be anything: a radial gradient centred
 * on the cursor means the edge is bright where the hand is and dark on the far
 * side, so the card reads as lit by the pointer rather than as switched on.
 *
 * A uniform rim was here before and it is the version that looks generated:
 * every card in a grid lighting up identically is a state change, not a
 * gesture. This costs the same two custom properties the spotlight already
 * writes, so it is free.
 */
.card .rim {
  position: absolute; inset: 0; z-index: 3; border-radius: inherit; padding: 1px; pointer-events: none;
  background:
    radial-gradient(240px circle at var(--cx, 50%) var(--cy, 50%),
      color-mix(in srgb, var(--vio-txt) 95%, transparent), color-mix(in srgb, var(--vio) 50%, transparent) 34%, color-mix(in srgb, var(--ion) 14%, transparent) 58%, transparent 74%);
  opacity: 0;
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  transition: opacity var(--move) ease;
}
.card:hover .rim { opacity: 1; }
/* Without a pointer there is no cursor to follow, so the rim goes back to being
   an even edge rather than a lamp stuck in the middle of every card. */
@media (hover: none) {
  .card .rim { background: var(--holo); }
}
/* The whole picture is the link. A button that reads "View" until you hover it
   and then buys is a trap on a phone, where there is no hover. */
.card .cover { position: absolute; inset: 0; z-index: 4; border-radius: inherit; }
.card .cover:focus-visible { outline: 2px solid var(--vio); outline-offset: -3px; }
.card .tags { position: absolute; z-index: 5; top: var(--s3); left: var(--s3); right: var(--s3);
  display: flex; gap: 6px; flex-wrap: wrap; pointer-events: none; }
/* The text block sits ABOVE the cover link and lets clicks through it, so the
   title and the price are part of the door rather than a dead patch in the
   middle of it. Only the action row takes the pointer back, because those are
   the two controls that do something other than open the page. */
.card .body { position: relative; z-index: 5; padding: 0 var(--s4) var(--s4); pointer-events: none; }
.card .acts { pointer-events: auto; }
/* h3 since 2026-09-27: the cards sit under a section's h2, and an h4 skipped a
   level. h4 is kept in the selector for markup cached from before. */
.card :is(h3, h4) { margin: 0 0 var(--s2); font-size: 18px; line-height: 1.3; text-shadow: 0 1px 14px rgba(7,6,13,.95); overflow-wrap: anywhere; }
/* The price on a plate, so it is legible over whatever the artwork happens to
   be doing at that corner. Measured against the worst case rather than the
   convenient one: sixty covers, and some of them are white. */
.plate {
  display: inline-flex; align-items: baseline; gap: 7px; margin-bottom: var(--s3);
  padding: 6px 11px; border-radius: 10px;
  background: rgba(7, 6, 13, 0.68); border: 1px solid var(--hair-2);
  backdrop-filter: blur(8px);
}
.plate b { font: 600 19px/1 var(--disp); font-variant-numeric: tabular-nums; }
.plate u { text-decoration: none; font: 400 9.5px/1 var(--disp); letter-spacing: 0.11em; text-transform: uppercase; color: var(--mute); }
.plate .from { color: var(--ion); }
.card .acts { display: flex; gap: var(--s2); }
.card .acts .btn { flex: 1; }

/* ══ SHOP ═════════════════════════════════════════════════════════════════ */

.phead { padding: var(--s13) 0 var(--s6); }
.phead h1 { margin: var(--s4) 0 var(--s3); font-size: clamp(28px, 4.4vw, 44px); }
.phead .sub { color: var(--mute); max-width: 60ch; margin: 0; font-size: 15px; }

/* the all-access panel: durations, a figure, and what it buys */
.access { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 300px); gap: var(--s8); padding: var(--s6); }
@media (max-width: 900px) { .access { grid-template-columns: 1fr; gap: var(--s6); } }
.durs { display: flex; gap: var(--s2); flex-wrap: wrap; margin-bottom: var(--s5); }
.durs button {
  padding: 9px 15px; border-radius: var(--r1); border: 1px solid var(--hair);
  color: var(--mute); font: 400 12px/1 var(--disp); letter-spacing: 0.06em;
  transition: color var(--tint) ease, border-color var(--tint) ease, background-color var(--tint) ease, transform var(--tint) var(--spring);
}
.durs button:hover { color: var(--ink); border-color: var(--hair-2); background: var(--raise); }
.durs button[aria-pressed="true"] {
  color: #fff; border-color: var(--hair-lit); background: color-mix(in srgb, var(--vio) 16%, transparent);
  box-shadow: 0 0 20px -12px var(--vio);
}
.tierline { color: var(--vio-txt); font: 400 11px/1 var(--disp); letter-spacing: 0.15em; text-transform: uppercase; }
.amt { display: flex; align-items: baseline; gap: var(--s2); margin: var(--s3) 0 var(--s2);
  font: 600 clamp(34px, 5.4vw, 52px)/1 var(--disp); letter-spacing: -0.005em; font-variant-numeric: tabular-nums; }
.amt u { text-decoration: none; font-size: 0.3em; font-weight: 500; color: var(--mute); letter-spacing: 0.02em; }
.perday { font: 500 13px/1 var(--mono); color: var(--mute); }
.perday i { font-style: normal; color: var(--ion); }
.charged { margin-top: var(--s4); padding-top: var(--s4); border-top: 1px solid var(--hair);
  color: var(--mute); font-size: 12px; line-height: 1.6; }
.charged b { color: var(--ink); font-weight: 500; }
.perks { list-style: none; margin: var(--s5) 0 0; padding: 0; display: grid; gap: var(--s2); }
.perks li { display: grid; grid-template-columns: auto 1fr; gap: var(--s3); align-items: start; font-size: 13.5px; }
.perks .ic { width: 15px; height: 15px; color: var(--ok); margin-top: 3px; }
.perks li.off { color: var(--faint); }
.perks li.off .ic { color: var(--faint); }
.paylist-note { display: flex; gap: 7px; flex-wrap: wrap; margin-top: var(--s4); }
.paylist-note span {
  font: 400 10.5px/1 var(--disp); letter-spacing: 0.1em; text-transform: uppercase; color: var(--mute);
  border: 1px solid var(--hair); padding: 6px 10px; border-radius: var(--r1);
}
.buycol { display: grid; align-content: start; gap: var(--s3); padding: var(--s5);
  border-radius: var(--r3); background: var(--sunk); border: 1px solid var(--hair); }

/* bundles */
.bgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); gap: var(--s4); }
.bundle {
  display: flex; flex-direction: column; gap: var(--s3); padding: var(--s5);
  border-radius: var(--r3); background: var(--surf); border: 1px solid var(--hair);
  box-shadow: inset 0 1px 0 var(--rim);
  transition: border-color var(--tint) ease, transform var(--move) var(--out);
}
.bundle:hover { border-color: var(--hair-2); background: var(--raise); }
.bundle h3 { font-size: 17px; }
.bundle .save { align-self: flex-start; padding: 4px 10px; border-radius: var(--r1);
  background: var(--btn-fill); color: #fff; font: 600 10px/1 var(--disp); letter-spacing: 0.12em; text-transform: uppercase; }
.bgames { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.bgames li { display: flex; align-items: center; gap: var(--s3); min-width: 0; font-size: 13.5px; }
.bgames img, .bgames i {
  width: 28px; height: 28px; flex: none; border-radius: var(--r1); object-fit: cover;
  background: var(--sunk); border: 1px solid var(--hair);
}
.bgames i { display: grid; place-items: center; font-style: normal; font: 600 12px/1 var(--disp); color: var(--faint); }
.bgames span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bundle .bfoot { margin-top: auto; padding-top: var(--s4); display: grid; gap: var(--s3); }
.bundle .bprice { display: flex; align-items: baseline; gap: var(--s2); font: 600 24px/1 var(--disp); font-variant-numeric: tabular-nums; }
.bundle .was { text-decoration: line-through; color: var(--faint); font: 500 13px/1 var(--mono); }

/* ══ GAME PAGE ════════════════════════════════════════════════════════════ */

.crumb { display: flex; align-items: center; gap: var(--s2); padding: var(--s6) 0 0;
  font: 400 11px/1 var(--disp); letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint); }
/* The way back is a target, so it is 24px tall however small its type is. */
.crumb a { display: inline-flex; align-items: center; min-height: 24px; color: var(--mute); }
.crumb a:hover { color: var(--ink); }
.crumb b { color: var(--ink); font-weight: 600; letter-spacing: 0.02em; text-transform: none; font-size: 12.5px; }
.crumb .sep { opacity: 0.4; }

.ghero { display: grid; grid-template-columns: 200px minmax(0, 1fr) 300px; gap: var(--s8); align-items: start; padding: var(--s6) 0 var(--s10); }
.gart {
  position: relative; aspect-ratio: 1; border-radius: var(--r4); overflow: hidden;
  border: 1px solid var(--hair-2);
  box-shadow: var(--shadow-lift), 0 0 40px -22px var(--bloom);
}
.gart img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gart .none { display: grid; place-items: center; height: 100%; font: 600 58px/1 var(--disp); color: var(--faint); background: var(--surf); }
.gbody h1 { font-size: clamp(25px, 3.4vw, 36px); margin: var(--s3) 0 var(--s4); }
/* pre-line: the description arrives as one string with the owner's own line
   breaks in it, one feature or one note per line. Collapsed into a paragraph it
   read as a single wall of emoji; kept, it reads as the list it was written as.
   Runs of spaces still collapse, so a stray double space changes nothing. */
.gbody .gdesc { color: var(--mute); font-size: 14.5px; max-width: 62ch; line-height: 1.72; margin: 0; white-space: pre-line; overflow-wrap: anywhere; }
.gtags { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s4) 0 0; padding: 0; }
.gfacts { display: flex; flex-wrap: wrap; gap: var(--s8); margin-top: var(--s6); padding-top: var(--s5); border-top: 1px solid var(--hair); }
.gfacts span { display: grid; gap: var(--s1); }
.gfacts b { font: 600 22px/1 var(--disp); font-variant-numeric: tabular-nums; }

.gbuy { position: sticky; top: 84px; display: grid; gap: var(--s3); padding: var(--s5);
  border-radius: var(--r3); background: var(--surf); border: 1px solid var(--hair);
  box-shadow: inset 0 1px 0 var(--rim), var(--shadow); }
.gbuy .amt { font-size: 36px; margin: var(--s1) 0 var(--s2); }
/* One row per length, as a RADIO GROUP. Each row used to be a checkout
   button, so a row that looked like an option was a purchase: touching "1 year"
   to compare it opened the payment dialog for a year. Choosing is now choosing,
   and the one button under the group buys.

   The one that costs least per day is marked, and it is computed rather than
   chosen: whichever plan wins the division gets the rim, so a price change
   moves the badge without anybody remembering to. The CHOSEN row is a
   different signal (the filled dot and the violet tint), so best and chosen
   can be told apart when they are two different rows. */
.plans { display: grid; gap: var(--s2); }
.plan {
  position: relative; display: flex; align-items: center; gap: var(--s3); cursor: pointer;
  padding: 12px 14px; border-radius: var(--r2); border: 1px solid var(--hair);
  background: var(--sunk); text-align: left; color: var(--mute); font-size: 13.5px;
  transition: border-color var(--tint) ease, background-color var(--tint) ease, transform var(--tint) var(--spring);
}
/* The native radio stays in the page, focusable and announced, and is drawn
   by the row instead. Arrow keys move between lengths for free. */
.plan input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
/* 18 and 8 differ by an even number, so the dot sits on whole pixels. */
.plan .plan-dot {
  width: 18px; height: 18px; flex: none; display: grid; place-items: center;
  border-radius: 50%; border: 1.5px solid var(--hair-2);
  transition: border-color var(--tint) ease;
}
.plan .plan-dot::after {
  content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--vio-txt);
  transform: scale(0); transition: transform var(--move) var(--spring);
}
.plan .plan-name { flex: 1; min-width: 0; }
.plan b { font: 600 16px/1 var(--disp); color: var(--ink); font-variant-numeric: tabular-nums; }
.plan .rate { display: block; margin-top: 3px; font: 500 11px/1 var(--mono); color: var(--faint); }
.plan:hover { border-color: var(--hair-lit); background: var(--raise); }
.plan.best { border-color: transparent; background:
  linear-gradient(var(--raise), var(--raise)) padding-box, var(--holo) border-box; color: var(--ink); }
.plan[data-on="1"] { border-color: var(--vio); background: color-mix(in srgb, var(--vio) 12%, var(--sunk)); color: var(--ink); }
.plan.best[data-on="1"] { border-color: transparent; background:
  linear-gradient(color-mix(in srgb, var(--vio) 14%, var(--raise)), color-mix(in srgb, var(--vio) 14%, var(--raise))) padding-box,
  var(--holo) border-box; }
.plan[data-on="1"] .plan-dot { border-color: var(--vio-txt); }
.plan[data-on="1"] .plan-dot::after { transform: scale(1); }
.plan:has(input:focus-visible) { outline: 2px solid var(--vio); outline-offset: 2px; }
.plan .flag { display: inline-flex; align-items: center; gap: 5px; margin-left: 7px;
  font: 600 9px/1 var(--disp); letter-spacing: 0.12em; text-transform: uppercase; color: var(--ion); }
/* The call to action names the length and the price, so it can be long in
   Thai; it wraps rather than overflowing the panel. */
.gbuy .gcta { white-space: normal; text-align: center; line-height: 1.3; padding-inline: var(--s4); }
.gbuy .note { margin: 0; color: var(--faint); font-size: 11.5px; }
/* Under the buy box, never above it. The owner's call: the buy box is still what
   is being sold, so it keeps the top of the panel and the free route is the
   footnote. Silence was the thing that was wrong, not the ranking. */
.freenote {
  display: flex; align-items: flex-start; gap: 8px; margin: 0;
  padding-top: var(--s3); border-top: 1px solid var(--hair);
  color: var(--mute); font-size: 12px; line-height: 1.5;
}
.freenote .ic-lead { flex: none; margin-top: 1px; color: var(--ok); }
.freenote a { color: var(--ok); text-decoration: underline; text-underline-offset: 2px; }

/* the feature list, as a reference sheet */
.groups { display: grid; gap: var(--s3); padding-bottom: var(--s10); }
.grp { border-radius: var(--r3); background: var(--surf); border: 1px solid var(--hair); overflow: hidden; }
.grp > summary {
  display: flex; align-items: center; gap: var(--s3); padding: var(--s4) var(--s5); cursor: pointer; list-style: none;
  transition: background-color var(--tint) ease;
}
.grp > summary::-webkit-details-marker { display: none; }
.grp > summary:hover { background: var(--raise); }
.grp > summary b { flex: 1; font: 600 14px/1.2 var(--disp); letter-spacing: 0.02em; }
.grp > summary .n { font: 500 11px/1 var(--mono); color: var(--faint); }
.grp > summary .ic-caret { width: 15px; height: 15px; transition: transform var(--move) var(--out); }
.grp[open] > summary .ic-caret { transform: rotate(180deg); }
.grp[open] > summary { border-bottom: 1px solid var(--hair); }
.feat {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s4); align-items: center;
  padding: var(--s3) var(--s5); border-top: 1px solid var(--hair);
  transition: background-color var(--tint) ease;
}
.feat:first-child { border-top: 0; }
.feat:hover { background: color-mix(in srgb, var(--vio) 5%, transparent); }
.feat b { display: block; font-weight: 500; font-size: 13.5px; }
.feat span { display: block; margin-top: 2px; color: var(--mute); font-size: 12.5px; line-height: 1.55; }
/* The control a feature actually gets in the menu. Not a screenshot: the real
   widget, so somebody can touch it before paying. */
.ctl { display: flex; align-items: center; gap: var(--s3); }
.sw {
  width: 38px; height: 21px; flex: none; border-radius: var(--pill); position: relative;
  border: 1px solid var(--hair-2); background: var(--sunk);
  transition: border-color var(--tint) ease, background-color var(--tint) ease, box-shadow var(--move) var(--out);
}
.sw::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 13px; height: 13px; border-radius: 50%;
  background: var(--faint);
  transition: transform var(--move) var(--spring), background-color var(--tint) ease, box-shadow var(--tint) ease;
}
.sw[aria-pressed="true"] { border-color: var(--vio); background: color-mix(in srgb, var(--vio) 18%, transparent); box-shadow: 0 0 16px -8px var(--vio); }
.sw[aria-pressed="true"]::after { transform: translateX(17px); background: var(--vio-txt); box-shadow: 0 0 9px var(--bloom); }
.sld {
  width: 120px; height: 5px; flex: none; border-radius: var(--pill); position: relative;
  background: color-mix(in srgb, var(--vio-txt) 14%, transparent); cursor: grab; touch-action: none;
}
.sld i { position: absolute; inset: 0 auto 0 0; width: var(--p, 50%); border-radius: inherit; background: var(--holo); }
.sld::after {
  content: ""; position: absolute; top: 50%; left: var(--p, 50%); width: 14px; height: 14px; border-radius: 50%;
  transform: translate(-50%, -50%); background: var(--ink); border: 2px solid var(--vio);
  box-shadow: 0 0 12px -3px var(--bloom); transition: transform var(--tint) var(--spring);
}
.sld:hover::after { transform: translate(-50%, -50%) scale(1.16); }
.sld[data-drag="1"] { cursor: grabbing; }
.sld[data-drag="1"]::after { transition: none; transform: translate(-50%, -50%) scale(1.2); }
.ctl .lim { font: 500 11px/1 var(--mono); color: var(--faint); }
.pick {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: var(--r1);
  border: 1px solid var(--hair); background: var(--sunk); color: var(--mute);
  font: 500 11.5px/1 var(--mono);
  transition: border-color var(--tint) ease, color var(--tint) ease;
}
.pick:hover { border-color: var(--hair-lit); color: var(--ink); }
.pick .ic { width: 12px; height: 12px; }

@media (max-width: 1000px) {
  .ghero { grid-template-columns: 150px minmax(0, 1fr); }
  .gbuy { grid-column: 1 / -1; position: static; }
}
@media (max-width: 620px) {
  .ghero { grid-template-columns: 1fr; gap: var(--s5); }
  .gart { max-width: 180px; }
  .feat { grid-template-columns: 1fr; gap: var(--s2); }
}

/* ══ SCRIPTS, AS A REFERENCE ══════════════════════════════════════════════
 *
 * The layout the owner pointed at: a rail of everything on the left, one
 * subject open on the right. It suits this page better than the list it
 * replaces, because the question is "is my game supported and what does it do",
 * which is two questions that want to be on screen at once.
 */
.ref { display: grid; grid-template-columns: 280px minmax(0, 1fr); align-items: start; gap: 0; }
.ref .rail {
  position: sticky; top: 68px; align-self: start;
  border-right: 1px solid var(--hair); padding: var(--s5) var(--s3) var(--s6) 0;
  display: grid; gap: 2px; align-content: start;
  max-height: calc(100vh - 68px); overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin;
}
.ref .rail .lbl { padding: var(--s4) var(--s3) var(--s2); }
.ref .rail .seek { margin: 0 var(--s2) var(--s3); }
.ritem {
  position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--s3); padding: 9px var(--s3); border-radius: var(--r1);
  color: var(--mute); font-size: 13.5px; text-align: left; width: 100%;
  transition: color var(--tint) ease, background-color var(--tint) ease;
}
.ritem img, .ritem i {
  width: 26px; height: 26px; flex: none; border-radius: 7px; object-fit: cover;
  background: var(--sunk); border: 1px solid var(--hair);
}
.ritem i { display: grid; place-items: center; font-style: normal; font: 600 11px/1 var(--disp); color: var(--faint); }
.ritem em { font-style: normal; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.ritem .n { display: inline-flex; align-items: center; gap: 6px; font: 500 10.5px/1 var(--mono); color: var(--faint); }
.ritem:hover { color: var(--ink); background: color-mix(in srgb, var(--vio) 7%, transparent); }
.ritem[aria-current="true"] { color: var(--ink); background: color-mix(in srgb, var(--vio) 12%, transparent); }
.ritem[aria-current="true"]::before {
  content: ""; position: absolute; left: 0; top: 8px; bottom: 8px; width: 2px; border-radius: 2px;
  background: var(--holo); box-shadow: 0 0 10px var(--bloom);
}

/* ── the free/paid split ──────────────────────────────────────────────────
   48 of the 52 working scripts run on a free key. The catalogue always knew,
   `category` shipped in the API response, and no frontend file read it, so the
   library rendered a price and a buy button on all of it and every visitor
   concluded the whole thing was paid. These are the marks that say otherwise. */

/* The rail is one narrow column, so the mark is a dot rather than a word. The
   filter buttons carry the same two colours next to their labels, which is what
   teaches the dot without spending a legend on it. */
.cdot { width: 6px; height: 6px; border-radius: 50%; flex: none; display: block; }
.cdot[data-c="free"] { background: var(--ok); box-shadow: 0 0 7px color-mix(in srgb, var(--ok) 55%, transparent); }
.cdot[data-c="paid"] { background: var(--warn); box-shadow: 0 0 7px color-mix(in srgb, var(--warn) 55%, transparent); }

.catfilter { display: flex; gap: 4px; padding: 0 var(--s3) var(--s2); }
.catfilter button {
  display: inline-flex; align-items: center; gap: 6px; flex: 1 1 0; justify-content: center;
  padding: 6px 4px; border-radius: var(--r1); border: 1px solid transparent;
  background: transparent; color: var(--faint); cursor: pointer;
  font: 400 10.5px/1 var(--disp); letter-spacing: 0.1em; text-transform: uppercase;
  transition: color var(--tint) ease, background-color var(--tint) ease, border-color var(--tint) ease;
}
.catfilter button:hover { color: var(--mute); background: color-mix(in srgb, var(--vio) 7%, transparent); }
.catfilter button[aria-pressed="true"] {
  color: var(--ink); background: color-mix(in srgb, var(--vio) 12%, transparent);
  border-color: var(--hair-2);
}
.ref .body { padding: var(--s6) 0 var(--s10) var(--s8); min-width: 0; }
.ref .body h2 { margin: var(--s3) 0 var(--s2); }
.ref .body .gdesc { color: var(--mute); font-size: 14px; max-width: 68ch; margin: 0 0 var(--s6); white-space: pre-line; overflow-wrap: anywhere; }

/* Back to the list, and only on a phone.
 *
 * On a laptop the rail is beside this and always on screen, so a back control
 * would point at something the reader can already see. Below 980px the two
 * panes take turns and this is the only way out of an open game. The
 * STYLESHEET decides that, not the script: the button is in the markup either
 * way, so there is no state where the detail is showing and its way back is
 * missing. */
.docback {
  display: none; align-items: center; gap: 7px; margin: 0 0 var(--s4);
  padding: 8px 13px 8px 9px; border-radius: var(--pill);
  border: 1px solid var(--hair); background: var(--surf); color: var(--mute);
  font: 400 11.5px/1 var(--disp); letter-spacing: 0.08em; text-transform: uppercase;
  transition: color var(--tint) ease, border-color var(--tint) ease, background-color var(--tint) ease;
}
.docback:hover { color: var(--ink); border-color: var(--hair-2); background: var(--raise); }
.docback:active { transform: scale(0.972); }
/* The caret is the only chevron in the sprite and it points down. A quarter
   turn costs nothing; a twenty-fourth icon meaning "left" costs a request. */
.docback .ic { width: 15px; height: 15px; transform: rotate(90deg); }
.catgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr)); gap: var(--s3); margin-bottom: var(--s6); }
.catcard {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--s3);
  padding: var(--s4); border-radius: var(--r2); background: var(--surf); border: 1px solid var(--hair);
  text-align: left; width: 100%;
  transition: border-color var(--tint) ease, transform var(--move) var(--out), background-color var(--tint) ease;
}
.catcard:hover { border-color: var(--hair-2); background: var(--raise); }
.catcard:hover .tile { transform: scale(1.06); border-color: var(--hair-lit); }
.catcard:hover > .ic-arw { opacity: 1; transform: none; }
.catcard b { display: block; font: 600 13.5px/1.3 var(--disp); }
/**
 * `b + span`, NOT a bare `span`, and this is the bug the owner kept seeing.
 *
 * The icon tile in this card is `<span class="tile">`. A rule written as
 * `.catcard span` therefore matched it, and matched it at specificity (0,1,1)
 * against the tile's own (0,1,0) from a later point in the sheet, so it won
 * twice over. The tile stopped being `display: grid`, `place-items: center`
 * stopped meaning anything, and the glyph fell to the top left of its box. It
 * also picked up `margin-top: 3px`, a muted colour and a 12px font.
 *
 * From outside it looks exactly like an icon that is not centred, which is what
 * it was reported as three times. Two earlier fixes were real and neither was
 * this: a half pixel of rounding, and a tile that was fine. This is the rest.
 *
 * The description is the span that FOLLOWS the title, so say that. A bare type
 * selector inside a component captures every element of that type in it,
 * including the ones wearing a utility class that was meant to win.
 */
.catcard b + span { display: block; margin-top: 3px; color: var(--mute); font-size: 12px; line-height: 1.5; }
/* ── the same two panes on a phone, taking turns ──────────────────────────
 *
 * Two defects lived in the four lines this replaces, and the first one did not
 * make the page awkward, it made it impossible: THE PAGE COULD NOT BE SCROLLED
 * AT ALL.
 *
 * The rail was reset here for `position` and `max-height` and not for the two
 * declarations whose only purpose is to serve them. With `max-height: none` it
 * becomes an `overflow-y: auto` box whose content always fits: a scroll
 * container with nowhere to scroll. `overscroll-behavior: contain` then tells
 * the browser not to hand the gesture on to the page. Between them, a finger
 * landing anywhere on the rail moved nothing, and on a phone the rail IS the
 * first four screens. Measured on a Pixel viewport: a touch drag starting on
 * the header scrolled 385px, and the identical drag starting on the list
 * scrolled 0. So undo the desktop rail completely here, not partly.
 *
 * The second: stacked, sixty seven rows put the opened game 3,192px down the
 * document, so tapping a row drew the answer four screens below the fold and
 * looked like it had done nothing. The panes take turns instead. The list is
 * the screen until a game is picked, and then the game is, with a way back.
 *
 * `data-pane` is written by uv-scripts.js and is absent until it runs, which
 * is deliberate: with no script the two panes simply stack, which is what this
 * page did before and is still readable. Nothing here can hide a pane that has
 * no way to be shown again.
 */
@media (max-width: 980px) {
  .ref { grid-template-columns: 1fr; }
  .ref .rail {
    position: static; max-height: none; overflow: visible; overscroll-behavior: auto;
    border-right: 0; padding: var(--s4) 0;
  }
  .ref .body { padding-left: 0; }
  .ref[data-pane="doc"] > .rail { display: none; }
  .ref[data-pane="list"] > .body { display: none; }
  /* A row of its own. Inline it landed beside the eyebrow, where it read as a
     second badge rather than as the way out of this pane. */
  .docback { display: flex; width: -moz-fit-content; width: fit-content; }
}

/* ══ EMPTY, LOADING AND ERROR ═════════════════════════════════════════════
 *
 * Loading is a designed state rather than a sentence, and it is seeded in the
 * MARKUP so a page whose scripts never run is still a designed screen. */
.sk { border-radius: var(--r4); background: var(--surf); border: 1px solid var(--hair);
  animation: uv-sk 1.5s var(--out) infinite alternate; }
.sk.card-sk { aspect-ratio: 1; }
.sk.line { height: 13px; border-radius: var(--pill); background: color-mix(in srgb, var(--vio-txt) 10%, transparent); border: 0; }
.sk.line.tall { height: 40px; border-radius: var(--r1); }
.skstack { display: grid; gap: var(--s3); max-width: 22rem; }
.skrow { display: flex; gap: var(--s2); }
.skrow .sk { height: 34px; width: 5rem; border-radius: var(--r1); }
.cards .sk:nth-child(even), .skstack .sk:nth-child(2) { animation-delay: 0.15s; }
.cards .sk:nth-child(3n), .skstack .sk:nth-child(3) { animation-delay: 0.3s; }
@keyframes uv-sk { from { opacity: 1; } to { opacity: 0.42; } }

.empty { padding: var(--s16) var(--s6); text-align: center; }
.empty .tile { margin: 0 auto var(--s4); }
.empty b { display: block; font: 600 19px/1.3 var(--disp); margin-bottom: var(--s2); }
.empty p { margin: 0 auto var(--s5); max-width: 46ch; color: var(--mute); font-size: 13.5px; }
.empty .acts { display: flex; gap: var(--s3); justify-content: center; flex-wrap: wrap; }
.msg { font-size: 13.5px; color: var(--mute); margin: var(--s3) 0 0; }
.msg[data-kind="bad"], .msg[data-kind="warn"] { color: var(--warn); }
.msg[data-kind="ok"] { color: var(--ok); }
.msg:empty { display: none; }

/* ══ THE PAYMENT DIALOG ═══════════════════════════════════════════════════
 *
 * checkout.js is reused UNCHANGED and this restyles the markup it emits. That
 * is deliberate and it is the difference between a preview and a mock: Stripe,
 * Coinbase, coupon validation, the merge-destination question and the Robux
 * quote are the same money path that production runs, proven and tested, while
 * everything visible about it is new.
 *
 * Rewriting the checkout to get a new look would be putting a redesign in front
 * of the one flow where a mistake costs a customer money.
 */
.paypick {
  position: fixed; inset: 0; z-index: 220; display: grid; place-items: center; padding: var(--s5);
  background: rgba(4, 3, 9, 0.72); backdrop-filter: blur(6px);
  opacity: 0; visibility: hidden;
  transition: opacity var(--move) var(--out), visibility 0s linear var(--move);
}
.paypick[data-open="1"] { opacity: 1; visibility: visible; transition: opacity var(--move) var(--out), visibility 0s; }
.paydialog {
  width: min(640px, 100%); max-height: min(88vh, 940px);
  border-radius: var(--r4); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain;
  background: linear-gradient(180deg, color-mix(in srgb, var(--deep) 62%, var(--raise)), var(--sunk));
  border: 1px solid var(--hair-2); position: relative;
  box-shadow: inset 0 1px 0 var(--rim), 0 60px 120px -50px rgba(0, 0, 0, 0.96), 0 0 90px -40px var(--bloom);
  transform: translateY(12px) scale(0.985);
  transition: transform var(--enter) var(--spring);
}
.paypick[data-open="1"] .paydialog { transform: none; }
.paydialog::before {
  content: ""; position: absolute; inset: -1px -1px auto; height: 1px; z-index: 5;
  background: var(--holo); opacity: 0.8;
  -webkit-mask-image: linear-gradient(90deg, transparent, #fff 14%, #fff 86%, transparent);
          mask-image: linear-gradient(90deg, transparent, #fff 14%, #fff 86%, transparent);
}
.payhead { padding: var(--s6) var(--s6) var(--s4); border-bottom: 1px solid var(--hair); }
.payhead h3 { margin: 0 0 var(--s2); font-size: 20px; }
.payhead p { margin: 0; color: var(--mute); font-size: 13px; }
.payhead .payfor { color: var(--vio-txt); font-weight: 600; }

.paylist { display: grid; }
.paymethod {
  position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--s4); width: 100%; text-align: left;
  padding: var(--s5) var(--s6); border-bottom: 1px solid var(--hair);
  transition: background-color var(--tint) ease;
}
/* The icon checkout.js emits is a bare `<svg class="ic">`. Wrapping it in a
   tile is not possible without touching that file, so the tile is drawn AROUND
   it here instead: same object, no change to the money path. */
.paymethod > .ic {
  width: 40px; height: 40px; padding: 10px; border-radius: var(--r2);
  background: linear-gradient(180deg, color-mix(in srgb, var(--vio) 14%, transparent), color-mix(in srgb, var(--vio) 2%, transparent));
  border: 1px solid var(--hair-2); box-shadow: inset 0 1px 0 var(--rim);
  color: var(--vio-txt);
  transition: transform var(--move) var(--spring), border-color var(--tint) ease, color var(--tint) ease;
}
.paymethod::before {
  content: ""; position: absolute; left: 0; top: 10px; bottom: 10px; width: 2px; border-radius: 2px;
  background: var(--holo); box-shadow: 0 0 12px var(--bloom);
  transform: scaleY(0); transition: transform var(--move) var(--out);
}
.paymethod:hover, .paymethod:focus-visible { background: color-mix(in srgb, var(--vio) 6%, transparent); }
.paymethod:hover::before, .paymethod:focus-visible::before { transform: scaleY(1); }
.paymethod:hover > .ic { transform: scale(1.06); border-color: var(--hair-lit); }
.paymethod b { display: block; font: 600 15.5px/1.2 var(--disp); letter-spacing: 0.02em; }
.paymethod p { margin: var(--s2) 0 0; color: var(--mute); font-size: 12.5px; line-height: 1.55; }
.paymethod .paygo { display: inline-flex; align-items: center; gap: 6px; color: var(--vio-txt);
  font: 600 12px/1 var(--disp); letter-spacing: 0.03em; white-space: nowrap; }
.paymethod[disabled] { opacity: 0.45; cursor: default; }
.paymethod[disabled]:hover { background: none; }
.paymethod[disabled]:hover::before { transform: scaleY(0); }
/* Robux is settled by a person rather than by a network, so it reads as a
   different KIND of route rather than as a lesser version of the first two. */
.paymethod[data-method="robux"] > .ic {
  color: var(--ion); border-color: color-mix(in srgb, var(--ion) 26%, transparent);
  background: linear-gradient(180deg, color-mix(in srgb, var(--ion) 13%, transparent), color-mix(in srgb, var(--ion) 2%, transparent));
}
.paymethod[data-method="robux"] .paygo { color: var(--ion); }
.paytag {
  display: inline-block; margin-left: var(--s2); padding: 3px 8px; border-radius: var(--r1);
  font: 600 9px/1 var(--disp); letter-spacing: 0.12em; text-transform: uppercase; vertical-align: middle;
}
.paytag.fast { background: var(--btn-fill); color: #fff; }
.paytag.manual { border: 1px solid var(--hair-2); color: var(--mute); }

/* where the purchase lands */
.paydest {
  display: grid; grid-template-rows: 0fr; opacity: 0; background: var(--sunk);
  border-bottom: 1px solid var(--hair);
  transition: grid-template-rows var(--slow) var(--out), opacity var(--move) var(--out);
}
.paydest[data-in="1"] { grid-template-rows: 1fr; opacity: 1; }
.destinner { overflow: hidden; padding: 0 var(--s6); transition: padding var(--slow) var(--out); }
.paydest[data-in="1"] .destinner { padding: var(--s5) var(--s6); }
.destlead { margin: 0 0 var(--s3); font: 400 10.5px/1 var(--disp); letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute); }
.destopts { display: grid; gap: var(--s2); max-height: min(44vh, 380px); overflow-y: auto; overscroll-behavior: contain; }
.destopt {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s3); align-items: start;
  text-align: left; padding: var(--s3) var(--s4); border-radius: var(--r2);
  border: 1px solid var(--hair); background: var(--surf);
  transition: border-color var(--tint) ease, background-color var(--tint) ease;
}
.destopt:hover { border-color: var(--hair-2); background: var(--raise); }
.destopt[aria-pressed="true"] { border-color: var(--vio); background: var(--raise); box-shadow: inset 0 0 0 1px var(--vio); }
.destmark {
  width: 15px; height: 15px; margin-top: 0.2em; border-radius: 50%; display: grid; place-items: center;
  border: 1px solid var(--hair-2); transition: border-color var(--tint) ease;
}
.destmark::after {
  content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--vio);
  transform: scale(0); transition: transform var(--tint) var(--spring);
}
.destopt[aria-pressed="true"] .destmark { border-color: var(--vio); }
.destopt[aria-pressed="true"] .destmark::after { transform: scale(1); }
.destopt b { display: block; font-weight: 600; font-size: 14px; }
.destopt code { font: 500 12.5px/1 var(--mono); color: var(--ion); letter-spacing: 0.06em; }
.destopt i { display: block; margin-top: 3px; font-style: normal; color: var(--mute); font-size: 12.5px; }
.destnote { margin: var(--s3) 0 0; color: var(--faint); font-size: 11.5px; }

.paycoupon { padding: var(--s5) var(--s6) var(--s3); display: grid; gap: var(--s2); }
.paycoupon .field { display: grid; gap: 7px; }
.paycoupon .field > span { font: 400 10.5px/1 var(--disp); letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute); }
.paycoupon input {
  width: 100%; padding: 11px 13px; border-radius: var(--r1);
  border: 1px solid var(--hair); background: var(--void); color: var(--ink);
  font: 500 13px/1.2 var(--mono); letter-spacing: 0.05em; text-transform: uppercase;
  transition: border-color var(--tint) ease, box-shadow var(--move) var(--out);
}
.paycoupon input::placeholder { color: var(--faint); text-transform: none; letter-spacing: 0; }
.paycoupon input:focus { outline: none; border-color: var(--vio); box-shadow: 0 0 0 3px color-mix(in srgb, var(--vio) 16%, transparent); }
.paycoupmsg { margin: 0; font: 400 11.5px/1.4 var(--disp); letter-spacing: 0.02em; }
.paycoupmsg:empty { display: none; }
.paycoupmsg[data-kind="ok"] { color: var(--ok); }
.paycoupmsg[data-kind="bad"] { color: var(--warn); }

.paymsg { margin: 0; padding: 0 var(--s6) var(--s4); font-size: 13px; color: var(--mute); }
.paymsg:empty { display: none; }
.paymsg[data-kind="bad"] { color: var(--warn); }
.payfoot { display: flex; align-items: center; justify-content: space-between; gap: var(--s4); padding: var(--s4) var(--s6) var(--s6); }
.payfoot .paynote { margin: 0; color: var(--faint); font-size: 11.5px; }

/* the Robux quote, which is shown before the ticket */
.robuxquote { padding: var(--s5) var(--s6) var(--s6); display: grid; gap: var(--s5); }
.rqamt {
  position: relative; display: grid; gap: var(--s1); justify-items: center; text-align: center;
  padding: var(--s6) var(--s5); border-radius: var(--r3);
  background: var(--sunk); border: 1px solid var(--hair); overflow: hidden;
}
/* NEGATIVE INSET, so the rule reaches the corners.
 *
 * `left: 0; right: 0` on an absolutely positioned child is measured from the
 * PADDING box, which is inside the border. On a box with a 1px border that
 * leaves the accent rule a pixel short at each end, and it reads as a line that
 * does not quite fit its container. Pulling it out by the border width spans
 * the border box instead, and `overflow: hidden` on the parent trims it to the
 * rounded corner. */
.rqamt::before { content: ""; position: absolute; inset: -1px -1px auto; height: 1px; background: var(--holo); }
.rqamt > span { font: 400 10.5px/1 var(--disp); letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute); }
/**
 * THE NUMBER is what lines up with the label and the rate, not the number plus
 * its icon.
 *
 * This was `inline-flex` with the icon as a flex item, so the grid centred the
 * PAIR: a 31px mark and an 8px gap pushed the digits about twenty pixels right
 * of the centre that "YOU PAY" and the rate share. Three things that should
 * read as one stack, and the biggest of them off axis.
 *
 * The icon is taken out of flow and hung off the left edge instead. The `<b>`
 * shrinks to the digits, the grid centres the digits, and the mark sits in
 * front of them without having a vote on where the middle is.
 */
.rqamt > b {
  position: relative; display: block;
  font: 600 clamp(30px, 6vw, 44px)/1 var(--disp); font-variant-numeric: tabular-nums;
}
.rqamt > b .ic {
  position: absolute; right: 100%; margin-right: 0.16em;
  width: 0.62em; height: 0.62em; color: var(--ion);
  /* 44% rather than 50%. What is centred here is a row of DIGITS, which have no
     descenders, so their optical middle sits above the middle of the line box.
     Half the line box would hang the mark visibly low against them. */
  top: 44%; transform: translateY(-50%);
}
.rqamt > u { text-decoration: none; color: var(--faint); font-size: 12px; }
.rqsteps { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--s3); counter-reset: rq; }
.rqsteps li { position: relative; padding-left: 30px; color: var(--mute); font-size: 13.5px; line-height: 1.55; }
.rqsteps li:first-child { padding-left: 0; color: var(--ink); font-weight: 600; }
/* The marker is centred on the FIRST LINE, computed from the line height rather
   than nudged with a magic `top: 1px`. That guess was tuned against one font at
   one size, and it went out by a couple of pixels the moment the display face
   changed, which is what the owner saw: numbered bullets sitting high against
   the text beside them. */
.rqsteps li:not(:first-child)::before {
  counter-increment: rq; content: counter(rq);
  position: absolute; left: 0; top: calc((1.55em - 20px) / 2);
  width: 20px; height: 20px; border-radius: 50%;
  display: grid; place-items: center; border: 1px solid var(--hair-2); background: var(--sunk);
  /* 11px/18px, NOT 11px/1. The box is 20px with a 1px border, so its content
     box is 18px, and a line box of 11px centres at (18 - 11) / 2 = 3.5px: a
     half pixel, rounded to one side, and the digit sits off centre inside its
     own circle. Matching the line height to the content box makes the offset
     exactly zero rather than nearly right. Same defect as the 19px glyph in a
     40px tile, one level down: it is the LINE BOX that gets centred here, not
     the glyph, so the line height is the number that has to divide evenly. */
  font: 400 11px/18px var(--disp); color: var(--vio-txt); font-variant-numeric: tabular-nums;
}
.rqnote {
  margin: 0; padding: var(--s3) var(--s4); border: 1px dashed var(--hair-2); border-radius: var(--r2);
  background: var(--sunk); color: var(--mute); font-size: 12px;
}
/* Two ways to pay in Robux, once the in-game shop sells this plan: in the
   game (instant) or in a ticket (a person, for codes and the garments). Two
   plain blocks, the instant one lit, because they are alternatives and the
   quote above them is the same for both. */
.rqroute {
  display: grid; gap: var(--s2); padding: var(--s4); border-radius: var(--r2);
  border: 1px solid var(--hair); background: var(--surf);
}
.rqroute.rqfast { border-color: var(--hair-lit); background: color-mix(in srgb, var(--vio) 7%, var(--surf)); }
.rqroute > b { font: 600 14.5px/1.3 var(--disp); }
.rqroute > p { margin: 0; color: var(--mute); font-size: 13px; line-height: 1.55; }
.rqroute > .btn { justify-self: start; }

@media (max-width: 560px) {
  .paymethod { grid-template-columns: auto minmax(0, 1fr); }
  .paymethod .paygo { grid-column: 2; }
}

/* ══ THE PREVIEW BANNER ═══════════════════════════════════════════════════
 *
 * This site is a proposal living at a real URL against real data. Somebody who
 * lands here from a search result or a pasted link has to be told that, and
 * told where the real one is, or the preview becomes a support ticket. */
.pvbar {
  position: relative; z-index: 2; display: flex; align-items: center; gap: var(--s3);
  flex-wrap: wrap; padding: 9px max(var(--s6), calc((100% - 1180px) / 2 + var(--s6)));
  background: linear-gradient(90deg, color-mix(in srgb, var(--vio) 14%, transparent), color-mix(in srgb, var(--ion) 7%, transparent));
  border-bottom: 1px solid var(--hair);
  font: 400 11.5px/1.4 var(--disp); letter-spacing: 0.05em; color: var(--mute);
}
.pvbar b { color: var(--ink); font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; font-size: 10.5px; }
.pvbar .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 9px var(--warn); flex: none; }
.pvbar a { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; color: var(--vio-txt); white-space: nowrap; }
.pvbar a .ic { width: 13px; height: 13px; color: currentColor; }

/* ══ REDUCED MOTION ═══════════════════════════════════════════════════════
 *
 * Arriving without the journey, never arriving at nothing. Every end state is
 * still reached; only the travel is removed. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001s !important; animation-iteration-count: 1 !important;
    transition-duration: 0.001s !important; transition-delay: 0s !important;
  }
  .uv-io [data-rise] > *, .uv-io [data-rise-kids] > * { opacity: 1 !important; transform: none !important; }
  .hero h1 .lit::after { transform: scaleX(1) !important; }
  .card:hover, .stat:hover, .step:hover, .bundle:hover, .catcard:hover, .btn:hover { transform: none; }
  html.uv-ready body, html:not(.uv-ready) body { opacity: 1 !important; transform: none !important; }
  .btn.pressed::before { animation: none !important; }
}

/* ══ ARRIVAL ══════════════════════════════════════════════════════════════
 *
 * The first paint, once.
 *
 * A page with a self-hosted face and four stylesheets does not become itself in
 * one frame: the type reflows as the font resolves and the panels settle as the
 * sheets apply, and the reader watches it assemble. Holding the document one
 * step back until the shell says it is ready turns that into a single composed
 * entrance.
 *
 * Written so that ITS ABSENCE MEANS VISIBLE. A blocked, failed or slow script
 * never adds `uv-ready`, and the rule that dims the body only applies while
 * `uv-hold` is present, which the document carries for at most 900ms because
 * the stylesheet itself times it out. Nothing can strand the page blank: that
 * is the mistake the live site made once and it cost a seven hundred pixel hole
 * on the home page.
 */
@keyframes uv-hold-out { to { opacity: 1; } }
html.uv-hold body {
  opacity: 0.001;
  /* The animation is the timeout. It runs regardless of any script, so the page
     is fully visible half a second after paint whatever else happens. */
  animation: uv-hold-out 0.44s var(--out) 0.06s forwards;
}
html.uv-ready body { opacity: 1; animation: none; }

/* OPACITY ONLY, and this is not a stylistic preference.
 *
 * The obvious version of this pairs the fade with a small `translateY`, and it
 * is a trap: a transform on an ancestor becomes the containing block for every
 * `position: fixed` descendant beneath it. The fixed ambient layer would
 * position itself against the body instead of the viewport for the length of
 * the animation and snap back at the end, and so would any dialog the reused
 * checkout script opens while the page is still arriving. */

/* ══ SHOWCASES ════════════════════════════════════════════════════════════
 *
 * A grid of video thumbnails, and the whole design problem is that YouTube
 * artwork is the loudest material on this site: saturated, high contrast, and
 * covered in somebody else's text. So the chrome around it gives up almost
 * everything. A hairline, a radius, and one violet edge under the pointer.
 *
 * Nothing embeds until it is pressed. See uv-showcases.js.
 */
.clips {
  display: grid; gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
}
.clip { min-width: 0; display: grid; gap: var(--s3); }

.clip .shot {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  border-radius: var(--r3); border: 1px solid var(--hair); background: var(--sunk);
  transition: border-color var(--tint) ease, box-shadow var(--move) var(--out);
}
.clip .shot img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  filter: saturate(0.92) brightness(0.82);
  transition: transform 0.7s var(--out), filter var(--move) ease;
}
.clip .shot iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.clip .shot .veil {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 40%, rgba(7, 6, 13, 0.6) 100%);
  transition: opacity var(--move) ease;
}
.clip:hover .shot { border-color: var(--hair-lit); box-shadow: 0 0 34px -18px var(--bloom); }
.clip:hover .shot img { transform: scale(1.045); filter: none; }
.clip:hover .shot .veil { opacity: 0.7; }

/**
 * The play control.
 *
 * It is a real button and it is always visible, never a hover-only affordance.
 * A play triangle that only appears on hover is invisible on every phone, which
 * is where most of this audience is.
 *
 * The ring grows and the triangle nudges right on hover. Both are transforms on
 * separate elements, so the ring can scale without stretching the glyph inside
 * it, which is what happens when you scale the parent.
 */
.clip .playbtn {
  position: absolute; left: 50%; top: 50%; z-index: 2;
  width: 56px; height: 56px; margin: -28px 0 0 -28px;
  display: grid; place-items: center; border-radius: 50%;
  background: rgba(10, 8, 20, 0.62); border: 1px solid var(--rim-hi);
  backdrop-filter: blur(6px);
  transition: transform var(--move) var(--out), background-color var(--tint) ease,
              border-color var(--tint) ease, box-shadow var(--move) var(--out);
}
.clip .playbtn .ic {
  width: 22px; height: 22px; color: #fff; margin-left: 3px;
  transition: transform var(--move) var(--out);
}
.clip:hover .playbtn, .clip .playbtn:focus-visible {
  transform: scale(1.09);
  background: var(--btn-fill); border-color: var(--hair-lit);
  box-shadow: 0 0 30px -6px var(--bloom);
}
.clip:hover .playbtn .ic { transform: translateX(2px); }
.clip .playbtn:active { transform: scale(0.97); transition-duration: 0.07s; }
/* Once a player is in there, the button is gone with the thumbnail it sat on. */
.clip .shot[data-playing="1"] .veil, .clip .shot[data-playing="1"] .playbtn { display: none; }

.clip .pin {
  position: absolute; z-index: 2; top: var(--s3); left: var(--s3);
  padding: 5px 9px; border-radius: var(--r1);
  background: rgba(10, 8, 20, 0.72); border: 1px solid var(--hair-lit);
  backdrop-filter: blur(6px);
  font: 400 9.5px/1 var(--disp); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--vio-txt);
}

.clip .meta { display: grid; gap: 3px; min-width: 0; }
.clip .meta b {
  font: 600 14px/1.35 var(--disp); color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  transition: color var(--tint) ease;
}
.clip:hover .meta b { color: #fff; }
.clip .meta span { color: var(--faint); font-size: 12px; }

/* The lead clip, twice the size and alone on its row. */
.clip.feature { max-width: 940px; margin-inline: auto; gap: var(--s4); }
.clip.feature .shot { border-radius: var(--r4); }
.clip.feature .playbtn { width: 74px; height: 74px; margin: -37px 0 0 -37px; }
.clip.feature .playbtn .ic { width: 28px; height: 28px; }
.clip.feature .meta b { font-size: 19px; -webkit-line-clamp: 1; }

/* The placeholder. Same box, same radius, so the page does not resize under
   the reader when the real thing lands. */
.clip.skel { pointer-events: none; }
.clip.skel::before {
  content: ""; display: block; aspect-ratio: 16 / 9;
  border-radius: var(--r3); border: 1px solid var(--hair);
  background: linear-gradient(100deg, var(--surf) 30%, var(--raise) 50%, var(--surf) 70%) 0 0 / 300% 100%;
  animation: uv-skel 1.5s ease-in-out infinite;
}
.clip.feature.skel { width: 100%; }
.clip.feature.skel::before { border-radius: var(--r4); }
@keyframes uv-skel { to { background-position: -150% 0; } }

/* ══ THE TERMS ════════════════════════════════════════════════════════════
 *
 * A contract, laid out like one people will actually read. The whole design
 * question here is legibility under suspicion: somebody arrives at this page
 * either before paying, when they are deciding whether to trust the shop, or
 * after something went wrong, when they are looking for the clause that
 * settles an argument. Both want the same things: a measure they can read
 * without losing their place, numbers they can quote, and no clever layout
 * between them and the sentence.
 *
 * So: one column of prose at a real reading measure, a contents rail that
 * stays put beside it, and nothing decorative inside a clause.
 */

.updated {
  margin: 0 0 var(--s5); color: var(--faint);
  font: 400 11.5px/1 var(--disp); letter-spacing: 0.06em;
}

/* The three hard terms, before the document. Bordered on the left rather than
   all round: a full box reads as an aside somebody may skip, and this is the
   part of the page that must not be skipped. */
.tosnote {
  margin: 0 0 var(--s6); padding: var(--s5) var(--s6);
  border-radius: var(--r3); border: 1px solid var(--hair-2);
  border-left: 2px solid var(--warn);
  background: linear-gradient(100deg, color-mix(in srgb, var(--warn) 7%, transparent), transparent 45%), var(--surf);
}
.tosnote h2 {
  display: flex; align-items: center; gap: var(--s2);
  margin: 0 0 var(--s3); font-size: 15px; font-weight: 600;
}
.tosnote h2 .ic { width: 17px; height: 17px; color: var(--warn); }
.tosnote ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.tosnote li {
  position: relative; padding-left: var(--s4);
  color: var(--mute); font-size: 13.5px; line-height: 1.65;
}
.tosnote li::before {
  content: ""; position: absolute; left: 0; top: 0.62em;
  width: 4px; height: 4px; border-radius: var(--pill); background: var(--warn);
}
.tosnote li b { color: var(--ink); font-weight: 600; }

.doc { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: var(--s8); align-items: start; }

/* The rail follows, because a thirteen clause document is longer than a screen
   and a contents that scrolls away is a contents you have to go back for. */
.toc { position: sticky; top: 88px; display: grid; gap: 1px; }
.toc a {
  display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: var(--s2);
  align-items: baseline; padding: 7px var(--s3);
  border-radius: var(--r1); color: var(--mute); font-size: 13px; text-decoration: none;
  transition: color var(--tint) ease, background-color var(--tint) ease;
}
.toc a u { text-decoration: none; color: var(--faint); font: 500 11px/1 var(--mono); font-variant-numeric: tabular-nums; }
.toc a:hover { color: var(--ink); background: color-mix(in srgb, var(--vio) 7%, transparent); }
.toc a:hover u { color: var(--vio-txt); }
/* Where the reader is. Set by the shell from an observer over the clauses, so
   the rail answers the question the reader has while scrolling: which clause
   am I in. Without a script it is simply absent, and the rail still works. */
.toc a[aria-current="true"] { color: var(--ink); background: color-mix(in srgb, var(--vio) 11%, transparent); }
.toc a[aria-current="true"] u { color: var(--vio-txt); }

.prose { min-width: 0; max-width: 68ch; }
.clause { scroll-margin-top: 96px; padding: 0 0 var(--s6); }
.clause h2 {
  display: flex; align-items: baseline; gap: var(--s3);
  margin: 0 0 var(--s2); font-size: 17px; font-weight: 600;
}
.clause h2 u {
  text-decoration: none; flex: none; color: var(--vio-txt);
  font: 500 12px/1 var(--mono); font-variant-numeric: tabular-nums;
}
.clause p { margin: 0; color: var(--mute); font-size: 14.5px; line-height: 1.75; }
.clause p b { color: var(--ink); font-weight: 600; }
/* Arriving at a clause from the contents. A flash rather than a permanent
   highlight: it answers "did it move" and then gets out of the way, because a
   clause that stays lit is a clause the next reader thinks is special. */
.clause:target h2 u { animation: uv-clause 1.4s var(--out); }
@keyframes uv-clause {
  0%, 40% { color: var(--ok); text-shadow: 0 0 18px var(--ok); }
  100% { color: var(--vio-txt); text-shadow: none; }
}

.govern {
  margin: var(--s5) 0 0; padding-top: var(--s5); border-top: 1px solid var(--hair);
  color: var(--faint); font-size: 12.5px; line-height: 1.7;
}

/* The way out of the document is the free key, not the shop. */
.tosend {
  margin-top: var(--s8); padding: var(--s6); border-radius: var(--r3);
  background: var(--raise); border: 1px solid var(--hair);
  box-shadow: inset 0 1px 0 var(--rim);
}
.tosend b { display: block; font-size: 15px; font-weight: 600; }
.tosend p { margin: 6px 0 var(--s4); color: var(--mute); font-size: 13.5px; line-height: 1.65; }

@media (max-width: 900px) {
  .doc { grid-template-columns: 1fr; gap: var(--s5); }
  /* Not sticky on a phone: a rail pinned to the top of a small screen spends
     the whole document covering the document. */
  .toc {
    position: static; grid-auto-flow: column; grid-auto-columns: max-content;
    gap: var(--s1); overflow-x: auto; padding-bottom: var(--s2);
    scrollbar-width: none;
  }
  .toc::-webkit-scrollbar { display: none; }
  .toc a { grid-template-columns: auto auto; white-space: nowrap; border: 1px solid var(--hair); }
  .prose { max-width: none; }
}

/* ══ THEMES (GENERATED by scripts/build-uv-themes.mjs) ═══════════════════ */

/* Derived from docs/theme-presets.json, the same file the admin console and
   tokens.css are built from. The preview keeps its own neutrals and takes the
   accents and surfaces from the theme; see the script for why. `original` is
   the palette in :root and needs no block, because copy.js applies it by
   REMOVING the attribute rather than by setting one. */

:root[data-theme="valentine"] { /* Valentine */
  --void: #120010;
  --deep: #2a0418;
  --sunk: #0c000a;
  --surf: #1f0e1d;
  --raise: #2a1a28;
  --vio: #e91e63;
  --vio-txt: #ed4780;
  --ion: #ff6090;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(233,30,99,0.3);
  --hair-lit: rgba(233, 30, 99, 0.5);
  --holo: linear-gradient(90deg, #e91e63, #ff6090);
  --btn-fill: linear-gradient(180deg, #c11952, #9a1442);
  --btn-fill-hi: linear-gradient(180deg, #db1c5d, #b4174c);
}
:root[data-theme="summer"] { /* Summer Beach */
  --void: #04111d;
  --deep: #032331;
  --sunk: #030b13;
  --surf: #121e29;
  --raise: #1d2934;
  --vio: #00b4d8;
  --vio-txt: #00b4d8;
  --ion: #ffd54f;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(0,180,216,0.3);
  --hair-lit: rgba(0, 180, 216, 0.5);
  --holo: linear-gradient(90deg, #00b4d8, #ffd54f);
  --btn-fill: linear-gradient(180deg, #006f85, #00596a);
  --btn-fill-hi: linear-gradient(180deg, #007e97, #00677c);
}
:root[data-theme="cyberpunk"] { /* Neon Cyberpunk */
  --void: #0a0a0f;
  --deep: #270d1b;
  --sunk: #07070a;
  --surf: #17171c;
  --raise: #232327;
  --vio: #ff2d7b;
  --vio-txt: #ff347f;
  --ion: #00fff0;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(255,45,123,0.3);
  --hair-lit: rgba(255, 45, 123, 0.5);
  --holo: linear-gradient(90deg, #ff2d7b, #00fff0);
  --btn-fill: linear-gradient(180deg, #bc215b, #971b49);
  --btn-fill-hi: linear-gradient(180deg, #d62667, #b01f55);
}
:root[data-theme="aurora"] { /* Aurora Borealis */
  --void: #02080d;
  --deep: #022021;
  --sunk: #010508;
  --surf: #10161a;
  --raise: #1b2125;
  --vio: #00d9c0;
  --vio-txt: #00d9c0;
  --ion: #946eff;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(0,217,192,0.3);
  --hair-lit: rgba(0, 217, 192, 0.5);
  --holo: linear-gradient(90deg, #00d9c0, #946eff);
  --btn-fill: linear-gradient(180deg, #007365, #005c51);
  --btn-fill-hi: linear-gradient(180deg, #008273, #006b5e);
}
:root[data-theme="sakura"] { /* Cherry Blossom */
  --void: #1a0f1a;
  --deep: #2c1d27;
  --sunk: #110a11;
  --surf: #271c27;
  --raise: #312731;
  --vio: #ffb7c5;
  --vio-txt: #ffb7c5;
  --ion: #ff8fab;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(255,183,197,0.3);
  --hair-lit: rgba(255, 183, 197, 0.5);
  --holo: linear-gradient(90deg, #ffb7c5, #ff8fab);
  --btn-fill: linear-gradient(180deg, #7e5a61, #65484e);
  --btn-fill-hi: linear-gradient(180deg, #8f666e, #75545a);
}
:root[data-theme="halloween"] { /* Halloween */
  --void: #0a0500;
  --deep: #271203;
  --sunk: #070300;
  --surf: #17130e;
  --raise: #231e1a;
  --vio: #ff7518;
  --vio-txt: #ff7518;
  --ion: #a264ff;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(255,117,24,0.3);
  --hair-lit: rgba(255, 117, 24, 0.5);
  --holo: linear-gradient(90deg, #ff7518, #a264ff);
  --btn-fill: linear-gradient(180deg, #a24a0f, #813b0c);
  --btn-fill-hi: linear-gradient(180deg, #b85411, #97450e);
}
:root[data-theme="christmas"] { /* Christmas */
  --void: #0a0a0a;
  --deep: #200a0d;
  --sunk: #070707;
  --surf: #171717;
  --raise: #232323;
  --vio: #c8102e;
  --vio-txt: #db6477;
  --ion: #46996b;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(200,16,46,0.3);
  --hair-lit: rgba(200, 16, 46, 0.5);
  --holo: linear-gradient(90deg, #c8102e, #46996b);
  --btn-fill: linear-gradient(180deg, #b00e28, #8d0b20);
  --btn-fill-hi: linear-gradient(180deg, #c8102e, #a40d26);
}
:root[data-theme="lunar"] { /* Lunar New Year */
  --void: #0a0000;
  --deep: #270006;
  --sunk: #070000;
  --surf: #170e0e;
  --raise: #231a1a;
  --vio: #ff0033;
  --vio-txt: #ff224e;
  --ion: #ffd700;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(255,0,51,0.3);
  --hair-lit: rgba(255, 0, 51, 0.5);
  --holo: linear-gradient(90deg, #ff0033, #ffd700);
  --btn-fill: linear-gradient(180deg, #ca0028, #a20020);
  --btn-fill-hi: linear-gradient(180deg, #e5002e, #bc0026);
}
:root[data-theme="galaxy"] { /* Galaxy Cosmos */
  --void: #000a14;
  --deep: #0f0d2d;
  --sunk: #00070d;
  --surf: #0e1721;
  --raise: #1a232c;
  --vio: #7b2ff7;
  --vio-txt: #a36df9;
  --ion: #38bdf8;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(123,47,247,0.3);
  --hair-lit: rgba(123, 47, 247, 0.5);
  --holo: linear-gradient(90deg, #7b2ff7, #38bdf8);
  --btn-fill: linear-gradient(180deg, #6c29d9, #5721ae);
  --btn-fill-hi: linear-gradient(180deg, #7b2ff7, #6527cb);
}
:root[data-theme="forest"] { /* Midnight Forest */
  --void: #06140b;
  --deep: #102215;
  --sunk: #040d07;
  --surf: #142118;
  --raise: #1f2c23;
  --vio: #5d9b6a;
  --vio-txt: #609d6c;
  --ion: #a3e635;
  --ok: #34d399;
  --warn: #fbbf24;
  --bloom: rgba(93,155,106,0.3);
  --hair-lit: rgba(93, 155, 106, 0.5);
  --holo: linear-gradient(90deg, #5d9b6a, #a3e635);
  --btn-fill: linear-gradient(180deg, #416d4b, #34573c);
  --btn-fill-hi: linear-gradient(180deg, #4a7c55, #3d6646);
}

/* ══ END GENERATED THEMES ════════════════════════════════════════════════ */

/* ══ FX TILES (GENERATED by scripts/build-theme-fx.mjs) ══════════════════ */

/* Eighteen tiles, one custom property each, drawn by
   scripts/build-theme-fx.mjs from a seeded jittered grid. Do not edit by hand:
   the placement is what stops a repeated tile from looking repeated, and the
   `-h` beside each one is the tile height the signature translates by to make
   the loop seamless. Change one and you must change the other. */

:root {
  --fx-stars-far-h: 420px;
  --fx-stars-far: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='420'%20height='420'%20fill='%23ffffff'%3E%3Ccircle%20cx='25'%20cy='35'%20r='0.7'%20opacity='0.5'/%3E%3Ccircle%20cx='48'%20cy='46'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='97'%20cy='40'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='179'%20cy='34'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='211'%20cy='24'%20r='0.9'%20opacity='0.7'/%3E%3Ccircle%20cx='277'%20cy='4'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='321'%20cy='42'%20r='0.8'%20opacity='0.4'/%3E%3Ccircle%20cx='361'%20cy='10'%20r='0.6'%20opacity='0.6'/%3E%3Ccircle%20cx='383'%20cy='33'%20r='0.6'%20opacity='0.4'/%3E%3Ccircle%20cx='32'%20cy='49'%20r='0.6'%20opacity='0.7'/%3E%3Ccircle%20cx='86'%20cy='64'%20r='0.9'%20opacity='0.6'/%3E%3Ccircle%20cx='117'%20cy='55'%20r='0.9'%20opacity='0.4'/%3E%3Ccircle%20cx='168'%20cy='55'%20r='0.6'%20opacity='0.5'/%3E%3Ccircle%20cx='221'%20cy='55'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='237'%20cy='82'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='283'%20cy='81'%20r='0.7'%20opacity='0.8'/%3E%3Ccircle%20cx='329'%20cy='78'%20r='0.9'%20opacity='0.6'/%3E%3Ccircle%20cx='415'%20cy='62'%20r='0.8'%20opacity='0.8'/%3E%3Ccircle%20cx='16'%20cy='122'%20r='0.8'%20opacity='0.8'/%3E%3Ccircle%20cx='89'%20cy='131'%20r='0.8'%20opacity='0.8'/%3E%3Ccircle%20cx='116'%20cy='94'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='141'%20cy='139'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='204'%20cy='122'%20r='0.6'%20opacity='0.6'/%3E%3Ccircle%20cx='267'%20cy='117'%20r='0.9'%20opacity='0.7'/%3E%3Ccircle%20cx='287'%20cy='98'%20r='0.6'%20opacity='0.6'/%3E%3Ccircle%20cx='343'%20cy='112'%20r='0.8'%20opacity='0.8'/%3E%3Ccircle%20cx='386'%20cy='134'%20r='0.9'%20opacity='0.5'/%3E%3Ccircle%20cx='18'%20cy='178'%20r='0.6'%20opacity='0.7'/%3E%3Ccircle%20cx='72'%20cy='143'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='114'%20cy='185'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='158'%20cy='163'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='229'%20cy='144'%20r='0.9'%20opacity='0.7'/%3E%3Ccircle%20cx='240'%20cy='154'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='300'%20cy='156'%20r='0.9'%20opacity='0.4'/%3E%3Ccircle%20cx='367'%20cy='140'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='388'%20cy='177'%20r='0.7'%20opacity='0.6'/%3E%3Ccircle%20cx='41'%20cy='193'%20r='0.6'%20opacity='0.6'/%3E%3Ccircle%20cx='65'%20cy='219'%20r='0.6'%20opacity='0.7'/%3E%3Ccircle%20cx='138'%20cy='190'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='141'%20cy='229'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='201'%20cy='187'%20r='0.7'%20opacity='0.8'/%3E%3Ccircle%20cx='273'%20cy='196'%20r='0.7'%20opacity='0.8'/%3E%3Ccircle%20cx='295'%20cy='191'%20r='0.8'%20opacity='0.6'/%3E%3Ccircle%20cx='352'%20cy='194'%20r='0.6'%20opacity='0.4'/%3E%3Ccircle%20cx='381'%20cy='231'%20r='0.9'%20opacity='0.6'/%3E%3Ccircle%20cx='12'%20cy='271'%20r='0.7'%20opacity='0.5'/%3E%3Ccircle%20cx='72'%20cy='277'%20r='0.9'%20opacity='0.6'/%3E%3Ccircle%20cx='96'%20cy='251'%20r='0.6'%20opacity='0.4'/%3E%3Ccircle%20cx='151'%20cy='274'%20r='0.8'%20opacity='0.8'/%3E%3Ccircle%20cx='212'%20cy='275'%20r='0.6'%20opacity='0.5'/%3E%3Ccircle%20cx='262'%20cy='276'%20r='1.0'%20opacity='0.8'/%3E%3Ccircle%20cx='287'%20cy='251'%20r='0.9'%20opacity='0.7'/%3E%3Ccircle%20cx='328'%20cy='271'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='408'%20cy='269'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='22'%20cy='292'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='63'%20cy='295'%20r='0.9'%20opacity='0.7'/%3E%3Ccircle%20cx='94'%20cy='305'%20r='0.6'%20opacity='0.5'/%3E%3Ccircle%20cx='167'%20cy='291'%20r='0.7'%20opacity='0.7'/%3E%3Ccircle%20cx='220'%20cy='301'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='262'%20cy='313'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='304'%20cy='307'%20r='0.8'%20opacity='0.8'/%3E%3Ccircle%20cx='369'%20cy='289'%20r='0.6'%20opacity='0.5'/%3E%3Ccircle%20cx='414'%20cy='308'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='37'%20cy='364'%20r='1.0'%20opacity='0.4'/%3E%3Ccircle%20cx='52'%20cy='335'%20r='0.6'%20opacity='0.7'/%3E%3Ccircle%20cx='108'%20cy='329'%20r='0.6'%20opacity='0.8'/%3E%3Ccircle%20cx='163'%20cy='348'%20r='0.7'%20opacity='0.8'/%3E%3Ccircle%20cx='230'%20cy='341'%20r='0.7'%20opacity='0.6'/%3E%3Ccircle%20cx='254'%20cy='359'%20r='0.7'%20opacity='0.7'/%3E%3Ccircle%20cx='321'%20cy='357'%20r='0.6'%20opacity='0.5'/%3E%3Ccircle%20cx='334'%20cy='335'%20r='1.0'%20opacity='0.8'/%3E%3Ccircle%20cx='399'%20cy='345'%20r='1.0'%20opacity='0.4'/%3E%3Ccircle%20cx='3'%20cy='411'%20r='0.7'%20opacity='0.7'/%3E%3Ccircle%20cx='58'%20cy='379'%20r='1.0'%20opacity='0.5'/%3E%3Ccircle%20cx='104'%20cy='418'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='180'%20cy='384'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='196'%20cy='389'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='270'%20cy='377'%20r='0.7'%20opacity='0.8'/%3E%3Ccircle%20cx='308'%20cy='374'%20r='0.7'%20opacity='0.5'/%3E%3Ccircle%20cx='350'%20cy='413'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='391'%20cy='374'%20r='0.8'%20opacity='0.4'/%3E%3C/svg%3E");
  --fx-stars-mid-h: 380px;
  --fx-stars-mid: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='380'%20height='380'%20fill='%23dfe8ff'%3E%3Ccircle%20cx='17'%20cy='49'%20r='1.4'%20opacity='0.6'/%3E%3Ccircle%20cx='100'%20cy='59'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='161'%20cy='15'%20r='1.1'%20opacity='0.6'/%3E%3Ccircle%20cx='237'%20cy='51'%20r='1.0'%20opacity='1.0'/%3E%3Ccircle%20cx='308'%20cy='18'%20r='1.4'%20opacity='0.5'/%3E%3Ccircle%20cx='367'%20cy='36'%20r='1.2'%20opacity='1.0'/%3E%3Ccircle%20cx='2'%20cy='126'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='68'%20cy='125'%20r='1.2'%20opacity='1.0'/%3E%3Ccircle%20cx='171'%20cy='111'%20r='1.3'%20opacity='0.9'/%3E%3Ccircle%20cx='204'%20cy='76'%20r='1.4'%20opacity='0.7'/%3E%3Ccircle%20cx='280'%20cy='105'%20r='1.1'%20opacity='1.0'/%3E%3Ccircle%20cx='334'%20cy='107'%20r='1.1'%20opacity='0.9'/%3E%3Ccircle%20cx='54'%20cy='148'%20r='1.1'%20opacity='0.7'/%3E%3Ccircle%20cx='100'%20cy='137'%20r='1.3'%20opacity='0.9'/%3E%3Ccircle%20cx='172'%20cy='176'%20r='1.3'%20opacity='0.6'/%3E%3Ccircle%20cx='249'%20cy='178'%20r='1.4'%20opacity='0.8'/%3E%3Ccircle%20cx='259'%20cy='176'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='368'%20cy='174'%20r='1.2'%20opacity='1.0'/%3E%3Ccircle%20cx='37'%20cy='191'%20r='1.3'%20opacity='0.7'/%3E%3Ccircle%20cx='75'%20cy='223'%20r='1.3'%20opacity='0.9'/%3E%3Ccircle%20cx='135'%20cy='229'%20r='1.4'%20opacity='0.9'/%3E%3Ccircle%20cx='229'%20cy='197'%20r='1.1'%20opacity='0.5'/%3E%3Ccircle%20cx='291'%20cy='209'%20r='1.5'%20opacity='0.6'/%3E%3Ccircle%20cx='337'%20cy='234'%20r='1.2'%20opacity='0.8'/%3E%3Ccircle%20cx='21'%20cy='307'%20r='1.1'%20opacity='1.0'/%3E%3Ccircle%20cx='126'%20cy='311'%20r='1.5'%20opacity='0.7'/%3E%3Ccircle%20cx='177'%20cy='277'%20r='1.3'%20opacity='0.6'/%3E%3Ccircle%20cx='223'%20cy='254'%20r='1.4'%20opacity='0.7'/%3E%3Ccircle%20cx='289'%20cy='270'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='326'%20cy='263'%20r='1.2'%20opacity='1.0'/%3E%3Ccircle%20cx='39'%20cy='362'%20r='1.0'%20opacity='0.5'/%3E%3Ccircle%20cx='113'%20cy='368'%20r='1.2'%20opacity='0.9'/%3E%3Ccircle%20cx='140'%20cy='365'%20r='1.4'%20opacity='0.5'/%3E%3Ccircle%20cx='242'%20cy='367'%20r='1.4'%20opacity='0.5'/%3E%3Ccircle%20cx='278'%20cy='363'%20r='1.1'%20opacity='0.8'/%3E%3Ccircle%20cx='351'%20cy='341'%20r='1.2'%20opacity='0.6'/%3E%3C/svg%3E");
  --fx-stars-near-h: 340px;
  --fx-stars-near: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='340'%20height='340'%20fill='%23ffffff'%3E%3Ccircle%20cx='15'%20cy='10'%20r='1.7'%20opacity='0.7'/%3E%3Ccircle%20cx='150'%20cy='22'%20r='1.5'%20opacity='0.6'/%3E%3Ccircle%20cx='192'%20cy='82'%20r='2.0'%20opacity='0.6'/%3E%3Ccircle%20cx='268'%20cy='46'%20r='1.6'%20opacity='0.8'/%3E%3Ccircle%20cx='45'%20cy='129'%20r='1.5'%20opacity='1.0'/%3E%3Ccircle%20cx='155'%20cy='156'%20r='2.0'%20opacity='0.8'/%3E%3Ccircle%20cx='244'%20cy='105'%20r='2.0'%20opacity='0.8'/%3E%3Ccircle%20cx='328'%20cy='128'%20r='1.5'%20opacity='1.0'/%3E%3Ccircle%20cx='69'%20cy='221'%20r='1.5'%20opacity='0.9'/%3E%3Ccircle%20cx='105'%20cy='222'%20r='1.8'%20opacity='1.0'/%3E%3Ccircle%20cx='244'%20cy='253'%20r='1.9'%20opacity='1.0'/%3E%3Ccircle%20cx='332'%20cy='212'%20r='1.5'%20opacity='0.9'/%3E%3Ccircle%20cx='75'%20cy='328'%20r='1.5'%20opacity='0.7'/%3E%3Ccircle%20cx='132'%20cy='283'%20r='1.6'%20opacity='0.7'/%3E%3Ccircle%20cx='218'%20cy='302'%20r='1.6'%20opacity='0.8'/%3E%3Ccircle%20cx='311'%20cy='274'%20r='1.6'%20opacity='0.8'/%3E%3C/svg%3E");
  --fx-snow-far-h: 300px;
  --fx-snow-far: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='300'%20height='300'%20fill='%23ffffff'%3E%3Ccircle%20cx='19'%20cy='22'%20r='1.1'%20opacity='0.6'/%3E%3Ccircle%20cx='73'%20cy='32'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='104'%20cy='34'%20r='1.1'%20opacity='0.4'/%3E%3Ccircle%20cx='137'%20cy='43'%20r='1.1'%20opacity='0.7'/%3E%3Ccircle%20cx='179'%20cy='7'%20r='1.2'%20opacity='0.4'/%3E%3Ccircle%20cx='253'%20cy='7'%20r='0.9'%20opacity='0.7'/%3E%3Ccircle%20cx='277'%20cy='19'%20r='0.9'%20opacity='0.4'/%3E%3Ccircle%20cx='20'%20cy='78'%20r='1.2'%20opacity='0.7'/%3E%3Ccircle%20cx='65'%20cy='71'%20r='0.9'%20opacity='0.5'/%3E%3Ccircle%20cx='106'%20cy='50'%20r='1.1'%20opacity='0.5'/%3E%3Ccircle%20cx='167'%20cy='61'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='180'%20cy='48'%20r='1.1'%20opacity='0.4'/%3E%3Ccircle%20cx='219'%20cy='63'%20r='1.2'%20opacity='0.7'/%3E%3Ccircle%20cx='283'%20cy='52'%20r='0.9'%20opacity='0.6'/%3E%3Ccircle%20cx='26'%20cy='90'%20r='0.9'%20opacity='0.5'/%3E%3Ccircle%20cx='60'%20cy='96'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='117'%20cy='117'%20r='1.1'%20opacity='0.6'/%3E%3Ccircle%20cx='136'%20cy='113'%20r='1.1'%20opacity='0.6'/%3E%3Ccircle%20cx='196'%20cy='89'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='241'%20cy='111'%20r='0.7'%20opacity='0.7'/%3E%3Ccircle%20cx='299'%20cy='109'%20r='1.1'%20opacity='0.8'/%3E%3Ccircle%20cx='41'%20cy='164'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='62'%20cy='159'%20r='1.0'%20opacity='0.4'/%3E%3Ccircle%20cx='91'%20cy='140'%20r='1.2'%20opacity='0.4'/%3E%3Ccircle%20cx='134'%20cy='132'%20r='0.8'%20opacity='0.4'/%3E%3Ccircle%20cx='209'%20cy='163'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='241'%20cy='141'%20r='0.7'%20opacity='0.7'/%3E%3Ccircle%20cx='277'%20cy='142'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='20'%20cy='194'%20r='0.8'%20opacity='0.4'/%3E%3Ccircle%20cx='51'%20cy='213'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='126'%20cy='206'%20r='0.7'%20opacity='0.5'/%3E%3Ccircle%20cx='158'%20cy='194'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='202'%20cy='212'%20r='1.2'%20opacity='0.8'/%3E%3Ccircle%20cx='236'%20cy='174'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='273'%20cy='214'%20r='1.0'%20opacity='0.5'/%3E%3Ccircle%20cx='22'%20cy='252'%20r='1.1'%20opacity='0.4'/%3E%3Ccircle%20cx='61'%20cy='244'%20r='1.1'%20opacity='0.7'/%3E%3Ccircle%20cx='112'%20cy='225'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='164'%20cy='217'%20r='0.9'%20opacity='0.4'/%3E%3Ccircle%20cx='200'%20cy='255'%20r='1.2'%20opacity='0.5'/%3E%3Ccircle%20cx='219'%20cy='253'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='297'%20cy='256'%20r='1.1'%20opacity='0.5'/%3E%3Ccircle%20cx='7'%20cy='269'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='57'%20cy='274'%20r='0.7'%20opacity='0.7'/%3E%3Ccircle%20cx='114'%20cy='291'%20r='0.9'%20opacity='0.5'/%3E%3Ccircle%20cx='135'%20cy='286'%20r='1.1'%20opacity='0.7'/%3E%3Ccircle%20cx='198'%20cy='294'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='250'%20cy='292'%20r='1.2'%20opacity='0.8'/%3E%3Ccircle%20cx='273'%20cy='277'%20r='0.7'%20opacity='0.6'/%3E%3C/svg%3E");
  --fx-snow-near-h: 260px;
  --fx-snow-near: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='260'%20height='260'%20fill='%23ffffff'%3E%3Ccircle%20cx='6'%20cy='28'%20r='1.5'%20opacity='0.7'/%3E%3Ccircle%20cx='109'%20cy='1'%20r='1.5'%20opacity='0.9'/%3E%3Ccircle%20cx='165'%20cy='48'%20r='2.2'%20opacity='0.6'/%3E%3Ccircle%20cx='242'%20cy='48'%20r='2.3'%20opacity='0.6'/%3E%3Ccircle%20cx='10'%20cy='112'%20r='1.5'%20opacity='0.7'/%3E%3Ccircle%20cx='78'%20cy='72'%20r='1.8'%20opacity='0.9'/%3E%3Ccircle%20cx='161'%20cy='92'%20r='1.8'%20opacity='1.0'/%3E%3Ccircle%20cx='209'%20cy='82'%20r='2.3'%20opacity='1.0'/%3E%3Ccircle%20cx='46'%20cy='167'%20r='2.0'%20opacity='0.7'/%3E%3Ccircle%20cx='118'%20cy='192'%20r='2.0'%20opacity='0.7'/%3E%3Ccircle%20cx='145'%20cy='140'%20r='2.2'%20opacity='0.8'/%3E%3Ccircle%20cx='240'%20cy='159'%20r='2.1'%20opacity='0.6'/%3E%3Ccircle%20cx='51'%20cy='224'%20r='1.7'%20opacity='0.8'/%3E%3Ccircle%20cx='121'%20cy='206'%20r='2.2'%20opacity='0.9'/%3E%3Ccircle%20cx='178'%20cy='207'%20r='1.5'%20opacity='0.6'/%3E%3Ccircle%20cx='249'%20cy='215'%20r='2.1'%20opacity='1.0'/%3E%3C/svg%3E");
  --fx-snow-flakes-h: 440px;
  --fx-snow-flakes: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='440'%20height='440'%20fill='none'%20stroke='%23ffffff'%20stroke-width='1.1'%20stroke-linecap='round'%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.6'%20transform='translate(57%20139)%20rotate(54)%20scale(0.97)%20translate(-7%20-7)'/%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.7'%20transform='translate(193%2019)%20rotate(75)%20scale(0.69)%20translate(-7%20-7)'/%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.6'%20transform='translate(389%20133)%20rotate(62)%20scale(0.58)%20translate(-7%20-7)'/%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.4'%20transform='translate(16%20290)%20rotate(25)%20scale(0.76)%20translate(-7%20-7)'/%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.5'%20transform='translate(259%20152)%20rotate(35)%20scale(0.98)%20translate(-7%20-7)'/%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.4'%20transform='translate(321%20239)%20rotate(21)%20scale(0.93)%20translate(-7%20-7)'/%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.6'%20transform='translate(71%20424)%20rotate(64)%20scale(0.64)%20translate(-7%20-7)'/%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.6'%20transform='translate(156%20417)%20rotate(63)%20scale(0.94)%20translate(-7%20-7)'/%3E%3Cpath%20d='M7%200.6v12.8M0.6%207h12.8M2.5%202.5l9%209M11.5%202.5l-9%209M7%203.1%205.4%204.5M7%203.1%208.6%204.5M7%2010.9l-1.6-1.4M7%2010.9l1.6-1.4'%20opacity='0.3'%20transform='translate(342%20299)%20rotate(1)%20scale(0.85)%20translate(-7%20-7)'/%3E%3C/svg%3E");
  --fx-petal-far-h: 660px;
  --fx-petal-far: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='660'%20height='660'%20fill='%23ffd7e2'%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.0'%20ry='1.3'%20opacity='0.5'%20transform='translate(32%20107)%20rotate(53)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.6'%20ry='1.8'%20opacity='0.7'%20transform='translate(166%20112)%20rotate(116)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.6'%20ry='1.8'%20opacity='0.6'%20transform='translate(420%2080)%20rotate(116)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='2.8'%20ry='1.4'%20opacity='0.4'%20transform='translate(555%20145)%20rotate(150)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.9'%20ry='2.0'%20opacity='0.7'%20transform='translate(94%20247)%20rotate(138)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.9'%20ry='2.2'%20opacity='0.7'%20transform='translate(296%20317)%20rotate(253)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='2.8'%20ry='1.7'%20opacity='0.5'%20transform='translate(345%20272)%20rotate(270)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='4.4'%20ry='2.5'%20opacity='0.8'%20transform='translate(579%20329)%20rotate(239)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='2.9'%20ry='1.4'%20opacity='0.5'%20transform='translate(126%20377)%20rotate(118)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.7'%20ry='2.2'%20opacity='0.7'%20transform='translate(170%20441)%20rotate(251)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.5'%20ry='2.2'%20opacity='0.6'%20transform='translate(331%20402)%20rotate(329)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='4.2'%20ry='1.9'%20opacity='0.8'%20transform='translate(657%20384)%20rotate(53)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='4.1'%20ry='2.6'%20opacity='0.8'%20transform='translate(17%20498)%20rotate(357)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.5'%20ry='1.9'%20opacity='0.6'%20transform='translate(301%20586)%20rotate(181)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.5'%20ry='2.2'%20opacity='0.6'%20transform='translate(396%20546)%20rotate(344)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='3.5'%20ry='1.6'%20opacity='0.6'%20transform='translate(620%20528)%20rotate(52)'/%3E%3C/svg%3E");
  --fx-petal-near-h: 720px;
  --fx-petal-near: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='720'%20height='720'%20fill='%23ffc2d4'%3E%3Cellipse%20cx='0'%20cy='0'%20rx='7.4'%20ry='3.9'%20opacity='1.0'%20transform='translate(127%20197)%20rotate(171)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='5.5'%20ry='3.4'%20opacity='0.7'%20transform='translate(391%2061)%20rotate(321)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='6.6'%20ry='3.8'%20opacity='0.9'%20transform='translate(527%20136)%20rotate(249)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='6.9'%20ry='3.5'%20opacity='0.9'%20transform='translate(73%20538)%20rotate(134)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='4.8'%20ry='2.9'%20opacity='0.5'%20transform='translate(293%20629)%20rotate(303)'/%3E%3Cellipse%20cx='0'%20cy='0'%20rx='6.4'%20ry='2.8'%20opacity='0.8'%20transform='translate(534%20369)%20rotate(30)'/%3E%3C/svg%3E");
  --fx-heart-far-h: 780px;
  --fx-heart-far: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='780'%20height='780'%20fill='%23ff88ad'%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.5'%20transform='translate(85%20122)%20rotate(-1)%20scale(0.74)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.4'%20transform='translate(474%20143)%20rotate(-11)%20scale(0.75)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.4'%20transform='translate(540%20247)%20rotate(-15)%20scale(0.75)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.5'%20transform='translate(14%20299)%20rotate(5)%20scale(0.73)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.6'%20transform='translate(364%20485)%20rotate(12)%20scale(0.78)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.4'%20transform='translate(754%20291)%20rotate(-11)%20scale(0.63)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.6'%20transform='translate(72%20740)%20rotate(11)%20scale(0.67)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.6'%20transform='translate(422%20689)%20rotate(9)%20scale(0.50)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.4'%20transform='translate(659%20630)%20rotate(-7)%20scale(0.62)%20translate(-8%20-8)'/%3E%3C/svg%3E");
  --fx-heart-near-h: 900px;
  --fx-heart-near: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='900'%20height='900'%20fill='%23ff6f9c'%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.5'%20transform='translate(259%2033)%20rotate(-10)%20scale(1.32)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.6'%20transform='translate(649%2036)%20rotate(0)%20scale(1.28)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.5'%20transform='translate(287%20786)%20rotate(-12)%20scale(0.98)%20translate(-8%20-8)'/%3E%3Cpath%20d='M8%2014.6C3.1%2011%20.9%208.6.9%206.1.9%203.9%202.7%202.2%204.9%202.2c1.3%200%202.4.6%203.1%201.6.7-1%201.8-1.6%203.1-1.6%202.2%200%204%201.7%204%203.9%200%202.5-2.2%204.9-7.1%208.5z'%20opacity='0.8'%20transform='translate(760%20665)%20rotate(21)%20scale(1.14)%20translate(-8%20-8)'/%3E%3C/svg%3E");
  --fx-ember-far-h: 320px;
  --fx-ember-far: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='320'%20height='320'%20fill='%23ffd76a'%3E%3Ccircle%20cx='4'%20cy='44'%20r='0.7'%20opacity='0.8'/%3E%3Ccircle%20cx='63'%20cy='45'%20r='0.9'%20opacity='0.4'/%3E%3Ccircle%20cx='158'%20cy='25'%20r='1.0'%20opacity='0.8'/%3E%3Ccircle%20cx='171'%20cy='22'%20r='0.8'%20opacity='0.5'/%3E%3Ccircle%20cx='228'%20cy='23'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='288'%20cy='53'%20r='1.2'%20opacity='0.7'/%3E%3Ccircle%20cx='10'%20cy='95'%20r='1.1'%20opacity='0.4'/%3E%3Ccircle%20cx='65'%20cy='95'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='122'%20cy='79'%20r='0.8'%20opacity='0.6'/%3E%3Ccircle%20cx='208'%20cy='99'%20r='1.0'%20opacity='0.5'/%3E%3Ccircle%20cx='259'%20cy='88'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='273'%20cy='84'%20r='1.1'%20opacity='0.4'/%3E%3Ccircle%20cx='27'%20cy='134'%20r='0.8'%20opacity='0.4'/%3E%3Ccircle%20cx='92'%20cy='107'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='153'%20cy='113'%20r='1.1'%20opacity='0.8'/%3E%3Ccircle%20cx='167'%20cy='144'%20r='1.1'%20opacity='0.7'/%3E%3Ccircle%20cx='259'%20cy='116'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='291'%20cy='137'%20r='1.0'%20opacity='0.4'/%3E%3Ccircle%20cx='47'%20cy='164'%20r='0.8'%20opacity='0.8'/%3E%3Ccircle%20cx='60'%20cy='209'%20r='1.0'%20opacity='0.4'/%3E%3Ccircle%20cx='117'%20cy='179'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='195'%20cy='165'%20r='1.2'%20opacity='0.7'/%3E%3Ccircle%20cx='265'%20cy='172'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='294'%20cy='168'%20r='0.8'%20opacity='0.6'/%3E%3Ccircle%20cx='46'%20cy='257'%20r='0.8'%20opacity='0.4'/%3E%3Ccircle%20cx='95'%20cy='238'%20r='1.1'%20opacity='0.6'/%3E%3Ccircle%20cx='148'%20cy='232'%20r='1.2'%20opacity='0.4'/%3E%3Ccircle%20cx='173'%20cy='215'%20r='1.0'%20opacity='0.4'/%3E%3Ccircle%20cx='266'%20cy='255'%20r='0.9'%20opacity='0.5'/%3E%3Ccircle%20cx='297'%20cy='222'%20r='0.9'%20opacity='0.8'/%3E%3Ccircle%20cx='26'%20cy='289'%20r='0.9'%20opacity='0.4'/%3E%3Ccircle%20cx='69'%20cy='312'%20r='0.8'%20opacity='0.7'/%3E%3Ccircle%20cx='126'%20cy='316'%20r='0.8'%20opacity='0.8'/%3E%3Ccircle%20cx='184'%20cy='306'%20r='0.7'%20opacity='0.4'/%3E%3Ccircle%20cx='237'%20cy='297'%20r='1.1'%20opacity='0.5'/%3E%3Ccircle%20cx='287'%20cy='273'%20r='1.1'%20opacity='0.8'/%3E%3C/svg%3E");
  --fx-ember-near-h: 280px;
  --fx-ember-near: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='280'%20height='280'%20fill='%23ffbf3c'%3E%3Ccircle%20cx='20'%20cy='2'%20r='1.8'%20opacity='0.6'/%3E%3Ccircle%20cx='113'%20cy='62'%20r='2.1'%20opacity='0.7'/%3E%3Ccircle%20cx='149'%20cy='47'%20r='1.9'%20opacity='0.7'/%3E%3Ccircle%20cx='236'%20cy='58'%20r='1.7'%20opacity='0.6'/%3E%3Ccircle%20cx='16'%20cy='105'%20r='1.9'%20opacity='0.9'/%3E%3Ccircle%20cx='124'%20cy='135'%20r='1.5'%20opacity='0.7'/%3E%3Ccircle%20cx='184'%20cy='83'%20r='2.1'%20opacity='0.6'/%3E%3Ccircle%20cx='261'%20cy='125'%20r='1.6'%20opacity='0.7'/%3E%3Ccircle%20cx='25'%20cy='206'%20r='1.7'%20opacity='0.5'/%3E%3Ccircle%20cx='97'%20cy='161'%20r='1.4'%20opacity='0.9'/%3E%3Ccircle%20cx='169'%20cy='169'%20r='1.4'%20opacity='0.8'/%3E%3Ccircle%20cx='268'%20cy='142'%20r='1.9'%20opacity='1.0'/%3E%3Ccircle%20cx='8'%20cy='278'%20r='1.9'%20opacity='0.7'/%3E%3Ccircle%20cx='101'%20cy='210'%20r='1.7'%20opacity='0.8'/%3E%3Ccircle%20cx='149'%20cy='244'%20r='1.5'%20opacity='0.9'/%3E%3Ccircle%20cx='273'%20cy='241'%20r='2.1'%20opacity='0.8'/%3E%3C/svg%3E");
  --fx-lantern-row-h: 260px;
  --fx-lantern-row: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='1800'%20height='260'%20%3E%3Cg%20opacity='0.72'%20transform='translate(93.2%2035.2)%20scale(1.62)'%3E%3Crect%20x='-0.5'%20y='-21.8'%20width='1'%20height='21.8'%20fill='%23ffd45e'%20opacity='0.4'/%3E%3Crect%20x='-7'%20y='0'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='rgba(255,%2092,%2062,%200.42)'/%3E%3Cellipse%20cx='-4.6'%20cy='12'%20rx='6.4'%20ry='8'%20fill='%23ffd45e'%20opacity='0.16'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1.4'/%3E%3Cpath%20d='M-8%204.6C-11%208-11%2022-8%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Cpath%20d='M0%202.4V27.6'%20stroke='%23ffd45e'%20stroke-width='0.9'%20opacity='0.5'/%3E%3Cpath%20d='M8%204.6C11%208%2011%2022%208%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Crect%20x='-7'%20y='27'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cpath%20d='M-2.6%2030v9M2.6%2030v9'%20stroke='%23ffd45e'%20stroke-width='1.2'%20stroke-linecap='round'/%3E%3C/g%3E%3Cg%20opacity='0.71'%20transform='translate(441.6%2036.0)%20scale(2.03)'%3E%3Crect%20x='-0.5'%20y='-17.7'%20width='1'%20height='17.7'%20fill='%23ffd45e'%20opacity='0.4'/%3E%3Crect%20x='-7'%20y='0'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='rgba(255,%2092,%2062,%200.42)'/%3E%3Cellipse%20cx='-4.6'%20cy='12'%20rx='6.4'%20ry='8'%20fill='%23ffd45e'%20opacity='0.16'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1.4'/%3E%3Cpath%20d='M-8%204.6C-11%208-11%2022-8%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Cpath%20d='M0%202.4V27.6'%20stroke='%23ffd45e'%20stroke-width='0.9'%20opacity='0.5'/%3E%3Cpath%20d='M8%204.6C11%208%2011%2022%208%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Crect%20x='-7'%20y='27'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cpath%20d='M-2.6%2030v9M2.6%2030v9'%20stroke='%23ffd45e'%20stroke-width='1.2'%20stroke-linecap='round'/%3E%3C/g%3E%3Cg%20opacity='0.62'%20transform='translate(594.4%2029.5)%20scale(1.65)'%3E%3Crect%20x='-0.5'%20y='-17.9'%20width='1'%20height='17.9'%20fill='%23ffd45e'%20opacity='0.4'/%3E%3Crect%20x='-7'%20y='0'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='rgba(255,%2092,%2062,%200.42)'/%3E%3Cellipse%20cx='-4.6'%20cy='12'%20rx='6.4'%20ry='8'%20fill='%23ffd45e'%20opacity='0.16'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1.4'/%3E%3Cpath%20d='M-8%204.6C-11%208-11%2022-8%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Cpath%20d='M0%202.4V27.6'%20stroke='%23ffd45e'%20stroke-width='0.9'%20opacity='0.5'/%3E%3Cpath%20d='M8%204.6C11%208%2011%2022%208%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Crect%20x='-7'%20y='27'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cpath%20d='M-2.6%2030v9M2.6%2030v9'%20stroke='%23ffd45e'%20stroke-width='1.2'%20stroke-linecap='round'/%3E%3C/g%3E%3Cg%20opacity='0.83'%20transform='translate(919.4%2054.2)%20scale(1.80)'%3E%3Crect%20x='-0.5'%20y='-30.0'%20width='1'%20height='30.0'%20fill='%23ffd45e'%20opacity='0.4'/%3E%3Crect%20x='-7'%20y='0'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='rgba(255,%2092,%2062,%200.42)'/%3E%3Cellipse%20cx='-4.6'%20cy='12'%20rx='6.4'%20ry='8'%20fill='%23ffd45e'%20opacity='0.16'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1.4'/%3E%3Cpath%20d='M-8%204.6C-11%208-11%2022-8%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Cpath%20d='M0%202.4V27.6'%20stroke='%23ffd45e'%20stroke-width='0.9'%20opacity='0.5'/%3E%3Cpath%20d='M8%204.6C11%208%2011%2022%208%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Crect%20x='-7'%20y='27'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cpath%20d='M-2.6%2030v9M2.6%2030v9'%20stroke='%23ffd45e'%20stroke-width='1.2'%20stroke-linecap='round'/%3E%3C/g%3E%3Cg%20opacity='0.61'%20transform='translate(1163.3%2049.2)%20scale(1.89)'%3E%3Crect%20x='-0.5'%20y='-26.1'%20width='1'%20height='26.1'%20fill='%23ffd45e'%20opacity='0.4'/%3E%3Crect%20x='-7'%20y='0'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='rgba(255,%2092,%2062,%200.42)'/%3E%3Cellipse%20cx='-4.6'%20cy='12'%20rx='6.4'%20ry='8'%20fill='%23ffd45e'%20opacity='0.16'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1.4'/%3E%3Cpath%20d='M-8%204.6C-11%208-11%2022-8%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Cpath%20d='M0%202.4V27.6'%20stroke='%23ffd45e'%20stroke-width='0.9'%20opacity='0.5'/%3E%3Cpath%20d='M8%204.6C11%208%2011%2022%208%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Crect%20x='-7'%20y='27'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cpath%20d='M-2.6%2030v9M2.6%2030v9'%20stroke='%23ffd45e'%20stroke-width='1.2'%20stroke-linecap='round'/%3E%3C/g%3E%3Cg%20opacity='0.66'%20transform='translate(1425.0%2033.6)%20scale(1.81)'%3E%3Crect%20x='-0.5'%20y='-18.5'%20width='1'%20height='18.5'%20fill='%23ffd45e'%20opacity='0.4'/%3E%3Crect%20x='-7'%20y='0'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='rgba(255,%2092,%2062,%200.42)'/%3E%3Cellipse%20cx='-4.6'%20cy='12'%20rx='6.4'%20ry='8'%20fill='%23ffd45e'%20opacity='0.16'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1.4'/%3E%3Cpath%20d='M-8%204.6C-11%208-11%2022-8%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Cpath%20d='M0%202.4V27.6'%20stroke='%23ffd45e'%20stroke-width='0.9'%20opacity='0.5'/%3E%3Cpath%20d='M8%204.6C11%208%2011%2022%208%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Crect%20x='-7'%20y='27'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cpath%20d='M-2.6%2030v9M2.6%2030v9'%20stroke='%23ffd45e'%20stroke-width='1.2'%20stroke-linecap='round'/%3E%3C/g%3E%3Cg%20opacity='0.84'%20transform='translate(1676.0%2048.8)%20scale(2.28)'%3E%3Crect%20x='-0.5'%20y='-21.4'%20width='1'%20height='21.4'%20fill='%23ffd45e'%20opacity='0.4'/%3E%3Crect%20x='-7'%20y='0'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='rgba(255,%2092,%2062,%200.42)'/%3E%3Cellipse%20cx='-4.6'%20cy='12'%20rx='6.4'%20ry='8'%20fill='%23ffd45e'%20opacity='0.16'/%3E%3Cellipse%20cx='0'%20cy='15'%20rx='15'%20ry='13'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1.4'/%3E%3Cpath%20d='M-8%204.6C-11%208-11%2022-8%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Cpath%20d='M0%202.4V27.6'%20stroke='%23ffd45e'%20stroke-width='0.9'%20opacity='0.5'/%3E%3Cpath%20d='M8%204.6C11%208%2011%2022%208%2025.4'%20fill='none'%20stroke='%23ffd45e'%20stroke-width='1'%20opacity='0.7'/%3E%3Crect%20x='-7'%20y='27'%20width='14'%20height='3'%20rx='1.2'%20fill='%23ffd45e'/%3E%3Cpath%20d='M-2.6%2030v9M2.6%2030v9'%20stroke='%23ffd45e'%20stroke-width='1.2'%20stroke-linecap='round'/%3E%3C/g%3E%3C/svg%3E");
  --fx-bat-flock-h: 210px;
  --fx-bat-flock: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='520'%20height='210'%20fill='%23000000'%3E%3Cg%20opacity='0.96'%20transform='translate(56.6%2034.0)%20rotate(13)%20scale(2.07)%20translate(-16%20-8.5)'%3E%3Cpath%20d='M0.5%203.2L4.2%208.6L6.6%206.6L9.4%2011.4L11.6%209L13.4%2013.6L16%2015.2L18.6%2013.6L20.4%209L22.6%2011.4L25.4%206.6L27.8%208.6L31.5%203.2L27%205L22%205.6L18.4%206.6L17.6%202.2L16.6%205.4L16%204.6L15.4%205.4L14.4%202.2L13.6%206.6L10%205.6L5%205Z'/%3E%3Cellipse%20cx='16'%20cy='10.4'%20rx='2.2'%20ry='4.4'/%3E%3C/g%3E%3Cg%20opacity='0.73'%20transform='translate(163.0%20166.1)%20rotate(-4)%20scale(1.70)%20translate(-16%20-8.5)'%3E%3Cpath%20d='M0.5%203.2L4.2%208.6L6.6%206.6L9.4%2011.4L11.6%209L13.4%2013.6L16%2015.2L18.6%2013.6L20.4%209L22.6%2011.4L25.4%206.6L27.8%208.6L31.5%203.2L27%205L22%205.6L18.4%206.6L17.6%202.2L16.6%205.4L16%204.6L15.4%205.4L14.4%202.2L13.6%206.6L10%205.6L5%205Z'/%3E%3Cellipse%20cx='16'%20cy='10.4'%20rx='2.2'%20ry='4.4'/%3E%3C/g%3E%3Cg%20opacity='0.71'%20transform='translate(271.8%2093.3)%20rotate(-10)%20scale(2.66)%20translate(-16%20-8.5)'%3E%3Cpath%20d='M0.5%203.2L4.2%208.6L6.6%206.6L9.4%2011.4L11.6%209L13.4%2013.6L16%2015.2L18.6%2013.6L20.4%209L22.6%2011.4L25.4%206.6L27.8%208.6L31.5%203.2L27%205L22%205.6L18.4%206.6L17.6%202.2L16.6%205.4L16%204.6L15.4%205.4L14.4%202.2L13.6%206.6L10%205.6L5%205Z'/%3E%3Cellipse%20cx='16'%20cy='10.4'%20rx='2.2'%20ry='4.4'/%3E%3C/g%3E%3Cg%20opacity='0.91'%20transform='translate(400.4%20116.1)%20rotate(-8)%20scale(2.43)%20translate(-16%20-8.5)'%3E%3Cpath%20d='M0.5%203.2L4.2%208.6L6.6%206.6L9.4%2011.4L11.6%209L13.4%2013.6L16%2015.2L18.6%2013.6L20.4%209L22.6%2011.4L25.4%206.6L27.8%208.6L31.5%203.2L27%205L22%205.6L18.4%206.6L17.6%202.2L16.6%205.4L16%204.6L15.4%205.4L14.4%202.2L13.6%206.6L10%205.6L5%205Z'/%3E%3Cellipse%20cx='16'%20cy='10.4'%20rx='2.2'%20ry='4.4'/%3E%3C/g%3E%3Cg%20opacity='0.80'%20transform='translate(477.2%20168.7)%20rotate(-4)%20scale(2.60)%20translate(-16%20-8.5)'%3E%3Cpath%20d='M0.5%203.2L4.2%208.6L6.6%206.6L9.4%2011.4L11.6%209L13.4%2013.6L16%2015.2L18.6%2013.6L20.4%209L22.6%2011.4L25.4%206.6L27.8%208.6L31.5%203.2L27%205L22%205.6L18.4%206.6L17.6%202.2L16.6%205.4L16%204.6L15.4%205.4L14.4%202.2L13.6%206.6L10%205.6L5%205Z'/%3E%3Cellipse%20cx='16'%20cy='10.4'%20rx='2.2'%20ry='4.4'/%3E%3C/g%3E%3C/svg%3E");
  --fx-fly-far-h: 360px;
  --fx-fly-far: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='360'%20height='360'%20fill='%23c8f06a'%3E%3Ccircle%20cx='12'%20cy='34'%20r='1.2'%20opacity='0.6'/%3E%3Ccircle%20cx='109'%20cy='36'%20r='1.2'%20opacity='0.6'/%3E%3Ccircle%20cx='211'%20cy='5'%20r='1.0'%20opacity='0.8'/%3E%3Ccircle%20cx='279'%20cy='51'%20r='1.5'%20opacity='0.7'/%3E%3Ccircle%20cx='293'%20cy='34'%20r='1.1'%20opacity='0.6'/%3E%3Ccircle%20cx='56'%20cy='140'%20r='1.2'%20opacity='0.8'/%3E%3Ccircle%20cx='115'%20cy='138'%20r='1.1'%20opacity='0.4'/%3E%3Ccircle%20cx='159'%20cy='80'%20r='1.4'%20opacity='0.7'/%3E%3Ccircle%20cx='256'%20cy='96'%20r='1.3'%20opacity='0.7'/%3E%3Ccircle%20cx='300'%20cy='104'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='8'%20cy='173'%20r='0.9'%20opacity='0.7'/%3E%3Ccircle%20cx='76'%20cy='186'%20r='1.0'%20opacity='0.6'/%3E%3Ccircle%20cx='160'%20cy='185'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='223'%20cy='210'%20r='1.2'%20opacity='0.5'/%3E%3Ccircle%20cx='357'%20cy='178'%20r='1.1'%20opacity='0.3'/%3E%3Ccircle%20cx='59'%20cy='259'%20r='1.3'%20opacity='0.3'/%3E%3Ccircle%20cx='75'%20cy='270'%20r='1.5'%20opacity='0.6'/%3E%3Ccircle%20cx='179'%20cy='247'%20r='1.5'%20opacity='0.7'/%3E%3Ccircle%20cx='268'%20cy='256'%20r='1.4'%20opacity='0.4'/%3E%3Ccircle%20cx='326'%20cy='218'%20r='1.0'%20opacity='0.7'/%3E%3Ccircle%20cx='67'%20cy='351'%20r='1.4'%20opacity='0.7'/%3E%3Ccircle%20cx='103'%20cy='322'%20r='1.3'%20opacity='0.5'/%3E%3Ccircle%20cx='199'%20cy='317'%20r='1.4'%20opacity='0.6'/%3E%3Ccircle%20cx='239'%20cy='305'%20r='1.3'%20opacity='0.7'/%3E%3Ccircle%20cx='331'%20cy='335'%20r='1.4'%20opacity='0.5'/%3E%3C/svg%3E");
  --fx-fly-near-h: 300px;
  --fx-fly-near: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='300'%20height='300'%20fill='%23e6ff8a'%3E%3Ccircle%20cx='49'%20cy='7'%20r='2.5'%20opacity='0.7'/%3E%3Ccircle%20cx='195'%20cy='34'%20r='2.1'%20opacity='0.7'/%3E%3Ccircle%20cx='270'%20cy='49'%20r='2.0'%20opacity='0.6'/%3E%3Ccircle%20cx='97'%20cy='195'%20r='2.4'%20opacity='1.0'/%3E%3Ccircle%20cx='133'%20cy='137'%20r='2.0'%20opacity='0.8'/%3E%3Ccircle%20cx='285'%20cy='110'%20r='1.9'%20opacity='0.9'/%3E%3Ccircle%20cx='58'%20cy='284'%20r='2.1'%20opacity='0.5'/%3E%3Ccircle%20cx='181'%20cy='270'%20r='2.1'%20opacity='0.8'/%3E%3Ccircle%20cx='271'%20cy='218'%20r='1.8'%20opacity='0.8'/%3E%3C/svg%3E");
  --fx-tree-far-h: 210px;
  --fx-tree-far: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='940'%20height='210'%20fill='%2310261a'%3E%3Cpath%20d='M15.5%20210.0L54.6%20156.3L93.7%20210.0ZM23.6%20188.1L54.6%20134.5L85.6%20188.1ZM31.7%20166.3L54.6%20112.6L77.6%20166.3ZM39.8%20144.4L54.6%2090.8L69.5%20144.4ZM51.1%20210L51.1%20195.7L58.1%20195.7L58.1%20210Z'%20opacity='0.64'/%3E%3Cpath%20d='M82.3%20210.0L119.1%20143.8L156.0%20210.0ZM89.9%20183.0L119.1%20116.8L148.3%20183.0ZM97.5%20156.1L119.1%2089.9L140.7%20156.1ZM105.1%20129.1L119.1%2062.9L133.1%20129.1ZM115.8%20210L115.8%20192.3L122.4%20192.3L122.4%20210Z'%20opacity='0.56'/%3E%3Cpath%20d='M140.0%20210.0L179.5%20141.8L219.0%20210.0ZM148.1%20182.2L179.5%20114.0L210.8%20182.2ZM156.3%20154.4L179.5%2086.2L202.6%20154.4ZM164.5%20126.6L179.5%2058.3L194.5%20126.6ZM175.9%20210L175.9%20191.8L183.0%20191.8L183.0%20210Z'%20opacity='0.71'/%3E%3Cpath%20d='M221.1%20210.0L254.5%20143.6L287.8%20210.0ZM228.0%20183.0L254.5%20116.6L280.9%20183.0ZM234.9%20155.9L254.5%2089.5L274.0%20155.9ZM241.8%20128.9L254.5%2062.5L267.1%20128.9ZM251.5%20210L251.5%20192.3L257.5%20192.3L257.5%20210Z'%20opacity='0.58'/%3E%3Cpath%20d='M290.6%20210.0L324.8%20159.8L359.0%20210.0ZM297.7%20189.5L324.8%20139.4L351.9%20189.5ZM304.8%20169.1L324.8%20118.9L344.9%20169.1ZM311.8%20148.6L324.8%2098.4L337.8%20148.6ZM321.7%20210L321.7%20196.6L327.9%20196.6L327.9%20210Z'%20opacity='0.71'/%3E%3Cpath%20d='M392.2%20210.0L422.9%20139.5L453.7%20210.0ZM398.6%20181.3L422.9%20110.8L447.3%20181.3ZM404.9%20152.6L422.9%2082.1L441.0%20152.6ZM411.3%20123.9L422.9%2053.4L434.6%20123.9ZM420.2%20210L420.2%20191.2L425.7%20191.2L425.7%20210Z'%20opacity='0.63'/%3E%3Cpath%20d='M419.3%20210.0L451.4%20142.8L483.6%20210.0ZM426.0%20182.6L451.4%20115.4L476.9%20182.6ZM432.6%20155.2L451.4%2088.0L470.3%20155.2ZM439.2%20127.8L451.4%2060.6L463.6%20127.8ZM448.5%20210L448.5%20192.1L454.3%20192.1L454.3%20210Z'%20opacity='0.58'/%3E%3Cpath%20d='M481.1%20210.0L522.2%20147.4L563.2%20210.0ZM489.6%20184.5L522.2%20121.9L554.7%20184.5ZM498.1%20159.0L522.2%2096.4L546.3%20159.0ZM506.6%20133.5L522.2%2070.9L537.8%20133.5ZM518.5%20210L518.5%20193.3L525.9%20193.3L525.9%20210Z'%20opacity='0.77'/%3E%3Cpath%20d='M579.6%20210.0L616.7%20144.0L653.8%20210.0ZM587.3%20183.1L616.7%20117.2L646.1%20183.1ZM595.0%20156.3L616.7%2090.3L638.4%20156.3ZM602.6%20129.4L616.7%2063.4L630.8%20129.4ZM613.4%20210L613.4%20192.4L620.0%20192.4L620.0%20210Z'%20opacity='0.57'/%3E%3Cpath%20d='M661.0%20210.0L697.7%20157.3L734.4%20210.0ZM668.6%20188.5L697.7%20135.9L726.8%20188.5ZM676.2%20167.1L697.7%20114.4L719.2%20167.1ZM683.7%20145.6L697.7%2092.9L711.6%20145.6ZM694.4%20210L694.4%20196.0L701.0%20196.0L701.0%20210Z'%20opacity='0.66'/%3E%3Cpath%20d='M695.5%20210.0L726.8%20142.3L758.0%20210.0ZM702.0%20182.4L726.8%20114.7L751.5%20182.4ZM708.4%20154.8L726.8%2087.0L745.1%20154.8ZM714.9%20127.2L726.8%2059.4L738.6%20127.2ZM724.0%20210L724.0%20191.9L729.6%20191.9L729.6%20210Z'%20opacity='0.72'/%3E%3Cpath%20d='M796.9%20210.0L828.0%20166.6L859.1%20210.0ZM803.3%20192.3L828.0%20149.0L852.7%20192.3ZM809.8%20174.7L828.0%20131.3L846.3%20174.7ZM816.2%20157.0L828.0%20113.6L839.8%20157.0ZM825.2%20210L825.2%20198.4L830.8%20198.4L830.8%20210Z'%20opacity='0.50'/%3E%3Cpath%20d='M849.5%20210.0L891.2%20164.9L932.8%20210.0ZM858.1%20191.6L891.2%20146.6L924.2%20191.6ZM866.7%20173.3L891.2%20128.2L915.6%20173.3ZM875.3%20154.9L891.2%20109.9L907.0%20154.9ZM887.4%20210L887.4%20198.0L894.9%20198.0L894.9%20210Z'%20opacity='0.71'/%3E%3C/svg%3E");
  --fx-tree-near-h: 300px;
  --fx-tree-near: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='1180'%20height='300'%20fill='%23030d07'%3E%3Cpath%20d='M47.2%20300.0L103.0%20207.1L158.7%20300.0ZM58.7%20262.2L103.0%20169.3L147.2%20262.2ZM70.2%20224.3L103.0%20131.5L135.7%20224.3ZM81.8%20186.5L103.0%2093.7L124.1%20186.5ZM97.9%20300L97.9%20275.2L108.0%20275.2L108.0%20300Z'%20opacity='0.78'/%3E%3Cpath%20d='M172.3%20300.0L239.4%20224.5L306.4%20300.0ZM186.2%20269.2L239.4%20193.7L292.5%20269.2ZM200.0%20238.5L239.4%20162.9L278.7%20238.5ZM213.9%20207.7L239.4%20132.2L264.8%20207.7ZM233.3%20300L233.3%20279.9L245.4%20279.9L245.4%20300Z'%20opacity='0.89'/%3E%3Cpath%20d='M268.6%20300.0L336.6%20218.8L404.7%20300.0ZM282.6%20266.9L336.6%20185.7L390.6%20266.9ZM296.7%20233.8L336.6%20152.7L376.6%20233.8ZM310.8%20200.8L336.6%20119.6L362.5%20200.8ZM330.5%20300L330.5%20278.4L342.8%20278.4L342.8%20300Z'%20opacity='0.94'/%3E%3Cpath%20d='M439.7%20300.0L502.4%20214.0L565.0%20300.0ZM452.6%20265.0L502.4%20179.0L552.1%20265.0ZM465.6%20229.9L502.4%20143.9L539.1%20229.9ZM478.5%20194.9L502.4%20108.9L526.2%20194.9ZM496.7%20300L496.7%20277.1L508.0%20277.1L508.0%20300Z'%20opacity='0.88'/%3E%3Cpath%20d='M633.6%20300.0L698.5%20185.2L763.4%20300.0ZM647.0%20253.2L698.5%20138.5L750.0%20253.2ZM660.4%20206.5L698.5%2091.7L736.6%20206.5ZM673.9%20159.7L698.5%2045.0L723.2%20159.7ZM692.7%20300L692.7%20269.4L704.4%20269.4L704.4%20300Z'%20opacity='0.92'/%3E%3Cpath%20d='M747.7%20300.0L798.9%20210.5L850.1%20300.0ZM758.2%20263.5L798.9%20174.0L839.5%20263.5ZM768.8%20227.1L798.9%20137.6L828.9%20227.1ZM779.4%20190.6L798.9%20101.1L818.4%20190.6ZM794.3%20300L794.3%20276.1L803.5%20276.1L803.5%20300Z'%20opacity='0.84'/%3E%3Cpath%20d='M822.1%20300.0L895.1%20212.9L968.1%20300.0ZM837.2%20264.5L895.1%20177.5L953.0%20264.5ZM852.3%20229.1L895.1%20142.0L937.9%20229.1ZM867.4%20193.6L895.1%20106.5L922.8%20193.6ZM888.5%20300L888.5%20276.8L901.7%20276.8L901.7%20300Z'%20opacity='0.75'/%3E%3Cpath%20d='M1033.0%20300.0L1094.8%20205.6L1156.7%20300.0ZM1045.8%20261.5L1094.8%20167.1L1143.9%20261.5ZM1058.5%20223.0L1094.8%20128.6L1131.1%20223.0ZM1071.3%20184.6L1094.8%2090.1L1118.3%20184.6ZM1089.3%20300L1089.3%20274.8L1100.4%20274.8L1100.4%20300Z'%20opacity='0.86'/%3E%3C/svg%3E");
}

/* ══ END GENERATED FX TILES ══════════════════════════════════════════════ */

/* ══ THEME SIGNATURES ═════════════════════════════════════════════════════
 *
 * The owner's complaint was exact: choosing a theme "just changed the Color".
 *
 * It did. A preset was fifteen custom properties, so Halloween, Christmas and
 * Galaxy were the same page three times with the accent swapped, and somebody
 * who only ever saw one of them could not tell there was a theme system at all.
 * A palette is not a theme. A theme is a place.
 *
 * So every preset owns a SIGNATURE: an ambient composition nothing else has,
 * and the tempo its own light moves at. Snow falls at Christmas and nowhere
 * else. Halloween has fog and a candle that will not sit still. Galaxy has a
 * star field with a shooting star in it about every seventeen seconds.
 *
 * ## The four rules, because the last ambient layer was deleted for breaking
 * ## all of them
 *
 * The first version of this design had drifting fog and a canvas of rising
 * motes, and the owner's word for it was slop. That verdict was right and none
 * of it is coming back. What is different this time:
 *
 *   IT IS BEHIND EVERYTHING AND IT IS QUIET. The whole layer lives in `.amb`
 *   at z-index 0, under content at z-index 1, and nothing in it is above about
 *   twelve percent opacity in the seasonal effects. The original's desktop
 *   artwork is individually dimmed and covered by a dark reading veil.
 *
 *   IT IS ONE IDEA PER THEME, NOT A BAG OF EFFECTS. Each preset gets one image
 *   built from a small set of surfaces. The original uses the game library;
 *   seasonal presets use the shapes and movement of their own setting.
 *
 *   NOTHING PAINTS. Every animation here moves `transform` or `opacity` and
 *   nothing else, so the work stays on the compositor and no scroll repaints.
 *   There is no canvas or animation script. Blur is fixed on individual desktop
 *   cards, never animated or applied to the full viewport. Light uses gradients.
 *   BRIGHTNESS LIVES IN THE COLOUR, never in `opacity`, because `opacity` is
 *   reserved for animation: a layer that sets both has to fight itself.
 *
 *   THE STILL PICTURE IS THE BASE RULE. Every keyframe below is life added on
 *   top of a composition that is already right without it. That is not taste.
 *   `prefers-reduced-motion` collapses animation to a single 0.001s iteration
 *   and the element then REVERTS TO ITS UNANIMATED STATE, so a layer that only
 *   becomes visible inside its keyframes would vanish for the people who asked
 *   for less movement. The two things that should be invisible when still, the
 *   cyberpunk glitch and the galaxy shooting star, carry `opacity: 0` in their
 *   base rule and are lit only by their keyframes.
 *
 * ## And it costs less on a phone
 *
 * Each particle stack drops its third layer at 780px and its second at 560px,
 * and the whole signature dims, because a small screen is a slower GPU showing
 * a smaller picture. That is also why the layers are ordered least important
 * last: `fx3` is always the one a theme can lose.
 */

/* ── the layer ──────────────────────────────────────────────────────────── */

.amb .fx { position: absolute; inset: 0; opacity: var(--fx-op); }

/* LIGHT sits on surfaces that cover exactly the window, so a gradient placed at
   "18% 0%" is over the part of the page the reader is actually looking at. */
.amb .fx::before, .amb .fx::after,
.amb .fx > i::before, .amb .fx > i::after {
  content: ""; position: absolute; inset: 0;
}

/* THINGS THAT TRAVEL. The box reaches one tile beyond the window at both ends,
   because the travel is exactly one tile and without that headroom the leading
   edge drags an empty band into view halfway through every cycle. Themes whose
   layer is light rather than particles override `inset` back to zero. */
.amb .fx > i {
  --fx-h: 300px; --fx-sway: 8px;
  position: absolute; display: block;
  left: -32px; right: -32px;
  top: calc(var(--fx-h) * -1); bottom: calc(var(--fx-h) * -1);
}

/**
 * One tile of travel, and the sway comes back to zero.
 *
 * `--fx-h` is the exact height of the tile the layer is painted with, which is
 * the only distance that makes a repeating background loop with no seam. The
 * horizontal sway returns to zero at 100% for the same reason: a drift that did
 * not come back would step the tile sideways once per cycle, and the seam would
 * arrive several minutes in, long after anybody was still looking for it.
 */
@keyframes fx-fall {
  0% { transform: translate3d(0, 0, 0); }
  25% { transform: translate3d(var(--fx-sway), calc(var(--fx-h) * 0.25), 0); }
  50% { transform: translate3d(0, calc(var(--fx-h) * 0.5), 0); }
  75% { transform: translate3d(calc(var(--fx-sway) * -1), calc(var(--fx-h) * 0.75), 0); }
  100% { transform: translate3d(0, var(--fx-h), 0); }
}
@keyframes fx-rise {
  0% { transform: translate3d(0, 0, 0); }
  25% { transform: translate3d(calc(var(--fx-sway) * -1), calc(var(--fx-h) * -0.25), 0); }
  50% { transform: translate3d(0, calc(var(--fx-h) * -0.5), 0); }
  75% { transform: translate3d(var(--fx-sway), calc(var(--fx-h) * -0.75), 0); }
  100% { transform: translate3d(0, calc(var(--fx-h) * -1), 0); }
}

/* Light that breathes. Full strength at both ends, so the still frame is the
   bright one and reduced motion loses nothing but the movement. */
@keyframes fx-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.44; } }
@keyframes fx-breathe-soft { 0%, 100% { opacity: 1; } 50% { opacity: 0.74; } }

/* Light that slides, for fog banks and curtains. Returns to where it began. */
@keyframes fx-sway-wide {
  0%, 100% { transform: translate3d(-4%, 0, 0); }
  50% { transform: translate3d(4%, 0, 0); }
}
@keyframes fx-sway-slow {
  0%, 100% { transform: translate3d(3%, 0, 0) scale(1.04); }
  50% { transform: translate3d(-3%, 0, 0) scale(1); }
}

/* ── original · GLADHUB DRIFT WALL ─────────────────────────────────────────
 * Based on the user's React Bits DriftWall reference: a perspective plane of
 * alternating vertical tracks. Two equal groups make each loop seamless.
 * Inert background only: no links, hover lift, pointer tracking or React runtime.
 * Images are only applied on desktop. Blur stays fixed on individual tiles;
 * seven track transforms provide all motion and pause when the tab is hidden.
 */
@media (min-width: 1100px) {
  :root:not([data-theme]) .amb .wash {
    background:
      radial-gradient(ellipse at 50% -18%, #241043, transparent 58%),
      linear-gradient(180deg, #100b1c, #07060c 72%);
  }
  :root:not([data-theme]) .amb .scriptfield {
    display: block; position: absolute; inset: 0;
    pointer-events: none; user-select: none; isolation: isolate;
  }
  :root:not([data-theme]) .amb .scriptfield::before {
    content: ""; position: absolute; inset: 0;
    background:
      radial-gradient(ellipse at 8% 28%, #8b5cf626, transparent 36%),
      radial-gradient(ellipse at 92% 58%, #7542df24, transparent 36%),
      radial-gradient(ellipse at 75% -10%, #46c6ff0d, transparent 45%);
  }
  :root:not([data-theme]) .amb .scriptfield::after {
    content: ""; position: absolute; inset: 0; z-index: 2;
    background:
      radial-gradient(ellipse 42% 62% at 50% 38%, #08060fe8 12%, #08060fb8 47%, #08060f00 100%),
      linear-gradient(180deg, #08060f38, #08060f26 22%, #08060f55 58%, #08060ff2 100%);
  }
  .drift-wall {
    --wall-w: clamp(200px, 18vw, 350px); --wall-h: max(150px, 18vh); --wall-gap: 18px;
    position: absolute; inset: 0; overflow: hidden; perspective: 1200px;
    mask-image: radial-gradient(ellipse 80% 86% at 50% 40%, #000 36%, transparent 100%);
  }
  .drift-wall__plane {
    position: absolute; top: 50%; left: 50%; display: flex; gap: var(--wall-gap);
    height: calc((var(--wall-h) + var(--wall-gap)) * 6); transform-style: preserve-3d;
    transform: translate(-50%, -50%) scale(1.18) rotateX(16deg) rotateY(-14deg) rotateZ(-8deg) translateZ(-120px);
  }
  .drift-wall__col { width: var(--wall-w); flex: none; }
  :root:not([data-theme]) .amb .drift-wall__track {
    --wall-time: 70s; --wall-phase: -16s; --wall-rest: -12%;
    transform: translate3d(0, var(--wall-rest), 0);
    animation: fx-wall-drift var(--wall-time) linear infinite;
    animation-delay: var(--wall-phase);
  }
  .drift-wall__group { display: flex; flex-direction: column; gap: var(--wall-gap); padding-bottom: var(--wall-gap); }
  .drift-wall__tile {
    position: relative; display: block; height: var(--wall-h); flex: none; overflow: hidden;
    border: 1px solid #b49cff4d; border-radius: 14px; background: var(--wall-image) center / cover;
    box-shadow: inset 0 1px 0 #e0d1ff33, 0 10px 28px #0008;
    opacity: 0.58; filter: blur(1.1px) saturate(0.65); pointer-events: none;
  }
  .drift-wall__tile::before {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(155deg, #160a3030, #0a061742 45%, #07040ecf 100%);
    box-shadow: inset 0 0 35px #0c071842;
  }
  .drift-wall__tile > span {
    position: absolute; left: 16px; right: 12px; bottom: 15px;
    font: 400 15px/1.2 var(--disp); color: #eeecf5;
  }
  .drift-wall__tile > span::before {
    content: "GLADHUB"; display: block; margin-bottom: 7px;
    font: 400 8px/1 var(--disp); letter-spacing: 0.2em; color: #c2aeff;
  }
  :root:not([data-theme]) .amb .drift-wall__col:nth-child(even) .drift-wall__track { animation-direction: reverse; }
  :root:not([data-theme]) .amb .drift-wall__col:nth-child(2) .drift-wall__track { --wall-time: 82s; --wall-phase: -38s; --wall-rest: -22%; }
  :root:not([data-theme]) .amb .drift-wall__col:nth-child(3) .drift-wall__track { --wall-time: 62s; --wall-phase: -8s; --wall-rest: -7%; }
  :root:not([data-theme]) .amb .drift-wall__col:nth-child(4) .drift-wall__track { --wall-time: 90s; --wall-phase: -54s; --wall-rest: -28%; }
  :root:not([data-theme]) .amb .drift-wall__col:nth-child(5) .drift-wall__track { --wall-time: 73s; --wall-phase: -25s; --wall-rest: -17%; }
  :root:not([data-theme]) .amb .drift-wall__col:nth-child(6) .drift-wall__track { --wall-time: 86s; --wall-phase: -67s; --wall-rest: -35%; }
  :root:not([data-theme]) .amb .drift-wall__col:nth-child(7) .drift-wall__track { --wall-time: 66s; --wall-phase: -12s; --wall-rest: -9%; }
  .drift-wall__col:nth-child(3n) .drift-wall__tile { filter: blur(1.6px) saturate(0.6); }
  :root:not([data-theme]) body:not([data-page="home"]) .amb .scriptfield { opacity: 0.52; }
  :root:not([data-theme]) .amb .watermark {
    top: -50px; right: calc(50% - 260px); width: 520px; opacity: 0.024;
  }
  :root:not([data-theme]) .amb::after {
    background: linear-gradient(90deg, #05040b66, transparent 18%, transparent 82%, #05040b66);
  }
  :root:not([data-theme]) .top[data-lift="0"] {
    background: linear-gradient(180deg, #0d091be8, #08060fb8);
    border-bottom-color: #b49cff1c;
  }
  :root:not([data-theme]) .home-intro {
    grid-template-columns: minmax(0, 1fr); gap: 0;
    padding-top: clamp(64px, 10vh, 100px); padding-bottom: 24px;
  }
  :root:not([data-theme]) .hero { text-align: center; width: 100%; max-width: 760px; margin-inline: auto; }
  :root:not([data-theme]) .hero h1 { margin-block: 26px; line-height: var(--hero-lh, 1.1); }
  :root:not([data-theme]) .hero h1 .lit { text-shadow: 0 0 40px #8b5cf652; }
  :root:not([data-theme]) .hero .sub { max-width: 49ch; margin-inline: auto; color: #b3aec4; }
  :root:not([data-theme]) .hero .cta { justify-content: center; }
  :root:not([data-theme]) .hero-library { display: none; }
  :root:not([data-theme]) .home-intro .trust {
    justify-content: center; column-gap: 40px; margin-top: 64px; padding-top: 26px;
    border-image: linear-gradient(90deg, transparent, #b49cff26, transparent) 1;
  }
  :root:not([data-theme]) .home-intro .trust .ic { color: var(--vio-txt); }
  :root:not([data-theme]) .bento > .stat {
    background: linear-gradient(145deg, #191225ed, #0d0a14f5 65%);
    border-color: #b49cff24;
    box-shadow: inset 0 1px 0 #c2aeff0d, 0 10px 32px #0003;
  }
}
@keyframes fx-wall-drift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(0, -50%, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .amb .drift-wall__track { animation: none !important; }
}

/* A brighter edge and a dark centre keep the original brand's controls tactile. */
:root:not([data-theme]) .hero .btn-p {
  background: linear-gradient(180deg, #8242e5, #6227bc);
  border-color: #bd94ff52;
  box-shadow: inset 0 1px 0 #ebdaff44, 0 0 24px -7px #8b5cf680, 0 8px 20px #0005;
}
:root:not([data-theme]) .hero .btn-s {
  background: linear-gradient(180deg, #1c1629f2, #100c1af5);
  border-color: #b49cff33;
  box-shadow: inset 0 1px 0 #ebdaff0d;
}
@media (hover: hover) {
  :root:not([data-theme]) .hero .btn-p:hover {
    box-shadow: inset 0 1px 0 #ebdaff66, 0 0 34px -5px #8b5cf699, 0 12px 28px #0006;
  }
  :root:not([data-theme]) .hero .btn-s:hover { border-color: #b49cff77; }
}

/* ── valentine · ROSE BLOOM ────────────────────────────────────────────────
 *
 * Two pools of rose light breathing out of step, and hearts small enough that
 * they read as warmth rather than as a greetings card. Nothing here is above
 * ten percent, and the hearts RISE, because falling hearts are melancholy and
 * that is the wrong feeling for the theme. */
:root[data-theme="valentine"] {
  --fx-op: 0.95; --fx-sweep: 0.78s; --fx-edge: 15s;
}
:root[data-theme="valentine"] .amb .fx::before {
  background:
    radial-gradient(48% 38% at 14% 4%, color-mix(in srgb, var(--vio) 20%, transparent), transparent 68%),
    radial-gradient(42% 34% at 92% 22%, color-mix(in srgb, var(--ion) 13%, transparent), transparent 70%);
  animation: fx-breathe-soft 16s ease-in-out infinite;
}
:root[data-theme="valentine"] .amb .fx::after {
  background: radial-gradient(70% 40% at 50% 108%, color-mix(in srgb, var(--vio) 16%, transparent), transparent 72%);
  animation: fx-breathe-soft 23s ease-in-out infinite reverse;
}
/* 780px tile / 78s = 10 px/s, which is what it was at 520/52. The durations
   here and in sakura are tied to the tile heights, not chosen: the travel is
   one tile per cycle, so growing the tile to hide its repeat speeds the layer
   up by the same factor unless the cycle grows with it. */
:root[data-theme="valentine"] .amb .fx > i.fx1 {
  --fx-h: var(--fx-heart-far-h); --fx-sway: 12px;
  background-image: var(--fx-heart-far);
  opacity: 0.42;
  animation: fx-rise 78s linear infinite;
}
/* 900 / 64 = 14.1 px/s, as it was at 480/34. */
:root[data-theme="valentine"] .amb .fx > i.fx2 {
  --fx-h: var(--fx-heart-near-h); --fx-sway: 18px;
  background-image: var(--fx-heart-near);
  opacity: 0.28;
  animation: fx-rise 64s linear infinite;
}
:root[data-theme="valentine"] .amb .grid { opacity: 0.5; }

/* ── summer · SUNLIT WATER ─────────────────────────────────────────────────
 *
 * A sun in the top right and caustics along the bottom. The caustics are two
 * bands of soft ellipses travelling in OPPOSITE directions at different speeds,
 * which is the whole trick: neither layer looks like water on its own, and the
 * interference between them is what the surface of a pool actually does. */
:root[data-theme="summer"] {
  --fx-op: 1; --fx-sweep: 0.7s; --fx-edge: 12s; --amb-floor: 0.62;
}
:root[data-theme="summer"] .amb .fx::before {
  background:
    radial-gradient(30% 24% at 88% -6%, color-mix(in srgb, var(--ion) 22%, transparent), transparent 68%),
    radial-gradient(72% 48% at 84% 0%, color-mix(in srgb, var(--ion) 7%, transparent), transparent 74%);
  animation: fx-breathe-soft 14s ease-in-out infinite;
}
/* One broad shaft out of the same corner the sun is in, not a fan of them. The
   fan was three hard-edged stripes at 74 degrees, and hard-edged stripes across
   a whole window are a pattern, not light. */
:root[data-theme="summer"] .amb .fx::after {
  background: linear-gradient(198deg,
    color-mix(in srgb, var(--ion) 7%, transparent), transparent 44%);
  -webkit-mask-image: radial-gradient(80% 68% at 90% -10%, #fff, transparent 78%);
          mask-image: radial-gradient(80% 68% at 90% -10%, #fff, transparent 78%);
  animation: fx-breathe 21s ease-in-out infinite;
}
/**
 * The water, and this is the third attempt at it.
 *
 * The first drew soft blobs and they read as smudges. The second drew two sets
 * of stripes crossing at 22 degrees to make a caustic net, and the owner was
 * right about it: at window scale a 22 degree crossing makes DIAMONDS, and a
 * page full of diamonds is a wireframe, not a pool. The lattice was the most
 * geometric thing on the site.
 *
 * What is actually legible as water is a swell: long, soft, nearly horizontal
 * bands of light travelling slowly through each other. Two things make the
 * difference here.
 *
 * The stops. `transparent 0, colour 34px, transparent 68px` interpolates the
 * whole way, so each band is a smooth rise and fall with no edge anywhere. The
 * old version wrote `transparent 0 30px, colour 34px 39px`, which is a flat run
 * and then a hard edge, and a hard edge at this scale is a drawn line.
 *
 * The angle between them. Five degrees, not twenty two. Two band sets crossing
 * at five degrees interfere in shapes about eleven times longer than they are
 * tall, so the bright places read as streaks lying along the surface. The
 * geometry is the same as before; only the aspect ratio is different, and the
 * aspect ratio is the entire difference between water and a net.
 *
 * The pattern is on the pseudo, not the band, so the band's mask can stay put
 * while the water moves through it. Both are oversized well past the band, so
 * the vertical travel never reaches an edge and there is nothing to seam.
 */
:root[data-theme="summer"] .amb .fx > i.fx1,
:root[data-theme="summer"] .amb .fx > i.fx2 {
  inset: auto 0 0; height: 46%;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, transparent, #fff 56%, #fff 88%, transparent);
          mask-image: linear-gradient(180deg, transparent, #fff 56%, #fff 88%, transparent);
}
/* The water is in the WATER colour. Both sets were `--ion` in the first pass,
   and summer's second accent is #ffd54f, so it came out the colour of straw
   over a blue sea. */
:root[data-theme="summer"] .amb .fx > i.fx1::before {
  inset: -160px 0;
  background-image: repeating-linear-gradient(183deg,
    transparent 0, color-mix(in srgb, var(--vio) 12%, transparent) 36px, transparent 72px);
  animation: fx-swell-a 16s linear infinite;
}
@keyframes fx-swell-a { to { transform: translate3d(0, -72px, 0); } }
:root[data-theme="summer"] .amb .fx > i.fx2::before {
  inset: -160px 0;
  background-image: repeating-linear-gradient(178deg,
    transparent 0, color-mix(in srgb, var(--vio) 8%, transparent) 49px, transparent 98px);
  animation: fx-swell-b 25s linear infinite;
}
@keyframes fx-swell-b { to { transform: translate3d(0, 98px, 0); } }
:root[data-theme="summer"] .amb .grid { opacity: 0.5; }

/* ── cyberpunk · NEON CITY ─────────────────────────────────────────────────
 *
 * The one theme allowed to be aggressive, and the only one with a veil over the
 * whole window. Scanlines cost nothing because they never move. Under them a
 * neon floor grid runs toward the reader, and about three times a minute the
 * screen tears for a fortieth of a second.
 *
 * The glitch is written in `steps(1, end)` on purpose. A glitch that eases is
 * not a glitch, it is a fade, and the difference between the two is the whole
 * character of this theme. */
:root[data-theme="cyberpunk"] {
  --fx-op: 1; --fx-sweep: 0.34s; --fx-edge: 7s; --amb-floor: 0.4;
}
/* Scanlines, at about half the weight they started at and a wider pitch. A
   three pixel comb at 34 percent black is a dark grille laid over the reading
   column, and on a high density screen it aliases against the pixel grid as
   well. Four pixels at 22 percent still says "screen" and stops competing with
   the text. */
:root[data-theme="cyberpunk"] .amb .fx::before {
  background: repeating-linear-gradient(0deg,
    rgba(0, 0, 0, 0.22) 0 1px, transparent 1px 4px);
  opacity: 0.42;
}
/* The roll. One soft bright band walking down the window every nine seconds,
   which is what a CRT does and what makes the scanlines above read as a screen
   rather than as a texture. */
:root[data-theme="cyberpunk"] .amb .fx::after {
  background: linear-gradient(180deg, transparent 0 42%,
    color-mix(in srgb, var(--ion) 9%, transparent) 47%, transparent 52% 100%);
  animation: fx-roll 9s linear infinite;
}
@keyframes fx-roll {
  from { transform: translate3d(0, -70%, 0); }
  to { transform: translate3d(0, 100%, 0); }
}
/**
 * The floor grid, and the geometry is the whole effect.
 *
 * `.fx1` is the band it lives in and owns the perspective. Its pseudo starts at
 * the TOP EDGE of that band, which becomes the horizon, and is rotated about
 * that same edge so everything below it lies away into the distance. The pseudo
 * has to be several times the band's height before the rotation, because a
 * plane at 73 degrees keeps only about a quarter of its length once projected:
 * the first version was one band tall and collapsed into a thirty pixel smear
 * that looked like a rule that had failed rather than like a floor.
 *
 * The travel is `translate3d` AFTER the rotation, so it runs along the plane
 * rather than across the screen, which is what makes the grid come toward the
 * reader instead of sliding down the page. One grid cell per cycle, so it
 * loops with no seam.
 */
/* The fade is on the BAND and not on the plane, which is the correction that
   made this usable. A mask on the rotated pseudo is a mask in the plane's own
   space, so "fade out by 88%" means eighty eight percent of the way into the
   distance, and the near lines, the thick bright ones directly under the
   reading column, are inside the opaque part. The result was a floor at full
   strength across the bottom third of the page with body text sitting on it.
   Masking the unrotated band fades by SCREEN position, which is the thing that
   actually has to get out of the way. */
:root[data-theme="cyberpunk"] .amb .fx > i.fx1 {
  inset: auto 0 0; height: 38%;
  perspective: 200px; perspective-origin: 50% 0%;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, #fff 0 12%, rgba(255, 255, 255, 0.34) 46%, transparent 92%);
          mask-image: linear-gradient(180deg, #fff 0 12%, rgba(255, 255, 255, 0.34) 46%, transparent 92%);
}
:root[data-theme="cyberpunk"] .amb .fx > i.fx1::before {
  inset: 0 -45% -260%;
  /* The two directions are not the same weight on purpose. The lines running
     AWAY are the perspective: they converge, and convergence is what says
     floor. The rungs across only mark distance, and at equal weight the first
     few near the horizon land close enough together to read as two or three
     bright bars laid over the page. Bright away-lines, faint rungs. */
  background-image:
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--ion) 26%, transparent) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--vio) 12%, transparent) 0 1px, transparent 1px 64px);
  transform-origin: 50% 0%;
  transform: rotateX(73deg);
  animation: fx-grid-run 6s linear infinite;
}
@keyframes fx-grid-run { to { transform: rotateX(73deg) translate3d(0, 64px, 0); } }
/* The horizon itself. One lit hairline where the plane vanishes, which is what
   tells the eye the grid is a floor and not a pattern. */
:root[data-theme="cyberpunk"] .amb .fx > i.fx1::after {
  inset: 0 0 auto; height: 1px;
  background: var(--holo);
  opacity: 0.5;
  -webkit-mask-image: linear-gradient(90deg, transparent, #fff 22%, #fff 78%, transparent);
          mask-image: linear-gradient(90deg, transparent, #fff 22%, #fff 78%, transparent);
}
:root[data-theme="cyberpunk"] .amb .fx > i.fx2 {
  inset: 0;
  opacity: 0;
  background: linear-gradient(180deg,
    transparent 0 46%,
    color-mix(in srgb, var(--ion) 44%, transparent) 46% 47.2%,
    color-mix(in srgb, var(--vio) 40%, transparent) 47.2% 48.6%,
    transparent 48.6%);
  animation: fx-glitch 13s steps(1, end) infinite;
}
@keyframes fx-glitch {
  0%, 100% { opacity: 0; transform: translate3d(0, 0, 0); }
  2% { opacity: 0.7; transform: translate3d(-2%, -22vh, 0); }
  3% { opacity: 0; }
  4% { opacity: 0.5; transform: translate3d(3%, 19vh, 0); }
  5% { opacity: 0; }
  46% { opacity: 0; }
  47% { opacity: 0.6; transform: translate3d(-1%, 6vh, 0); }
  48% { opacity: 0; }
}
/* Cyberpunk keeps a grid of its own, in the accent rather than the quiet text
   colour, and that part was right. The numbers were not: 24px cells at 11 and
   9 percent is nearly twice the default's weight at three quarters of its
   pitch, so the whole window read as graph paper with a page on top of it.
   That is also the third place this theme draws straight lines, after the
   scanlines and the floor, and three of them is one too many.
   Forty pixel cells at 5 and 4 percent. Present, not counted.
   `opacity: 1` because the shared rule turns the grid off and this is the one
   theme that wants it, rather than the one theme that forgot to say. */
:root[data-theme="cyberpunk"] .amb .grid {
  opacity: 1;
  background-image:
    linear-gradient(90deg, color-mix(in srgb, var(--ion) 5%, transparent) 0 1px, transparent 1px 40px),
    linear-gradient(0deg, color-mix(in srgb, var(--vio) 4%, transparent) 0 1px, transparent 1px 40px);
  background-size: 40px 40px;
}

/* ── aurora · NORTHERN LIGHTS ──────────────────────────────────────────────
 *
 * Three curtains, and they are the reason this theme exists.
 *
 * Each is a tall soft gradient cut into vertical striations by a repeating mask
 * and then SKEWED as it drifts. Skew is what separates an aurora from a wash:
 * the real thing leans, and the lean changes along its length. The three run at
 * eighteen, twenty three and twenty nine seconds, which never divide into each
 * other, so the composition never repeats itself exactly. */
:root[data-theme="aurora"] {
  --fx-op: 1; --fx-sweep: 0.66s; --fx-edge: 16s;
}
:root[data-theme="aurora"] .amb .fx::before {
  background-image: var(--fx-stars-far);
  background-size: var(--fx-stars-far-h) var(--fx-stars-far-h);
  -webkit-mask-image: linear-gradient(180deg, #fff, transparent 64%);
          mask-image: linear-gradient(180deg, #fff, transparent 64%);
  opacity: 0.3;
}
/**
 * The mask, and the two mistakes it has had.
 *
 * FIRST it read `#fff 0 14px, rgba(255,255,255,0.25) 14px 30px, #fff 30px 46px`:
 * flat runs with hard boundaries, which is a set of BARS. Over a curtain that
 * covers the top of the window, the eye stops seeing light and starts seeing a
 * grille. Three stops that interpolate the whole way give the same striation
 * with no edge anywhere in it, which is what a real aurora has.
 *
 * SECOND, and this is the one the softening exposed: all three curtains were
 * `inset: -18% -26% auto`, so all three were the full width of the window plus
 * a quarter again on each side. Three overlapping full-width sheets are not
 * three curtains, they are one, and once the striation stopped drawing bars
 * across it there was nothing left to give the shape away. It read as a flat
 * green field over the top third of the page.
 *
 * So each one is a RIBBON now: its own left, its own width, none of them the
 * same, and an elliptical mask that gives it a soft left and right edge as well
 * as a soft bottom. They overlap in two places and are alone in four, which is
 * the whole reason to draw three of something.
 *
 * The alpha stays where the softening pass put it, about forty percent of the
 * original. Aurora used to be the brightest theme by a distance: a slab of
 * near-opaque green under the hero with body text on top of it.
 */
:root[data-theme="aurora"] .amb .fx > i.fx1,
:root[data-theme="aurora"] .amb .fx > i.fx2,
:root[data-theme="aurora"] .amb .fx > i.fx3 {
  /* `right: auto` is not decoration: the shared `.amb .fx > i` rule pins both
     sides at -32px for the falling layers, and left + right + width is
     over-constrained, so without this the width below is silently dropped. */
  top: -16%; bottom: auto; right: auto;
  -webkit-mask-image:
    repeating-linear-gradient(96deg, rgba(255, 255, 255, 0.42) 0, #fff 22px, rgba(255, 255, 255, 0.42) 44px),
    radial-gradient(58% 74% at 50% 26%, #fff, rgba(255, 255, 255, 0.3) 62%, transparent 92%);
          mask-image:
    repeating-linear-gradient(96deg, rgba(255, 255, 255, 0.42) 0, #fff 22px, rgba(255, 255, 255, 0.42) 44px),
    radial-gradient(58% 74% at 50% 26%, #fff, rgba(255, 255, 255, 0.3) 62%, transparent 92%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
  /* Each ribbon leans from its own middle, so the lean reads as the sheet
     hanging rather than as the whole layer sliding. */
  transform-origin: 50% 0;
}
/* The green one, and the widest. Aurora is green first: the violet is the
   fringe on it, not an equal partner. */
:root[data-theme="aurora"] .amb .fx > i.fx1 {
  left: -14%; width: 66%; height: 92%;
  background: linear-gradient(168deg, color-mix(in srgb, var(--vio) 17%, transparent), transparent 76%);
  animation: fx-curtain-a 18s ease-in-out infinite;
}
/* The violet fringe, at nearly twice the alpha of the green ones and still the
   quieter of the two, because this palette's violet is a long way from white
   and loses most of its strength the moment it is mixed toward transparent. */
:root[data-theme="aurora"] .amb .fx > i.fx2 {
  left: 26%; width: 52%; height: 78%;
  background: linear-gradient(190deg, color-mix(in srgb, var(--ion) 24%, transparent), transparent 72%);
  animation: fx-curtain-b 23s ease-in-out infinite;
}
:root[data-theme="aurora"] .amb .fx > i.fx3 {
  left: 54%; width: 54%; height: 86%;
  background: linear-gradient(176deg, color-mix(in srgb, var(--vio) 15%, transparent), transparent 68%);
  animation: fx-curtain-c 29s ease-in-out infinite;
}
@keyframes fx-curtain-a {
  0%, 100% { transform: translate3d(-4%, 0, 0) skewX(-9deg) scaleY(1) scaleX(1); }
  50% { transform: translate3d(6%, -3%, 0) skewX(7deg) scaleY(1.12) scaleX(0.9); }
}
@keyframes fx-curtain-b {
  0%, 100% { transform: translate3d(5%, -2%, 0) skewX(11deg) scaleY(1.08) scaleX(0.94); }
  50% { transform: translate3d(-6%, 2%, 0) skewX(-9deg) scaleY(0.92) scaleX(1.1); }
}
@keyframes fx-curtain-c {
  0%, 100% { transform: translate3d(2%, 2%, 0) skewX(5deg) scaleY(0.94) scaleX(1.06); }
  50% { transform: translate3d(-7%, -3%, 0) skewX(-12deg) scaleY(1.16) scaleX(0.92); }
}
:root[data-theme="aurora"] .amb .grid { opacity: 0.4; }

/* ── sakura · PETAL FALL ───────────────────────────────────────────────────
 *
 * Three depths of blossom and a warm haze at the top. The sway is wider than
 * any other falling theme, twenty two pixels on the nearest layer, because a
 * petal does not fall, it is carried. Snow at the same sway would look wrong,
 * which is the difference the two themes are built on. */
:root[data-theme="sakura"] {
  --fx-op: 1; --fx-sweep: 0.9s; --fx-edge: 18s;
}
:root[data-theme="sakura"] .amb .fx::before {
  background:
    radial-gradient(58% 36% at 30% -8%, color-mix(in srgb, var(--vio) 11%, transparent), transparent 72%),
    radial-gradient(42% 28% at 90% 6%, color-mix(in srgb, var(--ion) 8%, transparent), transparent 74%);
  animation: fx-breathe-soft 20s ease-in-out infinite;
}
/* 660 / 91s = 7.25 px/s, as it was at 420/58. */
:root[data-theme="sakura"] .amb .fx > i.fx1 {
  --fx-h: var(--fx-petal-far-h); --fx-sway: 14px;
  background-image: var(--fx-petal-far);
  opacity: 0.26;
  animation: fx-fall 91s linear infinite;
}
/* 720 / 68s = 10.6 px/s, as it was at 400/38. */
:root[data-theme="sakura"] .amb .fx > i.fx2 {
  --fx-h: var(--fx-petal-near-h); --fx-sway: 22px;
  background-image: var(--fx-petal-near);
  opacity: 0.2;
  animation: fx-fall 68s linear infinite;
}
/* The nearest layer reuses the far tile enlarged, which makes bigger and
   sparser petals out of art that already exists. `--fx-h` has to be the SCALED
   height, not the tile's own: the travel is what makes the loop seamless, and
   matching it to 660px while the tile paints at 891px would step the whole
   field 231px sideways every cycle.
   The factor came down from 1.7 to 1.35 when the source tile grew, because 1.7
   of 660 is a 1122px tile, and a layer that reaches one tile past the window at
   both ends would then be nearly three thousand pixels of composited surface
   for the sake of eight petals. 891 / 34s = 26.2 px/s, as it was at 714/27. */
:root[data-theme="sakura"] .amb .fx > i.fx3 {
  --fx-h: calc(var(--fx-petal-far-h) * 1.35); --fx-sway: 30px;
  background-image: var(--fx-petal-far);
  background-size: calc(var(--fx-petal-far-h) * 1.35) calc(var(--fx-petal-far-h) * 1.35);
  opacity: 0.14;
  animation: fx-fall 34s linear infinite;
}
/* A grid under blossom is an office window. */
:root[data-theme="sakura"] .amb .grid { opacity: 0.22; }

/* ── halloween · MOON, BATS AND GROUND FOG ─────────────────────────────────
 *
 * A cold moon in the top right, a flight of bats across it every thirty four
 * seconds, two fog banks crossing each other along the floor of the window, a
 * pumpkin light that will not hold still, and sparks going up off it.
 *
 * The moon and the bats are the second pass. The first was fog, a candle glow
 * and a violet smudge in the corner, and next to a theme with a planet and a
 * black hole in it that is not a theme, it is a colour. The owner asked for
 * props, and halloween has the two most recognisable silhouettes there are.
 *
 * The moon is COLD and the candle is WARM, and that is the composition. Two
 * warm sources at opposite corners cancel out into an evenly lit page; one of
 * each gives the window a near side and a far side. It is also why the moon
 * keeps the violet the old counter glow used, rather than being the obvious
 * yellow: yellow moon plus orange candle is one light drawn twice.
 *
 * The flicker is deliberately irregular. Even flicker is a pulse, and a pulse
 * reads as electronics; a candle stutters, recovers, and stutters again. */
:root[data-theme="halloween"] {
  --fx-op: 1; --fx-sweep: 0.5s; --fx-edge: 9s; --amb-floor: 0.48;
}
:root[data-theme="halloween"] .amb .fx::before {
  background: radial-gradient(44% 34% at 26% -2%, color-mix(in srgb, var(--vio) 26%, transparent), transparent 68%);
  animation: fx-flicker 5.5s ease-in-out infinite;
}
@keyframes fx-flicker {
  0%, 100% { opacity: 1; }
  8% { opacity: 0.72; }
  12% { opacity: 0.96; }
  20% { opacity: 0.66; }
  24% { opacity: 1; }
  58% { opacity: 0.88; }
  62% { opacity: 0.6; }
  68% { opacity: 0.98; }
  84% { opacity: 0.76; }
}
/**
 * The flight.
 *
 * One box of six bats, no repeat, off the left edge for most of the cycle. A
 * tiled bat field slides identical rows past forever and reads as wallpaper,
 * which is why the tile is a flock drawn once rather than a distribution.
 *
 * It is visible for about sixty percent of the thirty four seconds and holds at
 * the far side, invisible, for the rest, so the jump back to the start happens
 * at zero opacity and cannot be seen. It also rises as it crosses, because a
 * flock leaving is going somewhere.
 */
:root[data-theme="halloween"] .amb .fx::after {
  top: 12%; left: 0; right: auto; bottom: auto;
  width: 48vmin; height: calc(48vmin * 210 / 520);
  background: var(--fx-bat-flock) no-repeat center / contain;
  opacity: 0;
  animation: fx-bat-flight 34s linear infinite;
}
@keyframes fx-bat-flight {
  0% { opacity: 0; transform: translate3d(-42vw, 9vh, 0); }
  7% { opacity: 0.78; }
  50% { opacity: 0.78; }
  60% { opacity: 0; transform: translate3d(116vw, -7vh, 0); }
  100% { opacity: 0; transform: translate3d(116vw, -7vh, 0); }
}
/**
 * The moon.
 *
 * A disc, three craters and a terminator, in that order, and the terminator is
 * the one that matters. A circle filled with an even pale colour is a hole
 * punched in the page; an inset shadow down the lower right makes it a sphere
 * lit from somewhere.
 *
 * The craters have been moved twice. The first arrangement put two small ones
 * side by side across the top and a bigger one centred below them, and a moon
 * with two eyes and a mouth is not a subtle prop, it is a smiley. The second
 * kept one high left, one high right and the big one low centre, which is the
 * same face slightly rearranged: what makes a face is TWO ABOVE AND ONE BELOW,
 * not the exact coordinates. So they are clustered now, a big one and a small
 * one together down on the left limb with a single small one away up on the
 * right, which is a grouping a face cannot be read into.
 *
 * The halo is two box shadows rather than one. A single ring is a glow effect;
 * a tight bright one inside a very wide faint one is what light does in air.
 * The whole thing sits at 62 percent, because at 82 it was the brightest object
 * on any page on the site and the page is the point.
 */
:root[data-theme="halloween"] .amb .fx > i.fx1 {
  inset: 15% 11% auto auto; left: auto; bottom: auto;
  width: 15vmin; height: 15vmin;
  border-radius: 50%;
  background:
    radial-gradient(circle at 27% 67%, rgba(70, 82, 116, 0.3) 0 10%, transparent 13%),
    radial-gradient(circle at 39% 77%, rgba(70, 82, 116, 0.22) 0 6%, transparent 8.5%),
    radial-gradient(circle at 73% 27%, rgba(70, 82, 116, 0.16) 0 4%, transparent 6%),
    radial-gradient(circle at 38% 33%, #d3ddf2 0 30%, #b4c0da 60%, #7d8aac 100%);
  box-shadow:
    inset -1.1vmin -1.3vmin 3vmin -0.4vmin rgba(8, 6, 18, 0.7),
    0 0 4vmin 0.6vmin rgba(214, 224, 250, 0.17),
    0 0 20vmin 5vmin color-mix(in srgb, var(--ion) 13%, transparent);
  opacity: 0.62;
  animation: fx-breathe-soft 24s ease-in-out infinite;
}
/* Ground mist, so the masses are WIDE and FLAT. The first pass used pools that
   were nearly as tall as they were broad, which is cloud rather than fog, and
   at nine percent grey over a warm brown ground it read as nothing at all.
   Both banks live on `i.fx2` now, the second on its pseudo, because `i.fx1` is
   the moon. The pseudo carries its own sway on top of its parent's, so the two
   still drift against each other rather than as one sheet. */
:root[data-theme="halloween"] .amb .fx > i.fx2 {
  inset: auto -22% 0; height: 52%;
  -webkit-mask-image: linear-gradient(180deg, transparent, #fff 58%);
          mask-image: linear-gradient(180deg, transparent, #fff 58%);
  background:
    radial-gradient(62% 26% at 22% 92%, rgba(212, 202, 232, 0.17), transparent 74%),
    radial-gradient(48% 20% at 68% 100%, rgba(212, 202, 232, 0.13), transparent 76%);
  animation: fx-sway-wide 34s ease-in-out infinite;
}
:root[data-theme="halloween"] .amb .fx > i.fx2::before {
  background:
    radial-gradient(70% 22% at 80% 96%, color-mix(in srgb, var(--vio) 14%, transparent), transparent 76%),
    radial-gradient(56% 18% at 30% 104%, rgba(212, 202, 232, 0.11), transparent 78%);
  animation: fx-sway-slow 47s ease-in-out infinite reverse;
}
/* Sparks off the candle, sparse and slow. The ember tile at a third opacity. */
:root[data-theme="halloween"] .amb .fx > i.fx3 {
  --fx-h: var(--fx-ember-far-h); --fx-sway: 16px;
  background-image: var(--fx-ember-far);
  opacity: 0.16;
  animation: fx-rise 44s linear infinite;
}
/* Fog and a hairline grid are two different rooms. */
:root[data-theme="halloween"] .amb .grid { opacity: 0; }

/* ── christmas · SNOWFALL AND FROST ────────────────────────────────────────
 *
 * Three depths of snow and frost in the two top corners. The nearest layer is
 * six armed flakes rather than dots, because at that size a dot is hail.
 *
 * The speeds were wrong twice over, and the note that used to be here argued
 * for the wrong one: it made the nearest layer the SLOWEST, on the reasoning
 * that big things fall slowly. That is terminal velocity, which is not what a
 * viewer sees. What a viewer sees is parallax, and parallax says the near
 * layer crosses the window fastest. Getting it backwards is most of why the
 * old version read as three separate drifts rather than as depth.
 *
 * It was also all one speed. A layer's apparent speed is its tile height over
 * its duration, and 300/46, 260/31 and 440/64 come to 6.5, 8.4 and 6.9 pixels
 * a second: three layers moving at the same rate, which is a flat picture with
 * three copies of itself in it. Now 13.6, 20 and 29, which is a fast near
 * layer, a slow far one, and roughly twice the snowfall the owner was looking
 * at when they asked for it to fall faster.
 */
:root[data-theme="christmas"] {
  --fx-op: 1; --fx-sweep: 0.62s; --fx-edge: 13s;
}
:root[data-theme="christmas"] .amb .fx::before {
  background:
    radial-gradient(38% 30% at -2% -4%, rgba(226, 240, 255, 0.1), transparent 72%),
    radial-gradient(34% 26% at 102% -2%, rgba(226, 240, 255, 0.08), transparent 74%);
  animation: fx-breathe-soft 26s ease-in-out infinite;
}
:root[data-theme="christmas"] .amb .fx::after {
  background: radial-gradient(52% 34% at 50% -8%, color-mix(in srgb, var(--vio) 14%, transparent), transparent 70%);
  animation: fx-breathe-soft 18s ease-in-out infinite reverse;
}
/* 300px tile / 22s = 13.6 px/s. The far one, and the slowest. */
:root[data-theme="christmas"] .amb .fx > i.fx1 {
  --fx-h: var(--fx-snow-far-h); --fx-sway: 9px;
  background-image: var(--fx-snow-far);
  opacity: 0.4;
  animation: fx-fall 22s linear infinite;
}
/* 260 / 13 = 20 px/s. */
:root[data-theme="christmas"] .amb .fx > i.fx2 {
  --fx-h: var(--fx-snow-near-h); --fx-sway: 14px;
  background-image: var(--fx-snow-near);
  opacity: 0.32;
  animation: fx-fall 13s linear infinite;
}
/* 440 / 15 = 29 px/s, and the sway is widest here because the near layer is
   the one close enough for a gust to be visible on. */
:root[data-theme="christmas"] .amb .fx > i.fx3 {
  --fx-h: var(--fx-snow-flakes-h); --fx-sway: 24px;
  background-image: var(--fx-snow-flakes);
  opacity: 0.24;
  animation: fx-fall 15s linear infinite;
}
:root[data-theme="christmas"] .amb .grid { opacity: 0.45; }

/* ── lunar · LANTERNS AND EMBERS ───────────────────────────────────────────
 *
 * A row of paper lanterns hung across the top of the window, two pools of light
 * breathing behind them, gold embers rising through both, and one slow sweep of
 * gold across the head of the document every fourteen seconds. Rising rather
 * than falling: this is the one theme about things going up.
 *
 * The lanterns are the second pass. This theme's line in the admin console has
 * always said "lanterns breathing", and for as long as it has said that the
 * lanterns were two soft radial pools with nothing hanging in them. A glow
 * where a lantern should be is a lantern that failed to load. */
:root[data-theme="lunar"] {
  --fx-op: 1; --fx-sweep: 0.58s; --fx-edge: 11s;
}
:root[data-theme="lunar"] .amb .fx::before {
  background:
    radial-gradient(30% 26% at 16% 6%, color-mix(in srgb, var(--vio) 24%, transparent), transparent 70%),
    radial-gradient(26% 22% at 84% 12%, color-mix(in srgb, var(--vio) 18%, transparent), transparent 72%);
  animation: fx-breathe-soft 13s ease-in-out infinite;
}
:root[data-theme="lunar"] .amb .fx::after {
  background: linear-gradient(96deg, transparent 38%,
    color-mix(in srgb, var(--ion) 12%, transparent) 50%, transparent 62%);
  -webkit-mask-image: linear-gradient(180deg, #fff, transparent 48%);
          mask-image: linear-gradient(180deg, #fff, transparent 48%);
  opacity: 0;
  animation: fx-scan 14s var(--out) infinite;
}
:root[data-theme="lunar"] .amb .fx > i.fx1 {
  --fx-h: var(--fx-ember-far-h); --fx-sway: 13px;
  background-image: var(--fx-ember-far);
  opacity: 0.42;
  animation: fx-rise 50s linear infinite;
}
:root[data-theme="lunar"] .amb .fx > i.fx2 {
  --fx-h: var(--fx-ember-near-h); --fx-sway: 19px;
  background-image: var(--fx-ember-near);
  opacity: 0.3;
  animation: fx-rise 33s linear infinite;
}
/**
 * The row, painted at its own size and never scaled.
 *
 * The tile is 1800px wide with six lanterns in it, every one of them well
 * inside its own slot, so on any window narrower than that there is no repeat
 * at all and on a wider one the join lands in empty sky between two lanterns.
 * Scaling it to a percentage of the window would have moved the repeat to
 * wherever the arithmetic happened to put it, which is how a seam ends up
 * halfway across a laptop screen and nowhere else.
 *
 * The sway is a rotation about a point well above the window, which is where
 * the cords would meet if they were long enough. That is the difference between
 * a hanging row and a sliding one: a rotation lifts one end of the row as it
 * drops the other, and a translate moves the whole thing sideways like a
 * curtain rail. Half a degree is enough to see and small enough that nothing
 * looks like it is about to come down.
 */
:root[data-theme="lunar"] .amb .fx > i.fx3 {
  inset: 0 0 auto; left: 0; right: 0; bottom: auto;
  height: var(--fx-lantern-row-h);
  background: var(--fx-lantern-row) repeat-x center top;
  opacity: 0.42;
  transform-origin: 50% -140%;
  animation: fx-lantern-sway 21s ease-in-out infinite;
}
@keyframes fx-lantern-sway {
  0%, 100% { transform: rotate(-0.55deg); }
  50% { transform: rotate(0.55deg); }
}
:root[data-theme="lunar"] .amb .grid { opacity: 0.55; }

/* ── galaxy · DEEP FIELD ───────────────────────────────────────────────────
 *
 * A nebula, a planet, a black hole, a distant sun, two shooting stars, and
 * three star densities that TWINKLE AND DO NOT MOVE.
 *
 * That last part is the correction. The first version drifted all three star
 * fields downward, slowly, on `fx-fall`. Slow was not slow enough: the owner
 * looked at it and saw snow, and they were right, because a field of small
 * pale dots descending is snow no matter what the palette says. Stars are
 * fixed. What changes is their brightness. So the drift is gone entirely and
 * the layers only breathe, on timers that do not divide into each other.
 *
 * Their keyframes rest at exactly the opacity the base rule sets, so with
 * reduced motion asked for the field is still a field, it has simply stopped
 * blinking.
 *
 * The objects are stacked with an explicit z-index, which is the only way this
 * works: `.fx > i` are siblings, so without it the near star field paints on
 * top of the planet, and stars in front of a planet is a picture of nothing.
 */
:root[data-theme="galaxy"] {
  --fx-op: 1; --fx-sweep: 0.72s; --fx-edge: 17s;
}
/* Three nebula masses now, not two, and the third is behind the planet so the
   planet has something to be silhouetted against. */
:root[data-theme="galaxy"] .amb .fx::before {
  background:
    radial-gradient(46% 40% at 22% 8%, color-mix(in srgb, var(--vio) 22%, transparent), transparent 70%),
    radial-gradient(38% 34% at 84% 26%, color-mix(in srgb, var(--ion) 13%, transparent), transparent 72%),
    radial-gradient(54% 40% at 88% 96%, color-mix(in srgb, var(--vio) 16%, transparent), transparent 74%);
  animation: fx-nebula 68s ease-in-out infinite;
}
@keyframes fx-nebula {
  0%, 100% { transform: scale(1) translate3d(0, 0, 0); }
  50% { transform: scale(1.12) translate3d(-2%, 1%, 0); }
}
/* Starfall, one of two. They run on 17 and 23 seconds, which do not divide
   into each other, so they cross the window on their own schedules and about
   once every six and a half minutes they happen to fall together. */
:root[data-theme="galaxy"] .amb .fx::after {
  top: 9%; left: -14%; right: auto; bottom: auto;
  width: 18%; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.9));
  opacity: 0;
  animation: fx-shoot 17s linear infinite;
}
@keyframes fx-shoot {
  0%, 93% { opacity: 0; transform: translate3d(0, 0, 0) rotate(15deg); }
  94% { opacity: 0.95; }
  100% { opacity: 0; transform: translate3d(128vw, 32vh, 0) rotate(15deg); }
}
/* The star fields no longer travel, so the layers no longer need to be bigger
   than the window. */
:root[data-theme="galaxy"] .amb .fx > i.fx1,
:root[data-theme="galaxy"] .amb .fx > i.fx2,
:root[data-theme="galaxy"] .amb .fx > i.fx3 { inset: 0; }

:root[data-theme="galaxy"] .amb .fx > i.fx1 {
  z-index: 3;
  background-image: var(--fx-stars-far);
  background-size: var(--fx-stars-far-h) var(--fx-stars-far-h);
  opacity: 0.55;
  animation: fx-tw-far 7s ease-in-out infinite;
}
@keyframes fx-tw-far { 0%, 100% { opacity: 0.55; } 50% { opacity: 0.3; } }
/**
 * The planet.
 *
 * One circle and one gradient offset inside it. The gradient is what makes it
 * a sphere: light at 30 percent across and 24 percent down, falling to almost
 * the page ground by 70 percent, which is a terminator. A flat disc with an
 * even fill would read as a hole.
 *
 * It sits mostly off the bottom right corner because a planet that fits in the
 * window is a moon in a stock illustration, and a limb running off the edge is
 * the thing that gives the window a scale.
 */
:root[data-theme="galaxy"] .amb .fx > i.fx1::before {
  inset: auto -14vmin -20vmin auto;
  width: 58vmin; height: 58vmin;
  border-radius: 50%;
  background: radial-gradient(circle at 30% 24%,
    color-mix(in srgb, var(--vio) 26%, transparent) 0 14%,
    color-mix(in srgb, var(--vio) 11%, transparent) 40%,
    rgba(3, 8, 18, 0.82) 68%,
    rgba(1, 5, 12, 0.94) 100%);
  box-shadow:
    inset 8px 8px 40px -10px color-mix(in srgb, var(--ion) 20%, transparent),
    0 0 18vmin 2vmin color-mix(in srgb, var(--vio) 7%, transparent);
  animation: fx-breathe-soft 34s ease-in-out infinite;
}
/* Starfall, two of two: lower, shallower and slower than the first. */
:root[data-theme="galaxy"] .amb .fx > i.fx1::after {
  top: 34%; left: -16%; right: auto; bottom: auto;
  width: 14%; height: 1px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--ion) 85%, #fff));
  opacity: 0;
  animation: fx-shoot-b 23s linear infinite;
}
@keyframes fx-shoot-b {
  0%, 95% { opacity: 0; transform: translate3d(0, 0, 0) rotate(7deg); }
  96% { opacity: 0.8; }
  100% { opacity: 0; transform: translate3d(132vw, 17vh, 0) rotate(7deg); }
}
/**
 * The black hole, and the trick is that the squash and the spin are on
 * different elements.
 *
 * `i.fx2` is the frame and carries a static `scaleY(0.42) rotate(-16deg)`: a
 * disc seen nearly edge on. `::before` carries a conic gradient and rotates
 * inside that frame, so the bright part of the accretion disc travels AROUND
 * the ellipse instead of the ellipse itself wobbling. Rotating a squashed
 * shape looks like a coin being flipped; rotating the light inside one looks
 * like orbiting matter, and there is no other difference between the two.
 *
 * `::after` is the shadow, and it undoes the frame's squash with
 * `scaleY(2.38)`, because 0.42 x 2.38 is 1 and the hole itself is round even
 * when its disc is edge on. It breathes on opacity only, so the counter-squash
 * survives the animation.
 */
/**
 * Where it sits, which took three goes.
 *
 * At `left: -8vmin` the near arm of the ring was cut off by the window edge and
 * the whole thing read as an effect that had failed to load. Pulled in to
 * `4vmin` and up at 27 percent the ring was complete and the hole was directly
 * behind the hero's trust row: a hard black disc under body text, which reads
 * as a rendering fault even though light text on a darker ground is MORE
 * legible, not less.
 *
 * So it is in the bottom left corner and half of it is off the bottom, exactly
 * like the planet in the opposite corner. Two large objects, both cropped by
 * the window, both away from the reading column, and the crop is what gives
 * either of them a scale: an object that fits inside the frame is a diagram.
 */
:root[data-theme="galaxy"] .amb .fx > i.fx2 {
  inset: auto auto -13vmin -7vmin;
  width: 42vmin; height: 42vmin;
  z-index: 2;
  transform: rotate(-16deg) scaleY(0.42);
}
:root[data-theme="galaxy"] .amb .fx > i.fx2::before {
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    transparent 0deg,
    color-mix(in srgb, var(--ion) 38%, transparent) 58deg,
    color-mix(in srgb, var(--vio) 30%, transparent) 146deg,
    transparent 214deg,
    color-mix(in srgb, var(--ion) 20%, transparent) 308deg,
    transparent 360deg);
  -webkit-mask-image: radial-gradient(circle, transparent 0 37%, #fff 45%, #fff 53%, transparent 64%);
          mask-image: radial-gradient(circle, transparent 0 37%, #fff 45%, #fff 53%, transparent 64%);
  animation: fx-accrete 26s linear infinite;
}
@keyframes fx-accrete { to { transform: rotate(360deg); } }
:root[data-theme="galaxy"] .amb .fx > i.fx2::after {
  inset: 31%;
  border-radius: 50%;
  background: radial-gradient(circle, #000 0 60%, rgba(0, 0, 0, 0.55) 80%, transparent 100%);
  box-shadow: 0 0 5vmin 0.6vmin color-mix(in srgb, var(--ion) 14%, transparent);
  transform: scaleY(2.38);
  animation: fx-breathe-soft 11s ease-in-out infinite;
}

:root[data-theme="galaxy"] .amb .fx > i.fx3 {
  z-index: 1;
  background-image: var(--fx-stars-mid);
  background-size: var(--fx-stars-mid-h) var(--fx-stars-mid-h);
  opacity: 0.45;
  animation: fx-tw-mid 4.6s ease-in-out infinite;
}
@keyframes fx-tw-mid { 0%, 100% { opacity: 0.45; } 50% { opacity: 0.22; } }
:root[data-theme="galaxy"] .amb .fx > i.fx3::before {
  background-image: var(--fx-stars-near);
  background-size: var(--fx-stars-near-h) var(--fx-stars-near-h);
  opacity: 0.8;
  animation: fx-tw-near 9.3s ease-in-out infinite;
}
@keyframes fx-tw-near { 0%, 100% { opacity: 0.8; } 50% { opacity: 0.45; } }
/**
 * The distant sun, and the whole thing is a size argument.
 *
 * It started as a two vmin disc with one soft ring round it, and at that size a
 * pale circle with a soft edge is a MOON. Nothing about the colour fixes that:
 * the eye reads a resolvable disc as a nearby rock and an unresolvable point as
 * a star, and eighteen pixels across is resolvable.
 *
 * So the core is ten pixels and viciously bright, and everything else is halo:
 * a tight warm one, then two cold ones reaching a quarter of the window. That
 * is a star seen from far enough away to have a planet in the same frame, which
 * is the only thing in this composition that has to be believable, because the
 * planet next to it is what sets the scale.
 */
:root[data-theme="galaxy"] .amb .fx > i.fx3::after {
  inset: 13% auto auto 17%;
  width: 1.1vmin; height: 1.1vmin;
  border-radius: 50%;
  background: #fff;
  box-shadow:
    0 0 0.9vmin 0.22vmin rgba(255, 250, 238, 0.9),
    0 0 3.4vmin 0.7vmin rgba(255, 236, 208, 0.32),
    0 0 11vmin 2.4vmin color-mix(in srgb, var(--ion) 24%, transparent),
    0 0 26vmin 7vmin color-mix(in srgb, var(--ion) 8%, transparent);
  animation: fx-breathe-soft 8s ease-in-out infinite;
}
:root[data-theme="galaxy"] .amb .grid { opacity: 0.35; }

/* ── forest · CANOPY, TREELINE AND FIREFLIES ───────────────────────────────
 *
 * Shafts of light from the top left, two rows of pines along the bottom, a
 * green pool between them, and fireflies drifting up through the lot.
 *
 * The trees are what the theme was missing. Everything else here is weather,
 * and weather with nothing to fall on is just tinted air: the light shafts had
 * no canopy to come through and the fireflies had no wood to be in. Two rows
 * of silhouette at the bottom turn all of it into a place.
 *
 * The rows sway in OPPOSITE directions, and the near one further. That is the
 * whole parallax, and it costs one keyframe each. Both are anchored past the
 * window edges so a sway never uncovers one.
 *
 * `min(tile, vh)` on the heights with `background-size: auto 100%`, so the
 * treeline is 300 pixels of pine on a desktop and a proportionate band on a
 * phone rather than a wall. Fireflies are in FRONT of the trees, which is the
 * right way round: a firefly behind a silhouette is a firefly nobody sees.
 */
:root[data-theme="forest"] {
  --fx-op: 1; --fx-sweep: 0.82s; --fx-edge: 19s; --amb-floor: 0.4;
}
/* One set of shafts with stops that interpolate the whole way, not flat runs
   between hard edges. The old version drew a hard boundary at 46px and another
   at 74px, and a hard boundary in a light shaft is a drawn line. */
:root[data-theme="forest"] .amb .fx::before {
  background: repeating-linear-gradient(102deg,
    transparent 0, color-mix(in srgb, var(--ion) 7%, transparent) 56px, transparent 124px);
  -webkit-mask-image: radial-gradient(72% 66% at 12% -8%, #fff, transparent 76%);
          mask-image: radial-gradient(72% 66% at 12% -8%, #fff, transparent 76%);
  animation: fx-breathe 27s ease-in-out infinite;
}
:root[data-theme="forest"] .amb .fx::after {
  background: radial-gradient(66% 34% at 46% 106%, color-mix(in srgb, var(--vio) 20%, transparent), transparent 72%);
  animation: fx-breathe-soft 22s ease-in-out infinite reverse;
}
/* The treeline. `i.fx1` is only a frame; both rows are its pseudos so they can
   move independently of each other. */
:root[data-theme="forest"] .amb .fx > i.fx1 { inset: 0; z-index: 1; }
:root[data-theme="forest"] .amb .fx > i.fx1::before {
  inset: auto -7% 0 -7%;
  height: min(var(--fx-tree-far-h), 22vh);
  background-image: var(--fx-tree-far);
  background-repeat: repeat-x;
  background-position: 50% 100%;
  background-size: auto 100%;
  opacity: 0.85;
  animation: fx-breeze-far 34s ease-in-out infinite;
}
@keyframes fx-breeze-far {
  0%, 100% { transform: translate3d(-0.8%, 0, 0); }
  50% { transform: translate3d(0.8%, 0, 0); }
}
:root[data-theme="forest"] .amb .fx > i.fx1::after {
  inset: auto -9% 0 -9%;
  height: min(var(--fx-tree-near-h), 32vh);
  background-image: var(--fx-tree-near);
  background-repeat: repeat-x;
  background-position: 50% 100%;
  background-size: auto 100%;
  animation: fx-breeze-near 47s ease-in-out infinite;
}
@keyframes fx-breeze-near {
  0%, 100% { transform: translate3d(1.3%, 0, 0); }
  50% { transform: translate3d(-1.3%, 0, 0); }
}
:root[data-theme="forest"] .amb .fx > i.fx2 {
  --fx-h: var(--fx-fly-far-h); --fx-sway: 26px;
  z-index: 2;
  background-image: var(--fx-fly-far);
  opacity: 0.34;
  animation: fx-rise 74s linear infinite;
}
:root[data-theme="forest"] .amb .fx > i.fx3 {
  --fx-h: var(--fx-fly-near-h); --fx-sway: 38px;
  z-index: 3;
  background-image: var(--fx-fly-near);
  opacity: 0.3;
  animation: fx-rise 52s linear infinite, fx-tw-fly 3.4s ease-in-out infinite;
}
@keyframes fx-tw-fly { 0%, 100% { opacity: 0.3; } 50% { opacity: 0.08; } }
:root[data-theme="forest"] .amb .grid { opacity: 0.18; }

/* ── the lit moments answer the theme too ──────────────────────────────────
 *
 * The signature is behind the page. These two are in front of it, and they are
 * what makes the theme felt rather than seen: the sweep across the primary
 * control, and the light along the top of a subject panel. Both were fixed
 * durations before, so every theme's buttons moved at exactly the same speed
 * whatever the page looked like. Cyberpunk now snaps at 0.34s and Sakura eases
 * at 0.9s, off one token each, with no extra element anywhere.
 */
.btn-p::after { transition-duration: var(--fx-sweep); }

/**
 * The panel hairline stops being a line and becomes a light travelling along
 * one.
 *
 * The gradient is sized to a third of the panel and walked across it, which
 * animates `background-position` rather than a transform. That is normally the
 * wrong choice and here it is the right one: the element is ONE PIXEL TALL, so
 * the repaint is a few hundred pixels and never touches layout, and doing it
 * with a transform would need a second element inside a panel that already
 * clips its own overflow.
 *
 * The resting position is CENTRED and not the keyframe's starting one. An
 * animation reverts to the base rule when it is switched off, so parking the
 * light at -40% would have left every lit panel with its top light entirely
 * off the left edge for anybody with reduced motion turned on: the one signal
 * that says "this panel is the subject of the section", gone, for the readers
 * least able to spot that it was missing. The keyframes name their own 0%, so
 * the base value costs the animation nothing.
 *
 * `:root[data-theme]` and not a bare rule, because `original` is applied by
 * REMOVING the attribute and the default is not supposed to have gained a
 * moving light. A page with no theme keeps the static hairline declared with
 * the panel itself, which is what it looked like before any of this.
 */
:root[data-theme] .panel.lit::before {
  background: var(--holo);
  background-size: 34% 100%;
  background-repeat: no-repeat;
  background-position: 50% 0;
  animation: fx-edge var(--fx-edge) var(--out) infinite;
}
@keyframes fx-edge {
  0% { background-position: -40% 0; opacity: 0.2; }
  22%, 78% { opacity: 0.85; }
  100% { background-position: 140% 0; opacity: 0.2; }
}

/* ── less of it on a small screen ──────────────────────────────────────────
 *
 * `:root[data-theme]` and not `:root`, because a theme sets `--fx-op` at two
 * classes of specificity and a bare `:root` in a media query does not beat it.
 * That is the whole reason this is written the long way. */
@media (max-width: 780px) {
  :root, :root[data-theme] { --fx-op: 0.66; }
  .amb .fx > i.fx3 { display: none; }
}
@media (max-width: 560px) {
  :root, :root[data-theme] { --fx-op: 0.5; }
  .amb .fx > i.fx2 { display: none; }
}

/* ── and none of it for anybody who asked for less ─────────────────────────
 *
 * The blanket rule near the top of this sheet already collapses every animation
 * to one 0.001s iteration, which is what the browser walk in
 * tests/design-motion.test.mjs measures. This says it again for the signature
 * layer specifically, because "collapsed to 0.001s" and "switched off" are not
 * the same claim, and the one worth making about a decorative layer is the
 * second one. Every composition above is still fully drawn; it has simply
 * stopped moving. */
@media (prefers-reduced-motion: reduce) {
  .amb .fx, .amb .fx::before, .amb .fx::after,
  .amb .fx > i, .amb .fx > i::before, .amb .fx > i::after {
    animation: none !important;
  }
}

/* ══ TOUCH ════════════════════════════════════════════════════════════════
 *
 * Two things that are only wrong on a phone, and both were invisible from a
 * desktop because a desktop is where the design was checked.
 *
 * ## Inputs, and the iOS zoom
 *
 * Safari on iOS ZOOMS THE WHOLE PAGE when a text field with a font smaller
 * than 16px receives focus, and it does not zoom back out afterwards. Every
 * field in this design is 13 or 13.5px, so tapping the redeem box, the sign-in
 * box or the coupon box left the reader on a page they then had to pinch back
 * into shape, mid-task, on the two flows that matter most.
 *
 * Sixteen pixels on a coarse pointer only. The desktop typography is unchanged,
 * because the problem is not the size, it is the size on a touch device.
 *
 * ## Tap targets
 *
 * The burger is 38px, which is the control that opens the navigation on every
 * phone. Apple asks for 44 and Google for 48. Four pixels sounds like nothing
 * and it is the difference between hitting it and hitting the header.
 */
@media (pointer: coarse) {
  .f input, .field input, .paycoupon input, input[type="text"], input[type="email"],
  input[type="password"], input[type="search"], textarea, select {
    font-size: 16px;
  }
  /* 44px, and grown with padding rather than height so the box the eye sees
     stays the size it was designed at. */
  .burger { width: 44px; height: 44px; }
  /* The demo switches on a script page are 38x21. The switch stays that size;
     the area that answers a thumb does not. */
  .sw { position: relative; }
  .sw::before {
    content: ""; position: absolute; inset: -12px -8px; border-radius: inherit;
  }
  /* Copy and reset get the same treatment: the control keeps its drawn size and
     the touch area extends past it. */
  .lic-copy, .rst, .del { min-height: 40px; }
}

/* ── waiting for the payment gateway ──────────────────────────────────────
 *
 * Between the press and the redirect there is a bot-check that may draw a
 * challenge, a checkout session to create on somebody else's server, and then a
 * navigation. On a slow connection that is several seconds, and this is the
 * moment a person has just decided to spend money: a dialog that looks inert
 * here is a dialog somebody presses twice.
 *
 * checkout.js sets two attributes and nothing else. `data-busy` on the dialog,
 * `aria-busy` on the row that was actually chosen. Everything below is the
 * shape of that, so it can change without touching the file that decides what
 * gets charged.
 *
 * The chosen row is the only thing that stays LIT. `[disabled]` already dims
 * every row to 0.45, which is right for the two that were not chosen and wrong
 * for the one that was: it made the choice look refused rather than accepted.
 */
.paypick[data-busy] .paymethod[disabled] { opacity: 0.3; }
.paypick[data-busy] .paymethod[aria-busy="true"] {
  opacity: 1;
  background: color-mix(in srgb, var(--vio) 7%, transparent);
}
.paypick[data-busy] .paymethod[aria-busy="true"]::before { transform: scaleY(1); }
.paypick[data-busy] .paymethod[aria-busy="true"] > .ic {
  color: var(--ink); border-color: var(--hair-lit);
  box-shadow: inset 0 1px 0 var(--rim), 0 0 26px -10px var(--vio);
}
/* A ring that turns AROUND the mark rather than replacing it: the icon is what
   tells you which route is being opened, and swapping it for a spinner takes
   that away at the one moment it is being confirmed.
   Drawn on the row and positioned over the tile, not on the tile itself: `.ic`
   is an <svg>, and an SVG element renders no pseudo-elements. The tile is 40px
   and sits inside the row's 24px of padding. */
.paypick[data-busy] .paymethod[aria-busy="true"]::after {
  content: ""; position: absolute; left: calc(var(--s6) - 6px); top: 50%;
  width: 52px; height: 52px; margin-top: -26px;
  /* A circle, not the tile's 4px radius. A near-square outline with one lit
     edge does not read as turning, it reads as a square being rotated. */
  border-radius: 50%;
  border: 1.5px solid color-mix(in srgb, var(--vio) 16%, transparent);
  border-top-color: var(--vio-txt);
  animation: pay-spin 0.72s linear infinite; pointer-events: none;
}
@keyframes pay-spin { to { transform: rotate(360deg); } }

/* The line under the rows carries the same news, so it stops looking like the
   fine print it sits next to. */
.paypick[data-busy] .paymsg { color: var(--ink); }

/* The word on the chosen row becomes what is happening. Its width is held by
   the longest of the three so the row cannot reflow underneath the pointer. */
.paypick[data-busy] .paymethod[aria-busy="true"] .paygo { color: var(--ink); }
.paypick[data-busy] .paymethod[aria-busy="true"] .paygo::after {
  content: ""; width: 4px; height: 4px; border-radius: 50%; margin-left: 2px;
  background: currentColor; animation: pay-pulse 1.1s ease-in-out infinite;
}
@keyframes pay-pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }

/* And one line of progress across the whole dialog, because the wait belongs to
   the dialog rather than to the row: it covers the bot-check too, which draws
   its own overlay on top of this one. Indeterminate on purpose. There is no
   honest percentage to show for somebody else's server. */
.paypick[data-busy] .paydialog::after {
  content: ""; position: absolute; inset: -1px -1px auto; height: 2px; z-index: 6;
  background: var(--holo); border-radius: 2px;
  animation: pay-sweep 1.15s var(--out) infinite;
}
@keyframes pay-sweep {
  0%   { transform: translateX(-100%) scaleX(0.35); opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translateX(100%) scaleX(0.35); opacity: 0; }
}

/* Nothing here is load-bearing, so a reader who asked for stillness gets the
   state without the movement. The row is still lit and still says what it is
   doing; only the three things that spin, sweep and pulse stop. */
@media (prefers-reduced-motion: reduce) {
  .paypick[data-busy] .paymethod[aria-busy="true"]::after,
  .paypick[data-busy] .paymethod[aria-busy="true"] .paygo::after,
  .paypick[data-busy] .paydialog::after { animation: none; }
  .paypick[data-busy] .paydialog::after { transform: none; opacity: 1; }
}

/* The free-outfit offer inside the Robux quote. A real checkbox, because it is
   a real choice and a styled div that behaves like one is a control a keyboard
   cannot reach. */
.rqmerch {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s3);
  align-items: start; margin: 0 var(--s6) var(--s5); padding: var(--s4);
  border-radius: var(--r2); cursor: pointer;
  background: linear-gradient(180deg, color-mix(in srgb, var(--vio) 10%, transparent), transparent);
  border: 1px solid color-mix(in srgb, var(--vio) 24%, transparent);
  transition: border-color var(--tint) ease, background-color var(--tint) ease;
}
.rqmerch:hover { border-color: var(--hair-lit); }
/* 16px so it is a comfortable target, and accent-color rather than a hand-drawn
   box: the native control already knows how to be focused, checked and read out,
   and every reimplementation of one loses at least one of those. */
.rqmerch input {
  width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--vio); cursor: pointer;
}
.rqmerch b { display: block; font: 600 13.5px/1.35 var(--disp); }
.rqmerch small { display: block; margin-top: 4px; color: var(--mute); font-size: 12.5px; line-height: 1.6; }
/* The decline, said out loud. It is not a second control: it is the label for
   the state the box is already in when it is empty, so nobody has to work out
   what unticking it means. */
.rqmerch-off {
  display: none; margin-top: 6px; text-decoration: none;
  font: 400 11px/1 var(--disp); letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint);
}
.rqmerch:has(input:not(:checked)) { border-color: var(--hair); background: none; }
.rqmerch:has(input:not(:checked)) .rqmerch-off { display: block; }
.rqmerch:has(input:not(:checked)) b { color: var(--mute); }

/* Pick one of the two. Radios, laid out as a row of chips that wrap, because
   the labels are Roblox item names and one of them is three words long. */
.rqmerch-one { display: block; }
.rqmerch-one > b { display: block; font: 600 13.5px/1.35 var(--disp); }
.rqmerch-one > small { display: block; margin-top: 4px; color: var(--mute); font-size: 12.5px; line-height: 1.6; }
.rqpicks { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); }
.rqpick {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  padding: 8px 12px; border-radius: var(--r1); font-size: 12.5px;
  border: 1px solid var(--hair); background: var(--sunk); color: var(--mute);
  transition: border-color var(--tint) ease, color var(--tint) ease, background-color var(--tint) ease;
}
.rqpick input { width: 14px; height: 14px; margin: 0; accent-color: var(--vio); cursor: pointer; }
.rqpick:hover { border-color: var(--hair-lit); color: var(--ink); }
.rqpick:has(input:checked) {
  border-color: var(--vio); color: var(--ink);
  background: color-mix(in srgb, var(--vio) 12%, transparent);
}

/* The suits, announced. Not a control, because there is nothing to decide: it
   comes with the plan. What it has to carry instead is the fact that it is not
   in anybody's hands yet, which is why the tag reads before the name does. */
.rqsoon { grid-template-columns: auto minmax(0, 1fr); align-items: center; cursor: default; }
.rqsoon:hover { border-color: color-mix(in srgb, var(--vio) 24%, transparent); }
.rqsoon-tag {
  align-self: start; padding: 4px 9px; border-radius: var(--r1); white-space: nowrap;
  font: 600 9px/1.4 var(--disp); letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 34%, transparent);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
}
.rqsoon b { display: block; font: 600 13.5px/1.35 var(--disp); }
.rqsoon small { display: block; margin-top: 4px; color: var(--mute); font-size: 12.5px; line-height: 1.6; }


/* ══ THE CLOTHING, PROMOTED ON THE HOME PAGE ══════════════════════════════
 *
 * One band, and it earns its place by being the only thing on that page you can
 * have without paying us anything.
 *
 * The two garments share ONE lit panel rather than getting a frame each. They
 * are two halves of an outfit and the point of the band is that they go
 * together; two frames side by side would say the opposite. The same light
 * studio as the shop, for the same reason: black clothes need it.
 */
.wearband {
  /* The picture takes the width it needs and no more. A fractional column gave
     it half the band and the picture is height-capped, so the other half of that
     half was empty and the words started a long way from the thing they are
     about. */
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s8);
  align-items: center; padding: var(--s6); border-radius: var(--r4); overflow: hidden;
  background:
    radial-gradient(80% 130% at 6% 50%, color-mix(in srgb, var(--vio) 15%, transparent), transparent 62%),
    linear-gradient(180deg, var(--raise), var(--sunk));
  border: 1px solid var(--hair); box-shadow: inset 0 1px 0 var(--rim), var(--shadow);
  transition: border-color var(--tint) ease, box-shadow var(--move) var(--out);
}
.wearband:hover { border-color: var(--hair-lit); box-shadow: inset 0 1px 0 var(--rim-hi), var(--shadow-lift); }
.wearband h3 { margin: 0 0 var(--s2); font-size: clamp(20px, 2.4vw, 26px); }
.wearband p { margin: 0 0 var(--s5); color: var(--mute); font-size: 14px; line-height: 1.68; max-width: 48ch; }
.wearband .cta { display: flex; gap: var(--s3); flex-wrap: wrap; }

/* One picture, and it is a photograph rather than a cut-out, so it needs no
   studio behind it: the scene is already lit and already dark enough to sit on
   this page. What the frame adds is a violet wash at the bottom edge, which is
   what ties a screenshot of somebody else's room to the ground the rest of the
   site stands on. */
.wearshot {
  position: relative; overflow: hidden; border-radius: var(--r2);
  aspect-ratio: 4 / 5; max-height: 340px; justify-self: start;
  border: 1px solid var(--hair); background: var(--sunk);
  box-shadow: inset 0 1px 0 var(--rim), var(--shadow);
}
.wearshot img { width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform var(--slow) var(--out); }
.wearshot::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 58%, color-mix(in srgb, var(--deep) 55%, transparent));
  transition: opacity var(--move) ease;
}
.wearband:hover .wearshot img { transform: scale(1.045); }
.wearband:hover .wearshot::after { opacity: 0.35; }

@media (max-width: 900px) {
  .wearband { grid-template-columns: 1fr; gap: var(--s5); }
  .wearband p { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .wearband:hover .wearshot img { transform: none; }
}

/* ══ THE CLOTHING ═════════════════════════════════════════════════════════
 *
 * Two kinds of picture, doing two different jobs.
 *
 * The lookbook is three screenshots of the outfit being worn in a game at
 * night. It answers "why would I want this", which a catalogue render cannot:
 * Roblox photographs a garment on a bare mannequin, head-on, and the print on
 * the sleeve does not appear in it at all.
 *
 * The two product shots are those renders, and they answer "what exactly am I
 * buying", which the screenshots cannot: in a lit scene you cannot tell where
 * the hoodie ends and the trousers begin.
 *
 * They are sized against each other deliberately. The lookbook is the picture
 * and gets the room; the products are a listing and get a strip each. Three
 * portraits at a third of the page would each be four hundred pixels tall and
 * the section would be the length of the shop above it.
 *
 * Nothing here posts anywhere. Roblox sells these and the buttons say so.
 */
.wearlead {
  display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  gap: var(--s6); align-items: center; margin-bottom: var(--s5);
}
@media (max-width: 860px) { .wearlead { grid-template-columns: 1fr; gap: var(--s5); } }

.lookbook { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s3); }
.lookbook figure {
  position: relative; margin: 0; overflow: hidden; border-radius: var(--r2);
  aspect-ratio: 4 / 5; min-width: 0;
  border: 1px solid var(--hair); background: var(--sunk);
  box-shadow: inset 0 1px 0 var(--rim), var(--shadow);
  transition: transform var(--move) var(--out), border-color var(--tint) ease, box-shadow var(--move) var(--out);
}
.lookbook img { width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform var(--slow) var(--out); }
/* The scene is already dark, so nothing is added to it. What the wash does is
   stop three photographs of three different rooms from fighting the page: a
   thin violet tint at the bottom of each ties them to the ground everything
   else sits on. */
.lookbook figure::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 55%, color-mix(in srgb, var(--deep) 55%, transparent));
  transition: opacity var(--move) ease;
}
.lookbook figure:hover {
  transform: translateY(-3px); border-color: var(--hair-lit);
  box-shadow: inset 0 1px 0 var(--rim-hi), var(--shadow-lift), 0 0 60px -34px var(--vio);
}
.lookbook figure:hover img { transform: scale(1.05); }
.lookbook figure:hover::after { opacity: 0.35; }
/* The middle one sits a little proud, which is what stops three equal rectangles
   reading as a contact sheet. Only where there is room for it to matter. */
@media (min-width: 861px) {
  .lookbook figure:nth-child(2) { transform: translateY(-10px); }
  .lookbook figure:nth-child(2):hover { transform: translateY(-14px); }
}

.wearsay h3 { margin: 0 0 var(--s3); font-size: clamp(19px, 2.1vw, 23px); }
.wearsay p { margin: 0 0 var(--s3); color: var(--mute); font-size: 14px; line-height: 1.68; max-width: 44ch; }
.wearsay .fineprint { margin: 0; }

/* ── the two garments ─────────────────────────────────────────────────────
 *
 * Along, not down. Each is one picture, one name, one price and one way out,
 * and a tall card for that is a great deal of frame around very little.
 *
 * The picture keeps its own LIGHT frame, and that is a decision. Both garments
 * are black, Roblox will only render them on a pale mannequin, and black on
 * this site's violet-black ground is a dark shape on a dark shape: the eye
 * finds the white head first and the product last. Every clothing shop shoots
 * black on a light backdrop for that reason. It also separates the two kinds of
 * picture at a glance: lit scene means worn, lit panel means the item.
 */
.wearrow { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); }
@media (max-width: 720px) { .wearrow { grid-template-columns: 1fr; } }

.mitem {
  position: relative; isolation: isolate;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--s4);
  align-items: center; padding: var(--s4); border-radius: var(--r4); min-width: 0;
  background: linear-gradient(180deg, var(--raise), var(--surf) 45%, var(--sunk));
  border: 1px solid var(--hair);
  box-shadow: inset 0 1px 0 var(--rim), var(--shadow);
  transition: transform var(--move) var(--out), border-color var(--tint) ease, box-shadow var(--move) var(--out);
}
.mitem:hover {
  transform: translateY(-3px); border-color: var(--hair-lit);
  box-shadow: inset 0 1px 0 var(--rim-hi), var(--shadow-lift), 0 0 70px -34px var(--vio);
}
@media (max-width: 460px) {
  .mitem { grid-template-columns: auto minmax(0, 1fr); }
  .mitem > .btn { grid-column: 1 / -1; }
}

.mshot {
  position: relative; display: grid; place-items: center; overflow: hidden;
  width: 96px; height: 96px; border-radius: var(--r2); flex: none;
  background: radial-gradient(78% 68% at 50% 24%, #f6f4fc, #d7d1ea 62%, #b9b2d4);
  border: 1px solid color-mix(in srgb, var(--vio) 22%, transparent);
  box-shadow: inset 0 -18px 30px -22px rgba(40, 26, 84, 0.5);
}
.mshot img {
  width: 88%; height: 88%; object-fit: contain;
  filter: drop-shadow(0 4px 6px rgba(38, 26, 76, 0.24)) drop-shadow(0 12px 16px rgba(38, 26, 76, 0.28));
  transition: transform var(--slow) var(--out);
}
.mitem:hover .mshot img { transform: scale(1.07) translateY(-2px); }

/* The light in the panel follows the pointer. `--cx` and `--cy` are written by
   uv.js from one delegated listener; the fallback centres it, so a page whose
   script never ran gets a lit panel rather than a flat one. Scoped to `.mitem`
   because that is the element the shell tracks, and the gate reads this rule to
   prove the tracking is not paid for and thrown away. */
.mitem .mshot::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(42% 46% at var(--cx, 50%) var(--cy, 26%), rgba(255, 255, 255, 0.85), transparent 70%);
  opacity: 0; transition: opacity var(--move) ease;
}
.mitem:hover .mshot::before { opacity: 1; }

/* A specular sweep, once per hover, never looping: a loop would turn a product
   shot into a carousel advert. */
.msheen {
  position: absolute; inset: -40% -60%; pointer-events: none;
  background: linear-gradient(75deg, transparent 42%, rgba(255, 255, 255, 0.55) 50%, transparent 58%);
  transform: translateX(-70%); opacity: 0;
}
.mitem:hover .msheen { animation: mshine 1.1s var(--out) 1; }
@keyframes mshine {
  0% { transform: translateX(-70%); opacity: 0; }
  18% { opacity: 1; }
  100% { transform: translateX(70%); opacity: 0; }
}

.mfoot { display: grid; gap: 3px; min-width: 0; }
.mfoot b { font: 600 14.5px/1.25 var(--disp); letter-spacing: 0.01em; }
.mkind { font: 400 9.5px/1 var(--disp); letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); }
.mprice {
  justify-self: start; margin-top: 3px;
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  padding: 5px 10px; border-radius: var(--pill);
  background: color-mix(in srgb, var(--ion) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--ion) 28%, transparent);
  font: 500 13.5px/1 var(--mono); font-variant-numeric: tabular-nums; color: var(--ink);
  transition: border-color var(--tint) ease, background-color var(--tint) ease;
}
.mitem:hover .mprice {
  background: color-mix(in srgb, var(--ion) 18%, transparent);
  border-color: color-mix(in srgb, var(--ion) 44%, transparent);
}
/* 14px in a 13.5px line: the two share a baseline box and the even size keeps
   the mark off a half pixel. */
.mprice .ic { width: 14px; height: 14px; color: var(--ion); }

/* ── what a Robux order earns ─────────────────────────────────────────────
 *
 * A band under the pair rather than a third card beside them. An offer is not
 * a product, and in the row it made the row read as three items, one of which
 * was a paragraph.
 */
.mfree {
  margin-top: var(--s4); padding: var(--s6); border-radius: var(--r4);
  background:
    radial-gradient(70% 130% at 8% 0%, color-mix(in srgb, var(--vio) 15%, transparent), transparent 60%),
    linear-gradient(180deg, var(--raise), var(--sunk));
  border: 1px solid color-mix(in srgb, var(--vio) 24%, transparent);
  box-shadow: inset 0 1px 0 var(--rim), var(--shadow);
}
.mfreehead {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--s4);
  align-items: center; margin-bottom: var(--s5);
}
.mfreehead b { display: block; font: 600 15.5px/1.3 var(--disp); }
.mfreehead p { margin: 5px 0 0; color: var(--mute); font-size: 13px; line-height: 1.65; max-width: 68ch; }
/* `.tick` is the receipt page's, reused rather than redrawn: it already means
   "this one is settled and good", which is what it means here too. */
.mfree .tick { width: 38px; height: 38px; flex: none; }
.mfree .tick .ic { width: 20px; height: 20px; }
@media (max-width: 760px) {
  .mfreehead { grid-template-columns: auto minmax(0, 1fr); }
  .mfreehead > .btn { grid-column: 1 / -1; justify-content: center; }
}

/* The six rungs, along rather than down: it is a progression, and a column of
   six lines reads as a price list. No numbers against them, because the plan
   names already carry the order. */
/* Filled rather than counted: only the rungs that exist are shown (the suit
   tiers are hidden until they are real, see shop.html), and a fixed six
   columns left two rungs in a third of the row. */
.mladder {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr)); gap: var(--s2);
}
.mladder li {
  display: grid; gap: 5px; align-content: start;
  padding: var(--s4) var(--s3); border-radius: var(--r2); min-width: 0;
  background: var(--sunk); border: 1px solid var(--hair);
  transition: border-color var(--tint) ease, transform var(--move) var(--out), background-color var(--tint) ease;
}
.mladder li:hover {
  transform: translateY(-2px); border-color: var(--hair-lit);
  background: color-mix(in srgb, var(--vio) 7%, transparent);
}
.mladder b { font: 600 12px/1.3 var(--disp); color: var(--ink); }
.mladder span { color: var(--mute); font-size: 12px; line-height: 1.45; }
.mladder i {
  justify-self: start; margin-top: 2px;
  font: 600 8.5px/1.4 var(--disp); font-style: normal; letter-spacing: 0.12em; text-transform: uppercase;
  padding: 3px 6px; border-radius: var(--r1); white-space: nowrap;
  color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
}
/* The rungs that are not yet real are visibly quieter than the two that are.
   Somebody scanning this row should see where the line falls without reading a
   tag on every one of them. */
.mladder .soon { background: none; }
.mladder .soon b { color: var(--mute); }

/* Nothing here carries meaning, so a reader who asked for stillness keeps the
   pictures, the lit panels and the ladder, and loses only the movement. */
@media (prefers-reduced-motion: reduce) {
  .lookbook figure:hover,
  .lookbook figure:nth-child(2),
  .lookbook figure:nth-child(2):hover { transform: none; }
  .lookbook figure:hover img { transform: none; }
  .mitem:hover { transform: none; }
  .mitem:hover .mshot img { transform: none; }
  .mitem:hover .msheen { animation: none; }
  .mladder li:hover { transform: none; }
}

/* On a phone three portraits across are 118px wide, which is a contact sheet
   rather than a lookbook. Two, bigger, and the third goes: the front and the
   angle are the two that show the print, and the detail shot is the one whose
   job the other two already do. */
@media (max-width: 560px) {
  .lookbook { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); }
  .lookbook figure:last-child { display: none; }
}

/* ══ THE BUY BAR ══════════════════════════════════════════════════════════
 *
 * A phone-only strip at the foot of the screen that mirrors a page's real Buy
 * control (UV.buyBar in uv.js). On the product page the buy box started about
 * 1,100 pixels down and on the shop the all-access Buy sat under the whole plan
 * breakdown; somebody who had decided had to go looking for the button.
 *
 * It rises in with a transform and leaves the same way, and `visibility` takes
 * a hidden bar out of the tab order. The page keeps a bar's height of room at
 * its foot the whole time it has one, so the bar arriving never moves anything
 * and never sits over the footer.
 */
.buybar { display: none; }
@media (max-width: 760px) {
  body[data-buybar] { padding-bottom: calc(76px + env(safe-area-inset-bottom, 0px)); }
  .buybar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
    display: flex; align-items: center; gap: var(--s3);
    padding: var(--s3) var(--s4) calc(var(--s3) + env(safe-area-inset-bottom, 0px));
    background: color-mix(in srgb, var(--raise) 94%, transparent);
    -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
    border-top: 1px solid var(--hair-2);
    box-shadow: 0 -14px 32px -22px rgba(0, 0, 0, 0.95), inset 0 1px 0 var(--rim);
    transform: none; visibility: visible;
    transition: transform var(--move) var(--out), visibility 0s;
  }
  .buybar[data-show="0"] {
    transform: translateY(calc(100% + 8px)); visibility: hidden;
    transition: transform var(--move) var(--out), visibility 0s linear var(--move);
  }
  .buybar-text { flex: 1; min-width: 0; display: grid; gap: 3px; }
  .buybar-price { font: 600 18px/1.1 var(--disp); font-variant-numeric: tabular-nums; color: var(--ink); }
  .buybar-price u { margin-left: 6px; text-decoration: none; font: 400 11.5px/1 var(--face); color: var(--mute); }
  .buybar-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--mute); font-size: 12px; }
  .buybar-go { flex: none; min-height: 44px; }
}
@media (prefers-reduced-motion: reduce) {
  .buybar, .buybar[data-show="0"] { transition: none; }
}

/* ══ CARDS ON A PHONE ═════════════════════════════════════════════════════
 *
 * Two across from a 360px screen up. One poster per row made every card a
 * 342px square: twelve games in the shop was four thousand pixels of scrolling
 * to compare prices. Two across halves it and still leaves each card a real
 * picture.
 *
 * A card that narrow keeps ONE action. The whole card already opens the game
 * (see .card .cover), so the second button ("Features", "See lengths") was a
 * second door to the same room; the Buy or Subscribe stays, full width, at the
 * size a thumb needs. Titles hold two lines and the first tag only.
 */
@media (min-width: 360px) and (max-width: 560px) {
  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); }
  .cards > .card { aspect-ratio: 3 / 4; }
  .cards > .card .tags .tag:nth-child(n+2) { display: none; }
  .cards > .card .body { padding: 0 var(--s3) var(--s3); }
  .cards > .card :is(h3, h4) {
    font-size: 15px; line-height: 1.25;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .cards > .card .plate { flex-wrap: wrap; gap: 2px 6px; padding: 5px 8px; margin-bottom: var(--s2); }
  .cards > .card .plate b { font-size: 16px; }
  .cards > .card .plate u { font-size: 8.5px; line-height: 1.25; }
  .cards > .card .acts .btn-s { display: none; }
  .cards > .sk.card-sk { aspect-ratio: 3 / 4; }
}

/* ══ A PACK ON ITS OWN ════════════════════════════════════════════════════
 *
 * With one pack in the store the grid gave it a quarter of the row and left
 * three empty tracks beside it. A lone pack spans the row, its games run in
 * columns instead of one long list, and the price and the button sit together
 * at the end of the row rather than under a column of names.
 */
.bgrid > .bundle:only-child { grid-column: 1 / -1; }
.bgrid > .bundle:only-child .bgames { grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr)); gap: var(--s2) var(--s5); }
@media (min-width: 760px) {
  .bgrid > .bundle:only-child .bfoot { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
  .bgrid > .bundle:only-child .bfoot .btn { width: auto; }
}

/* ══ THE LIBRARY COVERS, BEFORE THEY ARRIVE ═══════════════════════════════
 * uv-home.js fills them from the live catalogue, so for a moment they are
 * frames with nothing in them: shown as the same quiet pulse every other
 * loading surface uses rather than as three black holes. */
.library-cover:empty { animation: uv-sk 1.5s var(--out) infinite alternate; }
@media (prefers-reduced-motion: reduce) { .library-cover:empty { animation: none; } }

/* ── /robux: paying with Robux in the game ────────────────────────────────
 *
 * The purchase happens inside a Roblox game, so this page only links an
 * account and points at the game. The two panels are the key page's numbered
 * routes (uv-reuse.css); what is here is the plan list, the link code and the
 * linked state. */
.rbxplans { list-style: none; margin: var(--s5) 0 0; padding: 0; display: grid; gap: var(--s2); }
.rbxplans li {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  padding: var(--s3) var(--s4); border-radius: var(--r2); min-width: 0;
  background: var(--sunk); border: 1px solid var(--hair);
}
/* The plan the checkout sent somebody here for. The same pressed look as a
   chosen destination in the payment dialog, so it reads as "this one". */
.rbxplans li.on { border-color: var(--vio); box-shadow: inset 0 0 0 1px var(--vio); }
.rbxplans li > span:first-child { min-width: 0; }
.rbxplans b { display: block; font-weight: 600; font-size: 14.5px; }
.rbxplans small { display: block; margin-top: 2px; color: var(--mute); font-size: 12.5px; }
.rbxprice {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  font: 600 15px/1 var(--disp); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.rbxprice .ic { width: 16px; height: 16px; color: var(--ion); }

/* The code is read off this screen and typed into a game on another, maybe on
   another device, so it is set large, spaced, and selectable in one tap. */
.rbxcode { display: grid; gap: var(--s3); justify-items: start; margin: 0 0 var(--s4); }
.rbxcode code {
  font: 600 clamp(28px, 7vw, 38px)/1 var(--mono); letter-spacing: 0.24em;
  padding: var(--s4) var(--s5); border-radius: var(--r2);
  background: var(--sunk); border: 1px solid var(--hair-lit); color: var(--ink);
  -webkit-user-select: all; user-select: all;
}
.rbxcode p { margin: 0; color: var(--mute); font-size: 13.5px; line-height: 1.6; }
.rbxcode .rbxclock { color: var(--faint); font-size: 12.5px; font-variant-numeric: tabular-nums; }

.rbxlinked { display: flex; align-items: center; gap: var(--s4); margin: 0 0 var(--s4); }
.rbxlinked b { display: block; font-weight: 600; overflow-wrap: anywhere; }
.rbxlinked small { display: block; margin-top: 3px; color: var(--mute); font-size: 13px; line-height: 1.55; }
.rbxlinked .tick { flex: none; }
.rbxsignin { display: flex; gap: var(--s3); flex-wrap: wrap; }
.rbxclosed { max-width: 640px; }
.rbxnotes {
  margin: 0; padding: 0 0 0 1.1em; display: grid; gap: var(--s3);
  color: var(--mute); font-size: 14px; line-height: 1.7; max-width: 72ch;
}

/* ══ TYPE THAT IS NOT LATIN ═══════════════════════════════════════════════
 *
 * Two things in this sheet were drawn for Latin and read wrong in Chinese and
 * Thai.
 *
 * TRACKING. The label roles are spaced 0.1 to 0.16em because capitals need it.
 * Chinese and Thai have no capitals, and the same spacing pulls every character
 * apart into a row of separate glyphs. They keep a trace of it, so a label
 * still reads as a label.
 *
 * MONOSPACE. A handful of small lines are set in the mono face because in
 * English they are figures: "฿4.26 a day", "Updated 3 minutes ago". The mono
 * face has no Chinese or Thai, so those lines fell back glyph by glyph onto
 * whatever the system had, at mono spacing. In those two languages they are
 * sentences, and they are set as sentences.
 */
:is(:lang(zh), :lang(th)) :is(.lbl, .eyebrow, .kicker, .tierline, .plate u, .step .n, .plan .flag,
  .mkind, .routetag, .f > span, .f > label, .paylist-note span, .destlead, .rqamt > span, .stat .lbl) {
  letter-spacing: 0.03em;
}
:is(:lang(zh), :lang(th)) :is(.perday, .plan .rate, .gwmethod .gwmeta, .gwextinfo small, .gw-ext i,
  .count, .stfresh, .stdelta, .pod-share small) {
  font-family: var(--face); letter-spacing: 0;
}
