  /* --bg is the app's own ground, and --bg-rgb is the same colour in the form rgba() needs. They have
     to be kept equal by hand; anything that wants the ground at less than full strength takes the
     second one, so a change of scheme moves both. */
  :root { color-scheme: dark; --cw: 320px; --gap: 12px; --edge: 14px;
    /* A lighter blue than the one this started in, and the app clears its broadcast to the same
       value (LiveKitStreamer.CaptureBackground) so the picture and the space around it are one
       surface. The two earlier grounds are kept below, a class each. */
    --bg: #7da3ca; --bg-rgb: 125, 163, 202;
    --card: transparent; --card-narrow: rgba(var(--bg-rgb), .78);
    --line: rgba(158, 202, 255, .26);
    --panel-ink: #d8dde3; --panel-ink-2: #c3ccd4; --panel-ink-3: #a9b8c8; }
  /* THE LIGHT SET, kept and not deleted. Add `wall` to <html> and the page becomes the directory's
     ground: the wall behind the shelf, which is also what the app can clear its broadcast to
     (LiveKitStreamer.CaptureBackgroundWall). BOTH SIDES HAVE TO MOVE TOGETHER -- a taupe page around
     a blue picture, or the reverse, is worse than either on its own.
     Everything this needs is here and in the block at the end of this file; nothing above them was
     rewritten, so the dark set is still the page as it was drawn. */
  /* The blue this page was drawn in, kept. */
  :root.blue { --bg: #59738c; --bg-rgb: 89, 115, 140; }
  :root.wall {
    --bg: #ddd0be; --bg-rgb: 221, 208, 190;
    --card-narrow: rgba(255, 255, 255, .9);
    --line: rgba(169, 143, 112, .5);
    --panel-ink: #2e2822; --panel-ink-2: #6d6155; --panel-ink-3: #6b5f4d; }
  /* The old dark set is not deleted, it is one commit back: every rule below still says what it
     said, and the block at the end of this file is what turns it over for a light ground. */
  * { box-sizing: border-box; }
  /* pan but not pinch. The layout is fixed and the picture is the point, so the browser's own
     pinch-zoom only ever gets in the way -- aim at the avatar while somebody else holds the turn and
     you would zoom the DOCUMENT and end up looking at a corner of it. Panning is deliberately still
     allowed: the comment list and the two side panels scroll. Double-tap zoom goes with it, which
     this value also excludes. */
  /* overscroll-behavior is the declarative half of "the page itself never moves": it turns off
     pull-to-refresh and the rubber-band at the edges. The touchmove handler in viewer.js is the
     other half, for the browsers and versions that do not honour this one. Neither is enough alone.
     touch-action stays pan-x pan-y rather than becoming none, because touch-action INTERSECTS down
     the ancestor chain -- none here would take the comment list and the panels with it. */
  html, body { margin: 0; height: 100%; background: var(--bg); color: #e8e8ea;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    touch-action: pan-x pan-y; overscroll-behavior: none; }
  /* And each inner scroller keeps its overscroll to itself, so reaching the end of the comment list
     does not hand the rest of the gesture to the page behind it. */
  #chatList, #ctlAnim, #info, #caster, .nameTagBody #info { overscroll-behavior: contain; }
  #stage { position: fixed; top: 10%; bottom: 10%; left: 0; right: 0; display: grid; place-items: center; overflow: hidden; }
  /* Nothing over the picture is text, and a drag across it is a camera move. A trackpad reports a drag
     as a MOUSE drag, and the browser's answer to a mouse drag over something it may select is to
     select it -- so on an iPad with a trackpad the shot went blue and the orbit never started. Reported
     there, but it is every trackpad and every mouse.
     Scoped to the picture and its drag surface on purpose: the panels stay selectable, because a
     comment, an animation name, an avatar's name and a broadcaster's name are all worth copying.
     -webkit- alongside the standard property because iOS Safari is where this shows up, and it is not
     one of the places the unprefixed one can be relied on. #video is named rather than left to
     inheritance from #stage: user-drag does not inherit, and a press on a <video> turning into a drag
     of the frame as an image is the other half of the same problem. */
  #stage, #video, #ctl3d, #ctlPivot {
    user-select: none; -webkit-user-select: none;
    -webkit-user-drag: none; -webkit-touch-callout: none; }
  /* And no page gesture starts on the picture. html/body allow panning on purpose -- the comment list
     and the panels scroll -- but the picture has nothing to scroll, so a drag on it was being handed
     to the browser, which on iOS is the pull-to-refresh that looked like the page reloading.
     Only matters while the turn is somebody else's: with it in hand the canvas is over the picture
     and takes the touches, and OrbitControls sets this same value on itself. */
  #stage, #video { touch-action: none; }
  /* The stage stays the full width and the video is centred inside it, so the picture sits in the
     middle of the WINDOW rather than the middle of the leftover space. To keep that true while the
     comment column occupies --cw on the right, the video gives up that width on BOTH sides: it
     shrinks about its own centre instead of sliding left. object-fit keeps the 4:3 portrait aspect. */
  /* The picture gives up a panel, a gap AND an outer margin on each side. Reserving --edge here
     is what puts the margin at the window's edge: once the window is narrow enough for max-width
     to bind, the panels land exactly --edge in from it, with no second rule to keep in step. */
  video { width: 100%; height: 100%;
    max-width: calc(100vw - 2 * (var(--cw) + var(--gap) + var(--edge)));
    object-fit: contain; min-width: 0; min-height: 0; background: var(--bg); }
  /* Kept, not deleted: the version that reserves a SQUARE the size of the picture's long side
     instead of the width of the 4:3 portrait shot, so the space left for it does not change shape
     with the stream. Swap the max-width above for this one and un-comment the two square lines in
     alignChatToVideo (and the narrow-screen override below) to go back to it. The factor is the
     shot's width over its height, so it moves with the stream aspect.
  video { max-width: calc((100vw - 2 * (var(--cw) + var(--gap) + var(--edge))) * 3 / 4); } */
  #panel { position: fixed; inset: 0; display: grid; place-items: center; text-align: center;
    padding: 24px; transition: opacity .4s; }
  #panel.hidden { opacity: 0; pointer-events: none; }
  .card { max-width: 420px; }

  /* --- while the room is still arriving -------------------------------------------------------
     The stream, the avatar card and the motion list turn up at different moments, and a page that
     reveals each as it lands spends its first seconds rearranging itself. So everything that is
     WAITING ON DATA is held back together -- the picture, the four panels, the two name tags, the
     camera marker -- and one thing is shown instead: the character's own acrylic stand, turning.
     Held at opacity 0 rather than display:none on purpose. The video element keeps decoding, the
     columns keep the widths the layout code measured, and nothing is laid out a second time when
     the class comes off. The connection never pauses for any of this. */
  #stage, #leftcol, #rightcol { transition: opacity .3s; }
  body.booting #stage, body.booting #panel, body.booting #leftcol, body.booting #rightcol,
  body.booting #ctl3d, body.booting #ctlPivot, body.booting .nameTag, body.booting #viewSwap,
  body.booting #hint {
    opacity: 0 !important; pointer-events: none !important; }
  /* The stand is drawn from viewport coordinates into the box #bootSlot leaves for it, so the
     canvas covers the window and takes no input. Off entirely once the room is up: it is a second
     WebGL context on a page that already has the camera marker's. */
  #gl { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 30; pointer-events: none;
    display: none; }
  #boot { position: fixed; inset: 0; display: none; place-items: center; z-index: 29;
    pointer-events: none; }
  body.booting #gl { display: block; }
  body.booting #boot { display: grid; }
  /* Square plus the base's thickness, exactly as on the directory -- keep in step with DISC_H in
     stands.js. */
  #bootSlot { width: min(23vw, 115px); aspect-ratio: 1 / 1.02; }
  /* No vertical-align: it used to be `middle`, which is not the middle. That keyword puts the box's
     centre on the baseline plus half the x-height -- a Latin lowercase measure. Japanese glyphs sit
     higher in the line than that, so the dot came out 1.9px LOW beside 15px text (measured), which
     is what made the heading look like it was sagging. Both places the dot appears are flex rows
     now, and align-items does it properly. */
  .dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: #f0b429;
    margin-right: 8px; flex: none; animation: pulse 1.4s ease-in-out infinite; }
  @keyframes pulse { 0%,100% { opacity: .35 } 50% { opacity: 1 } }
  /* Flex for the dot's sake (see above). justify-content replaces the centring #panel's text-align
     was doing, which a flex container no longer takes. */
  h1 { font-size: 15px; font-weight: 600; letter-spacing: .02em; color: var(--panel-ink); margin: 0 0 6px;
    display: flex; align-items: center; justify-content: center; }
  .room { font-size: 26px; font-weight: 700; letter-spacing: .06em; font-variant-numeric: tabular-nums; }
  /* #panel has no surface of its own -- it is words on the ground -- so these are the one place
     that has to be dark now the ground is light. */
  .sub { margin-top: 14px; font-size: 13px; color: var(--panel-ink-2); line-height: 1.6; }
  #panel .card { color: var(--panel-ink); }
  /* 25 puts this row and the account pill opposite it above the two name tags at 21. Their cards hang
     down into the tags' rows, and at equal weight the tags won on being later in the document -- so
     the answer to a tap opened underneath the thing that was covering it. The pill needed the same
     number for a second reason: its own z-index made it a stacking context, which sealed its card's
     21 inside it and left the whole pill competing at its own value. */
  /* The row holds the position now, so the badge only has to be a badge -- and it stays `relative`
     because its settings card is absolutely positioned against it. The home button is outside the
     badge on purpose: the badge is a tap target that opens that card, and a link buried inside
     something that toggles reads as neither one thing nor the other. */
  #topleft { position: fixed; top: 14px; left: 14px; z-index: 25; display: flex; align-items: center; gap: 8px; }
  /* Always there, unlike the badge, which waits for a picture. Before the stream arrives is exactly
     when somebody might want to go back to the list. */
  #homeBtn { flex: none; width: 32px; height: 32px; display: grid; place-items: center;
    background: rgba(20,21,24,.55); backdrop-filter: blur(6px); border-radius: 999px;
    border: 1px solid rgba(255,255,255,.12); color: #e8e8ea;
    transition: background .15s, border-color .15s; }
  #homeBtn svg { display: block; fill: currentColor; opacity: .9; }
  #homeBtn:hover { background: rgba(20,21,24,.78); border-color: rgba(255,255,255,.28); }
  /* 32px, stated rather than arrived at. All three markers along the top -- home, badge, account
     pill -- were within a few tenths of each other because their text happened to lay out the same
     way, which is not the same as being equal: the badge came out 31.6 and the home button 32, so
     the row centred the badge 0.2px below the pill opposite it. Pinning the height makes them the
     same on purpose, and keeps them so if any of the three ever holds different text. */
  #badge { position: relative; min-height: 32px; font-size: 12px; font-weight: 600;
    background: rgba(20,21,24,.55); backdrop-filter: blur(6px); padding: 7px 12px; border-radius: 999px;
    border: 1px solid rgba(255,255,255,.12); display: none; align-items: center; gap: 7px; }
  #badge .live { width: 8px; height: 8px; border-radius: 50%; background: #e5484d; }
  /* No rule to its left any more: the divider was there to separate the count from the room name,
     and with the name moved into the card there is nothing on that side but the on-air dot. */
  #badgeViewers { display: flex; align-items: center; gap: 4px;
    font-variant-numeric: tabular-nums; }
  #badgeViewers svg { display: block; fill: currentColor; opacity: .85; }
  /* The broadcaster's own settings, hung under the room name. Same card as the account pill's on the
     other side of the screen -- it is the same offer, a marker you point at to be told more about
     what is behind it -- and the mirror of it, so it opens down the edge it belongs to.
     grid, so the two values line up under each other rather than each row finding its own indent. */
  #badgeInfo { position: absolute; top: calc(100% + 12px); left: 0; z-index: 21;
    width: max-content; max-width: calc(100vw - 28px); padding: 9px 13px;
    background: rgba(18,19,22,.95); backdrop-filter: blur(8px); color: #e4e7ea;
    border: 1px solid rgba(255,255,255,.14); border-radius: 10px;
    font-size: 11.5px; line-height: 1.55; text-align: left; font-weight: 500;
    display: grid; grid-template-columns: auto auto; gap: 3px 10px;
    box-shadow: 0 6px 20px rgba(0,0,0,.3); pointer-events: none;
    opacity: 0; visibility: hidden; transform: translateY(-3px);
    transition: opacity .13s, transform .13s, visibility .13s; }
  #badgeInfo.show { opacity: 1; visibility: visible; transform: none; }
  /* The room name, heading the card. Spans both columns because it is not a label/value pair --
     it is what the card is about. The rule under it is the same idea as the avatar panel's h2:
     the identity, then what is set about it. */
  #badgeInfo .r { grid-column: 1 / -1; margin-bottom: 5px; padding-bottom: 6px;
    border-bottom: 1px solid rgba(255,255,255,.14);
    font-size: 13px; font-weight: 700; letter-spacing: .08em;
    font-variant-numeric: tabular-nums; color: #eef2f6; }
  #badgeInfo .k { color: #9aa0a6; letter-spacing: .04em; }
  /* Grey is the default for a value, and green is spent on one thing: a superchat that will actually
     arrive. The angle limit reads plain either way, because on and off are both just the setting --
     colouring one of them would make the other look like something to fix, and neither is. */
  #badgeInfo .v { font-weight: 700; letter-spacing: .06em; color: #8b9096; }
  #badgeInfo .v.good { color: #6fd68d; }
  #unmute { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%);
    display: none; padding: 10px 18px; border-radius: 999px; border: 1px solid rgba(255,255,255,.18);
    background: rgba(20,21,24,.65); color: #e8e8ea; font-size: 13px; cursor: pointer; }
  /* Colour hint: wide-gamut panels with vendor "enhancement" modes render colour-managed
     content (this page) differently from non-managed apps, which reads as a colour shift. */
  #hint { position: fixed; right: 14px; bottom: 14px; z-index: 20; }
  /* Both "i" buttons, one rule. They are the same offer -- a small round mark with something to say
     about what it sits beside -- and two sets of numbers made them read as two kinds of control. */
  #hintBtn, #kofiHintBtn { width: 26px; height: 26px; border-radius: 50%; padding: 0; cursor: pointer;
    border: 1px solid rgba(255,255,255,.28); background: rgba(20,21,24,.5); color: #e8e8ea;
    font-size: 13px; font-weight: 700; line-height: 1;
    font-family: Georgia, "Times New Roman", serif; display: grid; place-items: center;
    opacity: .5; transition: opacity .2s, background .2s; }
  /* Dark while ITS PANEL IS OPEN, pale again the moment it closes -- which is the state worth
     showing, and the state a stuck :hover was standing in for. Hover itself is pointer-only: a tap
     leaves :hover on what it touched until something else is tapped, so on a phone the button kept
     the pressed look after the panel had gone. */
  #hint.open #hintBtn, #kofiHint.open #kofiHintBtn,
  #hintBtn:focus-visible, #kofiHintBtn:focus-visible {
    opacity: 1; background: rgba(20,21,24,.8); outline: none; }
  @media (hover: hover) {
    #hintBtn:hover, #kofiHintBtn:hover { opacity: 1; background: rgba(20,21,24,.8); }
  }
  /* 21rem, up from 19. One number for both layouts: the narrow one is capped by the viewport rather
     than by a rule of its own, and on any phone wider than 368px this is what binds there too. */
  #hintBody { position: absolute; right: 0; bottom: 34px; width: min(21rem, calc(100vw - 32px));
    background: rgba(18,19,22,.94); backdrop-filter: blur(8px); color: #d7dade;
    border: 1px solid rgba(255,255,255,.12); border-radius: 10px; padding: 12px 14px;
    font-size: 12px; line-height: 1.7; text-align: left;
    opacity: 0; visibility: hidden; transform: translateY(4px); transition: opacity .18s, transform .18s, visibility .18s; }
  #hint.open #hintBody, #hintBtn:focus-visible + #hintBody { opacity: 1; visibility: visible; transform: none; }
  /* Hover is only wired where hovering is a thing a pointer can do. A tap leaves :hover STUCK on
     what it touched until something else is tapped, so on a phone the second press removed .open and
     the panel stayed open anyway on the hover rule alone -- the button looked like it only opened. */
  @media (hover: hover) {
    #hint:hover #hintBody { opacity: 1; visibility: visible; transform: none; }
  }
  #hintBody b { color: #f0f1f3; font-weight: 600; display: block; margin-bottom: 4px; }
  /* Each machine gets its own heading, so the reader looks for their own and skips the rest --
     the settings path is different enough per platform that one merged sentence hid both. */
  #hintBody b.case { margin-top: 9px; }
  #hintBody span { color: #9aa0a6; }
  /* The panel holds a control now, so the pointer has to be able to travel into it on hover alone.
     The 4px between the button's top and the panel's bottom edge (measured) is dead space that
     closed it mid-journey; this bridges the two into one hover target. Inert while the panel is
     visibility: hidden. */
  #hintBody::after { content: ''; position: absolute; left: 0; right: 0; bottom: -9px; height: 9px; }
  /* The switches. Ruled off from the colour advice above, once, at the first of them: they are
     display controls rather than more of the same explanation, and the rule belongs to the boundary,
     not to each row. */
  #hintBody .hintChk { display: flex; align-items: center; gap: 8px; cursor: pointer; }
  #hintBody .hintChk:first-of-type { margin-top: 11px; padding-top: 10px;
    border-top: 1px solid rgba(255,255,255,.12); }
  #hintBody .hintChk + .hintChk { margin-top: 6px; }
  #hintBody .hintChk input { accent-color: #9ecaff; width: 14px; height: 14px; margin: 0; flex: none;
    cursor: pointer; }
  /* Wins over the dimmed #hintBody span above by specificity rather than by source order, so
     moving either rule cannot silently grey the label out. */
  #hintBody .hintChk span { color: #e4e7ea; }
  /* Measured inbound bitrate -- what is actually arriving, not what the publisher configured.
     Diagnostic rather than part of watching, so it is off until the switch above asks for it.
     display, not visibility: on the narrow layout it overlays the picture, and an invisible box there
     would still be reserving a corner of it.
     Kept up here beside #hint, and not down with the markers it used to sit among, because the
     narrow layout moves BOTH of them and its rules are further down the sheet: at equal specificity
     the later rule wins, so a base rule declared after the media query silently outranks it. That is
     what left this one pinned to the window while #hint moved. */
  /* Anchored to the PICTURE's own bottom-left corner, measured, not to the window's -- the same
     place the narrow layout has always put it, and the same measured corners #picSize uses for the
     opposite one. On a wide screen the window's corner is a long way from the shot it describes. */
  #rate { display: none; position: fixed; z-index: 20; font-size: 11px;
    left: calc(var(--pic-left, 12px) + 4px); bottom: calc(var(--pic-bottom, 12px) + 4px);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #5cf07a;
    opacity: .85; text-shadow: 0 1px 2px rgba(0,0,0,.5); pointer-events: none; }
  :root.debug #rate { display: block; }
  /* What the picture is actually DRAWN at, which is not what arrives -- the stream is a fixed 720x960
     and this is whatever the layout gave it. The other half of whether the resolution being sent is
     being used, so it wears the readout's green and appears with it. Anchored to the picture's own
     top-right corner, measured, for the same reason the bottom controls are. */
  #picSize { display: none; position: fixed; z-index: 20;
    top: calc(var(--pic-top, 10%) + 6px); right: calc(var(--pic-right, 14px) + 8px);
    font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #5cf07a;
    opacity: .85; text-shadow: 0 1px 2px rgba(0,0,0,.5); pointer-events: none; }
  :root.debug #picSize { display: block; }
  /* The avatar's name and the broadcaster's name, each under the marker its subject belongs to.
     Narrow layout only -- the wide one has both cards on screen already, so there is nothing to fold
     up. Off here; the media query at the foot of the sheet turns them on.
     .empty is "nothing to name yet": before a stream arrives both values are a placeholder dash, and
     an underlined dash is a control that leads nowhere. */
  .nameTag { display: none; position: fixed; z-index: 21; }
  .nameTag.empty { display: none !important; }
  /* No frame of its own -- it is a name written on the picture. The underline is the whole of the
     affordance: there is no border, no chevron and no background here to say it can be pressed. */
  .nameTagBtn { font: inherit; font-size: 12.5px; font-weight: 600; line-height: 1.2;
    color: #eef2f6; background: none; border: 0; padding: 0; cursor: pointer;
    max-width: min(60vw, 15rem); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
    /* Faded, not greyed and not thinned: the weight and the colour are the ones the rest of the
       chrome uses, and this lets a little of the shot through them. On the element rather than in the
       colour so the underline fades with the text -- a full-strength rule under faded letters is what
       gives away a colour-only version of this. */
    opacity: .8; transition: opacity .2s; }
  .nameTag.open .nameTagBtn { opacity: 1; }
  /* Hung off the tag rather than off the window, which is what keeps each panel on its own side: the
     tag is already fixed to that edge, so left: 0 / right: 0 below is measured from the NAME. The two
     cannot drift apart, and neither needs to know the other's width. */
  /* Sized to what the card says, between a floor and a ceiling, rather than one width for every card:
     a short name gets a short card, and a long one stops at the ceiling and ellipses inside it rather
     than growing across the shot. The floor keeps a two-word card off looking like a sliver.
     max-content, NOT auto. auto is shrink-to-fit, which is capped by the space available in the
     containing block -- and the containing block here is the tag, whose own width is the NAME BUTTON.
     So the panel came out as wide as the name above it: 178px for a card wanting 272 (measured), and
     both cards at the floor whenever the name was short. max-content asks the card instead. */
  .nameTagBody { position: absolute; top: calc(100% + 7px);
    width: max-content; min-width: 10rem; max-width: min(20rem, calc(100vw - 2 * var(--edge)));
    /* Shown outright, with no fade. The card inside is frosted, and an ancestor at opacity < 1 groups
       its subtree: backdrop-filter then has that group as its backdrop rather than the page, so the
       blur does not resolve until the fade finishes. On a Pixel 10a that was a visible couple of tenths
       of the panel sitting there unfrosted -- which is the fade's own .18s, not the device being slow.
       A panel that is simply there is also the honest reading of a tap. */
    visibility: hidden; }
  .nameTag.open .nameTagBody { visibility: visible; }
  /* The card arrives exactly as the wide layout draws it: its own border, and no fill behind it. A
     background was tried and is deliberately not here -- it turned the card into a different object
     from the one beside the picture, which is the thing this is supposed to be showing.
     The avatar card gets a ceiling, because here it opens over the shot rather than into a column and
     its own max-height is a percentage of that column -- out of it, that computes to no limit at all,
     and a long list of shop links would run off the screen. The id in the selector is what outranks
     it.
     The BROADCASTER card deliberately gets neither. It is 70px of fixed content and never needed a
     ceiling, and overflow: auto made it a scroll container -- which clips absolutely positioned
     descendants. Its "i" tooltip is 165px and hangs 153px below the card, so the whole explanation
     was being cut off at the card's edge (measured: a hit test at the tooltip's middle returned the
     video). A limit nothing needed cost the panel the only thing in it worth reading. */
  .nameTagBody #info { max-height: 50vh; overflow-y: auto; }
  /* Swaps the bottom slot between the comments and the avatar controls. Narrow layout only -- on a
     wide one both columns are on screen at once and there is nothing to trade. */
  #viewSwap { display: none; }
  /* Comment column. Watching never needs an account; the account controls live up here because
     signing in is only ever about being able to speak. */
  /* No panel: the comments float over the page as bubbles. --chat-left is measured in JS from the
     picture's real edge, which is not the same as the video element's -- the element is
     letterboxed whenever its box and the 4:3 portrait picture disagree. */
  /* Boxed to the picture on all four sides: top, left and height are measured, so the frame is
     exactly as tall as the video beside it. right: 0 is only the fallback for before the first
     measurement, since a fixed box with left AND width set ignores right. */
  /* The right side is one column holding two panels: the broadcaster card, which is as tall as
     its contents, and the comments taking whatever is left down to the video's bottom edge. A
     flex column does that without anyone having to measure the card's height. */
  #rightcol { position: fixed; top: var(--panel-top, 10%);left: var(--chat-left, auto); right: 0;
    height: var(--panel-h, 80%); width: var(--cw); z-index: 15;
    display: flex; flex-direction: column; gap: 12px; }
  /* Always present: a broadcast cannot start without a name, so there is no empty state to hide. */
  /* The two padding values the "i" in the corner has to agree with are named, so the button and the
     card cannot drift apart when one of them is resized. */
  #caster { --card-pad-t: 11px; --card-pad-x: 14px;
    position: relative; flex: none; display: flex; flex-direction: column; gap: 4px;
    padding: var(--card-pad-t) var(--card-pad-x) 12px;
    border: 1px solid rgba(255, 255, 255, .45); border-radius: 8px; }
  /* Ko-fi's tip widget, embedded rather than linked, so the amount is chosen and paid right here.
     The slot keeps its height either way, so the card does not resize when a handle arrives. */
  /* The panel is 366px tall whatever width it is given -- measured the same at 320 and at 600 --
     so it cannot be made shorter by reflowing it. It CAN be made smaller: render it at a wider
     layout width and scale the whole frame down, which shrinks the height by the same factor.
     The cost is type size, so the render width is a dial between compact and legible. Sized in JS
     against the column's real width. */
  /* A handle to pay, but Ko-fi not yet confirmed to be delivering here: the tip will go through and
     will not come back as a superchat. Said right above the widget, because that is where somebody
     is about to decide, and nowhere else -- the room card's ON/OFF is for whoever is only wondering.
     Aligned to the widget's right edge on the wide layout, where the widget sits under a card whose
     text runs left; and to its left edge in the popup, where there is nothing else to line up with.
     Only when there IS a handle -- with none, the widget is a placeholder and the note would be
     answering a question nobody asked. */
  /* It costs no height anywhere. A line of its own pushed the widget down in a card whose height is
     fixed, and pushed it down in the popup for a caption -- so instead it goes into a band that
     already exists and is half empty: the broadcaster's name row here, the popup's cross row there.
     Neither shares its end of that band, which is why one is right and the other left.
     -30px cancels the clearance .casterName keeps for the "i" in the corner. The name still needs
     that clearance; this note does not, because it sits BELOW the mark, and giving it back is what
     puts its right edge on the widget's rather than 30px inside it. */
  #kofiUnlinked { display: none; margin: 0 -30px 0 0; flex: none;
    font-size: 11px; letter-spacing: .02em; color: #f5c451; text-align: right; }
  /* Bottom of the row, not centred on it: the "i" in the corner reaches 3px into this row, and both
     of them want its right-hand end. Sat on the baseline the note passes under it. flex-end rather
     than a nudge in pixels, so it stays clear if either the mark or the row changes size. */
  .casterName #kofiUnlinked { margin-left: auto; align-self: flex-end; }
  #kofiUnlinked.show { display: block; }
  #kofiWrap { margin-top: 10px; border-radius: 8px; overflow: hidden;
    border: 1px solid rgba(214,222,230,.22); background: rgba(255,255,255,.05); }
  /* Ko-fi's own body is #f7f7f778 -- 47% opaque, and their CSS says why: "transparency to blend
     with site content". So the iframe ELEMENT's background is what tints the panel, and it shows
     through at 53%. Putting the app's #59738c here would composite to #a3b1be, too washed out to
     read as our colour, so the backdrop is pushed darker to land on the intended tone.
     #6284a6 over their overlay comes out #a8bacc: our blue, a shade lighter. */
  #kofiFrame { display: none; border: 0; background: #6284a6;
    transform-origin: top left; }
  /* Ko-fi leaves the frame on its receipt page after a tip and offers no way back -- goBack()
     exists but only on a compact mode we cannot switch on. So the way back is ours. */
  #kofiWrap { position: relative; }
  #kofiBack { position: absolute; right: 8px; bottom: 8px; font: inherit; font-size: 11.5px;
    color: #1a222c; background: rgba(249,250,252,.94); border: 1px solid rgba(0,0,0,.12);
    border-radius: 999px; padding: 5px 11px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,.2); }
  #kofiBack:hover { background: #fff; }
  #kofiBack[hidden] { display: none; }
  /* In the card's own top-right corner, not on the name's line. It is about the card -- what a tip
     does and what it buys -- rather than about the broadcaster, and on the name's line it read as
     something to do with the name. Overlaid on the WIDGET is where it started, which was worse still:
     it had to dodge a scrollbar it cannot measure, the frame being another origin, and it sat on top
     of the thing it was describing.
     Out of the flow, so the row it used to share is now just the name and the card's height is
     unchanged. The two rows above it are held clear of it by hand, below. */
  .casterName { display: flex; align-items: center; gap: 8px; }
  #caster > .infoLabel, .casterName { padding-right: 30px; }
  /* absolute against the card, off its named padding, so it lands in the corner at either size. It is
     still the containing block #kofiHintBody hangs its right edge off.
     The z-index is load-bearing: the body drops DOWN over #kofiWrap, which used to be its parent and
     is now its later sibling. Both positioned with z-index auto paint in DOM order, so without this
     the widget wins. */
  #kofiHint { position: absolute; top: var(--card-pad-t); right: var(--card-pad-x); z-index: 3; }
  /* Its look, size and states all come from the shared rule up beside #hintBtn. */
  /* Shown by toggling display from JS rather than by a :hover descendant rule. The rule matched
     and still lost -- rather than keep guessing why, this is the version whose state can be read
     back and asserted. */
  #kofiHintBody { display: none; position: absolute; right: 0; top: 27px; width: min(17rem, 78vw);
    background: rgba(18,19,22,.95); backdrop-filter: blur(8px); color: #d7dade;
    border: 1px solid rgba(255,255,255,.14); border-radius: 10px; padding: 11px 13px;
    font-size: 11.5px; line-height: 1.75; text-align: left; }
  #kofiHintBody.show { display: block; }
  /* Keyboard reaches it without the JS having to listen for focus -- which it used to, and could not:
     focus fires BEFORE click, so a tap opened it and then the click's toggle closed it again. */
  #kofiHintBtn:focus-visible + #kofiHintBody { display: block; }
  #kofiHintBody b { color: #f0f1f3; font-weight: 600; display: block; margin-bottom: 4px; }
  #kofiHintBody b.inline { display: inline; margin: 0; }
  #kofiHintBody span { color: #9aa0a6; }
  #kofiEmpty { display: flex; height: 100%; align-items: center; justify-content: center; gap: 8px;
    font-size: 13px; font-weight: 600; color: #98a3b0; }
  #kofiEmpty svg { display: block; fill: currentColor; opacity: .75; }
  #comments { flex: 1; min-height: 0;
    display: flex; flex-direction: column; padding: 12px; gap: 10px;
    border: 1px solid rgba(255, 255, 255, .45); border-radius: 8px; }
  /* Account pill: fixed to the window, deliberately independent of the comment frame. */
  #acct { position: fixed; top: 14px; right: 14px; z-index: 25;   /* see #topleft for why 25 */
    min-height: 32px;                                             /* and see #badge for why 32 */
    display: flex; align-items: center; gap: 8px; font-size: 12px; max-width: calc(100vw - 28px);
    /* The page's own light surface, the one the comment bubbles and the way-back button already
       wear, rather than a fourth tone. Light ground means the hover tint has to darken: a white
       wash on white is invisible. */
    background: rgba(249,250,252,.93); backdrop-filter: blur(8px);
    /* The padding is the ring around the avatar, and at 6px it read as a thick outline drawn ON the
       icon rather than a surface behind it. 4px leaves 5px of ring with the border, which reads as
       backing. top/right stay put, so the pill shrinks inward from the corner it is pinned to and
       the margin to the screen edge is unchanged. */
    border: 1px solid rgba(0,0,0,.12); border-radius: 999px; padding: 3px 4px; }
  /* The row's height, and through it the pill's: 24 + 3px padding either side + 1px border = 32,
     which is also 22px of avatar + 4px either side + the border. Collapsed to the avatar the pill is
     therefore SQUARE, and border-radius: 999px makes a square a circle -- at 36x37 it was an oval
     tall enough to notice. It is one number because both facts depend on it: change it, or either
     padding, without redoing this sum and the pill stops being round.
     The buttons give up their vertical padding to it. A button taller than the row would push the
     pill back out, so the row sets the height and the label is centred inside it. */
  #acct { --pill-row: 24px; }
  #acct button { font: inherit; color: #1a222c; background: none; border: 0; cursor: pointer;
    padding: 0 10px; border-radius: 999px;
    min-height: var(--pill-row); display: grid; place-items: center; }
  #acct button:hover { background: rgba(0,0,0,.08); }
  #signIn, #acctIn { min-height: var(--pill-row); }
  /* Signed in, the pill is just the avatar. Both the handle and the sign-out label are off it: the
     handle moves into the card below, where the address already was, and the label appears only on
     approach. No padding-left on the row any more -- with nothing but the avatar in it, the pill's
     own symmetric padding is what centres the icon. */
  #acctIn { display: none; align-items: center; gap: 0; min-width: 0; }
  #acctWho { display: flex; align-items: center; gap: 8px; min-width: 0; cursor: pointer; }
  #acctIn img { width: 22px; height: 22px; border-radius: 50%; display: block; flex: none; }
  #acctName { display: none; }
  #acctIn button { font-size: 11px; padding: 0 9px; flex: none; }
  /* The pill is anchored by its RIGHT edge, so widening it moves everything inside LEFTWARD. That
     is the whole animation: reveal the label and the icon slides left of its own accord.
     max-width rather than display, so the width can be interpolated; nowrap so the label does not
     wrap itself into two lines on the way out; and the padding and the gap collapse with it, or the
     pill would hold 26px of empty room while closed. Outranks `#acctIn button` above, which would
     otherwise keep reinstating the horizontal padding. */
  #acctIn #signOut { max-width: 0; margin-left: 0; padding-left: 0; padding-right: 0; opacity: 0;
    white-space: nowrap; overflow: hidden;
    transition: max-width .2s ease, margin-left .2s ease, padding .2s ease, opacity .15s ease; }
  #acct:hover #acctIn #signOut, #acct.open #acctIn #signOut {
    max-width: 7em; margin-left: 8px; padding-left: 9px; padding-right: 9px; opacity: 1; }
  /* The address the account signed in with, hung under the pill. Same card as the page's other
     hints rather than the browser's own tooltip: that one arrives a second late, in whatever the
     OS thinks a tooltip looks like, which is neither this page nor quick enough to feel connected
     to the thing you pointed at. Toggled from JS for the same reason #kofiHintBody is. */
  #acctMail { position: absolute; top: calc(100% + 12px); right: 0; z-index: 21;
    /* width, not just max-width: absolutely positioned, this box is shrink-to-fit, and against an
       INDEFINITE width the value column's minmax(0, 1fr) is free to resolve to zero -- measured, it
       did. max-content makes the width definite so the fr has something to be a fraction of, and the
       cap still applies on a narrow screen, where the column shrinks and the value ellipsises. */
    width: max-content; max-width: calc(100vw - 28px); padding: 9px 13px;
    background: rgba(18,19,22,.95); backdrop-filter: blur(8px); color: #e4e7ea;
    border: 1px solid rgba(255,255,255,.14); border-radius: 10px;
    font-size: 11.5px; line-height: 1.55; text-align: left;
    /* Two rows now that the handle lives here too, on a grid so the labels and the values line up
       rather than each row finding its own indent. The clipping moves to the values: it is an
       address that might not fit, never the word in front of it. */
    display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 3px 10px;
    box-shadow: 0 6px 20px rgba(0,0,0,.3); pointer-events: none;
    opacity: 0; visibility: hidden; transform: translateY(-3px);
    transition: opacity .13s, transform .13s, visibility .13s; }
  #acctMail.show { opacity: 1; visibility: visible; transform: none; }
  #acctMail .k { color: #9aa0a6; letter-spacing: .04em; }
  #acctMail .v { font-variant-numeric: tabular-nums;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  #acctMail .v.none { color: #f5c451; }
  /* Avatar panel: the comment column's mirror image on the other side of the picture, same width
     and same frame, so the video sits between two equal margins. */
  /* Mirror of #comments, so the constraint that must win is the opposite one: left stays auto and
     right is the measured edge. Setting both plus a width would silently drop right in LTR. */
  /* Height follows the content, capped at the picture: this is a short, static card, and stretching
     it to the video's full height left a column of empty frame under three lines of text. */
  /* Present from the first paint, like the broadcaster card: the panel holds em-dashes until the
     broadcast says otherwise, so the video does not shift sideways when the details land. */
  /* The left side is one column holding the avatar card and the control panel, the mirror of
     #rightcol. The card is as tall as its contents; the controls take what is left. */
  /* THE FOUR PANELS GET A SURFACE. They were outlined cards with no fill -- light text straight on
     a dark page -- which stopped working the moment the ground became the shelf's wall. The fill is
     the same dark glass the ⓘ panel has always used, so every colour inside them is still being
     read against dark, which is what all of them were drawn for. One rule instead of re-colouring a
     hundred literals. */
  #info, #ctrl, #caster, #comments { background: var(--card); }


  #leftcol { position: fixed; top: var(--panel-top, 10%);right: var(--info-right, 50%); left: auto;
    height: var(--panel-h, 80%); width: var(--cw); z-index: 15;
    display: flex; flex-direction: column; gap: 12px; }
  /* Panels off, from the hint panel's checkbox. These two columns ARE the four panels -- #leftcol
     holds the avatar card and the controls, #rightcol the streamer card and the comments -- so one
     rule covers all of them and nothing new has to be listed here when a panel moves between
     columns. What it deliberately does NOT reach is the three.js canvas and its pivot ring: those
     are siblings of these columns, not children, so a turn in progress keeps its gizmo. The
     picture keeps its size -- hiding the panels is not a request to re-frame the shot. */
  :root.bare #leftcol, :root.bare #rightcol { display: none !important; }
  /* The card keeps its natural height and the controls take what is left -- but not past 45% of
     the column, or a short window would leave the animation list nothing to sit in. */
  #info { flex: 0 0 auto; min-height: 0; max-height: 45%;
    display: flex; flex-direction: column; padding: 14px 16px 16px; gap: 15px; overflow-y: auto;
    border: 1px solid rgba(255, 255, 255, .45); border-radius: 8px;
    scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.22) transparent; }
  #info::-webkit-scrollbar { width: 6px; }
  #info::-webkit-scrollbar-thumb { background: rgba(255,255,255,.22); border-radius: 3px; }
  .infoBlock { display: flex; flex-direction: column; gap: 5px; }
  .infoLabel { font-size: 11px; color: #e3ecf5; letter-spacing: .06em; }
  .infoValue { font-size: 17px; line-height: 1.35; color: #f4f7fa; overflow-wrap: anywhere; }
  #infoUrls { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }
  /* Plain links, one line each. A shop URL is long and uninteresting past the item id, so it is
     shortened for display and kept whole in href and title. */
  /* inline-block, not block: a block fills the panel, so the underline ran the full width however
     short the link was. This shrink-wraps to the text and only ellipses when it would overflow. */
  #infoUrls a { display: inline-block; max-width: 100%;
    font-size: 13.5px; line-height: 1.5; color: #dcebfd;
    text-decoration: none; border-bottom: 1px solid rgba(220,235,253,.28);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  #infoUrls a:hover { color: #fff; border-bottom-color: rgba(255,255,255,.75); }
  #infoNone { font-size: 13px; color: #c3ccd4; }
  /* BOOTH's own 200x40 banner, under the shop links and pointing at the FIRST of them. The 40px box
     is held whether or not there is a link -- paintInfo adds `.empty`, which hides the image but
     keeps the height, so the panel does not jump when a URL lands. Left-aligned at the banner's own
     width, shrinking to fit a narrow card. */
  #infoBanner { display: block; margin-top: 2px; }
  #infoBanner img { display: block; width: 200px; max-width: 100%; height: auto; }
  #infoBanner.empty { height: 40px; }
  #infoBanner.empty img { display: none; }
  /* Avatar controls. Blue and white like the rest of the page -- the app's own menu is a different
     instrument on a different screen, and copying its look here would only make this one look
     borrowed. The one warm thing on the page is the button that takes the turn. */
  #ctrl { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; gap: 12px;
    padding: 13px 14px 14px;
    border: 1px solid rgba(255, 255, 255, .45); border-radius: 8px; }
  /* Deliberately large: it is the one thing on this page that changes what everyone else is
     watching, so it is sized against the comment field's send button, not against it. */
  /* A flat pill with a glyph on the left and the label stacked over its status line. The 92px of
     height is spent on CONTENT rather than on centred air, the glyph says which way the button
     goes before the words are read, and the fully round end matches every other control on the
     page -- the comment field, its send button, the account badge are all pills already. */
  #ctlBtn { flex: none; min-height: 92px; display: flex; flex-direction: row;
    align-items: center; justify-content: flex-start; gap: 13px; padding: 14px 18px;
    font: inherit; font-size: 17px; font-weight: 700; letter-spacing: .02em; text-align: left;
    color: #3d1e06; background: #f7a63a;
    border: 0; border-radius: 999px; cursor: pointer;
    transition: background .15s, transform .08s; }
  #ctlBtn:hover:not(:disabled) { background: #ffb457; }
  #ctlBtn:active:not(:disabled) { transform: scale(.985); background: #e89325; }
  /* Holding the turn is a different job from asking for one, so it gets its own colour: still
     warm, but hotter, and it reads as the thing you press to stop. */
  #ctlBtn.holding { color: #431409; background: #ef6a3d; }
  #ctlBtn.holding:hover:not(:disabled) { background: #f47a4f; }
  /* Waiting and locked are not being offered, so neither is filled. */
  #ctlBtn.queued { color: #f0e2cd; background: rgba(255,255,255,.10);
    box-shadow: inset 0 0 0 1px rgba(247,166,58,.45); }
  /* :not(.pending) so the locked grey means only what it has always meant -- no account, or no
     broadcast to control. A press waiting to be answered is a different thing and gets the
     treatment below, or the button would flash grey and back on every round trip. */
  #ctlBtn:disabled:not(.pending) { cursor: default; color: #a9b8c8; background: rgba(255,255,255,.08);
    box-shadow: inset 0 0 0 1px rgba(214,222,230,.26); }
  /* In flight: the press has gone out and the app has not answered yet. Inert, because a second
     press racing the first is what made rapid clicking misbehave, but still wearing the colour of
     the state it is in, so the wait reads as a pause rather than as the button going away. */
  #ctlBtn.pending { cursor: progress; opacity: .62; }
  .ctlGlyph { flex: none; width: 38px; height: 38px; border-radius: 50%; display: grid;
    place-items: center; background: rgba(255,255,255,.34); }
  #ctlBtn.queued .ctlGlyph { background: rgba(247,166,58,.28); }
  #ctlBtn:disabled:not(.pending) .ctlGlyph { background: rgba(255,255,255,.10); }
  /* One glyph per state, picked in CSS off the same class the label is picked by, so there is no
     second place that has to be told what the state is. */
  .ctlGlyph svg { display: none; width: 20px; height: 20px; fill: currentColor; }
  #ctlBtn .g-idle { display: block; }
  #ctlBtn.holding .g-idle, #ctlBtn.queued .g-idle { display: none; }
  #ctlBtn.holding .g-stop { display: block; }
  #ctlBtn.queued .g-wait { display: block; }
  .ctlStack { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
  /* pre-line so the timeout warning can put its parenthetical on its own line -- the number is
     what you read at a glance, and the reason should not be sharing a line with it. */
  #ctlBtnSub { font-size: 11.5px; font-weight: 600; letter-spacing: .03em; opacity: .82;
    font-variant-numeric: tabular-nums; white-space: pre-line; line-height: 1.45; }
  #ctlBtnSub:empty { display: none; }
  #ctlBody { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 11px; }
  /* Dimmed rather than removed while somebody else has the turn: what you would get to do is
     part of what the button is offering. */
  #ctlBody.off { opacity: .45; }
  .ctlRow { flex: none; display: flex; align-items: center; gap: 10px; }
  .ctlRow > label { flex: none; font-size: 11px; letter-spacing: .06em; color: #e3ecf5; }
  /* Both dropdowns, one rule -- they are the same control offering two catalogs, and two sets of
     numbers would drift. The animation one is only on the narrow layout (see the bottom of the
     sheet); on a wide one the list of pills beside it is better, since it shows every animation at
     once and takes one press rather than three. */
  #ctlFov, #ctlAnimSel { flex: 1; min-width: 0; font: inherit; font-size: 13px; font-weight: 600;
    color: #16324f; background-color: #e8f1fc; border: 1px solid rgba(158,202,255,.55);
    border-radius: 8px; padding: 7px 30px 7px 11px; cursor: pointer;
    -webkit-appearance: none; appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%2316324f' stroke-width='1.6' stroke-linecap='round'/></svg>");
    background-repeat: no-repeat; background-position: right 11px center; background-size: 10px 6px; }
  #ctlFov:disabled, #ctlAnimSel:disabled { cursor: default; }
  /* The dropdown's own optgroups carry the group names, so the labels are the app's headings either
     way -- <optgroup> is native, including in the iOS picker, so the grouping did not have to be
     given up to fit the narrow layout. */
  #ctlAnimSel optgroup { font-size: 12px; font-weight: 700; }
  #ctlAnimSel option { font-weight: 600; }
  /* Off unless the narrow layout asks for it. Declared before that block, so the override wins on
     source order at equal specificity -- the trap documented at the bottom of this sheet. */
  #ctlAnimRow { display: none; }
  #ctlAnim { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden;
    display: flex; flex-direction: column; gap: 11px; padding-right: 2px;
    scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.22) transparent; }
  #ctlAnim::-webkit-scrollbar { width: 6px; }
  #ctlAnim::-webkit-scrollbar-thumb { background: rgba(255,255,255,.22); border-radius: 3px; }
  .ctlGroup { display: flex; flex-direction: column; gap: 6px; }
  .ctlGroupName { font-size: 11px; letter-spacing: .06em; color: #e3ecf5; }
  .ctlItems { display: flex; flex-wrap: wrap; gap: 6px; }
  .ctlItem { font: inherit; font-size: 12px; line-height: 1.3; padding: 6px 11px; border-radius: 999px;
    /* SOLID, not a wash of whatever is behind. These were a white veil over the ground, so every
       change of ground moved them: on a lighter blue the same veil comes out paler and the label
       goes with it. The values here are what that veil composited to on the blue this page was
       drawn in -- measured, not guessed -- so the chips look exactly as they did and stay put
       whatever the page is grounded in next. The ON state was already opaque. */
    color: #dce9f8; background: #688096;
    border: 1px solid #7a99ba; cursor: pointer; transition: background .12s; }
  .ctlItem:hover:not(:disabled) { background: #778ca1; }
  .ctlItem.on { color: #123252; background: #e8f1fc; border-color: #e8f1fc; font-weight: 700; }
  .ctlItem:disabled { cursor: default; }
  #chatList { flex: 1; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column;
    gap: 8px; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.22) transparent; }
  #chatList::-webkit-scrollbar { width: 6px; }
  #chatList::-webkit-scrollbar-thumb { background: rgba(255,255,255,.22); border-radius: 3px; }
  #chatEmpty { margin: auto 0; text-align: center; font-size: 12px; color: #c3ccd4; line-height: 1.9;
    text-shadow: 0 1px 3px rgba(0,0,0,.4); }
  .cmt { display: grid; grid-template-columns: 26px 1fr; gap: 8px; align-items: start; }
  .cmt img, .cmt .noavatar { width: 26px; height: 26px; border-radius: 50%; display: block; margin-top: 2px; }
  .cmt .noavatar { background: rgba(255,255,255,.2); }
  /* Light bubbles with dark text: comments are the one thing here meant to be read at length, and
     they sit over a mid-tone video, so they carry their own contrast rather than borrowing it. */
  .cmt .bubble { background: rgba(249,250,252,.93); backdrop-filter: blur(8px);
    border: 1px solid rgba(255,255,255,.5); border-radius: 16px;
    padding: 7px 12px 8px; min-width: 0; color: #1a222c; }
  .cmt .who { font-size: 11px; color: #5f6975; margin-bottom: 2px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .cmt .txt { font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
  .cmt.mine .bubble { background: rgba(222,235,250,.94); border-color: rgba(255,255,255,.6); }
  .cmt.mine .who { color: #2f5b8c; }
  /* Superchats: one red for every amount, by choice -- tiering by size turns a thank-you into a
     leaderboard. Held back from full strength: solid #d00000 shouts next to the pale comment
     bubbles, and the backdrop takes enough out of it to read as a warm red, not a warning. */
  .cmt.tip .bubble { background: rgba(208,0,0,.78); border-color: rgba(255,255,255,.35); color: #fff; }
  .cmt.tip .who { color: rgba(255,255,255,.88); display: flex; gap: 6px; align-items: baseline; }
  .cmt.tip .amt { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums;
    font-size: 12.5px; color: #fff; }
  .cmt.tip .txt { font-weight: 500; }
  /* A tip whose sender could not be matched to an X account gets Ko-fi's own mark instead of a
     blank disc: where the money came from is the one thing actually known about them. No disc
     behind it -- the mark is wider than a circle this size and would lose its handle to the crop. */
  /* Ko-fi's official mark. Held in a custom property because two things wear it: an unmatched tip's
     avatar, and the button that opens the tip widget on a narrow screen.
     Served from our own origin as a file. It used to be a 6.7KB base64 data URI on one line, on the
     stated grounds that "a strict CSP blocks every external host" -- there is no CSP on this site at
     all (checked the response headers), and the page loads two libraries from jsdelivr, so that
     reasoning was never true. NOT named kofi-*: Caddy proxies /kofi* to the token service, so
     anything under that prefix would be answered by the webhook endpoint instead of served. */
  :root { --kofi-mark: url("/tip-mark.png"); }
  .cmt.tip .noavatar { border-radius: 0; background: var(--kofi-mark) center/contain no-repeat; }
  /* Always present, and starts DISABLED: that is the truth before the session is known, so there
     is no flash of an input box that turns out not to be usable -- and no row appearing from
     nowhere once it turns out to be. Signing in enables it; it never changes shape.
     Why the disabled row rather than a message in its place: the send button is half of what says
     this is a composer, and swapping it for a line of text left the panel looking like it had no
     way to write at all. The reason it is locked goes in the field's own placeholder, where the
     text it replaces would have been. */
  /* Grey rather than dark, so it does not compete with the bubbles -- but solid enough to read as
     a control you can click, which the near-invisible first attempt did not. */
  #chatForm { display: flex; gap: 8px; flex: none; }
  #chatInput { flex: 1; min-width: 0; font: inherit; font-size: 13px; color: #e4e8ec; padding: 8px 13px;
    background: rgba(255,255,255,.11); border: 1px solid rgba(214,222,230,.44); border-radius: 999px; }
  #chatInput::placeholder { color: #d3ddea; }
  #chatInput:focus { outline: none; border-color: rgba(214,222,230,.7); background: rgba(255,255,255,.16); }
  /* Solid, and the only filled thing in the frame: it is the one action here, and pairing a
     filled button with the outlined field is what makes which is which obvious. */
  #chatSend { font: inherit; font-size: 13px; font-weight: 600; color: #1b2430; background: #dbe4ee;
    border: 0; border-radius: 999px; padding: 9px 16px; cursor: pointer; flex: none;
    transition: background .15s; }
  #chatSend:hover:not(:disabled) { background: #eef4fa; }
  #chatSend:disabled { opacity: .4; cursor: default; }
  #chatInput:disabled { color: #aeb7c1; background: rgba(255,255,255,.06);
    border-color: rgba(214,222,230,.26); cursor: default; }
  /* Ko-fi on a narrow screen. The widget's home is the broadcaster card, which the narrow layout
     hides -- so rather than a second copy of the widget, #kofiWrap is RE-PARENTED into this popup
     below 900px and back into the card above it. One iframe and one code path; the cost is that the
     frame reloads when the breakpoint is actually crossed, which is a resize nobody performs
     halfway through paying. */
  #kofiOpen { display: none; }
  #kofiPop { display: none; position: fixed; inset: 0; z-index: 50;
    place-items: center; padding: 16px;
    background: rgba(8,10,14,.66); backdrop-filter: blur(3px); }
  #kofiPop.show { display: grid; }
  /* No panel behind the widget: it brings its own surface, and a card around it was a second frame
     that said nothing. What is left is a positioning box for the cross, and the room above the
     widget to put it in. The dimming stays on #kofiPop -- that is what says the page behind is not
     the thing to press. */
  /* Sized against the VIEWPORT, not against the popup. 100% here resolved to 380 rather than to the
     padded width and hung 7px off the right edge of a 390px screen (measured): the popup is a grid
     whose single column is auto-sized, so the column takes the item's max-content contribution, and
     the item's percentage then resolves against a column its own width decided. vw has no such loop.
     340 rather than 380 with it -- the widget filled the screen edge to edge, which for a payment
     panel over somebody's stream reads as having taken the page over. The type comes down with it,
     from about 11.8px to 10.6, which is the trade being made. */
  #kofiPopCard { position: relative; width: min(340px, calc(100vw - 48px)); padding: 42px 0 0; }
  /* Centred explicitly rather than by arithmetic: fitKofi scales the frame to the slot's measured
     width, so the two normally coincide -- but if they ever stop coinciding, this is what decides
     where the difference goes. */
  /* A column, because the unlinked note stacks ABOVE the widget in here rather than beside it -- as a
     row they were two flex items sharing a line. */
  #kofiPopBody { display: flex; flex-direction: column; }
  /* In the cross's own row, at the other end of it -- 42px of padding is already reserved up there
     for the cross, and one short line does not need a second band under it. Left, because the cross
     has the right, and centred on the cross's line: it is 4-34, this is 12-27. */
  #kofiPopCard > #kofiUnlinked { position: absolute; top: 12px; left: 0; margin: 0;
    text-align: left; }
  #kofiPopBody #kofiWrap { width: 100%; }
  /* Only the cross closes it, deliberately. A tap on the backdrop is far too easy to make by
     accident, and partway through paying is the one moment on this page where a stray tap costs
     the visitor something. */
  #kofiPopClose { position: absolute; top: 4px; right: 0; width: 30px; height: 30px;
    display: grid; place-items: center; line-height: 1; padding: 0;
    font: inherit; font-size: 19px; color: #e8e8ea; cursor: pointer;
    background: rgba(255,255,255,.10); border: 1px solid rgba(255,255,255,.20);
    border-radius: 50%; }
  #kofiPopClose:hover, #kofiPopClose:focus-visible { background: rgba(255,255,255,.2); outline: none; }
  /* The 10px belongs to sitting under the broadcaster's name, which it no longer does in here. */
  #kofiPopBody #kofiWrap { margin-top: 0; }
  /* Narrow screens: no room for a column, so it stacks -- video above, comments below. The
     comments take a FIXED height and the video gets whatever is left, rather than both scaling
     with the viewport: a comment list that shrinks with the window stops holding enough lines to
     be worth reading, while the video degrades gracefully at any size. --cw drops to 0 so the
     video stops reserving side space for a column that is no longer beside it. */
  /* The turn arriving is the one thing on this page that happens TO you rather than because you
     just clicked something -- and it can arrive while you are watching the picture, nowhere near
     the button. So it is announced over the picture, in the button's own colour, and then gets out
     of the way. Below the badge/account row so it collides with neither on a narrow window. */
  #ctlToast { position: fixed; top: 62px; left: 50%; z-index: 30;
    display: flex; align-items: center; gap: 9px; padding: 11px 20px 11px 13px;
    border-radius: 999px; background: #f7a63a; color: #3d1e06;
    font-size: 14px; font-weight: 700; letter-spacing: .02em; white-space: nowrap;
    box-shadow: 0 6px 22px rgba(0,0,0,.28); pointer-events: none;
    visibility: hidden; opacity: 0; transform: translateX(-50%) translateY(-7px);
    transition: opacity .3s ease, transform .3s ease, visibility .3s; }
  #ctlToast.show { visibility: visible; opacity: 1; transform: translateX(-50%) translateY(0); }
  #ctlToast svg { display: none; width: 19px; height: 19px; fill: currentColor;
    background: rgba(255,255,255,.34); border-radius: 50%; padding: 5px; box-sizing: content-box; }
  /* The second line is the consequence, not the news, so it is quieter than the line above it. */
  #ctlToast .t-text { display: flex; flex-direction: column; gap: 2px; }
  #ctlToastSub { font-size: 11.5px; font-weight: 600; opacity: .78; letter-spacing: .02em; }
  #ctlToastSub:empty { display: none; }
  #ctlToast .t-turn { display: block; }
  #ctlToast.queue .t-turn { display: none; }
  #ctlToast.queue .t-queue { display: block; }
  @media (prefers-reduced-motion: reduce) { #ctlToast { transition: opacity .3s, visibility .3s; transform: translateX(-50%); } }
  /* A size down on a phone: it is a nowrap pill across a 375px screen, so at the wide layout's
     measurements it spans nearly the whole width and stops reading as a notice over the picture and
     starts reading as a bar across it. Every part steps down together -- shrinking the type alone
     would leave the padding looking swollen.
     Its own media query, next to the rules it overrides, because the main narrow-layout block is
     further UP the sheet: at equal specificity the later rule wins, so an override placed there
     would silently lose to these. */
  /* The camera marker: a three.js scene laid exactly over the picture, sharing its origin and its
     lens, so what you drag around is registered with what you are looking at. Inert until the turn
     is yours -- a handle that does nothing is worse than no handle. */
  #ctl3d { position: fixed; z-index: 12; pointer-events: none; }
  #ctl3d.live { pointer-events: auto; cursor: grab; }
  #ctl3d.live:active { cursor: grabbing; }
  /* What the camera is turning around. It only ever draws at the middle of the picture -- the
     camera looks straight at it, so that is where it lands -- but panning moves the point in the
     WORLD, and seeing it sit still while the scene slides past is what says so. */
  #ctlPivot { position: fixed; z-index: 13; width: 13px; height: 13px; margin: -7px 0 0 -7px;
    border-radius: 50%; border: 1.5px solid rgba(255,255,255,.92);
    box-shadow: 0 0 0 1.5px rgba(0,0,0,.35), inset 0 0 0 1px rgba(0,0,0,.25);
    pointer-events: none; display: none; }
  #ctlPivot.on { display: block; }
  /* Shown for a moment when the turn becomes yours, so the picture itself says it can be dragged
     -- the button that granted the turn is off to the side, and this is where the dragging happens.
     Centred on the PICTURE without measuring anything: #stage is the picture's container and the
     shot is centred in it on both axes, so the middle of the stage is the middle of the shot.
     Sits under #ctl3d, which is harmless -- that canvas is transparent, and by the time it has a
     gizmo to draw the pointer is already down and the hint already gone. */
  /* Over the shot, because that is where the answer is wanted: the picture stops moving while the
     broadcaster changes character, and without a word for it a stopped picture is a stopped stream.
     Inside #stage and centred on it, like the drag hint -- the picture is centred there too, so the
     two agree without either measuring anything.
     The badge's own look, borrowed whole: it is the same kind of thing, a small pill saying what the
     stream is doing. The dot is the page's existing pulse, which is already what "wait" looks like
     here -- it is on the 配信待機中 card. */
  #switching { position: absolute; top: 50%; left: 50%; z-index: 3;
    transform: translate(-50%, -50%); display: none; align-items: center; gap: 8px;
    font-size: 12.5px; font-weight: 600; letter-spacing: .02em; white-space: nowrap;
    background: rgba(20,21,24,.62); backdrop-filter: blur(6px); color: #e8e8ea;
    padding: 8px 14px; border-radius: 999px; border: 1px solid rgba(255,255,255,.14);
    pointer-events: none; }
  #switching.show { display: flex; }
  #switching .dot { margin-right: 0; }
  /* Why the drag stopped. The clamp itself is silent, and a drag that simply refuses to move reads
     as the page having jammed rather than as a rule being kept.
     Words only -- no pill, no fill, no shadow. It answers a question about the shot, so it takes as
     little of the shot as an answer can: type alone, faded enough to sit under the picture's own
     weight rather than on top of it. High on the picture, clear of both the avatar's head at a
     normal framing and the controls along the bottom edge. */
  /* A fixed distance under the picture's own top edge, measured, rather than a percentage of the
     stage: the stage is as tall as the window allows and the percentage shrank with it, so on a
     short window the words ended up against the top of the shot. 74px is the same 74px at every
     size. Horizontally it needs nothing measured -- the picture is centred in the window on both
     layouts, so the window's centre is the picture's.
     No transition. It is an answer to a press: it should be there when the press happens and gone
     when it stops, not easing in over a fifth of a second at either end. */
  /* max-content, because shrink-to-fit against left: 50% is capped at the half of the screen to the
     RIGHT of that line -- 195px on a 390px phone, which broke one sentence over two. The transform
     puts it back on the centre; the cap is the screen. */
  #peekToast { position: fixed; top: calc(var(--pic-top, 10%) + 74px); left: 50%;
    transform: translateX(-50%); width: max-content;
    max-width: calc(100vw - 32px); text-align: center; z-index: 14;
    font-size: 14px; font-weight: 600; letter-spacing: .02em; line-height: 1.5;
    color: rgba(255,255,255,.72);
    visibility: hidden; pointer-events: none; }
  #peekToast.show { visibility: visible; }
  /* Two hints, one box: the finger, then the pinch in its place. Pictures rather than the font's
     own emoji -- the glyph is whatever the device happens to draw, and on a phone that is a
     different hand from the one on the desktop. These are the same two drawings everywhere.
     No shadow under them. They carry their own dark outline, which is what keeps them legible
     over a pale avatar, and a soft drop shadow is exactly the depth cue this interface does not
     use anywhere else. */
  #dragHint { position: absolute; top: 50%; left: 50%; z-index: 2;
    transform: translate(-50%, -50%); pointer-events: none;
    display: flex; flex-direction: column; align-items: center; gap: 1px;
    opacity: 0; visibility: hidden; transition: opacity .3s, visibility .3s; }
  #dragHint i { display: block; width: 52px; height: 52px;
    background: center / contain no-repeat; }
  /* The word under the hand, in the same voice the peek toast uses: white type straight on the
     picture, no chip and no shadow. */
  #dragHint b { font-size: 20px; font-weight: 600; letter-spacing: .06em; line-height: 1;
    color: rgba(255,255,255,.88); }
  #dragHint.drag i { background-image: url(/hint-drag.png); }
  #dragHint.pinch i { background-image: url(/hint-pinch.gif); }
  #dragHint.show { opacity: .82; visibility: visible; }
  /* The slide is what reads as "drag", and it is the HAND that slides -- the word under it stays
     where it is, or it reads as part of the gesture rather than as its name. The animation owns
     transform and the fade owns opacity, because an animation and a transition on the same
     property fight over it. The pinch does not slide: its movement is already in the drawing. */
  #dragHint.show.drag i { animation: dragHintSlide 2s ease-in-out infinite; }
  @keyframes dragHintSlide {
    0%, 100% { transform: translateX(-40%); }
    50%      { transform: translateX(40%); }
  }
  @media (prefers-reduced-motion: reduce) {
    #dragHint.show.drag i { animation: none; }
  }

  /* ---- Narrow layout -------------------------------------------------------------------------
     LAST in the file, and one block, deliberately. These overrides only ever need to beat a base
     rule, never each other, and at equal specificity the later rule wins -- so anywhere but the
     end means "does the base rule happen to be declared above or below me?" decides whether an
     override works. That question cost two silent failures already: #rate stayed pinned to the
     window while #hint moved, and the toast's override had to be given its own media query next
     to the rules it overrode. Here there is nothing below to lose to.
     Feature queries (hover, prefers-reduced-motion) stay beside what they modify: they sit
     immediately after their own base rules, so the ordering question does not arise for them. */
  @media (max-width: 900px) {
    /* Edges stacked up from the window's bottom, each named once so they cannot drift: the comment
       column, its top edge (where the two controls stand), and the stage above it.
       --ch is 200px, down from 240 in two steps of 20. The column is a FIXED height on this layout and
       the picture takes what is left, so every pixel off it is a pixel the picture gets to keep -- and
       on any phone with browser chrome on screen the picture is height-bound, so it is real picture
       rather than letterbox. The cost is the comment list, which is down to three and a half rows.
       The stage keeps its 12px over the column and does NOT reserve a row for the controls, which is
       what makes those 40px reach the picture: the controls are 32px tall, so a strip of their own
       would have cost 26px more than the column gave up and left every height-bound phone with a
       SMALLER picture than before (375px against 381px at 393x659, measured). They overlay the
       picture's bottom edge instead -- which is what they already did, and the only thing being
       changed here is what they are anchored to. */
    :root { --cw: 0px; --ch: 200px;
      --chat-top: calc(var(--ch) + 14px);
      --ctl-row: 32px; --ctl-lift: 6px;
      --stage-bottom: calc(var(--chat-top) + 12px); }
    /* Down to the screen's own margin. The 52px of clearance that used to be here was somewhere for
       the hint button and the bitrate readout to sit; they now overlay the picture instead, so the
       comments get the rows back -- which on a fixed-height column is the whole point. */
    #rightcol { top: auto; bottom: 14px; height: var(--ch); left: 14px; right: 14px; width: auto; }
    #stage { top: 12px; bottom: var(--stage-bottom); }
    /* Both controls stand just clear of the comment column's top edge -- --ctl-lift off it, the same
       6px they used to keep off the picture's bottom edge, so they read as sitting above the column
       rather than as growing out of it. They used to hang
       off the picture's MEASURED bottom edge, which put them wherever the letterboxing left that
       edge: on a tall screen the picture is width-bound and floats inside the stage, so they floated
       with it and ended up in open space with 60px of nothing under them. The column is a fixed
       height and the layout's one stable line, so anchored to it they land in the same place on every
       screen -- and where the picture IS height-bound they sit on its bottom edge anyway, which is
       where they were.
       --pic-* stays measured for #rate and #picSize: those two are labels ON the picture, and they
       are meant to move with it. */
    #hint { right: 14px; bottom: calc(var(--chat-top) + var(--ctl-lift)); }
    /* Left end of that strip, the mirror of the hint button at its right end. BOTH modes are on it at
       once -- a finger, an arrow, a bubble -- so it reads as a switch between two things rather than
       as whichever one happens to be on. Nothing about it changes when pressed: the panel below is
       what says which mode you are in, and a label that swapped would leave you working out whether
       it shows the state or the action. */
    #viewSwap { display: inline-flex; align-items: center; gap: 3px; position: fixed; z-index: 20;
      left: 14px; bottom: calc(var(--chat-top) + var(--ctl-lift));
      height: var(--ctl-row); padding: 0 11px; cursor: pointer; line-height: 1; font-size: 15px;
      border-radius: 999px; border: 1px solid rgba(255,255,255,.28);
      background: rgba(20,21,24,.55); opacity: .85; transition: opacity .2s, background .2s; }
    #viewSwap:focus-visible { opacity: 1; background: rgba(20,21,24,.8); outline: none; }
    /* Punctuation between the two modes, so it is set as text: left to the emoji font it would be
       coloured and sized like the things it separates instead of joining them. */
    #viewSwap .v-arrow { font-family: ui-sans-serif, system-ui, sans-serif;
      font-variant-emoji: text; font-size: 13px; color: #cfd4da; }
    /* Flush into the picture's own bottom-left corner, no inset. Overlapping the swap button is fine
       and expected -- this is a diagnostic that is off by default, and pinning it to the corner it
       describes beats keeping it clear of a control that will not be read at the same time. */
    #rate { left: var(--pic-left, 14px); bottom: var(--pic-bottom, var(--stage-bottom)); }
    /* Smaller here: it sits ON the picture rather than in an empty corner, so it is asking for more
       of the shot than it is worth. The panel's 34px offset is unchanged, which just leaves the hover
       bridge more to cover -- it has 9px for what was a 4px gap at 26px, 8px at 22px.
       Both of them, as on the wide layout: they are the same offer -- a small round mark with
       something to say about what it sits beside -- and one of the two staying at 26px is exactly the
       two-kinds-of-control look the shared rule exists to avoid. The card's tooltip closes its own
       gap to match, since it hangs off the button rather than off a panel edge. */
    #hintBtn, #kofiHintBtn { width: 22px; height: 22px; font-size: 12px; }
    #kofiHintBody { top: 23px; }
    /* No room to flank the video, and the comments are the live thing -- the two info cards stay
       reachable from the stream's page but not at the cost of the picture. */
    /* Both cards are moved out into the name tags here, so what these hide is only ever a card that
       has not been moved yet -- the frame before syncCardHomes runs. Scoped to the column for exactly
       that reason: unscoped, they would go on hiding the card after it had arrived in its tag, which
       is where it is supposed to be visible. */
    #rightcol > #caster { display: none !important; }
    #leftcol > #info { display: none; }
    #leftcol { display: none; }
    /* On, and pinned under the marker each belongs to. The badge and the account pill are both 32px
       tall at top: 14px (measured, and they agree), so 52 clears them by 6. */
    .nameTag { display: block; }
    #avatarTag { top: 52px; left: var(--edge); }
    #casterTag { top: 52px; right: var(--edge); }
    #avatarTag .nameTagBody { left: 0; }
    #casterTag .nameTagBody { right: 0; }
    /* Panels off takes the names with it. They are not labels on the picture -- they ARE the two
       panels, folded up, so leaving them would be leaving two of the four on screen. */
    :root.bare .nameTag { display: none !important; }
    /* Back to 12.5 here. The 14 above is for a picture the width of a desktop window; at this size it
       is a sentence across most of the shot, and it has to fit on one line. */
    #peekToast { font-size: 12.5px; }
    /* The cards were drawn for a column of their own beside the picture. Here they open ON the
       picture, so they are sized down to what a card over a shot should take.
       One content size, 13.5px, which is the size the shop links were already at -- the value and the
       heading come DOWN to the small text rather than the small text going up. The 11px labels are
       untouched: they are the other half of the pair, and the panel still needs to say which line is
       which. The heading keeps its weight and its rule, so it is still a heading at the body size. */
    .infoValue { font-size: 13.5px; }
    /* And in, on both axes. The padding is the height: with 13.5px content, 14/16/16 was spending
       more of a 393px screen on air than on what the card says. */
    .nameTagBody { max-width: min(16rem, calc(100vw - 2 * var(--edge))); }
    /* The app's own ground at 78%, and no filter. Frosting was tried twice: a tint under it read as
       the card going dark, and without one the blur was doing all the work and cost a Pixel 10a a
       visible wait before it resolved. This is the colour the page is already made of, so a card over
       the shot reads as part of the app rather than as a pane laid on top of it, and there is nothing
       for the compositor to compute -- it paints with the frame it appears on.
       Dropping backdrop-filter also gives up a stacking context and a containing block, which the "i"
       tooltip was the reason to check. It hangs well outside this card, and now has one less thing in
       its way. */
    .nameTagBody #info, .nameTagBody #caster {
      background: var(--card-narrow); backdrop-filter: none; -webkit-backdrop-filter: none; }
    .nameTagBody #info { padding: 11px 13px 12px; gap: 11px; }
    /* Through the variables, not the padding shorthand: the "i" in the corner is placed off these
       two, so setting the shorthand here would move the card's edges and leave the button on the
       wide layout's. */
    .nameTagBody #caster { --card-pad-t: 9px; --card-pad-x: 12px; padding-bottom: 10px; }
    /* The swap. #leftcol takes exactly the box #rightcol had, so the picture above does not move when
       the two trade places, and the avatar card stays out of it: what goes in place of the comments is
       the CONTROLS, not both of them stacked into 240px.
       No !important here, which is what lets :root.bare still win over both -- hiding every panel
       has to outrank a choice of which panel. */
    :root.ctlview #rightcol { display: none; }
    :root.ctlview #leftcol { display: flex; top: auto; bottom: 14px; height: var(--ch);
      left: 14px; right: 14px; width: auto; }
    /* Panels off, on this layout. The swap button goes with them: it trades the comment column for the
       control column, and with both hidden there is nothing to trade -- it was the one thing left
       pointing at a box that is not there. The info button stays, because it is how the checkbox gets
       un-ticked again, and moves to the corner it would have had if there had never been a column
       under it: --chat-top is measured off a comment column that is now display: none.
       The picture takes the whole window inside the screen's own margin and centres in it. On the wide
       layout hiding the panels deliberately does NOT re-frame the shot -- the picture is beside them,
       so its size was never about them -- but here they were stacked under it and were the only reason
       it was not already doing this.
       A turn in progress is untouched: #ctl3d and the pivot ring are siblings of the two columns
       rather than children, so nothing here reaches them, and the canvas is placed from the MEASURED
       picture box -- it grows with the shot instead of staying where the smaller one was. */
    :root.bare #viewSwap { display: none; }
    :root.bare #stage { top: var(--edge); bottom: var(--edge); }
    :root.bare #hint { bottom: var(--edge); }
    /* The one part of the broadcaster card that survives: a way to tip. Beside the comment field
       rather than above it, because it is the same kind of thing -- something you send. */
    /* A rounded square rather than a circle, and the mark sized rather than cropped to fit: the cup
       is wider than it is tall and a round button this size would take its handle off -- the same
       reason the tip avatar drops its border-radius. */
    #kofiOpen { display: block; flex: none;
      width: 36px; height: 36px; padding: 0; cursor: pointer;
      background: var(--kofi-mark) center / 23px auto no-repeat, rgba(255,227,150,.94);
      border: 1px solid rgba(122,84,12,.26); border-radius: 10px; }
    #kofiOpen:hover, #kofiOpen:focus-visible {
      background: var(--kofi-mark) center / 23px auto no-repeat, #ffeab4; outline: none; }
    /* The popup can be dismissed outright, so the guess at whether a tip has happened is not
       needed to get back out of the widget -- and guessing wrong is worse in a popup, where the
       button would sit over a payment the visitor is still in the middle of. */
    #kofiBack { display: none !important; }
    /* One line per comment instead of two. The column's height is FIXED here, so every line saved is
       another comment on screen: the name and the text run on as a single paragraph rather than the
       name owning a line of its own. Inline, not flex, so a long comment wraps under the name the way
       prose does instead of being squeezed into a column beside it. The gap is a margin because the
       two elements are adjacent siblings with no whitespace between them -- built by JS, so there is
       no text node to fall back on, and they would otherwise butt together. */
    .cmt .who { display: inline; margin-bottom: 0; margin-right: 6px; white-space: normal; }
    .cmt .txt { display: inline; }
    /* One size for the whole run. Three different sizes on a single line read as three fragments
       rather than one sentence, and the mixed baselines were what made it look like a mistake. Set
       once on the bubble so there is one number here, not three that have to be kept equal. */
    .cmt .bubble { font-size: 11px; }
    .cmt .who, .cmt .txt, .cmt.tip .amt { font-size: inherit; }
    /* The amount joins the run instead of being pushed to the far edge: margin-left: auto only has
       an edge to reach while .who is a flex line of its own. */
    .cmt.tip .who { display: inline; }
    .cmt.tip .amt { margin-left: 6px; }
    /* Matched to the shorter row -- a 26px disc beside a single line of text reads as the bigger of
       the two things, which it is not. */
    .cmt { grid-template-columns: 22px 1fr; }
    .cmt img, .cmt .noavatar { width: 22px; height: 22px; margin-top: 1px; }
    /* Two sizes down. The button was made deliberately large because it changes what everyone else is
       watching -- but that was reasoning about a column beside the picture. Here it shares 240px with
       the FOV row and the animation list, and 92px of it left the list a sliver. Height to about 60%,
       width in a little, type a step down, and the glyph with them so the proportions hold rather than
       leaving a big disc in a short pill.
       nowrap on the label because the longest of them would otherwise break mid-phrase, and a button
       whose words rearrange as its state changes reads as broken. The status line keeps pre-line: its
       breaks are deliberate, and if one ever does outgrow the width, wrapping is a better failure than
       spilling out of a pill. */
    #ctlBtn { min-height: 55px; margin-inline: 10px; padding: 8px 14px; gap: 10px; font-size: 15px; }
    #ctlBtn .ctlGlyph { width: 30px; height: 30px; }
    #ctlBtn .ctlGlyph svg { width: 17px; height: 17px; }
    #ctlBtnLabel { white-space: nowrap; }
    #ctlBtnSub { font-size: 10.5px; }
    /* The animation catalog as a dropdown instead of a scrolling list of pills. The list needs room
       to be worth having -- it is there so every animation is visible and one press away -- and this
       panel is 220px tall with the turn button and the FOV row already in it, which left the list a
       couple of rows. A dropdown costs a press and gives all of it back. */
    #ctlAnimRow { display: flex; }
    #ctlAnim { display: none; }
    /* Two rows in a box sized for a scrolling list, so there is room over: 58px of it at 220px. On the
       wide layout the list soaks that up, here it was all pooling under the second dropdown and
       reading as the panel having lost something. Spread it evenly instead -- the gap over FOV, the
       one between the two, and the one under the animation row all come out the same, so none of the
       three is the leftover. gap goes to 0 because it would otherwise be added to the middle one only,
       which is the asymmetry this is removing. Self-adjusting: it stays even at whatever height the
       turn button's status line leaves the box. */
    #ctlBody { gap: 0; justify-content: space-evenly; }
    /* Both labels to one width so the two dropdowns start on the same line. Only needed here, where
       the rows are both on screen: "FOV" and "アニメ" are nowhere near the same width. */
    .ctlRow > label { min-width: 3.3em; }
    /* The picture takes the full width between the screen's own margins -- the same 14px the comment
       column and everything else on this layout uses. The base rule reserves a comment panel, the
       gap beside it AND the outer margin on each side; here nothing stands beside the picture, since
       both columns are at the bottom, so --gap was 12px of nothing on each side and the whole
       reservation ran to 52px of a 393px screen.
       This is the direction that is left. When the picture is height-bound -- any phone with browser
       chrome on screen -- the 20px off --ch above is what makes it bigger and this changes nothing,
       object-fit keeping the 3:4 whatever the box does. When the viewport is tall enough for the
       picture to become width-bound instead, height stops being worth anything to it and this is the
       only growth available. One or the other applies at every size, which is the point.
       (The square variant of this rule, reserving the shot's long side, is with the base rule.) */
    video { max-width: calc(100vw - 2 * var(--edge)); }

    #ctlToast { top: 56px; gap: 7px; padding: 8px 16px 8px 10px; font-size: 12.5px; }
    #ctlToast svg { width: 16px; height: 16px; padding: 4px; }
    #ctlToastSub { font-size: 10.5px; }
  }

  /* ===== THE PANELS, READ ON PAPER ==================================================
     Everything from here down turns over what was written to be read against a dark page.
     It is one block at the end rather than edits scattered through the rules above, so the
     dark set is still intact and legible where it was written -- and so this is one block
     to delete if the ground ever goes dark again.

     SCOPED TO THE PANELS. What sits over the PICTURE -- the room badge, the account pill,
     the ⓘ, the toasts, the turn button -- stays exactly as it was: what is behind those is
     a picture, not the wall, and it was already the right thing there. */
  :root.wall #info, :root.wall #ctrl, :root.wall #caster, :root.wall #comments { border-color: var(--line); }
  :root.wall .nameTagBody { border-color: var(--line); }
  /* Headings and labels. The rule under the heading was a near-white line on a dark card;
     on paper it is the shelf's own hairline. */
  :root.wall #info .infoLabel, :root.wall #caster .infoLabel,
  :root.wall #ctrl .ctlRow label, :root.wall #ctrl .ctlGroupName { color: var(--panel-ink-3); }
  :root.wall #info .infoValue, :root.wall #caster .infoValue { color: var(--panel-ink); }
  :root.wall #infoNone, :root.wall #chatEmpty { color: var(--panel-ink-3); }
  :root.wall #infoUrls a { color: #5b2f86; }
  /* The animation list: chips on paper. */
  :root.wall #ctlAnim .ctlItem { color: var(--panel-ink); background: rgba(0, 0, 0, .05);
    border-color: rgba(169, 143, 112, .42); }
  :root.wall #ctlAnim .ctlItem:hover:not(:disabled) { background: rgba(0, 0, 0, .09); }
  :root.wall #ctlAnim .ctlItem.on { color: #3d1e06; }
  /* The comment field and the names above the bubbles. The bubbles themselves were already
     light with dark text -- they are the one thing on this page that was never dark. */
  :root.wall #chatInput { color: var(--panel-ink); background: rgba(0, 0, 0, .05);
    border-color: rgba(169, 143, 112, .4); }
  :root.wall #chatInput::placeholder { color: var(--panel-ink-3); }
  /* Comment names are NOT touched: .cmt .who was already #5f6975, which is a dark grey that reads
     on the wall, and .cmt.tip .who is white on its own superchat card. Overriding them here broke
     the second to fix the first. */
  /* Ko-fi: the frame's slot and the "not linked yet" line, which was amber for a dark card
     and is too pale for paper. */
  :root.wall #kofiWrap { background: rgba(0, 0, 0, .04); border-color: rgba(169, 143, 112, .3); }
  :root.wall #kofiUnlinked { color: #8a6a1f; }
  :root.wall #kofiEmpty { color: var(--panel-ink-3); }
  /* Its ⓘ, which was a dark glass disc on a dark card. */
  :root.wall #kofiHintBtn { color: var(--panel-ink-2); background: none; border-color: var(--panel-ink-3); }
  :root.wall #kofiHint.open #kofiHintBtn { color: #fff; background: var(--panel-ink); border-color: var(--panel-ink); }

  /* ---- Wide layout only: let the video through ----------------------------------------------
     A tenth off the opacity of everything the comment column and the turn button are painted
     with. On a phone these sit over the picture at the bottom of a small screen and need their own
     contrast; on a desktop there is a whole shot behind them and holding it back was costing more
     than it bought. The text is untouched -- it is what has to stay readable, and it now has a
     slightly lighter surface to be read against, not a thinner colour of its own. */
  @media (min-width: 901px) {
    .cmt .bubble { background: rgba(249, 250, 252, .84); border-color: rgba(255, 255, 255, .45); }
    .cmt.mine .bubble { background: rgba(222, 235, 250, .85); border-color: rgba(255, 255, 255, .54); }
    .cmt.tip .bubble { background: rgba(208, 0, 0, .70); border-color: rgba(255, 255, 255, .32); }
    /* The turn button, every state. The glyph's disc goes with it: it is a lighter wash of the same
       surface, so holding it back alone would have made a solid ring on a see-through button. */
    #ctlBtn { background: rgba(247, 166, 58, .90); }
    #ctlBtn:hover:not(:disabled) { background: rgba(255, 180, 87, .90); }
    #ctlBtn:active:not(:disabled) { background: rgba(232, 147, 37, .90); }
    #ctlBtn.holding { background: rgba(239, 106, 61, .90); }
    #ctlBtn.holding:hover:not(:disabled) { background: rgba(244, 122, 79, .90); }
    #ctlBtn.queued { background: rgba(255, 255, 255, .09); }
    #ctlBtn:disabled:not(.pending) { background: rgba(255, 255, 255, .072); }
  }
