/* ============================================================================
   site.css - the look of the scenes site.

   WHY A SEPARATE SHEET, and not a <style> in the page: the CSP served with
   site.html is `default-src 'self'` with 'unsafe-inline' only in style-src -
   and that 'unsafe-inline' exists for the HTML site.js generates (a
   style="width:40%" on a progress bar), not for the whole sheet. Keeping
   everything here means that dropping the 'unsafe-inline' one day does not wipe
   out the look.

   ── THE STYLE ───────────────────────────────────────────────────────────────
   Apple's design language, and not the dark palette of the license panel that
   used to be here. The five things that define it, and that matter more than
   any colour:

     1. SPACE. The breathing room is the main component. Sections 64-80 px
        apart, cards with 24-32 px of internal slack. Tightening this dismantles
        the whole style, even with the right colours.
     2. TYPOGRAPHY WITH NEGATIVE TRACKING. San Francisco is drawn to be tight:
        -0.022em in the body, up to -0.03em in the large titles. Without it the
        text reads "wide" and the page stops resembling the model for a reason
        nobody can point at.
     3. A HAIRLINE, NOT A BOX. Separation by 1px of #d2d2d7 and by space. A
        border on everything is the opposite of this style.
     4. THE CAPSULE. A button with border-radius 980px and solid #0071e3 blue.
     5. RESTRAINED. No gradient, no hard shadow, no accent colour beyond the
        blue. Green and red appear only as STATE, never as decoration.

   THE FONT IS THE SYSTEM'S (-apple-system / BlinkMacSystemFont). On a Mac and
   an iPhone that resolves to the real San Francisco; on Windows it falls back
   to Segoe UI, which is that platform's right font. There is NO embedded font
   file here - SF Pro is licensed by Apple for apps on its own platforms, and
   serving the file from your own server is not permitted use. There is also no
   Apple mark, logo or name anywhere: what is copied here is the design
   language, not anybody's identity.

   DARK MODE follows the system (prefers-color-scheme), as the model itself
   does. Only the :root tokens change; no rule below repeats a colour.

   ── THE VOCABULARY site.js HAS TO EMIT ──────────────────────────────────────
   The skeleton (table headers, labels, containers) lives in site.html; from
   here down is the other side of the contract with site.js, which fills in the
   <tbody> and the empty containers. A wrong class = an unstyled element, and
   the symptom is a crooked card in the middle of the gallery, not an error.

     gallery  #scenes
       <a class="card scene" href="#/scene/7" data-key="...">
         <div class="cover"><img alt="" src="..."></div>   or
         <div class="cover"><span class="dim">no cover</span></div>
         <div class="pad">
           <strong>Icon Beach</strong>
           <div class="dim">Acme - <span class="pill on">public</span></div>
           <div class="dim">v3 - 12.4 MiB - 2 days</div>
         </div>
       </a>

     This block HAS BEEN WRONG BEFORE: the old sheet described (and styled) a
     card with .meta/.nome/.linha/.iniciais that site.js never emitted, and the
     whole gallery fell into a selector that did not exist. If you change the
     markup over there, change it here - and the other way round.

     visibility pill: `pill` = private, `pill mid` = link, `pill on` = public.
     `pill off` (red) is reserved for "deactivated". Each one carries its own
     glyph, emitted by site.js's VIS_ICON - the colour says which state and the
     glyph says WHICH, and hue alone is the one difference a colourblind reader
     loses on a choice whose consequence is a client's scene going public.

     brand    -> <a class="brand"> in the header, <h1 class="brand"> on the
                 sign-in card. Inside, a <picture> with the ink logo.png and,
                 under prefers-color-scheme:dark, logo-dark.png. TWO FILES
                 because the mark is a monochrome silhouette: white is
                 invisible on the light page and ink is invisible on the dark
                 one, and no filter recolours the inside of a raster reliably.
     icons    -> <svg class="i"> from site.js's icon(name). No fill, stroke in
                 currentColor - so the same markup works on a white card, in a
                 blue capsule and on a black page. A hard-coded colour here
                 disappears in one of the two themes.
     search   -> <span class="search"> wrapping the glyph and the input.
     hint     -> <span class="tip"> holding a <button class="tip-b"> and the
                 sentence in a <span class="sr-only">. Emitted by site.js's
                 hint(); it replaced the grey explanations that used to sit
                 beside the controls. The bubble is NOT in here: there is one
                 .bubble on <body>, and site.js positions it.
     legend   -> <span class="legend"> with one <span> per line. It lives
                 inside the Visibility hint now, not under the select.
     pad      -> the 24px inset. It works BOTH ways: "card pad" on one element,
                 or "pad" on a child of a card (the "Add to the team" row, the
                 "New token" row). It did not always - see the rule itself.
     side     -> <div class="side"> holding a <nav class="rail"> and one
                 <section>. The Server screen's four categories: the rail
                 lists them, the section shows the one the route names
                 (#/admin/users, /teams, /runtimes, /audit). The current one
                 carries aria-current="page" - that attribute is the styling
                 hook AND the accessible state, which is why the mark is not a
                 class.
     cols     -> <div class="cols"> around two or more sibling cards that do
                 not each need the full width - the account's Password and
                 Two-factor, the scene's Name and Cover. It is a grid, so a
                 <div class="gap"> inside it stops being a spacer and becomes a
                 GRID ITEM that eats a column: put the gap AFTER the .cols, not
                 between its children.

     tables    -> only the <tr>; empty = <tr><td colspan="N" class="empty">...
                  columns: #versions 7, #members 4, #tokens 5, #users 5,
                  #teams 5, #runtimes 6, #audit 6
     versions  -> `tr.current` marks the live version with a blue bar. The rule
                  is available; today site.js marks the live row with a
                  <span class="pill on">live</span> in the first cell instead.
     warnings  -> <li> inside ul#warnings

   The buttons in the generated rows use the same data-act delegation as
   site.html: version-live (data-version = versionId), member-remove
   (data-user = userId), token-revoke, user-active (data-id, data-active),
   user-password, team-create. Inventing a new name here does not break the CSS
   - it breaks the click, silently.

   EVERY generated text goes through an escape. The scene name comes from
   whoever publishes it.
   ============================================================================ */

/* ── Tokens ──────────────────────────────────────────────────────────────── */
:root{
  color-scheme:light dark;

  /* Surfaces. --bg is the page's background and --panel the cards': in light
     mode the card is LIGHTER than the page (white on grey), and in dark mode it
     is the other way round. That is what makes the card "lift" in both modes
     with no strong shadow. */
  --bg:#f5f5f7;
  --panel:#ffffff;
  --panel2:#f5f5f7;
  --line:#d2d2d7;
  --line-strong:#c7c7cc;

  --fg:#1d1d1f;
  --dim:#6e6e73;
  --accent:#0071e3;
  --accent-hover:#0077ed;
  --link:#0066cc;

  /* State. Chosen to pass 4.5:1 over --panel; Apple's green (#00c853 and
     relatives) fails on small text. */
  --ok:#00792e;
  --warn:#8a6100;
  --bad:#c9252d;
  --ok-bg:#e8f6ec;   --ok-line:#a7dcb8;
  --warn-bg:#fdf3dd; --warn-line:#e8cf94;
  --bad-bg:#fdecec;  --bad-line:#f0b7b7;

  --shadow:0 4px 16px rgba(0,0,0,.06), 0 1px 3px rgba(0,0,0,.04);
  --shadow-high:0 12px 32px rgba(0,0,0,.10), 0 2px 6px rgba(0,0,0,.05);

  --r-card:18px;
  --r-field:12px;
  --r-pill:980px;

  --nav:48px;
  /* 1024 was a column with the page either side of it. On a 3440 monitor that
     left two thirds of the screen empty and the gallery showing two cards where
     six fit. 1760 is the widest this content stays READABLE at: past it the
     tables' own text lines get long enough to lose the eye between columns, and
     what a wider page would buy is emptiness inside the cards instead of beside
     them. The cap is what makes it a cap - below it the layout is fluid. */
  --width:1760px;

  /* -apple-system and BlinkMacSystemFont resolve to the system font; no file is
     served from here. See the note in the header. */
  --font:-apple-system,BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Helvetica,Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,"SF Mono","Cascadia Mono",Consolas,monospace;
}

@media (prefers-color-scheme:dark){
  :root{
    --bg:#000000;
    --panel:#1d1d1f;
    /* It must NOT be #000 like the page background. --panel2 is the RECESSED
       surface INSIDE a card (the cover area, the .sub-card, the link field),
       and in dark mode the card is already #1d1d1f on a black background: with
       #000 here, the cover area disappears into the background and the card
       looks like it starts halfway down, floating. In light mode the same value
       works because the card is white. */
    --panel2:#2c2c2e;
    --line:#38383a;
    --line-strong:#48484a;

    --fg:#f5f5f7;
    --dim:#a1a1a6;
    --accent:#0071e3;
    --accent-hover:#0077ed;
    --link:#2997ff;

    --ok:#4fd07a;
    --warn:#f0c250;
    --bad:#ff6961;
    --ok-bg:#12301d;   --ok-line:#2c5c3c;
    --warn-bg:#332a12; --warn-line:#6b5824;
    --bad-bg:#3a1a1a;  --bad-line:#6e3232;

    --shadow:0 4px 16px rgba(0,0,0,.5);
    --shadow-high:0 12px 32px rgba(0,0,0,.6);
  }
}

*{box-sizing:border-box}

/* The `hidden` attribute HAS to win, which is why it carries !important.
   The browser implements it as `[hidden]{display:none}` in the user-agent
   sheet - which loses to ANY author rule. Since `.gate`, `.field` and
   `.banner.warn` declare display, the three elements site.html marks with
   `hidden` were always showing: the 2FA code field on the first sign-in screen
   (telling whoever is guessing passwords that that account has 2FA), the
   temporary-password notice for people who have none, and the sign-in card OVER
   the panel after signing in. None of the three raises an error - what should
   be hidden simply shows up, and site.js carries on toggling `el.hidden` with
   no effect at all. */
[hidden]{display:none !important}

html{-webkit-text-size-adjust:100%}

body{
  margin:0;
  background:var(--bg);
  color:var(--fg);
  font-family:var(--font);
  /* 17px is the model's body size. 1.47 and -0.022em are the pair SF Text asks
     for: the font is already narrow, and without the negative tracking the text
     "opens up" and the page stops resembling the model without anybody being
     able to say why. */
  font-size:17px;
  line-height:1.47059;
  letter-spacing:-.022em;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}

/* ── Typography ──────────────────────────────────────────────────────────── */
/* The LARGER the text, the more negative the tracking - the rule San Francisco
   follows when it switches optical size (Text to Display) above 20pt. There are
   no two fonts here, so the tracking does that job on its own. */
h1{font-size:24px;line-height:1.16667;font-weight:600;letter-spacing:-.024em;margin:0}
h2{font-size:21px;line-height:1.19048;font-weight:600;letter-spacing:-.022em;margin:0}
h3{font-size:17px;line-height:1.23536;font-weight:600;letter-spacing:-.022em;margin:0 0 6px}

p{margin:0 0 12px}
p:last-child{margin-bottom:0}

a{color:var(--link);text-decoration:none}
a:hover{text-decoration:underline}

strong{font-weight:600}

code,.mono{font-family:var(--mono);font-size:.882em;letter-spacing:0}

.dim{color:var(--dim)}
.mini{font-size:14px;line-height:1.42859;letter-spacing:-.016em}
.nowrap{white-space:nowrap}

/* Screen reader: off-screen, but NOT display:none - display:none takes it out
   of the accessibility tree too, and these labels exist only for it. */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}

/* ── Navigation bar ──────────────────────────────────────────────────────── */
/* Translucent with a blur, 48px, a hairline underneath. The `saturate(180%)`
   before the blur is not decoration: a pure blur washes out the colour of what
   passes underneath, and the saturation gives it back. */
header{
  position:sticky;top:0;z-index:20;
  /* THREE TRACKS, and not a flex row with a spacer. The nav has to sit on the
     HEADER's centre line; a spacer only centres it on what is left over after
     the mark and the e-mail, so it would drift sideways the moment somebody
     with a longer address signed in. Two 1fr tracks are equal by construction,
     which puts the middle one on the page's centre whatever flanks it.

     minmax(0,1fr) and not 1fr: a grid track's automatic minimum is its own
     content, so a long e-mail would push the nav off centre instead of being
     clipped by the ellipsis that .sub already asks for. */
  display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  align-items:center;gap:16px;
  height:var(--nav);padding:0 22px;
  background:color-mix(in srgb, var(--bg) 72%, transparent);
  -webkit-backdrop-filter:saturate(180%) blur(20px);
  backdrop-filter:saturate(180%) blur(20px);
  border-bottom:1px solid var(--line);
}
/* THE MARK REPLACED THE TITLE, and that also retired a bug. The text title
   pushed the navigation off the screen at 360px - the whole page scrolled
   sideways (measured: scrollWidth 453 against a viewport of 360) - and it took
   min-width:0 + overflow:hidden + an ellipsis to hold it. An image with a fixed
   height and flex:none cannot do that: it occupies exactly what it declares,
   whatever the viewport.

   THE HEIGHT IS FIXED AND THE WIDTH FOLLOWS. logo.png is a raster of 486x120,
   and those two numbers are on the <img> as attributes: the browser reserves
   the right box before the bytes arrive (no reflow on a cold load) and derives
   the width from the ratio. Setting width here as well is how a logo ends up
   squashed on one screen and nobody can say since when. */
/* Three grid items, one per track: the mark, the nav, and the identity with the
   way out. justify-self:start on the mark because a grid item stretches to its
   track by default - and a stretched <a> would put the hover background across
   a third of the bar for a 22px badge. The trail does the opposite on purpose:
   it stretches, and pushes its own content to the far edge. */
header>.brand{justify-self:start}
header .trail{display:flex;align-items:center;justify-content:flex-end;gap:14px;min-width:0}
header .trail button{flex:none}
.brand{display:flex;align-items:center;flex:none;text-decoration:none}
/* The height is fixed and the width follows, as before - but the mark is now
   the PORTRAIT monogram (0.827:1), so the same 22px that suited a 4:1 lockup
   would leave an 18px stamp. 26px in a 48px bar reads as a badge and still
   clears the bar's own padding. */
.brand img{display:block;height:26px;width:auto}
header .brand{padding:3px 5px;margin-left:-5px;border-radius:7px;
              transition:background-color .2s ease}
header .brand:hover{background:color-mix(in srgb, var(--fg) 6%, transparent)}
header .brand:focus-visible{outline:none;
  box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 40%, transparent)}
header .sub{color:var(--dim);font-size:12px;letter-spacing:-.01em;white-space:nowrap;
            min-width:0;overflow:hidden;text-overflow:ellipsis}
.grow{flex:1;min-width:0}

/* Tabs: plain text, no box. The active one darkens and gains weight - not a
   coloured rectangle, which is another style's vocabulary. */
.tabs{display:flex;align-items:center;gap:4px;flex-shrink:0}
.tab{
  appearance:none;border:0;background:none;cursor:pointer;
  display:inline-flex;align-items:center;gap:7px;
  font:inherit;font-size:14px;letter-spacing:-.016em;
  color:var(--dim);padding:6px 10px;border-radius:8px;
  transition:color .2s ease, background-color .2s ease;
}
.tab:hover{color:var(--fg);background:color-mix(in srgb, var(--fg) 6%, transparent)}
.tab[aria-current]{color:var(--fg);font-weight:600}

/* ── Structure ───────────────────────────────────────────────────────────── */
main{max-width:var(--width);margin:0 auto;padding:40px 22px 96px}

/* THE RHYTHM BETWEEN SECTIONS. There was no rule here at all, and only the
   admin screen paints more than one <section> - so its "Teams", "Player
   runtimes" and "Audit log" headings sat hard against the table above them
   while "Users" had 26px under its own title. The page read as if the tables
   belonged to the heading BELOW them. */
#view>section+section{margin-top:56px}

/* The gaps between top-level cards, and only those: 12px is right INSIDE a card
   (between two rows of one form) and too tight between two cards a metre wide.
   Scoping by depth keeps one class doing both jobs. */
#view>section>.gap{height:22px}

/* COLUMNS, for the blocks that do not need the full width. auto-fit with a
   min() floor and no media query at all: one column under ~440px, two from
   ~880, three from ~1320. The floor is min(420px,100%) and not 420px, because a
   bare 420px minimum overflows a 360px phone - the track cannot shrink below
   its own minimum, and the page starts scrolling sideways.
   align-items:start so a short card does not stretch to the height of a tall
   neighbour and end up mostly empty. */
.cols{
  display:grid;gap:22px;align-items:start;
  grid-template-columns:repeat(auto-fit,minmax(min(420px,100%),1fr));
}

/* THE RAIL. A fixed track for the list and a fluid one for the panel: the four
   labels do not change length, so a column that breathes with the viewport
   would only move them about for no reason.

   It is STICKY under the header, and the offset is --nav plus the page's own
   top padding - the header is sticky too, so a rail pinned to 0 would slide
   underneath it and lose its first item behind the bar. */
.side{display:grid;grid-template-columns:216px minmax(0,1fr);gap:34px;align-items:start}
.rail{position:sticky;top:calc(var(--nav) + 18px);display:flex;flex-direction:column;gap:2px}
.rail a{
  display:flex;align-items:center;gap:11px;
  padding:9px 12px;border-radius:10px;
  color:var(--dim);text-decoration:none;
  font-size:14px;line-height:1.42859;letter-spacing:-.016em;
}
.rail a:hover{background:color-mix(in srgb, var(--fg) 6%, transparent);color:var(--fg);
              text-decoration:none}
.rail a:focus-visible{outline:none;box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 40%, transparent)}
/* The current one, and the selector is the ARIA attribute rather than a class:
   one thing to set, and it cannot drift out of step with what a screen reader
   is told. */
.rail a[aria-current]{background:color-mix(in srgb, var(--fg) 9%, transparent);
                      color:var(--fg);font-weight:600}
.rail .i{color:var(--dim)}
.rail a[aria-current] .i{color:var(--accent)}

/* Prose does not follow the page. A hint that runs the full 1760px is one the
   eye loses on the way back to the left margin - the measure, and not the
   container, is what bounds a line of text. */
#view .card p,#view .card>.dim,#view .empty{max-width:82ch}

/* The breathing room between screens is this style's main component. */
.screen>*+*{margin-top:20px}
.gap{height:12px}

.bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:20px}
.bar h2{margin-right:4px}

/* THE PAGE TITLE. Every screen site.js paints is a <section> whose first child
   is the <h2> - and it, and only it, gets the title size. A 21px <h2> at the
   top of a whole page is what most gives away that the layout is not this
   style: here the title is large, with strongly negative tracking, and the
   breathing room under it does half the work. */
#view>section>h2:first-child,
#view>.side>section>h2:first-child{
  font-size:32px;line-height:1.125;letter-spacing:-.028em;
  margin:0 0 26px;
}
#view>section>h2:first-child a{color:var(--dim);font-weight:600}
#view>section>h2:first-child a:hover{color:var(--fg);text-decoration:none}

.block{margin-top:44px}
.block>h2{margin-bottom:14px}

/* ── Cards ───────────────────────────────────────────────────────────────── */
/* No border: what separates it from the background is the soft shadow plus the
   difference in surface. A border on everything is exactly what this style does
   not do. */
.card{
  background:var(--panel);
  border-radius:var(--r-card);
  box-shadow:var(--shadow);
  overflow:hidden;
}
/* BOTH SELECTORS, and the second one is the fix for a class that was doing
   nothing. ".card.pad" needs the two classes on the SAME element, but three
   places write class="pad" on a CHILD of the card - the "Add to the team"
   row, the "New token" row, and the token box that appears after Create.
   Those got no inset from either side: the label began exactly where the
   card began, hard against the edge, directly under a table whose cells were
   properly inset. It reads as the form having fallen out of the card it
   belongs to.

   The gallery card keeps its own tighter figure: ".scene .pad" further down
   has equal specificity and comes later, so it wins there. */
.card.pad,.card>.pad{padding:24px}

/* A .sub-card carries 14px of its own, to part it from the row above it
   INSIDE a card. As the first thing in a padded box that 14 stacks on the
   box's 24 and the seam opens to 38 for no reason. */
.card>.pad>.sub-card:first-child{margin-top:0}

/* Nested: no shadow of its own (two stacked shadows look dirty), just the
   hairline. */
.sub-card{
  margin-top:14px;padding:16px;
  border:1px solid var(--line);border-radius:var(--r-field);
  background:var(--panel2);
}

/* ── Gallery ─────────────────────────────────────────────────────────────── */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:22px}

.scene{
  display:flex;flex-direction:column;
  color:inherit;text-decoration:none;
  transition:transform .3s cubic-bezier(.25,.1,.25,1), box-shadow .3s cubic-bezier(.25,.1,.25,1);
}
/* 4px of lift is enough to read as "clickable". More than that becomes another
   style. And the :hover does not underline either - the global <a> rule would
   apply here and strike through the scene's name. */
.scene:hover{transform:translateY(-4px);box-shadow:var(--shadow-high);text-decoration:none}
.scene:focus-visible{outline:none;box-shadow:var(--shadow-high),0 0 0 4px color-mix(in srgb, var(--accent) 40%, transparent)}

.scene .cover{
  display:grid;place-items:center;
  aspect-ratio:16/9;background:var(--panel2);
  border-bottom:1px solid var(--line);
}
.scene .cover img{width:100%;height:100%;object-fit:cover;display:block}
/* The placeholder used to be the words "no cover" - which read, at a glance, as
   the name of the scene. A glyph cannot be mistaken for content. */
.scene .cover .i{width:30px;height:30px;stroke-width:1.4;color:var(--dim);opacity:.5}
.scene .pad{padding:18px 20px 20px;display:flex;flex-direction:column;gap:5px}
.scene .pad strong{font-size:17px;letter-spacing:-.022em;overflow-wrap:anywhere}
.scene .pad .dim{font-size:14px;line-height:1.42859;letter-spacing:-.016em}

.cover-preview{
  width:148px;aspect-ratio:16/9;object-fit:cover;
  border-radius:10px;border:1px solid var(--line);background:var(--panel2);
}

/* ── Icons ───────────────────────────────────────────────────────────────── */
/* Sized in EM and not in px: the glyph then follows whatever text it sits
   next to - 14px in a button, 12px in a .tiny, 17px on the sign-in card -
   with no per-context rule. The colour is currentColor by way of the stroke
   attribute on the <svg>, so there is nothing to set here either.

   flex:none matters. Inside a button that runs out of room the icon would
   otherwise be the thing that shrinks, and a squashed glyph is unreadable
   long before a clipped word is. */
.i{width:1.05em;height:1.05em;flex:none}
/* For the INLINE cases - inside a .pill, a <strong>, a .banner - the svg is
   not a flex item and sits on the text baseline, which puts it too high. */
.pill .i,.banner .i,strong .i{vertical-align:-.16em}

/* ── Buttons ─────────────────────────────────────────────────────────────── */
/* The solid blue capsule is the model's primary button. Everything else is
   secondary and disappears into the background until the mouse arrives. */
button{
  appearance:none;cursor:pointer;font:inherit;
  /* inline-flex, so a button with a glyph centres the pair instead of hanging
     the icon off the text baseline. It survives .login's width:100% - the
     content simply centres inside the full width. */
  display:inline-flex;align-items:center;justify-content:center;gap:7px;
  font-size:14px;line-height:1.42859;letter-spacing:-.016em;
  padding:7px 16px;border-radius:var(--r-pill);
  border:1px solid var(--line-strong);
  background:transparent;color:var(--fg);
  transition:background-color .2s ease, border-color .2s ease, color .2s ease, opacity .2s ease;
}
button:hover{background:color-mix(in srgb, var(--fg) 6%, transparent)}
button:focus-visible{outline:none;box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 40%, transparent)}
button:disabled{opacity:.42;cursor:default}
button:disabled:hover{background:transparent}

/* Primary. `.ok` because that is the name site.html and site.js already emit -
   here it means "the main action", and not "green". */
button.ok,.btn.ok{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:400}
button.ok:hover,.btn.ok:hover{background:var(--accent-hover);border-color:var(--accent-hover)}

/* Destructive: red text on transparent. A SOLID red button would draw more
   attention than the primary blue one, inverting the screen's hierarchy on
   exactly the action nobody should click by mistake. */
button.bad,.btn.bad{color:var(--bad);border-color:var(--bad-line)}
button.bad:hover,.btn.bad:hover{background:var(--bad-bg);border-color:var(--bad)}

button.tiny,.tiny{font-size:12px;padding:4px 12px;gap:5px}
button.large,.large{font-size:17px;padding:11px 22px;letter-spacing:-.022em}

/* ── Fields ──────────────────────────────────────────────────────────────── */
.field{display:flex;flex-direction:column;gap:6px;min-width:0}
.field>label{font-size:12px;line-height:1.33;letter-spacing:-.01em;color:var(--dim)}
.field.inline{flex-direction:row;align-items:center;gap:8px}
.field.grow{flex:1}
.field.narrow{max-width:180px}

input,select,textarea{
  font:inherit;font-size:15px;letter-spacing:-.016em;
  padding:9px 12px;border-radius:var(--r-field);
  border:1px solid var(--line-strong);
  background:var(--panel);color:var(--fg);
  transition:border-color .2s ease, box-shadow .2s ease;
  max-width:100%;
}
input::placeholder{color:var(--dim);opacity:.8}
input:focus,select:focus,textarea:focus{
  outline:none;border-color:var(--accent);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 25%, transparent);
}
input[type=file]{border:0;padding:0;font-size:14px}
input[type=checkbox]{width:16px;height:16px;accent-color:var(--accent)}

.check{display:inline-flex;align-items:center;gap:8px;font-size:14px;letter-spacing:-.016em}

/* flex-end because the dominant case is a row of `.field` (label on top, field
   below) next to a button: aligning by the BOTTOM puts the button at the height
   of the fields, and not at the height of the labels. */
.row{display:flex;gap:14px;align-items:flex-end;flex-wrap:wrap}
/* But site.js paints rows WITHOUT .field - a loose <strong> next to an input.
   With flex-end, that text drops to the input's baseline and hangs in the
   bottom corner. These two centre themselves. */
.row>strong,.row>span,.row>label{align-self:center}
/* The section labels ("Link", "Cover", "New token") lead with a glyph. As flex
   items they blockify, so the gap is what separates the two - not a space in
   the markup, which would collapse. */
.row>strong{display:flex;align-items:center;gap:7px;white-space:nowrap}
.row>input.mono{font-family:var(--mono);font-size:13px;letter-spacing:0}

/* WHO GETS THE ROOM, and the rule is about what the field IS.

   A field that SHOWS A VALUE to be copied fills the row: the public address, a
   fresh token, the TOTP secret and URI. Without it they fall back to the HTML's
   size=52 and push "Copy" and "Open" outside the card instead of shrinking.
   [readonly] is what says "a value, not a question", and it has to be in the
   selector: .mono alone also matches the scene slug and the team slug, which
   are SHORT things you type. Those were being stretched the whole width of the
   card while the name box beside them kept its natural size - which reads as a
   bug in the layout, not as a choice. */
.row>input.mono[readonly]{flex:1;min-width:120px}

/* A field you TYPE INTO shares the row evenly with the other fields in it. At
   1024px a form sat comfortably at its natural size; at 1760 the same cluster
   left two thirds of its own card empty beside it. flex-basis 0 and not auto,
   so the sharing is even instead of inheriting whatever size= each field
   happens to carry. */
.row>input:is([type=text],[type=email],[type=password]),
.row>input:not([type]){flex:1 1 0;min-width:150px;max-width:480px}

/* THE CAP IS THE OTHER HALF OF THAT RULE. Without it, a row holding a single
   text field handed it every pixel the card had - an e-mail box 1250px wide,
   which is not a better e-mail box, and it left the hint beside it pressed flat
   against the card's padding with no air at all. 480px is about as wide as a
   single-line field stays comfortable to read back what you typed; the slack
   left over is what the hint breathes in.

   The value fields are the exception, and they override this by specificity
   rather than by order: a public address or a fresh token wants every pixel,
   because what it is FOR is being read whole and copied. */
.row>input.mono[readonly]{max-width:none}

/* Two exceptions, both because the field is a STUB whose narrowness is itself
   information: a day count and a six-digit code are not boxes that want a
   metre. The file picker is not ours to size at all. */
.row>input[inputmode=numeric]{flex:0 0 auto;min-width:0}
.row>input[type=file]{flex:0 0 auto}

/* The glyph sits INSIDE the field, which is what makes a text box read as a
   search box before a single word is typed. position:absolute and not a flex
   sibling, so the input keeps its own full width and the browser's own
   focus ring still traces the field and not the pair. */
.search{position:relative;display:flex;align-items:center;flex:1;min-width:190px}
.search>.i{position:absolute;left:13px;width:16px;height:16px;color:var(--dim);
           pointer-events:none}
.search>input{width:100%;padding-left:37px}

/* One line per state, with the glyph the cards use. As a single wrapped
   paragraph the three glyphs landed wherever the text broke, and the mapping
   they exist to teach was not readable. It lives inside a hint now, where it
   needs no top margin of its own. */
.legend{display:flex;flex-direction:column;gap:6px;margin-top:10px}
.legend>span{display:flex;align-items:center;gap:8px}
.legend .i{color:var(--dim)}
.bubble .legend{margin-top:0}

/* ── Hints ───────────────────────────────────────────────────────────────── */
/* The glyph that replaced the grey sentence beside each control, and the one
   bubble every glyph shares. See site.js's hint(): the text itself is in the
   page inside .sr-only, and this bubble is only its picture.

   position:fixed and a child of <body>, NOT of the card. .card is
   overflow:hidden - it is what clips the cover's rounded corners - so a bubble
   positioned inside one would be cut off by the card it belongs to, and the
   delete hint is in the last row of the last card on the screen. Fixed means
   its coordinates come from site.js, which measures and flips it.

   z-index above the header's 20: the account and gallery hints are near the
   top of the page and would open underneath the sticky bar. */
.tip{display:inline-flex;align-items:center}
.tip-b{
  appearance:none;-webkit-appearance:none;background:none;border:0;padding:0;margin:0;
  color:var(--dim);cursor:help;line-height:0;border-radius:var(--r-pill);
  transition:color .15s ease;
}
.tip-b:hover{color:var(--accent)}
.tip-b:focus-visible{outline:2px solid var(--accent);outline-offset:3px;color:var(--accent)}
/* Inside a .row the gap already spaces it, and inside a label the 7px of
   .row>strong does. Loose in a card - the delete button's hint - it is inline
   text next to a capsule, and has nothing but this. */
.card.pad:not(.row)>.tip{margin-left:6px}

.bubble{
  position:fixed;left:0;top:0;z-index:60;
  width:max-content;max-width:min(320px,calc(100vw - 24px));
  padding:11px 14px;
  background:var(--panel);border:1px solid var(--line);border-radius:var(--r-field);
  box-shadow:var(--shadow-high);
  color:var(--fg);font-size:14px;line-height:1.42859;letter-spacing:-.016em;
  /* It must not intercept the pointer: the mouse crossing its own bubble would
     count as leaving the glyph, and the hint would blink. */
  pointer-events:none;
  opacity:0;visibility:hidden;transition:opacity .13s ease,visibility .13s;
}
.bubble.on{opacity:1;visibility:visible}

/* ── State badge ─────────────────────────────────────────────────────────── */
/* A small capsule. No class = private (grey), `mid` = link (amber),
   `on` = public (green), `off` = deactivated (red). */
.pill{
  display:inline-block;padding:2px 10px;border-radius:var(--r-pill);
  font-size:12px;line-height:1.5;letter-spacing:-.01em;font-weight:500;
  border:1px solid var(--line);color:var(--dim);background:var(--panel2);
  vertical-align:baseline;
}
.pill .i{width:13px;height:13px;margin-right:5px}
.pill.on{color:var(--ok);border-color:var(--ok-line);background:var(--ok-bg)}
.pill.mid{color:var(--warn);border-color:var(--warn-line);background:var(--warn-bg)}
.pill.off{color:var(--bad);border-color:var(--bad-line);background:var(--bad-bg)}

/* ── The license sheet ───────────────────────────────────────────────────── */
/* Everything a license lets you CHANGE lives in here. The dialog is native: the
   top layer, the backdrop, Esc and the focus trap come from the browser, and
   none of them would have been as good hand-written - a div with a high z-index
   does not trap Tab, and that is the difference between a modal and a panel
   that merely looks like one.

   No `border` and no `padding` on the element: the UA stylesheet gives <dialog>
   a 1px solid black border and 1em of padding, which would draw a hard frame
   around a card that is defined by its shadow. */
.sheet{
  width:min(560px,calc(100vw - 32px));
  max-height:min(86vh,880px);
  padding:0;border:0;border-radius:var(--r-card);
  background:var(--panel);color:var(--fg);
  box-shadow:var(--shadow-high);
  overflow:auto;overscroll-behavior:contain;
}
/* Not black at 50%: the page behind stays legible, which is what says the sheet
   belongs to the row that was clicked and not to a screen of its own. */
.sheet::backdrop{background:rgba(0,0,0,.32)}
.sheet-top{display:flex;align-items:center;gap:14px;padding:22px 22px 0}
/* The key IS the sheet's title - it is what gets compared against the e-mail
   the customer sent - so it takes the size, and it breaks rather than pushing
   the x off the edge. */
.sheet-key{flex:1;min-width:0;font-size:15px;word-break:break-all}
.sheet-x{
  border:0;background:none;padding:0;flex:none;
  width:30px;height:30px;border-radius:50%;
  color:var(--dim);font-size:17px;line-height:1;
}
.sheet-x:hover{background:color-mix(in srgb, var(--fg) 8%, transparent);color:var(--fg)}
/* Its own strip, because the page's is UNDER the backdrop: <dialog> is in the
   top layer, so an answer written to #banner while the sheet is open is an
   answer nobody sees. Hidden it is display:none and the margins go with it,
   which is why there is no wrapper here holding empty padding. */
#licSheetBanner{margin:16px 22px 0}
.sheet-sec{padding:22px;border-top:1px solid var(--line)}
.sheet-sec:first-child{border-top:0}
.sheet-sec>h3{
  margin:0 0 12px;
  font-size:12px;font-weight:500;letter-spacing:.006em;text-transform:uppercase;
  color:var(--dim);
}
.sheet-sec>.dim.mini{margin:10px 0 0}
/* The destructive block last, set apart by its ground and not by a red border:
   a framed box reads as an alert that is already going off.

   NOT `.danger`, which already exists further down as a PAGE-level block
   modifier (44px of margin, 28px of padding and a rule above it - the shape the
   "Delete the scene" block has). Reusing the name here inherited all of it and
   opened a 70px hole between the machines and the revoke button, which reads as
   an element that failed to draw. */
.sheet-sec.sheet-risk{background:var(--panel2)}
.sheet-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}

/* Permissions: one checkbox per item in the license server's catalog, stacked
   in a column. The catalog is meant to grow, and a row of boxes is the shape
   that stops working the moment there are four of them. */
.feats{display:flex;flex-direction:column;gap:6px}
.feat{display:inline-flex;align-items:center;gap:8px;font-size:13px;
      letter-spacing:-.01em;cursor:pointer;color:var(--dim)}
.feat input{cursor:pointer;margin:0}
.feat input:checked+span{color:var(--fg);font-weight:500}

/* Unlocked machines: one pill per occupied seat, with the machine's name and
   the head of the HWID. The whole HWID lives in the title - 64 hex characters
   are not information, they are a wall. */
.devs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
.dev{
  display:inline-flex;align-items:center;gap:7px;
  padding:3px 4px 3px 11px;border-radius:var(--r-pill);
  border:1px solid var(--line);background:var(--panel2);
  font-size:13px;letter-spacing:-.01em;
}
.dev .hw{font-family:var(--mono);font-size:11px;letter-spacing:0;color:var(--dim)}
/* The x is a real button (a screen reader needs it to be), but it must not look
   like a pill inside another pill. */
.dev button{
  border:0;background:none;padding:0;width:20px;height:20px;border-radius:50%;
  color:var(--dim);font-size:14px;line-height:1;
}
.dev button:hover{background:var(--bad-bg);color:var(--bad)}

.seats{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--dim)}
.seats input{width:64px;padding:5px 8px;font-size:13px}

/* ── Tables ──────────────────────────────────────────────────────────────── */
/* `.tw` is the wrapper that scrolls on its own. Without it, the audit table (6
   columns) pushes the whole <body> sideways on a phone, and the page starts
   scrolling horizontally - the classic symptom of a broken layout. */
.tw{overflow-x:auto;-webkit-overflow-scrolling:touch}

table{width:100%;border-collapse:collapse;font-size:14px;letter-spacing:-.016em}
th,td{padding:12px 16px;text-align:left;vertical-align:middle;border-bottom:1px solid var(--line)}
/* THE OUTER COLUMNS TAKE THE CARD'S INSET, not the cell rhythm. 16px against
   the 24px of every padded card is an 8px zig-zag down a page that alternates
   tables and forms - and on the scene screen the two sit in the SAME card,
   one under the other, where the step is plain. The eye reads it as things
   being slightly out of true without being able to say why. The inner cells
   keep their 16: that is spacing BETWEEN columns and has no reason to match
   the edge. */
th:first-child,td:first-child{padding-left:24px}
th:last-child,td:last-child{padding-right:24px}
th{
  color:var(--dim);font-weight:500;font-size:12px;letter-spacing:.006em;
  text-transform:uppercase;background:transparent;white-space:nowrap;
}
/* A LINE OF BARE CONTROLS - a search box, a select, a button.
   .row is the wrong tool for it: .row aligns on flex-end because it is built
   for stacked label+field pairs, and three controls of three different heights
   with no captions have no common baseline - the select and the button end up
   hanging below the search box. Here they are CENTRED on one line.
   The search takes the free space but stops growing: past ~460px a single-line
   input reads as an empty band, and the controls beside it get pushed to the
   far edge of the card, a hand's width from the thing they filter. */
.toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.toolbar>.search{flex:1 1 240px;max-width:460px}
.toolbar>select{flex:0 0 auto}
/* Wrapped onto its own line on a narrow screen, the search should take that
   line rather than sit at 240px with a gap beside it. */
@media (max-width:560px){ .toolbar>.search{max-width:none} }

/* A SORTABLE HEADING IS A BUTTON, not a <th> with a click handler. It has to
   be reachable by Tab and pressable with the keyboard, and only a real button
   is both without a roomful of ARIA. It inherits the <th>'s type so the row
   goes on reading as a heading rather than as a strip of controls: the only
   thing that says it can be pressed is the cursor and the arrow that appears
   on the column in force. */
.th-sort{
  font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;
  background:none;border:0;padding:0;margin:0;cursor:pointer;
  display:inline-flex;align-items:center;gap:6px;
}
.th-sort:hover{color:var(--fg)}
.th-sort.on{color:var(--fg)}
/* The arrow keeps its width even while empty, so turning the order round does
   not shift the heading a few pixels sideways under the pointer. */
.th-sort .arrow{display:inline-block;min-width:9px;font-size:9px;line-height:1}
.th-sort:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}

tbody tr:last-child td{border-bottom:none}
tbody tr{transition:background-color .15s ease}
tbody tr:hover{background:color-mix(in srgb, var(--fg) 3%, transparent)}
/* The live version. A blue bar in the first cell, and not the whole row
   painted: the version list is long and a full band would dominate the
   screen. */
tr.current td:first-child{box-shadow:inset 3px 0 0 var(--accent)}
tr.current td{font-weight:500}

.empty{padding:36px 16px;color:var(--dim);text-align:center;font-size:15px}
/* A card is usually "list + form". When the list is empty, the .empty's 36px
   plus the form's 24px open a hole in the middle of the card that looks like a
   missing element. An ACCOMPANIED empty shrinks, and what comes after it gets
   the hairline that separates list from form - the same hairline the table
   already has. */
.card>.empty:not(:only-child){padding:22px 16px}
.card>.empty+*,.card>.tw+.pad,.card>table+.pad{border-top:1px solid var(--line)}

/* ── Key/value list ──────────────────────────────────────────────────────── */
.kv{display:grid;grid-template-columns:auto 1fr;gap:8px 18px;margin:0}
.kv dt{color:var(--dim);font-size:14px;letter-spacing:-.016em}
.kv dd{margin:0;font-size:14px;letter-spacing:-.016em}

/* ── Notice bands ────────────────────────────────────────────────────────── */
.banner{
  padding:14px 18px;border-radius:var(--r-field);margin-bottom:16px;display:none;
  font-size:15px;letter-spacing:-.016em;
  border:1px solid transparent;
}
.banner.err{display:block;background:var(--bad-bg);border-color:var(--bad-line);color:var(--bad)}
.banner.good{display:block;background:var(--ok-bg);border-color:var(--ok-line);color:var(--ok)}
.banner.warn{display:block;background:var(--warn-bg);border-color:var(--warn-line);color:var(--warn)}
.banner .i{width:17px;height:17px;margin-right:9px}
.banner button{margin-left:10px}

/* Package warnings (scripting with no assembly, package with no .br). They
   exist to be read: they are the only clue to a build that publishes
   successfully and opens wrong. */
.warnings{margin:0;padding-left:20px}
.warnings li{margin:6px 0;font-size:14px;letter-spacing:-.016em;color:var(--warn)}
.warnings li::marker{color:var(--warn)}

/* ── Scene detail ────────────────────────────────────────────────────────── */
/* min-width:0 on the field: without it the long address pushes the "Copy"
   button outside the card instead of shrinking. */
.linkbox{display:flex;gap:10px;align-items:center}
.linkbox input{
  flex:1;min-width:0;font-family:var(--mono);font-size:13px;letter-spacing:0;
  background:var(--panel2);
}

.slugline{display:flex;align-items:center;gap:2px;flex-wrap:wrap}
.slugline input{max-width:220px}

/* Choosing the visibility: three options side by side, each one a clickable
   card. The :has(input:checked) marks the chosen one with the blue border - the
   radio's little dot alone is far too small to read at a glance. */
.vis{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px}
.opt{
  display:block;padding:14px 16px;cursor:pointer;
  border:1px solid var(--line);border-radius:var(--r-field);
  background:var(--panel);
  transition:border-color .2s ease, box-shadow .2s ease, background-color .2s ease;
}
.opt:hover{border-color:var(--line-strong)}
.opt:has(input:checked){
  border-color:var(--accent);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.opt input{margin-right:8px}
.opt>span{font-weight:500;font-size:15px;letter-spacing:-.016em}
.opt .dim{display:block;margin-top:5px;font-size:13px;line-height:1.4;letter-spacing:-.01em}

/* The destructive zone: separated by space and by a hairline, never by a red
   background. Painting the whole block shouts before the person has read what
   it does. */
.danger{margin-top:44px;padding-top:28px;border-top:1px solid var(--line)}
.danger h2{color:var(--bad);margin-bottom:12px}

/* ── Sign in ─────────────────────────────────────────────────────────────── */
/* A centred card, nothing of the panel behind it. The model uses A LOT of space
   here: it is the first screen, and it should look empty on purpose. */
.gate{min-height:100vh;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:24px}
.login{
  padding:40px 36px;width:min(400px,100%);
  display:flex;flex-direction:column;gap:20px;
  box-shadow:var(--shadow-high);
}
.login h1{font-size:28px;line-height:1.14286;letter-spacing:-.028em;text-align:center}
.login .brand{justify-content:center}
.login .brand img{height:52px}
.login p{margin:0;font-size:15px;letter-spacing:-.016em;text-align:center;color:var(--dim)}
.login .field{gap:6px}
.login input{width:100%;font-size:17px;padding:12px 14px;letter-spacing:-.022em}
.login button[type=submit]{width:100%;padding:12px;font-size:17px;letter-spacing:-.022em}
.login .banner{margin-bottom:0;text-align:center}
.login .mini{text-align:center}

/* ── Responsive ──────────────────────────────────────────────────────────── */
/* Whoever sends a scene link usually opens it on a phone to check before
   sending it to the client. 360px is the real target. */
@media (min-width:1200px){
  main{padding:48px 32px 112px}
  .grid{gap:24px}
}

/* Under 900 the two tracks do not both fit, so the rail lies down above the
   panel and scrolls sideways on its own if the four labels run out of room -
   inside its own box, never taking the page with it. position:static because a
   sticky horizontal strip would cover the content it is scrolling past. */
/* THE E-MAIL LEAVES THE BAR, and it leaves EARLY. The ellipsis on .sub does
   its job all the way down, but "Heit…" is not an identity - it is the same
   number of pixels spent saying nothing. Measured: the full address fits above
   ~700px and is already truncated at 600. Below that the account is one click
   away in the nav, which is a better answer than three letters. */
@media (max-width:700px){
  header .sub{display:none}
}

@media (max-width:900px){
  .side{grid-template-columns:minmax(0,1fr);gap:22px}
  .rail{position:static;flex-direction:row;gap:6px;
        overflow-x:auto;-webkit-overflow-scrolling:touch;
        padding-bottom:2px;scrollbar-width:thin}
  .rail a{flex:none;border:1px solid var(--line);border-radius:var(--r-pill);padding:7px 14px}
  .rail a[aria-current]{border-color:transparent}
}

@media (max-width:720px){
  main{padding:28px 18px 72px}
  .grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}
  .bar{gap:10px}
  .card.pad,.card>.pad{padding:20px}
  /* With the card at 20, the outer columns follow it here and not one
     breakpoint later: between 561 and 720 the table would otherwise keep 24
     while everything beside it had moved, which is the same zig-zag on a
     narrower band of widths. The 560 block below drops the cell rhythm to
     10/12 but cannot undo this - th:first-child outranks th,td. */
  th:first-child,td:first-child{padding-left:20px}
  th:last-child,td:last-child{padding-right:20px}
  .block{margin-top:36px}
}

@media (max-width:560px){
  header{padding:0 14px;gap:10px}
  header .brand img{height:22px}
  /* The labels go and the glyphs stay. Four icons fit where four words did not,
     and aria-label on each button is what keeps the names in the accessibility
     tree - display:none takes the text out of it as well. */
  header button>span{display:none}
  .tabs{gap:2px}
  .tab{padding:6px 8px;font-size:13px}
  .grid{grid-template-columns:1fr;gap:14px}
  .row{gap:10px}
  .field.narrow{max-width:none}
  .login{padding:28px 22px}
  .login h1{font-size:24px}
  .login .brand img{height:44px}
  th,td{padding:10px 12px}
}

/* ── Motion ──────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important;scroll-behavior:auto !important;
  }
  .scene:hover{transform:none}
}
