/* ═══════════════════════════════════════════════════════════════════════
   SILL — one pane, one level, one lane.
   Every glass surface is a dark pane with a lit ledge across its foot; the
   product's two facts stand on the ledge as bright marks (the mode node
   under the lit word, the machine's state pip at the right end). The face
   is dark enough that white type clears AA over a white photograph — the
   shipped .55 tint measured 1.89:1 under the dim ramp, which is why the
   light moved from a top sheen to the floor. Adopted 4–0 (one revision) by
   the four-direction design vote of 2026-09-01; the arithmetic lives in
   the proposal's §2.5 and is asserted by tools/t_chrome.py against the
   pixels the browser actually paints.
   Amended 2026-09-03: controls floating over photographs are .knob — the
   pane without the ledge (the KNOB comment below carries the numbers).
   Type registers: serif italic names modes and states · sans speaks to
   you · mono counts · mono-caps is the machine's control alphabet (and
   never appears in member chrome).
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  --bg: #0b0b0d;
  --elev: #17171b;
  --elev-2: #202027;
  /* the pane: rgb split from alpha so per-piece fields share one hue */
  --pane: 12, 12, 16;
  --field: .84;                       /* never below .80 where text sits */
  --sill-h: 12px;                     /* the ledge */
  --sill-lit: rgba(255, 255, 255, .10);
  --sill-edge: rgba(255, 255, 255, .20);
  --node: rgba(255, 255, 255, .86);   /* the mode node + the pip's strokes */
  /* inks ON glass (measured over the pane composited on a white tile:
     11.55 / 7.38 / 5.14) and text OFF glass on the page ground */
  --ink: rgba(255, 255, 255, .95);
  --ink-2: rgba(255, 255, 255, .72);
  --ink-3: rgba(255, 255, 255, .56);
  --text: rgba(255, 255, 255, .93);
  --text-2: rgba(255, 255, 255, .64);
  --text-3: rgba(255, 255, 255, .58); /* was .40 — 3.78:1 fails AA on ground */
  --hair: rgba(255, 255, 255, .11);
  --hair-2: rgba(255, 255, 255, .20);
  --red: #ff2442;
  --blurple: #5865f2;                 /* Discord's, matching login.html */
  --blurple-hi: #4752c4;
  --red-soft: rgba(255, 36, 66, .14);
  --amber: #e2a33c;
  --shadow: 0 14px 44px rgba(0, 0, 0, .5), 0 2px 8px rgba(0, 0, 0, .4);
  --r-lg: 22px;
  --r-md: 14px;
  --r-tile: 10px;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --serif: ui-serif, Georgia, "Times New Roman", serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --sat: env(safe-area-inset-top, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --ease: cubic-bezier(.2, .7, .2, 1);
  --t-fast: 160ms;
  --t-med: 260ms;
  /* ── the declared altitude (SILL §3.1). Literals, not formulas: the deck
     is 8 + ctl + 12, re-declared per breakpoint so nobody
     has to evaluate anything. --wall-top lives on :root because #main
     and #arrivals are outside #bar. */
  --gutter: 10px;
  --pad-t: 8px;
  --ctl: 36px;
  --deck-top: calc(10px + var(--sat));
  --deck-h: 56px;
  --wall-gap: 12px;
  --wall-top: calc(var(--deck-top) + var(--deck-h) + var(--wall-gap));
  --lane-drop: 0px;
}
/* touch decks — every --deck-h a re-declared literal (8 + ctl + 12), so
   nobody evaluates formulas to know the altitude. New's second text lane
   went with the tab; the deck is one lane for every role now. */
@media (max-width: 900px) {
  :root {
    --gutter: 8px; --ctl: 40px; --wall-gap: 8px;
    --deck-top: calc(8px + var(--sat));
    --deck-h: 60px;
  }
}

/* ── base ─────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }
html { color-scheme: dark; -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.45 var(--sans);
  font-variant-numeric: tabular-nums;
  overflow-x: hidden;
  min-height: 100dvh;
}
[hidden] { display: none !important; }
img { max-width: none; user-select: none; -webkit-user-select: none; }
button, input, select { font: inherit; color: inherit; }
button { cursor: pointer; }
::selection { background: var(--red); color: #fff; }
.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; border-radius: 4px; }
.ico {
  width: 18px; height: 18px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
kbd {
  font: 11px/1 var(--mono); color: var(--text-2);
  border: 0; box-shadow: inset 0 0 0 1px var(--hair-2);
  border-radius: 5px; padding: 3px 5px; background: none;
}
.mono { font-family: var(--mono); }
.dim { color: var(--text-3); }

@media (pointer: fine) {
  ::-webkit-scrollbar { width: 10px; height: 10px; }
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); border-radius: 5px; border: 2px solid var(--bg); }
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.24); }
  ::-webkit-scrollbar-track { background: transparent; }
}

/* ── backdrop wash ────────────────────────────────────────────────────── */

#bg {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(1100px 520px at 50% -180px, rgba(120, 126, 168, .11), transparent 70%),
    radial-gradient(900px 500px at 100% 110%, rgba(255, 36, 66, .045), transparent 70%),
    var(--bg);
}

/* ── SILL: the pane, the ledge, the etched controls ───────────────────── */

/* CONTAINER — everything that holds other things. isolation: the ledge is
   painted on a full-size pseudo, and without a local stacking context it
   would veil the marks that stand on it (vote-C flag: the pip and the
   mode node carry the product's two facts; nothing may wash them). */
.glass {
  position: relative; isolation: isolate;
  border-radius: var(--r-lg);
  background: rgba(var(--pane), var(--field));
  border: 1px solid var(--hair);
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, .05);
}
@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
  .glass {
    -webkit-backdrop-filter: blur(18px) saturate(1.8);
    backdrop-filter: blur(18px) saturate(1.8);
  }
}
/* the ledge: painted on a full-size pseudo inheriting the pane's
   silhouette, so a 22px card, a 999px pill and a 16px menu all clip it
   correctly (a left/right/bottom band oversteps a 22px corner by 2.4px) */
:root {
  /* The foot catches light; it does not become a second pane. The shipped
     gradient ended in a hard `--sill-edge` line at `--sill-h`, which
     measured a 45-level luminance step 14px up the face of EVERY glass
     surface — a sharp horizontal boundary with a slab below it 75%
     brighter than the face. Read as a second shade of glass, reported
     twice, and correctly: a step that size is a seam, not a highlight.
     One smooth fade to nothing, no stop, no edge. The pip and the mode
     node still sit at the foot; neither ever needed a lit slab behind it
     to be legible on a dark pane. */
  /* Nothing. The ledge is off.
     It went hard-edged slab -> smooth fade -> gone, because the owner
     reported it three times and the third report was "less bright, but
     it's still there", about the deck. They were right each time: a 12px
     lit zone on a 56px header is a fifth of its height, and a fifth of a
     surface being brighter is a band whether or not it has a seam.
     Kept as a transparent gradient rather than deleted, because the
     crawl-state rules compose it — `linear-gradient(spill), var(--sill-paint)`
     — and `none` in that list voids the whole background-image, taking
     the spill with it. The pip and the mode node never needed a lit floor
     to be legible on a dark pane; they are their own marks. */
  --sill-paint: linear-gradient(to top,
      rgba(255, 255, 255, 0) 0,
      rgba(255, 255, 255, 0) var(--sill-h));
}
.glass::after {
  content: ""; position: absolute; inset: 1px; border-radius: inherit;
  pointer-events: none; z-index: 0;
  background: var(--sill-paint);
}
.glass.sm { --sill-h: 8px; }   /* toasts, menus, suggestions */

/* CONTROL — etched into the glass it sits on: no fill, no sill, one
   hairline. Containers are .glass, controls are .etch; nothing is both. */
.etch { background: none; border: 0; box-shadow: inset 0 0 0 1px var(--hair); }
.etch:hover { box-shadow: inset 0 0 0 1px var(--hair-2); }

/* KNOB — the third treatment: a control FLOATING over photographs (the
   viewer's arrows, close and counter; the arrivals pill), where .etch has
   no pane to be cut into. Same pane so ink clears AA over a white image
   (.92, the arrivals pill's shipped field — 15.1:1 for --ink over white),
   but NO ledge: the ledge is where the product's two facts stand, no fact
   ever stands on a thumb-sized control, and measured over an L=200 ground
   the 13px foot was 30-47% of these elements — a second shade of glass,
   not a sill. Panes are .glass, controls in a pane are .etch, floating
   controls are .knob; nothing is two of them. (.btn.knob restates the
   fill because .btn's own `background: none` is declared later.) */
.knob, .btn.knob {
  background: rgba(var(--pane), .92);
  border: 1px solid var(--hair);
  box-shadow: var(--shadow);
}
.btn.knob:hover {
  background: linear-gradient(rgba(255, 255, 255, .08) 0 0),
              rgba(var(--pane), .92);
  border-color: var(--hair-2);
  box-shadow: var(--shadow);
}
@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
  .knob {
    -webkit-backdrop-filter: blur(18px) saturate(1.8);
    backdrop-filter: blur(18px) saturate(1.8);
  }
}

/* ── the deck ─────────────────────────────────────────────────────────── */

#bar {
  position: fixed; z-index: 40;
  top: var(--deck-top);
  left: 50%; transform: translateX(-50%);
  width: min(1280px, calc(100% - 2 * var(--gutter)));
  height: var(--deck-h);
  padding: var(--pad-t) 12px 0;
  transition: transform var(--t-med) var(--ease);
}
#bar.tucked { transform: translate(-50%, calc(-100% - var(--deck-top) - 8px)); }

/* lane 1: one line, never wraps, never clips — pressure is absorbed by
   the chip row, then search, then the eviction ladder in the media
   queries below (the anti-jitter rule) */
.bar-row {
  display: flex; align-items: center; gap: 8px;
  height: var(--ctl); flex-wrap: nowrap; min-width: 0;
}

/* The wordmark IS the navigation. One size in every mode so the lane
   never re-measures; the lit word stands on a 2px node ON the ledge.
   applyModeChrome() is the ONLY writer of pressed state (boot and
   setMode both go through it) — the markup asserts no mode, so the
   pre-JS frame draws no node and is complete, not half-dressed. */
.mark { margin: 0; display: flex; align-items: center; gap: 8px; white-space: nowrap; flex: none; }
.mark-word {
  background: none; border: 0; padding: 0; cursor: pointer;
  position: relative; height: var(--ctl);
  display: inline-flex; align-items: center;
  font: italic 600 19px/1 var(--serif);
  letter-spacing: .012em; color: var(--ink-2);
  transition: color var(--t-fast);
}
.mark-word:hover { color: var(--ink); }
.mark-word[aria-pressed="true"] { color: var(--ink); cursor: default; }
.mark-word[aria-pressed="true"]::after,
.mark-word[aria-current="page"]::after {
  content: ""; position: absolute; left: 0; right: 0; height: 2px;
  border-radius: 1px; z-index: 1;
  top: calc(100% + var(--lane-drop));
  background: var(--node);
}
.mark-sep { font: italic 600 15px/1 var(--serif); color: var(--ink-3); }

/* the labelled count at the lane's end — mono, tabular, names what it
   counts ("75 saved", never a bare 75) */
#count { align-self: center; }

/* The operator's two masthead doors and their whole eviction ladder — the
   ≤900 rule that moved ADMIN into the Refine sheet and the ≤480 rule that
   moved CRAWL after it — were deleted on 2026-09-10 with the pages they
   opened. Nothing is appended to the mark when the role arrives any more,
   so the operator's lane and the member's are the same lane at every
   width. (t_chrome asserts those selectors are absent from this file, so
   they are deliberately not spelled here.) */

/* ── identity, at the lane's end ──────────────────────────────────────
   Two mutually exclusive states in one slot, so the lane's width does not
   jump when /api/me answers. paintMe() is the only writer of either
   [hidden]; the markup asserts neither. */
.me-slot {
  display: flex; align-items: center; justify-content: flex-end;
  flex: none; margin-left: 2px;
  /* RESERVED, and that is the whole point. Both states ship hidden until
     /api/me answers, and `.search` is the lane's only flexible item
     (flex: 1 1 200px) -- so revealing an unreserved control would shrink
     the search by its width and shift the chips, Refine, the tools and
     both doors left, mid-boot, on every load. This page's CLS is 0.0004;
     that would have spent most of the budget in the header alone.
     Holding the box from the first frame means the reveal paints INTO
     it and moves nothing.

     `width`, not `min-width`: with min-width the slot can still GROW past
     the reservation when a long display name arrives, and measured that
     still cost a shift (CLS 0.0000 -> 0.0004 on #btn-select). A fixed box
     cannot move its neighbours whatever lands in it; the name ellipsises
     instead, which is the right trade in a lane that never wraps.

     The box is 200px rather than 132px because the complaint that reached
     the owner was the ellipsis, not the shift: at 132 an ordinary Discord
     display name was cut off, and Refine and Select sat where a name half
     that length would have put them. Widening the RESERVATION fixes what
     was actually wrong -- names fit -- without giving the slot back the
     ability to move its neighbours, which is the thing the measurement
     above bought. Names longer than 200px still ellipsise; that is the
     same trade, at a size real names actually clear. */
  width: 200px;
}
.me-slot > * { max-width: 100%; }

/* Tucked away while the banner's own Discord button is on screen: two
   identical calls to action, side by side, is one too many. The slot
   collapses rather than merely emptying, or the reservation it exists for
   would read as the dead space this lane just had removed. The width is
   animated, so the return is a slide rather than a jump -- and it is
   scroll-driven, i.e. user-initiated, which is why it does not land in
   CLS the way the un-reserved reveal it protects against would. */
.me-slot { transition: width var(--t-fast), opacity var(--t-fast), visibility var(--t-fast); }
/* visibility, so a tucked door leaves the tab order (opacity 0 alone kept
   it focusable, invisibly); it transitions late on the way out and at
   once on the way back, which is what a fade wants */
.me-slot.tucked {
  width: 0 !important; min-width: 0; opacity: 0; visibility: hidden;
  pointer-events: none; overflow: hidden;
}
@media (prefers-reduced-motion: reduce) { .me-slot { transition: none; } }

/* signed out — Discord's own colour, because this button leaves for
   Discord and a member should recognise where it goes before reading it */
.me-in {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: var(--ctl); padding: 0 13px; border-radius: 999px;
  background: var(--blurple); color: #fff; text-decoration: none;
  font-size: 13.5px; font-weight: 600; white-space: nowrap; flex: none;
  transition: background var(--t-fast), transform var(--t-fast);
}
.me-in:hover { background: var(--blurple-hi); }
.me-in:active { transform: scale(.965); }
.d-logo { width: 19px; height: 15px; flex: none; }

/* signed in — .etch's treatment, worn by a control that is a peer of the
   doors rather than of the pill buttons */
.me-btn {
  display: inline-flex; align-items: center; gap: 8px;
  height: var(--ctl); padding: 0 10px 0 4px; border-radius: 999px;
  border: 0; background: none; box-shadow: inset 0 0 0 1px var(--hair);
  color: var(--ink); cursor: pointer; max-width: 190px;
  transition: box-shadow var(--t-fast), background var(--t-fast), transform var(--t-fast);
}
.me-btn:hover { box-shadow: inset 0 0 0 1px var(--hair-2); background: rgba(255, 255, 255, .05); }
.me-btn:active { transform: scale(.965); }
.me-btn[aria-expanded="true"] { box-shadow: inset 0 0 0 1px var(--hair-2); background: rgba(255, 255, 255, .05); }

/* the monogram stands in for an avatar: Discord's CDN hash is not stored
   (bot_users keeps id + display_name and nothing else), and a letter the
   page can draw beats a third-party image request it cannot */
.me-mono {
  width: 26px; height: 26px; flex: none; border-radius: 50%;
  display: grid; place-items: center; position: relative; overflow: hidden;
  background: var(--blurple); color: #fff;
  font: 600 11.5px/1 var(--sans); letter-spacing: .02em;
  text-transform: uppercase;
}
/* The real face covers the monogram rather than replacing it, so the
   letter is what shows while the CDN request is in flight and what stays
   if it never lands. Discord is a third party: `onerror` un-hides nothing,
   it re-hides this, and the fallback is already painted underneath. */
.me-face {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border-radius: 50%; object-fit: cover; display: block;
}

.me-name {
  font-size: 13px; font-weight: 500; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.me-btn:hover .me-name { color: var(--ink); }

#me-menu {
  position: fixed; z-index: 60; min-width: 216px; max-width: 280px;
  padding: 6px; border-radius: var(--r-md);
}
.me-head { padding: 8px 12px 6px; }
.me-full {
  font-size: 13.5px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.me-id { font-size: 11px; color: var(--ink-3); margin-top: 2px; }
.me-meta {
  padding: 0 12px 8px; margin-bottom: 4px;
  border-bottom: 1px solid var(--hair);
  font-size: 11.5px; color: var(--ink-3);
}
.me-out { color: var(--ink); }

#loadbar {
  position: absolute; left: 16px; right: 16px;
  top: calc(var(--pad-t) + var(--ctl) - 2px);
  height: 2px; z-index: 1;
  border-radius: 2px; background: rgba(255, 255, 255, .07); overflow: hidden;
}
#loadbar-fill {
  height: 100%; width: 0%; border-radius: 2px;
  background: linear-gradient(90deg, var(--red), #ff7a8d);
  transition: width .3s var(--ease);
}
/* a wait with no denominator (one page, not a drain): a sweep, not a fill */
#loadbar.indet #loadbar-fill {
  width: 30%; transition: none;
  animation: loadsweep 1.1s ease-in-out infinite;
}
@keyframes loadsweep { from { transform: translateX(-100%); } to { transform: translateX(400%); } }

/* ── search (etched) + facet suggestions ──────────────────────────────── */

.search {
  display: flex; align-items: center; gap: 8px;
  /* No cap. 492px was the right number when the lane was full of member
     controls and the search was competing for room; with the trailing
     cluster right-anchored it became the reason 364px of nothing sat
     between the search and Refine for an anonymous visitor at 1440 (264
     at 1200). The slack belongs to the lane's one flexible item, and a
     search-first archive is allowed a wide search. It still shrinks first
     when a member's tools arrive, because that is what `flex: 1 1` does. */
  flex: 1 1 200px; min-width: 120px;
  height: var(--ctl); padding: 0 10px 0 12px;
  border: 0; border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--hair);
  background: rgba(0, 0, 0, .18);
  transition: box-shadow var(--t-fast), background var(--t-fast);
}
.search:focus-within { box-shadow: inset 0 0 0 1px var(--hair-2); background: rgba(0, 0, 0, .3); }
/* The input's own outline is off (it sits inside the pill), so without
   this the lane's search was the one control a keyboard reader could
   land on and not see: a hairline going from .2 to .2 alpha (measured
   identical) was its whole focus ring. Same ring the crawl pane's input
   wears; on the pill, not the input, so it follows the shape. */
.search:has(> input:focus-visible) { outline: 2px solid var(--red); outline-offset: 2px; }
.search .ico { width: 16px; height: 16px; color: var(--ink-3); }
.search input {
  flex: 1; min-width: 0; background: none; border: 0; outline: none;
  color: var(--ink); font-size: 14px;
}
.search input::placeholder { color: var(--ink-3); }
.search input::-webkit-search-cancel-button { -webkit-appearance: none; }
.search kbd { flex: none; }
.search:focus-within kbd { display: none; }

#search-wrap { position: relative; }
.sug {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: 46;
  max-height: min(340px, 60vh); overflow-y: auto; overscroll-behavior: contain;
  padding: 6px 6px calc(6px + var(--sill-h)); border-radius: 16px;
}
.sug-h {
  padding: 6px 10px 3px;
  font: 600 11px/1.2 var(--mono); text-transform: uppercase;
  letter-spacing: .09em; color: var(--ink-3);
}
.sug-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 7px 10px; border: 0; border-radius: 9px;
  background: none; color: var(--ink); font-size: 13px; text-align: left;
}
.sug-row:hover, .sug-row.on { background: rgba(255, 255, 255, .08); }
.sug-row > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sug-n { flex: none; font: 11px/1 var(--mono); color: var(--ink-3); }

/* ── buttons: etched controls on the pane ─────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: var(--ctl); padding: 0 14px; border-radius: 999px;
  border: 0; background: none; box-shadow: inset 0 0 0 1px var(--hair);
  color: var(--ink); font-size: 13.5px; font-weight: 500;
  transition: box-shadow var(--t-fast), background var(--t-fast), transform var(--t-fast), color var(--t-fast);
  -webkit-tap-highlight-color: transparent;
  text-decoration: none; white-space: nowrap; flex: none;
}
.btn:hover { box-shadow: inset 0 0 0 1px var(--hair-2); background: rgba(255, 255, 255, .05); }
.btn:active { transform: scale(.965); }
.btn[aria-pressed="true"] { background: var(--ink); box-shadow: none; color: #101013; }
.btn[aria-pressed="true"] .ico { stroke: #101013; }
.btn-icon { width: var(--ctl); padding: 0; flex: none; }
.btn-primary { background: var(--ink); color: #101013; box-shadow: none; font-weight: 600; }
.btn-primary .ico { stroke: #101013; }
.btn-primary:hover { background: #fff; }
.btn-danger { color: #ff8296; }
.btn-danger .ico { stroke: currentColor; }
.btn-danger:hover { background: var(--red-soft); box-shadow: inset 0 0 0 1px rgba(255, 36, 66, .35); }
.btn-danger.arm { background: var(--red); box-shadow: none; color: #fff; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .38; pointer-events: auto; cursor: not-allowed; }
.btn.busy { pointer-events: none; opacity: .7; }
#btn-all.busy { pointer-events: auto; opacity: 1; box-shadow: inset 0 0 0 1px var(--hair-2); }
#btn-all.busy span { font-family: var(--mono); font-size: 12px; color: var(--ink-2); min-width: 58px; text-align: right; }
.btn.busy .ico { display: none; }
.btn.busy::before {
  content: ""; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(1turn); } }

/* the Refine toggle: the one control that is always in the lane */
/* `margin-left: auto` is what puts the identity control against the right
   edge, and it is a lane-level fix rather than a `.me-slot` one. `.search`
   is the only growing item (`flex: 1 1 200px`) and it stops at 492px, so
   past that width nothing absorbed the remainder: the whole trailing
   cluster -- Refine, the tools, the two doors and the identity -- sat
   wherever the search happened to end, with dead space to its right. The
   auto margin absorbs it here instead, which splits the lane into the two
   groups it always meant to be: wordmark and search on the left, controls
   and identity on the right. */
#bar-toggle { display: inline-flex; position: relative; width: auto; padding: 0 12px; gap: 7px;
              margin-left: auto; }

#bar-toggle .rf-label { font-size: 13px; font-weight: 500; }
#bar-toggle.dot::after {
  content: ""; position: absolute; top: 5px; right: 5px;
  width: 6px; height: 6px; border-radius: 50%; background: var(--red);
}

/* segmented controls (in the sheet) */
.seg {
  display: flex; gap: 2px; margin: 0; padding: 3px;
  border: 0; border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--hair);
  background: rgba(0, 0, 0, .18);
}
.seg legend { display: none; }
.seg label { position: relative; display: block; border-radius: 999px; }
.seg input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.seg span {
  display: block; padding: 7px 12px; border-radius: 999px;
  font-size: 12.5px; font-weight: 500; color: var(--ink-2);
  transition: background var(--t-fast), color var(--t-fast);
}
.seg input:checked + span { background: var(--ink); color: #101013; }
.seg input:focus-visible + span { outline: 2px solid var(--red); outline-offset: 1px; }
.seg label:hover input:not(:checked) + span { color: var(--ink); background: rgba(255,255,255,.07); }

/* sort (in the sheet) */
.sortbox { position: relative; display: block; }
.sortbox select {
  appearance: none; -webkit-appearance: none;
  height: var(--ctl); padding: 0 30px 0 14px; border-radius: 999px;
  border: 0; box-shadow: inset 0 0 0 1px var(--hair);
  background: none; color: var(--ink);
  font-size: 13.5px; font-weight: 500; cursor: pointer;
}
.sortbox::after {
  content: ""; position: absolute; right: 13px; top: 50%;
  width: 7px; height: 7px; pointer-events: none;
  border-right: 1.6px solid var(--ink-2); border-bottom: 1.6px solid var(--ink-2);
  transform: translateY(-70%) rotate(45deg);
}
.sortbox select:hover { box-shadow: inset 0 0 0 1px var(--hair-2); }
.sortbox select option { background: var(--elev); color: var(--text); }

/* active filter chips: state, visible in the lane at every width the
   lane exists ("the door hides controls, never state") */
.chips {
  display: flex; gap: 6px; flex-wrap: nowrap; overflow-x: auto;
  scrollbar-width: none; min-width: 0; max-width: 32%;
}
.chips::-webkit-scrollbar { display: none; }
.chips:empty { display: none; }
.chip {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  padding: 6px 12px; border-radius: 999px;
  border: 0; box-shadow: inset 0 0 0 1px var(--hair);
  background: none; color: var(--ink-2); font-size: 12.5px; font-weight: 500;
  max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast), box-shadow var(--t-fast);
}
.chip:hover { color: var(--ink); background: rgba(255, 255, 255, .07); }
.chip[aria-pressed="true"] { background: var(--ink); color: #101013; box-shadow: none; }
.chip[aria-pressed="true"] .ico { stroke: #101013; }
.chip .ico { width: 14px; height: 14px; }
.chip-x { box-shadow: inset 0 0 0 1px rgba(255, 36, 66, .4); color: var(--ink); background: var(--red-soft); }
.chip-x:hover { background: rgba(255, 36, 66, .24); }
.chip-x .x { font-size: 14px; line-height: 1; color: #ff8296; }

/* ── the Refine sheet: hangs from the deck, overlays, never pushes ────── */

#tray {
  position: absolute; top: calc(100% + 10px); left: 0; right: 0; z-index: 2;
  border-radius: var(--r-lg);
  background: rgba(var(--pane), .92);
  border: 1px solid var(--hair);
  box-shadow: var(--shadow);
  padding: 14px 14px calc(14px + var(--sill-h));
  max-height: 62dvh; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
}
@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
  #tray { -webkit-backdrop-filter: blur(18px) saturate(1.8); backdrop-filter: blur(18px) saturate(1.8); }
}
#tray::after {   /* the sheet is a pane; it wears the ledge too */
  content: ""; position: absolute; inset: 1px; border-radius: inherit;
  pointer-events: none; background: var(--sill-paint);
}
#bar:not(.xp) #tray { display: none; }

/* the tools wrapper: lane members ≥1081 (display:contents), a sheet row
   below that. One DOM, no duplicate controls, no JS reparenting. */
.tools { display: contents; }

/* ── the arrivals pill: a knob under the deck (fill, hairline, shadow and
   blur all come from .knob; only its geometry is its own). Centred, so it
   never stands on a tile's rating cluster (which sits top-right). ────── */

#arrivals {
  position: fixed; z-index: 39;
  top: calc(var(--deck-top) + var(--deck-h) + 12px);
  left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px;
  height: 34px; padding: 0 16px; border-radius: 999px;
  color: var(--ink); font-size: 12.5px; font-weight: 500; cursor: pointer;
  animation: rise var(--t-med) var(--ease);
  transition: transform var(--t-med) var(--ease);
}
#arrivals:hover { border-color: var(--hair-2); }
#bar.tucked ~ #arrivals { transform: translate(-50%, calc(-1 * var(--wall-top))); }
.pill-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--red); box-shadow: 0 0 8px rgba(255, 36, 66, .8); }

/* menu items (the account menu) */
.lqm {
  display: block; width: 100%; text-align: left; border: 0; background: none;
  padding: 8px 12px; border-radius: 9px; color: var(--ink); font-size: 13px;
}
.lqm:hover { background: rgba(255, 255, 255, .08); }

/* ── the wall ─────────────────────────────────────────────────────────── */

#main { padding-top: var(--wall-top); }   /* declared, not measured */
#wall { position: relative; margin: 0 auto; }

.cell { position: absolute; top: 0; left: 0; }
.tile {
  position: absolute; inset: 0; display: block;
  border-radius: var(--r-tile); overflow: hidden;
  background: var(--tint, var(--elev));
  outline-offset: 3px;
  -webkit-tap-highlight-color: transparent;
  transform: translateZ(0);
}
.cell, .tile { will-change: auto; }
.tile img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0; transition: opacity .35s ease, transform .45s var(--ease);
}
.cell.ld .tile img { opacity: 1; }
.cell.shimmer .tile {
  background: linear-gradient(100deg, var(--elev) 38%, var(--elev-2) 50%, var(--elev) 62%);
  background-size: 220% 100%;
  animation: shimmer 1.3s linear infinite;
}
@keyframes shimmer { from { background-position: 130% 0; } to { background-position: -90% 0; } }

@media (hover: hover) and (pointer: fine) {
  .tile:hover img { transform: scale(1.035); }
  .tile:hover .tile-scrim { opacity: 1; }
}
.tile-scrim {
  position: absolute; inset: auto 0 0 0; height: 46%;
  display: flex; align-items: flex-end; padding: 10px 12px;
  background: linear-gradient(180deg, transparent, rgba(0, 0, 0, .62));
  opacity: 0; transition: opacity var(--t-med);
  pointer-events: none;
}
/* No hover on touch, so the credit is always up -- shallower and lighter
   than the hover scrim, so the wall stays photographs first. */
@media (hover: none) {
  .tile-scrim {
    opacity: 1; height: 38%;
    background: linear-gradient(180deg, transparent, rgba(0, 0, 0, .56));
  }
}
.tile-acct {
  /* 12px, not 11.5: it is 95.7% of the page's text and Lighthouse's
     legibility floor is 12px. Half a pixel on an account id nobody could
     see the difference in; Best Practices 96 -> 100 on mobile. */
  font-size: 12px; color: rgba(255, 255, 255, .92); letter-spacing: .01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .6);
}

/* selection */
.tile-check {
  position: absolute; top: 8px; left: 8px; width: 24px; height: 24px;
  border-radius: 50%; display: none; align-items: center; justify-content: center;
  border: 1.6px solid rgba(255, 255, 255, .85);
  background: rgba(0, 0, 0, .35);
  box-shadow: 0 1px 5px rgba(0, 0, 0, .4);
  transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}
.tile-check .ico { width: 14px; height: 14px; stroke: #fff; stroke-width: 2.4; opacity: 0; }
#wall.selmode .tile-check { display: flex; }
#wall.selmode .tile { cursor: copy; }
.cell.on .tile-check { background: var(--red); border-color: var(--red); transform: scale(1.08); }
.cell.on .tile-check .ico { opacity: 1; }
.cell.on .tile { box-shadow: inset 0 0 0 2px var(--red), inset 0 0 0 4px rgba(255,255,255,.25); background: var(--elev-2); }
.cell.on .tile img { transform: scale(.94); }

/* rating cluster + state marks (browse mode; saved wall stays untouched).
   Buttons are pointer affordances (tabindex -1): the keyboard path is the
   viewer's ↑/↓/s, so the wall's roving tabindex stays one stop per tile. */
.tile-rate { position: absolute; top: 6px; right: 6px; display: flex; gap: 5px; z-index: 2; }
#wall:not(.browse) .tile-rate, #wall.selmode .tile-rate { display: none; }
.tr {
  width: 27px; height: 27px; border-radius: 50%; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(255, 255, 255, .28);
  background: rgba(10, 10, 14, .55); color: #fff;
  opacity: 0; transform: translateY(-2px);
  transition: opacity var(--t-fast), transform var(--t-fast),
              background var(--t-fast), border-color var(--t-fast);
  -webkit-tap-highlight-color: transparent;
}
.tr .ico { width: 14px; height: 14px; stroke-width: 2; }
.cell:hover .tr, .cell:focus-within .tr { opacity: 1; transform: none; }
.tr:hover { background: rgba(32, 32, 40, .9); border-color: rgba(255, 255, 255, .5); }
.tr:active { transform: scale(.9); }
/* state shows without hover — the wall stays scannable */
.cell.sv .tr-sv, .cell.lk .tr-lk, .cell.dk .tr-nope { opacity: 1; transform: none; }
.cell.sv .tr-sv { background: var(--red); border-color: var(--red); }
/* an arrival: it came in on the tail while this page was open (app.js
   admitArrival). The same dot the pill wears, in the tile's top-left, for
   the life of the page — so "N new" is N of these once Newest is shown */
.cell.nw .tile::after {
  content: ""; position: absolute; top: 8px; left: 8px; z-index: 2;
  width: 7px; height: 7px; border-radius: 50%; background: var(--red);
  box-shadow: 0 0 0 2px rgba(10, 10, 14, .55), 0 0 8px rgba(255, 36, 66, .8);
  pointer-events: none;
}
/* a member's own custom-crawl find, shown to operators only (server-side:
   `_item_json` sends `lane` behind the same gate as `source_query`). The
   arrival dot owns ::after and the top-left; this is a hairline bar down the
   tile's leading edge instead — a different shape in a different place, so
   the two read apart at a glance and stack without collision on a row that
   is both. Muted on purpose: it marks provenance, it is not a warning. */
.cell.cst .tile::before {
  content: ""; position: absolute; inset: 8px auto 8px 0; z-index: 2;
  width: 2px; border-radius: 0 2px 2px 0;
  background: linear-gradient(180deg,
              rgba(255, 255, 255, .06), rgba(120, 190, 255, .55));
  pointer-events: none;
}
@media (prefers-reduced-transparency: reduce) {
  .cell.cst .tile::before { background: rgba(120, 190, 255, .5); }
}

.cell.sv .tr-sv .ico { fill: #fff; }
.cell.lk .tr-lk { background: #fff; border-color: #fff; }
.cell.lk .tr-lk .ico { stroke: #101013; }
.cell.dk .tr-nope { background: rgba(16, 16, 20, .9); border-color: var(--hair-2); color: var(--text-2); }
/* a disliked tile recedes instead of vanishing: the layout holds still and
   the verdict stays one click from undone (except in the Disliked view) */
#wall.browse:not(.st-dk) .cell.dk .tile img { opacity: .38; filter: saturate(.35); }
#wall.browse:not(.st-dk) .cell.dk .tile { background: #101013; }
@media (hover: none) {
  .tr { display: none; }          /* touch rates in the viewer; marks stay */
  .cell.sv .tr-sv, .cell.lk .tr-lk, .cell.dk .tr-nope { display: flex; pointer-events: none; }
}

/* ── the operator's delete, on the tile ───────────────────────────────
   Hidden for everyone until `loadAll` puts `.op` on #wall, alongside the
   other operator chrome. One writer, one class, and a member's DOM never
   contains a control they cannot use. */
#wall:not(.op) .tr-del { display: none; }
.tr-del:hover { background: var(--red); border-color: var(--red); }
/* Armed is a SECOND click, not a modal. The bulk delete has a preview and a
   token because its count is a number nobody can see; this one is a
   photograph the operator is pointing at, so the guard only has to stop the
   mis-click — and a dialog restating "delete this image?" over the image
   itself would train them to dismiss dialogs. It disarms on a timer, so a
   half-pressed button never waits around to be finished by accident. */
.tr-del.arm {
  opacity: 1; transform: none;
  background: var(--red); border-color: var(--red); color: #fff;
  animation: del-arm 1.1s var(--ease) infinite;
}
@keyframes del-arm {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 36, 66, .55); }
  70%      { box-shadow: 0 0 0 6px rgba(255, 36, 66, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .tr-del.arm { animation: none; box-shadow: 0 0 0 2px rgba(255, 36, 66, .5); }
}

/* broken originals */
.tile-off {
  position: absolute; inset: 0; display: none;
  align-items: center; justify-content: center; color: var(--text-3);
  background:
    repeating-linear-gradient(45deg, transparent 0 14px, rgba(255,255,255,.02) 14px 15px),
    var(--elev);
}
.tile-off .ico { width: 26px; height: 26px; }
.cell.broken .tile img { display: none; }
.cell.broken .tile-off { display: flex; }

/* month labels between rows */
.wall-label {
  position: absolute; left: 0;
  font: italic 500 15px/1 var(--serif);
  color: var(--text-3); letter-spacing: .03em;
  display: flex; align-items: flex-end; padding-bottom: 6px;
  pointer-events: none; white-space: nowrap;
}
.wall-label::after {
  content: ""; flex: 1; margin: 0 0 5px 12px; height: 1px;
  background: linear-gradient(90deg, var(--hair), transparent);
  min-width: 40px;
}

/* relayout animation (after a removal) */
#wall.moving .cell { transition: transform var(--t-med) var(--ease); }
.cell.bye { transition: opacity var(--t-fast), transform var(--t-fast); opacity: 0; transform: scale(.9); pointer-events: none; }

/* skeleton before first data; the same blocks stand in for the next page
   at the foot of the wall while it is in flight */
#skeleton, #wall-more { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 6px; overflow: hidden; max-height: 80vh; }
/* the 429 wait, under the skeleton: a sentence, not a modal */
.wait { margin: 14px var(--gutter) 0; text-align: center; font-size: 13.5px; color: var(--text-2); }
#wall-more { max-height: 240px; padding-top: 6px; }
#skeleton i, #wall-more i {
  flex: 1 1 auto; height: 235px; border-radius: var(--r-tile);
  background: linear-gradient(100deg, var(--elev) 38%, var(--elev-2) 50%, var(--elev) 62%);
  background-size: 220% 100%;
  animation: shimmer 1.3s linear infinite;
}

/* endcap + messages */
#endcap { padding: 56px 20px calc(60px + var(--sab)); text-align: center; color: var(--text-3); font-size: 12.5px; letter-spacing: .04em; }
#endcap-text::before, #endcap-text::after { content: "·  ·  ·"; display: block; margin: 8px 0; opacity: .6; letter-spacing: .4em; }
/* The data-subject link, quiet by default: it is a right, not a call to
   action, and the endcap is the last thing a reader sees rather than
   something they came for. Inherits the endcap's size and tracking so it
   reads as part of the same line. */
#endcap-privacy { color: var(--text-3); text-decoration: none; border-bottom: 1px solid currentColor; padding-bottom: 1px; opacity: .75; }
#endcap-privacy:hover, #endcap-privacy:focus-visible { opacity: 1; color: var(--text-2); }

.stage-msg {
  max-width: 430px; margin: 16vh auto 0; padding: 36px 30px;
  text-align: center; color: var(--text-2);
}
.stage-msg h2 { font: italic 600 26px/1.2 var(--serif); color: var(--text); margin: 14px 0 10px; }
.stage-msg p { margin: 0 0 18px; }
.big-ico {
  width: 44px; height: 44px; color: var(--text-3);
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
}
.rx {
  display: inline-flex; padding: 2px 9px; border-radius: 8px;
  background: rgba(88, 101, 242, .18); border: 1px solid rgba(88, 101, 242, .35);
  font-size: 13px;
}

/* ── selection action bar ─────────────────────────────────────────────── */

#selbar {
  position: fixed; z-index: 45; bottom: calc(14px + var(--sab));
  left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px 8px 16px; max-width: calc(100% - 20px);
  animation: rise var(--t-med) var(--ease);
}
@keyframes rise { from { transform: translate(-50%, 12px); opacity: 0; } }
/* `rise` enters from below, which reads as wrong for a toast anchored to the
   top: it would slide up toward the edge it is already sitting against. */
@keyframes fall { from { transform: translate(-50%, -12px); opacity: 0; } }
#sel-count { font-size: 13px; color: var(--text-2); white-space: nowrap; }

/* ── viewer ───────────────────────────────────────────────────────────── */

#viewer, #editor, #keys, #gate { position: fixed; inset: 0; z-index: 50; }
.backdrop {
  position: absolute; inset: 0;
  background: rgba(5, 5, 8, .88);
}
@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
  .backdrop {
    background: rgba(5, 5, 8, .72);
    -webkit-backdrop-filter: blur(18px) saturate(1.4);
    backdrop-filter: blur(18px) saturate(1.4);
  }
}
/* The stage covers the backdrop, so it is the stage that is actually
   clicked and the stage that must show the affordance. Children (the
   frame, the image) set their own cursor and override this. */
#viewer .backdrop, #viewer .v-stage { cursor: zoom-out; }

.v-stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.v-frame {
  position: relative; flex: none;
  border-radius: 4px; overflow: hidden;
  box-shadow: 0 30px 90px rgba(0, 0, 0, .6);
  background: var(--elev);
  touch-action: none;
}
.v-frame img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
#v-full { opacity: 0; transition: opacity .28s ease; }
#v-full.on { opacity: 1; }
.v-frame.zoomable { cursor: zoom-in; }
.v-frame.zoomed { cursor: grab; border-radius: 0; }
.v-frame.zoomed:active { cursor: grabbing; }
#viewer.anim .v-frame { transition: transform var(--t-med) var(--ease), opacity var(--t-med); }
#viewer.slide .v-frame { animation: slide-in var(--t-med) var(--ease); }
@keyframes slide-in { from { transform: translateX(var(--slide-from, 26px)); opacity: .2; } }

.v-top {
  position: absolute; z-index: 3; top: calc(12px + var(--sat)); left: 12px; right: 12px;
  display: flex; align-items: center; justify-content: space-between;
  pointer-events: none;
}
.v-top > * { pointer-events: auto; }
.pill {
  padding: 8px 14px; border-radius: 999px; font-size: 12px; color: var(--text-2);
  letter-spacing: .04em;
}

.v-nav {
  position: absolute; z-index: 3; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: var(--text); border: 1px solid var(--hair);
  transition: opacity var(--t-fast), background var(--t-fast), transform var(--t-fast);
}
#v-prev { left: calc(12px + env(safe-area-inset-left, 0px)); }
#v-next { right: calc(12px + env(safe-area-inset-right, 0px)); }
/* hover brightens the knob WITHOUT dropping the pane — the old flat
   rgba(255,255,255,.14) replaced the fill, and over a white photograph
   the white arrow then sat on white */
.v-nav:hover {
  background: linear-gradient(rgba(255, 255, 255, .12) 0 0),
              rgba(var(--pane), .92);
  border-color: var(--hair-2);
}
.v-nav:active { transform: translateY(-50%) scale(.94); }
.v-nav[disabled] { opacity: .25; pointer-events: none; }

#v-info {
  position: absolute; z-index: 3;
  left: calc(14px + env(safe-area-inset-left, 0px));
  bottom: calc(14px + var(--sab));
  width: min(400px, calc(100vw - 28px));
  padding: 12px 14px;
  border-radius: var(--r-lg);
}
.v-info-head { display: flex; align-items: center; gap: 6px; }
.acct {
  flex: 1; min-width: 0; text-align: left;
  display: flex; align-items: center; gap: 6px;
  background: none; border: 0; padding: 4px 6px; border-radius: 8px;
  font-size: 14.5px; font-weight: 600; letter-spacing: .01em;
  transition: background var(--t-fast);
}
.acct #v-acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Only an operator's account line is a field. A member's is a label: no
   hover fill, no pencil, no pointer — the affordance and the permission say
   the same thing, so nothing invites a click the server will refuse. */
.acct.editable:hover { background: rgba(255, 255, 255, .08); }
.acct:not(.editable) { cursor: default; }
.acct:not(.editable) .pen { display: none; }
.acct.none { color: var(--text-3); font-weight: 400; }
/* The pencil is the affordance, not decoration: it says the name is a field.
   It holds its 15px whether shown or not, so revealing it never reflows. */
.acct .pen {
  width: 15px; height: 15px; opacity: 0; color: var(--text-3);
  transition: opacity var(--t-fast), color var(--t-fast);
}
.acct:hover .pen, .acct:focus-visible .pen { opacity: 1; }
.acct:hover .pen { color: var(--text-2); }
@media (hover: none) { .acct .pen { opacity: .55; } }
.v-info-head .btn-icon { width: 32px; height: 32px; border: 0; background: none; color: var(--text-2); }
.v-info-head .btn-icon:hover { background: rgba(255, 255, 255, .09); color: var(--text); }

/* inline account editor — same row, same rhythm, one field deep */
.v-acct-edit { display: flex; flex-direction: column; gap: 7px; }
.v-acct-row { display: flex; align-items: center; gap: 6px; }
.acct-input {
  flex: 1; min-width: 0; height: 32px; padding: 0 10px;
  border-radius: 9px; border: 1px solid var(--hair-2);
  background: rgba(0, 0, 0, .35); color: var(--text);
  font-size: 14px; font-weight: 600; letter-spacing: .01em;
}
.acct-input::placeholder { color: var(--text-3); font-weight: 400; }
.acct-input:focus { outline: none; border-color: rgba(255, 255, 255, .34); }
.acct-input.bad { border-color: rgba(255, 36, 66, .55); }
.v-acct-edit .btn-icon { width: 32px; height: 32px; flex: none; }
#v-acct-save { color: var(--text); }
.v-acct-edit.busy { opacity: .6; pointer-events: none; }
/* The bulk apply is opt-in and reads as the smaller thing it is. */
.v-acct-bulk {
  display: flex; align-items: center; gap: 8px;
  padding: 2px 6px; font-size: 12.5px; color: var(--text-2); cursor: pointer;
}
.v-acct-bulk input { accent-color: var(--red); width: 14px; height: 14px; margin: 0; }
.v-acct-bulk:hover { color: var(--text); }
.v-acct-msg { margin: 0; padding: 0 6px; font-size: 12px; color: #ffb3be; }

/* rating row: outside #v-info-body so collapsing the details (i) keeps the
   triage controls — flip, rate, flip is the whole browse loop */
.v-rate { display: flex; gap: 6px; margin-top: 10px; }
.v-rate .btn { flex: 1; height: 36px; padding: 0 8px; font-size: 12.5px; background: rgba(255, 255, 255, .07); }
.v-rate .btn:hover { background: rgba(255, 255, 255, .13); }
.v-rate .btn[aria-pressed="true"] { background: var(--text); border-color: transparent; color: #101013; }
.v-rate .btn[aria-pressed="true"] .ico { stroke: #101013; }
#v-save[aria-pressed="true"] { background: var(--red); border-color: var(--red); color: #fff; }
#v-save[aria-pressed="true"] .ico { stroke: #fff; fill: #fff; }

.acct[disabled] { cursor: default; }
.acct[disabled]:hover { background: none; }
.acct[disabled] .pen { display: none; }

#v-info.min #v-info-body { display: none; }
#v-info-body { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
#v-query { align-self: flex-start; max-width: 100%; }
/* Title then description, both the pinner's own words -- anything Pinterest
   generated is withheld server-side and never arrives. They are told apart
   by weight alone: a "Title"/"Description" label would be three words of
   chrome explaining two lines of content. */
.v-title {
  margin: 0; font-size: 13.5px; line-height: 1.45; font-weight: 620;
  color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere;
}
.v-desc {
  margin: 0; font-size: 13px; line-height: 1.5; font-weight: 400;
  color: var(--text-2);
  white-space: pre-wrap; overflow-wrap: anywhere;
  max-height: 8.4em; overflow-y: auto; overscroll-behavior: contain;
}
.v-note { margin: 0; font: italic 400 13.5px/1.5 var(--serif); color: var(--text-2); }
.v-meta { margin: 0; font-size: 12px; color: var(--text-3); letter-spacing: .02em; }
.v-warn {
  font-size: 12.5px; color: #ffb3be; padding: 8px 10px;
  border: 1px solid rgba(255, 36, 66, .3); border-radius: 10px;
  background: var(--red-soft);
}
.v-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
.v-actions .btn { height: 34px; padding: 0 12px; font-size: 12.5px; background: rgba(255,255,255,.07); }
.v-actions .btn:hover { background: rgba(255,255,255,.13); }

/* ── editor ───────────────────────────────────────────────────────────── */

#editor { display: flex; }
.e-stage {
  position: relative; flex: 1; min-width: 0;
  display: flex; align-items: center; justify-content: center;
  padding: calc(22px + var(--sat)) 22px 22px;
}
.e-canvas {
  position: relative; flex: none;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .55);
  background:
    conic-gradient(rgba(255,255,255,.06) 0 25%, transparent 0 50%,
                   rgba(255,255,255,.06) 0 75%, transparent 0) 0 0/18px 18px,
    #17171b;
  touch-action: none;
}
/* No image-orientation override: the editor works in the image's DISPLAYED
   orientation — the same decode drawImage samples for the export — so the
   crop maths, the preview and the rendered file share one coordinate space.
   (The old `none` here contradicted the server, which oriented before
   cropping; the server no longer crops at all.) */
#e-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none;
}

/* Where the creator's watermark lives (pill + account line, bottom-right).
   The fractions mirror MARK in app.js — measured, not guessed; t_web pins
   the two files to the same numbers. */
.e-badge {
  position: absolute; left: 80%; top: 86%; right: 0; bottom: 0;
  border: 1px dashed rgba(255, 255, 255, .38);
  border-right: 0; border-bottom: 0; border-radius: 6px 0 0 0;
  pointer-events: none;
  transition: border-color var(--t-fast);
}
.e-badge.cut { border-color: rgba(255, 36, 66, .7); }
.e-badge::after {
  content: "小红书"; position: absolute; right: 5px; top: 3px;
  font-size: 9px; letter-spacing: .06em; color: rgba(255, 255, 255, .45);
}
.e-badge.cut::after { color: rgba(255, 120, 140, .8); }

.e-marquee {
  position: absolute;
  border: 1.5px solid rgba(255, 255, 255, .95);
  box-shadow: 0 0 0 9999px rgba(5, 5, 8, .62);
  cursor: move; touch-action: none;
}
.e-marquee:focus-visible { outline: 2px solid var(--red); outline-offset: 0; }
.e-thirds {
  position: absolute; inset: 0; opacity: 0; transition: opacity var(--t-fast);
  background:
    linear-gradient(90deg, transparent calc(33.33% - .5px), rgba(255,255,255,.4) calc(33.33% - .5px) calc(33.33% + .5px), transparent 0),
    linear-gradient(90deg, transparent calc(66.66% - .5px), rgba(255,255,255,.4) calc(66.66% - .5px) calc(66.66% + .5px), transparent 0),
    linear-gradient(180deg, transparent calc(33.33% - .5px), rgba(255,255,255,.4) calc(33.33% - .5px) calc(33.33% + .5px), transparent 0),
    linear-gradient(180deg, transparent calc(66.66% - .5px), rgba(255,255,255,.4) calc(66.66% - .5px) calc(66.66% + .5px), transparent 0);
}
.e-marquee.live .e-thirds { opacity: 1; }

.e-handle {
  position: absolute; width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
}
.e-handle::after {
  content: ""; width: 11px; height: 11px; border-radius: 3px;
  background: #fff; box-shadow: 0 1px 5px rgba(0, 0, 0, .55);
}
.e-handle[data-h="n"], .e-handle[data-h="s"] { left: 50%; margin-left: -13px; cursor: ns-resize; }
.e-handle[data-h="e"], .e-handle[data-h="w"] { top: 50%; margin-top: -13px; cursor: ew-resize; }
.e-handle[data-h="n"] { top: -13px; } .e-handle[data-h="s"] { bottom: -13px; }
.e-handle[data-h="e"] { right: -13px; } .e-handle[data-h="w"] { left: -13px; }
.e-handle[data-h="nw"] { top: -13px; left: -13px; cursor: nwse-resize; }
.e-handle[data-h="se"] { bottom: -13px; right: -13px; cursor: nwse-resize; }
.e-handle[data-h="ne"] { top: -13px; right: -13px; cursor: nesw-resize; }
.e-handle[data-h="sw"] { bottom: -13px; left: -13px; cursor: nesw-resize; }
.e-handle[data-h="n"]::after, .e-handle[data-h="s"]::after { width: 22px; height: 5px; }
.e-handle[data-h="e"]::after, .e-handle[data-h="w"]::after { width: 5px; height: 22px; }

#e-panel {
  position: relative; z-index: 2; flex: none;
  width: 322px; margin: calc(14px + var(--sat)) 14px 14px 0;
  padding: 16px; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 14px;
}
.e-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.e-title { margin: 0; font: italic 600 19px/1 var(--serif); }
.e-sub { margin: -6px 0 0; font-size: 12px; color: var(--text-3); }

.e-preview-row { display: flex; gap: 12px; align-items: center; }
.e-preview {
  flex: none; width: 84px; height: 84px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 12px; border: 1px solid var(--hair);
  background: conic-gradient(rgba(255,255,255,.05) 0 25%, transparent 0 50%,
              rgba(255,255,255,.05) 0 75%, transparent 0) 0 0/14px 14px, #101013;
  overflow: hidden;
}
#e-preview-clip { position: relative; overflow: hidden; flex: none; }
#e-preview-img { position: absolute; transform-origin: 0 0; max-width: none; }
.e-readout { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.e-readout output { font-size: 15px; }
.e-readout .dim { font-size: 11.5px; }

.lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .09em; color: var(--text-3); font-weight: 600; }
.e-group { display: flex; flex-direction: column; gap: 7px; }

/* watermark row: quiet status lines and two chips — never blocks an export */
.e-wm-note { margin: 0; font-size: 12px; color: var(--text-3); }
.e-wm-note.done { color: var(--text-2); }
#e-wm-erase.busy { pointer-events: none; opacity: .8; }
#e-wm-erase.busy::before {
  content: ""; width: 11px; height: 11px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  flex: none; animation: spin .7s linear infinite;
}
#e-wm-erase.busy span { font-family: var(--mono); font-size: 11.5px; }
.e-chips { display: flex; gap: 5px; flex-wrap: wrap; }
.e-chips .chip { padding: 6px 10px; font-size: 12px; }

.e-sliders { gap: 9px; }
.e-slider { display: grid; grid-template-columns: 62px 1fr 40px; align-items: center; gap: 8px; }
.e-slider output { font-size: 12px; color: var(--text-2); text-align: right; }
.e-fine { margin: 0; font-size: 11px; color: var(--text-3); }
input[type="range"] {
  -webkit-appearance: none; appearance: none; width: 100%; height: 22px;
  background: none; cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 3px; border-radius: 2px; background: var(--hair-2);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 15px; height: 15px; margin-top: -6px;
  border-radius: 50%; background: #fff; border: 0;
  box-shadow: 0 1px 5px rgba(0, 0, 0, .5);
}
input[type="range"]::-moz-range-track { height: 3px; border-radius: 2px; background: var(--hair-2); }
input[type="range"]::-moz-range-thumb {
  width: 15px; height: 15px; border-radius: 50%; background: #fff; border: 0;
  box-shadow: 0 1px 5px rgba(0, 0, 0, .5);
}
input[type="range"]:focus-visible { outline: 2px solid var(--red); outline-offset: 4px; border-radius: 6px; }

.e-foot { display: flex; gap: 8px; margin-top: auto; padding-top: 4px; }
.e-foot .btn { flex: 1; }

/* ── gate & toasts ────────────────────────────────────────────────────── */

#gate { z-index: 70; }
.gate-card { margin-top: 24vh; border-radius: var(--r-lg); position: relative; padding-bottom: calc(36px + var(--sill-h)); }

#selbar:not([hidden]) ~ #toasts { bottom: calc(76px + var(--sab)); }
/* The editor's watermark badge is pinned to the image's bottom-right corner
   (`.e-badge`, left:80% top:86%), and a bottom-centred toast sat on top of
   it: erase the badge, and the one thing you asked to look at was covered
   by the message telling you it had happened. Toasts ride at the TOP while
   the editor is open — clear of that corner at every image aspect and every
   toast width, and still a toast, which is what was wanted. Same sibling
   idiom as `#selbar` above; `#toasts` follows `#editor` in index.html. */
#editor:not([hidden]) ~ #toasts {
  top: calc(16px + var(--sat)); bottom: auto;
}
#editor:not([hidden]) ~ #toasts .toast { animation-name: fall; }
#toasts {
  position: fixed; z-index: 80; bottom: calc(16px + var(--sab)); left: 50%;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  pointer-events: none; width: max-content; max-width: calc(100vw - 24px);
}
/* a toast is a small pane: field .94, NO backdrop-filter (it must be
   readable in its first 200ms), an 8px ledge. The error variant carries
   the same instrument as the masthead — a fail-form pip, riding centred
   in the pill, over the spill's ledge band — not a second red saying
   the same thing. */
.toast {
  position: relative; isolation: isolate;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 18px calc(10px + 6px); border-radius: 999px;
  background: rgba(var(--pane), .94); border: 1px solid var(--hair);
  box-shadow: var(--shadow); color: var(--ink); font-size: 13.5px;
  max-width: 100%; overflow: hidden; white-space: nowrap;
  animation: rise var(--t-med) var(--ease);
  --sill-h: 8px;
}
.toast::after {
  content: ""; position: absolute; inset: 1px; border-radius: inherit;
  pointer-events: none; z-index: 0; background: var(--sill-paint);
}
.toast > span { overflow: hidden; text-overflow: ellipsis; }
.toast.has-act { pointer-events: auto; }
.toast-act {
  flex: none; border: 0; background: none; padding: 2px; border-radius: 6px;
  color: var(--ink); font: 600 11px/1 var(--mono); text-transform: uppercase;
  letter-spacing: .1em; cursor: pointer; z-index: 1;
}
.toast-act:hover { color: #fff; text-decoration: underline; text-underline-offset: 3px; }
.toast.err { padding-right: 34px; }
.toast.err::after {
  background:
    linear-gradient(270deg, rgba(255, 36, 66, .34), transparent 120px) right bottom / 100% var(--sill-h) no-repeat,
    var(--sill-paint);
}
.toast.err::before {
  content: ""; position: absolute; z-index: 1; right: 13px; bottom: 50%;
  transform: translateY(50%);
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--red) linear-gradient(#fff 0 0) center / 7px 2px no-repeat;
  box-shadow: inset 0 0 0 1.4px var(--node);
}
.toast.out { opacity: 0; transform: translateY(6px); transition: opacity var(--t-med), transform var(--t-med); }

/* the viewer's query-line role actions */
.v-queryacts { display: flex; gap: 14px; margin: -2px 0 0; }
.linky {
  border: 0; background: none; padding: 2px 0; cursor: pointer;
  font-size: 12px; color: var(--ink-3);
  text-decoration: underline; text-underline-offset: 3px;
}
.linky:hover { color: var(--ink); }

/* ── Discord delivery ─────────────────────────────────────────────────
   A card, not a sheet: three numbered steps read top to bottom, and the
   one that matters (step 2, /here) is the only one with a lit edge. The
   numbers are mono because they are addresses, not prose; the channel
   is mono because it is a machine name; the server is ink because it is
   what the customer calls the place. Fixed and z-indexed like the other
   dialogs. */
#dest { position: fixed; inset: 0; z-index: 50; }
.dest-card {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(460px, calc(100vw - 24px));
  max-height: calc(100vh - 24px - var(--sat) - var(--sab));
  overflow: auto; padding: 18px 20px 16px;
  display: flex; flex-direction: column; gap: 12px;
}
.dest-steps { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.dest-step {
  border-radius: var(--r-md); padding: 12px 14px;
  box-shadow: inset 0 0 0 1px var(--hair);
  display: flex; flex-direction: column; gap: 7px;
}
/* the step customers stall on: the edge and the field both lift a notch */
.dest-step-key { box-shadow: inset 0 0 0 1px var(--hair-2); background: rgba(255, 255, 255, .035); }
.dest-step-head { display: flex; align-items: center; gap: 10px; min-height: 22px; }
.dest-n {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--hair-2);
}
.dest-step-key .dest-n { background: var(--ink); color: #101013; box-shadow: none; font-weight: 600; }
.dest-step-title { font-size: 14px; font-weight: 600; color: var(--ink); flex: 1; min-width: 0; }
.dest-step-text { margin: 0; font-size: 13px; line-height: 1.45; color: var(--ink-2); }
.dest-step-text b { color: var(--ink); font-weight: 600; }
.dest-dim { color: var(--ink-3); font-size: 12px; }
.dest-cmd, .dest-step-text code, .dest-warn code {
  font-size: .92em; padding: 1px 5px; border-radius: 5px;
  background: rgba(255, 255, 255, .07); box-shadow: inset 0 0 0 1px var(--hair);
}
/* the invite: Discord's own colour, same reason as .me-in -- this button
   leaves for Discord and should be recognised before it is read */
.dest-invite { align-self: flex-start; background: var(--blurple); color: #fff; }
.dest-invite .ico { stroke: none; fill: #fff; width: 16px; height: 16px; }
.dest-invite:hover { background: var(--blurple-hi); }
.dest-invite[aria-disabled="true"] { opacity: .38; pointer-events: none; }
.dest-warn { margin: 0; font-size: 12px; line-height: 1.45; color: var(--amber); }
.dest-refresh { width: 28px; height: 28px; }
.dest-refresh .ico { width: 14px; height: 14px; }
.dest-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
/* One destination, read top-down: the channel is the answer, the server
   and the age are its context. The previous grid put the server id on one
   line, the age on a second and the channel on a third, which is three
   fragments where there is only one fact. */
.dest-row {
  display: flex; flex-direction: column; gap: 1px;
  padding: 9px 11px; border-radius: 10px; min-width: 0;
  background: rgba(255, 255, 255, .04); text-decoration: none; color: inherit;
}
a.dest-row:hover { background: rgba(255, 255, 255, .08); }
.dest-chan {
  font: 600 13.5px/1.35 var(--mono); color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dest-meta {
  font-size: 12px; color: var(--ink-3); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dest-guild { font-weight: 500; color: var(--ink-2); }
.dest-when { font: 11px/1 var(--mono); color: var(--ink-3); }
/* the newest row is where the next find lands first */
.dest-row.newest .dest-chan::after {
  content: "latest"; margin-left: 8px; padding: 1px 6px; border-radius: 999px;
  font: 500 10px/1.6 var(--sans, inherit); letter-spacing: .04em;
  background: rgba(255, 255, 255, .10); color: var(--ink-2);
}
/* the /here list: what still needs doing, not a status board */
.dest-guild-row .dest-chan { font-size: 12px; font-weight: 500; color: var(--ink-3); }

/* ── the eviction ladder (SILL §3.2): the lane never wraps, so members
   leave it for the Refine sheet in a fixed order as width falls ──────── */

/* ≤1080: Zip all, Select, ? leave the lane for a sheet row */
@media (max-width: 1080px) {
  #bar:not(.xp) .tools { display: none; }
  #bar.xp .tools {
    position: absolute; z-index: 3;
    top: calc(100% + 24px); left: 26px; right: 26px;
    display: flex; align-items: center; gap: 8px;
  }
  #bar.xp #tray { padding-top: 64px; }
}
/* ≤900: touch lane, tighter gutters, no middots (the words are already
   told apart by tone; the dots were a third glyph at the weakest tone) */
@media (max-width: 900px) {
  .mark-sep { display: none; }
  .mark-word { font-size: 18px; }
  #bar-toggle .rf-label { display: none; }
  #bar-toggle { width: var(--ctl); padding: 0; }
  /* identity degrades rather than evicting: the monogram alone still
     answers "who am I", which is the whole reason it is not on the
     ladder. Same move as Refine's label directly above. */
  .me-name { display: none; }
  .me-btn { padding: 0 4px; }
  .me-in-label { display: none; }
  .me-in { width: var(--ctl); padding: 0; }
  .me-slot { width: var(--ctl); }       /* the reservation collapses too */
}
/* ≤640: search leaves the lane and becomes the sheet's first row; the
   active-filter chips stay in the lane (state stays visible) */
@media (max-width: 640px) {
  :root { --r-tile: 7px; --r-lg: 18px; }
  #bar:not(.xp) .search { display: none; }
  #bar.xp .search {
    position: absolute; z-index: 3;
    top: calc(100% + 24px); left: 26px; right: 26px;
    max-width: none;
  }
  #bar.xp .tools { top: calc(100% + 24px + 40px + 8px); }
  #bar.xp #tray { padding-top: 112px; }
  #btn-select span, #btn-all:not(.busy) span { display: none; }
  #btn-select, #btn-all:not(.busy) { width: var(--ctl); padding: 0; }
  /* the selection tray goes icon-only, like .v-actions — at 360 the
     labelled row ran past the right edge and clipped Save and the exit */
  #selbar { gap: 6px; padding: 8px 8px 8px 12px; }
  #selbar .btn span { display: none; }
  .v-nav { width: 38px; height: 38px; }
  #v-info { left: 8px; right: 8px; width: auto; bottom: calc(8px + var(--sab)); }
  .v-actions .btn span { display: none; }
  .v-actions .btn { flex: 1; }
  .v-actions #v-del { flex: none; }
  .v-actions .btn span#v-del-label { display: inline; }
  #editor { flex-direction: column; }
  .e-stage { padding: calc(12px + var(--sat)) 12px 10px; }
  #e-panel { width: auto; margin: 0; border-radius: var(--r-lg) var(--r-lg) 0 0; max-height: 46dvh; padding-bottom: calc(14px + var(--sab)); }
  .stage-msg { margin-top: 10vh; }
}
/* ≤480 */
@media (max-width: 480px) {
  .mark-word { font-size: 17px; }
}
@media (max-width: 340px) {
  .mark-word { font-size: 16px; }
  .mark { gap: 6px; }
}
@media (min-width: 1800px) {
  :root { --r-tile: 12px; }
}

/* ── motion & contrast preferences ────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .cell.shimmer .tile, #skeleton i, #wall-more i { animation: none; }
  /* the sweep becomes a still, dimmer bar: present, not moving */
  #loadbar.indet #loadbar-fill { animation: none; width: 100%; opacity: .55; }
  html { scroll-behavior: auto; }
}
/* the sill is paint, not motion — it is unchanged above; under reduced
   transparency the pane goes solid and the ledge brightens a step */
@media (prefers-reduced-transparency: reduce) {
  .glass, .knob, #tray, .toast {
    background: #121216;
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  :root { --sill-lit: rgba(255, 255, 255, .13); }
}
@media (prefers-contrast: more) {
  :root {
    --field: 1;
    --ink-3: rgba(255, 255, 255, .74); --text-3: rgba(255, 255, 255, .74);
    --hair: rgba(255, 255, 255, .30); --hair-2: rgba(255, 255, 255, .46);
    --sill-edge: rgba(255, 255, 255, .42); --sill-lit: rgba(255, 255, 255, .14);
  }
  .glass, .knob, #tray, .toast {
    background: #101014;
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
}

/* The mark's words are <button>s here. The `a.mark-word` rule that carried
   crawl.html's and admin.html's anchor doors went with those pages on
   2026-09-10. */

/* ── source marks ───────────────────────────────────────────────────────
   Which platform a handle belongs to, next to the handle. Both accounts
   read as `@name` now — the mark carries the distinction, so the text does
   not have to, and a Pinterest handle stops being spelled as a URL.

   Filled, not stroked: `.ico` sets `fill: none; stroke: currentColor`, which
   would erase a two-tone brand mark entirely. They keep their own colours
   for the same reason — a logo that recolours with the surrounding text is
   no longer the logo. 13px is where the Pinterest P and the RedNote tile are
   both still readable; below about 11 the P closes up.                    */
.badge {
  width: 13px; height: 13px; flex: none;
  fill: currentColor; stroke: none;
  vertical-align: -2px;
}
.tile-acct { display: flex; align-items: center; gap: 5px; }
/* On a tile the mark sits over the image, where a white rim separates it
   from whatever is behind it. In the viewer the panel is already dark, so
   the rim would read as an outline nobody asked for. */
.tile-acct .badge {
  filter: drop-shadow(0 0 1px rgba(0, 0, 0, .55));
}
.acct .badge { margin-right: 1px; }
/* The clip moves to the label. `text-overflow` acts on a block container's
   own inline content, and both of these became flex rows the moment a mark
   sat beside the text — so without this a long handle stretches the tile
   rather than ellipsing. */
.acct-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
             min-width: 0; }
.acct #v-acct-text { display: flex; align-items: center; gap: 6px;
                     min-width: 0; }

/* ── the open front door (2026-09-07) ───────────────────────────────────
   The archive is now the landing page for anyone. Two surfaces are for the
   stranger only: the sentence above the wall, and the sign-in pane every
   member-only action opens instead of failing. Members never see either;
   their chrome is what it was. */

/* the sentence: prose in the wall's own flow, so the deck's declared
   altitudes hold; the name in the mode words' serif so the strap and the
   navigation are visibly one voice */
/* The anonymous banner. A strip, not a card: one hairline under it and
   nothing else, so it reads as the wall's masthead rather than a box
   sitting on top of the photographs. The two halves justify apart and
   stack under 720px, where the CTA leads instead of trailing. */
.hello-anon {
  display: flex; align-items: flex-end; justify-content: space-between;
  flex-wrap: wrap; gap: 10px 28px;
  margin: 0 var(--gutter) 14px; padding: 0 2px 12px;
  border-bottom: 1px solid var(--hair);
  color: var(--text-2); text-wrap: pretty;
}
.hello-anon p { margin: 0; }
.hello-say { min-width: 0; max-width: 62ch; }

/* The count is the claim, so it is the only thing set at display size.
   Tabular figures because it is a number that changes under the reader —
   every crawl moves it, and digits that re-measure make the line twitch. */
.hello-lead {
  font: italic 600 20px/1.15 var(--serif); color: var(--text);
  letter-spacing: .012em;
}
.hello-lead b { font-weight: 600; font-variant-numeric: tabular-nums; }
.hello-sub { margin-top: 3px !important; font-size: 13.5px; line-height: 1.5; }

.hello-act {
  display: flex; flex-direction: column; align-items: flex-end; gap: 5px;
  flex: none;
}
.hello-cta {
  display: inline-flex; align-items: center; gap: 8px;
  height: var(--ctl); padding: 0 14px; border-radius: 999px;
  background: var(--blurple); color: #fff; text-decoration: none;
  font-size: 13.5px; font-weight: 600; white-space: nowrap;
  transition: background var(--t-fast), transform var(--t-fast);
}
.hello-cta:hover { background: var(--blurple-hi); }
.hello-cta:active { transform: translateY(1px); }
.hello-cta .d-logo { width: 18px; height: 14px; flex: none; }
.hello-fine { font-size: 12px; color: var(--text-3); }

/* The two halves stack once the lane cannot hold both: the sentence's
   62ch and the CTA's fine print need ~1080px between the gutters. Below
   that the flex wrap already put the CTA on its own line -- but
   right-aligned to the end of its own fine print, i.e. floating in the
   middle of the strip (seen at 900). Stacked, it stands at the left with
   the words above it. */
@media (max-width: 1100px) {
  .hello-anon { align-items: flex-start; }
  .hello-act { align-items: flex-start; width: 100%; }
}
@media (max-width: 720px) {
  .hello-lead { font-size: 18px; }
}
@media (prefers-reduced-motion: reduce) {
  .hello-cta { transition: none; }
  .hello-cta:active { transform: none; }
}

/* ── the sign-in pane: liquid glass ──────────────────────────────────────
   Still the house's one pane — same rgb, the field at the token (never
   below .80 where text sits), the corner family, the ledge — but this is
   the one surface where the photographs are *meant* to be felt through
   the glass: its backdrop dims less and blurs more than the viewer's, so
   the wall behind melts into colour, and the pane saturates what reaches
   it. Three things make it read as a liquid rather than a slab, one layer
   each: a refractive rim (a 1px gradient ring, brightest where light would
   enter at the top-left, picking up the two brand hues where it leaves),
   a soft specular pooled in that same corner, and an entry that settles
   rather than snaps. All of it is paint on the pane; nothing is a second
   pane, and under reduced transparency / more contrast the house rules
   above solidify it with every other .glass. */
#signin {
  position: fixed; inset: 0; z-index: 72;
  display: grid; place-items: center;
  padding: max(16px, var(--sat)) 16px max(16px, var(--sab));
}
#signin .backdrop { cursor: default; background: rgba(5, 5, 8, .62); }
@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
  #signin .backdrop {
    background: rgba(5, 5, 8, .46);
    -webkit-backdrop-filter: blur(26px) saturate(1.5);
    backdrop-filter: blur(26px) saturate(1.5);
  }
  #signin .si-card {
    -webkit-backdrop-filter: blur(34px) saturate(2);
    backdrop-filter: blur(34px) saturate(2);
  }
}
.si-card {
  position: relative; width: min(392px, 100%);
  max-height: calc(100dvh - 32px - var(--sat) - var(--sab)); overflow: auto;
  padding: 34px 30px calc(26px + var(--sill-h)); text-align: center;
  border-radius: 26px; border-color: transparent;
  /* the specular, pooled top-left, over the pane's own field */
  background:
    radial-gradient(140% 70% at 14% -12%, rgba(255, 255, 255, .11), transparent 58%),
    radial-gradient(90% 60% at 100% 110%, rgba(88, 101, 242, .10), transparent 60%),
    rgba(var(--pane), var(--field));
  box-shadow: var(--shadow), 0 40px 90px -30px rgba(88, 101, 242, .25),
              inset 0 1px 0 rgba(255, 255, 255, .09);
}
/* the refractive rim: a 1px gradient ring cut with a mask so only the
   border shows; brightest where light enters, tinted where it leaves */
.si-card::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 1px; pointer-events: none; z-index: 1;
  background: linear-gradient(135deg,
    rgba(255, 255, 255, .34) 0%, rgba(255, 255, 255, .10) 34%,
    rgba(255, 255, 255, .06) 58%, rgba(88, 101, 242, .26) 84%, rgba(255, 36, 66, .12) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
}
.si-close { position: absolute; top: 10px; right: 10px; z-index: 2; }
.si-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 50%; margin: 0 auto 14px;
  background: var(--red-soft); box-shadow: inset 0 0 0 1px rgba(255, 36, 66, .30);
}
.si-mark .ico { width: 20px; height: 20px; stroke: var(--red); fill: var(--red); }
.si-title {
  font: italic 600 27px/1.12 var(--serif); letter-spacing: .012em;
  color: var(--ink); margin: 0 0 8px; text-wrap: balance;
}
.si-text { margin: 0 0 18px; font-size: 14.5px; line-height: 1.5; color: var(--ink-2); text-wrap: pretty; }
.si-perks {
  list-style: none; margin: 0 0 20px; padding: 12px 14px; text-align: left;
  border-radius: var(--r-md); box-shadow: inset 0 0 0 1px var(--hair);
  background: rgba(255, 255, 255, .035);
  display: flex; flex-direction: column; gap: 8px;
}
.si-perks li { display: flex; gap: 10px; align-items: flex-start; font-size: 13.5px; line-height: 1.4; color: var(--ink-2); }
.si-perks .ico { width: 16px; height: 16px; margin-top: 1px; stroke: var(--ink); }
/* the one action: Discord's own colour (as .me-in and login.html), full
   width, a lift of its own light so it is the brightest thing on the pane */
.si-discord {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; height: 46px; border-radius: 13px;
  background: var(--blurple); color: #fff; text-decoration: none;
  font-weight: 600; font-size: 15px;
  box-shadow: 0 10px 26px -8px rgba(88, 101, 242, .65), inset 0 1px 0 rgba(255, 255, 255, .18);
  transition: background var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast);
}
.si-discord:hover { background: #6672f5; box-shadow: 0 14px 30px -8px rgba(88, 101, 242, .75), inset 0 1px 0 rgba(255, 255, 255, .22); }
.si-discord:active { transform: translateY(1px) scale(.99); }
.si-discord:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.si-discord .d-logo { width: 21px; height: 16px; }
.si-fine { margin: 14px 0 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-3); text-wrap: pretty; }

/* entry: the pane settles into place while the glass clears; one class
   toggled by openSignIn(). Reduced motion zeroes every animation duration
   (the rule above), so the same keyframes simply land on their last frame.
   `backwards`, never `forwards`/`both`: nothing here may keep asserting a
   frame after it ends (claim 22) — the resting state is the natural one. */
#signin.in .backdrop { animation: si-fade var(--t-med) var(--ease) backwards; }
#signin.in .si-card  { animation: si-rise 320ms var(--ease) backwards; }
@keyframes si-fade { from { opacity: 0; } }
@keyframes si-rise { from { opacity: 0; transform: translateY(14px) scale(.965); } }
@media (max-width: 480px) {
  .si-card { padding: 30px 22px calc(22px + var(--sill-h)); border-radius: 22px; }
  .si-title { font-size: 24px; }
}
@media (max-height: 560px) { .si-perks, .si-mark { display: none; } }

/* anonymous: the viewer's member actions stay in place but read as not
   yet available; a click opens the door rather than doing nothing */
#viewer.anon #v-down, #viewer.anon #v-edit { opacity: .55; }
#viewer.anon #v-down:hover, #viewer.anon #v-edit:hover { opacity: .8; }

/* the liquid goes solid with every other pane; the rim becomes a hairline
   (restated here because .si-card's layered background is declared after
   the house rule it would otherwise outrank) */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .si-card {
    background: #101014; border-color: var(--hair);
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  .si-card::before { display: none; }
}

/* ── the delivery card's verdict: which step is missing ─────────────────
   paintDest() writes `data-state` (no_bot / no_channel / ready) and lifts
   the matching step; the sentence names the step so the member who has
   already added the bot is never told to add it again. */
.dest-state {
  margin: 0; padding: 9px 12px; border-radius: 10px;
  font-size: 13px; line-height: 1.45; color: var(--ink);
  background: rgba(255, 255, 255, .05); box-shadow: inset 0 0 0 1px var(--hair-2);
}
.dest-state[data-state="ready"] { color: #b8ffd0; box-shadow: inset 0 0 0 1px rgba(120, 255, 170, .28); background: rgba(120, 255, 170, .07); }
.dest-state b, .dest-state code { color: var(--ink); }
.dest-done { display: inline-flex; width: 18px; height: 18px; border-radius: 50%; align-items: center; justify-content: center; background: rgba(120, 255, 170, .16); }
.dest-done .ico { width: 12px; height: 12px; stroke: #9cf5b9; }
.dest-guilds { margin-top: 2px; }
.dest-guild-row { display: flex; align-items: baseline; gap: 8px; padding: 6px 10px; border-radius: 8px; background: rgba(255, 255, 255, .04); font-size: 13px; color: var(--ink-2); }
.dest-guild-row .dest-guild { flex: 1; }
.dest-guild-row .dest-chan.other { color: var(--ink-3); }

/* ── Start a crawl: the member surface, on the liquid pane ─────────────
   The same pane as the sign-in door — same rgb, field, rim and specular,
   one card family for the two doors a member walks through — with the
   product's red where the sign-in carries Discord's blurple: this pane
   spends something, and the primary reads accordingly. Below #dest
   (z-50) on purpose: the delivery card opens over it when the switch is
   pressed before the setup is done. */
#crawl {
  position: fixed; inset: 0; z-index: 48;
  display: grid; place-items: center;
  padding: max(16px, var(--sat)) 16px max(16px, var(--sab));
}
#crawl .backdrop { cursor: default; background: rgba(5, 5, 8, .62); }
@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
  #crawl .backdrop {
    background: rgba(5, 5, 8, .46);
    -webkit-backdrop-filter: blur(26px) saturate(1.5);
    backdrop-filter: blur(26px) saturate(1.5);
  }
  #crawl .cr-card {
    -webkit-backdrop-filter: blur(34px) saturate(2);
    backdrop-filter: blur(34px) saturate(2);
  }
}
.cr-card {
  position: relative; width: min(444px, 100%); margin: 0;
  max-height: calc(100dvh - 32px - var(--sat) - var(--sab)); overflow: auto;
  padding: 30px 28px calc(22px + var(--sill-h));
  border-radius: 26px; border-color: transparent;
  display: flex; flex-direction: column; gap: 14px;
  background:
    radial-gradient(140% 70% at 14% -12%, rgba(255, 255, 255, .11), transparent 58%),
    radial-gradient(90% 60% at 100% 110%, rgba(255, 36, 66, .09), transparent 60%),
    rgba(var(--pane), var(--field));
  box-shadow: var(--shadow), 0 40px 90px -30px rgba(255, 36, 66, .18),
              inset 0 1px 0 rgba(255, 255, 255, .09);
}
.cr-card::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 1px; pointer-events: none; z-index: 1;
  background: linear-gradient(135deg,
    rgba(255, 255, 255, .34) 0%, rgba(255, 255, 255, .10) 34%,
    rgba(255, 255, 255, .06) 58%, rgba(255, 36, 66, .22) 84%, rgba(88, 101, 242, .16) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
}
.cr-close { position: absolute; top: 10px; right: 10px; z-index: 2; }
.cr-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--red-soft); box-shadow: inset 0 0 0 1px rgba(255, 36, 66, .30);
}
.cr-mark .ico { width: 18px; height: 18px; stroke: var(--red); }
.cr-title { font: italic 600 26px/1.12 var(--serif); letter-spacing: .012em; color: var(--ink); margin: -4px 0 0; text-wrap: balance; }
.cr-sub { margin: -6px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-2); text-wrap: pretty; }

.cr-field { display: flex; flex-direction: column; gap: 8px; }
.cr-lblrow { display: flex; align-items: baseline; justify-content: space-between; }
.cr-lbl { font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.cr-count { font-size: 11px; color: var(--ink-3); }
.cr-count.full { color: var(--amber); }
.cr-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.cr-chips:empty { display: none; }
.cr-chip {
  display: inline-flex; align-items: center; gap: 4px; max-width: 100%;
  padding: 5px 6px 5px 11px; border-radius: 999px;
  background: var(--ink); color: #101013; font-size: 12.5px; font-weight: 600;
  animation: cr-pop 180ms var(--ease) backwards;
}
.cr-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cr-chip button {
  width: 18px; height: 18px; border: 0; border-radius: 50%; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, .12); color: inherit; cursor: pointer;
}
.cr-chip button:hover { background: rgba(0, 0, 0, .28); }
.cr-chip button .ico { width: 11px; height: 11px; stroke: #101013; stroke-width: 2.4; }
.cr-chip button:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
@keyframes cr-pop { from { opacity: 0; transform: scale(.9); } }
.cr-inrow { display: flex; gap: 8px; }
.cr-inrow input {
  flex: 1; min-width: 0; height: var(--ctl); padding: 0 13px; border-radius: 12px;
  border: 0; box-shadow: inset 0 0 0 1px var(--hair-2);
  background: rgba(0, 0, 0, .22); color: var(--ink); font: 14px var(--sans);
}
.cr-inrow input::placeholder { color: var(--ink-3); }
.cr-inrow input:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
.cr-inrow input[aria-invalid="true"] { box-shadow: inset 0 0 0 1px rgba(255, 36, 66, .6); }
.cr-hint { margin: 0; font-size: 12px; line-height: 1.45; color: var(--ink-3); }
.cr-hint b { color: var(--ink-2); font-weight: 600; }
.cr-hint.warn { color: var(--amber); }

/* the Discord row: a switch, and the words that say why it is off */
.cr-discord {
  border-radius: var(--r-md); padding: 12px 14px;
  box-shadow: inset 0 0 0 1px var(--hair);
  display: flex; flex-direction: column; gap: 10px;
  transition: box-shadow var(--t-fast), background var(--t-fast);
}
.cr-discord[data-state="ready"] { box-shadow: inset 0 0 0 1px rgba(88, 101, 242, .45); background: rgba(88, 101, 242, .07); }
.cr-discord[data-state="loading"] .cr-dsub { color: var(--ink-3); }
.cr-drow { display: flex; align-items: center; gap: 12px; }
.cr-dtext { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.cr-dtitle { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; color: var(--ink); }
.cr-dtitle .d-logo { width: 18px; height: 14px; fill: var(--blurple); stroke: none; }
.cr-dsub { font-size: 12.5px; line-height: 1.4; color: var(--ink-2); text-wrap: pretty; }
.cr-dsub b, .cr-dsub code { color: var(--ink); }
.cr-dsub code { font-family: var(--mono); font-size: .92em; padding: 1px 5px; border-radius: 5px; background: rgba(255, 255, 255, .07); box-shadow: inset 0 0 0 1px var(--hair); }
.cr-switch {
  flex: none; position: relative; width: 42px; height: 24px; border-radius: 999px;
  border: 0; padding: 0; cursor: pointer;
  background: rgba(255, 255, 255, .14); box-shadow: inset 0 0 0 1px var(--hair-2);
  transition: background var(--t-fast), box-shadow var(--t-fast), opacity var(--t-fast);
  -webkit-tap-highlight-color: transparent;
}
.cr-knob {
  position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .5);
  transition: transform var(--t-fast) var(--ease);
}
.cr-switch[aria-checked="true"] { background: var(--blurple); box-shadow: none; }
.cr-switch[aria-checked="true"] .cr-knob { transform: translateX(18px); }
/* disabled: still a target (pressing it opens the setup), but it reads
   as off-limits and never moves */
.cr-switch[aria-disabled="true"] { opacity: .45; cursor: pointer; }
.cr-switch[aria-disabled="true"] .cr-knob { background: rgba(255, 255, 255, .7); }
.cr-switch:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.cr-dact { display: flex; flex-wrap: wrap; gap: 8px; }
.cr-invite { background: var(--blurple); color: #fff; box-shadow: none; }
.cr-invite:hover { background: var(--blurple-hi); }
.cr-invite .ico { stroke: none; fill: #fff; width: 15px; height: 15px; }
.cr-invite[aria-disabled="true"] { pointer-events: none; }

/* the crawler's pulse: one line, the state words the deck uses */
.cr-pulse { margin: 0; min-height: 18px; font-size: 12.5px; line-height: 1.45; color: var(--ink-3); display: flex; align-items: center; gap: 8px; }
.cr-pulse::before { content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none; background: rgba(255, 255, 255, .28); }
.cr-pulse[data-key="run"]::before { background: #7dff9a; box-shadow: 0 0 8px rgba(125, 255, 154, .7); }
.cr-pulse[data-key="run"] { color: var(--ink-2); }
.cr-pulse[data-key="off"]::before { background: var(--amber); }
.cr-pulse[data-key="off"] { color: var(--amber); }
/* in line: a ring, not a lamp -- a place is not a state */
.cr-pulse[data-key="wait"]::before { background: transparent; box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .7); }
.cr-pulse[data-key="wait"] { color: var(--ink-2); }
.cr-pulse:empty { display: none; }
.cr-err { margin: 0; font-size: 12.5px; line-height: 1.5; color: #ffb3be; }
.cr-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 2px; }
.cr-foot #cr-cancel { margin-right: auto; }
.cr-go { min-width: 128px; background: var(--red); color: #fff; }
.cr-go:hover { background: #ff3d58; }
.cr-go.arming { animation: cr-arm .4s linear; }
@keyframes cr-arm { from { opacity: .35; } }

#crawl.in .backdrop { animation: si-fade var(--t-med) var(--ease) backwards; }
#crawl.in .cr-card  { animation: si-rise 320ms var(--ease) backwards; }
@media (max-width: 480px) {
  .cr-card { padding: 26px 20px calc(18px + var(--sill-h)); border-radius: 22px; }
  .cr-title { font-size: 23px; }
  .cr-foot { flex-wrap: wrap; }
}
@media (max-height: 600px) { .cr-mark, .cr-sub { display: none; } }
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .cr-card { background: #101014; border-color: var(--hair); -webkit-backdrop-filter: none; backdrop-filter: none; }
  .cr-card::before { display: none; }
}


/* "Or pick for me" — the crawl modal's second way to answer "what should
   it look for". A quiet link, not a switch: it modifies the run rather
   than replacing the searches above it, and a control that looked like a
   toggle would imply the two were alternatives. */
.cr-or { margin: 8px 0 0; }
.cr-or .linky { font-size: 12.5px; }
.cr-or .linky.on { color: var(--text); box-shadow: inset 0 -1px 0 var(--red); }
.cr-hint.dim { opacity: .55; }
