/* EpicMoments — base and component classes.
   ---------------------------------------------------------------------------
   Values come from tokens.css; nothing here invents a colour, a radius or a
   spacing step. Read that file first — it carries the reasoning.

   MOBILE FIRST, and not as a preference. The core scenario is a player on a
   phone between matches asking "did a streamer just kill me?", so every rule
   below is written for ~390px and the `min-width: 1024px` blocks — each beside
   the component it widens — are the enlargement. Three rules recur and each has
   a comment where it applies: 44px minimum tap target, no hover-only affordance,
   and no horizontal page scroll — wide things scroll inside their own container.

   DESKTOP ALIGNMENT is the fourth: at >=1024px a content column is capped and
   CENTRED as a unit (`.content-column`, `.hero`), never capped and left where it
   falls. Text alignment inside is a separate decision — the hero centres its
   prose because it is a banner; a column of records does not.
   See docs/web/features/web-main-page.md §6. */

/* ===================================================================== base */

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

html {
    /* The one place a smooth-scroll is wanted, and it yields to the reader's own
       reduced-motion setting further down. */
    scroll-behavior: smooth;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-body);
    font-size: var(--text-base);
    line-height: 1.55;
    /* Belt and braces against a horizontal scrollbar: any child that overflows
       is a bug, and this makes it a clipped bug rather than a page that slides
       sideways under a thumb. */
    overflow-x: hidden;
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
    font-family: var(--font-display);
    line-height: 1.15;
    letter-spacing: -0.02em;
    margin: 0;
}

p {
    margin: 0;
}

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

a:hover {
    color: var(--accent);
}

img {
    max-width: 100%;
    display: block;
}

/* One focus treatment for the whole application, on the accent. `:focus-visible`
   rather than `:focus` so a mouse click does not leave a ring behind, while
   keyboard and switch-control users keep one. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* =================================================================== layout */

.page {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
}

.page__body {
    flex: 1 1 auto;
    width: 100%;
    max-width: var(--page-max);
    margin: 0 auto;
    padding: var(--space-5) var(--page-gutter) var(--space-8);
}

.stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.stack--tight {
    gap: var(--space-2);
}

.stack--loose {
    gap: var(--space-6);
}

/* Screen-reader-only: a label that a sighted user gets from context but an
   assistive technology has no other way to reach. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* ================================================================== app bar */

.appbar {
    position: sticky;
    top: 0;
    z-index: 20;
    height: var(--appbar-height);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 0 var(--page-gutter);
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--border);
}

.appbar__inner {
    width: 100%;
    max-width: var(--page-max);
    margin: 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.wordmark {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--text-lg);
    letter-spacing: -0.03em;
    color: var(--text);
    /* The wordmark is a link and a tap target, so it carries the 44px height
       through padding rather than through font size. */
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding-right: var(--space-2);
}

.wordmark:hover {
    color: var(--text);
}

.wordmark__accent {
    color: var(--accent);
}

.appbar__spacer {
    flex: 1 1 auto;
}

/* The signed-in identity. Not a menu yet — "My moments" points at /me, which is
   named and not designed (web-main-page.md §7), and a menu with one live entry
   is furniture. The name and the sign-out control are two explicit targets, 8px
   apart, rather than one control that does both. */
.identity {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.identity__name {
    font-size: var(--text-sm);
    color: var(--text-dim);
    max-width: 8rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 26rem) {
    /* At the narrowest widths the name loses to the sign-out control: a
       truncated name is decoration, and the control is the thing that has to
       stay tappable. */
    .identity__name {
        display: none;
    }
}

/* =================================================================== footer */

.footer {
    border-top: 1px solid var(--border);
    padding: var(--space-5) var(--page-gutter);
    color: var(--text-dimmer);
    font-size: var(--text-sm);
}

.footer__inner {
    width: 100%;
    max-width: var(--page-max);
    margin: 0 auto;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    align-items: baseline;
}

/* ================================================================== buttons */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--tap);
    padding: 0 var(--space-4);
    font: inherit;
    font-weight: 600;
    border-radius: var(--radius-control);
    border: 1px solid var(--border);
    background: var(--surface-raised);
    color: var(--text);
    cursor: pointer;
    transition: border-color var(--transition), background var(--transition), color var(--transition);
}

.btn:hover:not(:disabled) {
    border-color: var(--accent-dim);
    color: var(--accent);
}

.btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.btn--primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
}

.btn--primary:hover:not(:disabled) {
    background: var(--accent-strong);
    border-color: var(--accent-strong);
    color: var(--on-accent);
}

.btn--ghost {
    background: transparent;
}

.btn--block {
    width: 100%;
}

/* ======================================================== segmented control */

/* Player | Streamer. A two-state toggle rather than one box that guesses which
   kind of name was typed: the two searches have different result shapes, and a
   guess that lands wrong is worse than a control that is always right
   (web-main-page.md §2). */
.segmented {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: var(--space-1);
    padding: var(--space-1);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
}

.segmented__option {
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    font: inherit;
    font-weight: 600;
    color: var(--text-dim);
    background: transparent;
    border: 0;
    border-radius: calc(var(--radius-control) - 4px);
    cursor: pointer;
    transition: background var(--transition), color var(--transition);
}

.segmented__option:hover:not(:disabled) {
    color: var(--text);
}

.segmented__option[aria-pressed="true"] {
    background: var(--surface-raised);
    color: var(--text);
    box-shadow: inset 0 0 0 1px var(--border);
}

.segmented__option:disabled {
    color: var(--text-dimmer);
    cursor: not-allowed;
}

/* "coming soon" on the Streamer segment. Always painted, never hover-revealed:
   an affordance that only exists on hover does not exist on a phone. */
.segmented__badge {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-dimmer);
}

/* =============================================================== search box */

.searchbox {
    display: flex;
    align-items: stretch;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
    transition: border-color var(--transition);
}

.searchbox:focus-within {
    border-color: var(--accent-dim);
}

.searchbox__input {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 48px;
    padding: 0 var(--space-4);
    font: inherit;
    /* Never below 16px: iOS Safari zooms the viewport on focus otherwise, and
       the zoom does not come back. */
    font-size: var(--text-base);
    color: var(--text);
    background: transparent;
    border: 0;
    border-radius: var(--radius-control);
}

.searchbox__input::placeholder {
    color: var(--text-dimmer);
}

.searchbox__input:focus {
    outline: none;
}

/* The shard suffix lives inside the field's trailing edge because a shard
   belongs *to* the name — the same name on two shards is two players — rather
   than beside it as a separate filter (web-main-page.md §2). */
.searchbox__suffix {
    display: flex;
    align-items: center;
    position: relative;
    border-left: 1px solid var(--border);
}

.searchbox__suffix select {
    appearance: none;
    min-height: var(--tap);
    /* Right padding leaves room for the chevron painted by ::after below. */
    padding: 0 var(--space-6) 0 var(--space-3);
    font: inherit;
    /* ⚠ Never below 16px, and this is a <select> rather than a text input, which is easy to think exempt.
       It is not: iOS Safari zooms the viewport when ANY form control under 16px takes focus, and the zoom
       does not come back — so the reader is left on a horizontally scrolled page after choosing a
       platform. The suffix is kept visually subordinate to the name with weight and colour instead of with
       size, which is what it should have been doing anyway. */
    font-size: var(--text-base);
    font-weight: 600;
    color: var(--text-dim);
    background: transparent;
    border: 0;
    border-radius: 0 var(--radius-control) var(--radius-control) 0;
    cursor: pointer;
}

.searchbox__suffix select option {
    /* The native dropdown is painted by the OS, which does not inherit the page
       background — stated so the list is not black-on-black. */
    background: var(--surface-raised);
    color: var(--text);
}

.searchbox__suffix::after {
    content: "";
    position: absolute;
    right: var(--space-3);
    width: 0.5rem;
    height: 0.5rem;
    border-right: 2px solid var(--text-dimmer);
    border-bottom: 2px solid var(--text-dimmer);
    transform: translateY(-2px) rotate(45deg);
    pointer-events: none;
}

/* ============================================================== suggestions */

.suggestions {
    /* Opens *below* the input and in normal flow, not as an overlay: the phone
       keyboard covers the bottom half of the screen when the field is focused,
       and a list that pushes the layout around while someone is typing is worse
       than one that grows downward. */
    list-style: none;
    margin: var(--space-2) 0 0;
    padding: var(--space-1);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
}

.suggestions__item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    width: 100%;
    min-height: var(--tap);
    padding: 0 var(--space-3);
    font: inherit;
    text-align: left;
    color: var(--text);
    background: transparent;
    border: 0;
    border-radius: calc(var(--radius-control) - 4px);
    cursor: pointer;
}

.suggestions__item:hover {
    background: var(--surface-raised);
    color: var(--accent);
}

/* ==================================================================== cards */

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
    padding: var(--space-4);
}

.card--flush {
    padding: 0;
    overflow: hidden;
}

/* ============================================================== moment card */

/* The card, top to bottom: thumbnail full width, verdict, meta row, watch
   button. Never a small thumbnail beside text — at 390px that leaves ~140px of
   image, too small to recognise a scene, which removes the reason the thumbnail
   is there at all (docs/web/features/web-moment-card.md §6). */
.moment {
    display: flex;
    flex-direction: column;
}

.moment__thumb {
    position: relative;
    aspect-ratio: 16 / 9;
    background: var(--surface-raised);
    border-bottom: 1px solid var(--border);
}

.moment__thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.moment__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4);
}

.moment__verdict {
    font-size: var(--text-base);
    font-weight: 600;
    color: var(--text);
    margin: 0;
}

/* The other party's name is a link and looks like one, at 44px of effective
   height including its padding. The card is deliberately NOT one big anchor: it
   holds two targets, and nesting them inside a card-wide link makes a mis-tap go
   somewhere the reader did not intend. */
.moment__party {
    color: var(--accent);
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    padding: var(--space-2) 0;
}

.moment__party:hover {
    color: var(--accent-strong);
    text-decoration: underline;
}

.moment__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1) var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-dim);
}

.moment__meta-sep {
    color: var(--text-dimmer);
}

/* No clip is an ordinary state, not an error: the streamer had no covering
   video, the VOD has not reached this timestamp yet, or it has since expired.
   The card renders without a thumbnail and without a watch action — and
   deliberately not with a disabled button, which would promise that pressing it
   later works, and for an expired VOD it never will (web-moment-card.md §3). */
.moment__no-clip {
    font-size: var(--text-sm);
    color: var(--text-dimmer);
}

/* ================================================================== chips */

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-dim);
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
}

.chip--accent {
    color: var(--accent);
    border-color: var(--accent-dim);
}

/* Only a failure is coloured. Everything else in a processed-match list is an
   expected state, and colouring the ordinary case is what makes the one row
   that matters stop standing out. */
.chip--danger {
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 40%, transparent);
}

/* ============================================== provision funnel + progress */

/* A searched name nobody has heard of becomes this card, IN PLACE of the
   suggestion list, so the keyboard closing does not move it. Everything it
   adds grows downward (docs/web/features/web-player-moments.md §4–5). */
.funnel {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

/* Live states as they arrive, one row per event. An ordered list because they
   ARE ordered and a screen reader should say so; `aria-live` on the element in
   the markup announces each new row rather than re-reading the card. */
.funnel__progress {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.funnel__step {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-dim);
}

/* A dot rather than a spinner: a step that has already happened is not still
   happening, and a row of spinners claims otherwise. */
.funnel__step::before {
    content: "";
    flex: 0 0 auto;
    width: 0.5rem;
    height: 0.5rem;
    border-radius: var(--radius-pill);
    background: var(--accent-dim);
}

/* ====================================================== processing section */

/* A native <details>, deliberately: <summary> is a real tap target, is
   keyboard-operable and is announced as expandable without a line of
   JavaScript — and everything EpicMoments.Web.Client references is downloaded
   to the user's machine. Collapsed by default; it is reassurance for someone
   looking at an empty list, not the point of the page. */
.processing {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
}

/* This row is the "last updated" line (§5) — a 44px control that opens the
   section, never a caption with a button beside it. */
.processing__summary {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--tap);
    padding: 0 var(--space-4);
    font-size: var(--text-sm);
    color: var(--text-dim);
    cursor: pointer;
    /* The default triangle is replaced by the chevron below, which can be
       positioned and coloured with the rest of the system. `list-style` covers
       Firefox; the -webkit- pseudo-element covers Safari, which ignores it. */
    list-style: none;
}

.processing__summary::-webkit-details-marker {
    display: none;
}

.processing__summary:hover {
    color: var(--text);
}

.processing__summary-text {
    flex: 1 1 auto;
    min-width: 0;
}

/* Painted, never hover-revealed: an affordance that exists only on hover does
   not exist on a phone. */
.processing__summary::after {
    content: "";
    flex: 0 0 auto;
    width: 0.5rem;
    height: 0.5rem;
    border-right: 2px solid var(--text-dimmer);
    border-bottom: 2px solid var(--text-dimmer);
    transform: translateY(-2px) rotate(45deg);
    transition: transform var(--transition);
}

.processing[open] > .processing__summary::after {
    transform: translateY(2px) rotate(-135deg);
}

.processing__body {
    padding: var(--space-4);
    border-top: 1px solid var(--border);
    font-size: var(--text-sm);
}

/* ================================================== processed-match list */

/* A card list, not a table. Four columns do not fit 390px, and a table is
   EpicMoments.Admin's idiom — an operator grid, not a page a player reads on a
   phone (web-player-moments.md §5). */
.match-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.match {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3);
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
}

.match__main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.match__when {
    color: var(--text);
}

.match__moments {
    font-size: var(--text-xs);
    color: var(--text-dimmer);
}

/* ===================================================================== hero */

.hero {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-6) 0 var(--space-7);
}

.hero__headline {
    font-size: var(--text-3xl);
    font-weight: 700;
    /* Two lines maximum at 390px, which the wording is chosen for rather than
       clamped to — a clamped headline that loses its second half says nothing. */
    max-width: 18ch;
}

.hero__sub {
    font-size: var(--text-lg);
    color: var(--text-dim);
    max-width: 44ch;
}

/* Desktop: the hero is CENTRED on the page's axis — headline, sub, toggle and input.
   Mobile stays left-aligned, where a centred column of prose is harder to scan and
   the control is already full-bleed anyway.

   ⚠ Centring the container is not enough and that was the bug. `.page__body` has
   `max-width` + `margin: 0 auto`, so it centres correctly — but `.hero` is a flex
   COLUMN with the default `align-items: stretch`, and a stretch item carrying its
   own `max-width` is capped at that width and then left at the cross-axis START.
   So every capped child (18ch, 44ch, 40rem) hugged the left edge of a perfectly
   centred container, and on a wide window the whole hero looked off-centre while
   the shell around it did not. */
@media (min-width: 1024px) {
    .hero {
        padding: var(--space-8) 0;
        align-items: center;
        text-align: center;
    }

    .hero__headline {
        max-width: 22ch;
    }

    .hero__search {
        /* ⚠ `width: 100%` is required, not belt-and-braces. `align-items: center`
           replaces `stretch`, so this item's width becomes shrink-to-fit — and
           since its own children are a grid and a flex row, it would collapse to
           roughly its content width and the search field would stop being a
           search field. This restores the full width, which `max-width` then
           caps, and centring positions what is left. */
        width: 100%;
        max-width: 40rem;

        /* The BLOCK is centred; its contents are not. `text-align` inherits, and
           centred text inside the input means the caret starts mid-field and
           drifts as the name is typed, with the placeholder and every suggestion
           row centred too. The centring the design asks for is the hero's axis —
           the prose above, not the control's innards. */
        text-align: left;
    }
}

/* ============================================================ page headings */

.page-title {
    font-size: var(--text-2xl);
    font-weight: 700;
    overflow-wrap: anywhere;
}

.section-title {
    font-size: var(--text-xl);
    font-weight: 600;
}

.muted {
    color: var(--text-dim);
}

.muted-more {
    color: var(--text-dimmer);
    font-size: var(--text-sm);
}

/* ============================================================= empty states */

/* An empty list is a real and common answer here, not a failure: most players
   never met a streamer. It says so plainly rather than looking broken. */
.empty {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    align-items: flex-start;
    padding: var(--space-6) var(--space-4);
    background: var(--surface);
    border: 1px dashed var(--border);
    border-radius: var(--radius-card);
    color: var(--text-dim);
}

.empty__title {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text);
}

.alert {
    padding: var(--space-3) var(--space-4);
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
    border-radius: var(--radius-control);
    color: var(--text);
    font-size: var(--text-sm);
}

/* ================================================================ skeletons */

/* A skeleton, not a spinner: the card's shape is known, so reserving it stops
   the layout jumping as thumbnails arrive (web-moment-card.md §6). */
.skeleton {
    background: linear-gradient(
        90deg,
        var(--surface) 0%,
        var(--surface-raised) 50%,
        var(--surface) 100%
    );
    background-size: 200% 100%;
    animation: skeleton-sweep 1.4s ease-in-out infinite;
    border-radius: var(--radius-control);
}

.skeleton--thumb {
    aspect-ratio: 16 / 9;
    border-radius: var(--radius-card) var(--radius-card) 0 0;
}

.skeleton--line {
    height: 0.875rem;
}

.skeleton--line-short {
    width: 40%;
}

@keyframes skeleton-sweep {
    from { background-position: 200% 0; }
    to { background-position: -200% 0; }
}

/* ============================================================ content column */

/* A page whose content is one reading column — the player moments page today.

   At >=1024px the WHOLE column is capped and centred as a single unit: the
   identity header, the status/error area and the moment cards together. That is
   what keeps the left edge those three share intact while the column as a whole
   sits on the page's centre axis. Capping only the card list, which is what
   shipped first, produced exactly the misalignment this avoids — cards starting
   further right than the heading naming them — and leaving the column
   start-aligned to dodge that just moved the problem: on a wide window the page
   reads as pushed left.

   ⚠ This is COLUMN centring, not text centring. `text-align` is deliberately
   untouched, so everything inside stays left-aligned. The hero is the other case
   and does both (see its own block) — it is a standalone banner, whereas this is
   a column of records to be read down. */
.content-column {
    width: 100%;
}

@media (min-width: 1024px) {
    .content-column {
        max-width: 44rem;
        margin-inline: auto;
    }
}

/* The feed stays ONE column at every width — a two-column feed has no reading
   order. Its width comes from `.content-column` above, deliberately: capping the
   list here as well would let it disagree with the header sitting above it. */
.moment-list {
    width: 100%;
}

/* ================================================================= avatars */

/* One element, three sizes, used by the search rows, the roster tiles and the
   streamer header. `object-fit: cover` because Twitch profile images are square
   by convention and not by guarantee, and a non-square one letterboxed inside a
   circle reads as a rendering fault. */
.avatar {
    flex: 0 0 auto;
    border-radius: var(--radius-pill);
    object-fit: cover;
    background: var(--surface-raised);
}

.avatar--sm {
    width: 40px;
    height: 40px;
}

/* 72px, per web-streamer-page.md §5 — the roster tile's own size, chosen so
   roughly six fit at 390px with the next one peeking. */
.avatar--lg {
    width: 72px;
    height: 72px;
}

/* 64px, the streamer header's. Smaller than the roster tile deliberately: the
   header's job is to identify, and the display name below it is the thing being
   read. */
.avatar--xl {
    width: 64px;
    height: 64px;
}

/* ========================================================= streamer results */

/* A list of 56px rows in the hero's streamer mode. THE WHOLE ROW IS THE TARGET,
   not the name inside it — an avatar with a name beside it invites a tap
   anywhere in the strip, and a target that is only the text is the single most
   common way a list like this fails on a phone (web-streamer-page.md §5).

   `.suggestions--rich` reuses the suggestion list's container so the two modes
   share one shape below the input; only the row differs. */
.suggestions--rich {
    padding: var(--space-1);
}

.streamer-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    min-height: 56px;
    padding: var(--space-2) var(--space-3);
    border-radius: calc(var(--radius-control) - 4px);
    color: var(--text);
}

.streamer-row:hover {
    background: var(--surface-raised);
    /* The anchor's own :hover recolours it; the names below inherit, so nothing
       here needs to restate the accent — and the row is never the ONLY way to
       tell it is a link, since the whole list is a list of links. */
}

.streamer-row__names {
    display: flex;
    flex-direction: column;
    /* Without this a long display name pushes the row wider than its container
       instead of truncating, and the list scrolls sideways inside a page that
       must never scroll sideways. */
    min-width: 0;
}

.streamer-row__display {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The login, muted, underneath. It is what the channel URL is built from and it
   often differs from the display name by more than case, so it is shown rather
   than assumed. */
.streamer-row__login {
    font-size: var(--text-sm);
    color: var(--text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ========================================================== roster strip */

/* "Streamers on EpicMoments". Horizontal scroll with snap, INSIDE its own
   container — the page itself never scrolls sideways (web-main-page.md §6).
   At 390px roughly six tiles fit with the next peeking, which is what makes the
   scroll discoverable without a hint or a hover-only arrow. */
.roster {
    display: flex;
    gap: var(--space-4);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    /* Room for the focus ring on the first and last tile, which an
       `overflow: auto` container otherwise clips. */
    padding: var(--space-1) var(--space-1) var(--space-3);
    /* Chrome/Safari paint a scrollbar over the names otherwise; the snap and the
       peeking tile are the affordance. */
    scrollbar-width: none;
}

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

.roster__tile {
    flex: 0 0 auto;
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    /* The avatar is 72px; the tile is wider so a two-word name has somewhere to
       go, and this is also what sets how many peek at 390px. */
    width: 5.5rem;
    color: var(--text-dim);
    text-align: center;
}

.roster__tile:hover {
    color: var(--accent);
}

.roster__name {
    font-size: var(--text-sm);
    line-height: 1.3;
    /* Two lines maximum, then ellipsis: a third line makes the tiles disagree in
       height and the row stops reading as a row. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    /* The standard property beside the prefixed one: browsers that have shipped
       `line-clamp` no longer need the -webkit- form, and one without the other
       is a rule that quietly stops clamping. */
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    overflow-wrap: anywhere;
}

/* Desktop wraps the strip into a grid instead of scrolling it — the same tiles,
   no longer hidden off the right edge on a screen with room for all of them.
   NOTHING appears here that was not on the phone. */
@media (min-width: 1024px) {
    .roster {
        flex-wrap: wrap;
        overflow-x: visible;
        scroll-snap-type: none;
    }
}

/* ======================================================== streamer header */

/* ⚠ ONE STACKED BLOCK at 390px — avatar, display name below it, channel link
   below that — and not an avatar-beside-text row, which truncates the display
   name at exactly the width where names get interesting
   (web-streamer-page.md §5). */
.streamer-header {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
}

.streamer-header__text {
    /* Lets the name and the "last seen live" line truncate rather than widen the
       header past the column. */
    min-width: 0;
}

/* A real 44px control, not an underlined word in a sentence (§5). `align-self`
   keeps it at its content width instead of stretching across the column, so it
   reads as a button rather than as a banner. */
.streamer-header__channel {
    align-self: flex-start;
}

/* Desktop widens the header into avatar-beside-text — the enlargement the design
   asks for, and the reason the mobile form stacks rather than starting here. */
@media (min-width: 1024px) {
    .streamer-header {
        flex-direction: row;
        align-items: center;
        gap: var(--space-5);
    }
}

/* Two controls side by side inside an empty state — a retry and a way back.
   Wraps rather than shrinking, and the gap is the 8px minimum between adjacent
   tap targets (web-main-page.md §6). */
.empty__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* ========================================================== happening now */

/* The front page's live feed of streamer×streamer showdowns.
   Design: docs/web/features/web-happening-now.md, card in web-moment-card.md §4.

   Horizontal scroll with snap, INSIDE its own container — the page itself never
   scrolls sideways (web-main-page.md §6). One card is ~85% of the viewport at
   390px so the next one peeks, which is what makes the scroll discoverable
   without a hint or a hover-only arrow. */
.marquee {
    display: flex;
    gap: var(--space-4);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    /* Room for the focus ring on the first and last card, which an
       `overflow: auto` container otherwise clips. */
    padding: var(--space-1) var(--space-1) var(--space-3);
    /* Chrome/Safari paint a scrollbar over the cards otherwise; the snap and the
       peeking card are the affordance. */
    scrollbar-width: none;
}

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

.marquee__item {
    flex: 0 0 85%;
    scroll-snap-align: start;
    /* `align-items: stretch` would otherwise make every card as tall as the
       tallest, which leaves a one-button card with a gap under its button. */
    align-self: flex-start;
}

/* A feed item slides in ONCE on arrival (web-main-page.md §3). The class is
   applied only to cards that arrived over the stream, so the initial fill does
   not animate the whole row on first paint, and it is never removed — the
   animation does not repeat, and taking the class off would re-run it on the
   next render.

   `forwards` matters: without it the card snaps back to the start of the
   keyframe after the animation, which reads as a flicker rather than as motion. */
.marquee__item--new {
    animation: feed-item-in 240ms ease-out both;
}

@keyframes feed-item-in {
    from { opacity: 0; transform: translateY(-0.5rem); }
    to { opacity: 1; transform: none; }
}

/* ⚠ THE ONLY THING ON THIS SITE ALLOWED TO PULSE (web-main-page.md §3).
   Continuous motion beside video thumbnails competes with the thumbnails; a 6px
   dot does not. It appears only when the newest card is genuinely fresh — that
   decision is the feed's, from a threshold the server sends
   (web-happening-now.md §5.1). */
.live-dot {
    display: inline-block;
    width: 0.5rem;
    height: 0.5rem;
    margin-inline-start: var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--accent);
    vertical-align: middle;
    animation: live-pulse 1.8s ease-in-out infinite;
}

@keyframes live-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

/* Desktop turns the marquee into a wrapped grid rather than a longer scroll —
   the same cards, no longer hidden off the right edge on a screen with room for
   them. NOTHING appears here that was not on the phone. */
@media (min-width: 1024px) {
    .marquee {
        flex-wrap: wrap;
        overflow-x: visible;
        scroll-snap-type: none;
    }

    .marquee__item {
        /* Three across at 1024px and up, minus the two gaps. */
        flex: 0 0 calc((100% - (2 * var(--space-4))) / 3);
    }
}

/* ======================================================== two-POV card */

/* Two thumbnails side by side, split by a ⚔ seam in the gold accent.

   ⚠ THEY STAY SIDE BY SIDE AT 390px, each ~48% wide. Reflowing them vertically
   on a narrow screen loses the entire visual metaphor — a confrontation across
   the seam — and is not an acceptable degradation (web-moment-card.md §6). */
.showdown__povs {
    display: flex;
    align-items: stretch;
    gap: 1px;
    background: var(--border);
    border-bottom: 1px solid var(--border);
}

.showdown__pov {
    /* `min-width: 0` because a long display name would otherwise widen its half
       past 50% and push the seam off centre. */
    flex: 1 1 0;
    min-width: 0;
    background: var(--surface);
}

/* A fixed 16:9 box whether or not there is an image in it: a side with no
   covering VOD renders as an EMPTY SURFACE, never as a broken image and never as
   a collapsed column — the seam has to keep something on either side of it or
   the card stops reading as a confrontation.

   ⚠ `position: relative` is HERE, on the thumbnail, rather than on the row that
   also contains the name links — which is what the seam is positioned against,
   and the reason it lands on the thumbnails' centre line rather than somewhere
   below it. Centring on the whole block would put the ⚔ over the name links, at
   a vertical offset that changes with the name's line count. */
.showdown__thumb {
    position: relative;
    aspect-ratio: 16 / 9;
    background: var(--surface-raised);
}

.showdown__thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The name is a link and looks like one, at 44px of effective height including
   its padding.

   ⚠ THE TWO ARE 8px APART, which is the minimum between adjacent tap targets
   (tokens.css, web-main-page.md §6). The seam between the two halves is 1px, so
   the gap has to come from the links themselves: `margin-inline: var(--space-1)`
   on each puts 4px on either side of the join. Without it the two links share an
   edge and a mis-tap at the centre of the card opens the wrong streamer's page —
   which looks like it worked. */
.showdown__name {
    display: block;
    margin-inline: var(--space-1);
    padding: var(--space-3) var(--space-2);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--accent);
    text-align: center;
    /* One line, then ellipsis: a second line makes the two halves disagree in
       height and the seam stops looking like a seam. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.showdown__name:hover {
    color: var(--accent-strong);
    text-decoration: underline;
}

/* The accent used at size — the one place it is (web-main-page.md §3). A disc on
   the THUMBNAILS' centre line, half of it overhanging the join.

   ⚠ It is positioned against `.showdown__thumb` (the attacker's, which is the
   first one) rather than against `.showdown__povs`. That row also contains the
   two name links, so centring on it would drop the ⚔ below the images by half a
   line of text — and by a different amount whenever a name wrapped. Anchoring to
   the thumbnail puts it where the two frames actually meet, at any card width.
   `left: 100%` is the thumbnail's trailing edge, which is the join.

   A painted disc rather than a bare glyph: the seam sits over two arbitrary video
   frames, and an unbacked character is legible on some of them and invisible on
   others. `pointer-events: none` so it never intercepts a tap meant for either
   half, and it is `aria-hidden` in the markup — the verdict line already says who
   met whom. */
.showdown__pov:first-child .showdown__thumb {
    position: relative;
    overflow: visible;
}

.showdown__seam {
    position: absolute;
    top: 50%;
    left: 100%;
    transform: translate(-50%, -50%);
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: var(--radius-pill);
    background: var(--bg);
    border: 1px solid var(--accent-dim);
    font-size: var(--text-base);
    line-height: 1;
    color: var(--accent);
    pointer-events: none;
}

/* The two watch actions stack, full width, and the body's own gap keeps them the
   8px minimum apart. ⚠ Never one control that opens both — a phone browser
   blocks the second tab, silently (web-moment-card.md §4). */
.moment--showdown .moment__body {
    gap: var(--space-2);
}

.moment--showdown .moment__verdict {
    margin-bottom: var(--space-1);
}
