/* AdminAbuse.app — the partner skin for channel `sudo`.
   Every rule below opens on `html.skin-adminabuse`, so it wins by SPECIFICITY
   and never by order — the order is not this sheet's to rely on. The Worker's
   partner shell carries this file's `<link>` in the document's own `<head>`,
   so it loads AHEAD of `ui::STYLES` and `soft.rs`'s sheet, which the island
   injects at mount; the native `watch.html` the gates drive attaches it after
   both. The class is what makes those two the same page. No `!important`
   anywhere, and nothing outside the scope. A base declaration that has to
   survive this sheet is restated here under the same class — the footer
   lockup's `.brand .o` at the bottom is the one that is. See
   `specs/ADMINABUSE_ROOM.md` §3-§5 for the palette and the DOM it targets.

   `.pill.live` is never named here. That is the whole of how it stays
   untouched: `--ink` and `--orange` are never redeclared either, so the
   on-air indicator (`soft.rs`'s `.pill.live:where(.soft *)`) renders exactly
   as it does off this skin. The partner's gold mark does not appear below
   either — §3 holds that colour for the crown and the logo, never chrome. */

/* The partner's faces. Committed under `fonts/`, not fetched: unlike
   `web/player/vendor/` (gitignored, rebuilt by `fetch-vendor.sh` on a machine
   with `pip`), `skins/` ships in git on the same footing as `models/`, and
   the build that deploys this tree runs in a bare container with no path to
   install anything. `fonts/LICENSE.md` beside these two files has the
   licence, the exact `google/fonts` commit and paths, and the conversion
   command that produced them.

   Weight ranges are each file's own `fvar` axis, read with `fontTools` — 100-
   900 and 300-700 — not the 300-900 / 400-600 a first pass assumed from the
   family names alone. Relative `url()`s: this sheet is loaded as a `<link>`
   under `skins/adminabuse/`, and an external stylesheet's relative URLs
   resolve against the sheet's own location, not the page's — so `fonts/…`
   here is `skins/adminabuse/fonts/…` regardless of what page loaded it. */
@font-face {
  font-family: "Libre Franklin";
  src: url("fonts/libre-franklin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Spline Sans Mono";
  src: url("fonts/spline-sans-mono.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

html.skin-adminabuse {
  --bg: #050505;
  --surface: #0b0812;
  --surface-2: #120c1c;
  --line: rgba(168,85,247,.32);
  --text: #F7F7F3;
  /* 8.28:1 on --surface (#0b0812), 8.50:1 on --bg, 8.00:1 on --surface-2 —
     WCAG relative-luminance formula, all three clear 4.5:1 with room held in
     reserve; no lightness change needed. */
  --muted: #B39DDB;
  --cobalt: #7c3aed;       /* brand-violet: primary fill, 5.70:1 under white */
  --cobalt-lift: #a855f7;  /* brand-purple: the lifted accent, 5.15:1 on --bg */
  --heat: #e879f9;         /* brand-magenta: the meter's hot end, glow only */
  --cyan: #22d3ee;         /* the one cyan the palette table names; 11.28:1 on --bg */
  --radius: 14px;
  /* Hue-matched to the violet ground rather than the ink shadow ui.rs ships. */
  --shadow: 0 1px 2px rgba(60,20,120,.45), 0 8px 24px rgba(45,15,95,.38);
  --shadow-lift: 0 2px 4px rgba(60,20,120,.5), 0 14px 36px rgba(45,15,95,.48);
  --font-brand: "Libre Franklin", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-ui: "Libre Franklin", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "Spline Sans Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --font-display: "Libre Franklin", ui-sans-serif, system-ui, sans-serif;
  /* --danger and --orange are not redeclared: the error state and the LIVE
     signal stay exactly what `ui.rs` already set them to. */
}

/* The vortex: one wash, static, replacing the cobalt radial `body` ships
   with. A second, fainter ring in magenta keeps it from reading as a single
   flat glow. */
html.skin-adminabuse body {
  background:
    radial-gradient(1100px 680px at 50% -8%, rgba(124,58,237,.22), transparent 62%),
    radial-gradient(900px 520px at 88% 8%, rgba(232,121,249,.10), transparent 60%),
    var(--bg);
}

/* Cards: the gradient §5 specifies, not the surface tokens — a darker range
   than the chrome those tokens drive elsewhere. Border is a 1px violet→
   magenta seam at 35% alpha, via the padding-box/border-box split so no
   pseudo-element is spent on it. `border-radius` and the two shadows still
   come from `--radius` and `soft.rs`'s (now violet) `--up-2`. */
html.skin-adminabuse .card {
  background: linear-gradient(180deg, #0b0812, #07050c) padding-box,
              linear-gradient(135deg, rgba(124,58,237,.35), rgba(232,121,249,.35)) border-box;
  border: 1px solid transparent;
}

/* The soft layer's material, retinted off the ink hue onto violet. Every
   button, well, pill, meter groove and chat log inherits these four colours
   and two lights, so this one block is most of "the dock buttons" and
   "the chat-log family" at once — `soft.rs`'s own rules are untouched, only
   what they read is. Label still clears 7.74:1 on `--face`, 8.32:1 on
   `--plate` (`--muted`'s own formula, off `--face`'s new value). */
html.skin-adminabuse .soft {
  --plate: #1a1228;
  --face: #201933;
  --well: #130d1f;
  --shade: #0a0512;
  --sheen: #4a3568;
  --seam: #150f22;
  --label: #b9a8d9;
}

/* The stage frame. A static 1px violet→magenta→cyan ring on the padding-box
   split (reliable under any renderer), plus an oversized `::before` behind
   the picture that turns on `transform` alone — the compositor-only property
   `web.dev`'s stick-to-compositor-only-properties names, never the gradient's
   own `from <angle>`, which `MDN`'s animatable-properties page marks
   main-thread. `overflow: hidden` (unchanged from `ui.rs`) clips the square
   to the stage's own rounded rect. */
html.skin-adminabuse .stage {
  border: 1px solid transparent;
  background: linear-gradient(#04060B, #04060B) padding-box,
              conic-gradient(from 0deg, var(--cobalt), var(--heat), var(--cyan), var(--cobalt)) border-box;
}
html.skin-adminabuse .stage::before {
  content: "";
  position: absolute;
  inset: -45%;
  background: conic-gradient(from 0deg, var(--cobalt), var(--heat), var(--cyan), var(--cobalt));
  z-index: 0;
}
/* And it turns only while there is something to see it turn.
   On air the picture covers this square entirely, by design — so the
   animation was forty seconds a revolution of a gradient nobody could see,
   for the whole life of the page, on a page that is also decoding video and
   drawing a sky. Measured here at one core. `.placeholder` is the off-air
   panel and is a direct child of the stage exactly while the picture is not
   showing (`theatre.rs`'s `Show`), so `:has()` is the state, read from the
   DOM rather than from a second class the island would have to keep in step.
   The static ring at the edge is the `background` above and is always there.
   `getAnimations()` on air lists nothing for this element, which is what
   `sudoroom` reads. */
html.skin-adminabuse .stage:has(> .placeholder)::before {
  animation: adminabuse-frame-spin 40s linear infinite;
}
@keyframes adminabuse-frame-spin {
  to { transform: rotate(1turn); }
}

/* The dock's one filled action. `ui.rs` and `soft.rs` both hardcode GitHub
   blue mid-stops (`#2E4EFF`, `#4763FF`…) between token ends, which a bare
   `--cobalt` remap cannot reach — restated here so Watch, Enable sound and
   Sign in read as the palette table's brand-violet fill, not a blue-violet
   seam.

   The stops go DOWN from brand-violet, never up into purple and magenta.
   `.primary` sets its text to `var(--cloud)` (`ui.rs`), and Voxi Cloud on
   brand-purple is 3.68:1 and on brand-magenta 2.29:1 — a fill this text
   cannot be read on. `#7c3aed` measures 5.31:1 under it and `#6d28d9`
   6.62:1, which is what `scripts/check-skin-contrast.mjs` reads out of these
   two declarations. §3's palette table is the source of both hexes: violet is
   the primary fill and magenta "carries glow, never text". */
html.skin-adminabuse button.primary, html.skin-adminabuse a.button.primary {
  background: linear-gradient(180deg, #7c3aed, #5b21b6);
  border-color: var(--cobalt);
  box-shadow: 0 2px 12px rgba(124,58,237,.38);
}
html.skin-adminabuse button.primary:hover:not(:disabled), html.skin-adminabuse a.button.primary:hover {
  background: linear-gradient(180deg, #6d28d9, #4c1d95);
  border-color: var(--cobalt);
  box-shadow: 0 6px 22px rgba(124,58,237,.5);
}
html.skin-adminabuse button.primary:active:not(:disabled) {
  background: linear-gradient(180deg, #4c1d95, #5b21b6);
  box-shadow: inset 3px 3px 8px rgba(0,0,0,.45), inset -2px -2px 6px rgba(168,85,247,.25);
}
/* The Mute toggle's "on" face — same mop-up, on the state rather than the
   fill. The label is `--text` and not `--cobalt-lift`. Measured off the
   composited page (this wash renders as `rgb(22,11,31)` over the dock):
   brand-purple on it is 4.82:1 and Voxi Cloud 17.75:1 — the word on a pressed
   control is text, and it now has text's headroom rather than a third of a
   point of it. */
html.skin-adminabuse button.on {
  background: rgba(168,85,247,.20);
  border-color: rgba(168,85,247,.55);
  color: var(--text);
}

/* The Live Room call, in the partner's display weight — `ui.rs` never sets
   one, so it inherited the body's 400 and was the quietest heavy line on
   the page. */
html.skin-adminabuse .crowd-call { font-weight: 800; }
/* Violet to magenta, per §5, not the purple `--cobalt-lift` the base rule
   reads by default. */
html.skin-adminabuse .meter-fill { background: linear-gradient(90deg, var(--cobalt), var(--heat)); }

/* Chat: names in brand-purple, the HOST chip filled brand-violet under cloud
   text (5.70:1, the same pairing the primary button uses). */
html.skin-adminabuse .chat-name { color: var(--cobalt-lift); }
html.skin-adminabuse .chip { background: var(--cobalt); color: var(--text); }
/* Off-screen while scrolled past, per §13's render budget. The log's own
   `max-height: 220px` sets the intrinsic size; width is the chat card's
   typical content box. */
html.skin-adminabuse .chat-log { content-visibility: auto; contain-intrinsic-size: 300px 220px; }

/* The composer's focus ring. `soft.rs` hardcodes Voxi cobalt here
   (`rgba(23,59,255,.28)`) rather than reading a token, so it is the one place
   left blue by the remap above without this. `var(--down-2)` is restated
   because `box-shadow` does not merge — dropping it would also drop the
   pressed-well shadow underneath the ring. */
html.skin-adminabuse select:focus, html.skin-adminabuse input:focus {
  border-color: var(--cobalt-lift);
  box-shadow: var(--down-2), 0 0 0 3px rgba(168,85,247,.28);
}

/* Every other focus ring on the page (`:focus-visible` on every button, link
   and the volume slider) already reads `var(--cobalt-lift)`, so the token
   remap above alone turns it violet at 2px, offset per control exactly as
   `ui.rs`/`soft.rs` already set it — nothing to restate. */

/* Reactions: the partner's glyph images. `theatre.rs` renders the same
   `img.glyph` in two places — the floating burst and the React card's own
   buttons — and neither is sized anywhere but here, so an unstyled `<img>`
   would paint at its native (skin-asset) resolution in both. */
html.skin-adminabuse .float img.glyph {
  width: 28px; height: 28px;
  filter: drop-shadow(0 0 6px rgba(232,121,249,.65));
}
html.skin-adminabuse button.emoji img.glyph { width: 20px; height: 20px; display: block; }

/* Off air: Sudo idling above the stopwatch.

   The host is sized against the STAGE, which is why `.stage` is a size
   container. `max-height: 42%` resolved against the placeholder's own grid
   area, and a `place-content: center` grid sizes its rows from their content —
   an indefinite height, against which a percentage max-height computes to
   `none`. So the mascot painted at its intrinsic 215px inside a 180px stage at
   360px wide and the whole column overflowed a stage that clips: measured
   2026-09-05, a blank picture on a phone and a countdown cut off below the
   frame at 1440x900. `cqh` is a length against the container's height, and the
   container is the one box this content has to fit inside. */
html.skin-adminabuse .stage { container-type: size; }
html.skin-adminabuse .placeholder {
  align-content: center; justify-content: center;
  padding: 10px; gap: 6px;
}
html.skin-adminabuse .placeholder .host {
  height: 42cqh; max-height: none; width: auto; min-height: 0;
  object-fit: contain; justify-self: center;
  animation: adminabuse-host-float 6s ease-in-out infinite;
}
/* Under a skin the stopwatch IS the schedule (`theatre.rs`), so this headline
   is empty whenever there is one to draw — and an empty row is not a row. */
html.skin-adminabuse .placeholder .big:empty { display: none; }
/* Everything on this panel scales with the stage, because the stage is what it
   has to fit inside: at 360px wide it is 180px tall and the type is at these
   floors, at 1440x900 it is 478px tall and every clamp is at its ceiling. */
html.skin-adminabuse .placeholder .big { font-size: clamp(14px, 4.6cqh, 21px); }
html.skin-adminabuse .placeholder > div:not(.big) { font-size: clamp(11px, 3cqh, 14px); }
html.skin-adminabuse .placeholder .countdown {
  margin: 0; color: var(--muted); font-size: clamp(11px, 3.2cqh, 15px);
}
html.skin-adminabuse .placeholder .countdown .mono {
  display: block; color: var(--cyan); font-size: clamp(17px, 6cqh, 28px);
}
/* §3 ships `stopwatch.webp` and until now nothing drew it. Beside the digits
   at their own height, so it reads as the icon on a stopwatch face rather than
   as a second mascot — and as a background, so the line a screen reader
   announces is still the sentence and not a picture of one. */
html.skin-adminabuse .placeholder .countdown .mono::before {
  content: ""; display: inline-block;
  width: 1em; height: 1em; margin-right: .4em; vertical-align: -.14em;
  background: url("stopwatch.webp") center / contain no-repeat;
}
/* A short stage has no room for a 42% mascot above three lines of type. The
   picture is what gives, because the countdown is the room's whole product. */
@container (max-height: 300px) {
  html.skin-adminabuse .placeholder .host { height: 30cqh; }
  html.skin-adminabuse .placeholder { padding: 6px; gap: 4px; }
}
@keyframes adminabuse-host-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6%); }
}

/* The header's partner mark. `a.partner` carries no `.brand` class and no
   text, only this image — the page's one `.brand` is the footer's Voxi
   lockup below. */
/* 34px on a 960x250 wordmark cut is a 131x34 box, and the letters inside it
   stand about 20px — the height the word reads at. The whole mark in the same
   box was 59x44 with a four-pixel wordmark under the head. */
html.skin-adminabuse header.bar a.partner img.partner-mark { height: 34px; width: auto; display: block; }
html.skin-adminabuse header.bar a.partner:focus-visible { outline: 2px solid var(--cobalt-lift); outline-offset: 4px; border-radius: 4px; }
/* On a phone the bar has to fit. `header.bar` is a non-wrapping flex row
   (`ui.rs`), and on air it carries the LIVE pill as well as the Voxers count:
   at 360px the 34px mark is 131px wide and the count hung 17px past the
   stage's right edge, measured 2026-09-05. 28px on the same 960x250 cut is
   108px, which is the 23px that costs — and the letters still stand about
   16px, which is a size the word reads at. Below 400 rather than at 360, so
   the fix lands before the overflow does. */
@media (max-width: 399px) {
  html.skin-adminabuse header.bar a.partner img.partner-mark { height: 28px; }
}

/* The footer: the one place this page still speaks in Voxi's voice.
   `BRAND.md`'s lockup table, verbatim — Hubot Sans (fetched by
   `fetch-vendor.sh`, declared by `ui.rs`), `.v` at Voxi's own cobalt-lift,
   `.o` at Live Orange, the rest of the word at Voxi Cloud. None of the three
   is a token this skin repoints, because `--cobalt-lift` above now means the
   room's purple, not Voxi's blue. */
html.skin-adminabuse footer.powered {
  display: flex; align-items: center; justify-content: center;
  gap: 14px; flex-wrap: wrap; padding: 18px 4px 4px;
  font-size: 13px; color: var(--muted);
}
html.skin-adminabuse footer.powered .brand { font-family: "Hubot Sans", var(--font-brand); font-size: 15px; color: #F7F7F3; }
html.skin-adminabuse footer.powered .brand .v { color: #6E8BFF; }
html.skin-adminabuse footer.powered .brand .o { color: var(--orange); }

@media (prefers-reduced-motion: reduce) {
  /* The same selector the animation is set on, because a shorter one loses:
     `:has(> .placeholder)` adds the placeholder's class to the specificity,
     and `html.skin-adminabuse .stage::before` (0,2,2) cannot turn off a rule
     that reads (0,3,2). */
  html.skin-adminabuse .stage:has(> .placeholder)::before { animation: none; }
  html.skin-adminabuse .placeholder .host { animation: none; }
}
