/* 360sona — the whole visual system, in one file, in three ordered sections.
 *
 * NO @layer, DELIBERATELY. The design specs put tokens in `@layer tokens` and
 * left component CSS unlayered; layer order resolves before specificity, so
 * every unlayered `.tile { box-shadow }` silently beat the global
 * `:focus-visible { box-shadow: var(--aura-focus) }` and killed the focus ring —
 * which is the one accessibility override this project deliberately makes. One
 * file in a fixed order cannot misfire that way.
 *
 * Every value marked [M] was measured from Microsoft's own shipped CSS, brand
 * guidelines, press renders or art. [C] is computed from those. [D] is ours and
 * says so. Nothing here is a plausible number wearing a measurement's clothes.
 *
 *   1. Tokens
 *   2. Base
 *   3. Components
 */

/* ========================================================================
 * 1. TOKENS
 * ==================================================================== */

:root {
  /* --- ground and panel ------------------------------------------------
   * The house move of the entire era: a pure white panel on a tinted
   * off-white ground. White appears in exactly one place and that is the
   * panel. */
  --ground:       #DBDBDB;   /* [M] Xbox.com 2009 shell page background */
  --panel:        #FFFFFF;   /* [M] the only pure white on the page */
  --panel-2:      #EFEFEF;   /* [M] Marketplace grid area; a panel that is a ground */
  --panel-3:      #F5F5F5;   /* [M] note and callout fill */
  --panel-hover:  #F6F6F6;   /* [M] the stage horizon value, reused as row hover */

  /* --- the stage --------------------------------------------------------
   * Microsoft's own avatar backdrop, per-row means. Perfectly neutral:
   * maximum channel spread 2 across 1,081 rows. There is NO colour in the
   * ground — every colour on screen comes from the avatar and the accent. */
  /* The plate, composited at its AUTHORED 60% ALPHA over white — which is how
     the app uses it and is measurably calmer than the raw plate. Using the raw
     per-row means put a pure #FFFFFF band across the middle of the editor and
     the whole panel glared. Same gradient, same horizon at 57%, less blown out.
     A radial vignette rides on top so the figure sits in a pool of light
     instead of on a flat wash. */
  --stage:
    radial-gradient(120% 78% at 50% 46%,
      rgba(255,255,255,.55) 0%, rgba(255,255,255,.18) 46%, rgba(0,0,0,.05) 100%),
    linear-gradient(to bottom,
      #CFCFCF   0%,
      #DBDBDB  22%,
      #EDEDED  50%,
      #F2F2F2  57%,
      #E0E0E0  85%,
      #D8D8D8 100%);   /* [M] _studio_background.png at 60% over white */

  /* --- green, four slots, never substituted -----------------------------
   * These came off three physically different things — a brand PDF, a 3D
   * render, a GIF — and were never in competition. All three fail WCAG for
   * text (2.41 / 3.64 / 2.65 : 1 on white), so green is a FILL and a GLOW,
   * and --green-ink at 6.90:1 is the only green that may be ink. */
  --green-brand:  #76B900;   /* [M] PANTONE 376. Mark only, nothing interactive. */
  --green-select: #5C9514;   /* [M] console selection pill, identical on two themes */
  --green-action: #7FAD2A;   /* [M] buynow_middle.gif, flat */
  --green-ink:    #336600;   /* [M] a,a:link{color:#360} — links, focus ring, borders */
  --green-hover:  #ABEF2E;   /* [M] hover TEXT on a green fill. Never a fill itself. */

  /* --- ink -------------------------------------------------------------- */
  --ink:          #1E1E1E;   /* [D] modern floor; the era used #333 */
  --ink-body:     #333333;   /* [M] 2009 body text */
  --ink-dim:      #666666;   /* [M] module heading, secondary copy */
  --ink-carbon:   #848589;   /* [M] Carbon Gray, PANTONE Cool Gray 9 */

  /* --- rules ------------------------------------------------------------
   * Two weights, and the era named them in its own class names. */
  --rule-light:   #E2E2E2;   /* [M] Chill Gray, Cool Gray 1 — section rules */
  --rule-heavy:   #BDBEC0;   /* [M] Alloy Gray, Cool Gray 4 — the hr */
  --border:       #BCBCBC;   /* [M] the most-used border, 21 occurrences */
  --border-ctrl:  #D1D1D1;   /* [M] input, textarea */

  /* --- accents ---------------------------------------------------------- */
  --live-orange:  #E88D00;   /* [M] LIVE ink on SCREEN. #FF7200 is print-only. */
  --owned:        #F47400;   /* [M] purchased state. Ownership was ORANGE, not green. */
  --error:        #FF0103;   /* [M] error and required-field red */
  --refuse:       #848589;   /* [D] refusal flash — reads as "no" without meaning "store" */

  /* --- type -------------------------------------------------------------
   * Verdana, and NOT Segoe UI. All three font-family declarations in the
   * 2009 316 KB bundle are Verdana; Segoe is the 2010–11 tail and reads as
   * the beginning of the Metro slide. Verdana runs large for its size, so
   * this ramp sits 1–2px below an equivalent Segoe ramp on purpose. */
  --ui: Verdana, Tahoma, "DejaVu Sans", Geneva, sans-serif;
  --mono: Consolas, "Cascadia Mono", "Courier New", monospace;

  /* One step up across the board. The period ramp bottoms out at 11.2px, which
     is a legibility failure on a modern display, and the client could not tell
     what the controls were. The PATTERN is period; the sizes are ours and §6 of
     the brief already lists the uplift as a deviation. */
  --fs-display: 30px;  --lh-display: 1.15;  --fw-display: 700;
  --fs-section: 19px;  --lh-section: 1.25;  --fw-section: 700;
  --fs-subhead: 16px;  --lh-subhead: 1.30;  --fw-subhead: 700;
  --fs-body:    14px;  --lh-body:    1.45;  --fw-body:    400;
  --fs-second:  13px;  --lh-second:  1.40;  --fw-second:  400;
  --fs-meta:    13px;  --lh-meta:    1.30;  --fw-meta:    700;
  --fs-debug:   12px;  --lh-debug:   1.50;  --fw-debug:   400;

  /* --- space ------------------------------------------------------------
   * Every value appears in the shipped period CSS. 10px is the module
   * margin and the below-rule gap; 5px is the above-rule gap. */
  --sp-1: 2px;  --sp-2: 3px;  --sp-3: 4px;  --sp-4: 5px;  --sp-5: 6px;
  --sp-6: 8px;  --sp-7: 10px; --sp-8: 12px; --sp-9: 16px; --sp-10: 20px; --sp-11: 30px;

  /* THE STAGE IS THE HERO AND GETS THE REMAINDER. Fixed rails meant that on a
     1310px window the avatar had 505px and the chrome had 764 — the render was
     the smallest thing on screen. These shrink toward their floors before the
     stage gives up a single pixel. */
  /* The item list is where the time goes, so it gets roughly half the page.
     At 1310px that is ~590px — five tiles a row instead of two. */
  --rail-left: clamp(360px, 45vw, 720px);
  --rail-right: clamp(256px, 19vw, 320px);
  --appbar: 56px;

  /* --- radius -----------------------------------------------------------
   * Radius tracked component size, and the four measurements are
   * independent. 4px is the default and was recovered twice: a web sprite
   * sheet and a traced console arc. */
  --r-button: 3px;   /* [M] buynow_top/bottom.gif alpha stair on a 103px button */
  --r-tile:   4px;   /* [M] cornerImageSheet.png 4x4 tiles; console pill at 720p */
  --r-tab:    6px;   /* [M] tab rightCurve sprite, 6x35 */
  --r-panel:  8px;   /* [M] whitecorners.png 16x16 at four offsets */

  /* --- elevation --------------------------------------------------------
   * The era's mechanism was corner sprites and bitmaps; ten stylesheets
   * contain zero border-radius and one blurless box-shadow. These implement
   * measured bitmaps in modern CSS. */
  --shadow-rest:  0 1px 2px rgba(0,0,0,.10);
  --shadow-hover: 0 2px 5px rgba(0,0,0,.16);
  --shadow-press: inset 0 1px 2px rgba(0,0,0,.18);
  --shadow-panel: 0 1px 3px rgba(0,0,0,.12);

  /* --- gloss ------------------------------------------------------------
   * THE HARD 49.9% -> 50% BREAK IS THE ERA. A smooth ramp there reads 2013.
   * On press the break moves to 44%, which is what makes a control feel
   * compressed rather than merely darker. */
/* GLOSS IS A SHAPED HIGHLIGHT ON A CHROME SURFACE. It is not a wash, and it
     never goes over artwork.

     Two wrong versions came before this one. The first was a flat white band
     across the top half — a veil. The second added stops and a bevel and was
     still a full-bleed rectangle, so on a tile with a photograph in it the only
     thing it could do was fog the picture. What makes a surface read as glossy
     is CONTRAST and SHAPE: a bright, tightly-bounded highlight with a curved
     lower edge that falls off to nothing, sitting next to an untouched darker
     zone. A low-opacity rectangle over everything is the opposite of that.

     So: an ellipse anchored above the top edge, so its lower boundary curves the
     way light actually pools on a convex surface, cut off well before the
     midpoint. And it is applied ONLY to chrome — buttons, the label strip, the
     frame. Never to a picture. */
  --sheen: radial-gradient(ellipse 150% 108% at 50% -34%,
    rgba(255,255,255,.82)  0%,
    rgba(255,255,255,.46) 42%,
    rgba(255,255,255,.16) 62%,
    rgba(255,255,255,0)   72%);
  --sheen-press: radial-gradient(ellipse 150% 90% at 50% -30%,
    rgba(255,255,255,.42)  0%,
    rgba(255,255,255,.20) 40%,
    rgba(255,255,255,0)   62%);
  /* The bounce: light coming back off whatever the control sits on. Its own
     layer, bottom-anchored, so it never brightens the middle. */
  --bounce: linear-gradient(to top, rgba(255,255,255,.30) 0%,
    rgba(255,255,255,.10) 22%, rgba(255,255,255,0) 40%);
  /* Thickness. A 1px lit top edge and a 1px shaded bottom edge do more for
     "this is a physical object" than any amount of overlay. */
  --bevel: inset 0 1px 0 rgba(255,255,255,.90), inset 0 -1px 0 rgba(0,0,0,.10);
  --bevel-press: inset 0 2px 3px rgba(0,0,0,.22), inset 0 -1px 0 rgba(255,255,255,.40);
  /* A picture gets THIS instead: a hairline specular on its top edge and a
     contact shade at the bottom. No overlay, so the art stays the art. */
  --pic-edge: inset 0 1px 0 rgba(255,255,255,.55), inset 0 -1px 0 rgba(0,0,0,.07);

  /* --- the selection pill -----------------------------------------------
   * Fourteen stops, transcribed pixel-for-pixel from a measured column of a
   * press render: top-lit, a long flat plateau across the lower half, a rim
   * lift at the bottom. There is NO dark stroke — one 1px lighter rim.
   * Flattening this to a symmetric two-stop is the single most visible way
   * to miss the era. */
  --pill: linear-gradient(to bottom,
    #8BA47A  0%,   #B4C598  8.2%, #AFC294 12.3%, #A4BC82 17.8%,
    #9AB56E 23.3%, #8CAF5B 31.5%, #82AA4A 37.0%, #73A134 45.2%,
    #699C23 50.7%, #5D9317 56.2%, #5C9514 58.9%, #5C9514 87.7%,
    #629919 91.8%, #699C1B 94.5%, #6B9531 97.3%);
  --pill-rim: inset 0 0 0 1px #79936C;

  /* --- aura -------------------------------------------------------------
   * Bloom on a light ground, which is what separates 2009 from a modern
   * neon glow. Five layers: white keyline separates object from glow, a 3px
   * ring carries the state, two blooms fall off, one contact shadow. */
  --aura-select: 0 0 0 1px #FFFFFF, 0 0 0 3px #5C9514,
                 0 0 10px 1px rgba(92,149,20,.50),
                 0 0 22px 6px rgba(92,149,20,.22),
                 0 1px 2px rgba(0,0,0,.18);
  /* Focus is SHARP and has zero blur, so it stays legible stacked on the
     soft selection halo. Orthogonal in geometry, not in colour. */
  --aura-focus:  0 0 0 2px #FFFFFF, 0 0 0 4px #336600, 0 0 0 5px rgba(255,255,255,.9);

  /* --- motion -----------------------------------------------------------
   * The camera number and the animation number come from DIFFERENT
   * subsystems — ModifyCameraZoom vs BLEND_SECONDS — and borrowing one for
   * the other is the tidy substitution that turns a decision into a false
   * recovery. */
  --dur-instant: 0ms;      /* [D] state that must not be seen moving */
  --dur-half:    125ms;    /* [C] half of BLEND_SECONDS — hover, press, select */
  --dur-blend:   250ms;    /* [M] BLEND_SECONDS, the figure crossfade */
  --dur-camera:  500ms;    /* [M] ModifyCameraZoom */
  --ease-out: cubic-bezier(.2, 0, 0, 1);
}

/* Single-theme by choice. This is a recreation of one specific 2009 look, and
 * a dark mode would be a different product. Every colour is painted
 * explicitly so the page holds on any host ground. */

/* ========================================================================
 * 2. BASE
 * ==================================================================== */

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink-body);
  font: var(--fw-body) var(--fs-body)/var(--lh-body) var(--ui);
  overflow: hidden;
}

h1, h2, h3 { margin: 0; text-wrap: balance; }

/* The era's own heading pattern, verbatim: heading, 5px, hairline, 10px.
 * WEIGHT AND SIZE CARRY THE HEADING. Not tracking — twenty distinct NXE
 * labels were checked and not one is all-caps, and where the era touched
 * tracking at all it TIGHTENED display type. */
h2 {
  font: var(--fw-section) var(--fs-section)/var(--lh-section) var(--ui);
  color: var(--ink);
  letter-spacing: 0;
  text-transform: none;
  padding-bottom: var(--sp-4);
  margin-bottom: var(--sp-7);
  border-bottom: 1px solid var(--rule-light);
}

a { color: var(--green-ink); }
a:hover { color: var(--ink); }

button, input, select, textarea { font: inherit; color: inherit; }

/* THE HEADLINE DEVIATION. The period had NO focus state on any control across
 * ten stylesheets — confirmed deliberate, not a search gap. Accessibility
 * overrules authenticity here, and this rule is not removable. */
:focus-visible {
  outline: none;
  box-shadow: var(--aura-focus);
  border-radius: var(--r-button);
}
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--green-select); color: #fff; }

/* Scrollbars, kept quiet. The era had none of this; a default chrome
 * scrollbar on a 2009 recreation is a bigger anachronism than styling one. */
* { scrollbar-width: thin; scrollbar-color: var(--rule-heavy) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb {
  background: var(--rule-heavy); border-radius: 5px;
  border: 3px solid transparent; background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--ink-carbon); background-clip: content-box; }
*::-webkit-scrollbar-track { background: transparent; }

@media (prefers-reduced-motion: reduce) {
  /* 1ms rather than 0 so transitionend still fires and handlers do not strand. */
  * { transition-duration: 1ms !important; animation-duration: 1ms !important;
      animation-iteration-count: 1 !important; }
}

/* ========================================================================
 * 3. COMPONENTS
 * ==================================================================== */

/* --- the frame -------------------------------------------------------- */

#app {
  display: grid;
  grid-template-columns: var(--rail-left) minmax(0, 1fr) var(--rail-w);
  grid-template-rows: var(--appbar) minmax(0, 1fr);
  grid-template-areas: "bar bar bar" "left stage right";
  height: 100vh;
  gap: var(--sp-7);
  padding: 0 var(--sp-7) var(--sp-7);
  background: var(--ground);
}
/* THE RAIL SLIDES, IT DOES NOT VANISH.
   `display: none` on the column made the stage jump its whole width in one
   frame, the render re-centre instantly through object-fit, and then move AGAIN
   when the debounced refit landed 160 ms later. Two jumps out of one click,
   which is the twitch.

   The width is a REGISTERED custom property, not the grid shorthand. Putting a
   transition on `grid-template-columns` looks like it should work and does not
   reliably: the middle track is `minmax(0, 1fr)`, and a track list that is not
   all lengths is not interpolable. A plain custom property is not animatable
   either -- it has no type, so the engine has nothing to interpolate. @property
   gives it one, and then the column really does travel. */
@property --rail-w {
  syntax: '<length>';
  inherits: true;
  initial-value: 0px;
}
#app {
  --rail-w: var(--rail-right);
  transition: --rail-w var(--dur-camera) var(--ease-out);
}
#app.rail-closed { --rail-w: 0px; }
@media (prefers-reduced-motion: reduce) { #app { transition-duration: 1ms; } }

/* A CHANGE THAT MUST NOT BE SEEN MOVING. Applied around the restore at boot and
   around any programmatic set, so the class flip commits without the transition
   firing. Every transition inside the app is suppressed, not just the column's:
   the panel's own translate would otherwise slide in on its own while the column
   snapped, which is its own kind of wrong. */
#app.no-anim, #app.no-anim * { transition: none !important; }

/* --- app bar ---------------------------------------------------------- */

/* THE BAR IS THE SAME BAR ON EVERY SCREEN.
   Three copies of it exist because the front door, the flat pages and the
   editor load three different stylesheets and always have -- a document should
   not pull in the editor's CSS to use four rules of it. What they must not do
   is drift, and they had: the flat pages ruled their bar in #D9D9D9 while the
   front door used the measured #BCBCBC, the gaps were 18px against var(--sp-9),
   and the editor's bar had no ground, no rule and no shadow at all, so it read
   as a different site. These five declarations are the bar; change them in all
   three places or in none. */
/* THE EDITOR'S COPY. It keeps its grid slot and its 56px, and it bleeds back
   out through the app's own horizontal padding so the rule reaches both edges
   the way it does on every other screen -- a bar that stops short of the window
   is the tell that gave this one away. */
#bar {
  grid-area: bar;
  display: flex; align-items: center; gap: 18px;
  margin: 0 calc(var(--sp-7) * -1);
  padding: 0 30px;
  background: linear-gradient(#ffffff, #f5f5f5 54%, #ececec);
  border-bottom: 1px solid #BCBCBC;
  box-shadow: 0 1px 3px rgb(0 0 0 / .12);
}
/* THE MARK. Painted by src/ui/front.js -- a real render of the blank figure cut
   at the neck, with the drawn Xbox sphere where its head was, and the drawn
   wordmark beside it. The boxes hold their size before either lands so the bar
   does not reflow when they do. */
.brand {
  display: flex; align-items: center; gap: var(--sp-6);
  background: none; border: 0; padding: 2px var(--sp-5) 2px 2px;
  border-radius: var(--r-button); cursor: pointer; color: inherit;
  font: inherit; text-align: left;
}
.brand:hover { background: rgb(0 0 0 / .05); }
.brand:active { background: rgb(0 0 0 / .09); }
.logo { flex: none; display: block; line-height: 0; width: 28px; }
.logo img { display: block; height: 28px; width: auto; }
.wordmark { display: block; flex: none; line-height: 0; }
.wordmark svg { display: block; height: 20px; width: auto; stroke: var(--ink); }
/* THE ROOM, IN A BOX. Sentence case, because the rest of this bar is: nothing
   else in twenty NXE labels that were checked is set in capitals. */
.tag {
  display: inline-block;
  padding: 2px var(--sp-6);
  border: 1px solid var(--border-ctrl);
  border-radius: var(--r-button);
  background: var(--panel-2);
  color: var(--ink-dim);
  font: var(--type-meta);
  letter-spacing: .01em;
}

#bar .sp { margin-left: auto; }
.pane-search { flex: none; padding: var(--sp-5) var(--sp-8) var(--sp-6);
               border-bottom: 1px solid var(--rule-light); }
#search {
  width: 100%; height: 32px;
  padding: 0 var(--sp-7); border: 1px solid var(--border-ctrl);
  border-radius: var(--r-button); background: var(--panel);
  font-size: var(--fs-second); box-shadow: inset 0 1px 2px rgba(0,0,0,.06);
}
#search::placeholder { color: var(--ink-carbon); }
#search:focus-visible { border-color: var(--green-ink); }

/* --- panels ----------------------------------------------------------- */

.panel {
  background: var(--panel);
  border: 1px solid var(--rule-heavy);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-panel);
  min-height: 0;
}

#left  { grid-area: left;  display: flex; flex-direction: column; overflow: hidden; }
/* THE STRIP IS SUPERIMPOSED, NOT STACKED.
   It used to be a flex sibling of the surface, so it took real layout height and
   the render stopped where it began -- which put the bottom of the stage behind
   a panel, and props, which fly out and around, disappeared into it.
   As an overlay the render owns the whole stage and the strip floats over the
   last hundred pixels of it. Folding the strip away does not resize anything: it
   uncovers a part of the picture that was being drawn the whole time. */
#stage { grid-area: stage; position: relative; overflow: hidden; }
/* The panel travels with its column rather than being cut off at the edge: a
   column that narrows while its contents stay put reflows every row inside it
   on the way, which is the second half of the twitch. A fixed inner width and a
   translate means the content never reflows at all -- it just leaves. */
/* A COLUMN WITH A PINNED HEAD AND A SCROLLING BODY. It used to scroll as one
   block, which was right when the only thing in it was the settings list and
   its own sticky search bar. With two panes and a tab strip above them, the
   whole-column scroll took the tabs away with the content: twenty slot cards
   are 1,840px of grid, so by slot 12 there was no way back to Settings without
   scrolling up. The tabs and the search now stay put and the pane under them
   moves. */
#right { grid-area: right; overflow: hidden;
         display: flex; flex-direction: column; min-height: 0;
         transition: transform var(--dur-camera) var(--ease-out),
                     opacity var(--dur-camera) var(--ease-out); }
#rail { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; min-height: 0; }
#app.rail-closed #right {
  transform: translateX(calc(100% + var(--sp-7)));
  opacity: 0; pointer-events: none;
}
/* Kept off the layout once it is away, so a stray Tab cannot land in it. */
#app.rail-closed #right * { visibility: hidden; }

.pane-head {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: var(--sp-7) var(--sp-8) var(--sp-4);
  border-bottom: 1px solid var(--rule-light);
  flex: none;
}
.pane-head .t { font: var(--fw-subhead) var(--fs-subhead)/1.2 var(--ui); color: var(--ink); }
.pane-head .n { margin-left: auto; color: var(--ink-dim); font-size: var(--fs-second);
                font-variant-numeric: tabular-nums; }
.pane-body { flex: 1; overflow-y: auto; padding: var(--sp-7) var(--sp-8) var(--sp-9); }

/* --- buttons ---------------------------------------------------------- */

.btn {
  position: relative; overflow: hidden; cursor: pointer;
  padding: 7px 15px; border-radius: var(--r-button);
  background: var(--panel); border: 1px solid var(--rule-heavy); color: var(--ink);
  font-size: var(--fs-second);
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              color var(--dur-half) var(--ease-out);
}
.btn { box-shadow: var(--bevel); }
/* The highlight is the TOP HALF ONLY and it is a curve, not a band. Bottom
   anchored bounce is a second, separate layer. */
.btn::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
              background: var(--sheen), var(--bounce); pointer-events: none; }
.btn:hover { border-color: var(--ink-carbon); }
.btn:active { box-shadow: var(--bevel-press); transform: translateY(1px); }
.btn:active::after { background: var(--sheen-press); }
.btn[disabled] { background: var(--rule-light); border-color: var(--rule-light);
                 color: var(--ink-carbon); cursor: default; }
.btn[disabled]::after { display: none; }

/* A primary button's HOVER CHANGES THE INK, NOT THE FILL. That is the period
   tell most recreations miss. */
.btn-1 { background: var(--green-action); border-color: var(--green-ink); }
.btn-1:hover { color: var(--green-hover); border-color: var(--green-ink); }

.btn-quiet { background: none; border-color: transparent; color: var(--ink-dim); }
.btn-quiet:hover { color: var(--ink); border-color: var(--rule-light); }
.btn-quiet::after { display: none; }

.icon-btn {
  width: 36px; height: 34px; display: grid; place-items: center; padding: 0;
}
/* `display: grid` above BEATS the UA's `[hidden] { display: none }`, so every
   hidden icon button was still laid out -- which is why the home screen had a
   blank 36px box where a back arrow would be. One rule, once, rather than a
   `hidden` that means nothing on this class. */
[hidden] { display: none !important; }
.icon-btn svg { width: 19px; height: 19px; display: block; }

/* --- the item tile ----------------------------------------------------
 * The control the whole editor is made of. Specify once, derive the rest.
 * The selected tile stays WHITE — the glow is the cue, and tinting it green
 * would double the signal and put the label at 3.64:1. */

.grid {
  display: grid;
  /* THREE A ROW, and the tile is big enough to actually see. Packing twelve
     thumbnails into a row makes a wall of 60px stamps you cannot read; three
     makes each one a picture of a garment. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-6);
}
.tile {
  position: relative; display: flex; flex-direction: column; padding: 0;
  background: var(--panel); border: 1px solid var(--rule-light);
  border-radius: var(--r-tile); box-shadow: var(--shadow-rest);
  cursor: pointer; overflow: hidden; text-align: center;
  transition: background var(--dur-half) var(--ease-out),
              border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              transform var(--dur-half) var(--ease-out);
}
.tile .pic {
  position: relative; aspect-ratio: 1; display: block;
  background: var(--panel-2); overflow: hidden;
}
.tile { box-shadow: var(--shadow-rest), var(--bevel); }
/* NO OVERLAY ON THE ARTWORK. The picture keeps a lit top edge and a contact
   shade and nothing else — a wash over an item photo is the haze this used to
   have. */
.tile .pic { box-shadow: var(--pic-edge); }
.tile img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* THE MOVING PREVIEW, on hover, for the 2,644 items that have one.
   Stacked over the still rather than swapped into the same <img>: swapping src
   blanks the element for a frame and the tile flickers on every pass of the
   pointer. Both live in the same box; the GIF fades up once it has decoded. */
.tile .pic img.mov {
  position: absolute; inset: 0; opacity: 0;
  transition: opacity var(--dur-half) var(--ease-out);
}
.tile .pic img.mov.ready { opacity: 1; }
/* The still steps aside for the animation rather than showing through it. Same
   fade, so the two cross over instead of one blinking out. See armHoverPreview. */
.tile .pic.moving > img:not(.mov) {
  opacity: 0;
  transition: opacity var(--dur-half) var(--ease-out);
}
@media (prefers-reduced-motion: reduce) { .tile .pic img.mov { display: none; } }

.tile .n {
  position: relative; font-size: var(--fs-second);
  background: linear-gradient(to bottom, #FFFFFF, #F4F4F4);
  padding: var(--sp-4) var(--sp-5) var(--sp-5); font-size: var(--fs-debug);
  line-height: 1.25; color: var(--ink-carbon);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tile:hover { border-color: var(--rule-heavy);
              box-shadow: var(--shadow-hover), var(--bevel); transform: translateY(-2px); }
.tile:hover .n { color: var(--ink); }
/* NEVER SCALE IN THE TILE GRID. A transform on 300 nodes is a repaint storm.
   Buttons scale; tiles translate one pixel. */
.tile:active { border-color: var(--rule-heavy); box-shadow: var(--shadow-press);
               transform: translateY(1px); }

.tile.on { box-shadow: var(--aura-select); animation: breathe 10s ease-in-out infinite; }
.tile.on .n { color: var(--ink); font-weight: 700; }
.tile.on:focus-visible { box-shadow: var(--aura-select), var(--aura-focus); }
.tile.refused { animation: refuse 250ms var(--ease-out); }
.tile[disabled] { opacity: .40; filter: grayscale(1); cursor: default; box-shadow: none; }
/* NOTHING STAMPS AN AWARD. The shelf briefly did, on the argument that
   provenance was why the item was there -- but the shelf is six pictures you
   pick from by looking, and a label that changes nothing about what the item
   does is one more thing to read past. Kept for the marketplace's owned state,
   which is what it was measured for. */
.tile .badge {
  position: absolute; left: 4px; top: 4px; z-index: 2;
  background: var(--owned); color: #fff; font-size: 9px; font-weight: 700;
  padding: 1px 5px; border-radius: var(--r-button);
}

/* Only the outer halo breathes — never the ring, never the geometry, so
   nothing appears to resize. Ten seconds, taken from the avatar's own idle
   gaze cadence. */
@keyframes breathe {
  0%, 100% { box-shadow: 0 0 0 1px #FFF, 0 0 0 3px #5C9514,
             0 0 10px 1px rgba(92,149,20,.50), 0 0 22px 6px rgba(92,149,20,.22),
             0 1px 2px rgba(0,0,0,.18); }
  50%      { box-shadow: 0 0 0 1px #FFF, 0 0 0 3px #5C9514,
             0 0 10px 1px rgba(92,149,20,.50), 0 0 26px 8px rgba(92,149,20,.30),
             0 1px 2px rgba(0,0,0,.18); }
}
/* A refusal is a border pulse, NOT a shake. A shake says "you did something
   wrong"; the router refusing an item is usually the item's fault. */
@keyframes refuse {
  0%, 100% { border-color: var(--rule-light); }
  50%      { border-color: var(--refuse); }
}
@media (prefers-reduced-motion: reduce) {
  .tile.on { animation: none; }
  .tile.refused { animation: none; outline: 2px solid var(--refuse); }
}

/* --- the home menu ---------------------------------------------------- */

.group { color: var(--ink-dim); font-size: var(--fs-second); margin: var(--sp-8) 0 var(--sp-5); }
.group:first-child { margin-top: 0; }
/* VERTICAL, picture above text. A 34px icon beside a label is a list row that
   happens to have a picture on it; the picture has to be the thing you read. */
.cats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: var(--sp-6); }
.cat {
  display: flex; flex-direction: column; padding: 0;
  background: var(--panel); border: 1px solid var(--rule-light); border-radius: var(--r-tile);
  cursor: pointer; text-align: center; color: var(--ink); overflow: hidden;
  box-shadow: var(--shadow-rest), var(--bevel);
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              transform var(--dur-half) var(--ease-out);
}
.cat:hover { border-color: var(--rule-heavy);
             box-shadow: var(--shadow-hover), var(--bevel); transform: translateY(-2px); }
.cat:active { box-shadow: var(--bevel-press); transform: translateY(1px); }
.cat .pic {
  position: relative; display: block; width: 100%; aspect-ratio: 4 / 5;
  background: linear-gradient(to bottom, #F7F7F7, #E9E9E9);
  border-bottom: 1px solid var(--rule-light); overflow: hidden;
}
.cat .pic { box-shadow: var(--pic-edge); }
.cat img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A collection's art is a 64px square logo, not a body shot: contain it on its
   own ground rather than cropping a logo to 4:5. */
.cat .pic.banner { aspect-ratio: 420 / 95; background: var(--panel-2); }
.cat .pic.banner img { object-fit: cover; padding: 0; }
/* A collection tile is a wide banner, so the browse grid is two across. */
.cats.wide { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cat .cw { position: relative; padding: var(--sp-5) var(--sp-4) var(--sp-6);
            background: linear-gradient(to bottom, #FFFFFF, #F2F2F2); }
/* The sheen lives on the LABEL, which is a chrome surface. This is the one
   place on a tile where gloss belongs. */
.cat .cw::before { content: ""; position: absolute; inset: 0;
                   background: var(--sheen); pointer-events: none; opacity: .55; }
.cat .cn { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
           font-weight: 700; }
.cat .cc { display: block; color: var(--ink-dim); font-size: var(--fs-debug);
           font-variant-numeric: tabular-nums; margin-top: 1px; }
.cat[disabled] { opacity: .45; cursor: default; box-shadow: none; }

/* --- the home panel --------------------------------------------------- */

.home { margin-bottom: var(--sp-9); }
.card { background: var(--panel-3); border: 1px solid var(--rule-light);
        border-radius: var(--r-panel); padding: var(--sp-8);
        margin-bottom: var(--sp-7); }
.cardh { font: var(--fw-subhead) var(--fs-subhead)/1.2 var(--ui); color: var(--ink);
         margin: 0 0 var(--sp-7); display: flex; align-items: center; }
.cardh .seeall { margin-left: auto; padding: 3px 8px; font-size: var(--fs-debug); }

/* The body card. Two figures, a triangle, a colour row — on a grid rather than
   a flex chain, because the flex version let the gender tiles grow to fill the
   pane and squeezed the triangle to nothing at some widths. Each piece has a
   fixed job and a fixed share. */
.whorow {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-9);
  align-items: center;
}

.genders { display: flex; gap: var(--sp-6); flex: none; }
.gender {
  width: 128px; padding: 0; cursor: pointer; overflow: hidden;
  background: var(--panel); border: 1px solid var(--rule-light);
  border-radius: var(--r-tile); box-shadow: var(--shadow-rest), var(--bevel);
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              transform var(--dur-half) var(--ease-out);
}
.gender .pic { position: relative; display: block; aspect-ratio: 3 / 4;
               background: linear-gradient(to bottom, #F7F7F7, #E9E9E9);
               box-shadow: var(--pic-edge); overflow: hidden; }
.gender img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 20%;
              display: block; }
.gender .gl { display: block; padding: var(--sp-5) 0 var(--sp-6);
              font-size: var(--fs-debug); color: var(--ink-dim);
              background: linear-gradient(to bottom, #FFFFFF, #F2F2F2); }
.gender:hover { border-color: var(--rule-heavy); transform: translateY(-2px);
                box-shadow: var(--shadow-hover), var(--bevel); }
.gender.on { box-shadow: var(--aura-select); border-color: var(--rule-light); }
.gender.on .gl { color: var(--ink); font-weight: 700; }

/* The svg carries its own labels now, so the div needs no padding to hold them
   and no min-width games -- it is a box for one thing. Capped so it never
   becomes a giant lozenge on a wide pane. */
.shape { position: relative; display: grid; place-items: center; min-width: 0; }
#tri { width: 100%; max-width: 268px; height: auto; display: block;
       cursor: crosshair; touch-action: none; overflow: visible; }
#tri:focus-visible { outline: none; }
#tri:focus-visible .trishape { stroke: var(--green-ink); stroke-width: 2.5; }
.trishape { fill: url(#trifill); stroke: var(--rule-heavy); stroke-width: 1.5;
            stroke-linejoin: round; }
.tridot { fill: var(--green-select); stroke: #fff; stroke-width: 2.5;
          filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); }
/* Inside the viewBox, so they scale with the triangle and stay welded to the
   corners they name. Font-size is in USER UNITS here, not pixels -- the svg is
   260 units wide and renders at up to 268px, so 13 units is about 13px. */
#tri .tl { font: 13px var(--ui); fill: var(--ink-dim); pointer-events: none;
           user-select: none; }

.homebar { display: flex; gap: var(--sp-5); margin-top: var(--sp-8); flex-wrap: wrap; }
.homebar .btn { display: inline-flex; align-items: center; gap: var(--sp-5); }
.homebar .btn svg { width: 14px; height: 14px; }

/* Featured collections, as banners. */
.banners { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
           gap: var(--sp-5); }
.banner { padding: 0; cursor: pointer; overflow: hidden; text-align: center;
          background: var(--panel); border: 1px solid var(--rule-light);
          border-radius: var(--r-tile); box-shadow: var(--shadow-rest), var(--bevel);
          transition: border-color var(--dur-half) var(--ease-out),
                      box-shadow var(--dur-half) var(--ease-out),
                      transform var(--dur-half) var(--ease-out); }
.banner:hover { border-color: var(--rule-heavy); transform: translateY(-2px);
                box-shadow: var(--shadow-hover), var(--bevel); }
.banner .bpic { display: block; position: relative; aspect-ratio: 420 / 95;
                background: var(--panel-2); box-shadow: var(--pic-edge); overflow: hidden; }
.banner img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The fallback is a 64px square in a wide frame: contain it and give it air, so
   it reads as a logo rather than a stretched picture. */
.banner img.sq { object-fit: contain; padding: var(--sp-5); }
.cat .pic.banner img.sq { object-fit: contain; padding: var(--sp-6); }
.banner { text-align: left; }
.banner .bn { display: block; padding: var(--sp-4) var(--sp-5) 0; font-size: var(--fs-debug);
              white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 700; }
.banner .bc { display: block; padding: 0 var(--sp-5) var(--sp-5); font-size: 11px;
              color: var(--ink-dim); font-variant-numeric: tabular-nums; }

/* --- the two doors --------------------------------------------------- */

.doors { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-6);
         margin-bottom: var(--sp-7); }
.door { display: flex; flex-direction: column; padding: 0; cursor: pointer;
        background: var(--panel); border: 1px solid var(--rule-light);
        border-radius: var(--r-panel); overflow: hidden; text-align: left;
        box-shadow: var(--shadow-rest), var(--bevel);
        transition: border-color var(--dur-half) var(--ease-out),
                    box-shadow var(--dur-half) var(--ease-out),
                    transform var(--dur-half) var(--ease-out); }
.door:hover { border-color: var(--rule-heavy); transform: translateY(-2px);
              box-shadow: var(--shadow-hover), var(--bevel); }
.door:active { box-shadow: var(--bevel-press); transform: translateY(1px); }
.door .dpics { display: grid; grid-template-columns: repeat(3, 1fr);
               background: linear-gradient(to bottom, #F7F7F7, #E9E9E9);
               border-bottom: 1px solid var(--rule-light); }
.door .dpics img { width: 100%; aspect-ratio: 4/5; object-fit: cover; display: block; }
.door .dw { position: relative; padding: var(--sp-6) var(--sp-7) var(--sp-7);
            background: linear-gradient(to bottom, #FFFFFF, #F2F2F2); }
.door .dw::before { content: ""; position: absolute; inset: 0;
                    background: var(--sheen); opacity: .5; pointer-events: none; }
.door .dn { display: block; font: var(--fw-subhead) var(--fs-subhead)/1.2 var(--ui);
            color: var(--ink); }
.door .dc { display: block; color: var(--ink-dim); font-size: var(--fs-debug);
            margin-top: 2px; font-variant-numeric: tabular-nums; }

/* --- colour swatches -------------------------------------------------- */

.skinrow, .colourstrip {
  display: flex; align-items: flex-start; gap: var(--sp-6);
  margin-top: var(--sp-8); padding-top: var(--sp-7);
  border-top: 1px solid var(--rule-light);
}
.skinrow .skinl, .colourstrip .skinl { padding-top: 6px; }
.colourstrip { margin: 0 0 var(--sp-7); padding-bottom: var(--sp-7);
               border-bottom: 1px solid var(--rule-light); }
.skinl { flex: none; font-size: var(--fs-debug); color: var(--ink-dim); min-width: 3.4em; }
/* NINE COLUMNS, AND THE NUMBER IS NOT ARBITRARY. Every colour set Microsoft
   shipped is a multiple of nine -- skin 18, eyes 18, mouth and brows and the
   rest 27, hair 36 -- so nine columns gives every slot whole rows with no
   short last line, and the spectrum row below fits the same nine exactly.
   Wrapping flex could not do this: it packs by available width, so the columns
   moved with the pane and the rows did not line up with each other. */
/* THE COLUMN COUNT COMES FROM THE SET, not from a constant. Skin ships
   eighteen tones and wants all eighteen on one line; hair ships thirty-six,
   which at this pane width would be 14px chips, so it falls back to nine and
   takes four rows. renderSwatches picks the number and sets --sw-cols; the
   spectrum row underneath is then built to exactly that width, so both rows are
   full and their columns line up. */
.swatches { display: grid; grid-template-columns: repeat(var(--sw-cols, 9), 1fr);
            gap: var(--sp-3); flex: 1; min-width: 0; }
/* 26px visual with a 32px hit area via padding — the era had nothing like this
   control, so it is sized to WCAG 2.5.8 rather than to a 2009 pixel budget. */
/* FILLS ITS CELL. A fixed 30px chip in a stretched column left-packs, which is
   how eighteen tones ended up stacked across half the box with white to the
   right of them. Square by aspect-ratio rather than by a matching height, so it
   stays square at whatever width nine columns of this pane works out to. */
.swatches .sw {
  width: 100%; aspect-ratio: 1; height: auto; padding: 0; cursor: pointer;
  border: 1px solid rgba(0,0,0,.28); border-radius: var(--r-button);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.5);
  transition: transform var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out);
}
.swatches .sw::before, .swatches .sw::after { content: none; }
.swatches .sw:hover { transform: translateY(-2px);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.5), 0 2px 4px rgba(0,0,0,.22); }
/* An ink-and-white double ring is the only selected marker that survives every
   possible fill — a coloured ring vanishes on a swatch of that colour. */
.swatches .sw.on {
  box-shadow: 0 0 0 2px #FFFFFF, 0 0 0 4px var(--ink),
              inset 0 1px 0 rgba(255,255,255,.5);
}

/* Any colour: a swatch-shaped well holding a native picker, last in the row.
   The swatches are the period-correct answer; this is the escape hatch, and the
   manifest stores a full 0xAARRGGBB either way so a green avatar costs nothing. */
/* OURS, ON ITS OWN ROW, IN THE SAME COLUMNS. Spanning the full grid and
   repeating the parent's track size is what keeps the chips underneath the ones
   above rather than merely below them. The rule above it is the break: these are
   not Microsoft's colours and the row should read as a different row. */
.swrow {
  grid-column: 1 / -1;
  display: grid; grid-template-columns: repeat(var(--sw-cols, 9), 1fr);
  gap: var(--sp-3);
  margin-top: var(--sp-4); padding-top: var(--sp-4);
  border-top: 1px solid var(--rule-light);
}

/* A SMOOTH BLEND, NOT A COLOUR WHEEL WITH A HOLE IN IT.
   The white inset ring was there to say "this one is different", which the
   rainbow already says; all it actually did was shrink the colour to a frame
   and make the chip read as an icon of a swatch rather than a swatch. A linear
   sweep also beats a conic here at 30px: a cone puts its seam and its centre
   inside the chip, and both are visible at this size. */
.swatches .sw.any { position: relative; overflow: hidden; padding: 0; cursor: pointer;
  background: linear-gradient(105deg,
    #d2352c 0%, #e07a1f 17%, #e8c020 33%, #7fad2a 50%,
    #2f7fd1 67%, #4a48b5 83%, #8e46b8 100%); }
.swatches .sw.any input { position: absolute; inset: -25%; width: 150%; height: 150%;
  padding: 0; border: 0; background: none; cursor: pointer; opacity: 0; }

/* Facial sets are small and closed, so their tiles get more room. */
.grid.roomy { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* --- browse ----------------------------------------------------------- */

.tools { display: flex; gap: var(--sp-5); margin-bottom: var(--sp-6); }
.tools input { flex: 1; min-width: 0; height: 26px; padding: 0 var(--sp-5);
               border: 1px solid var(--border-ctrl); border-radius: var(--r-button);
               background: var(--panel); font-size: var(--fs-second); }
.tools select { flex: none; height: 26px; border: 1px solid var(--border-ctrl);
                border-radius: var(--r-button); background: var(--panel);
                font-size: var(--fs-second); }

/* --- the stage -------------------------------------------------------- */

#stage .surface {
  position: absolute; inset: 0;
  /* The backdrop is a LAYER BEHIND THE CANVAS, never the GL clear. The clear
     stays transparent -- that is what lets a picture composite whatever is
     behind it, and what makes an item render croppable. `cover` because the
     shipped backdrops are 1920x1080 and the stage is portrait. */
  background: var(--stage);
  background-size: cover; background-position: 50% 50%; background-repeat: no-repeat;
  border-radius: var(--r-panel);
  overflow: hidden;
}
/* THE PICTURE IS SIZED BY HEIGHT AND BY NOTHING ELSE.
   `height: 100%; width: auto` on a replaced element with an intrinsic ratio --
   which a canvas has, from its backing store -- means the surface's HEIGHT sets
   the scale and the width simply follows. Centred, and the surface clips.

   THIS IS WHAT STOPS THE SETTINGS RAIL DOUBLING THE AVATAR. It was
   `inset: 0; object-fit: contain`, chosen when the only thing that moved was the
   equipped strip -- a HEIGHT change, where contain scales by the tighter axis,
   the width is constant, and the picture therefore holds still. The rail changes
   the other axis. Measured: opening it takes the box from 783 to 498 wide
   against a 1566x1724 backing, a 57% aspect swing, and contain answers that by
   scaling the whole picture down to 64% over the 500 ms slide. Then the refit
   lands at the end and snaps it back to full size -- and for the two frames the
   held still is up, the shrunken copy sits on top of the full-size render.
   Two avatars, different sizes, one on the other. That is the double vision.

   Height-pinned, a width change cannot rescale anything: the frame gets narrower
   and crops, or wider and reveals. The figure stays exactly the size it was, and
   the held still lines up with what replaces it because both are the same height
   and both are centred.

   The strip is an absolutely-positioned overlay now, so it no longer changes the
   surface's height at all -- the fold is invisible because there is nothing to
   compensate for, not because of the fit. Measured: surface 67..929 with the
   strip open and with it shut. */
#stage .surface.plain { background-image: none !important; background: var(--panel-2); }
#gl, .holdframe {
  display: block; position: absolute;
  top: 0; left: 50%; transform: translateX(-50%);
  height: 100%; width: auto; max-width: none;
}

/* The held frame: a copy of the last render, laid over the canvas while the
   host is rebuilt for a new size. Sits UNDER the overbar so the controls stay
   live, and takes no pointer events of its own. Laid out by the rule above, so
   it cannot disagree with the canvas about where the picture goes. */
.holdframe { z-index: 1; pointer-events: none; }

#stage .overbar {
  position: absolute; left: 0; right: 0; top: 0; z-index: 2;
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-5) var(--sp-6);
}
#stage .overbar .sp { margin-left: auto; }
.chip {
  background: rgba(255,255,255,.86); border: 1px solid var(--rule-heavy);
  border-radius: var(--r-button); padding: 3px 8px; font-size: var(--fs-debug);
  color: var(--ink-dim); backdrop-filter: blur(2px);
}

/* The prop shelf. Three across like the picker, but on the home screen the
   pictures are the content, so the pane is narrower and the tiles are not. */
.grid.props { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid.props .pic { background: var(--panel-2); }

/* The zoom group. Three controls that act as one, so they are butted together
   and the seams between them are shared rather than doubled -- the period's own
   button groups did exactly this. */
.zoomgrp { position: relative; display: flex; align-items: stretch; }
.zoomgrp > .btn { border-radius: 0; margin-left: -1px; }
.zoomgrp > .btn:first-child { border-radius: var(--r-button) 0 0 var(--r-button); margin-left: 0; }
.zoomgrp > .btn:nth-child(3) { border-radius: 0 var(--r-button) var(--r-button) 0; }
.zoomgrp > .btn:hover { z-index: 1; }
.zoomval {
  min-width: 62px; padding: 0 var(--sp-5); height: 34px;
  font-variant-numeric: tabular-nums; font-size: var(--fs-debug);
}
.zoomval::after { content: ""; }

.zoommenu {
  position: absolute; top: calc(100% + 4px); left: 50%; transform: translateX(-50%);
  z-index: 20; min-width: 104px; padding: var(--sp-3) 0;
  background: var(--panel); border: 1px solid var(--rule-heavy);
  border-radius: var(--r-button); box-shadow: var(--shadow-hover);
}
.zoommenu button {
  display: block; width: 100%; text-align: left; padding: 5px var(--sp-6);
  border: 0; background: none; cursor: pointer; color: var(--ink);
  font: var(--fs-second)/1.3 var(--ui); font-variant-numeric: tabular-nums;
}
.zoommenu button:hover { background: var(--green-select); color: #fff; }
.zoommenu hr { margin: var(--sp-3) 0; border: 0; border-top: 1px solid var(--rule-light); }

/* The body, as numbers. A disclosure inside the card rather than a second
   card: it is the same body the triangle above is showing, and putting it in
   its own panel would imply there were two. */
.advbody { margin-top: var(--sp-6); border-top: 1px solid var(--rule-light);
           padding-top: var(--sp-5); }
.advbody > summary {
  cursor: pointer; list-style: none; font-size: var(--fs-debug); color: var(--ink-dim);
  padding: 2px 0; user-select: none;
}
.advbody > summary::-webkit-details-marker { display: none; }
/* The era's disclosure was a solid triangle, and it turned. */
.advbody > summary::before {
  content: ""; display: inline-block; width: 0; height: 0; margin-right: 6px;
  border: 4px solid transparent; border-left-color: currentColor;
  vertical-align: 1px;
  transition: transform var(--dur-half) var(--ease-out);
}
.advbody[open] > summary::before { transform: rotate(90deg) translate(1px, 2px); }
.advbody > summary:hover { color: var(--ink); }
.advrow { display: flex; align-items: center; gap: var(--sp-5); padding: var(--sp-4) 0; }
.advrow .advl { flex: 0 0 52px; font-size: var(--fs-debug); color: var(--ink-dim); }
.advrow input[type="range"] { flex: 1 1 auto; min-width: 0; }
.advrow .advv { flex: 0 0 34px; text-align: right; font-size: var(--fs-debug);
                color: var(--ink-dim); font-variant-numeric: tabular-nums; }

/* --- the equipped strip -----------------------------------------------
 * A horizontal scroller, not a grid. A grid of twenty cells is a wall that
 * competes with the figure; a strip is glanceable and scrolls rather than
 * wrapping. Empty slots stay so you can see what is still open. */

/* The strip slides DOWN out of the panel and the surface takes the room.
   Height is animated, not transform: a transform would leave the space behind
   and the render would not grow into it, which is the whole point of hiding it.
   The height is a custom property so script can set the measured value once and
   the transition has two real numbers to move between — `auto` does not
   animate. */
/* THE STRIP DOES NOT GROW WHEN THE RAIL CLOSES.
   The render should take that space -- that is what closing the rail is for --
   but the strip is a small horizontal list that already scrolls at every width,
   so widening it gains nothing and costs a reflow of a control nobody touched.
   The cap is the exact width the strip has while the rail is open: the stage
   minus the column that just went away, so the number does not change at all.
   Centred, because the figure above it re-centres too. */
#equipwrap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
}
/* NO WIDTH CAP. This briefly pinned the strip to the width it has while the
   rail is open, so it would not grow when the rail closed. Reverted at the
   client's request: it takes whatever width the stage gives it. */

/* THE HEIGHT IS WRITTEN BY SCRIPT, IN PIXELS, AS AN INLINE STYLE.

   It used to be `height: var(--eq-h)` with a `.shut` rule setting it to 0, which
   is the tidy version and did not survive contact: the declaration resolved to
   `auto` and the strip simply never moved, while every part of the machinery
   around it -- the class, the custom property, the matching rule -- reported
   itself working. An accordion measured and set in pixels has one source of
   truth and no cascade to lose an argument with.

   Padding still collapses from the class, because a border-box height of 0 is
   clamped up to padding + border and the strip would leave an 11px seam. */
#equipped {
  display: flex; gap: var(--sp-4); overflow-x: auto; overflow-y: hidden;
  padding: var(--sp-5); background: var(--panel);
  border-top: 1px solid var(--rule-light);
  border-radius: 0 0 var(--r-panel) var(--r-panel);
  /* Floating over a picture rather than sitting under one, so it needs an edge
     of its own. Upward, because everything it could overlap is above it. */
  box-shadow: 0 -6px 14px rgba(20, 22, 27, .10);
  transition: height var(--dur-blend) var(--ease-out),
              padding-top var(--dur-blend) var(--ease-out),
              padding-bottom var(--dur-blend) var(--ease-out);
}
#equipwrap.shut #equipped {
  padding-top: 0; padding-bottom: 0; overflow: hidden;
}

/* The handle. A tab on the seam, centred, with the era's solid triangle in it.
   It sits half over the strip's top edge so it reads as attached to the strip
   rather than floating above it.

   IT CHANGES ANCHOR WHEN THE STRIP IS SHUT, and it has to. #equipwrap collapses
   to its 1px top border at the bottom of the stage, so "centred on the seam" put
   more than a third of the tab -- 7 of 18px, measured -- past the stage's bottom
   edge, where #stage's overflow:hidden clipped both the paint AND the hit area.
   Cut off, and cut off exactly when it is the only control on screen and the
   only way to get the strip back.

   Shut, it therefore sits entirely ABOVE the seam with a couple of pixels to
   spare for its shadow. The transform is transitioned so the two anchors read as
   one control moving rather than two positions swapping. */
#eqToggle {
  position: absolute; left: 50%; top: 0; z-index: 4;
  transform: translate(-50%, -50%);
  /* Big enough to hit. 18px tall was under any reasonable touch target, and
     this is the one control that can be alone on the screen. */
  width: 64px; height: 26px; padding: 0; cursor: pointer;
  display: grid; place-items: center;
  color: var(--ink-dim);
  background: var(--panel);
  border: 1px solid var(--rule-heavy);
  border-radius: var(--r-button);
  box-shadow: var(--shadow-rest);
  transition: color var(--dur-half) var(--ease-out),
              border-color var(--dur-half) var(--ease-out),
              transform var(--dur-blend) var(--ease-out);
}
#equipwrap.shut #eqToggle { transform: translate(-50%, calc(-100% - 3px)); }
#eqToggle:hover { color: var(--ink); border-color: var(--ink-carbon); }
#eqToggle svg { width: 15px; height: 15px; display: block;
                transition: transform var(--dur-blend) var(--ease-out); }
/* ONE FLIP, AND IT IS THIS ONE. The script ALSO swapped the glyph -- `down`
   when open, `up` when shut -- so the two mechanisms cancelled: the arrow became
   an up arrow and was then rotated 180 degrees back into a down arrow, and the
   control looked identical in both states. The rotation stays because it
   animates and a glyph swap cannot; the script now always sets `down` and lets
   this turn it over. */
#equipwrap.shut #eqToggle svg { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  #equipped, #eqToggle, #eqToggle svg { transition-duration: 1ms; }
}
.eq {
  flex: 0 0 auto; width: 66px; padding: 0; cursor: pointer; position: relative;
  background: var(--panel); border: 1px solid var(--rule-light);
  border-radius: var(--r-tile); overflow: hidden;
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out);
}
.eq:hover { border-color: var(--rule-heavy); box-shadow: var(--shadow-hover); }
.eq .pic { position: relative; aspect-ratio: 1; background: var(--panel-2); display: block; }
.eq img { width: 100%; height: 100%; object-fit: contain; display: block; }
.eq .lb { display: block; padding: 2px 4px 4px; font-size: 11px; line-height: 1.2;
          color: var(--ink-carbon); white-space: nowrap; overflow: hidden;
          text-overflow: ellipsis; }
.eq.on .lb { color: var(--ink); }
.eq.on { border-color: var(--rule-heavy); }
.eq.empty { border-style: dashed; }
.eq.empty .pic { display: grid; place-items: center; color: var(--rule-heavy); font-size: 15px; }
/* Covered is a NORMAL state, not an edge case: 8,327 of 20,350 items set more
   than one slot bit, so a composite swallowing a slot is ordinary traffic. */
.eq.covered { opacity: .5; }
.eq.covered .pic::after {
  content: ""; position: absolute; inset: 0;
  background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(0,0,0,.07) 4px 8px);
}
.eq .dot { position: absolute; top: 3px; right: 3px; width: 9px; height: 9px;
           border-radius: 50%; border: 1px solid rgba(0,0,0,.35); z-index: 2; }
.eq .x {
  position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; z-index: 3;
  border: 0; border-radius: var(--r-button); background: rgba(0,0,0,.55); color: #fff;
  font-size: 10px; line-height: 15px; padding: 0; cursor: pointer; display: none;
}
.eq.on:hover .x { display: block; }
/* A LOCKED SLOT HAS NO REMOVE CONTROL AT ALL, rather than one that refuses.
   Eyes, eyebrows, a mouth and the three shape records are not accessories —
   an avatar without them is not a plainer avatar, it is a broken one. So the
   × is absent and the cell still opens its category, because SWAPPING is the
   operation that was always wanted. */
.eq.locked .x { display: none !important; }
.eq.locked { cursor: pointer; }

/* --- the photobooth ---------------------------------------------------
 * "Take a photo" is a destination, not a mode dialog: it takes over the left
 * pane with three pivots and puts a transport under the stage. */

.booth { display: flex; flex-direction: column; gap: var(--sp-6); }

/* Pivots. The era's tab was underlined text, not a filled pill -- the fill is
   the Metro idiom this project is deliberately not. */
.pivots { display: flex; gap: var(--sp-7); border-bottom: 1px solid var(--rule-light);
          margin: calc(var(--sp-5) * -1) 0 var(--sp-5); }
.pivot {
  padding: var(--sp-5) 2px var(--sp-5); border: 0; background: none; cursor: pointer;
  font: var(--fs-body)/1.2 var(--ui); color: var(--ink-dim);
  border-bottom: 3px solid transparent; margin-bottom: -1px;
}
.pivot:hover { color: var(--ink); }
.pivot.on { color: var(--ink); font-weight: 700; border-bottom-color: var(--green-select); }

.animgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-6); }
.animtile {
  position: relative; display: flex; flex-direction: column; padding: 0; cursor: pointer;
  background: var(--panel); border: 1px solid var(--rule-light);
  border-radius: var(--r-tile); overflow: hidden; text-align: center;
  box-shadow: var(--shadow-rest);
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              transform var(--dur-half) var(--ease-out);
}
.animtile .pic { display: block; aspect-ratio: 1; background: var(--panel-2);
                 box-shadow: var(--pic-edge); }
.animtile img { width: 100%; height: 100%; object-fit: contain; display: block; }
.animtile .n { display: block; padding: var(--sp-4) var(--sp-4) var(--sp-5);
               font-size: var(--fs-debug); color: var(--ink-carbon); line-height: 1.25; }
.animtile:hover { border-color: var(--rule-heavy); transform: translateY(-2px);
                  box-shadow: var(--shadow-hover); }
.animtile.on { box-shadow: var(--aura-select); }
.animtile.on .n { color: var(--ink); font-weight: 700; }
/* A clip the mining never recovered. Shown because the app shows 28, greyed
   because we cannot play it, and it says why on hover. */
.animtile.gone { cursor: default; opacity: .42; filter: grayscale(1); box-shadow: none; }
.animtile.gone:hover { transform: none; border-color: var(--rule-light); }

.bggrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-5); }
.bgtile { padding: 0; cursor: pointer; background: var(--panel); overflow: hidden;
          border: 1px solid var(--rule-light); border-radius: var(--r-tile);
          box-shadow: var(--shadow-rest); text-align: left; }
.bgtile img { width: 100%; aspect-ratio: 200 / 112; object-fit: cover; display: block;
              box-shadow: var(--pic-edge); }
.bgplain { display: block; width: 100%; aspect-ratio: 200 / 112;
           background: linear-gradient(to bottom, #C1C1C1, #FFF 57%, #DADADA); }
.bgplain.none { background: repeating-conic-gradient(#DDD 0% 25%, #FFF 0% 50%) 0 0 / 16px 16px; }
.bgtile .n { display: block; padding: var(--sp-4) var(--sp-5); font-size: var(--fs-debug);
             color: var(--ink-dim); white-space: nowrap; overflow: hidden;
             text-overflow: ellipsis; }
.bgtile:hover { border-color: var(--rule-heavy); }
.bgtile.on { box-shadow: var(--aura-select); }
.bgtile.on .n { color: var(--ink); font-weight: 700; }

.sizelist { display: flex; flex-direction: column; gap: var(--sp-4); }
.sizerow {
  display: flex; align-items: baseline; gap: var(--sp-5); cursor: pointer; text-align: left;
  padding: var(--sp-5) var(--sp-6); background: var(--panel);
  border: 1px solid var(--rule-light); border-radius: var(--r-tile);
}
.sizerow:hover { border-color: var(--rule-heavy); }
.sizerow.on { box-shadow: var(--aura-select); }
.sizerow .sl { font-size: var(--fs-body); color: var(--ink); }
.sizerow.on .sl { font-weight: 700; }
.sizerow .sd { font-size: var(--fs-debug); color: var(--ink-dim); }
/* Marked where a value is ours rather than the app's, in the UI and not only in
   a comment, because the person reading it is the one who cares. */
.sizerow .ours { margin-left: auto; font-size: 10px; letter-spacing: .04em;
                 color: var(--ink-dim); border: 1px solid var(--rule-light);
                 border-radius: 2px; padding: 0 4px; }

/* --- framing, and the numbers under it ---------------------------------- */

/* The shot rows are size rows: same shape, same behaviour, and they are the
   same kind of choice. Reusing the class would have coupled two lists that
   happen to look alike today, so they share the look and not the selector. */
.shotlist { display: flex; flex-direction: column; gap: var(--sp-4); }
.shotrow {
  display: flex; align-items: baseline; gap: var(--sp-5); cursor: pointer; text-align: left;
  padding: var(--sp-5) var(--sp-6); background: var(--panel);
  border: 1px solid var(--rule-light); border-radius: var(--r-tile);
}
.shotrow:hover { border-color: var(--rule-heavy); }
.shotrow.on { box-shadow: var(--aura-select); }
.shotrow .sl { font-size: var(--fs-body); color: var(--ink); }
.shotrow.on .sl { font-weight: 700; }
.shotrow .sd { font-size: var(--fs-debug); color: var(--ink-dim); margin-left: auto; text-align: right; }

/* Three dials in a grid so the labels, the tracks and the readouts each line up
   on their own column. Numbers that jitter as a slider moves are the reason for
   the tabular figures. */
.boothdials {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-5) var(--sp-6);
  margin-top: var(--sp-8);
}
.bdial { display: contents; }
.bdial .bdl { font-size: var(--fs-debug); color: var(--ink-dim); }
.bdial input { width: 100%; }
.bdial .bdv {
  font: var(--type-debug); color: var(--ink);
  font-variant-numeric: tabular-nums;
  min-width: 46px; text-align: right;
}

/* --- output ------------------------------------------------------------- */

.customsize {
  display: flex; align-items: flex-end; gap: var(--sp-6);
  margin-top: var(--sp-7);
}
.customsize label { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.customsize label span { font-size: var(--fs-debug); color: var(--ink-dim); }
.customsize input {
  width: 92px;
  padding: var(--sp-5) var(--sp-6);
  font: var(--type-body); color: var(--ink);
  background: var(--panel-2);
  border: 1px solid var(--rule-heavy); border-radius: var(--radius);
  font-variant-numeric: tabular-nums;
}
.customsize .bx { padding-bottom: var(--sp-5); color: var(--ink-dim); }

/* The checkbox reads as one thing with its sentence: what it governs is the
   sentence, not the words "transparent background". */
.boothchk {
  display: flex; gap: var(--sp-6); align-items: flex-start;
  margin-top: var(--sp-8);
  font: var(--type-secondary); color: var(--ink-body);
  cursor: pointer;
}
.boothchk input { margin-top: 3px; flex: 0 0 auto; }
.boothchk b { display: block; color: var(--ink); font-weight: 700; }
/* Dimmed rather than hidden: the option still exists, it just does not apply to
   the size that is selected, and hiding it would look like a missing feature. */
.boothchk.off { cursor: default; opacity: .55; }

/* --- the expression pickers -------------------------------------------- */

/* Shaped like a shot row, because it is the same kind of statement: it says
   which of two things is happening rather than offering a value. */
.exprfollow {
  display: flex; flex-direction: column; gap: 2px; width: 100%;
  cursor: pointer; text-align: left;
  padding: var(--sp-5) var(--sp-6);
  background: var(--panel);
  border: 1px solid var(--rule-light); border-radius: var(--r-tile);
  margin-bottom: var(--sp-8);
}
.exprfollow:hover { border-color: var(--rule-heavy); }
.exprfollow.on { box-shadow: var(--aura-select); }
.exprfollow .sl { font-size: var(--fs-body); color: var(--ink); font-weight: 700; }
.exprfollow .sd { font-size: var(--fs-debug); color: var(--ink-dim); }

.exprhead {
  margin: var(--sp-7) 0 var(--sp-5);
  font-size: var(--fs-subhead); font-weight: var(--fw-subhead); color: var(--ink);
}
.exprnote { margin: 0 0 var(--sp-5); font: var(--type-secondary); color: var(--ink-caption); }

/* WRAPPING CHIPS, NOT A GRID. The names are "Sad" and "Look down" and
   "Phonetic DTH" -- three to eleven characters -- and a grid would either cut
   the long ones or leave the short ones swimming. Wrapping puts as many on a
   line as fit and keeps every one the width of its own word. */
.exprset { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.exprchip {
  padding: var(--sp-4) var(--sp-6);
  font: var(--type-body); color: var(--ink-body);
  background: var(--panel);
  border: 1px solid var(--rule-light); border-radius: var(--r-tile);
  cursor: pointer; white-space: nowrap;
}
.exprchip:hover { background: var(--panel-hover); border-color: var(--rule-heavy); }
.exprchip.on {
  background: rgb(var(--green-select-rgb) / .10);
  border-color: var(--green-select);
  color: var(--ink); font-weight: 700;
}
.exprchip:focus-visible { outline: 2px solid var(--green-select); outline-offset: 1px; }

/* --- the transport ---------------------------------------------------- */

/* PINNED TO THE BOTTOM, the same way the equipped strip it stands in for is.
   It was a flow child of #stage back when #stage was a flex column, and making
   the surface an absolute overlay took that column away: a static child beside
   an absolutely-positioned sibling gets the whole box to lay out in, so it went
   to the TOP of the stage and sat behind the render. Which is exactly where the
   scrubber turned up. */
#transport {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  /* THE SCRUBBER IS THE INSTRUMENT, so it gets the width. In one flex row the
     read-out sat beside it and took an equal share -- 166px of scrub for a
     30-second clip is five pixels a second, which is not something you can land
     a frame with. Two rows put the numbers under the track they describe and
     hand the track everything left over.

     It also stops the bar twitching. The read-out is repainted every frame, and
     as a flex item sized to its text "frame 9/451" and "frame 10/451" are
     different widths, so the scrubber resized under the thumb mid-drag. In its
     own cell the text changes without moving anything. */
  display: grid; align-items: center;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  grid-template-areas: "replay step scrub shoot"
                       "replay step code  shoot";
  column-gap: var(--sp-6); row-gap: 2px;
  padding: var(--sp-5) var(--sp-6); background: var(--panel);
  border-top: 1px solid var(--rule-light);
  border-radius: 0 0 var(--r-panel) var(--r-panel);
  box-shadow: 0 -6px 14px rgba(20, 22, 27, .10);
}
#transport .scrub { position: relative; grid-area: scrub; min-width: 0; }
#transport #pbReplay { grid-area: replay; }
#transport #pbStep   { grid-area: step; }
#transport .shutter  { grid-area: shoot; }
#transport input[type="range"] { width: 100%; display: block; }
#transport .tcode { grid-area: code; justify-self: center;
                    font-size: var(--fs-debug); color: var(--ink-dim);
                    font-variant-numeric: tabular-nums; white-space: nowrap; }

/* THE SHUTTER. Round, because a shutter is round -- it is the one control on
   this page that is not a rectangle, and that is what makes it findable without
   a label. Ring, gap, disc: the same three parts a camera's release has. */
.shutter {
  flex: none; width: 46px; height: 46px; padding: 0; cursor: pointer;
  border-radius: 50%; border: 2px solid var(--rule-heavy);
  background: var(--panel); display: grid; place-items: center;
  box-shadow: var(--shadow-rest);
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              transform 90ms var(--ease-out);
}
.shutter i { display: block; width: 32px; height: 32px; border-radius: 50%;
             background: var(--green-select);
             transition: background var(--dur-half) var(--ease-out),
                         transform 90ms var(--ease-out); }
.shutter:hover { border-color: var(--ink-carbon); box-shadow: var(--shadow-hover); }
.shutter:hover i { background: var(--green-ink); }
/* The press is the picture being taken: the disc snaps in, not down. */
.shutter:active { transform: scale(.96); }
.shutter:active i { transform: scale(.78); }

/* Stopping is a different state, not a different button, so it reads as the
   same control doing the opposite thing. */
#danceBtn.stopping { color: var(--owned); border-color: var(--owned); }

/* Three of the six camera controls only exist in the booth. The other three --
   zoom in, zoom out, capture -- are useful everywhere and stay. */
.photoonly { display: none; }
#app.photo .photoonly { display: grid; }

/* In the booth the stage is the subject, so the equipped strip goes away and
   the transport takes its place. */
#app.photo #equipwrap { display: none; }

/* --- the photo preview ------------------------------------------------ */

#shot {
  position: fixed; inset: 0; z-index: 50; display: grid; place-items: center;
  background: rgba(20, 22, 27, .55); padding: var(--sp-8);
}
.shotcard {
  background: var(--panel); border: 1px solid var(--rule-heavy);
  border-radius: var(--r-panel); box-shadow: 0 8px 32px rgba(0,0,0,.35);
  max-width: min(760px, 92vw); max-height: 92vh; display: flex; flex-direction: column;
}
.shothead { display: flex; align-items: center; gap: var(--sp-6);
            padding: var(--sp-7) var(--sp-7) var(--sp-5);
            border-bottom: 1px solid var(--rule-light); }
.shothead .t { font: var(--fw-subhead) var(--fs-subhead)/1.2 var(--ui); color: var(--ink); }
.shothead .icon-btn { margin-left: auto; }
.shotpic { padding: var(--sp-7); display: grid; place-items: center; overflow: auto;
           background: var(--panel-2); }
.shotpic img { max-width: 100%; max-height: 56vh; display: block;
               box-shadow: var(--pic-edge); background: var(--panel); }
/* A gamerpic is round, so the preview of one is round. */
.shotpic img.round { border-radius: 50%; box-shadow: none; }
.shotbar { display: flex; flex-wrap: wrap; gap: var(--sp-5); padding: var(--sp-6) var(--sp-7); }
.shotnote { padding: 0 var(--sp-7) var(--sp-7); font-size: var(--fs-debug);
            color: var(--ink-dim); font-variant-numeric: tabular-nums; }

/* --- the model export dialog ------------------------------------------ */

#mex {
  position: fixed; inset: 0; z-index: 50; display: grid; place-items: center;
  background: rgba(20, 22, 27, .55); padding: var(--sp-8);
}
.mexcard {
  background: var(--panel); border: 1px solid var(--rule-heavy);
  border-radius: var(--r-panel); box-shadow: 0 8px 32px rgba(0,0,0,.35);
  width: min(600px, 92vw); max-height: 92vh; display: flex; flex-direction: column;
}
.mexbody { padding: var(--sp-7); overflow-y: auto; display: flex;
           flex-direction: column; gap: var(--sp-6); }
.mexlede { margin: 0 0 var(--sp-4); font-size: var(--fs-second); color: var(--ink-dim);
           max-width: 52ch; }
.mexrow { display: flex; align-items: center; gap: var(--sp-6); }
.mexrow.top { align-items: flex-start; }
.mexrow .ml { flex: 0 0 84px; font-size: var(--fs-second); color: var(--ink-dim);
              padding-top: 3px; }
.mopts { display: flex; gap: var(--sp-4); flex-wrap: wrap; }
.mopt {
  padding: 4px 10px; cursor: pointer; font-size: var(--fs-debug); color: var(--ink);
  background: var(--panel); border: 1px solid var(--border-ctrl);
  border-radius: var(--r-button);
}
.mopt:hover { border-color: var(--rule-heavy); }
.mopt.on { box-shadow: var(--aura-select); border-color: var(--rule-light); font-weight: 700; }
.mcheck { display: flex; align-items: center; gap: var(--sp-4);
          font-size: var(--fs-debug); color: var(--ink); cursor: pointer; }

/* The clips, as a wrapping bank of toggles. Twenty-five of them: a list would
   be a scroll, a grid of three would be a wall, and the captions are short. */
.mexanims { display: flex; flex-wrap: wrap; gap: var(--sp-4); flex: 1 1 auto;
            max-height: 190px; overflow-y: auto; padding-right: var(--sp-4); }
.manim {
  padding: 3px 9px; cursor: pointer; font-size: 11px; color: var(--ink-carbon);
  background: var(--panel); border: 1px solid var(--rule-light);
  border-radius: var(--r-button); white-space: nowrap;
}
.manim:hover { border-color: var(--rule-heavy); color: var(--ink); }
.manim.on { box-shadow: var(--aura-select); border-color: var(--rule-light);
            color: var(--ink); font-weight: 700; }
.mexsize { margin-left: auto; align-self: center; font-size: var(--fs-debug);
           color: var(--ink-dim); font-variant-numeric: tabular-nums; }
/* The report wraps and keeps its line breaks: it is the only place the export
   says what it actually produced. */
#mexNote { white-space: pre-wrap; line-height: 1.45; }

/* --- the right rail --------------------------------------------------- */

/* A select, styled to the period rather than left native: the era's dropdowns
   were a 1px box, a flat fill and a small solid triangle, and the OS control on
   a modern browser is a rounded pill that reads as a different decade. The
   triangle is an inline SVG data URI, so there is no extra element to keep
   aligned and no request. */
.row select {
  flex: 1 1 auto; min-width: 0; max-width: 168px; height: 24px;
  padding: 0 22px 0 var(--sp-4);
  font: var(--fs-second)/1 var(--ui); color: var(--ink);
  background-color: var(--panel);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0h8L4 5z' fill='%23555'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 7px center;
  border: 1px solid var(--border-ctrl); border-radius: var(--r-button);
  -webkit-appearance: none; appearance: none; cursor: pointer;
}
.row select:hover { border-color: var(--rule-heavy); }
.row select:disabled { cursor: default; opacity: 1; }

/* A read-out is not a control. It is right-aligned with the values, tabular so
   digits do not jitter as they update, and wraps rather than truncating -- a
   diagnostic that ends in an ellipsis is not a diagnostic. */
.row .val.ro {
  flex: 1 1 auto; text-align: right; white-space: normal; word-break: break-word;
  font-variant-numeric: tabular-nums; color: var(--ink-dim); font-size: 11px;
  line-height: 1.35;
}

/* Off, and it says why. The reason sits on its own line under the row, in the
   same voice as a hint, so the row reads as one sentence rather than as a
   control plus an error. */
.row.off { opacity: .55; }
.row.off input, .row.off select, .row.off .sw { pointer-events: none; }
.row .because {
  flex-basis: 100%; font-size: var(--fs-debug); color: var(--ink-dim);
  font-style: italic; padding-top: 2px;
}

/* --- accessibility ----------------------------------------------------
 * Three settings, applied to the root so no component has to remember to ask. */

/* One step up the ramp, not a zoom: line heights and spacing are already in
   ems off these, so raising the roots moves the whole system together. */
html.bigtext {
  --fs-body: 13px; --fs-second: 12px; --fs-debug: 11.5px; --fs-subhead: 17px;
}

/* Chill Gray -> Alloy Gray. Both are period tokens; this promotes the hairline
   to the heavier of the two rather than inventing a contrast colour. */
html.hicontrast { --rule-light: #BDBEC0; --rule-heavy: #8A8B8D; }

/* NEVER 0ms. transitionend still has to fire, or a handler waiting on it
   strands and the thing it was going to do never happens. */
html[data-motion="on"] *,
html[data-motion="on"] *::before,
html[data-motion="on"] *::after {
  transition-duration: 1ms !important;
  animation-duration: 1ms !important;
  animation-iteration-count: 1 !important;
}


#right .rail-head {
  flex: none; z-index: 2; background: var(--panel);
  padding: var(--sp-7) var(--sp-8) var(--sp-5);
  border-bottom: 1px solid var(--rule-light);
}
#right .rail-head input {
  width: 100%; height: 26px; padding: 0 var(--sp-5);
  border: 1px solid var(--border-ctrl); border-radius: var(--r-button);
  background: var(--panel); font-size: var(--fs-second);
}
.sec { border-bottom: 1px solid var(--rule-light); }
.sec > summary {
  list-style: none; cursor: pointer; padding: var(--sp-6) var(--sp-8);
  display: flex; align-items: center; gap: var(--sp-5);
  font: var(--fw-meta) var(--fs-meta)/1.2 var(--ui); color: var(--ink);
}
.sec > summary::-webkit-details-marker { display: none; }
.sec > summary::before {
  content: ""; width: 0; height: 0; flex: none;
  border: 4px solid transparent; border-left-color: var(--ink-carbon);
  transition: transform var(--dur-half) var(--ease-out);
}
.sec[open] > summary::before { transform: rotate(90deg) translateX(-1px); }
.sec > summary:hover { background: var(--panel-hover); }
/* Changed-from-default, so someone who has been turning knobs can see where
   they have been. */
.sec.dirty > summary::after {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--green-select); margin-left: auto;
}
.sec .body { padding: 0 var(--sp-8) var(--sp-7); }
.row { display: flex; align-items: center; gap: var(--sp-6); padding: var(--sp-4) 0;
       font-size: var(--fs-second); }
.row .lbl { flex: 1; color: var(--ink-body); }
.row .val { color: var(--ink-dim); font-variant-numeric: tabular-nums;
            min-width: 4.5em; text-align: right; font-size: var(--fs-debug); }
.row input[type=range] { flex: 1; min-width: 0; accent-color: var(--green-select); }
.row select { flex: 1; min-width: 0; height: 24px; border: 1px solid var(--border-ctrl);
              border-radius: var(--r-button); background: var(--panel);
              font-size: var(--fs-debug); }
/* Every disabled control says WHY, in the same row, in plain words. */
.row.off { opacity: .55; }
.row .why { font-size: var(--fs-debug); color: var(--ink-carbon); font-style: italic; }

/* Switch. 16px visual, 24px hit area via padding — the period had no such
   control at all, so this is ours and is sized to WCAG 2.5.8 rather than to
   1990s pixel budgets. */
.sw { position: relative; flex: none; width: 40px; height: 22px; padding: 0; border: 0;
      background: none; cursor: pointer; }
.sw::before {
  content: ""; position: absolute; inset: 2px 0; border-radius: 9px;
  background: var(--rule-heavy); transition: background var(--dur-half) var(--ease-out);
}
.sw::after {
  content: ""; position: absolute; top: 1px; left: 1px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--panel); border: 1px solid var(--ink-carbon);
  box-shadow: 0 1px 2px rgba(0,0,0,.2);
  transition: transform var(--dur-half) var(--ease-out),
              border-color var(--dur-half) var(--ease-out);
}
.sw[aria-checked="true"]::before { background: var(--green-select); }
.sw[aria-checked="true"]::after { transform: translateX(18px); border-color: var(--green-ink); }

.note { background: var(--panel-3); border: 1px solid var(--rule-light);
        border-radius: var(--r-tile); padding: var(--sp-6) var(--sp-7);
        color: var(--ink-dim); font-size: var(--fs-debug); margin: var(--sp-5) 0; }
.mono { font-family: var(--mono); font-size: var(--fs-debug); }

/* --- log -------------------------------------------------------------- */

#log { max-height: 190px; overflow-y: auto; font-family: var(--mono);
       font-size: var(--fs-debug); line-height: 1.5; }
#log div { padding: 1px 0; color: var(--ink-dim); }
#log .ok { color: var(--green-ink); }
#log .warn { color: var(--live-orange); }
/* say() is called with BOTH 'err' and 'bad'. The old stylesheet defined only
   one of them, so half of every error rendered as ordinary grey text. */
#log .err, #log .bad { color: var(--error); }

/* --- toast ------------------------------------------------------------ */

#toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%) translateY(8px);
  background: var(--panel); border: 1px solid var(--rule-heavy);
  border-radius: var(--r-tile); box-shadow: var(--shadow-hover);
  padding: var(--sp-5) var(--sp-8); font-size: var(--fs-second); color: var(--ink);
  opacity: 0; pointer-events: none; z-index: 50;
  transition: opacity var(--dur-half) var(--ease-out), transform var(--dur-half) var(--ease-out);
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; }
#toast .act { margin-left: var(--sp-7); }

/* --- loading ---------------------------------------------------------- */

/* Skeletons belong in exactly one place, the thumbnail grid, and they must NOT
   shimmer. The tile already reserves its square, so the fill is enough. */
.tile img[data-loading] { background: var(--rule-light); }

/* THE LOADER, and it is the Xbox 360's own shape.
 *
 * The console's wait indicator was a RING OF SEGMENTS that filled and emptied
 * around the circle, not a spinning arc and not a bar — that ring is the single
 * most recognisable piece of 360 chrome after the guide button. Eight segments,
 * each fading on a staggered delay, which is what reads as travel around the
 * ring rather than rotation of a whole object.
 *
 * It only ever appears after 400 ms of real work; see showLoader(). */
.loading {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-7);
  padding: var(--sp-11) 0; color: var(--ink-dim); font-size: var(--fs-second);
}
.ring { position: relative; width: 46px; height: 46px; display: block; }
.ring i {
  position: absolute; inset: 0;
  transform: rotate(calc(var(--i) * 45deg));
}
.ring i::before {
  content: ""; position: absolute; top: 0; left: 50%; margin-left: -3.5px;
  width: 7px; height: 13px; border-radius: 3.5px;
  background: var(--green-select);
  animation: seg 1s linear infinite;
  animation-delay: calc(var(--i) * 0.125s);
}
@keyframes seg { 0% { opacity: 1; } 70%, 100% { opacity: .12; } }
@media (prefers-reduced-motion: reduce) {
  .ring i::before { animation: none; opacity: .5; }
}

/* THE WHOLE WINDOW, NOT THE STAGE.
 *
 * It used to be absolute inside #stage, which covers the render and nothing
 * else -- so a cold load showed the app bar, the item pane and the rail
 * assembling themselves for several seconds, and then the front page dropped
 * over the top of the finished editor. Two products in a row, neither of which
 * anybody asked to see. Fixed to the viewport it covers everything until the
 * front door is ready to take over. */
#boot {
  position: fixed; inset: 0; display: grid; place-items: center;
  background: var(--stage); z-index: 70;
  transition: opacity var(--dur-blend) var(--ease-out);
}
#boot.gone { opacity: 0; pointer-events: none; }

/* A DETERMINATE BAR, NOT A SPINNER. A spinner says "something is happening";
 * a bar that moves says "this will finish", and the difference over fifteen
 * seconds is the difference between waiting and giving up. The steps underneath
 * are the same ones the code actually walks through, in the same words. */
.bootcard {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-7);
  width: min(380px, 84vw);
  text-align: center;
}
/* Above the mark and quiet: it is a greeting, and the name underneath it is the
   thing being read. The card centres its children, so this centres with them
   and the two lines read as one sentence broken over two.

   The negative margin eats most of the card's own flex gap. That gap is right
   between the mark, the bar and the step text, which are three separate things;
   between "Welcome to" and the name it is a gulf. */
.bootwelcome {
  margin: 0 0 calc(0px - var(--sp-7) + 2px);
  font: var(--type-secondary);
  color: var(--ink-caption);
}
.bootmark { display: flex; align-items: center; gap: 12px; margin-bottom: var(--sp-6); }
.bootmark .logo { width: 44px; }
.bootmark .logo img { display: block; height: 44px; width: auto; }
.bootmark .wordmark svg { height: 30px; }

.boottrack {
  width: 100%; height: 5px;
  border-radius: 3px;
  background: rgb(0 0 0 / .10);
  overflow: hidden;
}
.boottrack i {
  display: block; height: 100%; width: 0;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--green-select), var(--green-brand));
  transition: width 420ms cubic-bezier(.1, .9, .2, 1);
  position: relative;
  overflow: hidden;
}
/* THE BAR HAS TO LOOK ALIVE BETWEEN UPDATES, because some of the work it is
   reporting cannot report from inside.

   A phase is a loop and can count its iterations; a single cold fitShot is one
   await, and measured it can be ten seconds on its own -- ten seconds during
   which there is nothing truthful to say and a static bar reads as a hang. This
   is a barber-pole over the FILLED portion only: it claims nothing about
   progress, the width still does that, and it says "still working" for the
   whole of a stretch where the width cannot move.

   Stopped under reduce-motion by the global rule above, which is correct: the
   width is still the real signal. */
.boottrack i::after {
  content: '';
  position: absolute; inset: 0;
  background: repeating-linear-gradient(115deg,
    rgb(255 255 255 / .00) 0 10px,
    rgb(255 255 255 / .22) 10px 20px);
  animation: bootpole 900ms linear infinite;
}
/* 22.07, NOT A ROUND NUMBER. The stripes repeat every 20px ALONG THE GRADIENT
   AXIS, and that axis is 25 degrees off horizontal, so one period measured
   across the bar is 20 / cos(25) = 22.068px. Translating by anything else
   leaves a visible jump at every iteration boundary, which on a loading bar
   reads as a stutter -- the exact impression the stripe is there to remove. */
@keyframes bootpole { to { transform: translateX(22.07px); } }
.bootstep { font: var(--type-meta); color: var(--ink-body); }
.bootsub { font: var(--type-secondary); color: var(--ink-caption); min-height: 1.4em; }

/* --- responsive -------------------------------------------------------
 * The picker was already written for a phone; the layout was not. */

@media (max-width: 1180px) {
  :root { --rail-left: 320px; --rail-right: 280px; }
}
@media (max-width: 940px) {
  #app { grid-template-columns: minmax(0, 1fr);
         grid-template-rows: var(--appbar) minmax(0, 44vh) minmax(0, 1fr);
         grid-template-areas: "bar" "stage" "left"; }
  #right { position: fixed; inset: var(--appbar) 0 0 auto; width: min(320px, 88vw);
           z-index: 40; border-radius: var(--r-panel) 0 0 0; }
  #app:not(.rail-open) #right { display: none; }
  #search { width: 150px; }
  .tagline { display: none; }
}

/* ==========================================================================
   Outfits, and the flow that fills them
   ========================================================================== */

/* THE RAIL'S TWO TABS. Full-width halves rather than a pair of chips, because
   there are exactly two and a segmented control that fills its container reads
   as "this column is one of these" rather than as two buttons that happen to
   sit together. The era's tab was a sprite with a 6px right curve; this is that
   shape flattened to what a 2009 page could actually draw in CSS. */
.rail-tabs {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: var(--sp-3);
  padding: var(--sp-6) var(--sp-7) 0;
  flex: none;
}
.rtab {
  appearance: none; cursor: pointer;
  font: inherit; font-size: var(--fs-meta); font-weight: var(--fw-meta);
  padding: var(--sp-5) var(--sp-6);
  color: var(--ink-dim);
  background: var(--panel-2);
  border: 1px solid var(--rule-light);
  border-bottom-color: var(--border);
  border-radius: var(--r-tab) var(--r-tab) 0 0;
  transition: background var(--dur-half) var(--ease-out),
              color var(--dur-half) var(--ease-out);
}
.rtab:hover { color: var(--ink); background: var(--panel-hover); }
.rtab.on {
  color: var(--ink); background: var(--panel);
  border-color: var(--border); border-bottom-color: var(--panel);
  /* The selected tab joins the panel below it. One pixel of overlap is what
     turns two rectangles into a tab and a card. */
  margin-bottom: -1px; padding-bottom: calc(var(--sp-5) + 1px);
}

#sonas { flex: 1 1 auto; overflow-y: auto; min-height: 0; padding: var(--sp-7); }

.sonashead {
  display: flex; align-items: baseline; gap: var(--sp-5);
  padding-bottom: var(--sp-5);
}
.sonashead .sh { font-size: var(--fs-subhead); font-weight: var(--fw-subhead); color: var(--ink); }
.sonashead .sn { font-size: var(--fs-debug); color: var(--ink-dim);
                 font-variant-numeric: tabular-nums; margin-left: auto; }

.sonasbar { display: flex; gap: var(--sp-5); padding-bottom: var(--sp-7); }
.sonasbar .btn { flex: 1 1 0; min-width: 0; }

/* TWO COLUMNS OF PORTRAITS. A slot is a picture of a person, so the card is the
   picture and everything else is chrome on top of it -- the number, and the two
   tools that only appear when there is something to act on. */
.slots {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: var(--sp-7); padding: 2px;
}
.slot {
  position: relative;
  border-radius: var(--r-tile);
  transition: transform 90ms var(--ease-out);
}
.slot.busy { opacity: .55; pointer-events: none; }
.slotpic {
  appearance: none; cursor: pointer; padding: 0;
  display: block; position: relative; width: 100%;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--panel-2);
  border: 1px solid var(--rule-light);
  border-radius: var(--r-tile);
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              background var(--dur-half) var(--ease-out);
}
.slotpic img { display: block; width: 100%; height: 100%; object-fit: contain; }
.slot:hover .slotpic { border-color: var(--ink-carbon); box-shadow: var(--shadow-hover); }
.slot:active .slotpic { transform: none; box-shadow: var(--shadow-press); }
/* Worn is GREEN, the console's own selection colour, and it is a ring rather
   than a fill so the picture is not tinted by the state. */
/* AN OUTER RING, NOT AN INSET ONE. These cards are `overflow: hidden` with a
   full-bleed picture in them, and an inset shadow paints under the content --
   so the ring was hidden behind the render everywhere except the label strip,
   which is what made the green look half-drawn. Outside the border it cannot be
   covered by anything, and the grid gap is 10px so a 2px ring has room. */
.slot.on .slotpic { border-color: var(--green-select); box-shadow: 0 0 0 2px var(--green-select); }

.slot.empty .slotpic {
  background: var(--panel-3);
  border-style: dashed; border-color: var(--rule-heavy);
  display: grid; place-items: center;
}
.slot.empty:hover .slotpic { border-style: solid; background: var(--panel-hover); }
.slot .plus { display: grid; place-items: center; width: 22px; height: 22px; color: var(--ink-carbon); }
.slot .plus svg { width: 22px; height: 22px; }
.slot .nopic { font-size: var(--fs-debug); color: var(--ink-dim); }

.slotn {
  position: absolute; left: var(--sp-3); top: var(--sp-3);
  min-width: 16px; padding: 0 var(--sp-2);
  font-size: 10px; line-height: 16px; text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim); background: rgba(255, 255, 255, .82);
  border: 1px solid var(--rule-light); border-radius: var(--r-button);
}
.slot.on .slotn { color: var(--green-ink); border-color: var(--green-select); }

/* The tools appear on hover and on keyboard focus, so the card is a picture
   until you mean to do something to it. */
.slottools {
  position: absolute; right: var(--sp-3); top: var(--sp-3);
  display: flex; gap: var(--sp-2);
  opacity: 0; transition: opacity var(--dur-half) var(--ease-out);
}
.slot:hover .slottools, .slot:focus-within .slottools { opacity: 1; }
.sbtn {
  appearance: none; cursor: pointer; padding: 0;
  width: 20px; height: 20px; display: grid; place-items: center;
  color: var(--ink-dim); background: rgba(255, 255, 255, .9);
  border: 1px solid var(--rule-light); border-radius: var(--r-button);
}
.sbtn svg { width: 12px; height: 12px; }
.sbtn:hover { color: var(--ink); border-color: var(--ink-carbon); }
.sbtn.del:hover { color: var(--error); border-color: var(--error); }

/* ---- the tutorial card ---------------------------------------------------- */

/* OVER THE RENDER, UNDER THE CONTROLS. z-index 2 puts it beneath the overbar's
   own buttons, so the zoom and the camera stay reachable while it is up, and
   pointer-events none on the card itself means it can never eat a drag of the
   figure -- only its dismiss button takes clicks. */
#tutor {
  /* CLEAR OF THE OVERBAR, computed from the parts the overbar is made of rather
     than from a number somebody measured once: a 34px icon button between two
     --sp-5 paddings is 46, and one more --sp-5 is the gap. Measured overlapping
     it by 8px when this was 34 + --sp-7. */
  position: absolute; left: var(--sp-7); right: var(--sp-7);
  top: calc(34px + var(--sp-5) * 3);
  z-index: 2; pointer-events: none;
  display: flex; align-items: flex-start; gap: var(--sp-5);
  padding: var(--sp-6) var(--sp-7);
  background: rgba(255, 255, 255, .94);
  border: 1px solid var(--rule-light);
  border-left: 3px solid var(--green-select);
  border-radius: var(--r-tile);
  box-shadow: var(--shadow-panel);
}
#tutor .tw { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
#tutor .tt { font-size: var(--fs-meta); font-weight: var(--fw-meta); color: var(--ink); }
#tutor .tb { font-size: var(--fs-second); line-height: var(--lh-second); color: var(--ink-body); }
#tutor .icon-btn { pointer-events: auto; flex: none; margin-left: auto; }
/* CLICKABLE, so it can be put away. The card was pointer-events:none with the x
   punched back through, which is why dismissing one card was not on offer: the
   card could not be clicked at all. Now the card takes the click and the x
   still takes its own. */
#tutor { pointer-events: auto; cursor: pointer; }
#tutor:hover { border-color: var(--rule-heavy); }
/* It arrives rather than appearing. 160 ms and 6px is enough to be noticed as a
   change without being a performance. */
#tutor.in { animation: tutorin 160ms var(--ease-out) both; }
@keyframes tutorin { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { #tutor.in { animation-duration: 1ms; } }

/* ---- the flow ------------------------------------------------------------- */

/* The flow fills the pane and does its own scrolling, so the footer can sit
   outside the scrollport instead of at the end of it. */
/* NO PADDING WHILE A FLOW IS IN IT. The pane's own padding is what a grid of
   tiles needs; the flow supplies its own inside .flowscroll, and leaving the
   pane's on top would float the footer 16px above the panel's bottom edge -- a
   pinned bar with a gap under it reads as an accident. */
#pane.flowing { overflow: hidden; padding: 0; }

/* ---- what an item is ------------------------------------------------------
   ONE BUTTON FOR THE WHOLE GRID, which is forced rather than chosen: a tile is
   itself a <button> -- it is the thing you press to put the garment on -- and a
   button inside a button is invalid HTML that the parser unnests. It also keeps
   a three-hundred-tile category at one extra node instead of three hundred. */
#tilemore {
  position: fixed;
  z-index: 60;
  width: 22px; height: 22px;
  padding: 0;
  display: grid; place-items: center;
  font: 700 13px/1 var(--ui);
  letter-spacing: .06em;
  color: var(--ink-body);
  background: rgb(255 255 255 / .92);
  border: 1px solid var(--rule-heavy);
  border-radius: 4px;
  box-shadow: var(--shadow-rest);
  cursor: pointer;
}
#tilemore:hover { background: #fff; color: var(--ink); border-color: var(--green-select); }
#tilemore[hidden] { display: none; }

.itemcard {
  position: fixed;
  z-index: 70;
  width: 320px;
  max-height: 78vh;
  overflow: auto;
  padding: var(--sp-8);
  background: var(--panel);
  border: 1px solid var(--rule-heavy);
  border-radius: var(--radius);
  box-shadow: 0 10px 30px rgb(0 0 0 / .22);
  animation: iccome 160ms var(--ease-out) both;
}
@keyframes iccome { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

.ichead { display: flex; gap: var(--sp-7); align-items: flex-start; }
.icpic {
  flex: 0 0 auto;
  width: 84px; height: 84px;
  border-radius: var(--r-tile);
  overflow: hidden;
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px var(--rule-light);
}
.icpic img { width: 100%; height: 100%; object-fit: contain; display: block; }
.ictitles { min-width: 0; }
.ictitles h3 {
  margin: 0;
  font-size: var(--fs-subhead); font-weight: var(--fw-subhead); color: var(--ink);
  border-bottom: 0; padding-bottom: 0;
  /* A name can be forty characters; the card is 320 and must not grow to fit. */
  overflow-wrap: anywhere;
}
.icsub { margin: 2px 0 0; font: var(--type-secondary); color: var(--ink-caption); }
.icdesc {
  margin: var(--sp-6) 0 0;
  font: var(--type-secondary); color: var(--ink-body);
}

/* The collection, as the thing it is: a square icon and a name that goes there.
   Not a line of text saying "from Halo" -- the icon is how it is recognised
   everywhere else on this page, and clicking it is what somebody will try. */
.icfrom { margin-top: var(--sp-7); }
.icchip {
  display: inline-flex; align-items: center; gap: var(--sp-5);
  max-width: 100%;
  padding: 3px 9px 3px 3px;
  background: var(--panel-2);
  border: 1px solid var(--rule-light);
  border-radius: 999px;
  font: var(--type-secondary); color: var(--ink-body);
  text-align: left;
}
.icchip img { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; display: block; }
.icchip span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.icchip.go { cursor: pointer; }
.icchip.go:hover { border-color: var(--green-select); color: var(--green-ink); background: var(--panel-hover); }

.icfacts {
  margin: var(--sp-7) 0 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 2px var(--sp-7);
  font: var(--type-debug);
}
.icfacts dt { color: var(--ink-dim); }
.icfacts dd {
  margin: 0; color: var(--ink-body);
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}

.icmodel { margin-top: var(--sp-7); padding-top: var(--sp-7); border-top: 1px solid var(--rule-light); }
.icnote { margin: 0; font: var(--type-secondary); color: var(--ink-caption); }
.icdl {
  display: block;
  margin-top: var(--sp-7);
  text-align: center;
  text-decoration: none;
}
/* Quiet, and at the bottom. Reporting a problem is a rare action on a card
   whose usual job is telling you about an item, so it reads as the last resort
   it is rather than competing with the download. */
.icbug {
  display: block; width: 100%;
  margin-top: var(--sp-7);
  font: var(--type-secondary);
  color: var(--ink-dim);
}
.icbug:hover { color: var(--ink); }

/* ---- reporting a problem --------------------------------------------------
   A real modal, unlike the item card: this one is a form, it takes focus, and
   half-typing a report and losing it to a stray click is worse than the cost of
   an overlay. */
.bugwrap {
  position: fixed; inset: 0; z-index: 95;
  display: grid; place-items: center;
  padding: var(--sp-8);
  background: rgb(0 0 0 / .34);
  animation: bugin 140ms var(--ease-out) both;
}
@keyframes bugin { from { opacity: 0; } to { opacity: 1; } }
.bugcard {
  width: min(460px, 100%);
  max-height: 88vh; overflow: auto;
  padding: var(--sp-9);
  background: var(--panel);
  border: 1px solid var(--rule-heavy);
  border-radius: var(--radius);
  box-shadow: 0 14px 40px rgb(0 0 0 / .3);
}
.bugcard h3 {
  margin: 0;
  font-size: var(--fs-subhead); font-weight: var(--fw-subhead); color: var(--ink);
  border-bottom: 0; padding-bottom: 0;
}
.bugon { margin: 2px 0 var(--sp-8); font: var(--type-secondary); color: var(--ink-caption); }
.bugl { display: block; margin-bottom: var(--sp-5); font: var(--type-meta); color: var(--ink-body); }
.bugcard textarea {
  width: 100%;
  padding: var(--sp-6);
  font: var(--type-body);
  color: var(--ink);
  background: var(--panel-2);
  border: 1px solid var(--rule-heavy);
  border-radius: var(--radius);
  resize: vertical;
}
.bugcard textarea:focus-visible { outline: 2px solid var(--green-select); outline-offset: 1px; }
/* The checkbox and its sentence read as one thing, because what it governs is
   the sentence rather than the word "diagnostics". */
.bugchk {
  display: flex; gap: var(--sp-6); align-items: flex-start;
  margin-top: var(--sp-7);
  font: var(--type-secondary); color: var(--ink-body);
  cursor: pointer;
}
.bugchk input { margin-top: 3px; flex: 0 0 auto; }
.bugerr { margin: var(--sp-6) 0 0; font: var(--type-secondary); color: var(--error); }
.bugerr[hidden] { display: none; }
.bugbar { display: flex; align-items: center; gap: var(--sp-6); margin-top: var(--sp-8); }
.bugbar .sp { flex: 1; }

/* ---- the face -------------------------------------------------------------
   TWO COLUMNS: WHICH PART, AND EVERY OPTION FOR IT.

   The old screen was five sampled grids stacked in one scrolling column, so
   your own choices were always somewhere above the fold and half the features
   were not on it at all. This is a rail of the ten parts of a face beside one
   board that holds a whole set -- and a whole set is at most forty-five things,
   which is the fact the layout is built on. Nothing here pages. */
.faceboard {
  display: grid;
  /* 168, MEASURED. The longest label is "Eye shadow" at 84px, and the chip
     spends 34 on the picture, 12 on gaps, 9 on padding and about 20 on the
     count. At 132 every label over one word ellipsised -- "Eye s...", "Face
     p...", "Facial ..." -- which is the one thing a rail of ten names must not
     do, because the name IS the control. */
  grid-template-columns: 168px minmax(0, 1fr);
  gap: var(--sp-8);
  align-items: start;
  min-height: 0;
}

/* THE RAIL IS THE FACE, TOP TO BOTTOM. Hair, brows, eyes, shadow, nose, paint,
   mouth, beard, chin, ears -- the order they sit on a head, so looking for the
   nose means looking between the eyes and the mouth. It sticks, because the
   board beside it scrolls and losing the map while reading the territory is
   how the old screen lost people. */
.facerail {
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.facechip {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-6);
  padding: 3px 6px 3px 3px;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  text-align: left;
  font: var(--type-body);
  color: var(--ink-body);
}
.facechip:hover { background: var(--panel-hover); }
.facechip.on {
  background: rgb(var(--green-select-rgb) / .10);
  border-color: var(--green-select);
  color: var(--ink);
}
.facechip:focus-visible { outline: 2px solid var(--green-select); outline-offset: 1px; }

/* The current pick, rendered. This is what makes the rail a readout of the face
   as well as a way around it -- the question "what have I chosen" was
   unanswerable on the old screen without scrolling back through everything. */
.fcp {
  width: 34px; height: 34px;
  border-radius: 5px;
  overflow: hidden;
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / .12);
  display: grid; place-items: center;
}
.fcp img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A dash reads as "nothing"; an empty box reads as "not loaded yet". */
.fcp.none { color: var(--ink-dim); font: var(--type-secondary); }
.fcn { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fcc {
  font: var(--type-debug);
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}
/* One of the three the format will not go without, and it is empty. Marked
   rather than blocked: the flow's own footer already refuses to leave. */
.facechip.needs .fcp { color: var(--error); box-shadow: inset 0 0 0 1px var(--error); }
.facechip.needs .fcn { color: var(--error); }

.facepane { min-width: 0; }
.facehead {
  display: flex; align-items: baseline; gap: var(--sp-6);
  padding-bottom: var(--sp-6);
}
.facehead h3 {
  margin: 0;
  font-size: var(--fs-subhead); font-weight: var(--fw-subhead); color: var(--ink);
  border-bottom: 0; padding-bottom: 0;
}
.facen { font: var(--type-debug); color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.facenote { margin: 0 0 var(--sp-6); font: var(--type-secondary); color: var(--ink-caption); }

/* BIGGER THAN A WARDROBE TILE AND SMALLER THAN AN APPEARANCE ONE.

   A face part is a small texture, so two eyebrows at 60px are the same eyebrow
   -- but `.grid.roomy`, which every appearance category uses, is TWO COLUMNS,
   and two columns of forty-five eyes is a column of pictures a hand high that
   you scroll for a minute. Auto-fill at 96px is five or six across a 720px
   pane: each one still reads, and the whole set is on one screen, which is the
   entire reason not paginating was worth doing.

   `.grid.roomy` is two classes and would out-specify a bare `.facegrid`, so
   this has to carry all three. */
.grid.roomy.facegrid { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
.facegrid .tile .pic { aspect-ratio: 1; }
.facegrid .tile .n { font: var(--type-debug); padding: 3px 4px; }
.tile.facenone .pic {
  display: grid; place-items: center;
  color: var(--ink-dim);
  font: 300 26px/1 var(--ui);
}

/* ON THE PANE'S WIDTH, NOT THE WINDOW'S.

   A media query is the wrong instrument here: this pane is
   clamp(360px, 45vw, 720px), so it is narrow on a wide screen with the rails
   open and wide on a narrow one with them shut. Keyed to the viewport the rail
   collapsed to its horizontal form while sitting in a 720px column with room
   for both. A container query asks the only question that matters. */
.flowscroll { container-type: inline-size; }
@container (max-width: 460px) {
  /* Too narrow for two columns: the rail goes horizontal and scrolls, which
     keeps the map-and-territory idea rather than collapsing back to the stack
     this replaced. */
  .faceboard { grid-template-columns: minmax(0, 1fr); }
  .facerail {
    position: static;
    flex-direction: row;
    overflow-x: auto;
    gap: var(--sp-6);
    padding-bottom: var(--sp-6);
    scrollbar-width: thin;
  }
  .facechip {
    flex: 0 0 auto;
    grid-template-columns: 34px auto;
    grid-template-rows: auto auto;
  }
  .facechip .fcc { display: none; }
}

/* ---- what the creation flow takes off the table ---------------------------
   The flow is eight screens with one way forward, and the header sits over it
   the whole time offering four ways out: the slot grid, the settings rail, Save
   and the account button. Every one of them abandons a half-made figure --
   endFlow restores whoever was on the stage before, so the work is simply gone
   -- and none of them is a thing anybody needs mid-creation. The flow saves at
   the end, settings are settings, and there is nothing to sign in FOR yet.

   Hidden rather than disabled: a greyed-out control still says "there is a
   thing here you cannot have", which is worse than the control not being part
   of this screen at all. They all come straight back when the flow ends. */
body.in-flow #railBtn,
body.in-flow #acctBtn,
body.in-flow #saveBtn,
body.in-flow #shotBtn,
body.in-flow #modelBtn { display: none; }
/* The rail itself, if it was already open when the flow started. */
body.in-flow #right { display: none; }
.flow { height: 100%; display: flex; flex-direction: column; min-height: 0; }
.flowscroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--sp-8);
  padding: var(--sp-8);
}
/* NOTHING IN A SCROLLING COLUMN MAY BE SHRUNK TO FIT.
   A flex item's default is flex-shrink:1, so when the grid in a collection is
   taller than the pane the browser takes the difference out of its siblings
   before it scrolls -- and the first sibling is the "All collections" back
   button, which is why it was intermittently squashed: it depended on how tall
   that particular collection's grid happened to be. Every child keeps its own
   height and .flowscroll scrolls, which is what it is for. */
.flowscroll > * { flex-shrink: 0; }
/* And the back button is a control, not a bar: stretch is the default in a
   column and made a text-width button span the pane. */
.flowscroll > .btn-quiet[data-flow="unsub"] { align-self: flex-start; }
.flowlede {
  margin: 0; font-size: var(--fs-second); line-height: var(--lh-second);
  color: var(--ink-dim); max-width: 62ch;
}
.flowsub { margin: 0; font-size: var(--fs-subhead); font-weight: var(--fw-subhead); color: var(--ink); }
.flowsec { margin: 0; }
.flowsec h3 {
  margin: 0 0 var(--sp-5);
  font-size: var(--fs-meta); font-weight: var(--fw-meta);
  letter-spacing: 0; color: var(--ink-dim);
}

/* The two doors. Big, because there are two of them and this is the first
   screen anybody sees; the description under each is what makes the choice
   answerable without trying both. */
.flowdoors { display: grid; gap: var(--sp-7); }
.flowdoor {
  appearance: none; cursor: pointer; text-align: left;
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-9) var(--sp-10);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-rest);
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              background var(--dur-half) var(--ease-out);
}
.flowdoor:hover { border-color: var(--green-select); box-shadow: var(--shadow-hover); background: var(--panel-hover); }
.fd-n { font-size: var(--fs-subhead); font-weight: var(--fw-subhead); color: var(--ink); }
.fd-c { font-size: var(--fs-second); line-height: var(--lh-second); color: var(--ink-dim); }

.flowbodies { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-7); }
.flowbody {
  appearance: none; cursor: pointer; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-3);
  padding: var(--sp-7);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-panel);
  transition: border-color var(--dur-half) var(--ease-out), box-shadow var(--dur-half) var(--ease-out);
}
.flowbody img { width: 100%; max-width: 150px; aspect-ratio: 3 / 4; object-fit: contain; }
.flowbody:hover { border-color: var(--ink-carbon); box-shadow: var(--shadow-hover); }
.flowbody.on { border-color: var(--green-select); box-shadow: 0 0 0 2px var(--green-select); }

/* THE LINE-UP. Three across, because six candidates in two rows of three reads
   as a cast and six in one row of six reads as a filmstrip -- and at this pane
   width a row of six would be 60px each, which is not a face. */
.starters { display: grid; grid-template-columns: repeat(3, 1fr);
            gap: var(--sp-8); padding: 2px; }
.starter {
  appearance: none; cursor: pointer; padding: 0;
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-tile); overflow: hidden;
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out),
              transform 90ms var(--ease-out);
}
.starter img { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: contain;
               background: var(--panel-2); }
/* Before its render lands the tile is a labelled empty frame rather than a
   broken image. */
.starter.nopic::before { content: ''; display: block; width: 100%; aspect-ratio: 3 / 4;
                         background: var(--panel-2); }
.starter .st-n {
  padding: var(--sp-5) var(--sp-4);
  font-size: var(--fs-debug); color: var(--ink-body);
  border-top: 1px solid var(--rule-light);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.starter:hover { border-color: var(--green-select); box-shadow: var(--shadow-hover); transform: translateY(-1px); }
.starter.on { border-color: var(--green-select); box-shadow: 0 0 0 2px var(--green-select); }
.starter.on .st-n { color: var(--green-ink); font-weight: var(--fw-meta); }

.flowcols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-7); }
.flowcol {
  appearance: none; cursor: pointer; padding: 0; text-align: left;
  display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-tile); overflow: hidden;
  transition: border-color var(--dur-half) var(--ease-out), box-shadow var(--dur-half) var(--ease-out);
}
/* THEIR OWN SHAPE, WHICH IS 420x95. Microsoft's store banners are a 4.42:1
   letterbox; showing them in a 16:9 box with `cover` scaled them up until the
   height fitted and threw away sixty percent of the width, which is why the
   collection cards looked like crops of something else. The art was always the
   right art -- the box was the wrong shape. */
.flowcol img { display: block; width: 100%; aspect-ratio: 420 / 95;
               object-fit: cover; background: var(--panel-2); }
.flowcol.nopic::before { content: ''; display: block; width: 100%;
                         aspect-ratio: 420 / 95; background: var(--panel-2); }
/* One across, so a 4.4:1 banner is wide enough to read at this pane width. */
.flowcols { grid-template-columns: 1fr; }
.flowcol .fc-n { padding: var(--sp-5) var(--sp-6) 0; font-size: var(--fs-second);
                 color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.flowcol .fc-c { padding: 0 var(--sp-6) var(--sp-5); font-size: var(--fs-debug); color: var(--ink-dim); }
.flowcol:hover { border-color: var(--green-select); box-shadow: var(--shadow-hover); }

.flowgrid { margin: 0; }

/* The footer, and it is always in the same place: Back on the left, Skip and
   the green Next on the right. A flow whose exits move between steps is a flow
   people misclick. */
.flowbar {
  display: flex; align-items: center; gap: var(--sp-5);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--rule-light);
}
.flowbar .sp { flex: 1 1 auto; }

/* A PLACE MARKER, NOT A PROGRESS BAR. It says which room you are in; it does
   not report how much of your creativity remains. */
.flowdots { display: flex; gap: var(--sp-4); justify-content: center; }
.flowdots i {
  display: block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--rule-heavy);
}
.flowdots i.on { background: var(--green-select); }

.flowdone {
  padding: var(--sp-10);
  background: var(--panel-3);
  border: 1px solid var(--rule-light);
  border-left: 3px solid var(--green-select);
  border-radius: var(--r-panel);
}
.flowdone .fdn { font-size: var(--fs-section); font-weight: var(--fw-section); color: var(--ink); }
.flowdone .fdc { margin: var(--sp-5) 0 0; font-size: var(--fs-second);
                 line-height: var(--lh-second); color: var(--ink-body); }

/* ---- the question --------------------------------------------------------- */

/* The same scrim and card the photo preview uses, at a third of the width: this
   holds two sentences and two buttons and a 760px box around that reads as an
   announcement rather than a question. */
/* ABOVE THE FRONT DOOR (60) AND THE SIGN-IN PAGE. The recovery code is shown
 * once, from inside submitAuth, while the sign-in page is still up -- at 60 it
 * would have opened behind it and the six words nobody can ever be shown again
 * would have been invisible. */
/* The gamerpic picker: the 84 that shipped with the console, and the booth. */
#picksheet {
  position: fixed; inset: 0; z-index: 82; display: grid; place-items: center;
  background: rgba(20, 22, 27, .55); padding: var(--sp-8);
}
#picksheet[hidden] { display: none; }
.pickcard {
  background: var(--panel); border: 1px solid var(--rule-heavy);
  border-radius: var(--r-panel); box-shadow: 0 8px 32px rgba(0,0,0,.35);
  width: min(560px, 94vw); max-height: 82vh; display: flex; flex-direction: column;
}
.pickcard > h2 { margin: 0; padding: var(--sp-4) var(--sp-5); font-size: 15px;
  border-bottom: 1px solid var(--rule); }
.pickgrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: var(--sp-3); padding: var(--sp-4) var(--sp-5); overflow-y: auto;
}
.pickgrid button {
  padding: 0; cursor: pointer; line-height: 0; background: var(--panel-2);
  border: 1px solid var(--rule); border-radius: var(--r-tile); overflow: hidden;
  aspect-ratio: 1; display: grid; place-items: center;
}
.pickgrid button:hover { border-color: var(--rule-heavy); }
.pickgrid img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The booth, last, and deliberately not a picture: it is the door to the
   camera, not an eighty-fifth face. */
.pickgrid button.booth { background: var(--panel); color: var(--ink-dim);
  border-style: dashed; line-height: 1; font-size: 11px; padding: 4px; }
.pickbar { display: flex; gap: var(--sp-3); padding: var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--rule); }
.pickbar .sp { flex: 1; }

#ask {
  position: fixed; inset: 0; z-index: 80; display: grid; place-items: center;
  background: rgba(20, 22, 27, .55); padding: var(--sp-8);
}
#ask[hidden] { display: none; }
.askcard {
  background: var(--panel); border: 1px solid var(--rule-heavy);
  border-radius: var(--r-panel); box-shadow: 0 8px 32px rgba(0,0,0,.35);
  width: min(420px, 92vw);
}
.askbody {
  margin: 0; padding: var(--sp-8) var(--sp-9);
  font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-body);
}
/* DESTRUCTIVE IS RED, and it is the only red button in the app. Ownership was
   orange here and selection was green; red is kept for the one thing that
   cannot be undone. */
.btn.danger {
  background: var(--error); border-color: var(--error); color: #fff;
  font-weight: var(--fw-meta);
}
.btn.danger:hover { filter: brightness(1.08); }

/* ---- picking a slot ------------------------------------------------------- */

.pickhint { flex: 1 1 auto; font-size: var(--fs-debug); color: var(--ink-dim); line-height: 1.3; }
/* In pick mode every card is a target, so the whole grid says so rather than
   each card having to be read one at a time. */
.slot.target .slotpic { border-style: solid; border-color: var(--rule-heavy); }
.slot.target:hover .slotpic { border-color: var(--green-select); box-shadow: 0 0 0 2px var(--green-select); }

/* The flow's own slot grid: four across, because twenty in a 460px pane is five
   rows of four and the number is the thing being picked rather than the
   picture. */
.flowslots { display: grid; grid-template-columns: repeat(5, 1fr);
             gap: var(--sp-6); padding: 2px; }
.fslot {
  appearance: none; cursor: pointer; padding: 0; position: relative;
  aspect-ratio: 3 / 4; overflow: hidden;
  background: var(--panel-3);
  border: 1px dashed var(--rule-heavy); border-radius: var(--r-tile);
  transition: border-color var(--dur-half) var(--ease-out),
              box-shadow var(--dur-half) var(--ease-out);
}
.fslot.taken { border-style: solid; border-color: var(--border); background: var(--panel-2); }
.fslot img { display: block; width: 100%; height: 100%; object-fit: contain; }
.fslot .fsn {
  position: absolute; left: 0; right: 0; bottom: 0;
  font-size: 10px; line-height: 15px; text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim); background: rgba(255,255,255,.82);
  border-top: 1px solid var(--rule-light);
}
.fslot:hover { border-color: var(--ink-carbon); box-shadow: var(--shadow-hover); }
.fslot.on { border-style: solid; border-color: var(--green-select);
            box-shadow: 0 0 0 2px var(--green-select); }
.fslot.on .fsn { color: var(--green-ink); font-weight: var(--fw-meta); }

/* ---- the pinned footer ---------------------------------------------------- */

/* THE WAY FORWARD IS IN THE SAME PLACE ON EVERY SCREEN. The face step is four
   grids and 54 tiles, and Next at the end of that meant scrolling past
   everything you had just decided not to change in order to leave. Sticky
   rather than fixed, so it sits inside the pane and moves with it when the
   layout changes. */
.flowbar.pin {
  flex: none; z-index: 3;
  padding: var(--sp-6) var(--sp-8);
  background: var(--panel);
  border-top: 1px solid var(--rule-light);
  box-shadow: 0 -6px 14px rgba(20, 22, 27, .08);
}
/* A row of controls that belongs to the content, not to the navigation. */
.flowbar.plain { border-top: 0; padding-top: 0; }

/* ---- the prop button asks to be pressed ----------------------------------- */

/* A RING THAT BREATHES, not a flash. The button is 34px on a busy bar, so the
   attractor has to be visible without being an alarm: a green halo that grows
   and fades on a 1.6s cycle reads as "there is something here" at the edge of
   vision and is ignorable once seen. It stops for good the first time the clip
   plays, by either route -- an attractor that keeps going after it has been
   understood is a flashing light. */
@keyframes propcall {
  0%   { box-shadow: 0 0 0 0 rgba(92, 149, 20, .55); }
  70%  { box-shadow: 0 0 0 7px rgba(92, 149, 20, 0); }
  100% { box-shadow: 0 0 0 0 rgba(92, 149, 20, 0); }
}
#danceBtn.calling {
  color: var(--green-ink);
  border-color: var(--green-select);
  animation: propcall 1.6s var(--ease-out) infinite;
}
/* Hovering it means it has been found; the halo has done its job. */
#danceBtn.calling:hover { animation: none; }
@media (prefers-reduced-motion: reduce) {
  /* No pulse, but still marked -- the point is to be findable, and colour does
     that without motion. */
  #danceBtn.calling { animation: none; box-shadow: 0 0 0 2px var(--green-select); }
}

/* ---- the save hint --------------------------------------------------------
   THE SAME HALO, ON THE SAVE BUTTON, FOR TEN SECONDS.

   Coming out of the creation flow you are holding a figure that is not saved
   anywhere, and the old ending navigated to the slot grid to say so -- a filing
   cabinet in front of somebody who has just finished making something. Now the
   editor is what you land on and the one thing you need to know is pointed at:
   this is the propcall halo reused, deliberately, because it already means
   "there is something here" everywhere else in this app. */
/* Signed out the halo moves to My Sonas: Save only leads to the sign-in prompt
   and back, so pointing somebody with no account at it points them at a
   refusal. My Sonas opens the drawer, which already carries the button that
   makes an account. */
#saveBtn.calling, #sonasBtn.calling {
  color: var(--green-ink);
  border-color: var(--green-select);
  animation: propcall 1.6s var(--ease-out) infinite;
}
#saveBtn.calling:hover, #sonasBtn.calling:hover { animation: none; }
@media (prefers-reduced-motion: reduce) {
  #saveBtn.calling, #sonasBtn.calling {
    animation: none; box-shadow: 0 0 0 2px var(--green-select);
  }
}

/* The slot picker's close, in the corner of the head rather than at the end of
   a sentence in a 280px rail, where it was clipped by the edge of the pane.

   NO margin-left:auto HERE. `.sn` already carries it, so a second one made the
   two fight over the same leftover space and the button collapsed to two pixels
   and hung over the edge -- the very fault this was moved to fix. `flex-shrink:
   0` is the whole of what it needs: `.sn` pushes the pair right, and this one
   refuses to be squeezed. */
.sonasx {
  flex: 0 0 auto;
  align-self: center;
  width: 26px; height: 26px;
  padding: 0;
  font: 400 18px/1 var(--ui);
  display: grid; place-items: center;
}
/* And the title gives up its space first, so a long one cannot push the close
   out of the rail either. */
.sonashead .sh { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The card. Under the button, pointing up at it, and it never takes a click --
   the whole point is that the button underneath stays reachable. */
.savehint {
  position: fixed;
  z-index: 90;
  width: max-content;
  max-width: 260px;
  padding: 9px 12px;
  pointer-events: none;
  background: linear-gradient(#ffffff, #f4f6f0);
  border: 1px solid var(--green-select);
  border-radius: var(--radius);
  box-shadow: 0 6px 18px rgb(0 0 0 / .18);
  animation: savehintin 260ms var(--ease-out) both;
}
.savehint::before {
  content: '';
  position: absolute;
  top: -5px; left: var(--tip, 16px);
  width: 8px; height: 8px;
  background: #ffffff;
  border-left: 1px solid var(--green-select);
  border-top: 1px solid var(--green-select);
  transform: rotate(45deg);
}
/* POINTING DOWN, for a card above its button. The shutter lives on a transport
   pinned to the bottom of the stage, so a hint below it would be off screen.
   Same arrow, opposite edge and the borders swap corners with it. */
.savehint.up::before {
  top: auto; bottom: -5px;
  border-left: 0; border-top: 0;
  border-right: 1px solid var(--green-select);
  border-bottom: 1px solid var(--green-select);
  background: #f4f6f0;
}
.savehint.up { animation-name: savehintup; }
@keyframes savehintup {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

.savehint b {
  display: block;
  font: var(--type-meta);
  letter-spacing: 0;
  color: var(--green-ink);
}
.savehint span {
  display: block;
  margin-top: 2px;
  font: var(--type-secondary);
  color: var(--ink-body);
}
@keyframes savehintin {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .savehint { animation: none; } }

/* ---- the dialog's portrait ------------------------------------------------ */

/* Hidden during creation -- there is nothing on those screens to search, and a
   search from one would leave the flow for a results list. */
#left .pane-search[hidden] { display: none; }

.askrow { display: flex; gap: var(--sp-8); align-items: flex-start;
          padding: var(--sp-8) var(--sp-9); }
.askrow .askbody { padding: 0; }
/* The figure sits on the panel's own ground rather than in a frame: a border
   around a cut-out render reads as a photograph of them, and what is wanted is
   them, reacting. */
.askpic { flex: none; width: 96px; height: 128px; }
.askpic[hidden] { display: none; }
.askpic canvas { display: block; width: 100%; height: 100%; }

/* ---- there is something to save ------------------------------------------- */

/* A DOT, NOT A BADGE. The count is always one -- there is one figure and one
   slot -- so a number would be noise; the question is only "is there anything".
   Orange, which is this palette's ownership colour, and the one thing on the
   bar that is neither green (chosen) nor red (destroys). */
#saveBtn { position: relative; }
#saveBtn.dirty::after {
  content: ''; position: absolute; top: 3px; right: 3px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--owned);
  box-shadow: 0 0 0 1.5px var(--panel);
}
#saveBtn.dirty { color: var(--ink); }

/* ---- the flow's skin step ------------------------------------------------- */

/* CAPPED. The swatch grid stretches its chips to fill, which is right in the
   home card's narrow strip and wrong across a 460px creation pane -- eighteen
   tones at 1fr each became 25px tall rows of enormous squares that pushed the
   rest of the screen out of view. 34px is the size the same control is in the
   card it was designed for. */
.flowscroll .swatches { grid-template-columns: repeat(var(--sw-cols, 9), minmax(0, 34px));
                        justify-content: start; }
.flowscroll .swrow { grid-template-columns: repeat(var(--sw-cols, 9), minmax(0, 34px));
                     justify-content: start; }

/* ---- the primary button --------------------------------------------------- */

/* `.go` HAD NO STYLE AT ALL. Every Next, every Make a new one and every affirming
   answer carried the class and rendered as a plain grey button, so the way
   forward looked exactly like the way back.

   The treatment is Microsoft's own buy-now button, and the tokens say how:
   buynow_middle.gif is the flat #7FAD2A fill, a,a:link is #336600 for the edge,
   and #ABEF2E is documented as hover TEXT on a green fill and never a fill
   itself -- so the hover brightens the label rather than the button. */
.btn.go {
  background: var(--green-action);
  border-color: var(--green-ink);
  color: #FFFFFF;
  font-weight: var(--fw-meta);
  text-shadow: 0 1px 0 rgba(0, 0, 0, .18);
}
.btn.go:hover { color: var(--green-hover); border-color: var(--green-ink); }
.btn.go:active { box-shadow: var(--shadow-press); }
.btn.go:disabled, .btn.go[disabled] {
  background: var(--panel-2); border-color: var(--rule-heavy);
  color: var(--ink-carbon); text-shadow: none;
}

/* What is still needed before a step will let go. Said out loud, next to the
   button that is refusing, rather than left for somebody to work out. */
.flowneed {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fs-debug); color: var(--ink-dim); line-height: 1.3;
}
.flowneed b { color: var(--owned); font-weight: var(--fw-meta); }

/* ---- the filter bar ------------------------------------------------------- */

/* CHIPS, NOT DROPDOWNS. Every facet here is a short list with a count beside it,
   and a count is the thing that makes a filter answerable before you press it --
   "Hoodie 759" tells you whether it is worth trying. A select would hide the
   counts behind a click each. */
/* THE BAR IS SHUT BY DEFAULT AND ITS HANDLE IS NOT. Three rows of chips above a
   grid is three rows of chips in the way of the grid -- and open, they were
   wrapping into each other at this pane width, which is what "overlapping"
   was. The head carries the count and how many facets are live, so the state is
   readable without opening it, which is the whole reason it can be shut. */
.filterwrap {
  display: flex; flex-direction: column; gap: var(--sp-5);
  padding-bottom: var(--sp-6);
  border-bottom: 1px solid var(--rule-light);
  margin-bottom: var(--sp-7);
}
.fhead { display: flex; align-items: center; gap: var(--sp-6); }
.fbtn {
  display: inline-flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-6);
}
.fbtn svg { width: 13px; height: 13px; flex: none; }
/* Live filters colour the handle, so a short list always has a visible reason
   for being short even with the panel shut. */
.fbtn.live { color: var(--green-ink); border-color: var(--green-select); }
.fbtn.open { background: var(--panel-2); }
.fhead .fcount { margin-left: auto; }

.filters {
  display: flex; flex-direction: column; gap: var(--sp-5);
  /* Its own scroll rather than an unbounded stack: "From" alone can be six
     collections, and the panel must never be taller than the grid it filters. */
  max-height: 40vh; overflow-y: auto;
  padding: var(--sp-6);
  background: var(--panel-2);
  border: 1px solid var(--rule-light); border-radius: var(--r-tile);
}
/* GRID, NOT WRAPPING FLEX, so the label column lines up down the panel and a
   long chip cannot push the next row's label out from under the one above it.
   The chips get their own wrapping row inside the second column. */
.frow { display: grid; grid-template-columns: 46px minmax(0, 1fr); gap: var(--sp-5);
        align-items: start; }
.frow > .fchip { grid-column: 2; }
.frow.on { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.fchips { display: flex; flex-wrap: wrap; gap: var(--sp-3); min-width: 0; }
/* The chosen row reads as a statement about the list, so it sits above the
   choices and carries the only Clear. */
.frow.on { padding-bottom: var(--sp-4); }
.flab {
  padding-top: 2px;
  font-size: var(--fs-debug); font-weight: var(--fw-meta);
  color: var(--ink-dim); letter-spacing: 0;
}
.fchip {
  appearance: none; cursor: pointer;
  /* Capped, because a collection name can be forty characters and one chip
     should never be the whole row. */
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  padding: 2px var(--sp-5);
  font: inherit; font-size: var(--fs-debug); line-height: 18px;
  color: var(--ink-body); background: var(--panel-2);
  border: 1px solid var(--rule-light); border-radius: 9px;
  white-space: nowrap;
  transition: background var(--dur-half) var(--ease-out),
              border-color var(--dur-half) var(--ease-out),
              color var(--dur-half) var(--ease-out);
}
.fchip:hover { background: var(--panel-hover); border-color: var(--ink-carbon); color: var(--ink); }
.fchip.on {
  background: var(--green-select); border-color: var(--green-ink); color: #fff;
  font-weight: var(--fw-meta);
}
/* A chosen chip is a thing to take off, so it says so on hover rather than
   looking like it will apply itself twice. */
.fchip.on::after { content: ' \00d7'; opacity: .75; }
.fchip.clear { background: none; border-color: transparent; color: var(--ink-dim); }
.fchip.clear:hover { color: var(--error); border-color: var(--error); background: none; }
.fcount {
  font-size: var(--fs-debug); color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}

/* The empty slot's figure. Faded and floating, because it is a placeholder for
   somebody rather than somebody -- it should read as absent, not as a character
   who happens to be grey. */
.askpic canvas.aloof { opacity: .9; filter: saturate(.35); }

/* ---- naming a slot -------------------------------------------------------- */

/* The name sits UNDER the picture rather than over it. A caption on a portrait
   is a caption; the same text laid across the figure is a watermark, and these
   cards are 119px wide with somebody's face in the middle of them. */
.slotname {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) 2px 0;
  min-width: 0;
}
.slotname .sname {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fs-debug); line-height: 1.25; color: var(--ink-body);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* An unnamed slot shows what it is called anyway -- "Slot 3" -- but dimmed, so
   a named one is visibly the deliberate thing and the fallback reads as absence
   rather than as a name somebody chose. */
.slotname.unnamed .sname { color: var(--ink-carbon); font-style: italic; }
.slot.on .slotname .sname { color: var(--green-ink); font-weight: var(--fw-meta); }

/* THE PENCIL ARRIVES ON HOVER. It is the third control on a small card and the
   least often wanted, so it stays out of the way until the pointer is on the
   thing it edits -- and it is kept for the keyboard by focus-within, because a
   control that only exists on hover does not exist at all to a keyboard. */
.spen {
  appearance: none; cursor: pointer; padding: 0; flex: none;
  width: 16px; height: 16px; display: grid; place-items: center;
  color: var(--ink-carbon); background: none; border: 0; border-radius: var(--r-button);
  opacity: 0; transition: opacity var(--dur-half) var(--ease-out),
                          color var(--dur-half) var(--ease-out);
}
.spen svg { width: 12px; height: 12px; }
.slot:hover .spen, .slot:focus-within .spen { opacity: 1; }
.spen:hover { color: var(--green-ink); }
.spen:focus-visible { opacity: 1; }

/* The field takes the label's exact place, so committing does not move anything
   by a pixel. */
.sedit {
  width: 100%; min-width: 0;
  font: inherit; font-size: var(--fs-debug); line-height: 1.25;
  padding: 0 var(--sp-2); margin: 0;
  color: var(--ink); background: var(--panel);
  border: 1px solid var(--green-select); border-radius: var(--r-button);
  outline: none;
}

/* The flow's chooser: number, then the name where there is one. */
.fslot .fsn { padding: 0 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---- a series, folded ----------------------------------------------------- */

/* Two stacked images and a cross-fade. The outgoing one keeps its place until
   the incoming one has loaded, so a slow thumbnail never leaves a hole. */
.tile.series .pic { position: relative; }
.tile.series .pic img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; transition: opacity 420ms var(--ease-out);
}
.tile.series .pic img.on { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .tile.series .pic img { transition-duration: 1ms; }
}
/* The count is the point of the tile -- it is what says "this is a hundred and
   one shoes, not one shoe" -- so it is on the picture rather than in the name,
   where a long family name would push it off. */
.scount {
  position: absolute; right: var(--sp-3); bottom: var(--sp-3); z-index: 2;
  min-width: 16px; padding: 0 var(--sp-3);
  font-size: 10px; line-height: 15px; text-align: center;
  font-variant-numeric: tabular-nums; font-weight: var(--fw-meta);
  color: var(--panel); background: rgba(20, 22, 27, .62);
  border-radius: 8px;
}
.tile.series:hover .scount { background: var(--green-select); }
.seriesback { margin-bottom: var(--sp-6); }

/* ---- signing in ----------------------------------------------------------- */

/* THE 2009 SHELL: the avatar stands BESIDE the form, full height, on the panel's
   own ground. Xbox.com never put a stock photograph next to a sign-in -- it put
   somebody's figure there, and this app can render one. */
.signin {
  display: grid; grid-template-columns: minmax(0, 210px) minmax(0, 1fr);
  gap: var(--sp-10); align-items: end;
  padding: var(--sp-9) var(--sp-8) var(--sp-8);
}
.sifig { display: flex; flex-direction: column; align-items: center; gap: var(--sp-5); min-width: 0; }
.sifig img { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: contain; }
.siline {
  font-size: var(--fs-second); color: var(--ink-dim); text-align: center;
  font-style: italic;
}
.siform { display: flex; flex-direction: column; gap: var(--sp-6); min-width: 0; }
.sih { margin: 0; font-size: var(--fs-section); font-weight: var(--fw-section); color: var(--ink); }
.sisub { margin: 0; font-size: var(--fs-second); line-height: var(--lh-second); color: var(--ink-dim); }
/* An error is a line that stays, above the fields it is about -- not a toast
   that has gone by the time you look up from the keyboard. */
.sierr {
  margin: 0; padding: var(--sp-5) var(--sp-6);
  font-size: var(--fs-second); color: #fff; background: var(--error);
  border-radius: var(--r-button);
}
.sierr[hidden] { display: none; }
.sifield { display: flex; flex-direction: column; gap: var(--sp-3); }
.sil { font-size: var(--fs-debug); font-weight: var(--fw-meta); color: var(--ink-dim);
       letter-spacing: 0; }
.sifield input {
  width: 100%; height: 32px; padding: 0 var(--sp-6);
  font: inherit; font-size: var(--fs-body); color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--border-ctrl); border-radius: var(--r-button);
}
.sifield input:focus { outline: none; border-color: var(--green-select);
                       box-shadow: 0 0 0 2px rgba(92, 149, 20, .22); }
.sibar { display: flex; align-items: center; gap: var(--sp-6); }
.sibar .go { min-width: 96px; }
.sinote {
  margin: 0; font-size: var(--fs-debug); line-height: 1.45; color: var(--ink-dim);
  padding: var(--sp-5) var(--sp-6);
  background: var(--panel-3); border-left: 3px solid var(--live-orange);
  border-radius: var(--r-button);
}
.siskip { align-self: flex-start; padding-left: 0; }

@media (max-width: 720px) {
  .signin { grid-template-columns: 1fr; }
  .sifig { display: none; }
}

.sonasfoot { display: flex; flex-direction: column; gap: var(--sp-3); align-items: flex-start;
             padding-top: var(--sp-5); }
.sonasfoot .note { margin: 0; }

/* The recovery code. Monospace and spaced out, because it is going to be copied
   onto paper by hand and the difference between "rn" and "m" matters there. */
.recovery {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--mono); font-size: var(--fs-body); letter-spacing: .04em;
  color: var(--ink); background: var(--panel-3);
  padding: var(--sp-5) var(--sp-6);
  border: 1px solid var(--rule-heavy); border-radius: var(--r-button);
  user-select: all; word-break: break-all;
}

/* SIGNED OUT, THE OUTFITS PANE SAYS WHY. The twenty cards stay below it -- what
   the feature looks like is part of the pitch -- but the sentence and the way
   in come first. */
.sonagate {
  margin: var(--sp-7) var(--sp-8) var(--sp-8);
  padding: var(--sp-8);
  background: var(--panel-3);
  border: 1px solid var(--border-ctrl);
  border-radius: var(--r-tile);
  display: flex; flex-direction: column; gap: var(--sp-7);
}
.sonagate p { margin: 0; font: var(--type-secondary); color: var(--ink-body); }

/* --- the account button, and its two choices ---------------------------
   It was "Sign out" whether or not you were signed in, which put the only
   thing in the bar carrying your name on the same press as the one that
   throws you out. Now it wears the gamerpic and the name, and signing out is
   a deliberate second choice inside a menu. */
.btn.acct { display: flex; align-items: center; gap: var(--sp-5);
            padding-left: var(--sp-4); }
.acctpic {
  width: 22px; height: 22px; flex: none; border-radius: 3px; overflow: hidden;
  display: grid; place-items: center;
  background: var(--panel-2); box-shadow: inset 0 0 0 1px var(--rule-heavy);
  font: var(--type-debug); font-weight: 700; color: var(--ink-dim);
}
.acctpic img { width: 100%; height: 100%; object-fit: cover; display: block;
               image-rendering: pixelated; }
.acctname { max-width: 12ch; overflow: hidden; text-overflow: ellipsis;
            white-space: nowrap; }

.acctmenu {
  position: fixed; z-index: 95; min-width: 168px;
  display: flex; flex-direction: column;
  padding: var(--sp-3);
  background: var(--panel);
  border: 1px solid var(--rule-heavy); border-radius: var(--radius);
  box-shadow: 0 6px 18px rgb(0 0 0 / .18);
}
.acctmenu button {
  text-align: left; cursor: pointer; padding: var(--sp-5) var(--sp-6);
  font: var(--type-body); color: var(--ink-body);
  background: none; border: 0; border-radius: var(--r-tile);
}
.acctmenu button:hover { background: var(--panel-hover); color: var(--ink); }
/* The one that destroys, marked as the one that destroys. */
.acctmenu button.danger:hover { color: var(--error); }
.acctmenu button:focus-visible { outline: 2px solid var(--green-select); outline-offset: -2px; }

/* --- the stage's prerendered poster --------------------------------------
   The default figure, rendered once, shown until the live one draws over it.
   `contain` and bottom-anchored because the live camera stands the figure on
   the ground plane: matching that here is what makes the handover a fade
   rather than a jump. Behind the overbar, above the canvas, and never
   interactive -- it is a picture of the thing, not the thing. */
#stage .surface .stagePoster {
  position: absolute; left: 0; right: 0; bottom: 0; top: 0;
  margin: auto;
  max-width: 100%; max-height: 100%;
  object-fit: contain;
  pointer-events: none;
  z-index: 1;
  opacity: 0;
  transition: opacity 200ms ease-out;
}
#stage .surface .stagePoster[src] { opacity: 1; }
#stage .surface .stagePoster.gone { opacity: 0; }
