/* Home page only. Everything shared lives in shell.css; if a rule here would
 * be useful on a second page it belongs there instead. Same rule as the shell:
 * no colour, no duration, no radius that is not a token. */

/* LIGHT 3: a spotlight that follows the cursor. Set by shell.js on pointer
   devices only; the fallback centres it, so a touch device gets a still glow
   rather than nothing. */
.hero {
  position: relative;
  padding: var(--s-13) var(--s-6) var(--s-10);
  border-bottom: 1px solid var(--ln);
  overflow: hidden;
  background:
    radial-gradient(600px circle at var(--mx, 50%) var(--my, 0%), var(--spot), transparent 62%),
    radial-gradient(760px circle at 88% 8%, var(--sheen), transparent 60%);
}
.hero > * { position: relative; z-index: 2; }

/* ── the drifting fog ───────────────────────────────────────────────────────
 *
 * The brief was an atmospheric background like the one on the site the owner
 * pointed at. That site does it with a hard halftone dot matrix, which is a
 * strong look and a borrowed one. This is deliberately a different texture
 * family reaching for the same feeling: volumetric masses of light drifting
 * through a fine mesh, which is what this product's own accent already does on
 * every other surface, rather than a print-screen dither in someone else's hue.
 *
 * Three decisions worth keeping:
 *
 * 1. Radial gradients, not blur filters. A `filter: blur(80px)` over a hero-
 *    sized element is a full-surface raster on every resize and, on some mobile
 *    GPUs, on every frame. A gradient is soft for free and costs nothing.
 *
 * 2. Only `transform` animates. No width, no position, no opacity keyframes on
 *    the big layers, so the compositor moves an already-painted texture and the
 *    main thread never re-lays-out. That is what makes it smooth on a phone
 *    rather than only on the machine it was designed on.
 *
 * 3. The periods are 23s / 31s / 41s / 53s. They share no common factor, so the
 *    four never realign and the loop has no visible beat. Round numbers would
 *    have made it obviously a loop within a minute of looking at it.
 */
/* `.hero > .fog`, not `.fog`, and that is the whole reason this was invisible.
 *
 * `.hero > *` sets `position: relative; z-index: 2` on every child, and at
 * (0,1,1) it outranks a bare `.fog` at (0,1,0). So the fog was RELATIVE rather
 * than absolute: a zero-height box in the normal flow, with `overflow: hidden`
 * clipping four absolutely-positioned children against nothing. It rendered
 * exactly no pixels, on every page load, while the CSS read as correct.
 *
 * A cascade collision, not a mistake in the effect. Matching the specificity of
 * the rule it has to beat is the fix. */
.hero > .fog {
  position: absolute; inset: 0; z-index: 0; overflow: hidden;
  pointer-events: none;
  /* Fades out before the copy starts, so the text always sits on a calm ground
     no matter where a mass happens to have drifted. */
  -webkit-mask-image: radial-gradient(120% 100% at 50% 0%, var(--fg) 30%, transparent 78%);
  mask-image: radial-gradient(120% 100% at 50% 0%, var(--fg) 30%, transparent 78%);
}
.hero > .fog i {
  position: absolute; display: block;
  width: 46rem; height: 46rem; border-radius: 50%;
  will-change: transform;
}
.hero > .fog .f1 {
  top: -20rem; left: -12rem;
  background: radial-gradient(circle, var(--bloom), transparent 62%);
  animation: gh-fog-a 23s var(--ease) infinite alternate;
}
.hero > .fog .f2 {
  top: -26rem; right: -14rem; left: auto;
  width: 54rem; height: 54rem;
  background: radial-gradient(circle, var(--sheen), transparent 60%);
  animation: gh-fog-b 31s var(--ease) infinite alternate;
}
.hero > .fog .f3 {
  bottom: -30rem; left: 22%;
  width: 40rem; height: 40rem;
  background: radial-gradient(circle, var(--spot), transparent 66%);
  animation: gh-fog-c 41s var(--ease) infinite alternate;
}
/* The one that carries the second accent. A single cooler mass keeps the field
   from reading as one flat wash of brand purple. */
.hero > .fog .f4 {
  top: 30%; left: 46%;
  width: 32rem; height: 32rem;
  background: radial-gradient(circle, var(--bloom), transparent 64%);
  animation: gh-fog-d 53s var(--ease) infinite alternate;
}
@keyframes gh-fog-a { from { transform: translate3d(0, 0, 0) scale(1); }    to { transform: translate3d(14vw, 6vh, 0) scale(1.18); } }
@keyframes gh-fog-b { from { transform: translate3d(0, 0, 0) scale(1.1); }  to { transform: translate3d(-12vw, 9vh, 0) scale(0.92); } }
@keyframes gh-fog-c { from { transform: translate3d(0, 0, 0) scale(0.95); } to { transform: translate3d(9vw, -11vh, 0) scale(1.22); } }
@keyframes gh-fog-d { from { transform: translate3d(0, 0, 0) scale(1.05); } to { transform: translate3d(-16vw, -7vh, 0) scale(0.88); } }

/* The mesh the light moves against.
 *
 * Without it the masses read as a blurry gradient and the motion is hard to
 * perceive at all; a fixed structure in front of them is what makes the drift
 * legible. Kept at a hairline and a very low contrast so it is texture rather
 * than a grid somebody has to look at. */
.hero > .fog .mesh {
  position: absolute; inset: -1px;
  background-image:
    repeating-linear-gradient(90deg, var(--edge) 0 1px, transparent 1px 46px),
    repeating-linear-gradient(0deg, var(--edge) 0 1px, transparent 1px 46px);
  opacity: 0.5;
  -webkit-mask-image: radial-gradient(90% 75% at 50% 12%, var(--fg), transparent 70%);
  mask-image: radial-gradient(90% 75% at 50% 12%, var(--fg), transparent 70%);
}

/* Weather is the first thing to go. Somebody who has asked for less motion is
   asking about exactly this, and the hero still has its spotlight and its
   layered light, so switching the drift off costs the design nothing. */
@media (prefers-reduced-motion: reduce) {
  .hero > .fog i { animation: none; }
}
.hero h1 { margin: 0 0 var(--s-4); }
.hero .sub { color: var(--mut); font-size: 16px; max-width: 44ch; margin: 0 0 var(--s-6); }
.cta { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* LIGHT 4: the phrase is LIT, never boxed. A bloom that fades out before it
   reaches any edge, plus a line of light under the words. Nothing here has a
   corner, because the first version was a filled slab and it read as exactly
   what it was. */
.hero mark {
  background: none;
  color: var(--acc-txt);
  position: relative;
  padding: 0;
  text-shadow: 0 0 28px var(--bloom), 0 0 60px var(--bloom);
}
.hero mark::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  left: -0.4em; right: -0.4em; top: -0.34em; bottom: -0.3em;
  background: radial-gradient(56% 52% at 50% 56%, var(--bloom), transparent 72%);
  opacity: 0;
  transition: opacity var(--t-bloom) var(--ease);
}
.hero mark::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -0.03em; height: 2px;
  background: linear-gradient(90deg, transparent, var(--acc) 16%, var(--acc) 84%, transparent);
  box-shadow: 0 0 16px -3px var(--acc);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--t-reveal) var(--ease) 0.12s;
}
.hero[data-seen="1"] mark::before { opacity: 1; }
.hero[data-seen="1"] mark::after { transform: scaleX(1); }

.hint { color: var(--mut); font-size: var(--t-sm); max-width: 48ch; margin: 0 0 var(--s-4); }

/* how it works */
.steps { list-style: none; margin: 0 0 var(--s-5); padding: 0; }
.steps li {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-3);
  padding: var(--s-3) 0; border-bottom: 1px solid var(--ln); align-items: start;
}
.steps li:last-child { border-bottom: 0; }
.steps i {
  font-style: normal; width: 26px; height: 26px; display: grid; place-items: center;
  font-size: 11.5px; font-family: var(--mono); font-variant-numeric: tabular-nums;
  border: 1px solid var(--acc); border-radius: var(--rad-soft); color: var(--acc-txt);
  box-shadow: 0 0 18px -8px var(--acc), inset 0 1px 0 var(--edge);
}
.steps em { font-style: normal; display: block; font-size: var(--t-sm); font-weight: 500; margin-bottom: 2px; }
.steps small { color: var(--mut); font-size: var(--t-xs); line-height: 1.5; display: block; }

/* stat strip */
.stats { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--ln); }
.stat { padding: var(--s-5) var(--s-6); position: relative; transition: background-color var(--t-tint) ease; }
.stat::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: var(--acc); box-shadow: 0 0 10px -1px var(--acc);
  transform: scaleX(0); transform-origin: center;
  transition: transform var(--t-move) var(--ease);
}
.stat:hover { background: var(--hov); }
.stat:hover::before { transform: scaleX(1); }
.stat + .stat { border-left: 1px solid var(--ln); }
.stat b { display: block; font-size: clamp(23px, 3.2vw, 32px); font-weight: 600; letter-spacing: -0.025em; margin-bottom: 1px; }
.stat span { color: var(--mut); }
@media (max-width: 640px) {
  .stats { grid-template-columns: repeat(2, 1fr); }
  .stat + .stat { border-left: 0; }
  .stat:nth-child(even) { border-left: 1px solid var(--ln); }
  .stat:nth-child(n+3) { border-top: 1px solid var(--ln); }
}

.rname em { font-style: normal; display: block; font-size: var(--t-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.state { font-size: 11.5px; margin-top: 3px; display: inline-block; color: var(--mut); }
.state[data-s="working"] { color: var(--ok); }
.state[data-s="maintenance"] { color: var(--warn); }
.num { font-family: var(--mono); font-size: var(--t-sm); white-space: nowrap; color: var(--acc2); }
.art.none { display: grid; place-items: center; color: var(--rank); font-size: 15px; font-weight: 600; }

/* ── the hero buttons ──────────────────────────────────────────────────────
 * The owner asked for a real action on hover: the text changing and a neon
 * white glow, as smooth as it can be made.
 *
 * The label SLIDES rather than being replaced. Swapping textContent on hover
 * flickers, cannot be eased, and reflows the button under the pointer, which is
 * the one place a width change is felt as a mistake. Two labels stacked in the
 * same box move together instead, and the button is sized by the wider of them
 * so nothing shifts at all.
 *
 * Every property here is transform, opacity, colour or shadow. Nothing animates
 * a size, so this stays on the compositor and holds its frame rate on a phone,
 * which is most of what "smooth" actually means.
 */
.cta .btn.glow {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition: transform var(--t-move) var(--ease),
              box-shadow var(--t-move) var(--ease),
              border-color var(--t-tint) ease,
              color var(--t-tint) ease;
}
.cta .btn.glow .ph {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  transition: transform var(--t-enter) var(--ease), opacity var(--t-tint) ease;
}
/* The incoming label sits exactly on top of the outgoing one, so the control
   never changes width and the two never overlap visually. */
.cta .btn.glow .ph.b {
  position: absolute;
  inset: 0;
  justify-content: center;
  transform: translateY(105%);
  opacity: 0;
}
.cta .btn.glow:hover .ph.a,
.cta .btn.glow:focus-visible .ph.a { transform: translateY(-105%); opacity: 0; }
.cta .btn.glow:hover .ph.b,
.cta .btn.glow:focus-visible .ph.b { transform: none; opacity: 1; }

/* The glow. White rather than the accent, because the accent is already the
   button and a brighter version of it reads as a state change rather than as
   light. A wide soft halo plus a tight inner rim: one is the light, the other is
   the edge catching it. */
.cta .btn.glow:hover,
.cta .btn.glow:focus-visible {
  transform: translateY(-2px);
  border-color: var(--glow-rim);
  box-shadow:
    0 0 0 1px var(--glow-ring),
    0 0 18px -2px var(--glow-soft),
    0 0 44px -8px var(--bloom),
    0 14px 30px -18px var(--glow-shade);
}
.cta .btn.glow:active { transform: translateY(0) scale(0.985); }

/* A single sweep of light across the face, once per hover. Behind the label on
   its own layer, so it lights the button rather than washing out the words. */
.cta .btn.glow::before {
  content: "";
  position: absolute;
  inset: -40% -10%;
  z-index: -1;
  background: linear-gradient(105deg, transparent 38%, var(--sweep) 50%, transparent 62%);
  transform: translateX(-115%);
  opacity: 0;
  transition: transform 0.72s var(--ease), opacity var(--t-tint) ease;
}
.cta .btn.glow:hover::before,
.cta .btn.glow:focus-visible::before { transform: translateX(115%); opacity: 1; }

/* The arrow leans into the direction it points, which is the smallest possible
   confirmation that the control noticed the pointer. */
.cta .btn.glow:hover .ic-arw { transform: translateX(3px); }

@media (prefers-reduced-motion: reduce) {
  .cta .btn.glow,
  .cta .btn.glow .ph,
  .cta .btn.glow::before,
  .cta .btn.glow .ic-arw { transition: none; }
  .cta .btn.glow:hover { transform: none; }
  .cta .btn.glow::before { display: none; }
}

/* ── the menu, on the page ──────────────────────────────────────────────────
 *
 * Every script hub shows screenshots, and a screenshot proves nothing about
 * whether the thing responds. These are the real controls, wired by the same
 * controls.js the scripts page uses, so a visitor can flip a switch before
 * paying rather than look at a picture of one.
 *
 * The window is deliberately not a browser chrome or a phone frame. It is the
 * shape of the in-game panel, because that is what they will actually see. */
/* The copy is a column, the window is the room.
 *
 * This used to be `1fr` for the words and a hard 26rem cap for the window,
 * which gave the window 416 pixels to hold a five tab menu and left the copy
 * with six hundred. The window then grew downwards to fit its own contents and
 * ended up 416 by 707: a tall narrow slab, when the thing it is a picture of is
 * a landscape panel on a landscape screen.
 *
 * So the cap moved to the WORDS, which have a natural width anyway at 46ch, and
 * the window takes what is left. */
.uidemo {
  display: grid; grid-template-columns: minmax(0, 21rem) minmax(0, 1fr);
  gap: var(--s-8); align-items: center;
  padding: var(--s-10) var(--s-6);
  border-top: 1px solid var(--ln);
  position: relative; overflow: hidden;
  background: radial-gradient(620px circle at 88% 40%, var(--spot), transparent 64%);
}
.uidemo > * { position: relative; z-index: 2; }
.uicopy h2 { margin: 0 0 var(--s-3); font-size: var(--t-h2); }
.uicopy .sub { color: var(--mut); font-size: var(--t-sm); max-width: 46ch; margin: 0 0 var(--s-5); }

/* The window has to look like a window, which means it needs to sit ABOVE the
 * page rather than be drawn on it.
 *
 * It was a flat panel: one hairline border, one shadow, the same fill as every
 * card on the site. Next to a real screenshot of the in-game menu that reads as
 * a wireframe of the product rather than the product. Three things carry the
 * difference and none of them is decoration.
 *
 *   a rim that is lighter at the top than the bottom, which is what tells the
 *     eye a surface is raised rather than printed;
 *   a shadow with two ranges, one tight and one wide, because a single blur
 *     reads as a sticker and two read as height;
 *   a bloom underneath it in the brand accent, so the window is lit by the same
 *     light as the rest of the page instead of floating in its own vacuum.
 */
.uiwin {
  position: relative;
  border: 1px solid var(--ln); border-radius: var(--rad-large);
  background: linear-gradient(180deg, var(--sur), var(--bg) 78%);
  box-shadow:
    inset 0 1px 0 var(--glow-rim),
    inset 0 -1px 0 var(--glow-shade),
    0 2px 6px -2px var(--scrim-base),
    0 40px 80px -30px var(--scrim-base);
  overflow: hidden;
  transition:
    transform var(--t-reveal) var(--ease),
    opacity var(--t-reveal) ease,
    box-shadow var(--t-move) var(--ease);

  /* 16:10, because that is the shape of the thing it is a picture of.
   *
   * Left to its own devices this window was whatever height its longest panel
   * needed, which came out at 416 by 707. A portrait slab does not read as a
   * game overlay; it reads as a phone. A fixed ratio also means the window is
   * the same shape on every screen, so the screenshot in somebody's head
   * matches what they see when they buy.
   *
   * Three rows rather than free flow: the title bar and the status bar are the
   * window's edges and keep their own height, and the middle is whatever is
   * left. `minmax(0, 1fr)` rather than `1fr` because a grid row defaults to a
   * min-height of auto, and without it the tallest panel would push the window
   * taller and the ratio would go back to being a suggestion. */
  aspect-ratio: 16 / 10;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  /* A floor under the ratio.
   *
   * Between the 900px breakpoint and about 1100px the window is a side column
   * roughly 500px wide, and 16:10 of that is 312px tall. The title bar and the
   * status bar take 105 of those, which leaves less room than the five tab rail
   * needs, and the fifth tab was cut off by the window's own overflow: present
   * in the layout, drawing nothing. Found by the icon checker, which measured
   * #i-list at zero ink and had no idea why.
   *
   * min-height wins over aspect-ratio, so below this width the window is 25rem
   * tall and a little wider than 16:10 rather than a little shorter. Wider is
   * the harmless direction: nothing inside is cut off by having room. */
  min-height: 25rem;
}
/* Under 900px the window stops being a side-by-side column and gets the full
   width, where 16:10 is generous rather than cramped.
   Below 620px it turns over. A landscape ratio on a 342 pixel phone is a 214
   pixel window, which is a letterbox rather than a menu, so the shape follows
   the screen. Still a fixed ratio: without one the window grows to whatever its
   longest panel needs and stops looking like a window at all, which is the
   thing this ratio was added to fix. */
@media (max-width: 620px) {
  .uiwin { aspect-ratio: 3 / 4; }
}
/* The entrance, and the reason it is an animation rather than a resting state.
 *
 * This used to be `opacity: 0` on the rule above with the reveal keyed off
 * data-seen, which made shell.js load-bearing for the largest section on the
 * home page: block the file, fail its request, hit a parse error, and "Touch it
 * before you buy it" was a seven hundred pixel hole under its own heading.
 * Confirmed against production by loading the page with scripts refused.
 * shell.js opens by promising it is an enhancement over markup that already
 * works, and this was the one thing on the site that made that untrue.
 *
 * An animation on the SEEN state has no such state to be stuck in. Nothing is
 * hidden and then argued back into view: the window's resting appearance is the
 * finished one, and arriving is something that happens TO it when the section
 * is reached. No shell, no observer, no animation, and the window is simply
 * there. Reduced motion lands in the same place, since shell.css switches every
 * animation off and what remains is the resting state. */
@keyframes gh-uiwin-arrive {
  from { opacity: 0; transform: translateY(18px) scale(0.985); }
  to { opacity: 1; transform: none; }
}
@keyframes gh-uiwin-bloom { from { opacity: 0; } to { opacity: 1; } }
.uidemo[data-seen="1"] .uiwin { animation: gh-uiwin-arrive var(--t-reveal) var(--ease) both; }
.uidemo[data-seen="1"] .uiwin::after { animation: gh-uiwin-bloom var(--t-bloom) var(--ease) both; }
/* The light it sits in. Behind the window, wider than it, and clipped by
   nothing, so the edges of the panel are the brightest part of it. */
.uiwin::after {
  content: ""; position: absolute; inset: -1px; z-index: -1; pointer-events: none;
  border-radius: inherit;
  box-shadow: 0 0 70px -10px var(--bloom);
  transition: opacity var(--t-bloom) var(--ease);
}
/* Leans toward the pointer. A menu you can touch should answer when you get
   near it, and a fraction of a degree is enough to say so. */
.uiwin:hover {
  box-shadow:
    inset 0 1px 0 var(--glow-rim),
    inset 0 -1px 0 var(--glow-shade),
    0 2px 6px -2px var(--scrim-base),
    0 48px 90px -28px var(--scrim-base);
}

/* ── the Nova window ────────────────────────────────────────────────────────
 *
 * Shaped after the menu a customer actually gets in Roblox: a title bar naming
 * the game and its build, an icon rail down the left, panels with their own
 * headers, and a status strip along the bottom.
 *
 * It was a flat stack of four rows, which is a settings list and is not what
 * anybody sees when they run a script. The section is called "touch it before
 * you buy it", so the thing being touched has to be the thing being sold.
 * The controls inside are still the real ones, wired by the same controls.js
 * the scripts page uses, which is the entire reason this is markup rather than
 * a screenshot. */
.uibar {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4); border-bottom: 1px solid var(--ln);
  background: var(--tick);
}
.uimark { flex: none; display: grid; place-items: center; }
.uimark img {
  width: 22px; height: 22px; display: block; border-radius: var(--rad-soft);
  filter: drop-shadow(0 0 7px var(--bloom));
}
.uiname { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.uiname b {
  font-size: var(--t-xs); font-weight: 700;
  letter-spacing: var(--track-label); text-transform: uppercase;
}
.uiname em {
  font-style: normal; font-size: var(--t-xs); color: var(--mut);
  font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Not a real input. It is here because the real menu has one and its absence
   was the loudest difference; making it focusable would offer a search that
   does nothing, which is worse than showing none. */
.uisearch {
  margin-left: auto; display: flex; align-items: center; gap: var(--s-2);
  padding: 5px var(--s-2) 5px var(--s-3); border-radius: var(--rad-pill);
  border: 1px solid var(--ln); background: var(--inp);
  color: var(--mut); font-size: var(--t-xs); white-space: nowrap;
}
.uisearch .ic { width: 13px; height: 13px; color: var(--mut); filter: none; }
.uisearch kbd {
  font-family: var(--mono); font-size: var(--t-xs); color: var(--mut);
  border: 1px solid var(--ln); border-radius: var(--rad-sharp);
  padding: 0 5px; background: var(--sur);
}
.uiwbtn {
  width: 9px; height: 9px; border-radius: var(--rad-pill);
  background: var(--rank); flex: none;
}
.uiwbtn.is-x { background: var(--acc); box-shadow: 0 0 9px -1px var(--acc); }

/* A floor, so the window has presence whichever card is showing.
   The two cards hold different numbers of controls, and without this the whole
   frame shrank to whichever was shorter the moment somebody switched tabs,
   which reads as the product having less in it than it does. */
/* `min-height: 0` rather than the 26rem it used to carry: the window owns its
   own height now, and a minimum here would fight the ratio above and win. */
.uibody { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 0; }

.uirail {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--s-3) var(--s-2);
  border-right: 1px solid var(--ln); background: var(--tick);
  /* And a backstop under the floor. The min-height above is sized for five
     tabs; a sixth, or a longer word in another language, would put us straight
     back to a tab nobody can reach. */
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: none;
}
.uirail::-webkit-scrollbar { width: 0; }
.uitab {
  appearance: none; background: none; border: 0; cursor: pointer;
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: var(--s-2) var(--s-2); width: 4.6rem;
  border-radius: var(--rad-soft); color: var(--mut);
  font: inherit; font-size: var(--t-xs); line-height: 1.1; text-align: center;
  transition: color var(--t-tint) ease, background-color var(--t-tint) ease;
}
.uitab .ic {
  width: 17px; height: 17px; color: inherit; filter: none;
  transition: transform var(--t-move) var(--ease);
}
.uitab:hover { color: var(--fg); background: var(--hov); }
.uitab:hover .ic { transform: translateY(-2px); }
/* The selected tab is marked by a LIT RAIL down its inside edge, not by a
   filled box.
 *
 * It was a filled slab with a full accent outline, which at this size is the
 * heaviest object in the window and pulls the eye away from the controls the
 * section exists to show. A rail says the same thing in two pixels and is what
 * the real menu does. */
/* --hov, not --glow-soft. The comment above says the selected tab is a rail
   rather than a filled box, and the rule under it was filling it with
   rgba(255,255,255,.42): a light grey slab, the exact thing the comment says
   this is not, and the heaviest object in a window meant to show off controls.
   --hov is the surface every other selected row on the site uses, so the rail
   keeps carrying the accent and the tab just sits a shade proud of the menu. */
.uitab.is-on { color: var(--fg); background: var(--hov); }
.uitab.is-on::before {
  content: ""; position: absolute; left: 0; top: 18%; bottom: 18%; width: 2px;
  border-radius: var(--rad-pill);
  background: var(--acc); box-shadow: 0 0 12px -1px var(--acc);
}
.uitab.is-on .ic { color: var(--acc-txt); }

/* ── the page header inside the window ──────────────────────────────────────
   What the real script has and this did not: where you are, what the page is
   called, and the sub-tabs inside it. Without them the window read as a
   settings list. With them it reads as an application, which is what somebody
   is deciding whether to pay for. */
.uihead {
  padding: var(--s-3) var(--s-4) 0;
  border-bottom: 1px solid var(--ln);
  display: grid; gap: var(--s-2);
}
.uicrumb {
  margin: 0; display: flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 10px; letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--mut);
}
.uicsep { opacity: .5; }
.uititle {
  margin: 0; font-size: var(--t-h3); font-weight: 650; letter-spacing: -0.012em;
  color: var(--fg);
}
.uitabs { display: flex; gap: var(--s-2); flex-wrap: wrap; }
.uichip {
  appearance: none; border: 1px solid var(--ln); background: transparent;
  color: var(--mut); font: inherit; font-size: var(--t-xs); font-weight: 500;
  padding: 5px var(--s-3); border-radius: var(--rad-pill); cursor: pointer;
  position: relative; top: 1px;
  transition: color var(--t-tint) ease, border-color var(--t-tint) ease,
              background-color var(--t-tint) ease, transform var(--t-move) var(--ease);
}
.uichip:hover { color: var(--fg); border-color: var(--edge); transform: translateY(-1px); }
/* The selected sub-tab is the one place in this window that carries the accent
   as a FILL. It is the smallest object here, so it can hold the strongest
   colour without competing with the controls. */
.uichip.is-on {
  color: var(--on-acc); background: var(--acc); border-color: var(--acc);
  box-shadow: 0 0 14px -4px var(--acc);
}

/* Scrolls, because the window is now a fixed shape and the panel inside it is
   longer than that on purpose. The real menu scrolls too, so this is the
   product rather than a compromise, and `overscroll-behavior` stops the page
   from taking over when the panel reaches its end. */
.uipane {
  display: grid; gap: 0; align-content: start;
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin; scrollbar-color: var(--ln) transparent;
}

/* Two cards side by side, the way the real macros page lays out. One column
   under 760px, where side-by-side would make each card narrower than its own
   button row. */
.uicards {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  align-content: start;
}
.uicards .uicard { border-bottom: 1px solid var(--ln); border-right: 1px solid var(--ln); }
.uicards .uicard:last-child { border-right: 0; }

/* The action row the real cards end with. */
.uiacts {
  display: flex; gap: var(--s-2); flex-wrap: wrap;
  padding: var(--s-3) var(--s-4);
  border-top: 1px solid var(--ln);
}
.uibtn {
  appearance: none; border: 1px solid var(--ln); background: var(--inp);
  color: var(--fg); font: inherit; font-size: var(--t-xs); font-weight: 550;
  padding: 6px var(--s-3); border-radius: var(--rad-sharp); cursor: pointer;
  transition: border-color var(--t-tint) ease, background-color var(--t-tint) ease,
              transform var(--t-move) var(--ease);
}
.uibtn:hover { border-color: var(--edge); transform: translateY(-1px); }
.uibtn:active { transform: translateY(0); }
/* Semantic colour, carried by text and hairline rather than a fill, for the
   same reason the executor pills are: three saturated slabs in one row is
   confetti and the destructive one stops standing out. */
.uibtn.ok { color: var(--ok); border-color: var(--ok); }
.uibtn.danger { color: var(--acc-txt); border-color: var(--acc-txt); }
@media (prefers-reduced-motion: reduce) {
  .uichip, .uibtn { transition: none; }
  .uichip:hover, .uibtn:hover { transform: none; }
}

.uipanel { border-bottom: 1px solid var(--ln); }
.uipanel:last-child { border-bottom: 0; }
.uipanel h4 {
  display: flex; align-items: center; gap: var(--s-2);
  margin: 0; padding: var(--s-3) var(--s-4) var(--s-2);
  font-size: var(--t-xs); font-weight: 700;
  letter-spacing: var(--track-label); text-transform: uppercase;
  position: relative;
}
.uipanel h4 .ic { width: 14px; height: 14px; }
/* The lit rule under a panel title, which is the detail that makes the real
   menu look like itself. Short and left-aligned rather than a full divider. */
.uipanel h4::after {
  content: ""; position: absolute; left: var(--s-4); bottom: 0; width: 2.2rem; height: 2px;
  background: var(--acc); box-shadow: 0 0 10px -2px var(--acc); border-radius: var(--rad-pill);
}

/* Tighter than a settings page, because this is a game menu.
 *
 * The rows were on the site's ordinary card spacing, which left the window
 * mostly empty and made four controls look like a form. A real in-game menu is
 * dense: the label and its note are one block, and the control sits close. */
.uirow {
  display: grid; grid-template-columns: 1fr auto; gap: var(--s-4); align-items: center;
  padding: 9px var(--s-4); border-top: 1px solid var(--ln);
  position: relative;
  transition: background-color var(--t-tint) ease;
}
.uirow:hover { background: var(--hov); }
/* The same lit rail as the tab, arriving on hover. One idea used twice reads as
   a system; two different hover treatments read as two people. */
.uirow::before {
  content: ""; position: absolute; left: 0; top: 6px; bottom: 6px; width: 2px;
  border-radius: var(--rad-pill);
  background: var(--acc); box-shadow: 0 0 10px -1px var(--acc);
  transform: scaleY(0); transform-origin: center;
  transition: transform var(--t-move) var(--ease);
}
.uirow:hover::before { transform: scaleY(1); }
.uirow b { display: block; font-size: var(--t-sm); font-weight: 500; letter-spacing: -0.005em; }
.uirow span { display: block; font-size: var(--t-xs); color: var(--mut); margin-top: 1px; }

.uifoot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-2) var(--s-4); border-top: 1px solid var(--ln);
  background: var(--tick);
  font-family: var(--mono); font-size: var(--t-xs); color: var(--mut);
}
.uistat { display: flex; align-items: center; gap: 6px; letter-spacing: var(--track-label); }
/* The only thing in the window that moves on its own. A menu that is running
   should look like it is running, and one slow pulse says that without asking
   for attention. */
.uistat i {
  width: 7px; height: 7px; border-radius: var(--rad-pill); flex: none;
  background: var(--ok); box-shadow: 0 0 9px -1px var(--ok);
  animation: gh-ui-pulse 2.6s var(--ease) infinite;
}
@keyframes gh-ui-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.uimeta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

@media (max-width: 560px) {
  /* The rail becomes a strip of icons. Five labelled columns plus content does
     not fit a phone, and a horizontal rail is what the real menu does there. */
  .uibody { grid-template-columns: 1fr; }
  .uirail { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--ln); }
  .uitab { width: auto; flex: none; min-width: 4rem; }
  .uisearch { display: none; }
}

@media (max-width: 900px) {
  .uidemo { grid-template-columns: 1fr; gap: var(--s-6); padding: var(--s-8) var(--s-6); }
}
@media (prefers-reduced-motion: reduce) {
  /* Nothing to undo here any more. The window's entrance now lives inside a
     `no-preference` query, so under reduced motion it is simply never hidden
     rather than hidden and then argued back into view. Kept as a line so the
     next reader looking for the reduced-motion answer finds it. */
  .uiwin { transition: none; }
}
