/* The shared shell: one header, one footer, one nav, one set of components.
 *
 * Loads after tokens.css and never declares a colour, a size, a duration or a
 * radius of its own. If a value is needed that is not a token, the token is
 * missing and belongs in docs/design-tokens.json, where the contrast gate can
 * see it. That rule is the only thing keeping the gate honest.
 *
 * The rules that generated the rest, from docs/design-system.md:
 *   Hierarchy comes from size, weight and light. Never from a condensed
 *   typeface, a wall of capitals, or a colour too dim to read.
 *   Colour changes faster than geometry.
 *   Nothing sits behind text as a filled rectangle.
 *   Hover never animates layout.
 */

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

/* ── moving between pages ───────────────────────────────────────────────────
 *
 * Every page here is a real document, which is the right call for a site with no
 * build step: the back button works, a URL can be sent to somebody, and nothing
 * has to reimplement routing. What it costs is the white flash between pages,
 * and that flash is most of why a multi-page site can feel cheaper than an app
 * that is doing much less.
 *
 * Cross-document view transitions remove it with two declarations. The browser
 * holds the old page's pixels, paints the new one, and cross-fades between them.
 * Where the API is not implemented, both rules are ignored and navigation is
 * what it was: this makes a good browser better and no browser worse.
 *
 * Deliberately a cross-fade and nothing else. A slide implies a spatial
 * relationship between pages that does not exist, and something that happens on
 * EVERY navigation has to be nearly invisible or it becomes the thing you notice
 * about the site. */
@view-transition { navigation: auto; }

::view-transition-old(root) { animation: gh-page-out var(--t-move) var(--ease) both; }
::view-transition-new(root) { animation: gh-page-in var(--t-enter) var(--ease) both; }
@keyframes gh-page-out { to { opacity: 0; } }
@keyframes gh-page-in { from { opacity: 0; transform: translateY(6px); } }

/* The header and footer are the same furniture on both sides of a navigation, so
   they are held still while the body changes underneath. Without this they
   cross-fade with everything else and the whole frame blinks, which is the thing
   the transition was added to stop. */
.top { view-transition-name: gh-top; }
.foot { view-transition-name: gh-foot; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* The hidden attribute has to win.
 *
 * A UA stylesheet gives [hidden] display:none at the lowest priority there is,
 * so any author rule that sets display beats it. .btn is display:inline-flex,
 * which meant every hidden button on the site was on screen: the gateway claim
 * button offered itself before a method had been chosen, and the same held for
 * every hidden pane and row elsewhere.
 *
 * !important is right here and almost nowhere else. hidden is not a style, it is
 * a statement that the element is not currently relevant, and no layout rule
 * should be able to argue with it. */
[hidden] { display: none !important; }

/* The background belongs on html as well as body: if anything ever does
   overflow, body paints only to the viewport and the rest of the scroll width
   shows through as bare black. */
html { color-scheme: dark; background: var(--bg); }

/* The footer belongs at the BOTTOM, even when the page is short.
 *
 * Without this the footer sits wherever the content ends, so the signed-out
 * account page put it two-fifths of the way down a 900px window with four
 * hundred pixels of bare background below it. That reads as a page that failed
 * to load the rest of itself, and it is the state a first-time visitor sees:
 * signed out is the default.
 *
 * A flex column with a growing main is the whole fix, and it changes nothing on
 * a page that is already taller than the window. */
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font: 400 var(--t-body)/var(--lh-body) var(--font);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-synthesis-weight: none;
}

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

h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: var(--track-display); line-height: var(--lh-display); }
h1 { font-size: var(--t-h1); text-wrap: balance; }
h2 { font-size: var(--t-h2); text-wrap: balance; }
h3 { font-size: var(--t-h3); letter-spacing: -0.015em; display: flex; align-items: center; gap: var(--s-2); }
p  { margin: 0 0 var(--s-3); }

/* The ONE micro-label role. Nothing else may be uppercase, which is what stops
   the page shouting and leaves emphasis somewhere to go. */
.label, .kicker, .field > span, .stat span, .tier {
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
.kicker { color: var(--acc-txt); }

a { color: var(--acc-txt); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* A control that reads as a link because it belongs mid-sentence, but is a
   button because it does not navigate anywhere on its own: starting a sign-in
   means fetching a URL first. Written as its own class rather than reusing `a`,
   so nobody is tempted to make the real thing an anchor again and send somebody
   to a page of JSON. Inherits everything from `a` above, and resets only what a
   <button> brings with it. */
.linklike {
  color: var(--acc-txt); background: none; border: 0; padding: 0;
  font: inherit; cursor: pointer; text-decoration: none;
}
.linklike:hover { text-decoration: underline; text-underline-offset: 3px; }
.linklike[aria-busy="true"] { cursor: progress; opacity: .7; }

/* Keyboard users depend on this one, so it is declared once and never removed. */
:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }

/* ── LIGHT 1 ────────────────────────────────────────────────────────────────
   Every surface catches a hairline along its top edge. One pixel, and it is
   the whole difference between a box and a machined panel. */
.lit { position: relative; }
.lit::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 3;
  border-radius: inherit;
  box-shadow: inset 0 1px 0 var(--edge);
}

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

/* The interface reads as too big, and it is not the type.
 *
 * Measured on the live site at a 1512px viewport: body text 15px, h1 33px,
 * both perfectly reasonable, and `main` 1512px wide with no max-width at all.
 * `.wrap` has said 1180px since it was written and NOT ONE PAGE APPLIES IT, so
 * every page has been stretching edge to edge. On the admin's wider monitor that
 * is more than seven hundred pixels of extra spread, and the result is a line of
 * body copy running most of a metre and a catalogue row with a huge void down
 * the middle. That reads as oversized whatever size the letters are.
 *
 * So this constrains the CONTAINER rather than shrinking the text. Making the
 * type smaller would have answered the complaint by making the site harder to
 * read, which is the opposite of what was asked for, and it would have left the
 * actual cause in place.
 *
 * Applied to `main` directly rather than by adding a class to ten pages: a
 * layout rule that only works when somebody remembers to opt in is the rule that
 * has been silently off this whole time.
 */
:where(main), .wrap {
  max-width: 1180px;
  margin-inline: auto;
  padding-inline: var(--s-6);
}
/* Takes the slack, so the footer is pushed down rather than floating up. */
:where(main) { flex: 1 0 auto; width: 100%; }
/* The header keeps its own full-bleed background and aligns its CONTENTS to the
   same measure, so the bar spans the window while the logo and nav line up with
   the page under them. */
.top { padding-inline: max(var(--s-6), calc((100% - 1180px) / 2 + var(--s-6))); }

/* 1.5fr / 1fr gave the redeem panel 605px on a 1512px screen, for a form with
   one input in it. The catalogue is what people came for; the panel needs only
   enough room to be comfortable. */
.cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); }
/* Without this a wide child (the loader <pre>) sets the column's min-content
   width and pushes the page past the viewport. */
.cols > * { min-width: 0; }
/* A content pane is OPAQUE, and that is not a detail.
 *
 * `body::before` paints the banner as a fixed 4.5% watermark behind everything.
 * Fixed means it does not scroll, so anything drawn over it with no background
 * of its own has the artwork's diagonals sliding across it as the page moves,
 * starting and stopping mid-element. Over open space that reads as a watermark,
 * which is the intent. Over a panel of rows it reads as a rendering fault, and
 * it was reported as one twice: once on the executors grid and once here, on the
 * home page's "Top this week".
 *
 * `.pane + .pane` has always set a surface, so the SECOND pane on every page was
 * fine and the first was not, which is why this survived so long: the two panes
 * sit side by side and only one of them was wrong.
 *
 * The rule is worth stating rather than patching per page: the watermark is the
 * paper, and content sits ON the paper. */
.pane { padding: var(--s-6); position: relative; min-width: 0; background: var(--bg); }
.pane + .pane {
  border-left: 1px solid var(--ln);
  background: var(--sur) linear-gradient(180deg, var(--sheen), transparent 34%);
}
@media (max-width: 880px) {
  .cols { grid-template-columns: 1fr; }
  .pane + .pane { border-left: 0; border-top: 1px solid var(--ln); }
}

/* ── header ───────────────────────────────────────────────────────────────── */
/* The nav is centred on the BAR, not on the space the other two leave.
 *
 * It used `margin-inline: auto`, which is the usual trick and is subtly wrong
 * here. Auto margins split the FREE space evenly, so the nav's centre lands at
 * `logo + free/2 + nav/2` while the bar's centre is `(logo + free + nav +
 * actions)/2`. Those agree only when the logo and the actions are the same
 * width, and they are not: the logo is a mark and a word, the actions are a
 * language menu and a filled button. The nav therefore sat left of centre by
 * half the difference, which reads as sloppy without being nameable.
 *
 * See `.top > .nav` below for the fix. A three-column grid was tried first and
 * reverted: the right-hand controls are three separate children with no wrapper,
 * so they cannot share one grid cell, and giving them a `1fr` column each
 * stretched the call-to-action across half the bar. */
.top {
  display: flex; align-items: center; gap: var(--s-4);
  padding: var(--s-3) var(--s-6);
  border-bottom: 1px solid var(--ln);
  /* Above main, not level with it.
   *
   * The watermark needed `body > * { position: relative; z-index: 1 }` to sit
   * behind the page, which put the header and main on the same layer, and main
   * comes later in the document. Everything inside the header still painted
   * above its own background, so the header looked completely fine: the only
   * thing that broke was the language menu, which hangs BELOW the header and
   * therefore over main, where main won.
   *
   * A browser found this. Nothing static could have: the markup, the catalogue
   * and the token references were all correct, and the menu was on screen. It
   * simply could not be clicked. */
  position: relative; z-index: 30;
  /* So the banner below can be clipped to the bar rather than bleeding over the
     hero, and so the language menu still escapes downward (it is a child of a
     positioned ancestor, not of this overflow). */
  isolation: isolate;
}

/* The banner across the header.
 *
 * A slow band of light travelling the full width, once every eighteen seconds.
 * The restraint is the design: a header is furniture that sits on every page and
 * is looked past, not at, so anything showy here becomes irritating by the third
 * page. It is one pass of a soft sweep at low opacity, with a lit hairline along
 * the bottom edge that does not move at all.
 *
 * Behind the content and click-through, so it can never take a pointer event
 * from the nav or the language menu. Transform only, so it composites. */
/* Nothing is painted over the header. Not a sweep, not a hairline, not a glow.
 *
 * There were two, added at different times for the same reason: a bar that is a
 * flat dark rectangle looked plain, so it got a moving highlight across it and a
 * lit accent line under it. Both were wrong in the same way, and the owner said
 * so three times before this file believed it.
 *
 * A gradient band travelling across a near-black bar cannot be subtle. Its edges
 * are visible even at low opacity, so the header reads as a lighter PANEL sitting
 * inside a darker bar with two hard vertical seams, which is exactly the "sharpen"
 * the owner kept pointing at. It also moves forever, in the one region of the page
 * that should be the most still: the thing you navigate BY should not be the thing
 * that draws your eye.
 *
 * What is left does the whole job: one surface colour, one border-bottom, and the
 * accent reserved for the control you are meant to press. Expensive-looking
 * interfaces are quiet in the frame and loud in one place, and the frame is not
 * the place.
 */

/* The nav is centred on the BAR, by being taken out of the row entirely.
 *
 * A grid was tried first and rejected: the language menu, the call to action and
 * the burger are three separate children that have to sit side by side on the
 * right, and without a wrapper element there is no honest way to give them one
 * grid cell. Adding that wrapper means editing thirteen HTML files to fix an
 * alignment.
 *
 * Absolute centring needs neither. The nav leaves the flow, so the logo and the
 * actions lay out exactly as before, and `left: 50%` is measured against the bar
 * rather than against whatever space they happened to leave. Safe because the
 * nav is `display: none` under 880px, which is well before it could reach
 * either side. */
.top > .nav {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}
/* The first of the right-hand controls takes the remaining space, which pins the
   whole group to the right edge. Whichever one comes first on a given page. */
.top > .langsel { margin-left: auto; }
.top > .langsel ~ .btn,
.top > .langsel ~ .burger { margin-left: 0; }
.top > .btn:first-of-type { margin-left: auto; }
.top > .langsel ~ .btn:first-of-type { margin-left: 0; }
.mk {
  display: flex; align-items: center; gap: var(--s-2);
  appearance: none; background: none; border: 0; padding: 0;
  font: inherit; color: inherit; cursor: pointer;
}
.mk b { font-size: 19px; font-weight: 600; letter-spacing: -0.02em; }
/* The real mark, not a drawn bar.
 *
 * It was a 3px accent rule standing in for a logo, which is a fine placeholder
 * and is not the brand. logo-mark.webp is 7 KB and already at the site root, so
 * the path is absolute: these pages live under /v2/ and a relative one would
 * resolve to /v2/logo-mark.webp and load nothing.
 *
 * The glow moves to the image so the mark still reads as lit rather than pasted
 * on, and width/height are set so it reserves its space before it decodes. */
.mk img {
  width: 26px; height: 26px; display: block; object-fit: contain;
  filter: drop-shadow(0 0 7px var(--bloom));
  transition: filter var(--t-move) var(--ease), transform var(--t-move) var(--ease);
}
.mk:hover img { filter: drop-shadow(0 0 12px var(--acc)); transform: scale(1.06); }

/* There is no site watermark any more.
 *
 * There was: the banner, fixed, behind everything, at 4.5% opacity. It is gone,
 * and the reason is worth writing down because it will look like an omission.
 *
 * The artwork is hard-edged and geometric. At 4.5% on a near-black ground it
 * does not read as a logo; it reads as grey wedges, and because it was FIXED it
 * did not scroll with the page, so those wedges slid across whatever was drawn
 * over them. Two separate "the effects are glitching" reports traced back to
 * this one element: the executors grid and the home page's top-scripts panel.
 * Making those surfaces opaque fixed both, and left the watermark visible only
 * where there is open space, which is the hero.
 *
 * And the hero is exactly where it was least welcome. That section already has
 * four animated fog masses on periods of 23, 31, 41 and 53 seconds, built for
 * this job, plus a spotlight that follows the cursor. The watermark sat on top
 * of all of it contributing angular smudges. Two things doing the same job, and
 * the one that was designed for it is the one that stayed.
 *
 * The file and its immutable cache header remain, so putting it back is one
 * rule. It should not go back.
 */
body > * { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) { .mk img { transition: none; } }

.nav { display: flex; gap: var(--s-5); font-size: var(--t-sm); color: var(--mut); }
.nav a {
  color: inherit; text-decoration: none; position: relative; padding-bottom: 3px;
  display: inline-flex; align-items: center; gap: 7px;
  transition: color var(--t-tint) ease;
}
.nav a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 100%;
  background: var(--acc); box-shadow: 0 0 8px -1px var(--acc);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--t-move) var(--ease);
}
/* `[aria-current]` rather than `[aria-current="page"]`, because a page can be
   INSIDE a section without being the section. game.html is a script's own page
   and belongs under Scripts, so it marks that entry `aria-current="location"`,
   which is what the attribute means and is not "page" because the visitor is
   not on scripts.html. The highlight is the same either way. */
.nav a:hover, .nav a[aria-current] { color: var(--fg); text-decoration: none; }
.nav a:hover::after, .nav a[aria-current]::after { transform: scaleX(1); }
/* Held at full strength. They were at 0.62, which is a considered way of
   making an icon hard to see. */
.nav .ic { width: 16px; height: 16px; transition: transform var(--t-move) var(--ease); }
.nav a:hover .ic, .nav a[aria-current] .ic { transform: translateY(-1px); }
@media (max-width: 880px) { .nav { display: none; } }

/* ── mobile: a burger and a full-screen sheet, which is what production
      already does. A bottom tab bar would be a second navigation pattern on
      the same site, and nobody should have to learn the place twice. ────── */
.burger {
  display: none; width: 38px; height: 38px; padding: 0;
  border: 1px solid var(--ln); border-radius: var(--rad-soft);
  background: transparent; color: var(--fg); cursor: pointer;
  align-items: center; justify-content: center;
  transition: border-color var(--t-tint) ease, box-shadow var(--t-move) var(--ease);
}
.burger:hover { border-color: var(--acc); box-shadow: 0 0 20px -10px var(--acc); }
.burger i { display: block; width: 17px; height: 1.5px; background: currentColor; position: relative; transition: background-color var(--t-tint) ease; }
.burger i::before, .burger i::after {
  content: ""; position: absolute; left: 0; width: 100%; height: 1.5px;
  background: currentColor; transition: transform var(--t-move) var(--ease);
}
.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: 880px) { .burger { display: inline-flex; } }

.sheet {
  position: fixed; inset: 0; z-index: 40;
  background: var(--bg); padding: var(--s-3) var(--s-6) var(--s-6);
  display: flex; flex-direction: column; overflow-y: auto;
  opacity: 0; visibility: hidden; transform: translateY(-10px);
  transition: opacity 0.26s ease, transform var(--t-move) var(--ease), visibility 0s 0.3s;
}
.sheet[data-open="1"] {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity 0.26s ease, transform var(--t-move) var(--ease), visibility 0s;
}
.sheet .shead {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-4);
  padding-bottom: var(--s-5); border-bottom: 1px solid var(--ln); margin-bottom: var(--s-1);
}
.sheet .shead .burger { display: inline-flex; }
.sheet a {
  display: flex; align-items: center; gap: var(--s-4);
  font-size: 21px; font-weight: 500; letter-spacing: -0.02em; color: var(--fg);
  text-decoration: none; padding: var(--s-4) 0; border-bottom: 1px solid var(--ln);
  position: relative; opacity: 0; transform: translateY(9px);
  transition: color var(--t-tint) ease, padding-left var(--t-move) var(--ease);
}
.sheet[data-open="1"] a {
  opacity: 1; transform: none;
  transition: opacity var(--t-enter) ease, transform var(--t-enter) var(--ease),
              color var(--t-tint) ease, padding-left var(--t-move) var(--ease);
}
.sheet a::before {
  content: ""; position: absolute; left: 0; top: 12px; bottom: 12px; width: 2px;
  background: var(--acc); box-shadow: 0 0 12px -1px var(--acc);
  transform: scaleY(0); transform-origin: top;
  transition: transform var(--t-move) var(--ease);
}
.sheet a:hover, .sheet a[aria-current] { padding-left: 15px; text-decoration: none; }
.sheet a:hover::before, .sheet a[aria-current]::before { transform: scaleY(1); }
.sheet a[aria-current] { color: var(--acc-txt); }
.sheet a .ic { width: 19px; height: 19px; color: var(--mut); transition: color var(--t-tint) ease, transform var(--t-move) var(--ease); }
.sheet a:hover .ic, .sheet a[aria-current] .ic { color: var(--acc-txt); transform: translateX(2px); }
.sheet small { color: var(--rank); font-size: var(--t-xs); margin-top: auto; padding-top: var(--s-6); }
/* Each link arrives a beat after the one above it. */
.sheet[data-open="1"] a:nth-of-type(1) { transition-delay: 0.03s; }
.sheet[data-open="1"] a:nth-of-type(2) { transition-delay: 0.06s; }
.sheet[data-open="1"] a:nth-of-type(3) { transition-delay: 0.09s; }
.sheet[data-open="1"] a:nth-of-type(4) { transition-delay: 0.12s; }
.sheet[data-open="1"] a:nth-of-type(5) { transition-delay: 0.15s; }
.sheet[data-open="1"] a:nth-of-type(6) { transition-delay: 0.18s; }
.sheet[data-open="1"] a:nth-of-type(7) { transition-delay: 0.21s; }
.sheet[data-open="1"] a:nth-of-type(8) { transition-delay: 0.24s; }

/* ── footer ───────────────────────────────────────────────────────────────── */
.foot {
  border-top: 1px solid var(--ln); padding: var(--s-6);
  display: flex; flex-wrap: wrap; gap: var(--s-5);
  justify-content: space-between; align-items: center;
  background: var(--tick);
}
.foot nav { display: flex; gap: var(--s-5); flex-wrap: wrap; }
.foot a { color: var(--mut); font-size: var(--t-xs); display: inline-flex; align-items: center; gap: 6px; }
.foot a:hover { color: var(--fg); text-decoration: none; }
.foot .ic { width: 13px; height: 13px; opacity: 0.55; transition: opacity var(--t-tint) ease; }
.foot a:hover .ic { opacity: 1; }
.foot small { color: var(--rank); font-size: var(--t-xs); }

/* ── buttons ──────────────────────────────────────────────────────────────── */
.btn {
  font: inherit; font-size: var(--t-sm); font-weight: 500; letter-spacing: -0.005em;
  cursor: pointer; padding: var(--s-3) var(--s-5);
  border: 1px solid transparent; border-radius: var(--rad-soft);
  position: relative; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  will-change: transform;
  transition: background-color var(--t-tint) ease, color var(--t-tint) ease,
              border-color var(--t-tint) ease, box-shadow var(--t-move) ease,
              transform 0.12s var(--ease);
}
.btn:active { transform: scale(0.975); }

/* Copy confirmed.
 *
 * The old confirmation swapped the label text and nothing else, which on a
 * control that is mostly an icon reads as a flicker rather than an answer. This
 * is the moment somebody most needs to know whether it worked, because the thing
 * they copied is a licence key they cannot see in the clipboard.
 *
 * A wash of the accent, a tick where the copy glyph was, and the label sliding
 * up as it changes. The wash decays on its own so nothing has to clean it up
 * beyond removing the class. */
.btn.copied {
  border-color: var(--acc);
  color: var(--acc-txt);
  box-shadow: 0 0 0 3px var(--bloom);
}
.btn.copied::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--acc);
  opacity: 0.16;
  animation: gh-copied-wash 1.6s var(--ease) forwards;
  pointer-events: none;
}
.btn.copied .ic { animation: gh-copied-tick 0.4s var(--ease); }
.btn.copied > span, .btn.copied { animation: gh-copied-lift 0.34s var(--ease); }
@keyframes gh-copied-wash {
  0% { opacity: 0.22; }
  100% { opacity: 0; }
}
@keyframes gh-copied-tick {
  0% { transform: scale(0.4) rotate(-18deg); opacity: 0; }
  60% { transform: scale(1.18) rotate(0deg); opacity: 1; }
  100% { transform: none; opacity: 1; }
}
@keyframes gh-copied-lift {
  0% { transform: translateY(5px); opacity: 0; }
  100% { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .btn.copied::after, .btn.copied .ic, .btn.copied > span { animation: none; }
  .btn.copied::after { opacity: 0.12; }
}
.btn .ic { width: 15px; height: 15px; transition: transform var(--t-move) var(--ease), opacity var(--t-tint) ease; }
.btn:hover .ic-lead { transform: translateY(-1.5px); }
/* The arrow keeps its space at all times and only fades and slides. Animating
   its width relaid out the button every frame, pushed the label around and
   shoved the next button sideways. Opacity and transform never touch layout,
   so this is smooth by construction rather than by tuning a duration.

   It slides in from the RIGHT, and that direction is the fix rather than a
   preference. It used to rest at translateX(-7px), which is exactly the flex
   gap, so its resting position was flush against the last letter of the label.
   Invisible at rest, so that looked fine in a static screenshot. But opacity
   and transform ran together, so for the whole of the fade the arrow was drawn
   ON TOP of the word it sits after, and every hover showed a glyph crawling out
   from under the text. Coming from the right it starts in the button's own
   padding, moves toward the label, and never occupies the same pixels as it. */
.btn .ic-arw { opacity: 0; transform: translateX(7px); }
.btn:hover .ic-arw, .btn:focus-visible .ic-arw { opacity: 1; transform: none; }

.btn-p { background: var(--acc); color: var(--on-acc); box-shadow: inset 0 1px 0 var(--on-acc-sheen); }
/* LIGHT 2: the glow arrives on hover. Always-on glow is what reads as cheap.
 *
 * The label goes to WHITE on hover, and lifts a hair.
 *
 * The resting label is --on-acc, a near-black chosen for contrast against the
 * accent fill, which is correct and reads as heavy: a dark word on a bright
 * button looks like a disabled control to a lot of people. Going white on hover
 * is the cheapest possible way to make the button feel like it answered, and it
 * keeps the resting state at the contrast ratio the gate measured. Both states
 * are in docs/design-tokens.json, so both are still measured. */
.btn-p:hover {
  color: var(--on-acc-hover);
  box-shadow: inset 0 1px 0 var(--on-acc-sheen-hover), 0 0 26px -8px var(--acc);
  transform: translateY(-1px);
}
.btn-p:active { transform: translateY(0) scale(0.975); }
.btn-p:hover .ic { color: var(--on-acc-hover); }
.btn-p::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 40%, var(--sweep) 50%, transparent 60%);
  transform: translateX(-120%);
  transition: transform var(--t-sheen) var(--ease);
}
.btn-p:hover::after { transform: translateX(120%); }
.btn-s { background: transparent; color: var(--fg); border-color: var(--ln); }
.btn-s:hover {
  border-color: var(--acc); color: var(--acc-txt);
  box-shadow: 0 0 20px -10px var(--acc);
  transform: translateY(-1px);
}
.btn-s:active { transform: translateY(0) scale(0.975); }
.btn-sm { padding: var(--s-2) var(--s-4); font-size: var(--t-xs); }

/* Somebody who asked for less motion keeps the colour change and loses the lift.
   The colour is the information; the movement is the flourish. */
@media (prefers-reduced-motion: reduce) {
  .btn-p:hover, .btn-s:hover { transform: none; }
  .btn-p::after { display: none; }
}

/* ── what the key turned out to be ──────────────────────────────────────────
 *
 * verify-key has always returned the type, the expiry, the bound games and
 * whether the key is locked to a device, and the page showed none of it: you
 * redeemed a key and were told "redeemed". Every row here is a question the
 * support channel gets asked, so printing it is the cheapest support work
 * available.
 *
 * Revealed rather than appearing: the rows arrive after a real answer from the
 * server, and a block that pops in fully formed reads as if it was always
 * there. */
.kfacts {
  display: grid; gap: 0;
  margin-top: var(--s-4);
  border: 1px solid var(--ln); border-radius: var(--rad-soft);
  overflow: hidden;
  animation: gh-facts-in var(--t-enter) var(--ease) both;
}
.kfact {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--s-3);
  align-items: center; padding: var(--s-3) var(--s-4);
  font-size: var(--t-sm); border-bottom: 1px solid var(--ln);
  transition: background-color var(--t-tint) ease;
}
.kfact:last-child { border-bottom: 0; }
.kfact:hover { background: var(--hov); }
.kfact .ic { width: 15px; height: 15px; color: var(--acc-txt); }
.kfact span { color: var(--mut); }
.kfact b { font-weight: 500; text-align: right; }

@keyframes gh-facts-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .kfacts { animation: none; }
}

/* The key field says whether the SHAPE is right while you type. Local only:
   true of any string, so it tells nobody whether a key exists. */
.field[data-shape="partial"] input { border-color: var(--warn); }
.field[data-shape="ok"] input {
  border-color: var(--ok);
  box-shadow: 0 0 18px -10px var(--ok);
}
.field[data-shape="ok"] .ic { color: var(--ok); }


/* ── filters and chips ─────────────────────────────────────────────────────
 *
 * These lived in scripts.css, which only the scripts page loads. The executors
 * page shipped with four filter buttons and no styling at all: grey boxes with
 * black text, which is what an unstyled <button> looks like and exactly what it
 * looked like on production. Moved here, where every page that loads the shell
 * gets them, because a component used by two pages living in one page's
 * stylesheet is a bug waiting for the second page. */
.filters {
  display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: flex-end;
  padding: var(--s-4) var(--s-6); border-bottom: 1px solid var(--ln);
}
.filters .field { margin: 0; flex: 1 1 200px; }
.chips { display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: flex-end; }
.chips button {
  appearance: none; cursor: pointer; font: inherit; font-size: var(--t-xs);
  font-weight: 500; letter-spacing: -0.005em;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--ln); border-radius: var(--rad-soft);
  background: transparent; color: var(--mut);
  position: relative; overflow: hidden;
  transition: color var(--t-tint) ease, border-color var(--t-tint) ease,
              background-color var(--t-tint) ease, box-shadow var(--t-move) var(--ease),
              transform 0.12s var(--ease);
}
.chips button:hover {
  color: var(--fg); border-color: var(--mut);
  background: var(--hov); transform: translateY(-1px);
}
.chips button:active { transform: translateY(0) scale(0.97); }
.chips button[aria-pressed="true"] {
  color: var(--acc-txt); border-color: var(--acc);
  box-shadow: 0 0 22px -12px var(--acc);
}
/* The selected chip gets a lit underline that grows, so which filter is on is
   readable without relying on the accent colour alone. */
.chips button::after {
  content: ""; position: absolute; left: var(--s-4); right: var(--s-4); bottom: 0; height: 1px;
  background: var(--acc); box-shadow: 0 0 8px -1px var(--acc);
  transform: scaleX(0); transform-origin: center;
  transition: transform var(--t-move) var(--ease);
}
.chips button[aria-pressed="true"]::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .chips button:hover { transform: none; }
}

.count { font-size: var(--t-xs); color: var(--mut); font-family: var(--mono); padding: var(--s-4) var(--s-6) 0; }

/* ── fields ───────────────────────────────────────────────────────────────── */
.field { display: block; margin-bottom: var(--s-3); position: relative; }
.field > span { display: block; color: var(--mut); margin-bottom: 6px; }
.field input {
  width: 100%; font: inherit; font-family: var(--mono); font-size: var(--t-sm);
  padding: var(--s-3); border-radius: var(--rad-soft);
  background: var(--inp); color: var(--fg); border: 1px solid var(--ln);
  box-shadow: inset 0 1px 0 var(--edge);
  transition: border-color var(--t-tint) ease, padding var(--t-move) var(--ease),
              box-shadow var(--t-move) var(--ease);
}
.field input:focus {
  outline: none; border-color: var(--acc); padding-left: 15px;
  box-shadow: inset 0 1px 0 var(--edge), 0 0 22px -10px var(--acc);
}
/* A placeholder is a hint, never a label. Every field above has a real one. */
.field input::placeholder { color: var(--rank); }
.field .ic { position: absolute; right: 12px; bottom: 12px; color: var(--rank); pointer-events: none; transition: color var(--t-tint) ease; }
.field input:focus ~ .ic { color: var(--acc-txt); }

/* ── rows ─────────────────────────────────────────────────────────────────── */
.row {
  display: grid; grid-template-columns: auto auto 1fr auto; align-items: center; min-width: 0;
  gap: var(--s-3); padding: 10px var(--s-2);
  border-bottom: 1px solid var(--ln); position: relative;
  transition: background-color var(--t-tint) ease, padding var(--t-move) var(--ease);
}
.row:last-child { border-bottom: 0; }
/* LIGHT 5: a lit rail on the active row, growing from the top. Position says
   where you are; light says it is live. */
.row::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--acc); box-shadow: 0 0 12px -1px var(--acc);
  transform: scaleY(0); transform-origin: top;
  transition: transform var(--t-move) var(--ease);
}
.row:hover { background: var(--hov); padding-left: 18px; padding-right: var(--s-3); }
.row:hover::before { transform: scaleY(1); }
.rank { font-size: 13px; font-weight: 500; color: var(--rank); min-width: 30px; transition: color var(--t-tint) ease, transform var(--t-move) var(--ease); }
/* The `1fr` track is `minmax(auto, 1fr)`, and `auto` as a MINIMUM is min-content.
   The name inside is `white-space: nowrap` with an ellipsis, so its min-content
   is the whole untruncated name: the track refuses to go below it, the row grows
   past the viewport, and the ellipsis that was supposed to prevent exactly this
   never engages. `min-width: 0` on the ROW does not help, because the floor is
   on the ITEM. A long script name pushed the stats and home pages sideways at
   every width below 800px until this line existed. */
.rname { min-width: 0; }
.row:hover .rank { color: var(--acc-txt); transform: translateX(2px); }
.thumb {
  width: 40px; height: 40px; overflow: hidden; position: relative; flex: none;
  border-radius: var(--rad-soft); border: 1px solid var(--ln);
  box-shadow: inset 0 1px 0 var(--edge); background: var(--inp);
}
.art {
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: saturate(0.82) contrast(1.03);
  transition: transform var(--t-move) var(--ease), filter var(--t-tint) ease;
}
.row:hover .art, .card:hover .art { transform: scale(1.06); filter: none; }
@media (max-width: 560px) { .thumb { display: none; } .row { grid-template-columns: auto 1fr auto; } }

.bar { height: 3px; border-radius: 2px; overflow: hidden; background: var(--barbg); margin-top: 6px; }
.bar i { display: block; height: 100%; width: 0; border-radius: 2px; background: var(--acc2); box-shadow: 0 0 8px -2px var(--acc2); transition: width var(--t-grow) var(--ease); }
[data-seen="1"] .bar i { width: var(--w); }

/* ── arrival ──────────────────────────────────────────────────────────────
   The page had one keyframe in it. Everything that moved, moved because a
   cursor was already on it, so nothing ever happened on arrival and the site
   read as a static mock of itself.

   The rule that shapes this: NOTHING IS HIDDEN WAITING FOR JAVASCRIPT. The
   obvious build sets opacity to 0 and lets an IntersectionObserver put it
   back, and the failure mode of that is a blank shop page, which costs real
   money for a decoration. So the arrival is an animation ON the seen state
   rather than a hidden default. If the observer never runs, every section is
   simply already there: the end state, reached without the journey. */
[data-seen="1"] > * { animation: gh-rise var(--t-enter) var(--ease) both; }
@keyframes gh-rise { from { opacity: 0; transform: translateY(9px); } }

/* The stagger runs six deep and then stops. Past that the delay is longer than
   the eye will wait for, and a seventh card arriving late reads as a bug. */
[data-seen="1"] > :nth-child(2) { animation-delay: var(--t-step); }
[data-seen="1"] > :nth-child(3) { animation-delay: calc(var(--t-step) * 2); }
[data-seen="1"] > :nth-child(4) { animation-delay: calc(var(--t-step) * 3); }
[data-seen="1"] > :nth-child(5) { animation-delay: calc(var(--t-step) * 4); }
[data-seen="1"] > :nth-child(n + 6) { animation-delay: calc(var(--t-step) * 5); }

/* ── ambient ──────────────────────────────────────────────────────────────
   `--t-ambient` was declared at 34 seconds and used nowhere. A page whose
   light is perfectly still reads as a screenshot; one whose light moves fast
   enough to notice reads as a distraction. 34s is the setting where you cannot
   catch it moving but the page is not dead either.

   It moves the light that is ALREADY THERE. `.hero` and `.phead` each paint a
   radial gradient positioned at --mx/--my, so the first version of this added
   a ::before with its own gradient and would have shipped two spotlights
   stacked on one surface. Registering the custom property instead means the
   existing paint interpolates and nothing new is drawn.

   @property is the whole mechanism: an unregistered custom property is a
   string and snaps between keyframes instead of easing. A browser without it
   gets today's still spotlight, which is the correct thing to degrade to.

   On a fine pointer `wireSpotlight` writes --mx/--my as inline styles, which
   outrank this and take the light back. That precedence is deliberate: the
   drift is what the surface does when nobody is touching it. */
@property --my { syntax: "<percentage>"; inherits: true; initial-value: 0%; }
@property --mx { syntax: "<percentage>"; inherits: true; initial-value: 50%; }
[data-spot] { animation: gh-drift var(--t-ambient) ease-in-out infinite alternate; }
@keyframes gh-drift {
  from { --mx: 38%; --my: 0%; }
  to   { --mx: 62%; --my: 12%; }
}

/* ── icons ────────────────────────────────────────────────────────────────── */
/* Icons carry colour and light, not only shape.
 *
 * They were drawn in `currentColor`, which made every one of them the same grey
 * as the label beside it: correct, and invisible. An icon that reads as part of
 * the text is doing none of the work an icon exists for, which is being found
 * before the label is read.
 *
 * `accTxt` rather than `acc`, because that is the accent value the contrast gate
 * proves at 4.5:1 on both the page and a surface. The glow is `bloom`, a token
 * that already existed for exactly this, so the light comes from the palette
 * rather than from a number typed here. */
.ic {
  width: 17px; height: 17px; display: block; flex: none;
  color: var(--acc-txt);
  filter: drop-shadow(0 0 5px var(--bloom));
  transition: filter var(--t-tint) ease, color var(--t-tint) ease;
}
/* On the accent fill the mark is deliberately dark, and a purple glow against
   purple is not a glow, it is a smudge. */
.btn-p .ic, .gm-rec .ic { color: inherit; filter: none; }
/* Brighter where the pointer is, and on the page you are actually on. The hue
   does not move; only the light does. */
.nav a:hover .ic,
.nav a[aria-current] .ic,
.sheet a:hover .ic,
.sheet a[aria-current] .ic,
.foot a:hover .ic,
.btn-s:hover .ic,
.row:hover .ic {
  filter: drop-shadow(0 0 9px var(--acc)) drop-shadow(0 0 2px var(--acc));
}
/* The Discord mark is a solid shape rather than an outline, so the same glow
   reads twice as strong on it. Held back to match the others. */
.ic.brand { filter: drop-shadow(0 0 4px var(--bloom)); }
@media (prefers-reduced-motion: reduce) { .ic { transition: none; } }

/* Icons were monochrome everywhere: opacity against the text colour and
   nothing else. That is correct for an icon that is a label, and wrong for one
   that is a signal. These are the signals. */
h3 > .ic, h4 > .ic { color: var(--acc-txt); }
.btn-p .ic { color: var(--on-acc); }
.nav a[aria-current] .ic { color: var(--acc-txt); }
.ok .ic { color: var(--ok); }
.warn .ic { color: var(--warn); }

/* Reduced motion: arrive without the journey, never arrive at nothing. The
   token file already collapses every duration; these force the end states that
   are otherwise reached only by a transition. */
@media (prefers-reduced-motion: reduce) {
  .bar i { width: var(--w) !important; }
  *, *::before, *::after { animation: none !important; }
}

/* ── language ───────────────────────────────────────────────────────────────
   In the footer beside the legal line, because it is a preference rather than
   a destination. Roughly half of this shop reads Thai first, so it is a real
   control and not a courtesy. */
/* ── language selector ──────────────────────────────────────────────────────
   A button that opens a list, sitting in the header beside the Redeem action.

   It replaced two flat buttons in the footer. Two buttons work while there are
   two languages and stop working at three, and the footer is the wrong place
   for something a person reaches for in the first five seconds, before they
   have scrolled anything.

   A globe, not flags. A flag names a country; these are languages, and the
   mapping is wrong for most speakers of all three. */
.langsel { position: relative; }
.langbtn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px var(--s-3);
  background: none; border: 1px solid var(--ln); border-radius: var(--rad-soft);
  color: var(--mut); font: inherit; font-size: var(--t-sm); cursor: pointer;
  transition: color var(--t-tint) ease, border-color var(--t-tint) ease, background-color var(--t-tint) ease;
}
.langbtn:hover, .langbtn[aria-expanded="true"] { color: var(--fg); border-color: var(--acc); background: var(--hov); }
.langbtn b { font-weight: 500; letter-spacing: var(--track-label); }
.langbtn .ic-caret { width: 13px; height: 13px; transition: transform var(--t-move) var(--ease); }
.langbtn[aria-expanded="true"] .ic-caret { transform: rotate(180deg); }

/* Animated open and close.
   Kept off the display property so both directions can be seen: display cannot be
   transitioned, so a menu hidden that way appears and vanishes instantly however
   long the transition says it is. Visibility can, as a step at the end, which
   also keeps the closed menu out of the tab order without a hidden attribute
   that would kill the animation. */
.langmenu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 60;
  min-width: 172px; margin: 0; padding: var(--s-1); list-style: none;
  background: var(--sur); border: 1px solid var(--ln); border-radius: var(--rad-soft);
  box-shadow: 0 20px 44px -22px var(--bloom), 0 2px 10px -6px var(--bloom);
  opacity: 0; visibility: hidden; transform: translateY(-6px) scale(0.985);
  transform-origin: top right;
  transition: opacity var(--t-move) var(--ease), transform var(--t-move) var(--ease), visibility 0s linear var(--t-move);
}
.langsel[data-open="1"] .langmenu {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity var(--t-move) var(--ease), transform var(--t-move) var(--ease), visibility 0s;
}
.langmenu li {
  display: flex; align-items: center; gap: var(--s-3);
  padding: 9px var(--s-3); border-radius: var(--rad-soft);
  color: var(--mut); cursor: pointer; font-size: var(--t-sm);
  transition: background-color var(--t-tint) ease, color var(--t-tint) ease;
}
.langmenu li:hover, .langmenu li:focus-visible { background: var(--hov); color: var(--fg); }
.langmenu li[aria-selected="true"] { color: var(--acc-txt); }
/* The tick is drawn, so it inherits the accent and cannot be mistaken for a
   bullet the way a character would be. */
.langmenu li i {
  width: 11px; height: 11px; flex: none; opacity: 0;
  border-left: 2px solid var(--acc); border-bottom: 2px solid var(--acc);
  transform: rotate(-45deg) scale(0.8) translate(1px, -2px);
  transition: opacity var(--t-tint) ease;
}
.langmenu li[aria-selected="true"] i { opacity: 1; }
.langmenu li span { margin-left: auto; color: var(--rank); font-size: var(--t-xs); letter-spacing: var(--track-label); }

@media (prefers-reduced-motion: reduce) {
  .langmenu, .langbtn, .langbtn .ic-caret { transition: none; }
}
/* On a phone the header hides the nav and the selector would crowd the Redeem
   button, so it moves into the sheet instead, where it has room to be a list. */
@media (max-width: 900px) { .top .langsel { display: none; } }
.sheet .langsel { display: block; margin-top: var(--s-3); }
.sheet .langbtn { width: 100%; justify-content: space-between; }
.sheet .langmenu { right: auto; left: 0; width: 100%; transform-origin: top left; }

/* A message is always two things: what happened, and what to do about it.
   The lead carries the weight; the next carries the way out. */
.msg-lead { font-weight: 600; color: var(--fg); }
.msg-next { color: var(--mut); }

/* ── the in-game menu controls ──────────────────────────────────────────────
 *
 * The toggles, sliders and dropdown that a GladHub script actually renders in
 * Roblox. Used by the scripts page feature modal and by the home page showcase,
 * so they live here rather than in either one. */
.ctl { display: flex; align-items: center; gap: var(--s-2); font-size: var(--t-xs); color: var(--mut); font-family: var(--mono); white-space: nowrap; }

.sw {
  width: 32px; height: 18px; flex: none; cursor: pointer; position: relative;
  border: 1px solid var(--ln); border-radius: 6px; background: var(--inp);
  box-shadow: inset 0 1px 0 var(--edge);
  transition: border-color var(--t-tint) ease, box-shadow var(--t-move) var(--ease);
}
.sw::after {
  content: ""; position: absolute; top: 2.5px; left: 2.5px; width: 11px; height: 11px;
  border-radius: 3px; background: var(--rank);
  transition: transform var(--t-move) var(--ease), background-color var(--t-tint) ease;
}
.sw[aria-pressed="true"] { border-color: var(--acc); box-shadow: inset 0 1px 0 var(--edge), 0 0 18px -9px var(--acc); }
.sw[aria-pressed="true"]::after { transform: translateX(14px); background: var(--acc); }

/* The slider.
 *
 * It reads as a hairline and GRABS like a control, which are two different
 * sizes and used to be the same one. The visible bar is 5px; the target is the
 * 22px band in ::before, which is invisible, sits over the bar, and is what the
 * pointer actually hits. Making the bar itself thick enough to grab would have
 * turned a delicate control into a scrollbar.
 *
 * `touch-action: none` is required, not cosmetic: without it the browser claims
 * a vertical drag for scrolling and the slider never sees the moves at all, so
 * on a phone it simply did not work. */
.sld {
  width: 108px; height: 5px; border-radius: var(--rad-pill);
  background: var(--barbg); position: relative; flex: none;
  cursor: grab; touch-action: none;
  box-shadow: inset 0 1px 0 var(--edge);
  transition: box-shadow var(--t-move) var(--ease);
}
.sld::before {
  content: ""; position: absolute; left: -6px; right: -6px; top: 50%;
  height: 22px; transform: translateY(-50%);
}
.sld:hover, .sld:focus-visible { box-shadow: inset 0 1px 0 var(--edge), 0 0 14px -6px var(--acc); }
.sld[data-drag="1"] { cursor: grabbing; }
.sld i {
  display: block; height: 100%; border-radius: var(--rad-pill);
  background: linear-gradient(90deg, var(--acc2), var(--acc));
  box-shadow: 0 0 8px -2px var(--acc);
  transition: width var(--t-move) var(--ease);
}
/* A round handle with a rim, so it reads as a thing to pick up. */
.sld::after {
  content: ""; position: absolute; top: 50%; left: var(--p, 50%);
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--acc); border: 2px solid var(--bg);
  box-shadow: 0 0 12px -2px var(--acc), 0 1px 3px var(--glow-shade);
  transform: translate(-50%, -50%);
  transition: left var(--t-move) var(--ease), transform var(--t-move) var(--ease);
}
.sld:hover::after { transform: translate(-50%, -50%) scale(1.12); }
/* NOTHING eases while a finger is down.
 *
 * A transition on the property being dragged makes the handle chase the pointer
 * about a fifth of a second behind it, and that lag is exactly what "it does not
 * slide smoothly" describes. Easing is for changes the user did not make by
 * hand; during a drag the handle belongs under the finger, on the frame. */
.sld[data-drag="1"]::after,
.sld[data-drag="1"] i { transition: none; }
.sld[data-drag="1"]::after { transform: translate(-50%, -50%) scale(1.18); }

.dd { position: relative; }
.sel {
  display: inline-flex; align-items: center; gap: var(--s-1); cursor: pointer;
  border: 1px solid var(--ln); border-radius: var(--rad-soft); background: var(--inp); color: var(--mut);
  padding: var(--s-1) 7px var(--s-1) var(--s-2);
  box-shadow: inset 0 1px 0 var(--edge); font-family: var(--mono); font-size: var(--t-xs);
  transition: border-color var(--t-tint) ease, color var(--t-tint) ease;
}
.sel:hover { border-color: var(--acc); color: var(--fg); }
.sel .ic { width: 13px; height: 13px; transition: transform var(--t-move) var(--ease); }
.sel .val { min-width: 1.4em; text-align: right; }
.dd[data-open="1"] .sel { border-color: var(--acc); color: var(--fg); }
.dd[data-open="1"] .sel .ic { transform: rotate(180deg); }

.ddm {
  position: absolute; right: 0; top: calc(100% + 5px); z-index: 5; min-width: 100%;
  background: var(--sur); border: 1px solid var(--ln); border-radius: var(--rad-soft); padding: var(--s-1);
  box-shadow: inset 0 1px 0 var(--edge), 0 18px 40px -18px var(--scrim-base);
  display: flex; flex-direction: column; gap: 2px;
  opacity: 0; visibility: hidden; transform: translateY(-5px);
  transition: opacity 0.16s ease, transform var(--t-move) var(--ease), visibility 0s 0.2s;
}
.dd[data-open="1"] .ddm { opacity: 1; visibility: visible; transform: none; transition: opacity 0.16s ease, transform var(--t-move) var(--ease), visibility 0s; }
.ddm button {
  appearance: none; cursor: pointer; font: inherit; font-family: var(--mono); font-size: var(--t-xs);
  text-align: left; padding: 7px var(--s-3); border: 0; border-radius: var(--rad-soft);
  background: transparent; color: var(--mut);
  transition: color var(--t-tint) ease, background-color var(--t-tint) ease;
}
.ddm button:hover { color: var(--fg); background: var(--hov); }
.ddm button[aria-selected="true"] { color: var(--acc-txt); }
