/* Punchface component layer for the soft-glass design system
   (docs/ui-redesign.md). tokens.css supplies the design tokens,
   themes.css maps the per-user hue, fonts.css the typefaces. Pico CSS
   is gone: this file owns every element and component the templates
   use, including the 44px tap targets (ARCHITECTURE.md section 10.1),
   the admin table stacking (section 10.1), the chat pane, the player,
   and the reduced-motion escape hatch. */

/* -- reset -- */
*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    margin: 0;
    font-size: 15px;
    line-height: 1.5;
}

img,
video {
    max-width: 100%;
}

/* -- type (scale: 12 / 13 / 14 / 15 / 17 / 24 / 32) -- */
h1,
h2,
h3 {
    margin: 0 0 1rem;
    font-family: var(--font-display);
    font-weight: 700;
    line-height: 1.2;
    color: var(--text);
}

h1 {
    font-size: 32px;
}

h2 {
    font-size: 24px;
    margin-bottom: 0.75rem;
}

h3 {
    font-size: 17px;
    margin-bottom: 0.5rem;
}

p {
    margin: 0 0 1rem;
}

small {
    font-size: 12px;
    color: var(--text-faint);
}

strong {
    font-weight: 600;
}

a {
    color: var(--accent);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
    text-underline-offset: 3px;
}

ul,
ol {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* -- layout -- */
.container {
    width: 100%;
    max-width: 1200px;
    margin-inline: auto;
    padding-inline: 1rem;
}

.container-fluid {
    width: 100%;
    padding-inline: 1rem;
}

/* Frosted panel: nav bar, chat, cards, tabs, overlays. Never placed
   over the video itself (docs/ui-redesign.md, "Components"). */
.glass {
    background: var(--glass);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-lg);
}

/* Where backdrop-filter is unsupported, a more opaque panel color. */
@supports not (backdrop-filter: blur(1px)) {
    .glass {
        background: var(--glass-strong);
    }
}

/* -- nav bar -- */
nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.25rem 0.5rem;
    padding: 0.5rem 0.75rem;
}

nav ul {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem;
}

nav li {
    display: flex;
    align-items: center;
}

/* Nav links compute to a 44px tap target with the text vertically
   centered. */
nav li a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    padding: 0.5rem 0.9rem;
    border-radius: var(--r-md);
    color: var(--text);
    font-weight: 500;
}

nav li a:hover {
    background: var(--glass);
    text-decoration: none;
}

/* The logo is "punch" in --text plus "face" in the accent, Bricolage
   800, 17px. */
.logo {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 17px;
    letter-spacing: -0.01em;
    color: var(--text);
}

.logo span {
    color: var(--accent);
}

/* -- buttons -- */
/* The :not() keeps this base look off the Shaka player's own controls
   (its buttons and range sliders, which css/shaka-controls.css styles
   itself). It adds a class's worth of specificity (0,1,0), the same as
   the class rules (.btn-ghost, .switch, .chat-presence, ...), so those
   win by coming later in this file: keep component rules below the
   base form rules. */
:where(button, [role="button"]):not(.shaka-video-container *),
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    margin: 0;
    padding: 0.5rem 1.25rem;
    font: inherit;
    font-weight: 600;
    line-height: 1.2;
    color: var(--on-accent);
    background: var(--accent);
    border: 1px solid transparent;
    border-radius: var(--r-pill);
    cursor: pointer;
    text-decoration: none;
}

:where(button, [role="button"]):not(.shaka-video-container *):hover,
.btn:hover {
    filter: brightness(1.08);
    text-decoration: none;
}

/* Ghost: a glass fill. Outline: no fill at all. */
:where(button, [role="button"]).secondary:not(.shaka-video-container *),
.btn-ghost {
    background: var(--glass);
    color: var(--text);
    border-color: var(--glass-edge);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
}

:where(button, [role="button"]).secondary.outline:not(.shaka-video-container *) {
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}

/* Danger: a translucent red tint. */
:where(button, [role="button"]).danger:not(.shaka-video-container *),
.btn-danger {
    color: var(--danger);
    background: color-mix(in srgb, var(--danger) 18%, transparent);
    border-color: color-mix(in srgb, var(--danger) 45%, transparent);
}

/* Every tappable control must be at least 44px tall
   (ARCHITECTURE.md section 10.1). The hidden swatch radios are
   excluded: their label is the tap target. */
:where(button, [role="button"]):not(.shaka-video-container *),
:where(input:not([type="radio"]), select, textarea):not(.shaka-video-container *),
.btn {
    min-height: 44px;
}

/* -- form controls -- */
:where(input:not([type="radio"]), select, textarea):not(.shaka-video-container *) {
    margin: 0;
    padding: 0.625rem 0.875rem;
    font: inherit;
    color: var(--text);
    background: var(--input-bg);
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-md);
}

::placeholder {
    color: var(--text-faint);
    opacity: 1;
}

label {
    display: block;
    margin-bottom: 1rem;
    font-size: 14px;
    color: var(--text-muted);
}

label > input:not([type="checkbox"]):not([type="radio"]),
label > select,
label > textarea {
    display: block;
    width: 100%;
    margin-top: 0.375rem;
}

:where(input)[type="checkbox"]:not(.shaka-video-container *) {
    width: 1.25rem;
    height: 1.25rem;
    min-height: 0;
    margin: 0;
    accent-color: var(--accent);
}

/* Field hint: a muted 13px line on its own row under the field's
   checkbox/label row (the feature pickers on the admin pages). */
.field-hint {
    display: block;
    margin-top: 4px;
    font-size: 13px;
    color: var(--text-muted);
}

/* Focus: an accent border plus a 3px accent-soft ring. */
a:focus-visible,
:where(button, [role="button"]):not(.shaka-video-container *):focus-visible,
.btn:focus-visible,
:where(input, select, textarea):not(.shaka-video-container *):focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

/* -- alerts and flash -- */
.alert,
article[role="alert"] {
    margin-bottom: 1rem;
    padding: 0.75rem 1rem;
    border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
    border-radius: var(--r-md);
    background: color-mix(in srgb, var(--danger) 14%, transparent);
    color: var(--text);
}

article[role="status"] {
    margin-bottom: 1rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-md);
    background: var(--glass-strong);
}

/* -- status pill: a dot plus a label -- */
.pill {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.3rem 0.75rem;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-pill);
    background: var(--glass);
    color: var(--text-muted);
    font-size: 13px;
}

.pill::before {
    content: "";
    flex: none;
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: var(--text-faint);
}

.pill-pending::before {
    background: var(--warn);
}

.pill-used::before {
    background: var(--ok);
}

.pill-expired::before,
.pill-revoked::before {
    background: var(--text-muted);
}

/* -- tabs: a glass track, active tab on a lighter fill. The track
   scrolls sideways on narrow screens, with no visible scrollbar. -- */
.tabs {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    gap: 0.25rem;
    padding: 0.25rem;
    overflow-x: auto;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-pill);
    background: var(--glass);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    scrollbar-width: none;
}

.tabs::-webkit-scrollbar {
    display: none;
}

.tabs a,
.tabs button,
.tabs [role="tab"] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0.5rem 1rem;
    border-radius: var(--r-pill);
    color: var(--text-muted);
    font-weight: 500;
}

.tabs a:hover,
.tabs button:hover {
    color: var(--text);
    text-decoration: none;
}

.tabs .active,
.tabs a[aria-current],
.tabs [role="tab"][aria-selected="true"] {
    background: rgba(255, 255, 255, 0.12);
    color: var(--text);
}

/* -- avatar: a 34px circle with the user's initial -- */
.avatar {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 14px;
    font-weight: 700;
    text-transform: uppercase;
}

/* -- dialogs (confirm, chat moderation): a glass card -- */
dialog {
    padding: 1rem 1.25rem;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-lg);
    background: var(--glass-strong);
    color: var(--text);
    max-width: min(28rem, calc(100vw - 2rem));
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
}

dialog::backdrop {
    background: rgba(0, 0, 0, 0.55);
}

/* -- toasts: top right on desktop, top center on phones -- */
.toast {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 50;
    max-width: min(22rem, calc(100vw - 2rem));
    padding: 0.75rem 1rem;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-md);
    background: var(--glass-strong);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
}

@media (max-width: 599px) {
    .toast {
        right: 50%;
        transform: translateX(50%);
    }
}

/* -- tables -- */
table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
}

table th,
table td {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--glass-edge);
    text-align: left;
    vertical-align: middle;
}

table thead th {
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.overflow-auto {
    overflow-x: auto;
}

.status {
    color: var(--text-muted);
    font-size: 13px;
}

/* -- compact forms and row actions -- */
.nav-logout {
    margin: 0;
}

.nav-logout button,
.row-form button {
    width: auto;
}

.row-form {
    margin: 0;
}

/* Row actions (users table) wrap instead of overflowing, and keep each
   button and link compact. */
.row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.row-actions [role="button"],
.row-actions button {
    width: auto;
    margin: 0;
}

/* The users table's row action menu is a native popover: the table's
   overflow-auto wrapper clips the shared absolute .dropdown, and the
   panel's backdrop-filter would trap a fixed one too, so the menu
   renders in the top layer and place() in app.js pins it to its opener
   (it closes on scroll or resize, since the position is computed once,
   at open). The browser's centred-popover defaults go; .dropdown still
   supplies the width, padding, border and glass background. */
.row-menu[popover] {
    position: fixed;
    inset: auto;
    margin: 0;
    color: inherit;
}

/* .dropdown's display: flex would keep a closed popover visible; hide
   it until its opener opens it. */
.row-menu:not(:popover-open) {
    display: none;
}

/* Confirmation pages: the confirm and cancel controls side by side. */
.confirm-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: center;
}

.confirm-form button,
.confirm-form [role="button"] {
    width: auto;
    margin: 0;
}

/* -- player (watch page and admin test player): a 16:9 black stage
   that fills its container up to 1080p (1920x1080 CSS pixels), with the
   video stretched to fill it; the Shaka UI overlays its own controls on
   top, so the video carries none of its own. -- */
.player {
    width: 100%;
    max-width: 1920px;
    aspect-ratio: 16 / 9;
    background: #000;
}

.player video {
    width: 100%;
    height: 100%;
}

/* The refresh button over the player while there is no viewing session:
   watch.js retries on its own, and a press retries at once. */
.player-wrap {
    position: relative;
}

.player-retry {
    position: absolute;
    inset: 0;
    z-index: 2;
    width: 72px;
    height: 72px;
    margin: auto;
    padding: 16px;
    border: 0;
    border-radius: 50%;
    background: rgb(0 0 0 / 55%);
    color: #fff;
    cursor: pointer;
}

.player-retry svg {
    display: block;
    width: 100%;
    height: 100%;
    fill: currentColor;
}

.player-retry:hover,
.player-retry:focus-visible {
    background: rgb(0 0 0 / 75%);
}

/* Document Picture-in-Picture: Shaka moves the player into the PiP
   window and adds pip-mode, which (in shaka-controls.css) sizes it to
   the window. player_ui.js links this file into that window, so these
   rules apply there: the 16:9 cap goes, the video letterboxes inside
   the window, and the controls that make no sense in a small floating
   window are hidden. */
.player.pip-mode {
    max-width: none;
    aspect-ratio: auto;
}

.player.pip-mode video {
    object-fit: contain;
}

.pip-mode .shaka-fullscreen-button,
.pip-mode .shaka-vlc-link-button,
.pip-mode .shaka-current-time {
    display: none;
}

/* Touch screens always show the volume slider (player_ui.js), which
   needs about 60px more of the control bar. On phone widths the time
   display gives way so the bar still fits play, volume, PiP, VLC and
   fullscreen; the seek bar still shows where playback is. */
@media (hover: none) and (max-width: 474px) {
    .player .shaka-current-time {
        display: none;
    }
}

/* Shaka's controls default to Roboto; use the page font instead. */
.shaka-video-container {
    --shaka-font-family: var(--font-body);
}

/* -- chat pane (UI redesign step 5b, docs/ui-redesign.md "Watch" >
   "Chat"). The page uses the full-width container so the chat column
   fits; the pane is closed until chat.js opens it. -- */

/* The Reconnect button is a flex item in the chat body column; keep
   it from stretching across it. */
#chat-retry {
    align-self: flex-start;
}

/* The pane fills its glass column (.watch-chat owns the glass edge);
   chat.js keeps chat-open on the layout while it is open. No transitions
   are added, so the reduced-motion rule below already covers this
   section. */
.chat {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}

.chat-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.5rem;
    border-bottom: 1px solid var(--glass-edge);
}

.chat-header-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* The admin menu (admins only; chat.js unhides it on hello): a 44px
   hamburger that opens the shared glass dropdown, right-aligned under
   the button. */
.chat-admin-menu {
    position: relative;
}

.chat-admin-menu .dropdown {
    width: auto;
    min-width: 180px;
    gap: 0.25rem;
    padding: 0.5rem;
}

/* The two admin actions inside it: full-width menu items. */
.chat-admin-menu .dropdown .btn-ghost {
    width: 100%;
    padding-inline: 0.75rem;
    text-align: left;
}

/* The hamburger and the collapse arrow: square 44px icon buttons. */
.chat-admin-toggle,
.chat-collapse {
    width: 44px;
    min-width: 44px;
    height: 44px;
    padding: 0;
}

/* The left of the pane header: the collapse arrow, then the presence
   pill. */
.chat-header-start {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* The arrows point at where chat goes: sideways beside the player on
   desktop, down/up under it on phones (the narrow rule in the watch
   section swaps them). */
.chat-icon {
    flex: none;
}

.chat-icon-narrow {
    display: none;
}

/* The Chat button under the player (shown while chat is collapsed):
   chevrons and a label in a 44px glass pill, then an accent dot when
   messages arrived while chat was collapsed. */
.chat-expand {
    min-height: 44px;
    padding-inline: 12px 16px;
}

.chat-unread {
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--accent);
}

.chat-presence-wrap {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* The presence button: a small glass pill, "N here" plus a caret that
   opens the roster. */
.chat-presence {
    padding: 0.25rem 0.75rem;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-pill);
    background: var(--glass);
    color: var(--text);
    font-size: 13px;
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
}

.chat-presence::after {
    content: "\25BE";
    margin-left: 0.35rem;
    font-size: 10px;
    color: var(--text-muted);
}

/* The roster: a glass-strong dropdown under the pill, one dot per
   person in their color (themes.css). */
.chat-userlist {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 10;
    min-width: 12rem;
    max-height: 16rem;
    overflow-y: auto;
    margin: 0.25rem 0 0;
    padding: 0.5rem 0.75rem;
    list-style: none;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-md);
    background: var(--glass-strong);
    box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
}

.chat-userlist li {
    list-style: none;
    padding: 0.15rem 0;
}

.chat-body {
    position: relative;
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-height: 0;
    padding: 0.5rem;
    gap: 0.5rem;
}

/* The log: a scrolling column of bubbles, 8px apart. */
.chat-lines {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    gap: 8px;
    overflow-y: auto;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* A message line holds one bubble; own messages sit on the right. */
.chat-line {
    display: flex;
}

.chat-line .bubble {
    max-width: 85%;
    padding: 8px 12px;
    border-radius: var(--r-lg) var(--r-lg) var(--r-lg) 6px;
    background: rgba(255, 255, 255, 0.07);
    overflow-wrap: anywhere;
}

/* The name in the person's color (themes.css), bold, over the text. */
.chat-name {
    font-size: 13px;
    font-weight: 700;
}

.chat-text {
    display: block;
}

.chat-line.mine {
    justify-content: flex-end;
}

.chat-line.mine .bubble {
    border-radius: var(--r-lg) var(--r-lg) 6px var(--r-lg);
    background: var(--accent-soft);
}

/* System lines (join/leave, cleared, removed): centered and faint. */
.chat-system {
    padding: 0 0.5rem;
    color: var(--text-faint);
    font-size: 12px;
    text-align: center;
}

/* The admin badge: a tiny pill in the accent tint. */
.chat-badge {
    display: inline-block;
    margin-right: 0.35rem;
    padding: 0.1rem 0.5rem;
    border-radius: var(--r-pill);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
}

/* An admin can pick a line apart: hover and focus light the bubble. */
.chat-line[tabindex] {
    cursor: pointer;
}

.chat-line[tabindex]:hover .bubble,
.chat-line[tabindex]:focus .bubble {
    background: var(--accent-soft);
}

.chat-jump {
    position: absolute;
    left: 50%;
    bottom: 5.5rem;
    transform: translateX(-50%);
}

.chat-status {
    margin: 0;
    min-height: 1.2rem;
    color: var(--text-muted);
    font-size: 0.9rem;
}

/* The compose row: a pill input and the Send button, pinned to the
   foot of the pane. The pick-a-name form reuses the row for Join. */
.chat-form {
    display: flex;
    gap: 0.5rem;
    margin: 0;
}

.chat-form input {
    flex: 1 1 auto;
    margin: 0;
    border-radius: var(--r-pill);
}

.chat-counter {
    color: var(--text-muted);
    text-align: right;
}

.chat-handle-form {
    margin: 0;
}

.chat-handle-form label {
    margin-bottom: 0.5rem;
    font-size: 0.875rem;
}

/* The moderation dialog shares the glass card of every dialog; only
   the action row needs its own layout. */
.chat-dialog-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* Reduced motion: no transitions or animations anywhere. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        transition: none !important;
        animation: none !important;
    }
}

/* Chat color swatches (account page) and roster dots. */
.swatch,
.roster-dot {
  display: inline-block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  vertical-align: middle;
}
.roster-dot {
  width: 8px;
  height: 8px;
  margin-right: 6px;
}

/* -- nav bar (UI redesign step 3): a sticky glass bar. Desktop
   (>= 900px): inline links plus an account pill that opens a
   dropdown. Phone (< 900px): a "☰ punchface" button opening a
   frosted panel that drops down from the bar over a dim scrim.
   Every control is at least 44px (ARCHITECTURE.md section 10.1);
   the panel's slide-down, the scrim fade and the toast fade are the
   only animations here, and the reduced-motion rule above kills
   them. */

/* The x-bind:hidden panels set author display values, so the hidden
   attribute needs its own author rule to win. */
[hidden] {
    display: none !important;
}

/* The sticky position lives on the header, which spans the bar
   snugly: a sticky element cannot travel beyond its parent's box. */
header {
    position: sticky;
    top: 0;
    z-index: 40;
    padding-block: 0.75rem;
}

.nav-bar {
    /* The frosted panel is the ::before, not the element: a
       backdrop-filter on the element would make it the containing block
       for the fixed .phone-menu and clip the menu to the height of the bar. */
    position: relative;
    isolation: isolate;
    flex-wrap: nowrap;
    justify-content: flex-start;
}

/* Drop the shared .glass off the element (see above) so the bar never
   traps the fixed .phone-menu; the glass is re-applied on the ::before. */
.glass.nav-bar {
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border: none;
}

/* The glass look, moved onto the pseudo-element. The negative z-index
   sits behind the bar's content but, with isolation on .nav-bar, never
   behind the page. */
.nav-bar::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: var(--glass);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    border: 1px solid var(--glass-edge);
}

@supports not (backdrop-filter: blur(1px)) {
    .nav-bar::before {
        background: var(--glass-strong);
    }
}

.nav-bar .logo-desktop {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
}

/* The inline links (Watch, Admin / Log in). */
.nav-links {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.nav-links a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0.5rem 0.9rem;
    border-radius: var(--r-md);
    color: var(--text);
    font-weight: 500;
}

.nav-links a:hover {
    background: var(--glass);
    text-decoration: none;
}

/* The "Report bug" control in the top bar: a button dressed as one of
   the inline links — unfilled, no border or backdrop, same type and
   hover. The base button rules above keep the 44px tap target. */
.nav-link-button {
    padding: 0.5rem 0.9rem;
    border-radius: var(--r-md);
    background: transparent;
    border-color: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    color: var(--text);
    font-weight: 500;
}

.nav-link-button:hover {
    background: var(--glass);
    filter: none;
    text-decoration: none;
}

/* The account pill and its dropdown, pushed to the right of the bar. */
.nav-desktop {
    position: relative;
    display: flex;
    align-items: center;
    margin-left: auto;
}

.account-pill {
    gap: 0.5rem;
    padding-inline: 1rem;
}

.account-pill .caret {
    font-size: 12px;
    color: var(--text-muted);
}

.dropdown {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    z-index: 50;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    width: 15rem;
    padding: 1rem;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-lg);
    background:
        radial-gradient(120% 90% at 100% 0%, color-mix(in srgb, var(--bg-tint) 28%, transparent), transparent 70%),
        var(--glass-strong);
    box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
}

/* The account block: the name in the user's chat color, an admin tag. */
.account-name {
    margin: 0;
    font-size: 17px;
    font-weight: 600;
}

.admin-tag {
    margin-left: 0.5rem;
    padding: 0.125rem 0.5rem;
    border-radius: var(--r-pill);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 12px;
    font-weight: 600;
}

/* The links and logout form inside the dropdown and the phone menu.
   The phone menu's first group is a <nav>, so the base nav padding
   goes back to zero here; the rows stretch to the full width so the
   hover bar spans the whole row, the label still left-aligned. */
.menu-links {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0;
    align-items: stretch;
}

.menu-links a {
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: flex-start;
    text-align: left;
    min-height: 44px;
    padding: 0.5rem 0.75rem;
    border-radius: var(--r-md);
    color: var(--text);
    font-weight: 500;
}

.menu-links a:hover {
    background: var(--glass);
    text-decoration: none;
}

/* The phone menu's "Report bug" opener: full-width and left-aligned
   like the links around it. */
.menu-links .nav-link-button {
    display: flex;
    width: 100%;
    justify-content: flex-start;
    text-align: left;
    padding: 0.5rem 0.75rem;
}

.menu-links form {
    margin: 0;
    width: 100%;
}

.menu-links form button {
    display: flex;
    width: 100%;
    justify-content: flex-start;
    text-align: left;
}

/* Destructive dropdown items keep the danger text; .menu-links a would
   win the color by specificity, so the pair is stated together. */
.menu-links a.btn-danger {
    color: var(--danger);
}

/* The "You" card in the phone menu: avatar, name (or a link to pick
   one) and the color picker, on a glass panel. */
.menu-you {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 14px;
    border-radius: var(--r-lg);
    background: var(--glass);
    border: 1px solid var(--glass-edge);
}

.menu-label {
    margin: 0;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.menu-you-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    font-size: 17px;
    font-weight: 600;
}

/* The menu avatar is a touch bigger than the 34px default. */
.menu-you-row .avatar {
    width: 40px;
    height: 40px;
    font-size: 16px;
}

/* The shared color picker: 34px swatches in 44px tap targets. The
   accent ring follows the (visually hidden) radio's checked and
   focus-visible states. */
.color-form {
    margin: 0;
}

.swatch-fieldset {
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
}

.swatch-fieldset legend {
    margin-bottom: 0.375rem;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-muted);
}

.swatch-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
}

/* Belt and braces for the phone menu: the flex rows wrap and their
   children can shrink, so a wide row can never push the menu past
   the viewport. */
.menu-you-row > *,
.swatch-row > * {
    min-width: 0;
}

.swatch-row label {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    margin: 0;
    cursor: pointer;
}

.swatch-row .swatch {
    width: 34px;
    height: 34px;
}

.swatch-row input:checked + .swatch,
.swatch-row input:focus-visible + .swatch {
    outline: 3px solid var(--accent);
    outline-offset: 3px;
}

/* The phone controls: the "☰ punchface" button opens the full-screen
   menu. */
.nav-phone {
    display: flex;
    flex: 1;
    align-items: center;
    justify-content: flex-start;
    gap: 0.5rem;
}

/* The dot-cN tint (themes.css) must beat the default accent-soft fill,
   hence the paired selectors. */
.avatar.dot-c1 {
    background: var(--c1);
    color: var(--on-accent);
}
.avatar.dot-c2 {
    background: var(--c2);
    color: var(--on-accent);
}
.avatar.dot-c3 {
    background: var(--c3);
    color: var(--on-accent);
}
.avatar.dot-c4 {
    background: var(--c4);
    color: var(--on-accent);
}
.avatar.dot-c5 {
    background: var(--c5);
    color: var(--on-accent);
}
.avatar.dot-c6 {
    background: var(--c6);
    color: var(--on-accent);
}
.avatar.dot-c7 {
    background: var(--c7);
    color: var(--on-accent);
}
.avatar.dot-c8 {
    background: var(--c8);
    color: var(--on-accent);
}

/* The dim scrim behind the dropped-down phone menu: the rest of the
   page, dimmed. A tap on it closes the menu via its own click handler
   (it sits inside the .nav-phone scope, so click.outside never fires
   for it). */
.menu-scrim {
    position: fixed;
    inset: 0;
    z-index: 999;
    background: rgba(0, 0, 0, 0.55);
    animation: menu-scrim-in 150ms ease;
}

@keyframes menu-scrim-in {
    from {
        opacity: 0;
    }
}

/* The frosted panel dropping down from the topbar: as tall as its
   content, capped at the viewport, sliding down from the top. It must
   always fit the screen width: border-box, capped at the viewport,
   and no horizontal scroll. */
.phone-menu {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    box-sizing: border-box;
    width: 100%;
    max-width: 100vw;
    max-height: 100dvh;
    z-index: 1000;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1.5rem;
    padding-top: max(1.5rem, env(safe-area-inset-top));
    overflow-x: hidden;
    overflow-y: auto;
    border: 1px solid var(--glass-edge);
    border-radius: 0 0 var(--r-xl) var(--r-xl);
    background:
        radial-gradient(120% 90% at 100% 0%, color-mix(in srgb, var(--bg-tint) 28%, transparent), transparent 70%),
        var(--glass-strong);
    box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    animation: phone-menu-in 200ms ease;
}

@keyframes phone-menu-in {
    from {
        transform: translateY(-100%);
    }
}

.phone-menu-close {
    position: absolute;
    top: 0.75rem;
    right: 0.75rem;
    width: 44px;
    height: 44px;
    padding: 0;
    border-radius: var(--r-pill);
    background: var(--glass);
    border-color: var(--glass-edge);
    color: var(--text);
}

/* While the phone menu is open, stop the page behind it from
   scrolling. Toggled from the menu component (js/app.js) on <body>. */
body.menu-open {
    overflow: hidden;
}

/* Toasts stack in the #toasts container: top right on desktop,
   top center on phones, fading out before the 3s removal. */
#toasts {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    pointer-events: none;
}

/* The base .toast rule floats each toast at the viewport corner; in
   the container they stack, and the standalone phone re-centering and
   any transform are neutralized. */
#toasts .toast {
    position: static;
    transform: none;
    animation: toast-fade 3s ease forwards;
}

@keyframes toast-fade {
    0%,
    80% {
        opacity: 1;
    }
    100% {
        opacity: 0;
    }
}

/* The desktop/phone switch: below 900px the inline links, the pill
   and the dropdown give way to the "☰ punchface" button. */
@media (max-width: 899px) {
    .nav-bar .logo-desktop,
    .nav-links,
    .nav-desktop {
        display: none;
    }

    #toasts {
        right: 50%;
        transform: translateX(50%);
    }
}

@media (min-width: 900px) {
    .nav-phone {
        display: none;
    }
}

/* Screen-reader-only text (the swatch labels). !important on every
   declaration so no later input rule can resize a hidden element
   (the swatch radios lost to the label > input rule's width: 100%). */
.visually-hidden {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    margin: -1px !important;
    padding: 0 !important;
    overflow: hidden !important;
    clip: rect(0 0 0 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
}

/* -- auth (UI redesign step 4): the signed-out pages (login, register,
   reset) and the change-password page render as one glass card centered
   on the gradient backdrop (docs/ui-redesign.md, "Login, register,
   change password"). The signed-in pages keep the nav; the signed-out
   ones drop it (base.html's chrome block). */

/* The card centers in a full-viewport main, with a 16px side gutter. */
body.auth-page main {
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding-inline: 1rem;
}

.auth-card {
    width: 100%;
    max-width: 360px;
    padding: 28px;
    border-radius: var(--r-xl);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}

/* The logo: centered above the heading, Bricolage 800 at 24px. */
.auth-card .logo {
    display: block;
    margin-bottom: 1rem;
    font-size: 24px;
    text-align: center;
}

/* The heading: a step down from the page h1. */
.auth-card h1 {
    margin-bottom: 1rem;
    font-size: 24px;
    text-align: center;
}

/* Fields: the base form rules already make the label's input full
   width; here the fields sit 14px apart. */
.auth-card label {
    margin-bottom: 14px;
}

.btn-block {
    width: 100%;
}

/* Errors inside the card: a translucent red with a --danger border. */
.auth-card .alert {
    border-color: var(--danger);
    background: color-mix(in srgb, var(--danger) 18%, transparent);
}

/* Secondary links (for example "Back to sign in"): a small muted row
   at the foot of the card. */
.auth-footer {
    margin-top: 1rem;
    font-size: 13px;
    color: var(--text-muted);
}

.auth-footer a {
    color: var(--text-muted);
}

/* -- watch (UI redesign step 5a + 5b): the player beside the chat
   column (docs/ui-redesign.md, "Watch"). Desktop (>= 900px): one
   column for the player; while the chat pane is open the grid gains a
   290px chat column. The player fills its column's width, capped so the
   bar under it always stays in the viewport. The player has 18px
   corners and a soft accent glow; a bar under it holds the glass Live
   pill and, while chat is collapsed, the Chat button, so nothing
   overlays the video. The open chat column has the player's top and
   height. Phone: one column, the player
   full width with square corners, the open chat filling the rest of
   the viewport. The grid follows the pane's state: chat.js keeps
   chat-open on the layout while it is open. No transitions are added,
   so the reduced-motion rule above already covers this section. -- */

/* The sticky header is the 60px glass nav bar (44px of content plus
   8px of padding-block on each side) plus 12px of padding-block on each
   side, 84px in all; the page fills exactly what is left of the
   viewport. The stage bar is a 44px row plus 10px of padding-block on
   each side. */
.watch-layout {
    --watch-viewport: calc(100dvh - 84px);
    --stage-bar-height: 64px;
}

/* The row under the player: the Live pill on the left and, while chat
   is collapsed, the Chat button on the right. Nothing overlays the
   video, so Shaka's controls stay clear on every size. */
.stage-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    min-height: var(--stage-bar-height);
    box-sizing: border-box;
    padding-block: 10px;
}

/* The glass Live pill in the bar. */
.stage-meta {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 4px 14px;
    border-radius: var(--r-pill);
}

/* The Live badge: a --danger dot plus 12px bold uppercase text. */
.live-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--danger);
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

.live-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
}

/* The glass column that owns the pane: it joins the grid only while
   the pane is open (chat.js keeps chat-open on the layout). */
.watch-chat {
    display: none;
    min-height: 0;
}

.watch-layout.chat-open .watch-chat {
    display: block;
}

/* The message line only takes space while it has text. */
.watch-layout #player-message:empty {
    display: none;
}

@media (min-width: 900px) {
    /* One column for the player; the open chat joins as a 290px
       column. */
    .watch-layout {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        gap: 16px;
        container-type: inline-size;
    }

    .watch-layout.chat-open {
        grid-template-columns: minmax(0, 1fr) 290px;
    }

    .watch-stage {
        min-width: 0;
    }

    /* 18px corners and a soft accent glow on the player. */
    .player-wrap {
        border-radius: var(--r-lg);
        overflow: hidden;
        box-shadow: 0 0 48px color-mix(in srgb, var(--accent) 25%, transparent);
    }

    /* The chat column lines up with the player: same top, same
       height. The player is 16:9 at the width of its column (the
       layout's width, 100cqw, minus the 290px chat column and the 16px
       gap), capped by the stage's max-width below and the player's
       1920px max-width, so its height is the smallest of the three. */
    .watch-chat {
        align-self: start;
        height: min(
            (100cqw - 306px) * 9 / 16,
            var(--watch-viewport) - var(--stage-bar-height),
            1080px
        );
    }

    /* The player takes the full width of its column, but never so much
       that its 16:9 height pushes the stage bar (and the Chat button)
       below the viewport: on a short window it narrows and centers. The
       browser's fullscreen element ignores these rules, so fullscreen
       is unaffected. */
    .watch-stage {
        width: 100%;
        max-width: calc((var(--watch-viewport) - var(--stage-bar-height)) * 16 / 9);
        margin-inline: auto;
    }
}

/* Phone: one column, the player at full width with square corners and
   no shadow, then the meta pill; the open chat takes the rest of the
   viewport, scrolling inside, with the compose row pinned to the
   bottom. */
@media (max-width: 899px) {
    .watch-layout {
        display: grid;
        grid-template-rows: auto;
        gap: 12px;
    }

    .watch-layout.chat-open {
        grid-template-rows: auto minmax(0, 1fr);
        height: var(--watch-viewport);
    }

    /* The compose row sticks to the bottom of the viewport. */
    .chat-form {
        position: sticky;
        bottom: 0;
    }

    /* The video is edge to edge, so the bar keeps clear of it. */
    .stage-bar {
        padding-inline: 12px;
    }

    /* Chat sits under the player here: the arrows point down and up. */
    .chat-icon-wide {
        display: none;
    }

    .chat-icon-narrow {
        display: block;
    }
}

/* Landscape phones: the player fills the viewport, the chat and the
   stage bar stay out of the way. */
@media (max-width: 899px) and (orientation: landscape) {
    .watch-layout.chat-open .watch-chat,
    .stage-bar {
        display: none;
    }

    .watch-layout .player {
        aspect-ratio: auto;
        height: var(--watch-viewport);
    }

    .watch-layout .player video {
        object-fit: contain;
    }
}

/* The off-screen textarea watch.js copies the VLC link from when the
   clipboard API is missing (a plain-HTTP LAN): a class, never an
   inline style, so the page's CSP holds. */
.copy-buffer {
    position: fixed;
    top: 0;
    left: -9999px;
    opacity: 0;
}

/* The inline VLC link box under the stage bar, shown when the copy
   failed on an origin without a clipboard API: a glass row with the
   URL in a readonly input, a Copy button (a fresh gesture) and the
   close ✕. The input flexes to fill the row. */
.vlc-copy {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px;
    border: 1px solid var(--glass-edge);
    border-radius: var(--r-md);
    background: var(--glass);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
}

.vlc-copy-url {
    flex: 1;
    min-width: 0;
}

/* -- admin (UI redesign step 6a, docs/ui-redesign.md "Admin"): the tab
   strip is the .tabs rule above; these cover the panels, the invites
   split, monospaced dates and the stacked phone cards. -- */

/* Room between the admin tab strip and the page heading under it. */
nav.tabs {
    margin-bottom: 1.5rem;
}

.panel {
    padding: 20px;
    border-radius: var(--r-lg);
}

/* Invites: the create form panel beside the table panel on desktop,
   one column below 900px. */
.admin-split {
    display: grid;
    gap: 16px;
}

/* Feature picker (invite create form, user features form): a bare
   fieldset; the legend reads like a section label, like the swatch
   fieldset's. */
.features-fieldset {
    margin: 0 0 1rem;
    padding: 0;
    border: 0;
}

.features-fieldset legend {
    margin-bottom: 0.375rem;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-muted);
}

/* One feature: the checkbox in its own column, centered against the
   title and the smaller description stacked beside it. */
.features-fieldset .feature-option {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    column-gap: 0.75rem;
}

.features-fieldset .feature-option input[type="checkbox"] {
    margin: 0;
}

.features-fieldset .feature-option .field-hint {
    margin-top: 2px;
    font-size: 12px;
}

@media (min-width: 900px) {
    .admin-split {
        grid-template-columns: 300px 1fr;
    }
}

/* Monospaced date cells. */
.mono {
    font-family: var(--font-mono);
    font-size: 12px;
}

/* Invite created: the link input and the Copy button share a row; the
   button wraps below the input on phones. */
.invite-link-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
}

.invite-link-row label {
    flex: 1 1 16rem;
    margin-bottom: 0;
}

/* Phones: each table row becomes a glass card, its cells labeled by a
   small muted caption from the data-label attribute; the header row is
   hidden for sighted users (ARCHITECTURE.md section 10.1). */
@media (max-width: 699px) {
    table.stack thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    table.stack tbody {
        display: flex;
        flex-direction: column;
        gap: 10px;
    }

    table.stack tr {
        display: flex;
        flex-direction: column;
        gap: 10px;
        padding: 12px;
        border: 1px solid var(--glass-edge);
        border-radius: var(--r-md);
        background: var(--glass);
        -webkit-backdrop-filter: var(--glass-blur);
        backdrop-filter: var(--glass-blur);
    }

    table.stack td {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        gap: 0.5rem;
        padding: 0;
        border: none;
    }

    table.stack td::before {
        content: attr(data-label);
        font-size: 12px;
        font-weight: 600;
        color: var(--text-muted);
    }

    /* A full-width message row (e.g. "No invites yet.") has no label. */
    table.stack td:not([data-label])::before {
        content: none;
    }
}

/* Status alert (info variant): an accent tint instead of the danger red. */
.alert-info {
    border-color: var(--accent-soft);
    background: var(--accent-soft);
}

/* Confirm pages: a single centered glass card, 420px wide. */
.confirm-panel {
    max-width: 420px;
    margin: 0 auto;
}

/* A report body in the admin list: user text, whitespace preserved. */
.report-body {
    white-space: pre-wrap;
}

/* -- account (UI redesign step 6c, docs/ui-redesign.md "Account"): the
   settings page is one centered column of glass cards. -- */

/* One column, 16px apart, centered at 640px. */
.account-stack {
    display: grid;
    gap: 16px;
    max-width: 640px;
    margin-inline: auto;
}

/* The card's status lines run a point larger than the 13px base. */
.account-stack .status {
    font-size: 14px;
}

/* Chat access row: the label and status on the left, the switch on the
   right. */
.switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.switch-copy {
    display: grid;
    gap: 2px;
}

.switch-name {
    font-size: 15px;
    color: var(--text);
}

.switch-row .status {
    margin: 0;
}

/* The chat switch: a 52x30 pill track with a 24px thumb. On, the track
   is the accent and the thumb slides 22px. The 44px button is the tap
   target; the visually hidden span is the accessible label. */
.switch {
    flex: none;
    width: 52px;
    height: 44px;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: 0;
}

.switch-track {
    display: block;
    width: 52px;
    height: 30px;
    background: rgba(255, 255, 255, 0.14);
    border-radius: var(--r-pill);
    transition: background-color 150ms ease;
}

.switch-thumb {
    display: block;
    width: 24px;
    height: 24px;
    margin: 3px;
    background: var(--text);
    border-radius: 50%;
    transition: transform 150ms ease, background-color 150ms ease;
}

.switch[aria-checked="true"] .switch-track {
    background: var(--accent);
}

.switch[aria-checked="true"] .switch-thumb {
    background: var(--on-accent);
    transform: translateX(22px);
}

/* 16px between the swatches and the Save chat color button. */
.account-stack .swatch-fieldset {
    margin-bottom: 16px;
}

/* -- bug report dialog (base.html): the native <dialog> form, opened
   from the "Report bug" buttons in the top bar / phone menu. The
   dialog card itself reuses the glass `dialog` rules above. -- */

/* Centered in the visible viewport, not the layout one: app.js mirrors
   the visual viewport into --vv-top / --vv-height while the dialog is
   open, so the card rides above a phone's on-screen keyboard and
   scrolls inside itself when the space left is short. */
#report-dialog {
    position: fixed;
    inset: auto 0;
    margin: 0 auto;
    top: calc(var(--vv-top, 0px) + var(--vv-height, 100dvh) / 2);
    transform: translateY(-50%);
    max-height: calc(var(--vv-height, 100dvh) - 2rem);
    overflow-y: auto;
}

#report-dialog h2 {
    margin-bottom: 1rem;
    font-size: 18px;
}

/* The dialog's status line: the htmx swap target. Reserves one line so
   the actions don't jump when the message appears. */
.report-status {
    margin: 0 0 1rem;
    font-size: 14px;
    color: var(--text-muted);
    min-height: 1.25rem;
}

.report-status .report-result {
    margin: 0;
}

.report-status .report-result--ok {
    color: var(--ok);
}

.report-status .report-result--error {
    color: var(--danger);
}

.report-dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.75rem;
}
