/* ---------------------------------------------------------------------------
   Chrome primitives — the shell's small component layer.
   See docs/design/theming.md ("A chrome-primitive layer for the shell"). These are the hand-written
   semantic classes that style the Blazor components in WebClient/Components/UI/ (Button, IconBtn, Card,
   Modal, PanelModal). They read only `:root` tokens (the runtime palette + the static app tokens): no utility
   classes, no inline styles, no hardcoded palette colours. Every colour is a token, so a palette change
   re-tints the chrome with no CSS edit. Loaded after app-base.css (motion/type tokens + animations) and
   app.css (static design tokens), and before the runtime palette is injected — the primitives only add
   classes, they never redefine a token.
   --------------------------------------------------------------------------- */

/* --- Button (Components/UI/Button.razor) ------------------------------------ */

.app-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    padding: 0.7rem 1rem;
    border-radius: 0.7rem;
    font-size: 1rem;
    border: 1px solid transparent;
    cursor: pointer;
    transition: filter 0.15s ease, background-color 0.15s ease;
}

/* The single primary CTA: the runtime accent (--accent-blue), the app's one call-to-action treatment
   (see theming.md). */
.app-btn--primary {
    background: var(--accent-blue);
    color: var(--text-on-accent);
    box-shadow: 0 10px 22px -12px color-mix(in srgb, var(--accent-blue) 75%, transparent);
}
.app-btn--primary:hover:not(:disabled) { filter: brightness(1.06); }

.app-btn--secondary {
    background: var(--bg-card);
    border-color: var(--border-color);
    color: var(--text-primary);
}
.app-btn--secondary:hover:not(:disabled) { background: var(--bg-hover); }

/* A destructive confirm (e.g. resign): red where the primary is blue, so a click that can't be taken back
   reads as one. */
.app-btn--danger {
    background: linear-gradient(180deg, var(--warn-ember-light) 0%, var(--warn-ember) 55%, var(--warn-ember-deep) 100%);
    color: var(--text-on-accent-deep);
}
.app-btn--danger:hover:not(:disabled) { filter: brightness(1.05); }

.app-btn--ghost {
    background: transparent;
    border-color: transparent;
    color: var(--text-secondary);
}
.app-btn--ghost:hover:not(:disabled) {
    background: var(--bg-hover);
    color: var(--text-primary);
}

/* Inert: reads as unavailable but stays clickable and focusable, unlike the `disabled` attribute. For an action
   the app must explain rather than merely withhold (the login dialog's passkey buttons, which carry the reason
   in their title), a real `disabled` would swallow the tap and drop the button out of the tab order — leaving a
   player with no way to ask why. Callers pair this with aria-disabled="true", which is what assistive tech
   reads. */
.app-btn--inert {
    opacity: 0.55;
    cursor: not-allowed;
}
.app-btn--inert:hover { filter: none; }
.app-btn--secondary.app-btn--inert:hover { background: var(--bg-card); }
.app-btn--ghost.app-btn--inert:hover { background: transparent; color: var(--text-secondary); }

/* Sizes: Medium (the default) carries no modifier class. */
.app-btn--sm { padding: 0.5rem 0.75rem; font-size: 0.88rem; }
.app-btn--lg { padding: 0.85rem 1.25rem; font-size: 1.05rem; }

/* Opt-in full-width action, used for stacked dialog buttons. */
.app-btn--block { width: 100%; }

/* --- IconBtn (Components/UI/IconBtn.razor) ---------------------------------- */
/* The round icon-button recipe the shell had copy-pasted across the chrome. Every adopter is a standalone
   affordance (a dialog's close, a share toggle), so the circle is drawn at the app's 44px tap floor rather than
   at the glyph's size — the same 2.75rem .app-modal__panel-close states below. */

.app-btn-icon {
    width: 2.75rem;
    height: 2.75rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-color);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
}
.app-btn-icon:hover:not(:disabled) {
    background: var(--bg-hover);
    border-color: color-mix(in srgb, var(--accent-blue) 30%, var(--border-color));
    color: var(--text-primary);
}

/* --- Card (Components/UI/Card.razor) ---------------------------------------- */
/* The bordered rounded-surface recipe on --bg-card. */

.surface {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: 1rem;
    padding: 1rem;
}

/* --- Input ------------------------------------------------------------------ */
/* The text-entry recipe: a bordered field on --bg-card. Unlike its neighbours here this primitive is a class
   with no component wrapper, because the element varies — the same field is an <input>, a <textarea> and a
   <select> across its adopters — and Blazor's two-way binding means a wrapper would add @bind-Value plumbing
   at every call site while removing nothing. The adopter supplies layout (width, flex, min-height) in its own
   class, the way a PanelModal adopter owns its card's padding and scroll.

   Focus states the accent on the border and does NOT suppress the outline, so the app-wide :focus-visible ring
   (app.css) still answers the keyboard. The border is the always-on cue; the ring is the keyboard one. */
.app-input {
    /* 1rem, not smaller: iOS Safari zooms the page in when a field whose text is under 16px takes focus, and
       nothing zooms it back out. The padding then carries the field past the app's 44px tap floor — a floor
       stated here rather than as a min-height, which an adopter's own layout rule would silently outrank. */
    padding: 0.6rem 0.7rem;
    border-radius: 0.6rem;
    border: 1px solid var(--border-color);
    background: var(--bg-card);
    color: var(--text-primary);
    font-size: 1rem;
}
.app-input:focus { border-color: var(--accent-blue); }

/* Sizes. Medium (the default) carries no modifier class. */
.app-input--sm { padding: 0.15rem 0.4rem; border-radius: 0.4rem; font-size: 0.72rem; }
.app-input--lg { padding: 0.6rem 0.8rem; font-size: 1rem; }

/* The composer shape: a rounded field that reads as somewhere to type a line rather than fill a form. */
.app-input--pill { padding: 0.6rem 0.9rem; border-radius: var(--radius-pill); font-size: 1rem; }

/* The compact field reads small where a cursor points at it; on a touch device it takes the full body size,
   since a smaller one costs the player a page zoom they cannot undo. */
@media (pointer: coarse) {
    .app-input--sm { font-size: 1rem; }
}

/* --- Chip ------------------------------------------------------------------- */
/* The small pill that labels the thing beside it: a group's posture, a roster role, a session's cadence, a
   user's tag. A class rather than a component — it wraps one word, and a component around a <span> would be
   ceremony. The adopter supplies placement (align-self, margin) in its own class.

   The `--status` modifier is the load-bearing distinction, and it is about the *source of the text*, not the
   look: a status chip carries a word the app chose (OPEN, ADMIN, WEEKLY, HOST), so shouting it reads as chrome.
   A chip carrying text a player typed — a group's tags — must never take it, because uppercasing someone's
   content rewrites it. */
.app-chip {
    flex: 0 0 auto;
    padding: 0.05rem 0.45rem;
    border-radius: var(--radius-pill);
    font-size: 0.68rem;
    font-weight: 600;
    background: var(--bg-hover);
    color: var(--text-muted);
}

.app-chip--status { font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }

/* Tones. Each is a tint of a palette accent on the card fill, or the card fill inside a hairline. */
.app-chip--accent {
    background: color-mix(in srgb, var(--accent-blue) 16%, var(--bg-card));
    color: var(--accent-blue-ink);
}
/* Teal's ink is mixed toward the surrounding text rather than being the deep teal outright: the deep shade is
   legible on a table's white card and disappears into the shell's navy, and the mix reads on either canvas. */
.app-chip--teal {
    background: color-mix(in srgb, var(--accent-teal) 20%, var(--bg-card));
    color: color-mix(in srgb, var(--accent-teal) 45%, var(--text-primary));
}
.app-chip--outline {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
}

/* --- EmptyState (Components/UI/EmptyState.razor) ---------------------------- */
/* The centered "nothing here yet" column. Its type is the dialog-body vocabulary below at one size up, since
   an empty state is the only thing on its surface rather than a line inside a card. The hint's max-width is
   what keeps it a readable measure on a wide shelf. */

.app-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 0.35rem;
    padding: 2.5rem 1.25rem 2rem;
}
.app-empty__art { font-size: 2.4rem; line-height: 1; color: var(--text-muted); }
.app-empty__title { text-wrap: balance; margin: 0; font-size: 1.1rem; color: var(--text-primary); }
.app-empty__hint {
    text-wrap: pretty;
    margin: 0;
    max-width: 22rem;
    font-size: 0.86rem;
    line-height: 1.4;
    color: var(--text-muted);
}
.app-empty__action { margin-top: 0.65rem; }

/* --- Overflow menu ----------------------------------------------------------
   The small floating list a ⋯ button opens. The primitive owns the surface (the card fill, the hairline, the
   corner, the lift) and the column it stacks its rows in; the adopter owns only where the box hangs from its
   own anchor, since that is the one thing a friends row and a chat line genuinely disagree about — a row in an
   overflow: visible list opens downward from its right edge, a message in a scrolling log opens on whichever
   side has the room. Both adopters place a positioned ancestor themselves. */
.app-menu {
    position: absolute;
    z-index: var(--z-chrome);
    min-width: 8rem;
    padding: 0.3rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: 0.6rem;
    box-shadow: var(--shadow-md);
    display: flex;
    flex-direction: column;
}
.app-menu__item {
    text-align: left;
    padding: 0.45rem 0.6rem;
    border: none;
    background: transparent;
    border-radius: 0.45rem;
    font: inherit;
    font-size: 0.85rem;
    color: var(--text-primary);
    cursor: pointer;
}
.app-menu__item:hover { background: var(--bg-hover); }

/* --- Micro button -----------------------------------------------------------
   The smallest real action in the app: a bordered word sitting inside a dense row (a member's Promote, a
   message's Report), too small for .app-btn--sm and not an icon, so it is its own recipe rather than a
   modifier of the button family. Standalone like .app-btn-icon above; a caller carrying it does not also
   carry .app-btn. */
.app-btn-micro {
    padding: 0.1rem 0.4rem;
    font-size: 0.72rem;
    border-radius: 0.4rem;
    border: 1px solid var(--border-color);
    background: var(--bg-card);
    color: var(--text-secondary);
    cursor: pointer;
}
.app-btn-micro:hover { background: var(--bg-hover); color: var(--text-primary); }

/* --- Action chip ------------------------------------------------------------
   The compact ghost action that tints to the platform blue on hover — the Watch on a match-history row and the
   Replay on a group's. It reads as a ghost until pointed at, which is what keeps a row of them from competing
   with the row's own content. An adopter that must differ on a property named here compounds its selector with
   this class, since chrome.css loads last. */
.app-action-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.4rem 0.75rem;
    border-radius: 0.6rem;
    font-size: 0.82rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    color: var(--text-primary);
    background: var(--bg-hover);
    border: 1px solid var(--border-color);
    transition: background-color var(--tat-ui-base) ease, border-color var(--tat-ui-base) ease,
                color var(--tat-ui-base) ease;
}
.app-action-chip:hover {
    background: color-mix(in srgb, var(--accent-blue) 24%, var(--bg-hover));
    border-color: var(--accent-blue);
    color: var(--text-on-accent);
}

/* --- Modal (Components/UI/Modal.razor) -------------------------------------- */
/* The scrim, shared with PanelModal below, and the narrow centered card of a small confirm. One scrim recipe
   serves every dialog in the app; the layer modifiers pick which tier it sits on. */

.app-modal {
    position: fixed;
    inset: 0;
    /* The scrim is the box a card centres in, so it is bounded to the viewport that is actually on screen.
       height wins over the over-constrained bottom from inset: 0, which on a handheld with retracted browser
       bars measures the larger box and centres the card below the fold. */
    height: 100dvh;
    z-index: var(--z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.25rem;
    background: color-mix(in srgb, var(--scrim) 34%, transparent);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    animation: mp-fade-in 0.15s ease both;
}
/* The over-room tier: a dialog summoned from inside a match (the rules reference, the group view) clears the
   full-screen room and its overflow menu while staying below the input-grabbing top. */
.app-modal--over-room { z-index: var(--z-reading); }
/* The top tier: an input-grabbing action modal (connection-lost, resign-confirm) and the global settings panel
   float above every other overlay. */
.app-modal--top { z-index: var(--z-modal-top); }

.app-modal__card {
    position: relative;        /* anchor for the close button's corner pin */
    width: 100%;
    max-width: 20rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.6rem;
    padding: 1.5rem;
    border-radius: 1.3rem;
    background: var(--bg-elevated);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-lg);
    text-align: center;
    animation: mp-pop-in 0.28s var(--ease-spring) both;
    /* The scrim is a fixed, non-scrolling box, so the card carries its own bound and its own scroll: a card
       taller than the screen (a long notice, or any dialog at a large page zoom) would otherwise put its action
       row past the bottom edge with nothing able to reach it. 100% is the scrim's content box, so the scrim's
       padding stays clear. */
    max-height: 100%;
    overflow-y: auto;
}

.app-modal__head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
}
.app-modal__title { font-size: 1.5rem; color: var(--text-primary); text-wrap: balance; }
.app-modal__body { width: 100%; }

/* The dialog body's typography vocabulary — used inside either card, and by the few page surfaces that carry a
   dialog-shaped block. */
.app-modal__art { font-size: 3rem; line-height: 1; }
.app-modal__meta { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
.app-modal__blurb { font-size: 0.9rem; color: var(--text-secondary); line-height: 1.4; text-wrap: pretty; }
/* A full-sentence aside inside a dialog (why an action is unavailable, say). Quieter than a blurb, but left
   as written — .app-modal__meta's uppercase + letter-spacing suits a short label, not prose to be read. */
.app-modal__note { font-size: 0.8rem; color: var(--text-muted); line-height: 1.45; }
.app-modal__placeholder { font-size: 1rem; color: var(--text-secondary); padding: 0.5rem 0; }

/* The manual install steps (docs/design/pwa-installation.md) — Add to Home Screen on a handset, Add to Dock on
   a Mac — opened from the install button in the app chrome. The chrome carries a backdrop-filter, which makes
   it the containing block for any fixed-position descendant, so a centered overlay rendered from in here
   resolves against the chrome's own box and lands half off screen — the card is absolutely positioned instead,
   and it takes its anchor from whichever shape the chrome is in.

   Rail shape (wide): the chrome is a full-height column at the left edge, so a drop below it would land off the
   bottom of the screen. The wrapper is positioned and the card opens beside the button, inward over the content
   column. */
.install-affordance { position: relative; display: inline-flex; }

.install-pop {
    position: absolute;
    bottom: 0;
    left: calc(100% + 0.5rem);
    z-index: 1;
    /* Bounded by the viewport the offsets above are measured within. */
    width: min(19rem, calc(100vw - 1.8rem));
    padding: 1.1rem 1.15rem 1.2rem;
    border-radius: 1rem;
    background: var(--bg-elevated);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-lg);
    text-align: left;
    animation: mp-pop-in 0.22s var(--ease-spring) both;
}
.install-pop__title {
    font-size: 1.05rem;
    color: var(--text-primary);
    /* Clear of the close button pinned in the corner (its own 2.75rem square plus the 0.45rem it is inset by). */
    padding-right: 3.4rem;
    margin-bottom: 0.4rem;
}
.install-pop__close { position: absolute; top: 0.45rem; right: 0.45rem; }

.install-steps__intro {
    color: var(--text-secondary);
    margin-bottom: 0.75rem;
}
.install-steps {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin: 0;
    padding-left: 1.25rem;
    list-style: decimal;
    color: var(--text-primary);
}
.install-steps strong { color: var(--text-primary); font-weight: 600; }

/* Phone shape: the chrome is a top bar spanning the viewport, so the card drops from the bar rather than from
   the button. Un-positioning the wrapper hands the anchor to the bar itself, which keeps the card on screen at
   every width — aligning to the button would push the card's left edge off a narrow phone, since the button sits
   partway along the action cluster rather than at the screen edge. Dropping from the bar also puts the steps
   directly under the control that opened them. */
@media (max-width: 56rem) {
    .install-affordance { position: static; }
    .install-pop {
        top: calc(100% + 0.4rem);
        right: 0.9rem;
        bottom: auto;
        left: auto;
    }
}
/* Stacked full-width actions — Block buttons stretch the column. */
.app-modal__actions {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: 100%;
}
/* The close affordance: the round icon-button pinned to the card's top-right corner, clear of the centered
   title and the card's rounded radius. */
.app-modal__close { position: absolute; top: 0.75rem; right: 0.75rem; }

/* Reduced motion: mp-pop-in is a scale overshoot (0.85 → 1.03 → 1), which is exactly what a player asking for
   less motion is asking not to see — and these three primitives draw every dialog, panel and popover in the
   app. They still fade, which carries the same "something arrived" signal without the movement. */
@media (prefers-reduced-motion: reduce) {
    .app-modal__card,
    .app-modal__panel,
    .install-pop { animation: mp-fade-in 0.15s ease both; }
}

/* --- PanelModal (Components/UI/PanelModal.razor) ---------------------------- */
/* The large left-aligned panel card, sharing .app-modal's scrim and layer tiers with the centered card above.
   The primitive owns the frame, the header row and the keyboard; the adopter's own card class owns padding,
   gap, max-height and scroll, since the panels differ in all four. The body renders as direct children of this
   flex column, so a panel's tabs / list / sections stay its own flex items. */

.app-modal__panel {
    position: relative;        /* the positioning anchor for anything the card floats over its own content */
    width: 100%;
    max-width: 32rem;
    display: flex;
    flex-direction: column;
    /* The primitive owns the fill and the corner: the panels that adopt it draw one surface at one radius, so
       an adopter states only what it genuinely differs on. --chromeless resets both after this rule. */
    border-radius: 1rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-lg);
    text-align: left;
    animation: mp-pop-in 0.28s var(--ease-spring) both;
}

/* Sizes cap the width and nothing else, so an adopter's own width rule composes rather than fighting.
   Standard (the default) carries no modifier class. */
.app-modal__panel--compact { max-width: 22rem; }
.app-modal__panel--wide { max-width: 38rem; }
.app-modal__panel--full { max-width: 72rem; }

/* Chromeless: the primitive frames a card its child already draws (the game-detail popup) rather than drawing
   one, so the sizing box keeps only its width cap and its dialog semantics. */
.app-modal__panel--chromeless {
    flex-direction: row;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    animation: none;
}

/* The header row: an optional glyph, the heading block taking the remaining width, any extra controls, then
   the close. */
.app-modal__panel-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}
.app-modal__panel-icon { font-size: 1.25rem; }
/* The grow box. min-width:0 is what lets a long title ellipsize instead of stretching the row. */
.app-modal__panel-heading {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}
.app-modal__panel-eyebrow {
    margin: 0;
    color: var(--accent-blue-ink);
    font-size: 0.7rem;
    font-weight: 750;
    letter-spacing: 0.09em;
    text-transform: uppercase;
}
.app-modal__panel-title { margin: 0; font-size: 1.05rem; font-weight: 700; color: var(--text-primary); }
.app-modal__panel-subtitle { margin: 0; font-size: 0.8rem; color: var(--text-muted); }
/* The close affordance: a round icon button on the card's own fill, sized to the app's tap-target floor and
   held at that size while the heading beside it takes the remaining width. */
.app-modal__panel-close {
    flex: none;
    width: 2.75rem;
    height: 2.75rem;
    border: none;
    background: var(--bg-hover);
    font-size: 1.2rem;
}
