/* Heartipedia, system fonts only, zero third-party requests before intent.
 *
 * 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 site has a look, and the toggle in the
 * header is how a reader overrides it (theme.js stamps data-theme on <html> before first paint).
 */
:root {
  --ink: #e7e9ec; --muted: #a2a9b3; --line: #2c313a; --bg: #14161a; --tint: #1b1e24;
  --link: #7fb2ff; --accent: #f08a7e; --on-link: #10131a; --strong: navajowhite;
  --moment: #ffab4a;  /* a timestamp is not a web link, so it is not link-blue */
  --footnote: rgb(158, 158, 158);
  --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; --accent: #b3261e; --on-link: #fff; --strong: #8b4513;
  --moment: #a55a00;
  --footnote: rgb(158, 158, 158);
  --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. The color is a
   token, so each of light and dark picks its own, navajowhite on the dark ground, and the
   saddle brown it becomes when the ground is white. */
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); font-size: .95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The theme list 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"], .account-menu button[role="menuitemcheckbox"] {
  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,
.account-menu button[role="menuitemcheckbox"]:hover, .account-menu button[role="menuitemcheckbox"]:focus { background: var(--tint); }
.account-menu button[role="menuitemradio"] .tick, .account-menu button[role="menuitemcheckbox"] .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; }
/* The /settings page draws the same groups as the Settings menu (account.js appearance), so its
   choices wear the menu's rows: a tick on the one chosen, none on the others. */
#appearance-settings { max-width: 22rem; }
#appearance-settings 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;
}
#appearance-settings button[role="menuitemradio"]:hover, #appearance-settings button[role="menuitemradio"]:focus { background: var(--tint); }
#appearance-settings button[role="menuitemradio"] .tick { flex: none; width: .9rem; color: var(--link); }
#appearance-settings button[aria-checked="false"] .tick { visibility: hidden; }
#appearance-settings .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; }

/* --- 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 article, and an encyclopedia page is long by design. --header-h is what
   everything that scrolls to an anchor has to clear. */
:root { --header-h: 4.15rem; }
/* The header sits in the same shell as the page, so the logo starts where the leftmost text
   starts and the avatar ends where the rightmost text ends, the header reads as the top of the
   page rather than as a band laid over it. The shell widens when the contents move into a column
   of their own, so the logo moves out with them; that is what makes it look deliberate rather
   than loose. The middle track holds the search box and takes what is left. */
header.site {
  position: sticky; top: 0; z-index: 30; background: var(--bg);
  max-width: var(--shell); margin: 0 auto;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 1rem;
  /* No rule under the header: the whitespace already says where it ends, and a line across
     the full width of a wide window is the loudest thing on the page. */
  padding: .9rem 1rem;
}
header.site .brand { justify-self: start; }
header.site nav { justify-self: end; }
/* Centered in its track, and free to grow to 600px when the window has the room. */
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); }
header.site .brand { display: inline-flex; align-items: center; gap: .45rem; font-weight: 400; color: var(--ink); font-size: 1.15rem; letter-spacing: .01em; }
header.site .brand-mark { width: 1.45rem; height: 1.29rem; display: block; flex: none; }
header.site .brand:hover { text-decoration: none; color: var(--link); }
header.site nav { display: flex; gap: .8rem; align-items: center; font-size: .95rem; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }

/* --- 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 and for browsers that will not submit on Enter alone: it
   is 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 sign-in button: an outlined pill with a face on it, the shape heart.photos uses and the
   one everyone already reads as "become someone here". The color is this site's link token, so
   it works on both grounds rather than being pinned to the dark one. */
.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; }

.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 {
  /* As wide as what is in it, never wrapping a line (George, Oct 17, 2026: "Don't wrap "Sign
     out""), and never wider than the screen: a long email ends in an ellipsis instead. */
  position: absolute; right: 0; top: calc(100% + .4rem); z-index: 20; width: max-content; max-width: min(24rem, calc(100vw - 1.5rem));
  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; min-width: 0; }
.account-email { font-size: .85rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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; white-space: nowrap;
}
.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): 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 a menu capped at the screen's width. */
.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-add { margin-bottom: .25rem; }
.account-links { margin: .2rem 0 0; padding-top: .25rem; border-top: 1px solid var(--line); }
.account-signout { margin: .45rem 0 0; padding-top: .25rem; }
.account-signout + .account-signout { margin-top: 0; padding-top: 0; }
.account-notice { margin: .3rem .6rem 0; font-size: .8rem; color: var(--accent); }

/* --- Settings ------------------------------------------------------------------------------------
   The gear left of Sign in or the avatar, for every reader, and every setting in its menu, as on
   Vi.ki (George, Oct 21, 2026). The menu is the avatar menu's card. Unlike Vi.ki's it does not
   scroll inside itself (TestOnlyOneScrollbar): it is four short groups, shorter than any 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 { min-width: 13rem; }
.settings-menu .account-themes:first-child { margin-top: 0; padding-top: 0; }
.account-themes[hidden] { display: none; }
/* "Reset defaults", as on Vi.ki and Heart Photos: a pill at the foot of the menu, gray rather than
   gone when there is nothing to put back. */
.account-menu button.settings-reset[role="menuitem"], #appearance-settings button.settings-reset {
  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);
}
#appearance-settings button.settings-reset { margin-left: 0; }
.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, #appearance-settings button.settings-reset:disabled { opacity: .38; cursor: default; }

/* The subject check: shown only to the person the page is about. */
.subject-check { border: 1px solid var(--link); border-radius: 8px; padding: .9rem 1rem; margin: 1.5rem 0; background: var(--tint); }
.subject-check h2 { margin-top: 0; font-size: 1.1rem; }
.subject-check label { display: block; font-size: .9rem; margin-bottom: .25rem; }
.subject-check textarea { width: 100%; font: inherit; padding: .5rem; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); }
.subject-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin: .7rem 0 .3rem; }
.subject-check .button { cursor: pointer; background: var(--link); color: var(--on-link); border-color: var(--link); font: inherit; }
.subject-check .button.secondary { background: var(--bg); color: var(--link); }
.subject-check .button[disabled] { opacity: .6; cursor: progress; }
.subject-current:empty { display: none; }
.subject-current { font-size: .9rem; }
dl.settings > div { display: flex; gap: .6rem; padding: .35rem 0; }
dl.settings dt { min-width: 9rem; color: var(--muted); }
dl.settings dd { margin: 0; }
main { padding-top: 1.5rem; padding-bottom: 3rem; }
footer.site { padding-top: 2.2rem; padding-bottom: 2rem; color: var(--muted); font-size: .85rem; }
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 "Skip to" control floats so the TITLE sets beside it, and only the title. A short
   title ("India") does not reach the foot of that float, and without this the summary's first
   line wrapped around it too, one indented line, for no reason a reader could see. */
p.lead, p.summary { font-size: 1.1rem; color: var(--muted); margin-top: 0; clear: left; }
.notice { background: var(--tint); border-left: 3px solid var(--accent); padding: .6rem .8rem; }
/* The copyright agent's address, set apart so a rights holder finds it at a glance. */
.agent { margin: .4rem 0 1.1rem; padding: .8rem 1rem; border: 1px solid var(--line); border-radius: 8px; background: var(--tint); }
.agent p { margin: 0; }
.agent .who { font-weight: 600; margin-bottom: .2rem; }
.small { font-size: .85rem; color: var(--muted); }
.button { display: inline-block; border: 1px solid var(--link); border-radius: 6px; padding: .45rem .9rem; }
.button:hover { text-decoration: none; background: var(--tint); }
button { font: inherit; }

/* mechanism list on the landing */
.mechanism { padding-left: 1.2rem; }
.mechanism li { margin: .5rem 0; }

/* --- article + sidebar ---------------------------------------------------------------------
   The reading column keeps its width; the sidebar is what an encyclopedia reader reaches for
   when they finish a page, the other pages on the site. It sticks, so it is still there after
   a long article, and it drops below the page on a narrow screen. */
/* 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 one block, including the widening for a docked
   video: as a bare rule it outranked the old max-width override and squeezed a phone's article
   column to a few characters wide. */
/* How wide the Contents rail is when it is a column of its own. */
: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: Contents, then the video and the other pages. Below the
   three-column breakpoint it IS that column, one sticky, self-scrolling box, so the two regions
   can never stick over one another. 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; the reading column gives way if it must.
     33.5rem, not 30: the column has its own padding and border, and the player behind them still
     has to be 480px. */
  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 used to be its own scroller, stuck under the
     header, which meant a second scrollbar beside the first and two things to drag. It scrolls
     with the page now, and only the video stays: a reader who is watching wants it in view, and a
     contents list and a list of other pages are things you go to, not things you keep. */
  .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, so this is what a reader without
     JavaScript gets, the contents at the top, open, which is the honest fallback. */
  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 for it to follow
     the reader down a long article; that is what align-self: stretch above is for. */
  .related > .hb-dock { position: sticky; top: calc(var(--header-h) + 1rem); z-index: 5; }
  /* The gap between the header and a stuck video is a window: the list of other pages scrolls up
     through it and appears above the video, which reads as a rendering fault. This fills the gap
     with the page's own background, anchored to the dock so it travels with it. It reaches a
     little past the dock's edges to cover the column's padding as well. */
  .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. The box has a 10px radius, so the list of other
     pages scrolling up behind it shows through the four corner notches, the rectangular cover
     above never touched those, because they are inside the dock's own bounding box rather than
     above it. A zero-offset, zero-blur shadow paints the page color right around the rounded
     edge and closes them. */
  .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; }

/* --- thumbnails in the rail --------------------------------------------------------------------
   Twenty-two episode titles are twenty-two identical rows, and the frame is what tells them apart
   before the title is read. The picture is 16:9 and fixed-width, so the titles beside it all start
   at the same place; a ragged left edge would cost more than the pictures gain. */
.related .pages li a { color: var(--ink); display: block; }
/* A row with a picture is two columns: the still, then the title beside it, the shape YouTube's
   watch-page sidebar uses, because it is the right one for scanning a list of videos. Rows without
   a picture stay a plain block, so a list of people does not grow an empty column. */
.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="160" height="90" so
     the box is reserved before the picture arrives, and a presentational height attribute is not
     `auto`, which silently disabled the ratio and left these nearly square. */
  height: auto; aspect-ratio: 16 / 9; object-fit: cover;
  border-radius: 6px; background: var(--tint); display: block;
}
.pg-text { min-width: 0; }

/* --- when there is no still ---------------------------------------------------------------------
   YouTube answers a missing maxresdefault with a 404 carrying a 120x90 gray placeholder, which
   decodes as a valid image and so never errors. Rather than show it, the box paints its own: the
   site's own tint with the play control still on it, which reads as "a video" instead of as a
   broken picture. */
.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 { visibility: hidden; }
/* Two lines of title, then the tag under it, as a video list reads. A title that would run to four
   lines is clipped rather than pushing the next row down the column. */
.pg-text {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3;
  overflow: hidden;
}
.related .pages li a:has(.pg-thumb) .kind { margin-top: .15rem; }
.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; }

/* The table of contents: the left column when the window has room for one, and otherwise the head
   of the rail beside the article, a reader mid-way down a long page wants "where am I" before
   "where else could I go". 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; }

/* --- 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 rather than under it, 
   the contents are at the top of the page and cost one line, which is the point. The open list
   overlays the text instead of pushing it down: a menu that reflows the page under the reader's
   finger is worse than one that covers it for a moment. */
.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. The homepage,
   /about/* and any page short enough to have no headings stay two-column at every width, so no
   page ever shows an empty left column.

   The rail wrapper dissolves, so Contents, the article and the other pages become three grid
   items on ONE row, each sticking on its own. 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
   The breakpoints sit a little below the ideal shell, so between them the reading column gives
   way rather than the rail vanishing. */
@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%; }
  /* display: contents leaves the wrapper with no box at all, so its children become the grid
     items and the border and padding move on to them. */
  body.has-toc:not(.has-dock) .rails { display: contents; }
  /* A column of its own is worth keeping in view: it is how a reader mid-article knows where
     they are. It sticks without a scrollbar of its own (the page has exactly one) so a
     contents list longer than the window keeps its tail below the fold while stuck. Every list
     on the site today is a handful of entries. */
  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. A separate query because a media
   condition cannot read a class off <body>; the two selectors are mutually exclusive, so exactly
   one regime is ever live. */
@media (min-width: 92rem) {
  /* The video column takes everything the contents and the reading measure do not. 33.5rem is
     its floor, not its size: the player has a 480px floor of its own and the column carries its
     own padding and border. On a wide screen the reading column stays at its measure and the
     video grows instead, which is the right way round; a wider column of text is not easier to
     read, and a bigger video is easier to watch. */
  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);
  }
  /* No shell cap here. Every other layout is centered inside a measure, because a line of text
     that runs the width of a wide window is unreadable, but this one's third column is a video,
     and the wider the window the more of it should be video. Capping the shell at 120rem threw
     away 640px on a 2560px screen: 313 to the left of the contents and 327 to the right of the
     player, both of them just background. The reading column still stops at its measure and the
     contents at theirs; only the video takes what is left. */
  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; }
  /* Stretched to the row, so the sticky video inside it has the whole article to travel over. */
  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; }
.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; }
th { background: var(--tint); }
.keyfacts { margin: 1rem 0; border: 1px solid var(--line); border-radius: 8px; padding: .4rem .9rem; background: var(--tint); }
.keyfacts div { display: grid; grid-template-columns: 9rem 1fr; gap: .5rem; padding: .32rem 0; }
.keyfacts dt { font-weight: 600; }
.keyfacts dd { margin: 0; }
.timeline { padding-left: 0; list-style: none; margin: 1rem 0 1rem .4rem; }
.timeline li { padding: .2rem 0 .6rem 1rem; }
.timeline time { font-weight: 600; margin-right: .4rem; }
/* A quotation is marked as one by its quotation marks and by the space it sits in, not by a bar
   drawn down its left-hand side. The whole line is the control: click it and it plays, which is
   why the timestamp lights up with it rather than sitting beside it as a separate "Play". */
figure.quote { margin: 1.15rem 0; padding: 0; }
.q-line { display: block; color: inherit; }
.q-line:hover, .q-line:focus-visible { text-decoration: none; }
.q-text { font-size: 1.05rem; line-height: 1.5; }
.q-meta { display: block; margin-top: .15rem; font-size: .9rem; color: var(--muted); }
.q-who { font-style: normal; }
.q-who + .q-at::before { content: "\00b7"; margin: 0 .4rem; }
.q-at {
  font-variant-numeric: tabular-nums; font-weight: 600; color: var(--moment);
  background: var(--tint); border: 1px solid color-mix(in srgb, var(--moment) 45%, transparent);
  border-radius: 4px; padding: 0 .35rem;
}
a.q-line:hover .q-at, a.q-line:focus-visible .q-at {
  border-color: var(--moment); background: color-mix(in srgb, var(--moment) 14%, transparent);
}
a.q-line:hover .q-text { color: var(--moment); }
figure.quote .ref { display: block; margin-top: .15rem; font-size: .85rem; color: var(--muted); }

/* --- a quote you can hear ----------------------------------------------------------------------
   The footer under a quote carries who said it, a control that plays the line, and, where the
   wording came from a machine transcript nobody has checked yet, a plain admission of that. The
   admission sits beside the quote rather than in a box at the foot of the page, because the reader
   deciding whether to trust the line is looking at the line. */
blockquote.quote footer.ref { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; margin-top: .35rem; font-size: .85rem; }
.q-who { color: var(--muted); font-style: normal; font-weight: 600; }
.q-who::before { content: "\2014\00a0"; color: var(--muted); font-weight: 400; }
.q-play { font-variant-numeric: tabular-nums; }

/* Dashed, not red: this is "not checked yet", which is the ordinary state of a new page here, and
   not an error. It reads as a note in the margin rather than a warning. */
.q-flag {
  color: var(--muted); font-size: .8rem; border-bottom: 1px dashed var(--line);
  cursor: help;
}


/* A glossary is a list of terms and what they mean, which is the shape `keyfacts` already has and
   already renders as a <dl> with cited rows. It needs content, not a second block type, so the
   Farscape glossary is a keyfacts block under its own heading, and there is no .glossary rule here
   to drift out of sync with one. */
blockquote footer { font-size: .85rem; color: var(--muted); }
/* The [n] markers are off by default: the references are still at the foot of the page, and
   the text reads better without a number after every clause. The avatar menu turns them on. */
sup.ref { font-size: .7em; line-height: 0; }
/* The footnote marker is gray rather than link-blue: a page carries dozens of them and in blue
   they speckle the prose, pulling the eye to the citation instead of the sentence. Hover and
   keyboard focus restore the link color, so the marker still announces itself as a link at the
   moment a reader reaches for it. */
sup.ref a { color: var(--footnote); text-decoration: none; }
sup.ref a:hover, sup.ref a:focus-visible { color: var(--link); text-decoration: underline; }
:root[data-footnotes="hidden"] sup.ref, :root[data-footnotes="hidden"] blockquote footer.ref { display: none; }
q { quotes: "\201C" "\201D"; }

/* moments */
.moment-block, .moment-list li { margin: .5rem 0; }
/* A moment is a place in a video, not a page somewhere else, so it does not wear link blue. */
.moment-ts, .moment-link { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--moment); }
.moment-ts:hover, .moment-link:hover { text-decoration: none; }
.moment-ts time, .moment-link time { 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-link:hover time,
.moment-ts:focus-visible time, .moment-link:focus-visible time { border-color: var(--moment); background: color-mix(in srgb, var(--moment) 14%, transparent); }
.moment-block.playing { border-left-color: var(--moment); }
.moment-note, .moment-desc { color: var(--muted); display: block; font-size: .95rem; }
.moment-page { display: inline-block; margin-top: .15rem; font-size: .95rem; }   /* a whole episode's own page (Oct 24, 2026) */

/* --- the transcript dashboard (root only) -------------------------------------------------------
   An operator screen, not a page: it exists so one person can read a moment against the captions it
   was drawn from and say "yes, that is what happens there". So the captions are quiet and the
   moments are loud, the opposite of everywhere else on the site, where the moment is a small link
   in a paragraph and the paragraph is the thing.

   No rules here either. The moments stand out by having a ground of their own, which is what a card
   is for; the captions are a column of gray with the clock in a fixed track so the eye can run down
   the times without the text moving under it. */
.admin-transcripts .tx-summary, .admin-transcript .tx-summary { color: var(--muted); font-size: .95rem; margin: .2rem 0 1rem; }
.tx-typed { color: var(--moment); font-weight: 600; }
.tx-machine { color: var(--muted); }
.tx-flag { color: var(--accent); }

.tx-table-wrap { overflow-x: auto; }
.tx-table { width: 100%; border-collapse: collapse; font-size: .92rem; }
.tx-table th { text-align: left; color: var(--muted); font-weight: 600; padding: .35rem .6rem .35rem 0; white-space: nowrap; }
.tx-table td { padding: .3rem .6rem .3rem 0; vertical-align: baseline; }
.tx-table .n { text-align: right; font-variant-numeric: tabular-nums; }
.tx-table tbody tr:hover { background: var(--tint); }

.tx-lines { list-style: none; padding: 0; margin: 1.2rem 0 0; }
.tx-lines li { display: grid; grid-template-columns: 4.5rem 1fr; gap: .6rem; padding: .12rem 0; align-items: baseline; }
/* Every timestamp on this screen plays that second of the episode, so every timestamp is
   the one color that means "playable". Captions were muted gray and only the moments
   were colored, which read as "these few are links and the other seven hundred are not".
   Weight still separates a moment from a caption; color no longer lies about what clicks. */
.tx-at { font-variant-numeric: tabular-nums; color: var(--moment); font-size: .85rem; }
.tx-cue .tx-body { color: var(--muted); font-size: .95rem; }
/* Two transcripts of one episode, side by side. The left column is what the recognizer heard and
   holds the times; the right is what a person typed and holds the words. A moment row still spans
   the full width, because a moment belongs to the episode rather than to either transcript. */
.tx-two li { grid-template-columns: 4.5rem 1fr 1fr; }
.tx-two .tx-moment { grid-template-columns: 4.5rem 1fr; }
.tx-human { font-size: .95rem; }
/* Each line of the second column carries its OWN time, several can sit inside one caption cue,
   and none of them starts where that cue starts. Laid out as its own little row so the times line
   up down the column and can be read against the ones on the left. */
.tx-hline { display: grid; grid-template-columns: 3.2rem 1fr; gap: .5rem; align-items: baseline; }
.tx-approx { color: var(--muted); }
.tx-moment {
  margin: .5rem 0; padding: .5rem .7rem; border-radius: 8px; background: var(--tint);
  scroll-margin-top: calc(var(--header-h, 3.5rem) + 1rem);
}
.tx-moment .tx-at { font-weight: 600; }
.tx-moment .tx-body { display: block; }
.tx-kind {
  display: inline-block; margin-right: .5rem; padding: 0 .4rem; border-radius: 4px;
  font-size: .72rem; letter-spacing: .04em; text-transform: uppercase;
  background: color-mix(in srgb, var(--moment) 18%, transparent); color: var(--moment);
}
.tx-quote .tx-kind { background: color-mix(in srgb, var(--link) 18%, transparent); color: var(--link); }
.tx-episode .tx-kind { background: color-mix(in srgb, var(--muted) 18%, transparent); color: var(--muted); }
.tx-note { display: block; color: var(--muted); font-size: .93rem; margin-top: .15rem; }
.tx-moment .tx-flag { display: block; font-size: .85rem; margin-top: .15rem; }

@media (max-width: 40rem) {
  .tx-lines li { grid-template-columns: 3.6rem 1fr; gap: .4rem; }
  /* Two transcripts do not fit beside each other on a narrow screen, so the second goes UNDER the
     first. It has to be placed explicitly: this rule and .tx-two li have the same specificity, so
     this one wins by coming later, and the second transcript would otherwise fall into the next
     row's first column, the one sized for a clock, about one word wide. It rendered as a column
     of single words. */
  .tx-two li { grid-template-columns: 3.6rem 1fr; }
  .tx-two .tx-human { grid-column: 2; padding-bottom: .3rem; }
}

/* --- previous / next, through a series -----------------------------------------------------------
   A series is a sequence, and after one episode the thing a reader wants is the next one. It sits
   directly under the title because that is where someone who has just finished watching looks.

   Three tracks, and the middle one is the series itself, so the strip also answers "what am I in?".
   The episode names are long, "Farscape s02e11: Look At The Princess Part 1: A Kiss Is But a
   Kiss", so each is given its own track to fill and truncated rather than allowed to wrap the
   strip into a paragraph. On a phone the names go and the direction words carry it: a 375px screen
   has no room for two episode titles and there is nothing to gain from squeezing them in.

   The run is a ring: Previous on the first goes to the last and Next on the last comes back to the
   first. There is no end state to style, because there is no end. */
.epnav {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: baseline;
  gap: .3rem 1rem; margin: .1rem 0 1.2rem; font-size: .95rem;
}
/* Two things have to be true for the names to end in an ellipsis instead of running into each
   other, and the first version had neither.

   A grid item's default min-width is auto, which means "never narrower than your content", so a
   long episode title pushes its own track wider than the 1fr it was given, and the two sides meet
   in the middle. min-width: 0 is what lets a track actually be 1fr.

   And justify-self: start/end sizes an item to its CONTENT rather than to its track, which undoes
   the first fix on its own: the item is as wide as the title, so there is nothing for the title to
   overflow and text-overflow never fires. The items stay stretched; text-align does the aligning. */
.epnav > * { min-width: 0; }
.epnav-prev { text-align: left; }
.epnav-next { text-align: right; }
.epnav-up { text-align: center; color: var(--muted); white-space: nowrap; }
.epnav-up:hover { color: var(--link); }
.epnav-dir { color: var(--muted); font-size: .85rem; letter-spacing: .04em; text-transform: uppercase; }
.epnav-prev .epnav-dir::before { content: "\2190\00a0"; }   /* ← */
.epnav-next .epnav-dir::after { content: "\00a0\2192"; }    /* → */
.epnav-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 40rem) {
  .epnav-name { display: none; }
  .epnav { gap: .3rem .5rem; }
}

/* --- the key moment ------------------------------------------------------------------------------
   One line under the summary, on every page that has a moment. It is the first thing a reader can
   DO here, and on a narrow screen it stands where the resting video used to: a line that says what
   is worth watching, rather than a screenful of picture before a single fact.
   Set apart by the space around it, not by a rule: it is a sentence, not a panel. */
.key-moment { margin: .9rem 0 1.3rem; }
.key-moment .moment-desc { margin-top: .15rem; }
/* The next moments under it, like a video's chapters (Oct 23, 2026): smaller and closer together than
   the key moment, so the one line still leads and these read as what follows. */
.next-moments { margin: -.6rem 0 1.3rem; font-size: .92rem; }
.next-moments ol { list-style: none; margin: 0; padding: 0; display: grid; gap: .25rem; }
.next-moments time { display: inline-block; min-width: 3.2em; color: var(--muted); font-variant-numeric: tabular-nums; }
.next-moments-all { display: inline-block; margin-top: .35rem; font-size: .88rem; }
/* A companion video wiki on Vi.ki, under the summary: one block, one link, in the link color so it
   reads as somewhere to go rather than as a notice. */
.viki-card { margin: 1rem 0 1.4rem; }
.viki-card a { display: grid; gap: .15rem; padding: .85rem 1rem; border: 1px solid var(--link); border-radius: 8px; background: var(--tint); color: var(--ink); }
.viki-card a:hover { text-decoration: none; background: var(--bg); }
.viki-card a:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
.viki-kicker { font-size: .78rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.viki-name { font-size: 1.15rem; font-weight: 600; color: var(--link); }
.viki-desc { color: var(--muted); }
.viki-go { margin-top: .35rem; font-weight: 600; color: var(--link); }
.moment-list { list-style: none; padding: 0; }
.moment-list li { padding: .5rem .6rem; border-radius: 6px; }
.moment-list li .moment-ts { display: inline-flex; gap: .6rem; align-items: baseline; }
.moment-list li .moment-label { color: inherit; }
.moment-list li.selected, .moment-list li.playing { background: var(--tint); outline: 1px solid var(--line); }
/* The paragraph the player is currently sitting under, so it is obvious which timestamp
   opened the video below it. */
.moment-block.playing { background: var(--tint); border-left: 3px solid var(--link); padding: .4rem .6rem; border-radius: 0 6px 6px 0; }
/* The player moves to sit under whichever moment was clicked, so it is laid out to belong
   in the body flow rather than only at the head of the Moments section. */
/* THE BUFFER BOX, the same one Vi.ki gives its player (wowforever.vi.ki, web/static/app.css;
   George, 21 September 2026). The box is the picture's height PLUS a band of 242px, and YouTube
   letterboxes the video inside it -- so its control bar, its captions and the row of thumbnails it
   throws up at the end all render in the band, under the picture, instead of lying over the thing
   the reader came to watch.

   Written as PADDING rather than as Vi.ki's container query, because this player also sits in the
   article's own flow: a percentage padding resolves against the width of the containing block,
   which for a full-width player is the player's own width, and it needs no container. A container
   would be worse than unnecessary here -- `container-type` makes an element the containing block
   for its FIXED descendants, and the pinned dock at the foot of a phone is one of them.

   Two cases keep no band, exactly as Vi.ki has it: a portrait video, which YouTube draws its
   controls over regardless, and the pinned dock on a phone, where the box is the picture and every
   pixel of the window is worth more than a clear control bar. */
.hb-player {
  position: relative; width: 100%; background: #000; border-radius: 8px; overflow: hidden;
  margin: .6rem 0 1.2rem; scroll-margin-top: calc(var(--header-h) + .6rem);
}
.hb-player:not(.failed) {
  aspect-ratio: auto; height: 0; min-height: 0;
  padding-bottom: calc(100% * var(--hb-ar-h, 9) / var(--hb-ar-w, 16) + var(--hb-buffer, 242px));
}
/* CONTAIN, not cover, now that the box is taller than the picture: the still sits exactly where
   YouTube will draw the video -- centered in the frame, with the band around it -- so nothing
   moves when the iframe replaces it. Covering stretched the still over the band as well and the
   picture jumped on the first play. */
.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. Aiming at a pill in the middle of a picture is a small target on a
   phone and a pointless one anywhere: everything a reader can see here does the same thing. The
   link covers the frame and centers its own label, so it is still one link, keyboard-reachable,
   named "Play", and working with no JavaScript, rather than a click handler bolted onto an image. */
.hb-player .hb-play {
  position: absolute; inset: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 600;
}
/* YouTube's own large play button over the still, red, as the standard embed shows it (George, Oct 21, 2026: "on heartipedia for videos that aren't playing, show the standard YouTube embed with its iconic red play button instead of creating our own "Play" button"),
   in place of the black "Play" pill drawn here before. Its shape and colors are YouTube's (the 68 by 48 rounded
   rectangle and the white triangle); the word stays for screen readers. */
.hb-player .hb-yt-btn { width: 68px; height: 48px; display: block; filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .35)); }
/* ...and animated as YouTube's embed animates it (George, Oct 21, 2026: "On heart.id the embedded video has an animated
   YouTube play button when the mouse hovers over it why don't we have the same on heartipedia"): dark and see-through
   at rest, solid red once the pointer is anywhere over the video, with YouTube's own quick ease. A screen without
   hover (a phone) keeps it red. */
.hb-player .hb-yt-bg { fill: #f00; transition: fill .1s cubic-bezier(.4, 0, 1, 1), fill-opacity .1s cubic-bezier(.4, 0, 1, 1); }
@media (hover: hover) {
  .hb-player .hb-yt-bg { fill: #212121; fill-opacity: .8; }
  .hb-player:hover .hb-yt-bg, .hb-player .hb-play:focus-visible .hb-yt-bg {
    fill: #f00; fill-opacity: 1; transition: fill .1s cubic-bezier(0, 0, .2, 1), fill-opacity .1s cubic-bezier(0, 0, .2, 1);
  }
}
.hb-player .hb-play:hover, .hb-player .hb-play:focus-visible { text-decoration: none; }
.hb-player .hb-play:focus-visible { outline: 3px solid #fff; outline-offset: -3px; }
/* The caption strip across the foot of the still is gone. It repeated the moment's own label,
   which is already beside the timestamp in the article, and on a small player it covered the part
   of the picture worth seeing. The text stays in the markup, unseen, because it is what tells a
   screen reader WHICH video the Play link plays; 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 { aspect-ratio: auto; background: var(--tint); color: var(--ink); padding: 1rem; min-height: 0; }

/* --- Heartipedia's video controls ----------------------------------------------------------------
   Vi.ki's, ported whole (George, Oct 21, 2026: "Video controls section just like on aion2.vi.ki but
   replace "Vi.ki" with "Heartipedia" in the Video controls section and make "Heartipedia video
   controls" the default for everyone"); keep the two in step. With them YouTube's are off
   (controls=0, moments.js), the box is the picture with no band, and the bar sits under it in
   .hb-stage, the wrapper moments.js puts round the box so full screen takes both. The rules below
   from "YouTube still draws" on are Vi.ki's own, comments and all, with the attribute renamed.
   .hb-stage is the container the band and the speed chips measure against; it holds nothing fixed,
   so it does not capture the pinned dock (which is its parent, not its child). */
.hb-stage { container-type: inline-size; }
/* The reader's "Video controls overlap" (theme.js), Vi.ki's, for a landscape video in the sidebar
   with YouTube's controls: None, the default, gives YouTube's controls a band of their own (242px,
   above); Minimal lets the tallest of them reach the picture's foot (151px); All lets them lie over
   it (0). A portrait video and the pinned phone player keep no band either way. Heartipedia's
   controls need no band at all, so their rule comes after these and wins. */
: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; }
:root[data-controls="heartipedia"] .hb-dock:not(.hb-dock--portrait):not(.hb-dock--bottom) { --hb-buffer: 0px; }
/* 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 asked for them gone on Vi.ki,
   16 September 2026). With Heartipedia'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="heartipedia"] .hb-player.live iframe { pointer-events: none; }
:root[data-controls="heartipedia"] .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="heartipedia"] .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="heartipedia"] .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="heartipedia"] .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="heartipedia"] .hb-dock { --hb-bar-h: 4rem; }
.hb-bar { display: none; }
:root[data-controls="heartipedia"] .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="heartipedia"] .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="heartipedia"] .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; }
}

/* Heartipedia's box is sized by padding (above) rather than by a height, so in full screen the
   padding goes and the box fills the screen; and the pinned dock's cap is the stage's, less the
   bar, so the picture and its bar together keep to half the window. */
.hb-stage:fullscreen .hb-player:not(.failed) { padding-bottom: 0; margin: 0; }
:root[data-controls="heartipedia"] .hb-dock--bottom { --hb-bar-h: 4rem; }
.hb-dock.hb-dock--bottom .hb-stage {
  width: min(100%, calc((50vh - 1rem - var(--hb-bar-h, 0px)) * var(--hb-ar-w) / var(--hb-ar-h)));
}
.hb-dock.hb-dock--bottom .hb-stage .hb-player { width: 100%; }

/* references + provenance */
.refs li { margin: .3rem 0; font-size: .95rem; }
.refkind { margin-left: .5rem; font-size: .75rem; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.provenance { margin-top: 2rem; border: 1px solid var(--line); border-radius: 8px; padding: .4rem 1rem 1rem; background: var(--tint); }
.provenance h2 { margin-top: .6rem; font-size: 1.1rem; }
.provenance dl div { display: grid; grid-template-columns: 8rem 1fr; gap: .5rem; padding: .25rem 0; }
.provenance dd { margin: 0; }
.coi { border-left: 3px solid var(--accent); padding-left: .6rem; }

/* The sign-in card on a private page somebody reached signed out: one button, into the hand-off. */
.signin-sheet { border: 1px solid var(--line); border-radius: 8px; padding: 1rem; max-width: 22rem; background: var(--bg); }
form.inline { display: inline; }
.admin-table .actions form { display: inline-block; margin: 0 .2rem .2rem 0; }
.admin-table td code { font-size: .85rem; }
.empty { color: var(--muted); }

/* 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 { 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; }
.search-page form.search button { position: static; margin-left: .4rem; border: 1px solid var(--link); border-radius: 999px; background: none; color: var(--link); padding: .35rem .9rem; cursor: pointer; }

/* /docs/errors: the box looks like the header's search, and a row a link points at is lit. */
.errors-find {
  display: block; width: 100%; max-width: 26rem; margin: 1rem 0 .4rem; 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;
}
.errors-find[hidden] { display: none; }
.errors-find::placeholder { color: var(--muted); }
.errors-find:focus { border-color: var(--link); outline: none; box-shadow: 0 0 0 2px color-mix(in srgb, var(--link) 35%, transparent); }
.errors-shown { margin: 0 0 .4rem; font-size: .85rem; color: var(--muted); }
.errors-table td { border-top: 1px solid var(--line); }
.errors-table td.code { white-space: nowrap; font-variant-numeric: tabular-nums; }
.errors-table td.from { color: var(--muted); white-space: nowrap; }
.errors-table tr:target td { background: color-mix(in srgb, var(--link) 16%, transparent); }

/* the front-page index: every page one click from the front door */
.index-jump { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin: .8rem 0 1.4rem; font-size: .92rem; }
.index-group { margin-bottom: 1.6rem; }
.index-group h3 { margin: 0 0 .5rem; font-size: 1.05rem; }
.page-list { list-style: none; padding: 0; margin: 0; }
.page-list li { padding: .55rem 0; }
.page-list li > a { font-weight: 600; font-size: 1.05rem; }
.page-list p { margin: .2rem 0 0; color: var(--muted); font-size: .93rem; }
.page-list .kind { font-size: .75rem; color: var(--muted); margin-left: .6rem; text-transform: uppercase; letter-spacing: .05em; }
/* New pages: the short list on the home page and the day headings of /new */
.new-list { list-style: none; padding: 0; margin: .4rem 0 .5rem; }
.new-list li { padding: .3rem 0; }
.new-list li > a { font-weight: 600; }
.new-list time { color: var(--muted); font-size: .88rem; margin-left: .5rem; white-space: nowrap; }
.home-new { margin-bottom: 1.8rem; }
.home-new .see-all { margin: .2rem 0 0; font-size: .95rem; }
.new-pages .feed-note { color: var(--muted); font-size: .93rem; }
.new-pages .index-group h2 { margin: 1.4rem 0 .4rem; font-size: 1.15rem; }
.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.draft { border-color: var(--accent); color: var(--accent); }

/* NARROW, the search box becomes a button rather than a second row.
   A row of its own cost every page 2.4rem of permanent header on the screen with the least of it,
   for a control most readers are not using on any given page. Collapsed, it is one icon; opened, it
   covers the wordmark and takes the whole bar, which is the only place on a phone wide enough to
   type in. The mark stays as the way home. */
@media (max-width: 46rem) {
  header.site { grid-template-columns: auto 1fr auto; row-gap: 0; }
  header.site .brand { order: 1; }
  form.search { order: 2; }
  header.site nav { order: 3; }
  header.site nav .hide-narrow { display: none; }
  :root { --header-h: 4rem; }

  /* Collapsed: the input is a circle showing only the magnifier. It is a real input, not a
     stand-in that gets swapped for one, so a tap lands straight in the field with the keyboard up
     and nothing to re-focus. */
  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 works before any JavaScript runs and cannot get stuck
     open. The brand is what it grows over. */
  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 span { visibility: hidden; }
}
@media (max-width: 600px) {
  body { font-size: 16px; }
  /* A label column of 8-9rem leaves nothing for the value on a phone: stack them. */
  .keyfacts div, .provenance dl div { grid-template-columns: 1fr; gap: .1rem; }
  .keyfacts { padding: .4rem .7rem; }
  h1 { font-size: 1.65rem; }
  h2 { font-size: 1.2rem; }
  h3 { font-size: 1.05rem; }
  p.lead, p.summary { font-size: 1.02rem; }
  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; }
  /* The diff puts two versions of a paragraph side by side; on a phone they stack. */
  .diff li { padding: .45rem .55rem; }
  .s-decide { flex-direction: column; align-items: stretch; }
  .s-decide input[type="text"] { flex: 1 1 auto; }
}

/* Nothing on any page may push the viewport sideways: a long URL in a reference, an id in the
   admin table, a word in a heading. Wide things scroll inside their own box instead.

   On the ROOT only, never on body. A non-visible overflow on `html` propagates to the viewport
   and the root's own used value goes back to `visible`, so nothing becomes a scroll container and
   the page still cannot be pushed sideways. On `body` the same declaration forces overflow-y to
   compute to `auto`, making body a scroll container, and `position: sticky` resolves against the
   nearest scrolling ancestor, so the header and the sidebar started sticking to a box that scrolls
   with the document. That is how they silently stopped sticking when this rule arrived with the
   mobile work. Not `clip` either: `clip` on body would keep sticky but establishes a clip context
   over the fixed video dock, and it is unsupported before Safari 16. */
html { overflow-x: hidden; }
.body, .refs li, .s-what, .search-results p, .provenance dd, .rails li a { overflow-wrap: anywhere; }
.refs li a, .provenance a { word-break: break-word; }
pre, code { overflow-wrap: anywhere; }
.admin-table { min-width: 34rem; }
.admin .table-wrap, .diff, .versions { max-width: 100%; }

/* --- 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. There is no close control, it
   is part of the page, not a popup. At least 480x270, because that is the floor the YouTube API
   terms set; the title sits ABOVE the player rather than over it, for the same reason. On a
   narrow screen the sidebar is under the article, so on first play the dock pins to the foot of
   the viewport instead. */
/* No frame, and no title above the picture, at any width, not just on a phone. A border around a
   video is a rectangle drawn around a rectangle, and the title above it repeats the one the player
   already carries in its own corner. Both were costing the picture room and giving nothing back. */
.hb-dock { background: var(--bg); border: 0; border-radius: 0; padding: 0; }
.hb-dock .hb-player { margin: 0; border-radius: 6px; min-height: 0; }
/* The bar above the video is gone: the title it held is repeated by the player itself, and the
   close control moved to the header. It is not merely hidden; it was a FLEX ITEM of the pinned
   dock, so while a `display: none` sat above the `display: flex` that outlived it, the bar went on
   claiming 218px and shoving the video to the right of a band that was correctly full width. That
   is what made a full-width band look like a floating card.
   The element is still rendered by the template, so this keeps it out of layout entirely. */
.hb-dock-bar, .hb-dock-title { display: none !important; }
/* In the sidebar: first thing in the sticky column, so it rides just under the header. */
.related > .hb-dock { margin: 0 0 1rem; }
/* On a phone: with the lead, directly under the summary. */
.article > .hb-dock { margin: 0 0 1.2rem; }
/* At the bottom: a floating box, and the page gets room so its end is never under it. */
/* Pinned, the video is a band across the foot of the window, not a card floating over one corner.
   A floating card covers whatever happens to be under it and reads as a pop-up; a band has a top
   edge the page can end at. Half the viewport is the ceiling, so the reading column always keeps
   the larger share; the video is what the reader came to watch, but not instead of the page. */
.hb-dock.hb-dock--bottom {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; width: auto;
  max-height: 50vh; background: var(--bg); box-shadow: 0 -8px 24px rgba(0, 0, 0, .45);
  padding: .4rem .5rem calc(.4rem + env(safe-area-inset-bottom, 0px));
  display: flex; justify-content: center;
}
/* The player keeps the video's own shape and is bounded by the band's height, so a 9:16 upload does
   not stretch the band to fill the screen. Written as a width, because capping the height of an
   aspect-ratio box is what breaks it. */
/* Pinned at the foot of a phone the box is the picture again, with no band: the player is capped
   at half the window and a band would take that out of the video. The cap is a WIDTH, so this box
   goes back to being an aspect-ratio box -- a percentage padding here would resolve against the
   band's full width rather than the capped player's. */
.hb-dock.hb-dock--bottom { --hb-buffer: 0px; }
.hb-dock.hb-dock--bottom .hb-player {
  width: min(100%, calc((50vh - 1rem) * var(--hb-ar-w) / var(--hb-ar-h)));
}
.hb-dock.hb-dock--bottom .hb-player:not(.failed) {
  height: auto; padding-bottom: 0; aspect-ratio: var(--hb-ar-w) / var(--hb-ar-h);
}
body.has-dock.dock-bottom { padding-bottom: calc(var(--hb-dock-h, 0px) + 1rem); }


/* --- how much of a small screen the player may take -------------------------------------------
   Every cap below is written as a WIDTH, including the ones asked for as heights. Capping the
   height of a box that owns an aspect-ratio does not shrink the box, it breaks it: the width
   stays where it was, the ratio is overruled and the video letterboxes inside a container that
   is no longer its shape. A width the ratio derives keeps the box exact, "at most 50vh tall" is
   "at most calc(50vh * w / h) wide".

   The shape is a pair of tokens so those sums can be written once. 16:9 is the default and what
   an unrecorded ratio means, so nothing changes for a video whose shape was never captured. */
.hb-dock { --hb-ar-w: 16; --hb-ar-h: 9; --hb-buffer: 242px; }
/* No band under a portrait video: YouTube draws its controls over one whatever the box is, and the
   band would be black screen under a tall picture that already fills the height it is given. */
.hb-dock.hb-dock--portrait { --hb-ar-w: 9; --hb-ar-h: 16; --hb-buffer: 0px; }
.hb-dock[hidden] { display: none; }
/* A video the reader hid stays hidden from the first paint (theme.js sets the mark before it), as on Vi.ki (George,
   Oct 21, 2026: "remember a hidden video on Heartipedia to be consistent with Vi.ki"); moments.js then closes the dock
   and gives the column back. */
:root[data-video="off"] .hb-dock { display: none; }

/* PHONE, PORTRAIT; a vertical video takes at most half the screen's height. A 16:9 video is
   already right in this orientation and no rule here matches it. */
@media (orientation: portrait) and (max-width: 60rem) {
  .article > .hb-dock.hb-dock--portrait,
  .hb-dock.hb-dock--bottom.hb-dock--portrait {
    width: min(100%, calc(50vh * var(--hb-ar-w) / var(--hb-ar-h)));
    margin-inline: auto;
  }
  /* A pinned vertical dock is half the screen plus its own bar, so the page needs more room at
     its foot than the landscape reservation leaves. */
  body.has-dock.dock-bottom.dock-portrait { padding-bottom: min(62vh, 34rem); }
}

/* PHONE, LANDSCAPE; the screen is short and wide, and clamp(480px, 38vw, 640px) was taking 93%
   of an 812px phone and more than its whole height. Three ceilings, whichever binds first:
     50vw    what was asked for: never more than half the screen's width;
     640px   never bigger than it is today, for a short desktop window that also matches;
     66vh    a floor under the text, written again as the width that height implies.
   A 16:9 video lands on the 50vw term; a 9:16 one lands on the 66vh term, which puts it at about
   a fifth of the width. Nothing here sets a height, so no box is broken. */
@media (orientation: landscape) and (max-height: 33rem) {
  /* In the flow, before anything is pinned, the video is centered and bounded by the short screen's
     height rather than its width. */
  .article > .hb-dock {
    width: min(100%, calc(66vh * var(--hb-ar-w) / var(--hb-ar-h)));
    margin-inline: auto;
  }
  /* Pinned, it is the same band as everywhere else and obeys the same ceiling, half the height of
     the window, which on a short landscape screen is what keeps any text on it at all. */
  body.has-dock.dock-bottom .to-top { left: 1rem; right: auto; }
}


/* --- suggestions ---------------------------------------------------------------------------- */
.suggest-form { display: flex; flex-direction: column; gap: .2rem; margin: 1.2rem 0; }
.suggest-form label { font-weight: 600; margin-top: .8rem; }
.suggest-form .optional { font-weight: 400; color: var(--muted); }
.suggest-form .small { margin: 0 0 .3rem; }
.suggest-form textarea, .suggest-form input[type="url"] {
  width: 100%; font: inherit; padding: .55rem .7rem; border: 1px solid var(--line);
  border-radius: 8px; background: var(--tint); color: var(--ink);
}
.suggest-form textarea:focus, .suggest-form input:focus { border-color: var(--link); outline: none; }
.suggest-form button { cursor: pointer; background: var(--link); color: var(--on-link); border: 1px solid var(--link); border-radius: 6px; padding: .5rem 1.1rem; font: inherit; }

.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; }

.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-accepted, .suggestion.s-published { border-left-color: var(--link); }
.suggestion.s-rejected, .suggestion.s-needs_source { border-left-color: var(--accent); }
.s-what { margin: 0 0 .5rem; white-space: pre-wrap; }
.s-status { margin: 0 0 .3rem; }
.s-meta { margin: 0; font-size: .85rem; color: var(--muted); }
.s-log { margin-top: .5rem; font-size: .88rem; }
.s-log summary { cursor: pointer; color: var(--muted); }
.s-log ol { margin: .4rem 0 0; padding-left: 1.2rem; }
.s-when { color: var(--muted); }
.s-who { color: var(--muted); }
.s-decide { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }
.s-decide select, .s-decide input[type="text"] { font: inherit; font-size: .9rem; padding: .3rem .5rem; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); }
.s-decide input[type="text"] { flex: 1 1 14rem; min-width: 0; }
.s-decide button { font: inherit; font-size: .9rem; padding: .3rem .8rem; border: 1px solid var(--link); border-radius: 6px; background: none; color: var(--link); cursor: pointer; }

/* --- the diff -------------------------------------------------------------------------------
   Unchanged paragraphs stay quiet so the changed ones are the page. */
.diff { list-style: none; padding: 0; margin: 1.2rem 0; }
.diff li { padding: .45rem .7rem; border-left: 3px solid transparent; margin: .2rem 0; }
.d-same { color: var(--muted); }
.d-changed { border-left-color: var(--link); background: var(--tint); border-radius: 0 6px 6px 0; }
.d-changed p { margin: .1rem 0; }
.d-add { border-left-color: var(--link); background: var(--tint); border-radius: 0 6px 6px 0; }
.d-del { border-left-color: var(--accent); background: var(--tint); border-radius: 0 6px 6px 0; }
.diff ins { text-decoration: none; background: color-mix(in srgb, var(--link) 26%, transparent); border-radius: 3px; padding: 0 .12rem; }
.diff del { background: color-mix(in srgb, var(--accent) 26%, transparent); border-radius: 3px; padding: 0 .12rem; }
.d-old { color: var(--muted); }
.versions { padding-left: 1.2rem; font-size: .93rem; }
.versions li { margin: .3rem 0; }

/* --- back to top ------------------------------------------------------------------------------
   Worth offering only once the reader is a whole screen down, so it starts invisible and
   account.js adds .on. visibility, not display, so the transition can run and so the link leaves
   the tab order while it is not being offered: a control nobody can see must not be one a keyboard
   can reach. It shares the bottom of the screen with the pinned video dock (z-index 40), so it sits
   one above and rides on top of it, moments.js publishes the dock's measured height as
   --hb-dock-h while the dock is pinned and removes it otherwise, so with no dock, no moments or a
   closed dock the offset is simply 0.

   It stands at the bottom-right corner of the article's TEXT, not of the window (George, 13
   September 2026). On a wide screen that is well in from the window's edge, left of the video
   rail; account.js measures the column and publishes the distance 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;
  /* Above the dock, but never above the header. On a short landscape phone the dock can be
     two-thirds of the screen, and "1rem above it" put this control on top of the avatar. The
     second term is the ceiling: far enough below the header that the two cannot meet. */
  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 { transition: none; }
}

/* --- keyboard shortcuts ------------------------------------------------------------------------
   A <dialog> opened with showModal(), so Escape, the focus trap and the backdrop are the
   browser's job rather than three more things to get wrong. Everything here is a token, so both
   themes 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, as on Vi.ki (George, Oct 21, 2026: "Don't wrap "Ctrl + /"
   or any key combination"; Vi.ki had it since 16 September 2026), and as wide in every list, 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 form { margin: 0; text-align: right; }
@media (max-width: 30rem) {
  .hotkeys dl > div { grid-template-columns: minmax(0, 1fr); gap: .15rem; }
}

/* --- marking a passage -------------------------------------------------------------------------
   The bar that appears over a selection. Fixed, because it is anchored to where the text is on
   screen rather than to where it is in the document, and it goes away the moment the page moves. */
.note-bar {
  position: fixed; z-index: 45; transform: translate(-50%, -100%);
  display: flex; align-items: center; gap: .4rem; padding: .35rem;
  border: 1px solid var(--line); border-radius: 999px; background: var(--bg);
  box-shadow: var(--shadow); font-size: .88rem; max-width: min(92vw, 28rem);
}
.note-bar.below { transform: translate(-50%, 0); }
.note-btn {
  font: inherit; font-size: .88rem; cursor: pointer; white-space: nowrap;
  padding: .3rem .75rem; border-radius: 999px; border: 1px solid transparent;
  background: var(--tint); color: var(--ink); text-decoration: none;
}
.note-btn:hover, .note-btn:focus-visible { border-color: var(--link); color: var(--link); text-decoration: none; }
.note-btn.n-interesting { border-color: color-mix(in srgb, var(--moment) 45%, transparent); }
.note-btn.n-didnt_know { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.note-offer { color: var(--muted); padding-left: .5rem; }
.note-said { padding: .3rem .75rem; }
.note-said.ok { color: var(--moment); }
.note-said.warn { color: var(--accent); }
@media (max-width: 34rem) {
  .note-bar { flex-wrap: wrap; justify-content: center; border-radius: 12px; }
}

/* --- the notes page ---------------------------------------------------------------------------- */
.note-counts { display: flex; gap: .5rem; flex-wrap: wrap; margin: .2rem 0 1.4rem; }
.note-chip {
  font-size: .82rem; padding: .2rem .7rem; border-radius: 999px;
  border: 1px solid var(--line); color: var(--muted);
}
.note-chip b { color: var(--ink); font-weight: 600; }
.notes { list-style: none; margin: 0; padding: 0; }
.note { margin: 0 0 1.6rem; padding-left: .9rem; border-left: 3px solid var(--line); }
.note.n-interesting { border-left-color: var(--moment); }
.note.n-didnt_know { border-left-color: var(--accent); }
.note-kind { margin: 0 0 .3rem; font-size: .75rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.note-quote { margin: 0 0 .4rem; padding: 0; border: 0; font-size: 1rem; }
.note-meta { margin: 0; font-size: .88rem; color: var(--muted); display: flex; align-items: baseline; gap: .45rem; flex-wrap: wrap; }
.note-meta .gone { font-style: italic; }
.note-del { display: inline; margin: 0; }
.note-del button {
  font: inherit; font-size: .88rem; padding: 0; border: 0; background: none;
  color: var(--muted); cursor: pointer; text-decoration: underline;
}
.note-del button:hover { color: var(--accent); }

/* A moment whose video this viewer cannot play. It is not hidden; 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; }

/* --- not found -----------------------------------------------------------------------------------
   A 404 that helps (server/lost.go): why, a search box already holding the address's words, the
   pages it was most likely reaching for, and the ways in (George, 17 September 2026). */
.lost .lost-why { color: var(--muted); margin: 0 0 1.2rem; }
.lost-search { margin: 0 0 1.4rem; max-width: 36rem; }
.lost-search label { display: block; margin: 0 0 .35rem; font-weight: 600; }
.lost-search .lost-row { display: flex; gap: .5rem; }
.lost-search input {
  flex: 1 1 auto; min-width: 0; padding: .5rem .75rem; font: inherit;
  color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
}
.lost-search .button { cursor: pointer; font: inherit; background: var(--link); color: var(--on-link); }
.lost-near h2 { margin: 1.4rem 0 .6rem; font-size: 1.05rem; }
.lost-pages { list-style: none; margin: 0; padding: 0; }
.lost-pages li { border-top: 1px solid var(--line); }
.lost-pages li:last-child { border-bottom: 1px solid var(--line); }
.lost-pages a { display: block; padding: .7rem 0; color: var(--ink); }
.lost-pages a:has(.pg-thumb) { display: grid; grid-template-columns: 10rem 1fr; gap: .8rem; align-items: start; }
.lost-pages a:hover strong { text-decoration: underline; }
.lost-text strong { display: block; }
.lost-text .kind { display: block; font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin-top: .1rem; }
.lost-sum { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; margin-top: .25rem; color: var(--muted); font-size: .92rem; }
.lost-ways { margin: 1.6rem 0 0; }

/* --- a picture ------------------------------------------------------------------------------------
   From heart.photos, the library the three sites share (the shared photo plan, step 12). A lead
   picture stands beside the opening text on a wide screen and above it on a narrow one, the way
   an encyclopedia puts a portrait at the top of a page. The caption carries the credit and the
   license, which is what the license asks in return for the picture being here.

   photo.js wraps the picture in a button and opens it in the Heart Photos player; without that
   file it is a plain picture, which is why nothing here draws a control. */
figure.photo { margin: 1.15rem 0; padding: 0; }
figure.photo img { display: block; width: 100%; height: auto; border-radius: 8px; background: var(--tint); }
figure.photo figcaption { margin-top: .4rem; font-size: .88rem; color: var(--muted); }
figure.photo figcaption .cap { display: block; color: var(--ink); }
figure.photo figcaption .credit { display: block; margin-top: .15rem; }
figure.photo .photo-open { display: block; width: 100%; padding: 0; border: 0; background: none; cursor: zoom-in; line-height: 0; border-radius: 8px; }
figure.photo .photo-open:focus-visible { outline: 2px solid var(--link); outline-offset: 3px; }
@media (min-width: 60rem) {
  figure.photo.lead { float: right; width: 22rem; margin: .2rem 0 1rem 1.6rem; }
}

/* The player's own dialog: the picture on the ground, and one line under it saying what it is. */
dialog.photo-viewer {
  width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; padding: 0;
  border: 0; background: #0b0c0e; color: #e7e9ec; overflow: hidden;
  display: flex; flex-direction: column;
}
dialog.photo-viewer::backdrop { background: rgb(0 0 0 / .8); }
dialog.photo-viewer:not([open]) { display: none; }
.photo-stage { flex: 1 1 auto; min-height: 0; position: relative; }
.photo-stage .hic { width: 100%; height: 100%; }
.photo-bar { flex: none; display: flex; align-items: center; gap: 1rem; padding: .5rem 1rem .7rem; }
.photo-cap { flex: 1 1 auto; margin: 0; font-size: .88rem; color: #c8cad2; }
.photo-close {
  flex: none; width: 2.4rem; height: 2.4rem; border: 0; border-radius: 999px; cursor: pointer;
  background: rgb(255 255 255 / .12); color: #fff; font-size: 1.4rem; line-height: 1;
}
.photo-close:hover, .photo-close:focus-visible { background: rgb(255 255 255 / .25); outline: none; }

/* The sign-in card's button sits in the middle of it (George, Oct 2, 2026: "Center the title and
   these two buttons in the frame, also on vi.ki sign in"); the note under it stays left-aligned. */
.signin-sheet .signin-alt { text-align: center; }

/* A listed company's share price under a page's title (stock.js; George, Oct 21, 2026: "show the spacex stock price
   at the top similar to how we show steam player counts for games like aion2.vi.ki"): Vi.ki's Steam players chart
   (vi.ki app.css, .players*) carried over, with green and red for the day's move. */
:root { --stock: #c6f000; --stock-up: #4cc38a; --stock-down: #f07a7a; }
:root[data-theme="light"] { --stock: #5d8a00; --stock-up: #1a7f4b; --stock-down: #c0362c; }
section.stock { position: relative; margin: 1rem 0 1.5rem; padding: 1rem 1rem .75rem; border: 1px solid var(--line); border-radius: 12px; background: var(--tint); }
section.stock[hidden] { display: none; }
.stock-asof { margin: 0 0 .5rem; font-size: .8rem; color: var(--muted); text-align: center; }
.stock-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: .75rem .5rem; text-align: center; margin-bottom: .75rem; }
.stock-stat strong { display: block; white-space: nowrap; font-size: clamp(1.05rem, 3vw, 1.45rem); font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.stock-stat strong.stock-up { color: var(--stock-up); }
.stock-stat strong.stock-down { color: var(--stock-down); }
.stock-stat span { display: block; font-size: .8rem; color: var(--muted); }
.stock-zoom { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; margin-bottom: .5rem; }
.stock-zoom-label { font-size: .8rem; color: var(--muted); margin-right: .15rem; }
.stock-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; }
.stock-zoom-btn[aria-pressed="true"] { background: var(--line); color: var(--ink); }
.stock-chart { position: relative; min-height: 200px; }
.stock-svg { display: block; width: 100%; height: auto; touch-action: pan-y; outline: none; }
.stock-svg:focus-visible { box-shadow: 0 0 0 2px var(--link); border-radius: 4px; }
.stock-svg.stock-pannable { cursor: grab; }
.stock-panning, .stock-panning .stock-svg { cursor: grabbing; user-select: none; }
.stock-grid { stroke: var(--line); stroke-width: 1; }
.stock-tick { stroke: var(--muted); stroke-width: 1; }
.stock-axis { fill: var(--muted); font-size: 11px; }
.stock-ipo { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 5 4; opacity: .8; }
.stock-ipo-label { fill: var(--muted); font-size: 11px; }
.stock-line { fill: none; stroke: var(--stock); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.stock-cross { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; }
.stock-dot { fill: var(--stock); stroke: var(--bg); stroke-width: 2; }
.stock-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); }
.stock-tip span, .stock-tip strong { display: block; }
.stock-tip-when, .stock-tip-avg { color: var(--muted); }
.stock-wait { color: var(--muted); font-size: .9rem; padding: 4rem 0; text-align: center; margin: 0; }
.stock-next { margin: .5rem 0 0; font-size: .8rem; color: var(--muted); }
.stock-next[hidden] { display: none; }
.stock-source { margin: .35rem 0 0; font-size: .75rem; color: var(--muted); padding-right: 2.5rem; }
.stock-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; }
.stock-full:hover, .stock-full:focus-visible { color: var(--ink); background: var(--line); outline: none; }
section.stock: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.stock:fullscreen .stock-chart { flex: 1 1 0; min-height: 0; }
section.stock:-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.stock:-webkit-full-screen .stock-chart { flex: 1 1 0; min-height: 0; }
section.stock.stock-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.stock.stock-window .stock-chart { flex: 1 1 0; min-height: 0; }
html.stock-window-open { overflow: hidden; }
.stock-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; }
.stock-event:hover, .stock-event:focus-visible, .stock-event[aria-expanded="true"] { border-color: var(--stock); color: var(--stock); outline: none; box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--stock); }
.stock-event::before { content: ""; position: absolute; inset: -6px; }
.stock-note { pointer-events: auto; white-space: normal; width: max-content; max-width: min(18rem, 80vw); z-index: 3; }
.stock-note .stock-note-text { color: var(--ink); margin-top: .15rem; }
.stock-note .stock-note-source { display: inline-block; margin-top: .3rem; color: var(--link); }
@media (max-width: 30rem) { section.stock { padding: .75rem .6rem .6rem; } .stock-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .stock-stat span { font-size: .7rem; } }

/* The note a plain mouse wheel over the chart shows: hold Ctrl (Command) to zoom, as an embedded Google map says
   (George, Oct 21, 2026). It covers the chart, dimmed, and lets the pointer through. */
section.stock .stock-hint {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 1rem;
  border-radius: inherit; background: rgb(0 0 0 / .55); color: #fff; font-size: 1.05rem; font-weight: 500;
  text-align: center; pointer-events: none; opacity: 0; transition: opacity .25s ease; z-index: 3;
}
section.stock .stock-hint.on { opacity: 1; transition-duration: .1s; }

/* --- cookie choice ----------------------------------------------------------------------------
   consent.js's bar (George, Oct 21, 2026: "only show them where we absolutely have to"), shown only
   to visitors in the EU, the EEA, the UK and Switzerland who have not chosen, or to anyone who
   opens Cookie settings in the footer. A small bar fixed at the bottom, in the site's own colors in
   both themes, never a modal: the page scrolls as ever, and .consent-space, as tall as the bar, is
   added after the footer so the end of the page can scroll out from under it. It sits above the
   pinned video dock, and above the photo viewer so it can always be answered. The two buttons are
   the same button: neither answer is dressed up as the expected one. Vi.ki and Heartipedia share
   this block: keep the two the same. */
.consent {
  position: fixed; left: 0; right: 0; bottom: var(--hb-dock-h, 0px); z-index: 1001;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .5rem 1rem;
  margin: 0; padding: .6rem max(1rem, env(safe-area-inset-right, 0px)) calc(.6rem + env(safe-area-inset-bottom, 0px)) max(1rem, env(safe-area-inset-left, 0px));
  background: var(--bg); color: var(--ink); border-top: 1px solid var(--line); box-shadow: var(--shadow);
  font-size: .9rem; line-height: 1.4;
}
.consent[hidden], .consent-space[hidden] { display: none; }
.consent-text { margin: 0; max-width: 44rem; }
.consent-text a { text-decoration: underline; }
.consent-buttons { display: flex; gap: .5rem; flex: none; }
.consent-button {
  min-width: 6.5rem; padding: .45rem 1rem; border: 1px solid var(--line); border-radius: 6px;
  background: var(--tint); color: var(--ink); font: inherit; cursor: pointer;
}
.consent-button:hover, .consent-button:focus-visible { border-color: var(--link); color: var(--link); }
.to-top { margin-bottom: var(--consent-h, 0px); }
