/* Pocket GameMaster — visual theme, layered over Bootstrap.
   Design tokens + a serif display face for a tabletop-book feel, an arcane-violet
   accent, warm parchment surfaces, and an atmospheric landing hero. A future
   "selectable themes" feature (ROADMAP) can swap these tokens. */

:root {
    --pgm-accent: #6d28d9;          /* arcane violet */
    --pgm-accent-dark: #5b21b6;
    --pgm-ink: #20242e;
    --pgm-paper: #f6f4ef;           /* warm parchment */
    --pgm-display: Georgia, "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
    --pgm-line: rgba(32, 36, 46, .08);   /* hairline borders */
    --pgm-shadow: rgba(32, 36, 46, .05); /* card shadow */
}

/* Dark mode (ADR-033). Bootstrap 5.3 recolors its OWN components off `data-bs-theme="dark"`;
   here we make the custom PGM tokens dark-aware so the parchment surfaces, hairlines, and accent
   follow suit. The `.pgm-auth-shell` hero is intentionally dark in both themes. */
[data-bs-theme="dark"] {
    --pgm-accent: #8b5cf6;          /* violet, brightened for a dark ground */
    --pgm-accent-dark: #a78bfa;     /* links/hover — lighter so they read on dark */
    --pgm-ink: #e7e4f0;             /* light ink */
    --pgm-paper: #17181f;           /* dark surface (replaces parchment) */
    --pgm-line: rgba(255, 255, 255, .10);
    --pgm-shadow: rgba(0, 0, 0, .45);
    color-scheme: dark;             /* native controls, scrollbars, form widgets follow */
}

body {
    background-color: var(--pgm-paper);
    color: var(--pgm-ink);
}

h1, h2, h3, h4, .navbar-brand, .card-title, .display-3, .display-6 {
    font-family: var(--pgm-display);
    letter-spacing: .2px;
}

.card {
    border: 1px solid var(--pgm-line);
    /* Squarer than Bootstrap's default. A table is boards, cards and tiles — soft corners read as a
       web dashboard, and this is meant to read as furniture. */
    border-radius: .2rem;
    box-shadow: 0 1px 2px var(--pgm-shadow), 0 8px 24px var(--pgm-shadow);
}

a { color: var(--pgm-accent-dark); }

.btn-primary {
    --bs-btn-bg: var(--pgm-accent);
    --bs-btn-border-color: var(--pgm-accent);
    --bs-btn-hover-bg: var(--pgm-accent-dark);
    --bs-btn-hover-border-color: var(--pgm-accent-dark);
    --bs-btn-active-bg: var(--pgm-accent-dark);
    --bs-btn-active-border-color: var(--pgm-accent-dark);
    --bs-btn-disabled-bg: var(--pgm-accent);
    --bs-btn-disabled-border-color: var(--pgm-accent);
}
.btn-outline-primary {
    --bs-btn-color: var(--pgm-accent-dark);
    --bs-btn-border-color: var(--pgm-accent);
    --bs-btn-hover-bg: var(--pgm-accent);
    --bs-btn-hover-border-color: var(--pgm-accent);
    --bs-btn-active-bg: var(--pgm-accent-dark);
}
.badge.bg-primary { background-color: var(--pgm-accent) !important; }

/* Landing / auth pages — atmospheric hero (AuthLayout wraps these) */
.pgm-auth-shell {
    background:
        radial-gradient(1100px 560px at 50% -12%, rgba(109, 40, 217, .28), transparent 60%),
        linear-gradient(180deg, #12131a 0%, #1b1d29 100%);
    color: #eae7f2;
}
.pgm-auth-shell h1.display-3 {
    color: #fff;
    text-shadow: 0 2px 28px rgba(109, 40, 217, .55);
}
.pgm-auth-shell .text-muted { color: #b7b2c6 !important; }
/* The hero behind this is always dark, but the CARD must still follow the theme: it used to be a
   hardcoded light #fbfaf7 while its text used var(--pgm-ink), which flips to LIGHT in dark mode —
   i.e. light text on a light card. Using --pgm-paper keeps the pair consistent in both themes. */
.pgm-auth-shell .card {
    background-color: var(--pgm-paper);
    color: var(--pgm-ink);
    border: 1px solid rgba(255, 255, 255, .12);
}
/* Inputs inside the card inherit the theme too, so placeholders/values stay legible. */
.pgm-auth-shell .card .form-control,
.pgm-auth-shell .card .form-select {
    background-color: color-mix(in srgb, var(--pgm-paper) 94%, var(--pgm-ink) 6%);
    color: var(--pgm-ink);
    border-color: color-mix(in srgb, var(--pgm-paper) 70%, var(--pgm-ink) 30%);
}
.pgm-auth-shell .card .text-muted { color: color-mix(in srgb, var(--pgm-ink) 65%, transparent) !important; }

/* Sidebar brand a touch larger */
.sidebar .navbar-brand { font-size: 1.15rem; }

/* Live-mic indicator (ADR-041): a player's video frame lights up while they are speaking, so an
   always-on mic is visible at a glance — and visible to the whole table, not just to that player.
   The transition keeps it from strobing on short bursts of speech. */
/* Camera tile.
   Height is set DIRECTLY, never as a ratio of width. Bootstrap's .ratio-4x3 sets height via
   padding-top:75%, so a narrower window (fewer grid columns → each card WIDER) made every tile
   taller and pushed the room and GM panels off the bottom of the screen. Clamping to viewport
   height instead means resizing horizontally no longer changes vertical layout at all. */
.pgm-cam-slot {
    position: relative;
    border-radius: .15rem;
    height: clamp(84px, 11vh, 132px);
    overflow: hidden;
    outline: 3px solid transparent;
    outline-offset: 1px;
    transition: outline-color .12s ease-in, box-shadow .12s ease-in;
}
/* LiveKit injects a <video> into the slot; cover it without letterboxing or stretching. */
.pgm-cam-slot > video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
/* ⚠️ A portrait in a camera slot had NO rule, so the <img> rendered at its natural size — a
   1024px portrait inside a 132px slot with overflow:hidden, which shows the top-left corner
   enormously magnified and reads as a broken image rather than a mis-sized one. `.pgm-gm-slot > img`
   existed for exactly this and the markup never carried that class.

   object-position favours the upper third: these are head-and-shoulders portraits, so cropping a
   square image into a wide slot from the centre cuts the face in half. */
.pgm-cam-slot > img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 25%;
    display: block;
}
.pgm-cam-slot > .pgm-cam-placeholder {
    position: absolute;
    inset: 0;
}
/* The GM's tile is a bit taller and visually distinct — it is the table's focal point, and it
   carries a portrait or camera rather than a character sheet. */
.pgm-gm-slot {
    height: clamp(110px, 15vh, 180px);
}
.pgm-gm-slot > img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 25%;   /* the face, not the chest — see the note above */
    display: block;
}
.pgm-cam-slot.pgm-speaking {
    outline-color: #22c55e;                          /* green = transmitting */
    box-shadow: 0 0 14px rgba(34, 197, 94, .65);
}
/* Hard mute must read as clearly OFF, never as "just not talking right now". */
.pgm-cam-slot.pgm-muted {
    outline-color: #dc3545;
    outline-style: dashed;
    box-shadow: none;
}

/* A recessed, theme-aware panel (play transcript, log panes, …). Use this instead of a hardcoded
   light colour like #f8f9fa, which turns into light-text-on-light in dark mode. */
.pgm-surface {
    background-color: color-mix(in srgb, var(--pgm-paper) 92%, var(--pgm-ink) 8%);
    color: var(--pgm-ink);
}

/* Outline drag & drop: while a room is being dragged, every group card advertises itself as a target. */
.pgm-droppable { outline: 2px dashed var(--pgm-accent); outline-offset: 3px; }
.pgm-droppable:hover { background-color: rgba(128, 128, 128, .10); }

/* Auto-growing prose textareas (room description, GM notes, …). `field-sizing: content` makes the
   box grow with its text in supporting browsers; min/max keep it sane and let it scroll past the cap.
   pgm-autogrow.js drives the same behaviour where field-sizing isn't supported yet.
   https://developer.mozilla.org/docs/Web/CSS/field-sizing */
textarea.pgm-autogrow {
    field-sizing: content;
    min-height: 4.5rem;
    max-height: 60vh;
    overflow-y: auto;
}

/* Room-graph Map (ADR-034 Phase 2) — theme-aware SVG graph. */
.pgm-room-map { background: var(--pgm-paper); user-select: none; }
/* Edges need to read against the paper AND against the node cards they pass over — the hairline
   --pgm-line was far too faint to trace. Mid-grey works in both themes; the halo keeps a line
   legible where it crosses a card. */
.pgm-map-edge {
    stroke: #8b8fa3;
    stroke-width: 2px;
    stroke-linecap: round;
    paint-order: stroke;
}
.pgm-map-arrow { fill: #8b8fa3; }
/* Nodes paint after edges, so a line passing behind a card is hidden by it rather than crossing it. */
.pgm-map-node { cursor: pointer; }
/* The card frame (fill shows as a border around the thumbnail + behind the caption). */
.pgm-map-node .pgm-node-card { fill: var(--pgm-paper); stroke: var(--pgm-accent); stroke-width: 1.5px; }
.pgm-map-node:hover .pgm-node-card { stroke-width: 3px; filter: brightness(1.1); }
.pgm-map-node .pgm-node-caption { fill: var(--pgm-ink); font-size: 13px; pointer-events: none; }
.pgm-map-node .pgm-node-img { pointer-events: none; }
.pgm-map-node .pgm-node-noimg { fill: rgba(128, 128, 128, .14); }        /* placeholder when no image */
.pgm-map-node .pgm-node-noimg-icon { font-size: 30px; opacity: .45; pointer-events: none; }
/* State frames — the coloured padding reads as a border around the scene thumbnail. */
.pgm-map-node.start .pgm-node-card { fill: #198754; stroke: #198754; stroke-width: 3px; }
.pgm-map-node.start .pgm-node-caption { fill: #fff; }
.pgm-map-node.deadend .pgm-node-card { stroke: #fd7e14; stroke-width: 3px; }
.pgm-map-node.unreachable .pgm-node-card { stroke: #dc3545; stroke-width: 2px; stroke-dasharray: 5 3; }
.pgm-map-node.connecting .pgm-node-card { stroke: var(--pgm-accent); stroke-width: 3.5px; }
.pgm-map-node.faded { opacity: .18; }               /* group highlight: non-group rooms recede */
.pgm-map-node.moving { opacity: .85; cursor: grabbing; }
/* A pinned room (hand-placed by the author) gets a subtle marker so you can tell it from an
   auto-placed one — "Re-arrange" clears every pin. */
.pgm-map-node.pinned .pgm-node-card { stroke-dasharray: none; filter: drop-shadow(0 2px 4px rgba(0,0,0,.35)); }
/* The link handle: drag from here to another room to create an exit. */
.pgm-node-link { fill: var(--pgm-accent); opacity: .35; cursor: crosshair; }
.pgm-map-node:hover .pgm-node-link { opacity: 1; }
.pgm-node-link-icon { fill: #fff; font-size: 10px; pointer-events: none; opacity: 0; }
.pgm-map-node:hover .pgm-node-link-icon { opacity: 1; }
.pgm-map-key { font-weight: bold; }
.pgm-map-key.start { color: #198754; }
.pgm-map-key.deadend { color: #fd7e14; }
.pgm-map-key.unreachable { color: #dc3545; }

/* Spoken narration reveals one word at a time as the voice reaches it (ADR-041). A hard pop draws
   the eye to the mechanism instead of the story, so each word fades up as it arrives.

   Deliberately `inline`, NOT `inline-block`: an inline-block box collapses the whitespace at its own
   edges, which ran every word together into one unreadable string. Staying inline means the words
   flow, wrap and inherit exactly like plain text — the only thing that changes is opacity. That also
   rules out transforms (they don't apply to inline boxes), which is fine: the ask was a fade. */
.pgm-word {
    animation: pgm-word-in 260ms ease-out both;
    /* Preserve the trailing space baked into each word, and any newlines the narration carries.
       Without this the browser collapses the separator and every word runs together. */
    white-space: pre-wrap;
}

@keyframes pgm-word-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* Motion here is decoration. For anyone who has asked the OS to reduce it, keep the reveal but make
   it near-instant rather than removing the cue entirely.
   https://developer.mozilla.org/docs/Web/CSS/@media/prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
    .pgm-word { animation-duration: 80ms; }
}

/* ── Ember indicator (ADR-053) ──────────────────────────────────────────────
   Quiet by default: this is a resource meter, not a call to action, and a
   balance that shouts at a player mid-session is worse than one they forget
   about. It only raises its voice when it is nearly out. */
.pgm-embers {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .15rem .6rem;
    margin-right: .75rem;
    border-radius: 999px;
    font-variant-numeric: tabular-nums;   /* the number must not jitter as it counts down */
    font-size: .9rem;
    line-height: 1.6;
    /* Explicit colours, not currentColor. Inheriting the bar's colour made this dark-on-dark and
       all but invisible: the top bar sets a colour for ITS content, and a pill that borrows it has
       no contrast against a background derived from the same value. A meter you cannot read is
       worse than no meter, because you believe you are watching it. */
    /* Light theme by default. The first attempt used one pale-orange colour for both themes and
       was unreadable on the light top bar (#f7f7f7) — orange on near-white. Each theme needs its
       own value; there is no single ember colour that reads on both. */
    color: #a85b00;
    background: rgba(240, 173, 78, .18);
    border: 1px solid rgba(200, 130, 30, .45);
    transition: color .3s ease, background-color .3s ease, border-color .3s ease;
}

[data-bs-theme="dark"] .pgm-embers {
    color: #ffc078;
    background: rgba(240, 173, 78, .16);
    border-color: rgba(240, 173, 78, .40);
}

.pgm-embers-icon { font-size: .95em; }

/* Running low — warm, not alarming. */
.pgm-embers-low {
    color: #8a4500;
    background: rgba(240, 173, 78, .38);
    border-color: rgba(200, 130, 30, .75);
}

[data-bs-theme="dark"] .pgm-embers-low {
    color: #ffd8a8;
    background: rgba(240, 173, 78, .30);
    border-color: rgba(240, 173, 78, .70);
}

/* Burned out. The icon changes to a dark moon as well, so this reads without relying on colour. */
.pgm-embers-out {
    color: #495057;
    background: rgba(108, 117, 125, .18);
    border-color: rgba(108, 117, 125, .40);
}

[data-bs-theme="dark"] .pgm-embers-out {
    color: #ced4da;
    background: rgba(173, 181, 189, .18);
    border-color: rgba(173, 181, 189, .35);
}

/* ── A grouping row inside a table ──────────────────────────────────────────
   Bootstrap's .table-light forces white regardless of the active theme, which is
   wrong for a header that only needs to sit slightly apart from its rows. Tinted
   from the theme instead, so it reads as a divider in both. */
.pgm-row-group > td {
    background-color: rgba(0, 0, 0, .04);
    letter-spacing: .02em;
    text-transform: uppercase;
    opacity: .75;
}

[data-bs-theme="dark"] .pgm-row-group > td {
    background-color: rgba(255, 255, 255, .06);
}

/* ── Sidebar: separating everyday navigation from admin ─────────────────────
   Tinted from the current theme rather than a fixed grey, for the same reason
   the status categories are: a hard-coded colour is right in one theme and
   wrong in the other. */
.pgm-nav-divider {
    height: 1px;
    margin: .75rem 1rem .25rem;
    background: rgba(255, 255, 255, .18);
}

.pgm-nav-section {
    padding: .1rem 1.5rem .35rem;
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    opacity: .55;
}

/* The sidebar keeps its dark treatment in light mode, so the divider only needs
   flipping where the surrounding chrome actually changes. */
[data-bs-theme="light"] .pgm-nav-divider {
    background: rgba(255, 255, 255, .22);
}

/* ── Transcript entries ─────────────────────────────────────────────────────
   Full width made the old tight stacking unreadable: without separation the
   whole session became one block of text, and the boundary between a player
   speaking and the game master answering — the most useful structure in the
   panel — disappeared. A hairline and real breathing room, tinted from the
   theme rather than a fixed grey. */
.pgm-line {
    padding: .45rem .25rem;
    border-top: 1px solid rgba(0, 0, 0, .07);
    line-height: 1.55;
}

/* No rule above the first line — a divider at the very top separates nothing. */
.pgm-line:first-child { border-top: 0; }

[data-bs-theme="dark"] .pgm-line { border-top-color: rgba(255, 255, 255, .09); }

/* The game master's lines get a little more air: they are what people are
   actually reading, and they arrive word by word. */
.pgm-line-gm { padding-block: .6rem; }

/* A continuation paragraph of the SAME entry. No hairline and no top padding, because the rule
   above .pgm-line marks the boundary between one speaker and the next — repeating it inside a
   single narration would turn one answer into what looks like three. The gap between paragraphs
   comes from the previous line's padding-bottom, which is already the right size. */
.pgm-line-cont {
    border-top: 0;
    padding-top: 0;
}

/* ── Who is speaking ────────────────────────────────────────────────────────
   A colour per player so the table is readable at a glance. Applied to the NAME
   and a left accent only — colouring the body text would trade legibility for
   identification, and the body is the part being read.

   Two palettes, because one set cannot serve both themes: pastels wash out to
   near-white on a light background, and saturated colours glare on a dark one.
   Same hue order in both, so a player's colour stays recognisably "the green
   one" whichever theme you use. */
.pgm-line { border-left: 3px solid transparent; padding-left: .55rem; }

.pgm-actor-0 { --pgm-speaker: #b3261e; }   /* red    */
.pgm-actor-1 { --pgm-speaker: #1e6b3a; }   /* green  */
.pgm-actor-2 { --pgm-speaker: #1a4f8a; }   /* blue   */
.pgm-actor-3 { --pgm-speaker: #8a6100; }   /* amber  */
.pgm-actor-4 { --pgm-speaker: #6b3fa0; }   /* violet */
.pgm-actor-5 { --pgm-speaker: #0f6b6b; }   /* teal   */

[data-bs-theme="dark"] .pgm-actor-0 { --pgm-speaker: #ff9a8f; }
[data-bs-theme="dark"] .pgm-actor-1 { --pgm-speaker: #86d9a4; }
[data-bs-theme="dark"] .pgm-actor-2 { --pgm-speaker: #8fbcf5; }
[data-bs-theme="dark"] .pgm-actor-3 { --pgm-speaker: #f2c86b; }
[data-bs-theme="dark"] .pgm-actor-4 { --pgm-speaker: #c9a6f0; }
[data-bs-theme="dark"] .pgm-actor-5 { --pgm-speaker: #7fd6d6; }

.pgm-speaker { color: var(--pgm-speaker); }

/* The accent repeats the name's colour down the left edge, so a run of lines
   from one player reads as a block without tinting a word of the text. */
.pgm-line[class*="pgm-actor-"] { border-left-color: var(--pgm-speaker); }

/* The game master is deliberately NOT in the palette: it is not one of the
   players, and giving it a colour would put it in the same visual category. */
.pgm-line-gm { border-left-color: transparent; }

/* ── 3D dice (proof of concept, ADR-070) ─────────────────────────────────────
   Geometry only. The tumble is a transform set by js/pgm-dice3d.js on an element
   Blazor rendered — see Components/Die3d.razor for why that split is not
   optional.

   In the global sheet rather than a scoped component stylesheet because these
   elements are addressed by data attribute from both Razor and JS, and because
   the dice will appear in more than one place (the lab, the tray, character
   creation).

   `--d6-size` is the one dial: everything else derives from it, so a die can be
   shrunk for a compact tray by overriding a single custom property. */
.pgm-d6-stage,
.pgm-token-stage {
    --d6-size: 3rem;
    --d6-half: calc(var(--d6-size) / 2);

    width: var(--d6-size);
    height: var(--d6-size);
    /* The depth of the illusion. About 6x the die's size reads as "held at arm's
       length"; much less and the cube looks shoved into your face as it turns. */
    perspective: calc(var(--d6-size) * 6);
    flex: 0 0 auto;
}

.pgm-d6,
.pgm-token {
    position: relative;
    width: 100%;
    height: 100%;
    /* Without this the faces flatten into the parent's plane and the cube
       collapses to a single square — the classic CSS-3D non-effect. */
    transform-style: preserve-3d;
    will-change: transform;
}

/* ⚠️ NO `filter` on these elements, ever — not even a drop-shadow on settle.
   `filter` is a grouping property: it flattens the 3D context of everything
   inside it, so a shadow added "just to finish it off" turns the cube into a
   flat pile of squares. Depth comes from box-shadow on the faces instead. */

.pgm-d6-face {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template: repeat(3, 1fr) / repeat(3, 1fr);
    padding: 12%;
    box-sizing: border-box;

    border-radius: 12%;
    background: linear-gradient(145deg,
        color-mix(in srgb, var(--pgm-paper) 88%, #fff),
        color-mix(in srgb, var(--pgm-paper) 70%, var(--pgm-ink)));
    border: 1px solid var(--pgm-line);
    box-shadow: inset 0 0 0.5rem rgba(0, 0, 0, .06);
}

/* Face placement. Opposite faces sum to 7 — 1/6, 2/5, 3/4 — which is how a real
   die is numbered, and the first thing anyone who has held one notices if it is
   wrong.

   Each rotation here has an exact inverse in FACE_TO_FRONT in pgm-dice3d.js: a
   face placed by rotateX(90deg) is brought to the camera by rotating the CUBE
   rotateX(-90deg). The two tables must stay inverses of each other. */
.pgm-d6-face[data-face="1"] { transform: rotateY(0deg)    translateZ(var(--d6-half)); }
.pgm-d6-face[data-face="3"] { transform: rotateY(90deg)   translateZ(var(--d6-half)); }
.pgm-d6-face[data-face="6"] { transform: rotateY(180deg)  translateZ(var(--d6-half)); }
.pgm-d6-face[data-face="4"] { transform: rotateY(-90deg)  translateZ(var(--d6-half)); }
.pgm-d6-face[data-face="2"] { transform: rotateX(90deg)   translateZ(var(--d6-half)); }
.pgm-d6-face[data-face="5"] { transform: rotateX(-90deg)  translateZ(var(--d6-half)); }

.pgm-d6-pip {
    align-self: center;
    justify-self: center;
    width: 62%;
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--pgm-ink);
}

/* ── Everything that is not a d6 ──────────────────────────────────────────────
   A shaped token, not a fake polyhedron. A d20's twenty triangular faces cannot
   be built out of CSS boxes, and a cube with "17" written on it is a lie about
   what the renderer can do — which is exactly what an earlier version
   accidentally did to a d4. The silhouette says which die it is; the number says
   what it rolled; nobody is invited to believe it is a solid. */
.pgm-token {
    display: grid;
    place-items: center;
    border-radius: 14%;
    background: linear-gradient(145deg,
        color-mix(in srgb, var(--pgm-accent, #6d4aff) 70%, var(--pgm-paper)),
        color-mix(in srgb, var(--pgm-accent, #6d4aff) 30%, var(--pgm-ink)));
    color: #fff;
    box-shadow: 0 1px 3px var(--pgm-shadow);
}

.pgm-token-value {
    grid-area: 1 / 1;
    font-family: var(--pgm-display, inherit);
    font-size: calc(var(--d6-size) * .42);
    font-weight: 700;
    line-height: 1;
}

/* The die type, small, under the number — so a table of mixed dice is readable
   without counting silhouette edges. */
.pgm-token-sides {
    grid-area: 1 / 1;
    align-self: end;
    font-size: calc(var(--d6-size) * .16);
    opacity: .75;
    transform: translateY(15%);
}

/* Silhouettes. Rough on purpose: these are recognisable shorthand for a die
   type, not projections of a polyhedron. clip-path rather than borders because a
   clipped element still takes pointer events and shadows correctly.
   https://developer.mozilla.org/docs/Web/CSS/clip-path */
.pgm-token-d4  { clip-path: polygon(50% 2%, 98% 96%, 2% 96%); border-radius: 0; }
.pgm-token-d8  { clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); border-radius: 0; }
.pgm-token-d10 { clip-path: polygon(50% 0%, 92% 35%, 74% 100%, 26% 100%, 8% 35%); border-radius: 0; }
.pgm-token-d12 { clip-path: polygon(50% 0%, 95% 33%, 79% 92%, 21% 92%, 5% 33%); border-radius: 0; }
.pgm-token-d20 { clip-path: polygon(25% 4%, 75% 4%, 100% 50%, 75% 96%, 25% 96%, 0% 50%); border-radius: 0; }
.pgm-token-d100 { border-radius: 50%; }

/* A settled die gets a moment of emphasis so the eye knows the number is final
   rather than mid-tumble. Scale, not filter — see the warning above. */
.pgm-d6-stage[data-settled] .pgm-d6-face,
.pgm-token-stage[data-settled] .pgm-token {
    border-color: color-mix(in srgb, var(--pgm-ink) 25%, transparent);
}

@media (prefers-reduced-motion: reduce) {
    /* The renderer already skips the animation; this makes sure nothing else
       here animates either. */
    .pgm-d6, .pgm-token { transition: none !important; }
}

/* ── Top-bar menus and overlay panels ────────────────────────────────────────
   The account menu is a <details> element: MainLayout is statically rendered, so
   an @onclick there never fires, and this app ships Bootstrap's CSS without its
   JS, so data-bs-toggle would render a dead button. <details> toggles natively.
   js/pgm-menu.js adds close-on-outside-click and Escape as an enhancement. */

.pgm-icon-btn {
    display: inline-grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    border: 1px solid var(--pgm-line);
    border-radius: .375rem;
    background: transparent;
    color: color-mix(in srgb, var(--pgm-ink) 70%, transparent);
    text-decoration: none;
    flex: 0 0 auto;
}

.pgm-icon-btn:hover,
.pgm-icon-btn:focus-visible {
    color: var(--pgm-ink);
    border-color: color-mix(in srgb, var(--pgm-ink) 25%, transparent);
    background: color-mix(in srgb, var(--pgm-ink) 6%, transparent);
}

.pgm-menu {
    position: relative;
    flex: 0 0 auto;
}

/* Kill the native disclosure triangle. `list-item` is what Safari uses, so both
   are needed — one alone leaves a marker on one browser and looks like a bug
   nobody can reproduce. */
.pgm-menu > summary { list-style: none; }
.pgm-menu > summary::-webkit-details-marker { display: none; }
.pgm-menu > summary::marker { content: ""; }

.pgm-menu-toggle {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .25rem .5rem;
    border-radius: .375rem;
    cursor: pointer;
    color: var(--pgm-ink);
    user-select: none;
}

.pgm-menu-toggle:hover,
.pgm-menu[open] > .pgm-menu-toggle {
    background: color-mix(in srgb, var(--pgm-ink) 8%, transparent);
}

.pgm-menu-caret { font-size: .7em; opacity: .6; }

.pgm-menu-panel {
    position: absolute;
    right: 0;
    top: calc(100% + .35rem);
    min-width: 12rem;
    padding: .35rem;
    border: 1px solid var(--pgm-line);
    border-radius: .5rem;
    background: var(--pgm-paper);
    box-shadow: 0 6px 20px var(--pgm-shadow);
    /* Above the play surface and its scroll regions, below the reconnect modal —
       a menu must not cover the one panel that explains why nothing is
       responding. */
    z-index: 1040;
}

.pgm-menu-head {
    padding: .3rem .55rem .45rem;
    font-size: .8rem;
    color: color-mix(in srgb, var(--pgm-ink) 55%, transparent);
}

.pgm-menu-item {
    display: block;
    width: 100%;
    padding: .4rem .55rem;
    border: 0;
    border-radius: .3rem;
    background: transparent;
    color: var(--pgm-ink);
    text-align: left;
    text-decoration: none;
    font-size: .9rem;
}

.pgm-menu-item:hover,
.pgm-menu-item:focus-visible {
    background: color-mix(in srgb, var(--pgm-ink) 8%, transparent);
    color: var(--pgm-ink);
}

.pgm-menu-danger:hover { color: var(--bs-danger, #dc3545); }

.pgm-menu-sep {
    height: 1px;
    margin: .35rem .25rem;
    background: var(--pgm-line);
}

/* ── Overlay panels ──────────────────────────────────────────────────────────
   The pattern that lets the rules, and later the settings, open OVER a live
   table instead of navigating away from it — which drops the voice room,
   restarts the music and republishes your camera (R-57). See the ROADMAP entry
   "leaving the table page must not leave the table". */
.pgm-overlay {
    position: fixed;
    inset: 0;
    display: grid;
    place-items: center;
    padding: 1rem;
    background: rgba(0, 0, 0, .5);
    /* Under the reconnect modal (1060) on purpose: if the server has gone away,
       that message matters more than whatever panel was open. */
    z-index: 1050;
}

.pgm-overlay-panel {
    display: flex;
    flex-direction: column;
    width: min(46rem, 100%);
    /* ⚠️ A FIXED height, not a max-height, and that is the whole point.
       With max-height the panel is sized by its contents, so every tab was a
       different size — the Dice tab tall, the Embers tab short — and the box
       jumped under the cursor each time you switched. Tabs that move are tabs
       you have to re-aim at.

       So the panel is always the same size and the BODY scrolls inside it. The
       height still adapts to the VIEWPORT (dvh), just never to the content.

       dvh, not vh: vh is the tallest the viewport ever gets, so a panel sized in
       it hides its own footer behind a mobile URL bar. */
    height: min(82dvh, 44rem);
    /* On a very short window the fixed height would push the footer off screen;
       this is the floor at which it stops shrinking and the body takes over. */
    min-height: min(20rem, 90dvh);
    border: 1px solid var(--pgm-line);
    border-radius: .6rem;
    background: var(--pgm-paper);
    box-shadow: 0 12px 40px var(--pgm-shadow);
    overflow: hidden;
}

.pgm-overlay-head,
.pgm-overlay-foot {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .6rem .8rem;
    flex: 0 0 auto;
}

.pgm-overlay-head { border-bottom: 1px solid var(--pgm-line); }
.pgm-overlay-foot { border-top: 1px solid var(--pgm-line); justify-content: space-between; }
.pgm-overlay-search { margin-left: auto; max-width: 16rem; }

.pgm-overlay-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
    padding: .5rem .8rem 0;
    flex: 0 0 auto;
}

.pgm-tab {
    padding: .25rem .6rem;
    border: 1px solid transparent;
    border-radius: 999px;
    background: transparent;
    color: color-mix(in srgb, var(--pgm-ink) 65%, transparent);
    font-size: .85rem;
}

.pgm-tab:hover { background: color-mix(in srgb, var(--pgm-ink) 7%, transparent); }

.pgm-tab.active {
    background: color-mix(in srgb, var(--pgm-ink) 10%, transparent);
    border-color: var(--pgm-line);
    color: var(--pgm-ink);
}

/* min-height: 0 is load-bearing on a flex child that scrolls — the default
   min-height:auto refuses to shrink below its content, so the panel would grow
   past its max-height and push the footer off screen. */
.pgm-overlay-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: .8rem;
}

.pgm-help-entry + .pgm-help-entry {
    margin-top: .9rem;
    padding-top: .9rem;
    border-top: 1px solid var(--pgm-line);
}

.pgm-help-q { font-weight: 600; margin-bottom: .15rem; }
.pgm-help-a { font-size: .92rem; color: color-mix(in srgb, var(--pgm-ink) 80%, transparent); }

.pgm-help-tab {
    margin-top: .3rem;
    font-size: .75rem;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: color-mix(in srgb, var(--pgm-ink) 45%, transparent);
}

/* ── The audio uploader's waveform (ADR-073) ─────────────────────────────────
   Colours are custom properties so the canvas can read them with
   getComputedStyle and follow the theme — a canvas cannot inherit a colour the
   way an element does, so the values have to be handed to it. */
.pgm-wave {
    --pgm-wave-ink: #7c6cff;
    --pgm-wave-dim: rgba(128, 128, 128, .30);

    display: block;
    width: 100%;
    height: 8rem;
    border: 1px solid var(--pgm-line);
    border-radius: .4rem;
    background: color-mix(in srgb, var(--pgm-paper) 94%, var(--pgm-ink));
    /* The handles are dragged, so the browser must not decide the gesture is a
       scroll and steal it half-way through. */
    touch-action: none;
    cursor: ew-resize;
}

[data-bs-theme="dark"] .pgm-wave {
    --pgm-wave-dim: rgba(200, 200, 220, .22);
}

.pgm-wave-legend {
    display: flex;
    flex-wrap: wrap;
    gap: .9rem;
    font-size: .8rem;
    color: color-mix(in srgb, var(--pgm-ink) 65%, transparent);
}

.pgm-wave-key {
    display: inline-block;
    width: .7rem;
    height: .7rem;
    border-radius: .15rem;
    margin-right: .3rem;
    vertical-align: -1px;
}


/* ── Menu panels that hang from the LEFT of their toggle ─────────────────────────────────────
   .pgm-menu-panel is right-aligned, which is correct for the account menu at the right edge of the
   bar and wrong for a nav menu at the left: the panel would run off the side of the window. */
.pgm-menu-panel--start {
    right: auto;
    left: 0;
}

/* The admin menu has thirteen entries plus separators, which on a laptop is taller than the space
   below the bar. Cap it and let it scroll rather than letting it run off the bottom, where the last
   items would be unreachable with no indication that they exist. */
.pgm-menu-panel--tall {
    max-height: min(70vh, 34rem);
    overflow-y: auto;
}


/* ── The top navigation (TopNav.razor) ───────────────────────────────────────────────────────
   Global rather than component-scoped on purpose: it is styled from MainLayout's top bar, and a
   scoped rule there cannot reach into a child component's markup without ::deep. Keeping the
   layout here and only the overrides there means there is one place to look. */
.pgm-topnav {
    display: flex;
    flex-direction: row;      /* stated, not assumed: this is the rule whose absence broke the bar */
    flex-wrap: nowrap;
    align-items: center;
    gap: .15rem;
    margin-left: 1.25rem;
    min-width: 0;             /* lets the bar shrink instead of forcing the page to scroll */
}

.pgm-topnav-link {
    padding: .25rem .55rem;
    border-radius: .375rem;
    color: color-mix(in srgb, var(--pgm-ink) 78%, transparent);
    text-decoration: none;
    white-space: nowrap;
}

.pgm-topnav-link:hover {
    background: color-mix(in srgb, var(--pgm-ink) 8%, transparent);
    color: var(--pgm-ink);
    text-decoration: none;
}

/* NavLink adds .active on the current route, which is why these are NavLinks and not anchors:
   a nav that does not say where you are is a list of links.
   https://learn.microsoft.com/aspnet/core/blazor/fundamentals/routing#navlink-component */
.pgm-topnav-link.active {
    color: var(--pgm-ink);
    background: color-mix(in srgb, var(--pgm-accent) 18%, transparent);
    font-weight: 600;
}

/* Narrow screens: the shortcuts fold into the menu, which already holds everything else. */
.pgm-topnav-narrow { display: none; }

@media (max-width: 820px) {
    .pgm-topnav-wide { display: none; }
    .pgm-topnav-narrow { display: block; }
    .pgm-topnav { margin-left: .5rem; }
}


/* ── The two-factor QR code ──────────────────────────────────────────────────────────────────
   No border, no rounding, no padding: the SVG already contains the white quiet zone a scanner
   needs, and anything added outside it is decoration that made the box and the code disagree
   about where the edge was. */
.pgm-qr {
    width: 12rem;
    max-width: 100%;
}

/* display:block also kills the inline-baseline gap under the SVG, which is the other reason a
   "tight" container ends up with a stripe along one side.
   https://developer.mozilla.org/docs/Web/CSS/display */
.pgm-qr svg {
    display: block;
    width: 100%;
    height: auto;
}

/* ── Campaign cards and the browser grid (ADR-084) ───────────────────────────────────────────
   One card, used by both the public browser and your own campaigns. See CampaignCard.razor for
   why: they are two questions about the same object, and drawing it twice would make them feel
   like different kinds of thing. */

.pgm-grid {
    display: grid;
    /* auto-fill with a minmax floor rather than fixed breakpoints: the grid reflows at every width
       without a media query per size, and a card never drops below a readable 17rem.
       https://developer.mozilla.org/docs/Web/CSS/repeat */
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    /* Each card is its own height. Stretching them to match the tallest is what created the gap
       above the footer on every shorter card. */
    align-items: start;
    gap: .85rem;
}

.pgm-grid-item {
    display: block;
    color: inherit;
    text-decoration: none;
}

.pgm-grid-item:hover .pgm-card {
    border-color: color-mix(in srgb, var(--pgm-accent) 45%, var(--pgm-line));
    transform: translateY(-2px);
}

/* Cards on the authoring page are not themselves links: the buttons underneath are. */
.pgm-grid-item-static:hover .pgm-card { transform: none; }

.pgm-card {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--pgm-line);
    border-radius: .6rem;
    background: var(--pgm-paper);
    overflow: hidden;
    transition: transform .12s ease, border-color .12s ease;
}

@media (prefers-reduced-motion: reduce) {
    .pgm-card { transition: none; }
    .pgm-grid-item:hover .pgm-card { transform: none; }
}

.pgm-card-art {
    position: relative;
    aspect-ratio: 16 / 9;
    display: grid;
    place-items: center;
    background: color-mix(in srgb, var(--pgm-ink) 7%, transparent);
}

.pgm-card-art img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* The initial, for a campaign with no artwork. Different for every campaign, so a grid of them
   does not read as a grid of identical missing things the way a placeholder image would. */
.pgm-card-initial {
    font-size: 2.6rem;
    font-weight: 700;
    opacity: .25;
    user-select: none;
}

/* The one badge on the art, because it is the only thing on the card that is a reason to click
   TODAY rather than a fact about the campaign. */
.pgm-card-live {
    position: absolute;
    top: .4rem;
    right: .4rem;
    padding: .1rem .4rem;
    border-radius: 999px;
    background: #1f9d55;
    color: #fff;
    font-size: .72rem;
    font-weight: 600;
}

.pgm-card-body {
    display: flex;
    flex-direction: column;
    gap: .3rem;
    padding: .6rem .7rem .7rem;
    flex: 1;
}

.pgm-card-title { font-weight: 600; line-height: 1.25; }

.pgm-card-by,
.pgm-card-facts {
    font-size: .78rem;
    color: color-mix(in srgb, var(--pgm-ink) 55%, transparent);
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
    align-items: baseline;
}

.pgm-card-dot { opacity: .5; }
.pgm-card-ai { color: var(--pgm-accent); font-weight: 600; }

.pgm-card-blurb {
    margin: .1rem 0 0;
    font-size: .84rem;
    line-height: 1.4;
    /* NO line clamp. The stored value is capped at CampaignLimits.MaxBlurbLength (280 characters),
       so the text is already bounded — and a LINE clamp is width-dependent, so it clips exactly
       where a card is narrow, which is where somebody most needs the sentence to finish.

       It was three lines, then six. Six still cut the 220-character blurbs off in a one-column
       layout, which is how this was reported the second time. Bounding the DATA and bounding the
       BOX are different things, and only one of them was ever needed here. */
}

.pgm-card-facts {
    /* Directly under the tags, NOT pinned to the bottom.

       It was `margin-top: auto`, which aligned every card's footer with the tallest card in the
       row. That reads as deliberate when blurbs are a uniform length and as a hole when they are
       not — and blurbs vary from 162 to 226 characters across the shipped campaigns. Alignment
       across cards is worth less than no gap inside one. */
    margin-top: .35rem;
}

.pgm-card-tags { display: flex; flex-wrap: wrap; gap: .25rem; }

.pgm-card-actions {
    display: flex;
    gap: .35rem;
    padding: 0 .7rem .7rem;
    flex-wrap: wrap;
}

/* ── Tags ──────────────────────────────────────────────────────────────────────────────────── */
.pgm-tag {
    padding: .05rem .4rem;
    border: 1px solid var(--pgm-line);
    border-radius: 999px;
    font-size: .72rem;
    white-space: nowrap;
    color: color-mix(in srgb, var(--pgm-ink) 70%, transparent);
}

/* Content warnings answer a different question from descriptive tags ("is this for me?" rather
   than "what is it?"), and the age gating depends on them being noticed (ADR-074). Marked by
   border AND weight, not colour alone, because colour alone fails for a lot of readers. */
.pgm-tag-warn {
    border-color: color-mix(in srgb, #c2410c 55%, var(--pgm-line));
    color: color-mix(in srgb, #c2410c 80%, var(--pgm-ink));
    font-weight: 600;
}

.pgm-tag-more { opacity: .6; }

.pgm-tag-btn {
    background: transparent;
    cursor: pointer;
}

.pgm-tag-btn:hover { border-color: color-mix(in srgb, var(--pgm-accent) 50%, var(--pgm-line)); }

.pgm-tag-on {
    background: color-mix(in srgb, var(--pgm-accent) 18%, transparent);
    border-color: var(--pgm-accent);
    color: var(--pgm-ink);
    font-weight: 600;
}

.pgm-tag-count { opacity: .55; font-variant-numeric: tabular-nums; }

/* ── Facets ────────────────────────────────────────────────────────────────────────────────── */
.pgm-facets { display: flex; flex-direction: column; gap: .3rem; }

.pgm-facet { display: flex; flex-wrap: wrap; gap: .25rem; align-items: center; }

.pgm-facet-name {
    font-size: .72rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    opacity: .5;
    min-width: 6rem;
}

/* ── Empty states ─────────────────────────────────────────────────────────────────────────── */
.pgm-empty {
    border: 1px dashed var(--pgm-line);
    border-radius: .6rem;
    padding: 1.4rem;
    text-align: center;
}

/* ── The detail overlay (ADR-085) ────────────────────────────────────────────────────────────
   One fixed size that does not change with its contents, internally scrollable. The fixed size is
   the point: a panel that resizes as you move between things is hard to use, which is the fault
   the help box had before it was pinned. */
.pgm-overlay-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, .5);
    z-index: 1050;
}

.pgm-overlay {
    position: fixed;
    z-index: 1051;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: min(42rem, calc(100vw - 2rem));
    height: min(34rem, calc(100vh - 3rem));
    display: flex;
    flex-direction: column;
    border: 1px solid var(--pgm-line);
    border-radius: .6rem;
    background: var(--pgm-paper);
    box-shadow: 0 12px 40px var(--pgm-shadow);
}

.pgm-overlay-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .6rem .75rem;
    border-bottom: 1px solid var(--pgm-line);
}

.pgm-overlay-body {
    padding: .75rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    flex: 1;
}

.pgm-table-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .35rem 0;
    border-top: 1px solid var(--pgm-line);
}

.pgm-table-row:first-child { border-top: 0; }

.pgm-overlay-danger {
    border-top: 1px solid var(--pgm-line);
    padding-top: .6rem;
}

/* Why you can see a campaign that is not published. Top-LEFT, opposite the "N open" badge, so a
   draft with a live table shows both without them colliding. Muted rather than alarming: an
   unpublished draft is a normal state, not a fault. */
.pgm-card-flag {
    position: absolute;
    top: .4rem;
    left: .4rem;
    padding: .1rem .4rem;
    border-radius: 999px;
    background: rgba(0, 0, 0, .62);
    color: #fff;
    font-size: .72rem;
    font-weight: 600;
}

/* ── Character sheet: the numbers combat uses (ADR-088) ────────────────────────────────────── */
.pgm-sheet-stats {
    display: flex;
    flex-wrap: wrap;
    gap: .1rem .75rem;
    font-size: .82rem;
    padding: .35rem .5rem;
    border-radius: .35rem;
    background: color-mix(in srgb, var(--pgm-ink) 5%, transparent);
}

/* A trained skill, with its total. Untrained skills are deliberately not listed: eighteen rows would
   bury the three that make this character distinct, and an untrained modifier is just the ability
   modifier already shown above it. */
.pgm-skill-chip {
    display: inline-block;
    padding: .05rem .4rem;
    margin: 0 .15rem .15rem 0;
    border: 1px solid var(--pgm-line);
    border-radius: 999px;
    font-size: .74rem;
    white-space: nowrap;
}

/* Expertise reads as more than proficiency, by weight as well as colour. */
.pgm-skill-expert {
    border-color: var(--pgm-accent);
    background: color-mix(in srgb, var(--pgm-accent) 16%, transparent);
    font-weight: 600;
}

/* ── The detail overlay's cover art ───────────────────────────────────────────────────────────
   Inside the overlay the cover is a BANNER, not the subject. At the card's 16/9 it filled the
   first screen on a laptop, so the things only this panel can tell you — the full blurb, and which
   tables are running and joinable — sat below the fold behind a scrollbar. Opening it looked like
   it had added nothing, because everything it added was out of sight. */
.pgm-overlay .pgm-card-art {
    aspect-ratio: auto;
    max-height: 11rem;
}

.pgm-overlay .pgm-card-art img {
    height: 11rem;
    object-fit: cover;
}

/* ── Field groups ────────────────────────────────────────────────────────────────────────────────
   A labelled box around fields that belong together, for forms long enough that a flat run of
   inputs stops being readable.

   ⚠️ The admin settings page is the case this was written for. Its ElevenLabs card had eleven
   controls at one visual level: a credential, a voice, two model pickers, three refresh buttons and
   the relay, with nothing to say which button acted on which field. One of them was two fields away
   from the thing it refreshed, and nobody noticed for weeks, because at one level everything looks
   equally related to everything else.

   <fieldset>/<legend> rather than a div and a heading: a screen reader announces the legend with
   each field inside it, so "Base URL" is heard as "Relay, Base URL" — which is the same grouping the
   sighted reader gets from the border, rather than a decoration only one of them receives.
   https://developer.mozilla.org/docs/Web/HTML/Element/fieldset */
.pgm-field-group {
    border: 1px solid var(--pgm-line);
    border-radius: .5rem;
    padding: .35rem .9rem .9rem;
    margin-top: 1.15rem;
    min-width: 0;   /* fieldset defaults to min-content, which breaks flex children inside it */
}

.pgm-field-group > legend {
    float: none;            /* Bootstrap's reboot floats legends; that collapses the border box */
    width: auto;
    padding: 0 .4rem;
    margin: 0 0 .25rem -.4rem;
    font-size: .8rem;
    font-weight: 600;
    letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}

/* The actions that belong to a group (Refresh, Test) sit against its bottom edge, so a button is
   never adrift between the field it acts on and the next one down. */
.pgm-field-actions {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
    margin-top: .5rem;
}

/* An optional group nobody needs most days. Collapsed unless it is in use. */
.pgm-field-group > summary {
    cursor: pointer;
    font-size: .8rem;
    font-weight: 600;
    letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
    margin-bottom: .25rem;
}
