/* vi.ki: system fonts only, zero third-party requests before intent.
 *
 * Ported from heartipedia's stylesheet: the shell, the header, the rails, the dock and the
 * article typography are that site's, restyled for a video wiki. Dark is the default for
 * everyone: the bare :root block IS the dark palette, so a reader with no JavaScript, no stored
 * choice and a light operating system still gets the dark page. There is deliberately no
 * prefers-color-scheme rule; the avatar menu is how a reader overrides it (theme.js stamps
 * data-theme on <html> before first paint).
 */
:root {
  /* The page is pure black (George, 16 September 2026). */
  --ink: #e7e9ec; --muted: #a2a9b3; --line: #2c313a; --bg: #000; --tint: #1b1e24;
  --link: #7fb2ff; --on-link: #10131a; --strong: navajowhite;
  /* The accent is the warm pair of a site about footage: a video red for what is happening
     (playing, pending, deleted) and an amber for a place in time. Both read on both grounds. */
  --accent: #ff6b57; --moment: #ffb057;
  --ok: #7fd39a;
  --shadow: 0 10px 30px rgba(0, 0, 0, .55);
  --max: 44rem;     /* the reading column: unchanged, whatever else is beside it */
  --shell: var(--max);
  color-scheme: dark;
}
:root[data-theme="light"] {
  --ink: #1b1b1b; --muted: #5a5a5a; --line: #e2e2e2; --bg: #fff; --tint: #f6f6f4;
  --link: #0b57d0; --on-link: #fff; --strong: #8b4513;
  --accent: #d63a22; --moment: #b05a00;
  --ok: #1f7a3e;
  --shadow: 0 8px 24px rgba(0, 0, 0, .12);
  color-scheme: light;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 17px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
/* Links carry their color, not a permanent rule under them; the underline arrives on hover and
   on keyboard focus. Readers who ask their browser for more contrast get it back in prose. */
a { color: var(--link); text-decoration: none; }
a:hover, a:focus-visible { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
@media (prefers-contrast: more) {
  .body a, footer.site a { text-decoration: underline; }
}
:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }

/* --- themes ---------------------------------------------------------------------------------
   Two themes, chosen from the avatar menu and independent of light/dark:

     2 (the default)  bold text keeps the body weight and is told apart by color alone
     1                bold text is heavier, the way a browser renders <strong> by default

   Theme 2 is the bare rule and theme 1 is the attribute, for the same reason dark is the bare
   palette: the default has to be what a reader gets before any script runs. */
strong, b { font-weight: inherit; color: var(--strong); }
:root[data-variant="1"] strong, :root[data-variant="1"] b { font-weight: bolder; color: inherit; }
/* The name in the account menu is chrome, not prose: it is bold because it is a label. */
.account-who strong { font-weight: 600; color: var(--ink); }

/* The setting lists in the avatar menu. */
.account-themes { margin: .45rem 0 0; padding-top: .3rem; }
.account-themes-h { margin: 0; padding: .3rem .6rem .15rem; font-size: .74rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.account-menu button[role="menuitemradio"] {
  display: flex; width: 100%; gap: .45rem; align-items: baseline; text-align: left;
  padding: .4rem .6rem; border: 0; border-radius: 6px; background: none; font: inherit;
  color: var(--ink); cursor: pointer;
}
.account-menu button[role="menuitemradio"]:hover, .account-menu button[role="menuitemradio"]:focus { background: var(--tint); }
.account-menu button[role="menuitemradio"] .tick { flex: none; width: .9rem; color: var(--link); }
.account-menu button[aria-checked="false"] .tick { visibility: hidden; }
.account-menu .theme-hint { color: var(--muted); font-size: .8rem; }
.skip { position: absolute; left: -999px; top: 0; }
.skip:focus { left: 8px; top: 8px; background: var(--bg); color: var(--ink); padding: 6px 10px; z-index: 10; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }

/* --- shell ---------------------------------------------------------------------------------
   Every page is one 44rem column. An article is that column plus a sidebar, so its shell is
   wider; body.wide is what says so, and the header and footer widen with it rather than sitting
   narrow above a wide page. */
body.wide { --shell: 66rem; }
main, footer.site { max-width: var(--shell); margin: 0 auto; padding: 0 1rem; }
/* The header stays: the search box and the way home are the two things a reader wants at any
   depth of a long page. --header-h is what everything that scrolls to an anchor has to clear. */
:root { --header-h: 4.15rem; }
/* The header is the window's width on every page, so nothing in it moves from one page to the
   next (George, Oct 8, 2026: "/tools and /guides have different width headers than /pages which
   causes content shift when clicking from Tools to Pages"). It used to sit in the page's shell,
   which is 44rem on a list, 66rem on an article and the whole window beside a contents rail, so
   its links jumped as a reader moved between them. The middle track holds the search box and
   takes what is left. */
header.site {
  position: sticky; top: 0; z-index: 30; background: var(--bg);
  margin: 0 auto;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 1rem;
  padding: .9rem 1rem;
}
header.site .brand { justify-self: start; }
header.site nav { justify-self: end; }
header.site form.search { justify-self: center; width: min(100%, 600px); max-width: none; }
/* Anything an in-page link jumps to stops below the header rather than under it. */
[id] { scroll-margin-top: calc(var(--header-h) + .6rem); }

/* --- the brand row -----------------------------------------------------------------------------
   On the hub: the mark and the word "Vi.ki". On a viki: the viki's name is the brand, and a
   small More menu beside it is the way to the other vikis. The name is the bigger thing because
   it is where the reader is; the menu is chrome. */
header.site .brand {
  display: inline-flex; align-items: baseline; gap: .5rem; min-width: 0;
  font-weight: 600; color: var(--ink); font-size: 1.15rem; letter-spacing: -.005em;
}
/* The "vi" badge is 120 x 84, so it is wider than tall (Oct 14, 2026). */
header.site .brand-mark { width: 1.93rem; height: 1.35rem; display: block; flex: none; align-self: center; }
/* A viki's game icon, the size of the hub's mark (George, Oct 14, 2026). */
header.site .brand-icon { width: 1.35rem; height: 1.35rem; display: block; flex: none; align-self: center; border-radius: 5px; object-fit: cover; }
header.site .brand:hover { text-decoration: none; color: var(--link); }
header.site .brand > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The Vikis menu, drawn as a pill: the other vikis, and all of them on the hub (George, 16
   September 2026). Since Oct 2, 2026 it is the header's first child, in the top-left corner left
   of the viki's name, and the grid grows a track for it. */
/* ...and since Oct 17, 2026 the logo itself is the menu (layout.html, THE LOGO IS THE MENU): the summary is the brand,
   with a down arrow after it saying it opens. */
header.site > .nav-more { justify-self: start; position: relative; min-width: 0; max-width: 100%; }
header.site .nav-more > summary { list-style: none; cursor: pointer; white-space: nowrap; align-items: center; }
header.site .nav-more > summary::-webkit-details-marker { display: none; }
header.site .nav-more > summary::after {
  content: ""; flex: none; align-self: center; width: .42rem; height: .42rem; margin: -.25rem .1rem 0 .05rem;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(45deg);
  color: var(--muted); transition: transform .15s ease;
}
header.site .nav-more[open] > summary::after { transform: translateY(.2rem) rotate(225deg); }
header.site .nav-more > summary:hover, header.site .nav-more > summary:focus-visible,
header.site .nav-more[open] > summary { color: var(--link); outline: none; }
header.site .nav-more > summary:hover::after, header.site .nav-more[open] > summary::after { color: var(--link); }
/* Start your own viki, beside the hub's logo. */
header.site > .nav-start { justify-self: start; white-space: nowrap; color: var(--ink); font-size: .95rem; }
header.site > .nav-start:hover { color: var(--link); text-decoration: none; }
header.site:has(> .nav-start) { grid-template-columns: auto auto minmax(0, 1fr) auto; }
header.site nav { display: flex; gap: .8rem; align-items: center; font-size: .95rem; }
header.site nav > a { color: var(--ink); }
header.site nav > a:hover { color: var(--link); text-decoration: none; }
header.site nav > a[aria-current="page"] { color: var(--link); }

header.site .nav-more-menu {
  position: absolute; left: 0; top: calc(100% + .5rem); z-index: 20;
  min-width: 14rem; max-width: min(22rem, calc(100vw - 1.5rem));
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: .35rem;
  box-shadow: var(--shadow); text-align: left;
}
header.site .nav-more-menu a {
  display: block; padding: .45rem .6rem; border-radius: 6px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
header.site .nav-more-menu a:hover, header.site .nav-more-menu a:focus {
  background: var(--tint); text-decoration: none;
}
/* "All vikis" heads the menu and stands out (George, Oct 14, 2026: "move "All vikis" to the top and make it
   stand out"): bold, in the link color, on a tinted ground. Then "Start your own viki" and a line. */
/* ...no ground since Oct 17, 2026 (George, Oct 17, 2026: "at vi.ki rename "+ Start your own viki" to "Start your own viki" in each game viki dropdown move "Home" to be inline with the text, add a nice Home icon, add "All pages" below "Home" with an appropriate icon, remove the shading behind "More vikis""), the link color and weight still. */
header.site .nav-more-menu .nav-more-all { color: var(--link); font-weight: 600; }
/* Home, the viki's own front page, heads a viki's menu (Oct 17, 2026). */
header.site .nav-more-menu .nav-more-home { font-weight: 600; }
/* ...and "Start your own viki" looks different from it (George, Oct 14, 2026: "make "All vikis" look different from
   "Start your own viki" in the more menu"): plain text in the ink color, after a plus, no ground. */
header.site .nav-more-menu .nav-more-start { color: var(--ink); font-weight: 400; }
/* No "+" since Oct 17, 2026: its words stand where the others' do, after an icon's room. */
header.site .nav-more-menu .nav-more-start { padding-left: calc(.6rem + 20px + .5rem); }
/* Home's and All pages' icons, drawn in lines in the text's color (Oct 17, 2026). */
header.site .nav-more-menu .nav-more-glyph { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; border-radius: 0; }
/* Each game's official icon before its name (George, Oct 14, 2026). */
header.site .nav-more-menu .nav-more-icon { width: 20px; height: 20px; margin-right: .5rem; vertical-align: -5px; border-radius: 4px; object-fit: cover; }
header.site .nav-more-menu .nav-more-sep { border: 0; border-top: 1px solid var(--line); margin: .35rem 0; }

/* --- the launch countdown ----------------------------------------------------------------------
   A band under the header on a viki whose viki.json declares one (George, 16 September 2026,
   after Blizzard's World of Warcraft: Forever page): the line on the left, the clock on the
   right, the two kept together in the middle so a wide window does not push them to opposite
   edges (George, 16 September 2026). It is dark in both themes, like the pages it copies, and
   scrolls away with the page. A countdown with a count-up line keeps its band after the launch,
   the clock then counting up from it (George, Oct 1, 2026).
   The color scheme is the countdown's "style". */
.countdown {
  --cd-bg: linear-gradient(120deg, #15161c, #22232c 55%, #15161c);
  --cd-edge: #3a3b46; --cd-ink: #eceef2; --cd-digits: #ffffff;
  background: var(--cd-bg); color: var(--cd-ink);
  border-top: 3px solid var(--cd-edge); border-bottom: 3px solid var(--cd-edge);
  /* Above the video dock (z-index 5): the page-colored shield the dock draws above itself
     reaches up over the banner and hid the clock. The dock only sticks once the banner has
     scrolled under the header (z-index 30), so the two never meet on screen. */
  position: relative; z-index: 6;
}
.countdown-teal {
  --cd-bg: radial-gradient(ellipse at 50% 45%, #17647a, transparent 60%), linear-gradient(120deg, #0b3947, #0f4f63 55%, #0b3947);
  --cd-edge: #c4a877; --cd-ink: #f6eedd; --cd-digits: #fbf4e4;
}
.countdown-ember {
  --cd-bg: radial-gradient(ellipse at 50% 50%, #5a140c, transparent 65%), linear-gradient(120deg, #120505, #2a0906 55%, #120505);
  --cd-edge: #6e1b12; --cd-ink: #eadcc8; --cd-digits: #f3d9b1;
}
.countdown-sky {
  --cd-bg: radial-gradient(ellipse at 50% 45%, #2a5d98, transparent 62%), linear-gradient(120deg, #0a1830, #122d55 55%, #0a1830);
  --cd-edge: #9cc1ea; --cd-ink: #e9f1fb; --cd-digits: #ffffff;
}
/* A Vice City sunset for Grand Theft Auto VI: violet sky, a low glow of the sun, a pink edge. */
.countdown-vice {
  --cd-bg: radial-gradient(ellipse 70% 90% at 50% 120%, rgba(255, 120, 90, .38), transparent 70%), linear-gradient(120deg, #240a36, #561660 55%, #240a36);
  --cd-edge: #ff5fa8; --cd-ink: #fbeaf4; --cd-digits: #ffffff;
}
/* Deep northern water for Valheim: a fjord at dusk under a pale sky light, a birch-bark edge. */
.countdown-fjord {
  --cd-bg: radial-gradient(ellipse at 50% 40%, #2f5a57, transparent 62%), linear-gradient(120deg, #0b1a1c, #163434 55%, #0b1a1c);
  --cd-edge: #c9b99a; --cd-ink: #eef0e6; --cd-digits: #ffffff;
}
.countdown-in {
  /* Never laid out narrower than the wide shell: on a page in the narrow reading column (search,
     changes) the words and the clock would otherwise stack when the window has room for both on
     one line (George, 17 September 2026). */
  max-width: max(var(--shell), 66rem); margin: 0 auto; padding: 1rem;
  display: flex; align-items: center; justify-content: center; gap: .8rem 2rem; flex-wrap: wrap;
}
.countdown-text { margin: 0; max-width: 32rem; line-height: 1.25; }
.countdown-text strong { display: block; font-size: 1.45rem; font-weight: 600; letter-spacing: -.005em; text-wrap: balance; }
.countdown-note { display: block; margin-top: .3rem; font-size: .85rem; opacity: .82; }
.countdown-clock { margin: 0; display: flex; align-items: flex-start; gap: .35rem; font-variant-numeric: tabular-nums; }
.countdown .cd { display: flex; flex-direction: column; align-items: center; min-width: 3.9rem; }
.countdown .cd b {
  font-size: 2.9rem; font-weight: 500; line-height: 1; letter-spacing: .02em; color: var(--cd-digits);
  text-shadow: 0 1px 0 rgb(0 0 0 / .35);
}
.countdown .cd small { margin-top: .4rem; font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; }
.countdown .cd-sep { font-size: 1.7rem; line-height: 1.7; opacity: .55; }
/* The X closes the launch clock and Settings brings it back (George, Oct 1, 2026). theme.js sets
   data-clock before the first paint, so a closed clock never flashes in. Only the banner has one:
   a card on the hub's Upcoming shelf is content, not chrome. */
.countdown-close {
  position: absolute; top: .45rem; right: .5rem; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; padding: 0; border: 0; border-radius: 999px;
  background: none; color: var(--cd-ink); opacity: .7; cursor: pointer;
}
.countdown-close:hover, .countdown-close:focus-visible { opacity: 1; background: rgb(255 255 255 / .14); }
.countdown-close svg { display: block; }
:root[data-clock="hidden"] .countdown:not(.countdown-card) { display: none; }
@media (max-width: 600px) {
  /* Room for the X on both sides, so the centered words never run under it. */
  .countdown-in { padding: .8rem 2.6rem; justify-content: center; text-align: center; }
  .countdown-close { top: .3rem; right: .3rem; }
  .countdown-text { max-width: none; }
  .countdown-text strong { font-size: 1.15rem; }
  .countdown-clock { margin: 0 auto; }
  .countdown .cd { min-width: 3rem; }
  .countdown .cd b { font-size: 2.1rem; }
  .countdown .cd-sep { font-size: 1.3rem; line-height: 1.5; }
}

/* --- header search -------------------------------------------------------------------------
   A plain GET form to /search: it works with no JavaScript, and the results page is the same
   page a bookmark or a shared link opens. */
form.search { display: flex; min-width: 0; max-width: 24rem; }
form.search input[type="search"] {
  flex: 1 1 auto; min-width: 0; font: inherit; font-size: .95rem; padding: .35rem .7rem;
  border: 1px solid var(--line); border-radius: 999px; background: var(--tint); color: var(--ink);
  -webkit-appearance: none; appearance: none;
}
form.search input[type="search"]::placeholder { color: var(--muted); }
form.search input[type="search"]:focus { border-color: var(--link); outline: none; box-shadow: 0 0 0 2px color-mix(in srgb, var(--link) 35%, transparent); }
/* The submit button is for keyboards: off-screen until focused, never a dead pixel in the header. */
form.search button { position: absolute; left: -999px; }
form.search button:focus { position: static; margin-left: .4rem; }

/* --- the video button ------------------------------------------------------------------------------
   One toggle in the header, only on a page with a dock: the video shown or hidden. It is chrome,
   not a control on the video: the player is YouTube's own and nothing is drawn over it.
   aria-pressed carries the state; hidden, the screen in the icon is struck through. */
.video-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; padding: 0;
  background: none; border: 1px solid transparent; border-radius: 999px;
  color: var(--muted); cursor: pointer;
}
.video-toggle:hover, .video-toggle:focus-visible { color: var(--link); border-color: var(--line); }
.video-toggle svg { display: block; }
.video-toggle-slot { display: inline-block; flex: none; width: 2.25rem; height: 2.25rem; }
.video-toggle .toggle-slash { opacity: 0; }
.video-toggle[aria-pressed="false"] .toggle-slash { opacity: 1; }

/* --- Settings ------------------------------------------------------------------------------------
   The gear left of Sign in or the avatar, for every reader, and every setting in its menu
   (George, Oct 1, 2026). The menu is the avatar menu's card; it scrolls when it is taller than
   the window. */
.settings-box { position: relative; display: inline-flex; }
.settings-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; padding: 0;
  background: none; border: 1px solid transparent; border-radius: 999px;
  color: var(--muted); cursor: pointer;
}
.settings-btn:hover, .settings-btn:focus-visible, .settings-btn[aria-expanded="true"] { color: var(--link); border-color: var(--line); }
.settings-btn svg { display: block; }
.settings-menu { max-height: calc(100vh - var(--header-h, 3.5rem) - 1rem); overflow-y: auto; overscroll-behavior: contain; }
/* Heart Photos' settings surface, in the dark theme (George, Oct 17, 2026: "on all vi.ki sites use the same background color for the settings dropdown as we use on heart.photos and use "Reset defaults" as the phrase everywhere not "Restore defaults""): its gear menu's ground, edge and shadow
   (he.art photoweb/gallery.css .gear-menu), lifted off the black page; a row lit under the pointer is lighter still. */
/* ...and the avatar menu and the logo's menu with it (George, 17 October 2026: "on all vi.ki sites use the same background color for the dropdown from the logo and the avatar menu as for the settings dropdown and on heart.photos avatar dropdown and viewer info sidebar"). */
:root:not([data-theme="light"]) .account-menu, :root:not([data-theme="light"]) header.site .nav-more-menu {
  background: rgb(36 36 44 / .98); border-color: rgb(255 255 255 / .26); border-radius: 12px; box-shadow: 0 14px 40px rgb(0 0 0 / .7);
}
:root:not([data-theme="light"]) .account-menu button[role="menuitemradio"]:hover,
:root:not([data-theme="light"]) .account-menu button[role="menuitemradio"]:focus,
:root:not([data-theme="light"]) .account-menu a[role="menuitem"]:hover,
:root:not([data-theme="light"]) .account-menu button[role="menuitem"]:not(.settings-reset):hover,
:root:not([data-theme="light"]) header.site .nav-more-menu a:hover,
:root:not([data-theme="light"]) header.site .nav-more-menu a:focus { background: rgb(255 255 255 / .08); }
/* "Reset defaults", as on Heart Photos: a pill at the foot of the menu, gray rather than gone
   when there is nothing to put back (George, Oct 1, 2026). */
.account-menu button.settings-reset[role="menuitem"] {
  display: flex; align-items: center; width: fit-content; margin: .7rem auto .3rem;
  padding: .45rem 1rem; border-radius: 999px; font: inherit; font-size: .85rem; cursor: pointer;
  background: color-mix(in srgb, var(--link) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--link) 55%, transparent); color: var(--ink);
}
.account-menu button.settings-reset[role="menuitem"]:hover:not(:disabled), .account-menu button.settings-reset[role="menuitem"]:focus-visible:not(:disabled) {
  background: color-mix(in srgb, var(--link) 26%, transparent); border-color: var(--link);
}
.account-menu button.settings-reset[role="menuitem"]:disabled { opacity: .38; cursor: default; }
/* The tip after the first hide of the launch clock (George, Oct 1, 2026), under the gear, its
   arrow on the gear's middle (--tip-arrow, account.js). Fixed, because the header it points into
   stays at the top while the page scrolls. */
.settings-tip {
  position: fixed; z-index: 40; width: max-content; max-width: min(17rem, calc(100vw - 1rem));
  display: flex; align-items: flex-start; gap: .4rem;
  padding: .55rem .4rem .55rem .8rem; border-radius: 8px;
  background: var(--bg); color: var(--ink); box-shadow: var(--shadow);
  border: 1px solid color-mix(in srgb, var(--link) 55%, var(--line));
  font-size: .9rem; line-height: 1.35;
}
.settings-tip::before {
  content: ""; position: absolute; top: -7px; left: calc(var(--tip-arrow, 50%) - 6px);
  width: 12px; height: 12px; transform: rotate(45deg); background: var(--bg);
  border-left: 1px solid color-mix(in srgb, var(--link) 55%, var(--line));
  border-top: 1px solid color-mix(in srgb, var(--link) 55%, var(--line));
}
.settings-tip p { margin: .1rem 0 0; }
.settings-tip-close {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; padding: 0; border: 0; border-radius: 999px;
  background: none; color: var(--muted); cursor: pointer;
}
.settings-tip-close:hover, .settings-tip-close:focus-visible { color: var(--ink); background: var(--tint); }
.settings-tip-close svg { display: block; }

/* The sign-in button: an outlined pill with a face on it, the shape heart.photos uses. */
.signin-link {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  padding: .4rem .85rem .4rem .65rem; border-radius: 999px; font-size: .9rem; font-weight: 500;
  border: 1px solid color-mix(in srgb, var(--link) 45%, transparent); color: var(--link);
  background: transparent; cursor: pointer;
}
.signin-link:hover {
  text-decoration: none;
  border-color: color-mix(in srgb, var(--link) 75%, transparent);
  background: color-mix(in srgb, var(--link) 12%, transparent);
}
.signin-link svg { display: block; flex: none; }
/* Signed in, before /api/me has answered (theme.js sets <html data-in> from the viki_in cookie):
   never Sign in, even for a moment (George, Oct 8, 2026), and the avatar's place is held, with
   the reader's initial when this browser knows it, so nothing moves when the avatar arrives. */
html[data-in] .account .signin-link { display: none; }
html[data-in] .account:not(:has(.account-box))::before {
  content: var(--me-initial, ""); display: inline-flex; width: 32px; height: 32px; padding: 2px;
  border: 1px solid var(--line); border-radius: 50%; background: var(--tint) content-box;
  align-items: center; justify-content: center; font-size: .95rem; line-height: 1; font-weight: 600;
}

.account img.avatar { border-radius: 50%; vertical-align: middle; margin-right: .35rem; }
.account button { background: none; border: 1px solid var(--line); border-radius: 6px; padding: .3rem .6rem; font: inherit; cursor: pointer; color: var(--ink); }

/* The avatar menu: a button in the header, a card under it. */
.account-box { position: relative; }
.account .avatar-btn { padding: 2px; border-radius: 50%; border: 1px solid var(--line); line-height: 0; }
.account .avatar-btn:hover { border-color: var(--link); }
.account img.avatar, .account .avatar-initial { margin: 0; }
.account .avatar-initial { display: inline-flex; width: 32px; height: 32px; border-radius: 50%; align-items: center; justify-content: center; background: var(--tint); font-size: .95rem; line-height: 1; font-weight: 600; }
.account-menu {
  position: absolute; right: 0; top: calc(100% + .4rem); z-index: 20; min-width: 15rem;
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: .35rem;
  box-shadow: var(--shadow); text-align: left;
}
.account-who { padding: .5rem .6rem; margin-bottom: .45rem; display: flex; flex-direction: column; gap: .1rem; }
.account-who strong { font-size: .95rem; }
.account-email { font-size: .85rem; color: var(--muted); }
.account-via { font-size: .78rem; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.account-menu a[role="menuitem"], .account-menu button[role="menuitem"] {
  display: block; width: 100%; text-align: left; padding: .45rem .6rem; border: 0; border-radius: 6px;
  background: none; font: inherit; color: var(--ink); text-decoration: none; cursor: pointer;
}
.account-menu a[role="menuitem"]:hover, .account-menu button[role="menuitem"]:hover,
.account-menu a[role="menuitem"]:focus, .account-menu button[role="menuitem"]:focus { background: var(--tint); text-decoration: none; }
/* The other accounts on this browser (stage 3 of accounts.he.art, as on Heartipedia): a 24px face,
   then the name and the email on a line each, each ending in an ellipsis rather than wrapping. The
   button is a flex row so the text column can shrink (min-width: 0) inside the capped menu. */
.account-menu .account-other { margin: 0; }
.account-menu .account-other button[role="menuitem"] { display: flex; align-items: center; gap: .55rem; }
.account-other-avatar { flex: none; width: 24px; height: 24px; border-radius: 50%; position: relative; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; background: var(--tint); font-size: .75rem; font-weight: 600; line-height: 1; }
.account-other-avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.account-other-text { min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
.account-other-text span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-other-email { font-size: .82rem; color: var(--muted); }
.account-menu .account-add { margin-bottom: .25rem; }
.account-menu .account-site { margin-top: .2rem; padding-top: .55rem; border-top: 1px solid var(--line); border-radius: 0 0 6px 6px; }
.account-menu .account-signout { margin-top: .45rem; }
.account-notice { margin: .3rem .6rem 0; font-size: .8rem; color: var(--accent); }
/* George, Oct 17, 2026: "Don't wrap "Sign out"". The menu is as wide as its longest item, no item
   wraps, and a long email is cut short with an ellipsis instead of widening or breaking it. */
.account-menu a[role="menuitem"], .account-menu button[role="menuitem"] { white-space: nowrap; }
.account-box .account-menu { width: max-content; max-width: min(22rem, calc(100vw - 1.5rem)); }
.account-who { min-width: 0; }
.account-who strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

main { --main-top: 1.5rem; padding-top: var(--main-top); padding-bottom: 3rem; }
footer.site { padding-top: 2.2rem; padding-bottom: 2rem; color: var(--muted); font-size: .85rem; }
footer.site .version { color: var(--muted); }
h1 { font-size: 2rem; line-height: 1.2; margin: .2rem 0 .6rem; text-wrap: balance; font-weight: 600; letter-spacing: -.01em; }
h2 { font-size: 1.35rem; margin: 2rem 0 .6rem; font-weight: 600; }
h3 { font-size: 1.1rem; margin: 1.4rem 0 .4rem; font-weight: 600; }
/* clear: the contents dropdown floats so the TITLE sets beside it, and only the title. */
p.lead, p.summary { font-size: 1.1rem; color: var(--muted); margin-top: 0; clear: left; }
/* "Browse all N pages": a filled pill at the top of the body, the one control on the page that
   is the link color as a background, so it is the first thing that reads as "go here", with the
   categories beside it as outlined pills. On an article the same bar is one size smaller. */
.all-pages-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem .5rem; margin: .2rem 0 1.4rem; clear: left; }
.all-pages-btn { display: inline-flex; align-items: center; gap: .45rem; padding: .55rem 1.05rem; border-radius: 999px;
  background: var(--link); color: var(--on-link); font-weight: 650; line-height: 1.2; }
.all-pages-btn::after { content: "\2192"; }
.all-pages-btn:hover, .all-pages-btn:focus-visible { text-decoration: none; filter: brightness(1.1); }
.all-pages-cats { display: contents; }
.all-pages-cats a { padding: .3rem .7rem; border: 1px solid var(--line); border-radius: 999px; font-size: .88rem; color: var(--ink); }
.all-pages-cats a:hover, .all-pages-cats a:focus-visible { border-color: var(--link); color: var(--link); text-decoration: none; }
.all-pages-bar.compact { margin: -.2rem 0 1.1rem; }
.all-pages-bar.compact .all-pages-btn { padding: .32rem .8rem; font-size: .9rem; }
.all-pages-bar.compact .all-pages-cats a { font-size: .85rem; }
.notice { background: var(--tint); border-left: 3px solid var(--accent); padding: .6rem .8rem; }
.notice.ok { border-left-color: var(--ok); }
.small { font-size: .85rem; color: var(--muted); }
.muted { color: var(--muted); }
.button { display: inline-block; border: 1px solid var(--link); border-radius: 6px; padding: .45rem .9rem; font: inherit; color: var(--link); background: none; cursor: pointer; }
.button:hover { text-decoration: none; background: var(--tint); }
.button.primary { background: var(--link); color: var(--on-link); }
.button.primary:hover { background: color-mix(in srgb, var(--link) 85%, var(--ink)); }
.button.danger { border-color: var(--accent); color: var(--accent); }
.button[disabled] { opacity: .6; cursor: progress; }
button { font: inherit; }
.empty { color: var(--muted); }

/* --- forms -------------------------------------------------------------------------------------
   Every form on the site (the editor's fields, the admin's settings, the review notes, the
   create-a-viki form) is plain and readable: a label over a field, fields the width of the
   column, one accent for the primary action. `form.stack` stacks; `.field` is one row of it. */
form.stack { display: flex; flex-direction: column; gap: .9rem; margin: 1rem 0 1.5rem; }
.field { display: flex; flex-direction: column; gap: .3rem; }
.field label, form.stack > label { font-weight: 600; font-size: .95rem; }
.field .hint, .field .optional { font-weight: 400; color: var(--muted); font-size: .85rem; }
input[type="text"], input[type="search"], input[type="url"], input[type="email"], input[type="number"],
textarea, select {
  font: inherit; color: var(--ink); background: var(--tint);
  border: 1px solid var(--line); border-radius: 8px; padding: .5rem .7rem;
  max-width: 100%;
}
form.stack input[type="text"], form.stack input[type="url"], form.stack input[type="email"],
form.stack textarea, form.stack select, .field input, .field textarea, .field select { width: 100%; }
input:focus, textarea:focus, select:focus { border-color: var(--link); outline: none; box-shadow: 0 0 0 2px color-mix(in srgb, var(--link) 30%, transparent); }
textarea { min-height: 6rem; resize: vertical; }
.actions, .form-actions { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-top: .4rem; }
form.inline { display: inline; }
form.inline button { padding: .3rem .8rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; padding: .6rem 1rem 1rem; margin: 0 0 1rem; }
legend { padding: 0 .4rem; font-weight: 600; }

/* --- article + sidebar ---------------------------------------------------------------------
   The reading column keeps its width; the sidebar holds the video. One column is the default
   (a phone is not a special case) and the second column arrives at 60rem. Every two-column rule
   lives inside that block. */
:root { --toc-w: 14rem; }

.with-aside { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.75rem; align-items: start; }
/* .rails is the column beside the article. Below the three-column breakpoint it IS that column,
   one box; above it the wrapper dissolves with display: contents and its children become grid
   items in tracks of their own. */
.rails { min-width: 0; display: flex; flex-direction: column; gap: 1.25rem; }
.page-main { min-width: 0; }
@media (min-width: 60.01rem) {
  .with-aside { grid-template-columns: minmax(0, var(--max)) minmax(0, 19rem); gap: 2.5rem; }
  /* With a video docked the sidebar has to hold a 480px player (the floor the YouTube API terms
     set), so its track widens and the shell with it. */
  body.has-dock.dock-side .with-aside { grid-template-columns: minmax(0, var(--max)) 33.5rem; }
  body.has-dock.dock-side.wide { --shell: 82rem; }
}
.related { font-size: .93rem; padding-top: 1.25rem; }
@media (min-width: 60.01rem) {
  /* One scrollbar on the page and no other. The rail scrolls with the page and only the video
     sticks: a reader who is watching wants it in view. */
  .rails { align-self: stretch; padding-left: 1.1rem; }
  .rails > .related { flex: 1 1 auto; }
  /* Two columns: the contents sit ABOVE the article, never beside the video and never below it.
     account.js collapses them to a dropdown beside the title. */
  body.has-toc .with-aside { grid-template-rows: auto 1fr; }
  body.has-toc .toc { grid-column: 1; grid-row: 1; }
  body.has-toc .page-main { grid-column: 1; grid-row: 2; }
  body.has-toc .rails { grid-column: 2; grid-row: 1 / span 2; }
  .related { padding-top: 0; }
  /* The dock sticks inside .related, so .related has to be as tall as the row. It sticks flush
     under the header, and on arrival it starts flush under the header (or the countdown), above
     main's own top padding: YouTube's frame already has a band above the picture (George,
     16 September 2026). --hb-top is the header's measured height (moments.js). */
  .related > .hb-dock { position: sticky; top: var(--hb-top, var(--header-h)); z-index: 5; }
  /* The gap between the header and a stuck video is a window the rest of the rail would scroll
     up through. This fills it with the page's own background, anchored to the dock. */
  .related > .hb-dock::before {
    content: ""; position: absolute; left: -1.2rem; right: -1.2rem; bottom: 100%;
    height: 100vh; background: var(--bg);
  }
  /* And a solid ring around the dock itself, closing the four corner notches of its radius. */
  .related > .hb-dock { box-shadow: 0 0 0 1rem var(--bg); }
}
.related h2 { font-size: .8rem; margin: .3rem 0 .7rem; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); font-weight: 600; }
.related .pages { list-style: none; margin: 0 0 .9rem; padding: 0; }
.related .pages li { padding: .42rem 0; }
.related .pages li a { color: var(--ink); display: block; }
.related .pages li a:hover { color: var(--link); }
.related .kind { display: block; font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin-top: .1rem; }
.related .all { margin: 0; font-size: .88rem; }

/* --- thumbnails in a list --------------------------------------------------------------------
   A row with a picture is two columns: the still, then the title beside it, the shape YouTube's
   sidebar uses because it is the right one for scanning a list of videos. */
.related .pages li a:has(.pg-thumb) {
  display: grid; grid-template-columns: 10rem 1fr; gap: .7rem; align-items: start;
}
.pg-thumb {
  width: 10rem;
  /* height MUST be auto for aspect-ratio to apply; the markup carries width/height attributes. */
  height: auto; aspect-ratio: 16 / 9; object-fit: cover;
  border-radius: 6px; background: var(--tint); display: block;
}
.pg-text { min-width: 0; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; }
/* When there is no still: YouTube answers a missing rendition with a 120x90 gray placeholder,
   which decodes as a valid image. account.js marks those; the box paints its own tint. */
.hb-player.no-still { background: linear-gradient(160deg, var(--tint), var(--bg)); }
.hb-player.no-still img.hb-facade { display: none; }
.pg-thumb.no-still, img.no-still { visibility: hidden; }

/* The table of contents: the left column when the window has room for one, otherwise the head
   of the article as a dropdown. No numbers, and no rules between entries. */
.toc { margin: 0 0 1.1rem; font-size: .9rem; }
.toc summary { font-size: .8rem; margin: .3rem 0 .5rem; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); font-weight: 600; list-style: none; }
.toc summary::-webkit-details-marker { display: none; }
.toc ul { list-style: none; margin: 0; padding: 0; }
.toc li { padding: .18rem 0; border: 0; }
.toc li.l3 { padding-left: .9rem; }
.toc li.l4 { padding-left: 1.8rem; }

/* --- the contents as a dropdown ---------------------------------------------------------------
   When the window has no room for a column, account.js closes the details, moves it to the head
   of the article and adds this class. It floats, so the title sets beside it. The open list
   overlays the text instead of pushing it down. */
.toc.is-drop { float: left; margin: .35rem .9rem .2rem 0; position: relative; }
.toc.is-drop .toc-d { border: 1px solid var(--line); border-radius: 8px; background: var(--bg); }
.toc.is-drop summary {
  margin: 0; padding: .4rem .8rem; cursor: pointer; color: var(--ink);
  display: flex; align-items: center; gap: .4rem; white-space: nowrap;
}
.toc.is-drop summary::after { content: "▾"; color: var(--muted); font-size: .9em; }
.toc.is-drop[open] summary::after { content: "▴"; }
.toc.is-drop summary:hover, .toc.is-drop summary:focus-visible { color: var(--link); }
.toc.is-drop .toc-d[open] > ul {
  position: absolute; z-index: 25; top: calc(100% + .3rem); left: 0; min-width: 14rem;
  max-height: min(60vh, 26rem); overflow-y: auto;
  border: 1px solid var(--line); border-radius: 8px; background: var(--bg);
  box-shadow: var(--shadow); padding: .5rem .8rem;
}

/* --- three columns: Contents on the left, sticky ----------------------------------------------
   Only when the page HAS a contents list and there is room for a third track. Widths, against
   the 16px root the media queries use:
     no dock   14 + 2.5 + 44 + 2.5 + 17   = 80rem of track + 2rem of padding =  82rem shell
     docked    14 + 2.5 + 44 + 2.5 + 33.5 = 96.5rem of track + 2rem          = 98.5rem shell */
@media (min-width: 84rem) {
  body.has-toc:not(.has-dock) .with-aside {
    grid-template-columns: minmax(0, var(--toc-w)) minmax(0, var(--max)) minmax(0, 19rem);
  }
  body.has-toc:not(.has-dock).wide { --shell: 100%; }
  body.has-toc:not(.has-dock) .rails { display: contents; }
  body.has-toc:not(.has-dock) .toc {
    grid-column: 1; grid-row: 1; padding-right: 1.1rem;
    position: sticky; top: calc(var(--header-h) + 1rem); align-self: start;
  }
  body.has-toc:not(.has-dock) .page-main { grid-column: 2; grid-row: 1; }
  body.has-toc:not(.has-dock) .related { grid-column: 3; grid-row: 1; padding-left: 1.1rem; }
}
/* The same, with room for the docked player's 480px floor. The video column takes everything the
   contents and the reading measure do not: a wider column of text is not easier to read, and a
   bigger video is easier to watch. */
@media (min-width: 92rem) {
  body.has-toc.has-dock.dock-side .with-aside {
    grid-template-columns: minmax(0, var(--toc-w)) minmax(0, var(--max)) minmax(33.5rem, 1fr);
  }
  body.has-toc.has-dock.dock-side.wide { --shell: 100%; }
  body.has-toc.has-dock.dock-side .rails { display: contents; }
  body.has-toc.has-dock.dock-side .toc {
    grid-column: 1; grid-row: 1; padding-right: 1.1rem;
    position: sticky; top: calc(var(--header-h) + 1rem); align-self: start;
  }
  body.has-toc.has-dock.dock-side .page-main { grid-column: 2; grid-row: 1; }
  body.has-toc.has-dock.dock-side .related {
    grid-column: 3; grid-row: 1; align-self: stretch;
    padding-left: 1.1rem;
  }
}

/* --- article body ----------------------------------------------------------------------------- */
.body { max-width: 100%; }
.body p { margin: 0 0 1rem; }
.body ul, .body ol { padding-left: 1.4rem; }
.body li { margin: .25rem 0; }
.body hr { border: 0; border-top: 1px solid var(--line); margin: 1.6rem 0; }
.body h2 { padding-top: .2rem; }
.table-wrap { overflow-x: auto; margin: 1rem 0; }
table { border-collapse: collapse; width: 100%; font-size: .95rem; }
th, td { text-align: left; padding: .4rem .6rem; vertical-align: top; border-bottom: 1px solid var(--line); }
th { background: var(--tint); }
blockquote { margin: 1rem 0; padding: .2rem 0 .2rem 1rem; border-left: 3px solid var(--line); color: var(--muted); }
blockquote p { margin: .3rem 0; }
code { font-family: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace; font-size: .92em; background: var(--tint); border-radius: 4px; padding: .05em .3em; }
pre { background: var(--tint); border-radius: 8px; padding: .8rem 1rem; overflow-x: auto; font-size: .88rem; line-height: 1.5; }
pre code { background: none; padding: 0; font-size: inherit; }
q { quotes: "\201C" "\201D"; }
/* A wiki link to a page that does not exist yet: the red of the encyclopedias, told by color
   and by a dashed rule, and it still leads to the editor. */
a.wikilink[data-missing] { color: var(--accent); text-decoration: underline dashed; text-decoration-thickness: 1px; text-underline-offset: 3px; }

/* The line under the title on a page: category, last change, the Edit link and the history. */
.page-meta { display: flex; flex-wrap: wrap; gap: .3rem 1rem; align-items: baseline; margin: 0 0 1rem; font-size: .88rem; color: var(--muted); }
.page-meta .category { text-transform: uppercase; letter-spacing: .05em; font-size: .75rem; }
.page-edit { font-weight: 500; }
.needs-signin[hidden] { display: none; }

/* --- moments ---------------------------------------------------------------------------------
   A moment is a place in a video, not a page somewhere else, so it does not wear link blue. The
   chip is the time in a small box; the label, when there is one, is plain words beside it. */
.moment-ts { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--moment); }
.moment-ts:hover, .moment-ts:focus-visible { text-decoration: none; }
.moment-ts time {
  display: inline-block; line-height: 1.35;
  background: var(--tint); border: 1px solid color-mix(in srgb, var(--moment) 45%, transparent);
  border-radius: 4px; padding: 0 .35rem;
}
.moment-ts:hover time, .moment-ts:focus-visible time { border-color: var(--moment); background: color-mix(in srgb, var(--moment) 14%, transparent); }
.moment-label { font-weight: 500; color: var(--ink); }
.moment-ts:hover .moment-label { color: var(--moment); }
.moment-note, .moment-desc { color: var(--muted); display: block; font-size: .95rem; }
/* A moment whose video this viewer cannot play: not hidden, because the reader may still want to
   follow it to YouTube, but it stops looking like a way into something that will happen here. */
.moment-blocked { opacity: .55; text-decoration: line-through; text-decoration-thickness: 1px; }
.moment-blocked:hover, .moment-blocked:focus-visible { opacity: 1; }

/* A paragraph that opens with a moment. On a narrow screen the chip is inline before the text.
   At two columns and up it sits in the paragraph's left margin, the way the Heartbeat demo lists
   timestamps beside each block: the reading column takes padding on its left and the chip is
   positioned back into that padding, so the measure is 44rem including the margin and the chip
   never leaves the column. The label wraps under the time in the margin. */
p.has-moment { position: relative; }
p.has-moment > .moment-ts:first-child { margin-right: .4rem; }
p.has-moment.playing > .moment-ts:first-child time, .moment-block.playing .moment-ts time, .moment-list li.playing .moment-ts time, .key-moment.playing .moment-ts time {
  border-color: var(--moment); background: color-mix(in srgb, var(--moment) 22%, transparent); color: var(--ink);
}
@media (min-width: 60.01rem) {
  :root { --chip-w: 6.5rem; }
  .body:has(p.has-moment) { padding-left: var(--chip-w); }
  p.has-moment > .moment-ts:first-child {
    position: absolute; left: calc(-1 * var(--chip-w)); top: .1em; width: calc(var(--chip-w) - .8rem);
    margin: 0; text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: .1rem;
  }
  p.has-moment > .moment-ts:first-child .moment-label {
    font-size: .78rem; line-height: 1.3; font-weight: 500; color: var(--muted);
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden;
  }
  p.has-moment > .moment-ts:first-child:hover .moment-label { color: var(--moment); }
  /* The paragraph the player is sitting under gets a quiet mark down the margin. */
  p.has-moment.playing::before {
    content: ""; position: absolute; left: -.6rem; top: 0; bottom: 0;
    width: 0; border-left: 2px solid color-mix(in srgb, var(--moment) 55%, transparent);
  }
}
/* A paragraph that is nothing but a moment: a card. The chip and the label are the whole line,
   and the whole card is the control. */
.moment-block {
  margin: .9rem 0; padding: .55rem .8rem; border-radius: 8px;
  background: var(--tint); border: 1px solid var(--line);
}
.moment-block .moment-ts { display: inline-flex; gap: .6rem; align-items: baseline; }
.moment-block.playing { border-color: color-mix(in srgb, var(--moment) 55%, transparent); }
/* The key moment: one line under the summary on a page that has moments. It is the first thing
   a reader can DO here, set apart by the space around it rather than by a rule. */
.key-moment { margin: .9rem 0 1.3rem; }
.key-moment .moment-desc { margin-top: .15rem; }
/* Each entry is one line where it fits: the time, then its words beside it rather than under
   it (George, 16 September 2026). */
.moment-list { list-style: none; padding: 0; }
/* The words after the times start in one column, wrapped lines included (George, 16 September
   2026: "not snaking"): each entry is two columns, and every time chip in a list is as wide as its
   longest time (--clock-ch, from the server; moments.js sets it on the list under the video). A
   moment with a label has no words after it, so its link takes the whole row. */
.moment-list li {
  padding: .3rem .6rem; border-radius: 6px; margin: .1rem 0;
  display: grid; grid-template-columns: max-content minmax(0, 1fr); column-gap: .6rem; align-items: baseline;
}
.moment-list li .moment-ts { display: inline-flex; gap: .6rem; align-items: baseline; }
.moment-list li .moment-ts:last-child { grid-column: 1 / -1; }
.moment-list li .moment-desc { margin: 0; }
.moment-list .moment-ts time, .hb-moments-list .moment-ts time {
  box-sizing: border-box; text-align: center;
  min-width: calc(var(--clock-ch, 5) * 1ch + .7rem + 2px);
}
.moment-list li.selected, .moment-list li.playing, .moment-block.selected { background: var(--tint); outline: 1px solid var(--line); }
/* Grouped by video: the video's title, its channel under it, then the moments the page takes from
   it (George, 16 September 2026). */
.moment-video + .moment-video { margin-top: 1.1rem; }
.moment-video-h { font-size: 1.02rem; margin: 0; line-height: 1.35; }
.moment-video-by { margin: .1rem 0 .3rem; color: var(--muted); font-size: .85rem; }
.moment-video .moment-list { margin: 0; }

/* --- photos in the article --------------------------------------------------------------------
   A figure is the full column width, rounded, with the caption and the credit under it in the
   small muted type a museum label uses. The picture is a link to the wall, and wall.js opens the
   viewer over it. */
figure.photo { margin: 1.4rem 0; }
figure.photo .photo-link { display: block; border-radius: 10px; overflow: hidden; background: var(--tint); }
figure.photo img { display: block; width: 100%; height: auto; }
/* <picture> wraps a photo only to offer the AVIF set first; it takes no box of its own, so the
   rules for the img inside (here, on tiles and on viki cards) apply exactly as without it. */
picture { display: contents; }
figure.photo figcaption, .inline-photo + figcaption { margin: .45rem .2rem 0; font-size: .88rem; color: var(--muted); line-height: 1.45; }
figcaption .credit { display: inline-block; margin-left: .4rem; font-size: .8rem; color: var(--muted); opacity: .85; }
figcaption .credit::before { content: "\2014\00a0"; }
.inline-photo { display: inline-block; max-width: 100%; height: auto; vertical-align: middle; border-radius: 6px; }
.body img { max-width: 100%; height: auto; }

/* --- the page-photos strip -----------------------------------------------------------------------
   Under the article: the photos dropped on this page that are not in the text. Small tiles in a
   row; a pending one is grayed with a label, and only its uploader and an admin ever see it. */
.page-photos { display: flex; flex-wrap: wrap; gap: .4rem; margin: 1.2rem 0; }
.page-photos:empty { display: none; }
.page-photos .tile {
  position: relative; display: block; width: 7.5rem; height: 5rem; border-radius: 6px; overflow: hidden;
  background: var(--tint); flex: none;
}
.page-photos .tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.page-photos .tile.pending img { opacity: .5; filter: grayscale(.6); }
.page-photos .tile .pending-label, .tile .pending-label {
  position: absolute; left: 0; right: 0; bottom: 0; padding: .15rem .4rem;
  font-size: .7rem; text-transform: uppercase; letter-spacing: .05em; text-align: center;
  background: rgba(0, 0, 0, .65); color: #fff;
}
.page-photos .tile:hover, .page-photos .tile:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }

/* --- the hub ----------------------------------------------------------------------------------- */
.hero { padding: 1.5rem 0 1rem; }
.hero h1 { font-size: 2.6rem; letter-spacing: -.02em; margin-bottom: .5rem; }
.hero p.lead { max-width: 36rem; }
/* The three sentences that say what the site is. */
.hero > p { max-width: 40rem; }
.hero .mechanism-h { margin: 1.4rem 0 .2rem; font-size: 1.05rem; }
.mechanism { padding-left: 1.2rem; max-width: 40rem; }
.mechanism li { margin: .5rem 0; }
/* The hub's front page takes the whole window, as YouTube's home does: the shelves of vikis run
   edge to edge with as many cards to a row as fit, and the explanation waits at the foot, in a
   column narrow enough to read (George, 16 September 2026). */
body.full { --shell: 100%; }
body.full main, body.full footer.site {
  padding-left: clamp(.85rem, 3vw, 2.5rem); padding-right: clamp(.85rem, 3vw, 2.5rem);
}
/* Five cards to a row at most, however wide the window, and the cards grow instead (George, 16
   September 2026, four; Oct 14, 2026: "make it max 5 thumbnails per row ... all other vi.ki pages that
   have a wall of thumbnails"): a column is never narrower than a fifth of the shelf, less its gaps, so
   a sixth never fits; on a narrower window the 17rem floor decides and fewer fit. */
body.full .viki-cards { grid-template-columns: repeat(auto-fill, minmax(max(17rem, calc((100% - 4 * 1.1rem) / 5)), 1fr)); }
.shelf { margin: 0 0 2.2rem; }
.shelf > h2 { margin-top: .4rem; }
/* The hub's Popular pages: a column of links under each viki's name, as many columns as fit. */
.popular-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: 1.2rem 1.6rem; margin-top: .9rem; }
/* Each viki is a card headed by its game's icon and its name in the ink color, bigger and bolder than
   the page links under it (George, Oct 14, 2026: "make each viki title stand out"). */
.popular-viki { background: var(--tint); border: 1px solid var(--line); border-radius: 12px; padding: .8rem 1rem .7rem; }
.popular-viki h3 { margin: 0 0 .5rem; padding-bottom: .5rem; border-bottom: 1px solid var(--line); font-size: 1.12rem; font-weight: 700; }
.popular-viki h3 a { display: flex; align-items: center; gap: .55rem; color: var(--ink); }
.popular-viki h3 a:hover { color: var(--link); text-decoration: none; }
.popular-viki .popular-icon { width: 28px; height: 28px; border-radius: 6px; object-fit: cover; flex: none; }
.popular-viki ul { margin: 0; padding: 0; list-style: none; }
.popular-viki li { margin: .18rem 0; font-size: .94rem; }
body.full .hero { max-width: 44rem; margin-top: 1rem; padding-top: 1.6rem; border-top: 1px solid var(--line); }
.hero > h2 { margin-top: 0; }
.featured .all { margin: 1.2rem 0 .4rem; }
.featured .section-note { margin: .2rem 0 0; color: var(--muted); font-size: .92rem; }
.featured.upcoming { margin-bottom: 2.2rem; }
/* An Upcoming card opens with a strip of its launch countdown, above the picture, in the viki's
   own banner colors, flush with the card's edges. */
.viki-card-text { flex: 1 1 auto; }
.countdown.countdown-card {
  display: flex; flex-direction: column; align-items: center; gap: .35rem;
  padding: .55rem .6rem .6rem; border-top: 0; border-bottom-width: 2px; z-index: auto;
}
/* The launch line, in the card's own type size (George took the doubled size away again on 16
   September 2026). Lines wrap differently from card to card, so the cards in a row share their
   rows (a subgrid): every clock, every picture and every title starts at the same height. */
.countdown-card-text {
  display: block; font-weight: 600; text-align: center; line-height: 1.25;
  text-wrap: balance;
}
.upcoming .viki-cards { row-gap: 0; }
.upcoming .viki-card { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; margin-bottom: 1.1rem; }
.upcoming .viki-card > a { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; height: auto; }
.upcoming .countdown.countdown-card { justify-content: center; }
.countdown-card .countdown-clock { margin: 0; gap: .15rem; }
.countdown-card .cd { min-width: 2.35rem; }
.countdown-card .cd b { font-size: 1.35rem; }
.countdown-card .cd small { margin-top: .2rem; font-size: .55rem; }
.countdown-card .cd-sep { font-size: .95rem; line-height: 1.5; }
.start-viki { margin-top: 1.4rem; }
/* A viki is a card: its lead picture (a photo, or the lead video's still), its title, its
   tagline, and a count or two. Cards in a grid that fits as many as the width allows. */
.viki-cards { list-style: none; margin: 1.2rem 0 0; padding: 0; display: grid; gap: 1.1rem; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); }
.viki-card > a {
  display: flex; flex-direction: column; height: 100%; color: var(--ink); border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  background: var(--tint); transition: transform .15s ease, box-shadow .15s ease;
}
.viki-card > a:hover, .viki-card > a:focus-visible { text-decoration: none; transform: translateY(-2px); box-shadow: var(--shadow); border-color: color-mix(in srgb, var(--link) 45%, var(--line)); }
/* The picture's box has the 16:9 shape itself and the image fills it: in the Upcoming shelf's
   shared rows a grid sizes a row from its items, and an image sized by a percentage width gives
   it no height at all (the pictures collapsed to a sliver on 16 September 2026). */
.viki-card-pic { display: block; position: relative; aspect-ratio: 16 / 9; overflow: hidden; background: var(--bg); }
.viki-card-pic > picture { display: block; position: absolute; inset: 0; }
.viki-card-img { display: block; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; background: var(--bg); }
.viki-card-blank { background: linear-gradient(160deg, var(--tint), var(--bg)); }
.viki-card-text { display: block; padding: .7rem .9rem .85rem; }
.viki-card-text strong { display: block; font-size: 1.1rem; font-weight: 600; color: var(--ink); }
.viki-card-tagline { display: block; margin: .2rem 0 0; color: var(--muted); font-size: .92rem; line-height: 1.4; }
.viki-card-meta { display: block; margin: .5rem 0 0; font-size: .78rem; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.search-page form.search button { border: 1px solid var(--link); border-radius: 999px; background: none; color: var(--link); padding: .35rem .9rem; cursor: pointer; font: inherit; }
nav.paging { display: flex; gap: 1rem; align-items: baseline; margin: 1.4rem 0; font-size: .92rem; }
nav.paging .page-n { color: var(--muted); }

/* --- the pages index -----------------------------------------------------------------------------
   Every page one click from the front door: category headings, two columns when there is room. */
.index-jump { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin: .8rem 0 1.4rem; font-size: .92rem; }
.pages.index { column-gap: 2.5rem; }
.pages.index > h2 { column-span: all; }
@media (min-width: 46rem) { .pages.index { columns: 2; } }
.index-group { margin-bottom: 1.6rem; break-inside: avoid; }
.index-group h2, .index-group h3 { margin: 0 0 .5rem; font-size: 1.05rem; }
.page-list { list-style: none; padding: 0; margin: 0; }
.page-list li { padding: .45rem 0; }
.page-list li > a { font-weight: 600; font-size: 1.02rem; }
.page-list p { margin: .15rem 0 0; color: var(--muted); font-size: .9rem; }
.page-list .kind { font-size: .75rem; color: var(--muted); margin-left: .6rem; text-transform: uppercase; letter-spacing: .05em; }
.chip { display: inline-block; margin-left: .5rem; padding: .05rem .45rem; border-radius: 999px; font-size: .72rem;
  letter-spacing: .02em; text-transform: uppercase; border: 1px solid var(--line); color: var(--muted); vertical-align: middle; }
.chip.pending, .chip.draft { border-color: var(--moment); color: var(--moment); }
.chip.approved { border-color: var(--ok); color: var(--ok); }
.chip.rejected { border-color: var(--accent); color: var(--accent); }

/* --- search results ------------------------------------------------------------------------------ */
.search-results { list-style: none; padding: 0; margin: 1.2rem 0 0; }
.search-results li { padding: .75rem 0; }
.search-results h2 { font-size: 1.1rem; margin: 0 0 .2rem; }
.search-results p { margin: .2rem 0 0; color: var(--muted); }
.search-results mark, .search-page mark { background: color-mix(in srgb, var(--link) 30%, transparent); color: inherit; border-radius: 3px; padding: 0 .1rem; }
.search-page form.search { max-width: 26rem; margin: 1rem 0; }
/* The hub search's two groups: the vikis that match, then their pages. */
.search-page .search-group {
  margin: 1.6rem 0 .6rem; font-size: .8rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .07em; color: var(--muted);
}

/* --- changes, history, my edits -------------------------------------------------------------------
   Each row is a change: the page, who, when, the note, and the links to the revision and its
   diff. Quiet rows separated by space, not rules, and the note in the reader's own words. */
.versions { list-style: none; padding: 0; margin: 1rem 0; font-size: .95rem; }
.versions li { padding: .6rem 0; border-top: 1px solid var(--line); }
.versions li:first-child { border-top: 0; }
.versions li > a:first-child, .versions li > strong { font-weight: 600; }
.versions .note { font-style: italic; color: var(--ink); }
.versions .links { display: inline-block; margin-left: .4rem; font-size: .88rem; }
.versions .chip { margin-left: .3rem; }
.revision-banner { margin: 0 0 1.2rem; font-size: .92rem; }
/* The account pages: the edits list wears the suggestion classes, one card per revision with its
   status colored down the left edge. */
.me .who img.avatar { border-radius: 50%; vertical-align: middle; margin-right: .4rem; }
.me .role { text-transform: uppercase; letter-spacing: .05em; font-size: .8rem; color: var(--muted); }
.me-links { padding-left: 1.2rem; }
.me-links li { margin: .3rem 0; }
.suggestions { list-style: none; padding: 0; margin: 1rem 0 0; }
.suggestion { border: 1px solid var(--line); border-left: 3px solid var(--line); border-radius: 8px; padding: .8rem 1rem; margin: 0 0 .8rem; }
.suggestion.s-approved { border-left-color: var(--ok); }
.suggestion.s-pending { border-left-color: var(--moment); }
.suggestion.s-rejected { border-left-color: var(--accent); }
.suggestion.just-submitted { background: var(--tint); }
.s-what { margin: 0 0 .4rem; }
.s-what .kind { font-size: .75rem; color: var(--muted); margin-left: .4rem; text-transform: uppercase; letter-spacing: .05em; }
.s-status { margin: 0 0 .3rem; }
.s-meta { margin: 0; font-size: .85rem; color: var(--muted); }

/* --- the diff --------------------------------------------------------------------------------
   Unchanged paragraphs stay quiet so the changed ones are the page. Two shapes of markup wear
   the same classes: a list, one row per paragraph, and a table with the old and new text in two
   columns. The word marks are <del> and <ins> (server/diff.go), and .del/.add as spans. */
.diff { margin: 1.2rem 0; }
ul.diff, ol.diff { list-style: none; padding: 0; }
.diff li { padding: .45rem .7rem; border-left: 3px solid transparent; margin: .2rem 0; }
.diff .d-same { color: var(--muted); }
.diff li.d-changed, .diff li.d-add, .diff li.d-del { background: var(--tint); border-radius: 0 6px 6px 0; }
.diff li.d-changed, .diff li.d-add { border-left-color: var(--ok); }
.diff li.d-del { border-left-color: var(--accent); }
.d-changed p { margin: .1rem 0; }
.diff-table { width: 100%; table-layout: fixed; font-size: .95rem; }
.diff-table th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); background: none; }
.diff-table td { width: 50%; vertical-align: top; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
.diff-table tr.d-same td { color: var(--muted); }
.diff-table tr.d-add td:last-child, .diff-table tr.d-changed td.d-new { background: color-mix(in srgb, var(--ok) 9%, transparent); }
.diff-table tr.d-del td:first-child, .diff-table tr.d-changed td.d-old { background: color-mix(in srgb, var(--accent) 9%, transparent); }
.diff ins, .diff-table ins, .diff .add { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; background: color-mix(in srgb, var(--ok) 28%, transparent); border-radius: 3px; padding: 0 .12rem; }
.diff del, .diff-table del, .diff .del { text-decoration: line-through; text-decoration-thickness: 1px; background: color-mix(in srgb, var(--accent) 26%, transparent); border-radius: 3px; padding: 0 .12rem; }
.d-old { color: var(--muted); }
@media (max-width: 40rem) {
  /* Two columns of prose do not fit a phone: the old text stacks over the new. */
  .diff-table, .diff-table tbody, .diff-table tr, .diff-table td { display: block; width: 100%; }
  .diff-table thead { display: none; }
  .diff-table td:first-child { border-bottom: 0; }
  .diff-table td:empty { display: none; }
}

/* --- the review queue and the admin ------------------------------------------------------------
   Plain, readable forms and tables. A queue item is a card: what, who, when, the note, a diff
   against current, and the two buttons with an optional note beside them. */
.queue, .queue-photos { list-style: none; padding: 0; margin: 1rem 0; }
.queue-item, .queue-photo { border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1.1rem; margin: 0 0 1rem; background: var(--tint); }
.queue-item h3 { margin: 0 0 .3rem; font-size: 1.1rem; }
.queue-item h3 .kind { font-size: .75rem; color: var(--muted); margin-left: .5rem; text-transform: uppercase; letter-spacing: .05em; font-weight: 500; }
.queue-item .s-what { margin: .4rem 0; }
.queue-diff { margin: .6rem 0; }
.queue-diff summary { cursor: pointer; color: var(--muted); font-size: .9rem; }
.queue-diff .diff { margin: .5rem 0 0; background: var(--bg); border-radius: 8px; padding: .3rem; }
.queue-photo img { display: block; max-width: 20rem; max-height: 14rem; width: auto; height: auto; border-radius: 6px; margin: 0 0 .5rem; }
/* The decision: two forms side by side, each an optional note and its button. */
.s-decide { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin-top: .7rem; }
.s-decide form.inline { display: flex; flex: 1 1 18rem; gap: .4rem; align-items: center; min-width: 0; }
.s-decide input[type="text"] { flex: 1 1 8rem; min-width: 0; padding: .35rem .6rem; font-size: .9rem; background: var(--bg); }
.s-decide button { font-size: .9rem; padding: .35rem .9rem; white-space: nowrap; }
.button.secondary { border-color: var(--line); color: var(--ink); }
.button.secondary:hover { border-color: var(--link); color: var(--link); }
.admin .who, .me .who { color: var(--muted); margin-top: 0; }
.notice.error { border-left-color: var(--accent); color: var(--accent); }
.needs-admin[hidden] { display: none; }
.admin-table { min-width: 34rem; font-size: .93rem; }
.admin-table .actions form { display: inline-block; margin: 0 .2rem .2rem 0; }
.admin-table td code { font-size: .85rem; }
.admin .table-wrap, .diff, .versions { max-width: 100%; }
.stats { display: flex; flex-wrap: wrap; gap: .8rem; margin: 1rem 0; }
.stat { min-width: 8rem; padding: .7rem .9rem; border: 1px solid var(--line); border-radius: 8px; background: var(--tint); }
.stat b, .stat strong { display: block; font-size: 1.5rem; font-weight: 600; color: var(--ink); }
.stat span { font-size: .8rem; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
dl.settings > div { display: flex; gap: .6rem; padding: .35rem 0; }
dl.settings dt { min-width: 9rem; color: var(--muted); }
dl.settings dd { margin: 0; }
.audit { font-size: .9rem; }

/* --- the sign-in gate ------------------------------------------------------------------------------ */
.signin-gate { border: 1px solid var(--link); border-radius: 10px; padding: 1rem 1.2rem; background: var(--tint); margin: 1.2rem 0; }
.signin-gate h2 { margin-top: .2rem; font-size: 1.15rem; }

/* --- the editor page -----------------------------------------------------------------------------
   The editor fills its column; the dock sits beside it in the rail when the page has moments, so
   an editor can scrub the video while writing. The editor's own internals (the two panes, the
   resizer, the chips) are in editor.css. What is here is the frame: the fields above, the
   note and the buttons below, and the helpers beside the toolbar. */
.editor-page { margin-top: .2rem; }
.editor-page h1 { font-size: 1.5rem; }
.editor-form .field { margin: 0 0 .8rem; }
.editor-form textarea#summary { min-height: 3.5rem; }
.editor-page .viki-editor { min-height: 60vh; }
/* The helpers beside the toolbar: small buttons in a row above the editor. */
.editor-tools { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0 .4rem; }
.editor-tools .button { font-size: .88rem; padding: .3rem .7rem; background: var(--tint); border-color: var(--line); color: var(--ink); }
.editor-tools .button:hover { border-color: var(--link); color: var(--link); }
.editor-tools .button[disabled] { opacity: .5; cursor: not-allowed; }
.editor-form #note { width: 100%; }
.editor-actions { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin: .8rem 0 .6rem; }
/* The small dialog the Insert moment helper opens. */
.moment-dialog { border: 1px solid var(--line); border-radius: 10px; padding: 1rem 1.2rem; background: var(--bg); color: var(--ink); box-shadow: var(--shadow); max-width: min(28rem, calc(100vw - 2rem)); }
.moment-dialog::backdrop { background: rgba(0, 0, 0, .45); }
.moment-dialog h2 { margin: 0 0 .7rem; font-size: 1.05rem; }
.moment-dialog .field { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .6rem; margin: 0 0 .7rem; }
.moment-dialog .field label { flex: 0 0 auto; }
.moment-dialog .field input[type="url"], .moment-dialog .field input#moment-label { flex: 1 1 100%; }
.moment-dialog .field input[type="text"] { width: 6rem; }
.moment-dialog #moment-now { font-size: .85rem; padding: .3rem .7rem; }
.editor-start .new-page-form p label { display: block; font-weight: 600; margin-bottom: .3rem; }
.editor-start .new-page-form input[type="text"] { width: 100%; }

/* --- dropping photographs -----------------------------------------------------------------------
   The veil says the drop will be caught: a page that accepts a file and looks exactly like a page
   that does not is a page nobody tries it on. Over everything, because the drop is over
   everything, and pointer-events: none so it cannot itself swallow the drop it is advertising.
   drop.js builds both of these when a page lacks them. */
.dropveil {
  position: fixed; inset: 0; z-index: 60; pointer-events: none;
  display: grid; place-items: center;
  background: rgba(10, 10, 14, .72);
  box-shadow: inset 0 0 0 3px var(--accent);
}
.dropveil[hidden] { display: none; }
.dropveil-say {
  display: grid; place-items: center; gap: 10px;
  color: #fff; font-size: 1.05rem; text-align: center;
  padding: 1.4rem 2rem; border-radius: 16px;
  background: rgba(20, 20, 26, .92); border: 1px solid rgba(255, 255, 255, .14);
}
.dropveil-say p { margin: 0; }
/* What is going up. Bottom-right, narrow, never over the middle of the page: uploading is
   something you set going and then carry on around. It sits above the pinned dock. */
.uploads {
  position: fixed; right: .9rem; bottom: calc(.9rem + var(--hb-dock-h, 0px)); z-index: 55;
  width: min(340px, calc(100vw - 1.8rem));
  background: var(--bg); color: var(--ink);
  border: 1px solid var(--line); border-radius: 12px;
  box-shadow: var(--shadow); overflow: hidden; font-size: .85rem;
}
.uploads[hidden] { display: none; }
.up-head {
  display: flex; align-items: center; gap: .5rem;
  padding: .55rem .5rem .55rem .8rem; border-bottom: 1px solid var(--line); background: var(--tint);
}
.up-say { flex: 1 1 auto; }
.up-x {
  flex: 0 0 auto; display: grid; place-items: center; width: 1.7rem; height: 1.7rem;
  background: none; border: 0; border-radius: 7px; color: var(--muted); cursor: pointer; font: inherit;
}
.up-x:hover { background: var(--line); color: var(--ink); }
.up-list { max-height: min(46vh, 340px); overflow-y: auto; }
/* A row is: what it is called, how it is getting on, the one button that applies, and the
   progress along the bottom of the row. */
.up-row {
  position: relative; display: flex; align-items: center; gap: .5rem;
  padding: .55rem .6rem .65rem .8rem; border-bottom: 1px solid var(--line);
}
.up-row:last-child { border-bottom: 0; }
.up-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.up-state { flex: 0 0 auto; color: var(--muted); font-size: .78rem; font-variant-numeric: tabular-nums; }
.up-act {
  flex: 0 0 auto; background: none; color: var(--muted); cursor: pointer;
  border: 1px solid var(--line); border-radius: 999px; padding: .15rem .6rem; font: inherit; font-size: .78rem;
}
.up-act:hover { color: var(--ink); border-color: var(--accent); }
.up-act[hidden] { display: none; }
.up-bar { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--line); }
.up-fill { display: block; height: 100%; width: 0; background: var(--accent); transition: width .18s ease; }
.up-row[data-state="done"] .up-fill { background: var(--ok); }
.up-row[data-state="failed"] .up-fill, .up-row[data-state="stopped"] .up-fill { background: var(--muted); }
.up-row[data-state="failed"] .up-state { color: var(--accent); }
.up-row[data-state="stopped"] .up-name, .up-row[data-state="failed"] .up-name { color: var(--muted); }
/* The look-alike warning (drop.js lookAlike) sits under the row's name. */
.up-row.has-alike { flex-wrap: wrap; }
.up-alike { flex: 1 0 100%; margin: .2rem 0 0; font-size: .8rem; color: var(--ink); }
.up-alike a { font-weight: 600; }
/* The same warning in the review queue, and Studio's Similar photos report. */
.looks-like { border-left: 3px solid var(--accent); padding: .4rem .7rem; margin: .5rem 0; background: var(--bg); border-radius: 0 6px 6px 0; }
.looks-like p { margin: 0 0 .3rem; }
.looks-like ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
.looks-like img { display: inline-block; vertical-align: middle; width: 96px; height: auto; max-height: 64px; object-fit: cover; border-radius: 4px; margin: 0 .4rem 0 0; }
.studio-links { padding-left: 1.2rem; }
.studio-links li { margin: .45rem 0; }
.similar-pairs { list-style: none; padding: 0; margin: 1rem 0; }
.similar-pair { border: 1px solid var(--line); border-radius: 10px; padding: .8rem 1rem; margin: 0 0 1rem; background: var(--tint); }
.similar-two { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); gap: 1rem; }
.similar-side { margin: 0; }
.similar-side img { display: block; width: 100%; max-width: 20rem; height: auto; border-radius: 6px; }
.similar-side figcaption { display: grid; gap: .15rem; margin-top: .4rem; }
.similar-side.other { opacity: .75; }
/* Studio: Create, a picture made with a model. */
.create .rule { border-left: 3px solid var(--accent); padding: .5rem .8rem; background: var(--tint); border-radius: 0 6px 6px 0; }
.create-form .field { display: grid; gap: .3rem; max-width: 42rem; }
.create-form textarea, .create-form input[type="text"], .create-form input[type="number"] { font: inherit; padding: .5rem; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); }
.create-form fieldset { border: 1px solid var(--line); border-radius: 8px; padding: .5rem .8rem; max-width: 42rem; }
/* A tick or a bullet is not a field. The rule above gives everything in a .field the full width,
   which turns a radio into a box as wide as the form and drops its words onto the next line, so
   the mark sits ABOVE the thing it marks (George, 19 September 2026). */
.field .check { display: flex; align-items: baseline; gap: .5rem; margin: .3rem 0; font-weight: 400; }
.field .check input[type="radio"], .field .check input[type="checkbox"] { flex: none; width: auto; margin: 0; }
/* Draw it sits directly under the words it draws, ahead of the settings: the button is what the
   page is for, and everything under it already has an answer. */
.create .ask-go { display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap; margin: .8rem 0 1.2rem; }
.made-pic { display: block; max-width: min(100%, 34rem); height: auto; border-radius: 8px; margin: .6rem 0; }
.made-links { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin: .6rem 0; }
.made .waiting { color: var(--muted); }
/* What the page is for, at the bottom: read once, and then in the way of the box you came to
   type in. Quieter than the picture above it, and marked off from it. */
.create .about { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid var(--line); color: var(--muted); }
.create .about h2 { font-size: 1rem; }
.create .about strong { color: var(--ink); }

/* Studio: what I share publicly. */
.shared-name { display: flex; gap: 1rem; align-items: flex-start; }
.shared-avatar { border-radius: 50%; flex: none; }
.name-form { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin: 0 0 .4rem; }
.name-form input[type="text"] { min-width: 14rem; }
.shared .bulk { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; margin: .6rem 0 1rem; }
.shared .bulk form { margin: 0; }
.shared-photo img { display: inline-block; vertical-align: middle; width: 64px; height: auto; max-height: 48px; object-fit: cover; border-radius: 4px; margin-right: .5rem; }
.shared-edits { list-style: none; padding: 0; margin: .6rem 0; display: grid; gap: .35rem; }
.viki-scope { border: 1px solid var(--line); border-radius: 8px; padding: .5rem .8rem; }
@media (max-width: 700px) {
  .uploads { right: .5rem; left: .5rem; width: auto; }
  .up-list { max-height: 34vh; }
}

/* --- the wall page shell ------------------------------------------------------------------------
   The wall itself and the viewer are wall.css; this is the page around them: the title row and
   the load-more control. The wall is edge to edge inside a full-width shell. */
body:has(> main > .wall-page) { --shell: 100%; }
.wall-page h1 { font-size: 1.5rem; margin-bottom: .8rem; }
.wall-more { display: flex; justify-content: center; padding: 1.2rem 0; margin: 0; }
.wall-more[hidden] { display: none; }
.wall-page .empty { margin-top: 1rem; }
/* The strip of a page's own photos, under the article. */
.photos-strip { margin-top: 1.6rem; }
.photos-strip h2 { margin-top: 0; }
.page-actions { display: flex; flex-wrap: wrap; gap: .6rem 1rem; align-items: center; }
.provenance { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.provenance .page-meta { margin: 0 0 .7rem; }

/* --- NARROW ----------------------------------------------------------------------------------------
   The search box becomes a button rather than a second row. Collapsed, it is one icon; opened,
   it covers the brand and takes the whole bar. The mark stays as the way home. */
@media (max-width: 46rem) {
  header.site { grid-template-columns: auto 1fr auto; row-gap: 0; }
  /* The viki's name gives way first: its track can shrink below the name, which then ends in an
     ellipsis inside it, so the Vikis pill beside it never covers it (George, 16 September
     2026), and the search icon always keeps its own room. */
  header.site:has(> .nav-start) { grid-template-columns: auto auto minmax(2.25rem, 1fr) auto; }
  header.site .hide-narrow { display: none; }
  header.site .brand { max-width: 100%; }
  header.site > .nav-more { order: 1; }
  header.site > .nav-start { order: 2; }
  form.search { order: 3; }
  header.site nav { order: 4; }
  header.site nav .hide-narrow { display: none; }
  /* The Settings gear joined the bar on Oct 1, 2026; the gaps make its room, not the viki's
     name (at 375px the name had shrunk to its first letter). The round buttons carry their own
     padding, so they still read as separate. */
  header.site { column-gap: .4rem; }
  header.site nav { gap: .2rem; }
  :root { --header-h: 4rem; }
  header.site .brand > span { max-width: 38vw; min-width: 0; }
  /* With the More pill in every header (Oct 14, 2026) a phone left the name a letter or two ("A..")
     beside the icon, and the hub's "Vi.ki" none at all, so on a phone the icon or mark stands for
     the name, which stays for screen readers. */
  header.site .brand:has(> .brand-icon, > .brand-mark) > span {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
  /* A header without the logo's menu may give its name more of the bar. */
  header.site:not(:has(> .nav-more)) .brand > span { max-width: 60vw; }
  /* Three words of navigation do not fit beside a brand, a search and an avatar on a phone;
     the footer carries the same links, and the wordmark is the way home. */
  header.site nav > a { display: none; }

  form.search { justify-self: end; width: 2.25rem; transition: width .18s ease; }
  form.search input[type="search"] {
    width: 100%; padding-left: 2.1rem; border-radius: 999px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a2a9b3' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: .5rem 50%; background-size: 1.1rem;
  }
  form.search input[type="search"]::placeholder { color: transparent; }
  /* Opened by focus alone: no script, so it cannot get stuck open. */
  form.search:focus-within { position: absolute; left: 1rem; right: 1rem; width: auto; z-index: 2; }
  form.search:focus-within input[type="search"] { background-color: var(--bg); }
  form.search:focus-within input[type="search"]::placeholder { color: var(--muted); }
  header.site:has(form.search:focus-within) .brand { visibility: hidden; }
}
@media (max-width: 600px) {
  body { font-size: 16px; }
  h1 { font-size: 1.65rem; }
  h2 { font-size: 1.2rem; }
  h3 { font-size: 1.05rem; }
  p.lead, p.summary { font-size: 1.02rem; }
  .hero h1 { font-size: 2rem; }
  header.site, main, footer.site { padding-left: .85rem; padding-right: .85rem; }
  /* Touch targets: 44px is the smallest a finger reliably hits. */
  .toc li { padding: .42rem 0; }
  .related .pages li { padding: .6rem 0; }
  .moment-list li { padding: .6rem .5rem; }
  .signin-link { padding: .5rem .9rem .5rem .7rem; }
  .button { padding: .55rem 1rem; }
  /* A menu wider than the screen is a menu you cannot use. The avatar menu takes its own width
     from its items instead (.account-box .account-menu above), inside the same bound. */
  .account-menu { min-width: 0; width: min(17rem, calc(100vw - 1.5rem)); }
  .diff li { padding: .45rem .55rem; }
  .s-decide form.inline { flex-basis: 100%; }
  dl.settings > div { flex-direction: column; gap: .1rem; }
}

/* Nothing on any page may push the viewport sideways. On the ROOT only, never on body: a
   non-visible overflow on body makes it a scroll container and position: sticky then resolves
   against it, which is how the header silently stops sticking. */
html { overflow-x: hidden; }
.body, .search-results p, .rails li a, .change-note, .queue-note { overflow-wrap: anywhere; }
pre, code { overflow-wrap: anywhere; }
/* But a table cell keeps its words whole (George, Oct 14, 2026: "Don't split words in table columns"):
   "anywhere" lets a table squeeze a column to one letter ("Release / d"); break-word wraps only a word
   wider than the table can give it, and .table-wrap scrolls a table too wide for the screen. */
.body th, .body td { overflow-wrap: break-word; }

/* --- the video dock --------------------------------------------------------------------------
   The video is never in the text. It is the top of the sidebar, just under the header, from the
   first paint: a facade that plays nothing until asked, in the same place whichever moment is
   clicked, and it stays there through any amount of scrolling. At least 480x270, because that is
   the floor the YouTube API terms set. On a narrow screen the sidebar is under the article, so
   on first play the dock pins to the foot of the viewport instead, with a Close button.

   THE BUFFER BOX. The player box is taller than the picture: the picture's height PLUS a band
   (--hb-buffer, 242px; 0 for a portrait video) in which YouTube's own control bar and captions
   render, never over the picture. The box cannot be an aspect-ratio box, so it is sized in
   container units: the dock is an inline-size container and the box's height is
   100cqw * h / w + buffer. 100cqw is the dock's CONTENT width, which is what lets the pinned and
   theater docks cap the player with padding alone. The player's own container is .hb-stage, the
   wrapper it shares with Vi.ki's bar, which is as wide as the dock unless capped (the sidebar,
   below). The facade fills the whole box with
   object-fit: contain, so the still sits exactly where YouTube will draw the picture (centered
   in the frame, band below) and nothing moves when the iframe replaces it. */
.hb-dock { --hb-ar-w: 16; --hb-ar-h: 9; --hb-buffer: 242px; container-type: inline-size; }
.hb-stage { container-type: inline-size; }
.hb-dock.hb-dock--portrait { --hb-ar-w: 9; --hb-ar-h: 16; --hb-buffer: 0px; }
/* The reader's "Video controls overlap" (theme.js), for a landscape video in the sidebar: None,
   the default since 17 September 2026, gives YouTube's controls a band of their own
   (heartbeatme.com's buffer 242, above); Minimal lets the tallest of them reach the picture's
   foot (buffer 151); All lets them lie over it (buffer 0). A portrait video and the pinned phone
   player keep no band either way. */
:root[data-overlap="minimal"] .hb-dock:not(.hb-dock--portrait):not(.hb-dock--bottom) { --hb-buffer: 151px; }
:root[data-overlap="all"] .hb-dock:not(.hb-dock--portrait):not(.hb-dock--bottom) { --hb-buffer: 0px; }
/* Vi.ki's controls (theme.js, moments.js): YouTube's are off, so the box is the picture and the bar
   sits under it; how much YouTube's controls overlap no longer applies, and the menu drops it. */
:root[data-controls="viki"] .hb-dock:not(.hb-dock--portrait):not(.hb-dock--bottom) { --hb-buffer: 0px; }
:root[data-controls="viki"] #settings-overlap { display: none; }
/* YouTube still draws its title, a play and pause button, a link button, More videos and its logo
   over the picture whenever the pointer moves over the player, even with controls=0 (George,
   16 September 2026, asked for them gone). With Vi.ki's controls the player takes no pointer, so
   they never appear; the box under it plays and pauses on a click (moments.js). */
:root[data-controls="viki"] .hb-player.live iframe { pointer-events: none; }
:root[data-controls="viki"] .hb-player.live:not(.failed) { cursor: pointer; }
/* ...and with no pointer, still when it pauses: its title along the top, and its link button, More
   videos and logo along the foot (George, Oct 10, 2026: "on vi.ki can we remove these youtube overlays
   when playing the video ... can we enlarge the video height to remove them without changing the video
   aspect ratio"). So YouTube's frame is taller than the box by a band each way, as Heart Photos' is
   (moment.js place): YouTube centers the video in its frame, so the picture fills the box exactly, and
   what it draws in the bands is cut off by the box. The pause sign it flashes in the middle stays. Its
   captions are drawn in the bottom band too, so with captions on (.cc-on, moments.js) there is no band.
   YouTube's terms ask that its player's branding is not hidden; YouTube's logo stays in the bar, as the
   link to the video. */
:root[data-controls="viki"] .hb-dock:not(.hb-dock--portrait) .hb-player { --hb-yt-band: clamp(64px, calc(100cqw * var(--hb-ar-h) / var(--hb-ar-w) * .15), 150px); }
:root[data-controls="viki"] .hb-dock:not(.hb-dock--portrait) .hb-player iframe {
  top: calc(-1 * var(--hb-yt-band)); bottom: auto; height: calc(100% + 2 * var(--hb-yt-band));
}
:root[data-controls="viki"] .hb-dock:not(.hb-dock--portrait) .hb-player.cc-on iframe { top: 0; height: 100%; }
/* The bar's height (a 1.1rem seek row, a 2.25rem row and .65rem of padding), for the caps that fit
   the whole player in the window. Kept before the first play too, so nothing resizes on it. */
:root[data-controls="viki"] .hb-dock { --hb-bar-h: 4rem; }
.hb-bar { display: none; }
:root[data-controls="viki"] .hb-player.live:not(.failed) + .hb-bar {
  display: block; padding: .35rem .2rem .3rem; background: #000; color: #eee;
}
.hb-bar .hb-row { display: flex; align-items: center; gap: .35rem; min-width: 0; }
.hb-bar .hb-gap { flex: 1 1 auto; }
.hb-btn {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  min-width: 2.25rem; height: 2.25rem; padding: 0 .35rem; border: 0; border-radius: 6px;
  background: none; color: inherit; font: 600 .78rem/1 system-ui, sans-serif; cursor: pointer;
}
.hb-btn:hover, .hb-btn:focus-visible { background: rgba(255, 255, 255, .12); }
/* CC in the speed's pill, as Heart Photos' (George, Oct 10, 2026: "use the same CC style on vi.ki as we use on
   heart.photos"): faint white, brighter under the pointer, the letters dim while off. On, as YouTube marks
   it: the letters white and a short line of the player's red under them, nothing filled (George, Oct 9,
   2026: "When CC is on make it fully colored", then Oct 10, 2026: "Our CC button when enabled is too
   visually distracting please make it less distracting for its "On" state"). */
.hb-btn.hb-cc {
  position: relative; height: 2.25rem; padding: 0 .8rem; border: 0; border-radius: 999px; background: rgba(255, 255, 255, .08);
  color: rgba(255, 255, 255, .6); font: 600 .82rem/1 system-ui, sans-serif; transition: background .15s ease, color .15s ease;
}
.hb-btn.hb-cc:hover, .hb-btn.hb-cc:focus-visible { background: rgba(255, 255, 255, .2); color: #fff; }
.hb-cc[aria-pressed="true"] { color: #fff; }
.hb-cc[aria-pressed="true"]::after {
  content: ""; position: absolute; left: 30%; right: 30%; bottom: 6px; height: 2px; border-radius: 2px; background: #e62117;
}
/* The time, with room on each side as if it sat in a pill of its own as YouTube's does, but with no pill
   (George, Oct 10, 2026: "add more space for the time as if it was in its own bubble like youtube ... but don't
   show a bubble"), a button that shows the time left (moments.js) with no look of one but its pointer. */
.hb-time {
  flex: none; height: 2.25rem; padding: 0 .85rem; border: 0; border-radius: 999px; background: none;
  font: .82rem/1 system-ui, sans-serif; font-variant-numeric: tabular-nums; white-space: nowrap; color: #ccc; cursor: pointer;
}
.hb-time:hover, .hb-time:focus-visible { color: #fff; }
/* The volume is one control (George, Oct 9, 2026: "the volume is one control not two as they currently
   are"): the speaker and its slider in one box that lights as a whole, the speaker no box of its own. */
/* Play and the volume wear Heart Photos' round controls (George, Oct 10, 2026: "use the circular
   background for the play button and the volume controls on vi.ki same as we use on heart.photos"): a
   faint white disc behind play, a pill of the same around the speaker and its slider (a disc while the
   slider is folded away), both brighter under the pointer, as Heart Photos' .ctl (.08, then .2). */
.hb-pp, .hb-volume .hb-mute { width: 2.25rem; min-width: 0; padding: 0; border-radius: 999px; }
.hb-pp { background: rgba(255, 255, 255, .08); transition: background .15s ease; }
.hb-pp:hover, .hb-pp:focus-visible { background: rgba(255, 255, 255, .2); }
.hb-volume {
  display: inline-flex; align-items: center; flex: none; height: 2.25rem; border-radius: 999px;
  background: rgba(255, 255, 255, .08); transition: background .15s ease;
}
.hb-volume:hover, .hb-volume:has(:focus-visible) { background: rgba(255, 255, 255, .2); }
.hb-volume .hb-mute:hover, .hb-volume .hb-mute:focus-visible { background: none; }
/* The slider at YouTube's thickness (George, Oct 9, 2026: "change the thickness of the volume bar to be
   the same as YouTube's"): a 3px line, white as far as the volume goes and faint after it (--hb-vol,
   which moments.js keeps), and a 12px white knob. */
.hb-vol {
  flex: none; width: 4.5rem; height: 1.5rem; margin: 0 .6rem 0 0; background: none; cursor: pointer;
  -webkit-appearance: none; appearance: none;
  transition: width .2s ease, margin .2s ease, opacity .2s ease, visibility 0s;
}
.hb-vol::-webkit-slider-runnable-track {
  height: 2px; border-radius: 1px;
  background: linear-gradient(to right, #fff var(--hb-vol, 100%), rgba(255, 255, 255, .3) var(--hb-vol, 100%));
}
.hb-vol::-moz-range-track {
  height: 2px; border-radius: 1px;
  background: linear-gradient(to right, #fff var(--hb-vol, 100%), rgba(255, 255, 255, .3) var(--hb-vol, 100%));
}
/* Thinner than the seek line (George, Oct 10, 2026: "reduce the thickness of the volume slider by 2px",
   then "increase the volume slider height by 1px"): a 2px line, the knob centered on it. */
.hb-vol::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; margin-top: -5px; border-radius: 50%; background: #fff; border: 0; }
.hb-vol::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: #fff; border: 0; }
/* Folded away until the speaker is pointed at, opening and closing smoothly (moments.js keeps
   .vol-open on the row while the pointer is in it). */
.hb-row:not(.vol-open) .hb-vol {
  width: 0; margin-right: 0; opacity: 0; visibility: hidden;
  transition: width .2s ease, margin .2s ease, opacity .15s ease, visibility 0s .2s;
}

/* THE CONTROLS AT YOUTUBE'S SIZE, as Heart Photos' (George, Oct 10, 2026: "Propertionally increase all our video
   control font sizes to match YouTube's"). Measured that day on youtube.com at 1920 by 1080: 40px round buttons,
   the time in 14px at weight 500; the speed and CC a quarter bigger with them (moments.js has the icons' sizes). */
.hb-bar .hb-btn { min-width: 2.5rem; height: 2.5rem; }
.hb-bar .hb-pp, .hb-bar .hb-volume .hb-mute { width: 2.5rem; }
.hb-bar .hb-volume { height: 2.5rem; }
.hb-bar .hb-btn.hb-cc, .hb-bar .hb-btn.hb-rate { height: 2.5rem; padding: 0 .95rem; font-size: 1rem; }
.hb-bar .hb-cc[aria-pressed="true"]::after { bottom: 8px; }
.hb-bar .hb-time { height: 2.5rem; font-size: .875rem; font-weight: 500; }
.hb-bar .hb-vol { width: 5rem; margin-right: .95rem; }
/* ...and the speaker's waves shrink into it on mute and grow out of it on unmute, as YouTube's (moments.js turnSpeaker). */
.hb-mute .hb-wave { transform-box: fill-box; transform-origin: left center; }
.hb-mute.waves-in .hb-wave { animation: hb-wave-in .22s ease-out both; }
.hb-mute.waves-in .hb-wave-2 { animation-delay: .05s; }
.hb-mute.waves-out .hb-wave { animation: hb-wave-out .17s ease-in both; }
.hb-mute.big-in .hb-wave-2 { animation: hb-wave-in .2s ease-out both; }
.hb-mute.big-out .hb-wave-2 { animation: hb-wave-out .15s ease-in both; }
@keyframes hb-wave-in { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes hb-wave-out { from { transform: scale(1); opacity: 1; } to { transform: scale(0); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .hb-mute .hb-wave { animation: none !important; } }
/* The speed, in Heart Photos' YouTube-style pills (George, Oct 10, 2026: "use the new heart.photos playback
   speed style on vi.ki"): a pill saying it, beside the round controls, and the chips it opens over the
   video, pills of faint white with no edge, the one chosen brighter, on a rounded dark panel. */
.hb-rate-wrap { position: relative; flex: none; display: inline-flex; }
.hb-btn.hb-rate {
  height: 2.25rem; padding: 0 .8rem; border-radius: 999px; background: rgba(255, 255, 255, .08);
  font: 600 .82rem/1 system-ui, sans-serif; font-variant-numeric: tabular-nums; transition: background .15s ease;
}
.hb-btn.hb-rate:hover, .hb-btn.hb-rate:focus-visible, .hb-btn.hb-rate[aria-expanded="true"] { background: rgba(255, 255, 255, .2); }
/* The chips open below the pill, and above it where the window has no room below (moments.js rateOpen; George,
   Oct 10, 2026: "open the playback speed below the button not above unless there isn't room in the browser to
   show it below"). */
.hb-rate-pop {
  position: absolute; right: 0; top: calc(100% + .6rem); z-index: 5;
  display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; padding: .6rem;
  width: max-content; max-width: calc(100cqw - 1rem);
  border-radius: .9rem; background: rgba(28, 28, 28, .92); box-shadow: 0 6px 24px rgba(0, 0, 0, .45);
}
.hb-rate-pop[hidden] { display: none; }
.hb-rate-pop.up { top: auto; bottom: calc(100% + .6rem); }
.hb-rate-chip {
  min-width: 3.25rem; padding: .55rem .9rem; border: 0; border-radius: 999px; background: rgba(255, 255, 255, .1);
  color: #fff; font: 500 .82rem/1 system-ui, sans-serif; font-variant-numeric: tabular-nums; cursor: pointer;
  transition: background .15s ease;
}
.hb-rate-chip:hover, .hb-rate-chip:focus-visible { background: rgba(255, 255, 255, .2); }
.hb-rate-chip[aria-pressed="true"] { background: rgba(255, 255, 255, .34); font-weight: 700; }
/* YouTube's logo, at the right just before captions (George, Oct 1, 2026: first "Source: {YouTube
   logo}", then without "Source:"; Oct 9, 2026: "move the YouTube button to the right just before
   the CC button"): one link to the video on YouTube in a new tab. The logo is YouTube's red play
   mark beside the name, as in YouTube's own "Watch on" link: since Oct 10, 2026 YouTube's own
   logo, its 2024 drawing ("make the YouTube logo exactly as the official YouTube logo"). */
.hb-source {
  display: inline-flex; align-items: center; flex: 0 1 auto; min-width: 0;
  height: 2.25rem; padding: 0 .45rem; border-radius: 6px;
  color: #eee; text-decoration: none; font: 500 .78rem/1 system-ui, sans-serif; white-space: nowrap;
}
/* No underline, even over the site's own a:hover (George, Oct 1, 2026). */
.hb-source:hover, .hb-source:focus-visible { background: rgba(255, 255, 255, .12); text-decoration: none; }
/* The name goes when the row runs out of room, and only then (George, Oct 1, 2026: "lose
   'YouTube' if there isn't enough space"): the link is the one thing in the row that shrinks,
   and the logo's box is one line tall, so once it is narrower than the mark and the name the
   name wraps onto a second line that is never shown. The red mark always stays. */
.hb-source-logo {
  display: flex; flex-wrap: wrap; align-items: center; column-gap: 1.93px;
  height: 18px; overflow: hidden; min-width: 26.1px;
}
/* The gap is the logo's own between the button and the wordmark, at this height. */
.hb-source-logo svg { display: block; flex: none; }
/* The seek row: the played part in YouTube's red, from --hb-seek, which moments.js keeps. */
.hb-seek {
  display: block; width: 100%; height: 1.1rem; margin: 0; background: none; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.hb-seek::-webkit-slider-runnable-track {
  height: 3px; border-radius: 1.5px;
  background: linear-gradient(to right, #e62117 var(--hb-seek, 0%), rgba(255, 255, 255, .28) var(--hb-seek, 0%));
}
.hb-seek::-moz-range-track {
  height: 3px; border-radius: 1.5px;
  background: linear-gradient(to right, #e62117 var(--hb-seek, 0%), rgba(255, 255, 255, .28) var(--hb-seek, 0%));
}
/* As thin as YouTube's (George, Oct 10, 2026: "change the thickness of the seek bar to match youtube's
   thickness"): a 3px line and a 12px knob, as the volume's. */
.hb-seek::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; margin-top: -4.5px; border-radius: 50%; background: #e62117; border: 0; }
.hb-seek::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: #e62117; border: 0; }
/* Full screen, as YouTube's (George, Oct 10, 2026: "clicking fullscreen in vi.ki video controls chops off the
   bottom part of the video where the controls are (it shouldn't) instead do what youtube does in full screen:
   overlay the controls over the video and hide them very soon after the mouse stops moving and show them only
   when needed"). The picture takes the whole screen and the bar lies over its foot on a shade, going with the
   pointer while the video plays and nothing moves (.hb-idle, moments.js). With the bar under it the player
   was shorter than the screen, so the picture, fitted to the width of a frame made taller by the band, lost
   its top and foot. The band YouTube's title hides in is kept only where the screen is no wider than the
   video: wider, the picture fits the screen's height, and any band would cut it. */
.hb-stage:fullscreen { display: block; position: relative; background: #000; container-type: size; }
.hb-stage:fullscreen .hb-player { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 0; }
:root[data-controls="viki"] .hb-stage:fullscreen .hb-player {
  --hb-yt-band: min(clamp(64px, calc(100cqw * var(--hb-ar-h) / var(--hb-ar-w) * .15), 150px),
    max(0px, calc((100cqh - 100cqw * var(--hb-ar-h) / var(--hb-ar-w) + 1px) * 1000)));
}
:root[data-controls="viki"] .hb-stage:fullscreen .hb-player.live:not(.failed) + .hb-bar {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; padding: 3rem 1rem .6rem;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, .75)); transition: opacity .2s ease;
}
.hb-stage:fullscreen.hb-idle .hb-bar { opacity: 0; pointer-events: none; }
.hb-stage:fullscreen.hb-idle, .hb-stage:fullscreen.hb-idle * { cursor: none !important; }
/* The bar fits the player it sits under (the dock is the container): the volume slider goes first,
   then the speed; mute, captions, full screen and the way to YouTube always stay. */
@container (max-width: 30rem) {
  .hb-vol { display: none; }
}
@container (max-width: 25rem) {
  .hb-rate-wrap { display: none; }
}
.hb-dock { background: var(--bg); border: 0; border-radius: 0; padding: 0; }
.hb-player {
  position: relative; width: 100%; background: #000; border-radius: 8px; overflow: hidden;
  margin: 0; min-height: 0; scroll-margin-top: calc(var(--header-h) + .6rem);
  aspect-ratio: auto;
  height: calc(100cqw * var(--hb-ar-h) / var(--hb-ar-w) + var(--hb-buffer));
}
.hb-player img.hb-facade { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
.hb-player iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* The whole still is the button: the link covers the frame and centers its own label, so it is
   one link, keyboard-reachable, named "Play", working with no JavaScript. The pill is drawn
   behind the word by a pseudo-element at z-index -1 inside the link's own stacking context. */
.hb-player .hb-play {
  position: absolute; inset: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 600; text-shadow: 0 1px 3px rgba(0, 0, 0, .9);
}
.hb-player .hb-play::before {
  content: ""; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 7.5rem; height: 3rem; border-radius: 999px; background: rgba(0, 0, 0, .8);
  z-index: -1; transition: background .15s ease;
}
.hb-player .hb-play:hover, .hb-player .hb-play:focus-visible { text-decoration: none; }
.hb-player .hb-play:hover::before, .hb-player .hb-play:focus-visible::before { background: rgba(0, 0, 0, .92); }
/* The title is for a screen reader: a control named only "Play" says nothing. */
.hb-player .hb-title {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.hb-player.live .hb-facade, .hb-player.live .hb-play, .hb-player.live .hb-title { display: none; }
.hb-player.failed { height: auto; min-height: 0; background: var(--tint); color: var(--ink); }
.hb-player.failed .hb-facade, .hb-player.failed .hb-play, .hb-player.failed .hb-title, .hb-player.failed iframe { display: none; }
.hb-player .hb-fail { margin: 0; padding: 1rem; }
/* The bar that used to sit above the pinned video (its time and a Close) is gone: the header's
   video button shows and hides it now (George, 16 September 2026). `display: none` rather than
   merely hidden, because a flex item that is hidden still claims its track. */
.hb-dock-bar { display: none; margin: 0; }
.related > .hb-dock { margin: 0 0 1rem; }
@media (min-width: 60.01rem) { .related > .hb-dock { margin-top: calc(-1 * var(--main-top, 0px)); } }
/* A WIDE, SHORT WINDOW shows the whole player (George, 16 September 2026): in the sidebar the
   stage is at most as wide as lets the picture, YouTube's band and Vi.ki's bar fit between the
   header and the foot of the window, and it is centered in the column. At the top of the page the
   countdown stands between the header and the video too, so its height counts (--hb-above,
   measured by moments.js, guessed here for the first paint); once the video sticks, that room goes
   to the moments under it. 200px is the narrowest YouTube allows an embedded player; a window too
   short even for that keeps it. The moments keep the column's full width. */
:root:has(body > .countdown) { --hb-above: 6.8rem; }
:root[data-clock="hidden"]:has(body > .countdown) { --hb-above: 0px; }
.related > .hb-dock {
  --hb-fit: max(200px, calc((100vh - var(--hb-top, var(--header-h)) - var(--hb-above, 0px) - var(--hb-buffer) - var(--hb-bar-h, 0px)) * var(--hb-ar-w) / var(--hb-ar-h)));
}
.related > .hb-dock .hb-stage { max-width: var(--hb-fit); margin-inline: auto; }
.related > .hb-dock .hb-stage:fullscreen { max-width: none; }
/* A portrait video in the sidebar would be taller than the window at the column's width. The
   same padding cap as the pinned dock, against 70vh, keeps it on screen. */
.related > .hb-dock.hb-dock--portrait {
  padding-inline: max(0px, calc((100% - 70vh * var(--hb-ar-w) / var(--hb-ar-h)) / 2));
}
.hb-dock[hidden] { display: none; }
/* A reader who hid the video keeps it hidden (theme.js marks <html> before the first paint;
   moments.js then hides the dock itself and hands the sidebar back). */
:root[data-video="off"] .related > .hb-dock { display: none; }
/* The players moments.js keeps warm for a quick switch back: in the box, the same size, unseen. */
.hb-player iframe.hb-standby { visibility: hidden; pointer-events: none; }

/* --- the moments under the video ---------------------------------------------------------------
   Under the player in the sidebar, every moment on the page, the playing one marked (George,
   16 September 2026). It travels with the stuck video, so it is in view while the reader is, and
   when it is longer than the room left under the player it scrolls on its own, like the list
   beside a YouTube video. It has no heading (the row is a moment's), and it runs to the foot of
   the window: moments.js sets its max-height to the room below it. A pinned player at the foot of
   a narrow window has no room for it. */
.hb-moments { margin: .35rem 0 0; }
.hb-moments[hidden], .hb-dock--bottom .hb-moments { display: none; }
@media (max-width: 60rem) { .hb-moments { display: none; } }
.hb-moments-list {
  position: relative; list-style: none; margin: 0; padding: 0 .2rem 0 0;
  max-height: calc(100vh - var(--hb-top, var(--header-h)) - min(100cqw, var(--hb-fit, 100cqw)) * var(--hb-ar-h) / var(--hb-ar-w) - var(--hb-buffer) - var(--hb-bar-h, 0px) - .35rem);
  min-height: 6rem; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
}
.hb-moments-list li { margin: 0; }
.hb-moments-list ol { list-style: none; margin: 0; padding: 0; }
.hb-mv + .hb-mv { margin-top: .5rem; }
/* Each video's title stays at the top of the list while its moments scroll under it. */
.hb-mv-h {
  position: sticky; top: 0; z-index: 1; margin: 0; padding: .3rem .45rem;
  background: var(--bg); color: var(--muted); font-size: .8rem; font-weight: 600; line-height: 1.35;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
.hb-moments-list .moment-ts {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .6rem; align-items: baseline;
  padding: .35rem .45rem; border-radius: 6px; font-weight: 600;
}
.hb-moments-list .moment-ts:hover, .hb-moments-list .moment-ts:focus-visible { background: var(--tint); }
/* One line each, so more of them are in view (George, 16 September 2026). */
.hb-moments-list .hb-moment-text {
  color: var(--ink); font-weight: 400; font-size: .88rem; line-height: 1.4;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hb-moments-list .moment-ts:hover .hb-moment-text { color: var(--moment); }
.hb-moments-list li.playing .moment-ts { background: var(--tint); box-shadow: inset 3px 0 0 var(--moment); }
.hb-moments-list li.playing .moment-ts time {
  border-color: var(--moment); background: color-mix(in srgb, var(--moment) 22%, transparent); color: var(--ink);
}

/* Pinned: a band across the foot of the window, not a card floating over one corner. Half the
   viewport is the ceiling, so the reading column always keeps the larger share. The player is
   capped by PADDING on the dock, because the box is sized from the dock's content width: the
   padding centers a content box of at most the capped width, and 100cqw is that width. The cap
   is written as a width, from the height the picture-plus-band may take. It has a floor: on a
   short landscape phone half the viewport would leave the picture a sliver, so the picture is
   never narrower than 320px (and the page under it keeps scrolling, because the body's padding
   follows the dock's published height). Where the cap is wider than the window, as on a phone
   held upright, the player runs edge to edge with no padding at all, so the picture is as big
   as the screen allows (George, 16 September 2026). */
.hb-dock.hb-dock--bottom {
  --hb-cap: max(calc((50vh - var(--hb-buffer)) * var(--hb-ar-w) / var(--hb-ar-h)), 320px);
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; width: auto;
  background: #000;
  padding: 0 max(0px, calc((100% - var(--hb-cap)) / 2)) env(safe-area-inset-bottom, 0px);
}
/* No band under the picture on the pinned dock: the box is the picture, and YouTube's controls
   sit over its foot as they do anywhere else, which leaves the most room for the video. */
.hb-dock.hb-dock--bottom { --hb-buffer: 0px; }
.hb-dock.hb-dock--bottom .hb-player { border-radius: 0; }
/* The text fades to black into the video above it rather than stopping at a hard edge (George,
   16 September 2026). The fade is the dock's own, so it goes wherever the dock goes, and it lets
   clicks through to the text under it. */
.hb-dock.hb-dock--bottom::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: 2.5rem;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0), #000);
  pointer-events: none;
}
body.has-dock.dock-bottom { padding-bottom: calc(var(--hb-dock-h, 0px) + 2.5rem); }
/* PHONE, PORTRAIT: a vertical video takes at most half the screen's height; the cap above does
   that. The page needs more room at its foot than the landscape reservation leaves. */
@media (orientation: portrait) and (max-width: 60rem) {
  body.has-dock.dock-bottom.dock-portrait { padding-bottom: min(62vh, 34rem); }
}

/* --- links that leave the site ---------------------------------------------------------------
   George, 2026-09-12: a mark after every link that takes the reader away from vi.ki. The
   renderer writes class="ext" on every absolute URL in a page's prose (render/impl.go, which
   also gives them rel="noopener" target="_blank"), and the handful of hand-written links in the
   templates carry the class too. The dock's Play control does not: it is a player, not a way
   out. A masked shape over currentColor rather than an arrow character; it takes the link's
   own color, needs no font to be installed, and a screen reader announces nothing extra, which
   an arrow glyph ("north east arrow") does.

   The mark never wraps onto a line of its own (George, 16 September 2026). An inline-block
   leaves a break opportunity before it, so the mark is an inline box instead: its only content
   is a word joiner (U+2060), which forbids a break between it and the link's last word, and the
   shape is painted in its padding. */
:root {
  --ext-mark: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M7 17 17 7"/%3E%3Cpath d="M10 7h7v7"/%3E%3C/svg%3E');
}
a.ext::after {
  content: "\2060"; display: inline; padding-left: .82em;
  background-color: currentColor; opacity: .8;
  -webkit-mask: var(--ext-mark) no-repeat right 68% / .58em .58em;
  mask: var(--ext-mark) no-repeat right 68% / .58em .58em;
}
a.ext:hover::after, a.ext:focus-visible::after { opacity: 1; }

/* --- Related ------------------------------------------------------------------------------------
   The viki's other pages, in the article just above See also, as See also's cards (George,
   16 September 2026; it was "More pages", two columns of titles, and before that the rail). */
.related-pages .all { margin: .8rem 0 0; font-size: .9rem; }

/* --- See also ---------------------------------------------------------------------------------
   The last section of an article: where to go next, with a picture each (George, 2026-09-12).
   The pages come from the article's own links (server/seealso.go) and the picture from what each
   one leads with: its first photograph, else the thumbnail of its first moment's video. The
   cards are the hub's viki cards at article scale, so the site has one card, not two. */
.see-cards {
  list-style: none; margin: 1rem 0 0; padding: 0;
  display: grid; gap: .9rem; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
}
.see-card > a {
  display: block; height: 100%; color: var(--ink); background: var(--tint);
  border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.see-card > a:hover, .see-card > a:focus-visible {
  text-decoration: none; transform: translateY(-2px); box-shadow: var(--shadow);
  border-color: color-mix(in srgb, var(--link) 45%, var(--line));
}
/* A category page's cards (/guides, /tools, /bosses): the window's width (body.full), each card at least 20rem,
   about twice what it was in the old column, and five to a row at most, as the hub's shelves (George, Oct 14,
   2026: "double the size of the thumbnails and allow up to 4 on one row when space allows", then "make it max 5
   thumbnails per row for /guides /tools and all other vi.ki pages that have a wall of thumbnails"). */
.category-cards { grid-template-columns: repeat(auto-fill, minmax(min(100%, max(20rem, calc((100% - 4 * 1.2rem) / 5))), 1fr)); gap: 1.2rem; }
.category-cards .see-text { padding: .75rem .95rem .9rem; }
.category-cards .see-text strong { font-size: 1.12rem; }
.category-cards .see-sum { font-size: .92rem; }
.see-img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; background: var(--bg); }
.see-blank { background: linear-gradient(160deg, var(--tint), var(--bg)); }
.see-text { display: block; padding: .55rem .7rem .7rem; }
.see-text strong { display: block; font-size: .98rem; font-weight: 600; line-height: 1.3; }
/* Two lines of the summary, whatever its length: the card is a signpost, not the page. */
.see-sum {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  margin: .25rem 0 0; color: var(--muted); font-size: .84rem; line-height: 1.4;
}
@media (prefers-reduced-motion: reduce) {
  .see-card > a { transition: none; }
}

/* PHONE, LANDSCAPE: the screen is short and wide, so the pinned video takes the right half,
   under the header and beside the text, instead of a band across the foot (George, 16 September
   2026). moments.js pins it from the first paint. The picture is as wide as the half allows and no
   taller than the height left under the header (the cap, as padding); the header keeps the full
   width, and the page's own blocks make room on the left. The query is moments.js's too. */
@media (max-width: 60rem) and (orientation: landscape) and (max-height: 33rem) {
  .hb-dock.hb-dock--bottom {
    --hb-cap: calc((100dvh - var(--hb-top, var(--header-h)) - var(--hb-bar-h, 0px)) * var(--hb-ar-w) / var(--hb-ar-h));
    top: var(--hb-top, var(--header-h)); bottom: 0; left: auto; right: 0; width: 50vw;
    padding: 0 max(env(safe-area-inset-right, 0px), calc((50vw - var(--hb-cap)) / 2)) 0 max(0px, calc((50vw - var(--hb-cap)) / 2));
    overflow: hidden;
  }
  .hb-dock.hb-dock--bottom::before { display: none; }
  body.has-dock.dock-bottom, body.has-dock.dock-bottom.dock-portrait { padding-bottom: 0; }
  body.has-dock.dock-bottom > main,
  body.has-dock.dock-bottom > footer.site,
  body.has-dock.dock-bottom > .countdown { margin-right: 50vw; }
}

/* --- back to top ------------------------------------------------------------------------------
   Worth offering only once the reader has left the top, so it starts invisible and account.js
   adds .on. visibility, not display, so the transition can run and the link leaves the tab order
   while it is not offered. It sits above the pinned dock by the dock's published height.

   It stands at the bottom-RIGHT corner of the text (George, 13 September 2026, the same place as
   on heartipedia), which on a wide screen is well in from the window's edge, left of the video
   column: account.js measures the column and publishes the distance from the window's right edge
   as --to-top-right. The 1rem fallback is the text's own right edge on a phone, and what a reader
   with no JavaScript gets. */
.to-top {
  position: fixed; right: var(--to-top-right, 1rem); z-index: 41;
  bottom: min(calc(1rem + var(--hb-dock-h, 0px)), calc(100dvh - var(--header-h) - 4.5rem));
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.75rem; height: 2.75rem; border-radius: 999px;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
  box-shadow: var(--shadow);
  opacity: 0; visibility: hidden; transform: translateY(.35rem);
  transition: opacity .18s ease, transform .18s ease, visibility 0s linear .18s;
}
.to-top.on { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
.to-top:hover, .to-top:focus-visible { text-decoration: none; color: var(--link); border-color: var(--link); }
.to-top svg { display: block; }
@media (prefers-reduced-motion: reduce) {
  .to-top, .to-top.on, .viki-card { transition: none; }
}

/* --- keyboard shortcuts ------------------------------------------------------------------------
   A <dialog> opened with showModal(), so Escape, the focus trap and the backdrop are the
   browser's job. Everything here is a token, so both palettes come free. */
.hotkeys {
  border: 1px solid var(--line); border-radius: 10px; padding: 1.25rem 1.4rem;
  background: var(--bg); color: var(--ink); box-shadow: var(--shadow);
  max-width: min(30rem, calc(100vw - 2rem)); font-size: .95rem;
}
.hotkeys::backdrop { background: rgba(0, 0, 0, .45); }
.hotkeys h2 { margin: 0 0 .9rem; font-size: 1rem; }
/* One grid per list, the keys in a column that never wraps ("Ctrl + /" stays on one line, George,
   16 September 2026) and is as wide in both lists, so the descriptions line up. */
.hotkeys dl {
  margin: 0 0 1.1rem; display: grid; gap: .55rem 1rem; align-items: baseline;
  grid-template-columns: minmax(9.5rem, max-content) minmax(0, 1fr);
}
.hotkeys dl > div { display: contents; }
.hotkeys dt { margin: 0; white-space: nowrap; }
.hotkeys dd { margin: 0; color: var(--muted); }
.hotkeys h3 {
  margin: 0 0 .6rem; font-size: .78rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted);
}
.hotkeys kbd {
  font: inherit; font-size: .85em; padding: .12rem .4rem; border-radius: 5px;
  border: 1px solid var(--line); background: var(--tint); color: var(--ink);
}
.hotkeys .or { color: var(--muted); font-size: .85em; padding: 0 .15rem; }
.hotkeys .viewer-keys { display: none; }
/* Opening straight onto a photograph (theme.js): the page is not drawn under the viewer until the
   viewer is up and has been closed, so nothing of the page shows and then jumps away; the ground
   is the viewer's own dark. */
:root[data-viewer="opening"] { background: rgb(6 6 8); overflow: hidden; }
:root[data-viewer="opening"] body { visibility: hidden; }
:root[data-viewer="opening"] .viewer, :root[data-viewer="opening"] .cw-viewer { visibility: visible; }
:root:has(#viewer) .hotkeys .viewer-keys { display: block; }
/* With a photograph OPEN, the list is the viewer's keys and nothing else: the page's keys are not
   the ones under the hand, and the heading over the only section left says nothing either
   (George, 19 September 2026). */
:root:has(#viewer[open]) .hotkeys .page-keys,
:root:has(#viewer[open]) .hotkeys .viewer-keys-h { display: none; }
.hotkeys form { margin: 0; text-align: right; }
@media (max-width: 30rem) {
  .hotkeys dl { grid-template-columns: minmax(0, 1fr); gap: .15rem; }
  .hotkeys dd { margin-bottom: .45rem; }
}

/* --- the 404 ---------------------------------------------------------------------------------- */
.notfound p { color: var(--muted); }
.notfound .button { margin-top: .4rem; }
/* A 404 that helps (server/lost.go): why, a search box already holding the address's words, what
   the address was most likely reaching for, and the ways into everything else. */
.lost h1 { margin-bottom: .35rem; }
.lost .lost-why { margin: 0 0 1.2rem; }
.lost .lost-guess { margin: 0 0 1.2rem; color: var(--ink); font-size: 1.1rem; }
.lost .lost-guess a { font-weight: 600; }
.lost .lost-write { color: var(--ink); }
.lost-search { margin: 0 0 1.2rem; max-width: 36rem; }
.lost-search label { display: block; margin: 0 0 .35rem; font-weight: 600; color: var(--ink); }
.lost-search .lost-row { display: flex; gap: .5rem; }
.lost-search input {
  flex: 1 1 auto; min-width: 0; padding: .55rem .75rem; font: inherit;
  color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
}
.lost-search .button { margin: 0; }
.lost-near { margin: 1.8rem 0 0; }
.lost-near h2 { margin: 0 0 .2rem; font-size: 1.1rem; }
.lost .lost-ways { margin: 1.8rem 0 0; }

/* A Play or moment clicked before the player loaded is held until it does (theme.js): the pointer
   says it is on its way. */
:root[data-play-held], :root[data-play-held] a { cursor: progress; }

/* ---- Vi.ki Premium (server/premium.go, start.go; 2 October 2026) ---- */
/* The pricing table is Heart Pay's <heart-pricing>, dark by default with its blue buttons, as on
   heart.photos/pricing; its panels, text and lines take Vi.ki's own colors so it sits in either theme. */
.pricing > .lead, .pricing-faq { max-width: 44rem; }
heart-pricing {
  display: block; margin: 1.6rem 0 2.4rem;
  --hp-bg: var(--bg); --hp-panel: var(--tint); --hp-panel-2: var(--bg);
  --hp-ink: var(--ink); --hp-muted: var(--muted); --hp-line: var(--line);
}
:root[data-theme="light"] heart-pricing { --hp-good: #2da44e; }
.pricing-faq h3 { margin: 1.3rem 0 .3rem; font-size: 1.05rem; }
.pricing-faq p { margin: 0 0 .6rem; }
.start .plan-line, .me-plan p { max-width: 44rem; }
.start-form { max-width: 34rem; margin: 1.2rem 0 1.8rem; }
.slug-input { display: flex; align-items: center; gap: .35rem; }
.slug-input input { flex: 1 1 auto; min-width: 0; }
.slug-apex { color: var(--muted); white-space: nowrap; }
.owned-vikis { padding-left: 1.2rem; }
.owned-vikis li { margin: .45rem 0; }
.manage-plan { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin: .8rem 0 1.4rem; }
.people-list { list-style: none; padding: 0; max-width: 34rem; }
.people-list li { display: flex; align-items: center; gap: .8rem; padding: .4rem 0; border-bottom: 1px solid var(--line); }
.people-list li span { min-width: 0; overflow-wrap: anywhere; }
.people-list form.inline { margin-left: auto; }
.people-add { max-width: 34rem; }
.keep-form { max-width: 44rem; }
.keep-list { list-style: none; padding-left: 0; }
.keep-list label { display: inline-flex; align-items: center; gap: .45rem; cursor: pointer; }

/* --- error codes (/docs/errors) ---------------------------------------------------------------- */
/* Every code, filtered as you type; a code's own address (#VK-0190) lights its row. */
.errors-find {
  width: 100%; margin: .8rem 0 .3rem; padding: .55rem .75rem; font: inherit;
  color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
}
.errors-find:focus { outline: none; border-color: var(--accent); }
.errors-shown { margin: 0 0 .4rem; font-size: .85rem; color: var(--muted); }
.errors-table td.code { white-space: nowrap; }
.errors-table td.from { color: var(--muted); white-space: nowrap; }
.errors-table tr:target td { background: var(--tint); box-shadow: inset 0 -2px 0 var(--accent); }

/* Steam players at the top of a viki's home page (players.js; George, Oct 14, 2026: "show the same interactive chart
   as on steamdb"): SteamDB's yellow-green line and gray daily average, its three figures and its zoom row. */
:root { --players: #c6f000; }
:root[data-theme="light"] { --players: #5d8a00; }
section.players { margin: 1rem 0 1.5rem; padding: 1rem 1rem .75rem; border: 1px solid var(--line); border-radius: 12px; background: var(--tint); }
section.players[hidden] { display: none; }
.players-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .5rem; text-align: center; margin-bottom: .75rem; }
.players-stat strong { display: block; font-size: clamp(1.25rem, 4vw, 1.9rem); font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.players-stat span { display: block; font-size: .8rem; color: var(--muted); }
.players-zoom { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; margin-bottom: .5rem; }
.players-zoom-label { font-size: .8rem; color: var(--muted); margin-right: .15rem; }
.players-zoom-btn { font: inherit; font-size: .8rem; font-weight: 600; padding: .15rem .55rem; border: 1px solid var(--line); border-radius: 6px; background: transparent; color: var(--link); cursor: pointer; }
.players-zoom-btn[aria-pressed="true"] { background: var(--line); color: var(--ink); }
.players-chart { position: relative; min-height: 200px; }
.players-svg { display: block; width: 100%; height: auto; touch-action: pan-y; outline: none; }
.players-svg:focus-visible { box-shadow: 0 0 0 2px var(--link); border-radius: 4px; }
/* A chart showing less than everything is dragged along the time (players.js; George, Oct 20, 2026: "Add click and pan
   to steam charts not just zoom"). */
.players-svg.players-pannable { cursor: grab; }
.players-panning, .players-panning .players-svg { cursor: grabbing; user-select: none; }
.players-grid { stroke: var(--line); stroke-width: 1; }
.players-tick { stroke: var(--muted); stroke-width: 1; }
.players-axis { fill: var(--muted); font-size: 11px; }
.players-line { fill: none; stroke: var(--players); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.players-avg { fill: none; stroke: var(--muted); stroke-width: 1; opacity: .7; }
.players-cross { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; }
.players-dot { fill: var(--players); stroke: var(--bg); stroke-width: 2; }
.players-tip { position: absolute; pointer-events: none; padding: .35rem .55rem; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--ink); font-size: .8rem; line-height: 1.35; white-space: nowrap; box-shadow: 0 4px 14px rgba(0,0,0,.25); }
.players-tip span, .players-tip strong { display: block; }
.players-tip-when, .players-tip-avg { color: var(--muted); }
.players-wait { color: var(--muted); font-size: .9rem; padding: 4rem 0; text-align: center; margin: 0; }
.players-source { margin: .5rem 0 0; font-size: .75rem; color: var(--muted); padding-right: 2.5rem; }
section.players { position: relative; }
/* Full screen, in the bottom-right corner as a video player keeps it (George, Oct 15, 2026). */
.players-full { position: absolute; right: .5rem; bottom: .4rem; display: grid; place-items: center; width: 2rem; height: 2rem; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--muted); cursor: pointer; }
.players-full:hover, .players-full:focus-visible { color: var(--ink); background: var(--line); outline: none; }
section.players:fullscreen { display: flex; flex-direction: column; margin: 0; border: 0; border-radius: 0; padding: 1.25rem 1.5rem 1rem; background: var(--bg); overflow: hidden; }
section.players:fullscreen .players-chart { flex: 1 1 0; min-height: 0; }
section.players:-webkit-full-screen { display: flex; flex-direction: column; margin: 0; border: 0; border-radius: 0; padding: 1.25rem 1.5rem 1rem; background: var(--bg); overflow: hidden; }
section.players:-webkit-full-screen .players-chart { flex: 1 1 0; min-height: 0; }
section.players.players-window { position: fixed; inset: 0; z-index: 1000; display: flex; flex-direction: column; margin: 0; border: 0; border-radius: 0; padding: 1.25rem 1.5rem 1rem; background: var(--bg); overflow: hidden; }
section.players.players-window .players-chart { flex: 1 1 0; min-height: 0; }
html.players-window-open { overflow: hidden; }
/* What caused each big change: a small circled "i" over the day's peak and its note (players.js; George, Oct 15, 2026:
   "for every major change in the steam player count in our graphs add a little info icon that explains what happened
   to cause the big change"). */
.players-event { position: absolute; z-index: 2; display: grid; place-items: center; width: 16px; height: 16px; padding: 0; border: 1.5px solid var(--muted); border-radius: 50%; background: var(--bg); color: var(--ink); font: italic 700 10px/1 Georgia, "Times New Roman", serif; cursor: pointer; }
.players-event:hover, .players-event:focus-visible, .players-event[aria-expanded="true"] { border-color: var(--players); color: var(--players); outline: none; box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--players); }
.players-event::before { content: ""; position: absolute; inset: -6px; }
.players-note { pointer-events: auto; white-space: normal; width: max-content; max-width: min(18rem, 80vw); z-index: 3; }
.players-note .players-note-text { color: var(--ink); margin-top: .15rem; }
.players-note .players-note-source { display: inline-block; margin-top: .3rem; color: var(--link); }
@media (max-width: 30rem) { section.players { padding: .75rem .6rem .6rem; } .players-stat span { font-size: .7rem; } }
