@font-face {
    font-family: 'Cinzel Web';
    src: url('assets/fonts/Cinzel-SemiBold.ttf') format('truetype');
    font-weight: 600;
}

:root {
    color-scheme: dark;
    --bg: #141410;
    --panel: #26271f;
    --panel-border: #3c3d33;
    --text: #eee;
    --muted: #a0a196;
}

* { box-sizing: border-box; }

html, body {
    /* Hard guarantee, regardless of any sub-pixel rounding or per-browser
       layout quirk in whatever sits inside: nothing can ever create a
       horizontal scrollbar or visually escape past the screen edge.
       Doesn't affect dropdowns/menus opening below their button - those
       overflow vertically, which this leaves untouched. */
    overflow-x: hidden;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: Georgia, 'Times New Roman', serif;
}

header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
    padding: 10px 20px;
    background: var(--panel);
    border-bottom: 1px solid var(--panel-border);
}

header h1 {
    margin: 0;
    margin-top: 6px;
    font-family: 'Cinzel Web', Georgia, serif;
    font-size: 1.9em;
    letter-spacing: 1px;
}

#headerLeft {
    display: flex;
    align-items: center;
}

#homeLink {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-left: 10px;
    color: inherit;
    text-decoration: none;
}

#siteLogo {
    height: 34px;
    width: auto;
}

/* Shared "click to open, click elsewhere to close" dropdown look, for both
   the hamburger nav menu and the board's display-settings menu. */
#navMenu, #displaySettings, #notifBell, #avatarMenu {
    position: relative;
}

#accountHeader {
    align-items: center;
    gap: 10px;
    margin-left: 14px;
}

#accountHeader:not([hidden]) {
    display: flex;
}

#notifBtn {
    font-size: 1.1em;
    line-height: 1;
    padding: 4px 8px;
    position: relative;
}

.bellIcon {
    height: 1.2em;
    width: auto;
    vertical-align: -0.3em;
    display: inline-block;
}

#notifBadge {
    position: absolute;
    top: -2px;
    right: -2px;
    min-width: 16px;
    height: 16px;
    padding: 0 3px;
    border-radius: 8px;
    background: #b5442e;
    color: #fff;
    font-size: 0.7em;
    line-height: 16px;
    text-align: center;
}

/* display must live in a :not([hidden]) rule, not the unconditional one
   above - see the #navMenuDropdown comment for why. */
#notifBadge:not([hidden]) {
    display: inline-block;
}

#notifDropdown {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 4px;
    flex-direction: column;
    gap: 4px;
    padding: 8px;
    min-width: 240px;
    max-width: 320px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    z-index: 20;
}

#notifDropdown:not([hidden]) {
    display: flex;
}

#notifDropdown p {
    margin: 4px;
    color: var(--muted);
    font-size: 0.9em;
}

.notifRow {
    display: block;
    padding: 6px 8px;
    border-radius: 4px;
    color: var(--text);
    text-decoration: none;
    font-size: 0.9em;
}

.notifRow:hover {
    background: #40412f;
}

#avatarMenu {
    display: flex;
    align-items: center;
    gap: 8px;
}

#headerUserLabel {
    font-size: 1.1em;
    color: var(--muted);
    white-space: nowrap;
}

/* display:contents on desktop - a pure grouping wrapper (see index.html's
   comment) so rating+trophy row still behave exactly like direct
   #avatarMenu flex children, matching the layout from before this
   wrapper existed. Mobile overrides this to an actual flex row - see the
   touch media query. */
#headerMetaRow {
    display: contents;
}

#headerRatingText {
    font-size: 1.1em;
    color: var(--muted);
    white-space: nowrap;
}

#avatarMenuBtn {
    padding: 2px;
    border-radius: 50%;
    line-height: 0;
}

#avatarMenuBtn img, #avatarMenuBtn canvas {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    object-fit: cover;
    vertical-align: middle;
}

#avatarMenuDropdown {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 4px;
    flex-direction: column;
    gap: 4px;
    padding: 8px;
    min-width: 140px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    z-index: 20;
}

#avatarMenuDropdown:not([hidden]) {
    display: flex;
}

#avatarMenuDropdown a, #avatarMenuDropdown button {
    display: block;
    width: 100%;
    padding: 4px 8px;
    border-radius: 4px;
    color: var(--text);
    text-decoration: none;
    text-align: left;
    background: transparent;
    border: none;
}

#avatarMenuDropdown a:hover, #avatarMenuDropdown button:hover {
    background: #40412f;
}

#navMenuBtn {
    font-size: 1.3em;
    line-height: 1;
    padding: 4px 10px;
}

#navMenuDropdown, #displaySettingsDropdown {
    position: absolute;
    left: 0;
    flex-direction: column;
    gap: 8px;
    padding: 10px;
    min-width: 170px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    z-index: 20;
}

#navMenuDropdown {
    top: 100%;
    margin-top: 4px;
}

/* Opens upward instead of downward - this button sits near the bottom of
   the page content, so a downward menu would open into empty space below
   the board rather than toward anything useful. */
#displaySettingsDropdown {
    bottom: 100%;
    margin-bottom: 4px;
}

/* `display` must live here, not in the rule above - an ID selector still
   beats the browser's built-in `[hidden] { display: none }` regardless of
   its own specificity (author rules always win over user-agent ones), so
   putting it on the unconditional rule would make the JS hidden-attribute
   toggle silently do nothing. */
#navMenuDropdown:not([hidden]), #displaySettingsDropdown:not([hidden]) {
    display: flex;
}

#navMenuDropdown a {
    display: block;
    padding: 4px 8px;
    border-radius: 4px;
    color: var(--text);
    text-decoration: none;
}

#navMenuDropdown a:hover {
    background: #40412f;
}

#newGamePanel, #displayPanel {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 0.9em;
}

/* #authPanel is now toggled via .hidden (see renderAuthUI in main.js) -
   split out of the rule above and given its own :not([hidden]) guard, or
   this hits the same #navMenuDropdown gotcha (an id-selector display rule
   beats [hidden] regardless of specificity). */
#authPanel {
    align-items: center;
    gap: 12px;
    font-size: 0.9em;
}

#authPanel:not([hidden]) {
    display: flex;
}

#authPanel {
    gap: 6px;
}

#authStatus {
    color: var(--muted);
    font-size: 0.9em;
}

#forgotPasswordBtn {
    background: transparent;
    border: none;
    color: var(--muted);
    text-decoration: underline;
    padding: 0;
    font-size: 0.85em;
}

#forgotPasswordBtn:hover:not(:disabled) {
    background: transparent;
    color: var(--text);
}

/* A plain #id{display:...} rule beats [hidden]'s built-in display:none
   regardless of specificity - see the #navMenuDropdown comment below for
   the same gotcha, hit again here. */
#verifyEmailBanner {
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 8px 15px;
    background: #4a3a1a;
    color: #f2d9a6;
    font-size: 0.9em;
    text-align: center;
}

#verifyEmailBanner:not([hidden]) {
    display: flex;
}

#verifyEmailBanner button {
    font-size: 0.85em;
}

select, button, input {
    font-family: inherit;
    font-size: 0.95em;
    padding: 4px 8px;
    background: #33342a;
    color: var(--text);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
}

button { cursor: pointer; }
button:hover:not(:disabled) { background: #40412f; }
button:disabled { opacity: 0.5; cursor: default; }

#playModes {
    display: flex;
    flex-direction: column;
    max-width: 1000px;
    margin: 10px auto 0;
    padding: 0 15px;
}

.playModeBand {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 12px;
    padding: 6px 0;
    border-bottom: 1px solid var(--panel-border);
}

.playModeBand:last-child {
    border-bottom: none;
}

.playModeBand h2 {
    margin: 0;
    min-width: 130px;
    font-family: 'Cinzel Web', Georgia, serif;
    font-size: 0.95em;
    font-weight: normal;
    color: var(--muted);
    white-space: nowrap;
}

#challengePanel {
    font-size: 0.9em;
}

/* Lives at the bottom of #boardWrap now (after display settings, and
   after chat during a multiplayer game) - boardWrap itself is already
   centered/width-constrained, so this just needs the same top-margin
   spacing its siblings (#belowBoard, #chatPanel) use. */
#liveGamesPanel {
    margin: 10px 15px 0;
}

#liveGamesPanel h2 {
    margin: 0 0 4px;
    font-family: 'Cinzel Web', Georgia, serif;
    font-size: 0.95em;
    font-weight: normal;
    color: var(--muted);
}

#liveGamesList p {
    color: var(--muted);
    font-size: 0.9em;
    margin: 4px 0;
}

#challengeSend {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
}

.challengeRow {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 0;
    color: var(--muted);
}

#statusLine {
    text-align: center;
    padding: 8px;
    font-size: 0.95em;
    color: var(--muted);
    min-height: 1.2em;
}

#boardWrap {
    position: relative;
    width: 1000px;
    max-width: 100%;
    margin: 10px auto 40px;
}

#boardCanvas {
    display: block;
    width: 100%;
    height: auto;
    cursor: pointer;
    margin-top: 8px;
    aspect-ratio: 1000 / 860;
    /* Only ever visible with no game loaded - drawScreen() (render.js)
       opaquely fills the whole canvas the instant a game exists, painting
       right over this; it's only ever showing through on the actual
       canvas pixels during the blank state before/between games. */
    background: var(--bg) url('assets/misc/queen_watermark.png') center / auto 60% no-repeat;
}

/* Three tracks - left (display settings/flip board), center (Abort/
   Resign for a vs-AI game, OR Offer Draw/Resign for human-vs-human -
   see below), right (history arrows) - sized 1fr/auto/1fr so the center
   track sits exactly midway between the two 1fr tracks regardless of
   how wide either side's content is, rather than "wherever space-
   between happens to leave it". */
#belowBoard {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 12px;
    margin-top: 10px;
    padding: 0 15px;
}

#displayPanel {
    grid-column: 1;
    justify-self: start;
}

/* #quickActions (Abort/Resign, a vs-AI game) and #gameActions (Offer
   Draw/Resign, human-vs-human) deliberately share the SAME center
   column rather than each getting their own - exactly one of them ever
   has visible content at a time (updateGameActionButtons in main.js
   hides whichever game type doesn't apply), so whichever one is active
   simply becomes "the centered middle group" in both modes, with no
   need to merge them into one element or juggle two different layouts. */
#quickActions {
    grid-column: 2;
    justify-self: center;
    display: flex;
    gap: 10px;
}

#quickActions button {
    padding: 6px 14px;
}

#historyControls {
    grid-column: 3;
    display: flex;
    gap: 10px;
    justify-self: end;
}

#historyControls button {
    min-width: 60px;
    padding: 6px 14px;
    font-size: 1em;
}

/* Shares #quickActions' center column (see the comment on #quickActions
   above) - hidden for a vs-AI game (no draw offers to make), holding
   Offer Draw/Accept/Decline and (only in a human-vs-human game) Resign
   otherwise. grid-column is pinned explicitly (same reasoning as
   #displayPanel/#historyControls above) so hiding this element can
   never shift #historyControls into its column - that's exactly what
   broke the vs-AI layout before this was added: grid auto-placement
   skips a display:none item entirely, sliding everything after it back
   by one column instead of leaving its track empty. */
#gameActions {
    grid-column: 2;
    justify-self: center;
    gap: 10px;
}

/* See the comment above #navMenuDropdown:not([hidden]) - same reason. */
#gameActions:not([hidden]) {
    display: flex;
}

#gameActions button {
    padding: 6px 14px;
}

#acceptDrawBtn, #declineDrawBtn {
    background: rgb(159, 43, 104);
    color: white;
}

/* Plain/functional for now - a placeholder for the alchemical-hourglass
   styling planned for these later. */
#playerNames {
    justify-content: space-between;
    padding: 4px 15px 0;
    font-size: 0.95em;
    color: var(--text);
}

#playerNames:not([hidden]) {
    display: flex;
}

.playerSlot {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}

.playerNameLink {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--text);
    text-decoration: none;
}

.playerNameLink:hover .playerNameText {
    text-decoration: underline;
}

.playerAvatarWrap img, .playerAvatarWrap canvas {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    object-fit: cover;
    vertical-align: middle;
}

.playerNameText {
    font-size: 0.95em;
}

/* PC only (mouse + hover, not a touchscreen) - names/avatars were too small
   to read comfortably here; leave phone's sizing above untouched. */
@media (hover: hover) and (pointer: fine) {
    .playerAvatarWrap img, .playerAvatarWrap canvas {
        width: 32px;   /* radius 12px -> 16px */
        height: 32px;
    }

    .playerNameText {
        font-size: 1.1em;
    }
}

.playerTrophyRow {
    display: flex;
    gap: 4px;
    padding-left: 30px; /* lines up under the name text, past the 24px avatar + gap */
}

/* Sits inline in the top nav's account area (#avatarMenu already has its
   own gap between headerUserLabel/this/avatarMenuBtn), not stacked under
   a name - the 30px offset above is for the board's inline-avatar layout
   and doesn't apply here. */
#headerTrophyRow {
    padding-left: 0;
}

#headerTrophyRow .playerTrophyIcon {
    width: 24px;
    height: 24px;
}

.playerTrophyIcon {
    width: 16px;
    height: 16px;
    object-fit: contain;
}

#clocks {
    justify-content: space-between;
    padding: 4px 15px 8px;
    font-family: monospace;
    font-size: 1.3em;
    color: var(--muted);
}

/* See the comment above #navMenuDropdown:not([hidden]) - same reason. */
#clocks:not([hidden]) {
    display: flex;
}

#clocks .clockActive {
    color: var(--text);
    font-weight: bold;
}

/* Only shown for a human-vs-human game with a real opponent (see
   updateChatPanel in main.js) - same gating as #gameActions. */
#chatPanel {
    flex-direction: column;
    gap: 6px;
    margin: 10px 15px 0;
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    background: var(--panel);
}

/* See the comment above #navMenuDropdown:not([hidden]) - same reason. */
#chatPanel:not([hidden]) {
    display: flex;
}

/* Same accent as CAST_DOT_COLOR/the committed Strike-Raise button in
   render.js (rgb(159, 43, 104)) - keeps the chat alert's "color language"
   consistent with the rest of the board chrome instead of introducing a
   new one-off color. Timing matches .chatFlashIcon.flashing exactly (see
   below) so the panel border and the status-prompt icon blink in lockstep. */
@keyframes chatPanelFlashBorder {
    0%, 100% { border-color: var(--panel-border); box-shadow: none; }
    50% { border-color: rgb(159, 43, 104); box-shadow: 0 0 8px rgba(159, 43, 104, 0.6); }
}

#chatPanel.flashing {
    animation: chatPanelFlashBorder 0.5s ease-in-out 3;
}

#chatMessages {
    max-height: 140px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 3px;
    font-size: 0.9em;
}

#chatMessages .chatMsg {
    overflow-wrap: break-word;
}

#chatMessages .chatAuthor {
    font-weight: bold;
    color: var(--muted);
}

#chatInputRow {
    display: flex;
    gap: 6px;
}

#chatInputRow input {
    flex: 1;
    min-width: 0;
}

/* Sits directly under #chatPanel, same visual family (border/background/
   margin) so it reads as a continuation of it - collapsed, it's just
   #movesToggleBtn, sized to roughly match #chatInputRow's own height/
   width; open, #movesList drops down from it as an overlay (same
   absolute-positioned-dropdown pattern #navMenuDropdown/
   #displaySettingsDropdown already use) rather than pushing
   #liveGamesPanel down the page. */
#movesPanel {
    position: relative;
    margin: 10px 15px 0;
    padding: 0;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    background: var(--panel);
}

#movesPanel:not([hidden]) {
    display: block;
}

#movesToggleBtn {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    padding: 8px 12px;
    background: transparent;
    border: none;
    color: var(--text);
    font: inherit;
    cursor: pointer;
}

#movesList {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 4px;
    max-height: 220px;
    overflow-y: auto;
    grid-template-columns: auto 1fr 1fr;
    align-content: start;
    gap: 3px 8px;
    padding: 8px 12px;
    font-size: 0.9em;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    z-index: 20;
}

/* `display` must live here, not on the unconditional rule above - see
   the comment above #navMenuDropdown:not([hidden]) for why. */
#movesList:not([hidden]) {
    display: grid;
}

#movesEmpty {
    grid-column: 1 / -1;
    color: var(--muted);
}

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

.moveEntry {
    cursor: pointer;
    padding: 1px 4px;
    border-radius: 3px;
}

.moveEntry:hover {
    background: var(--panel-border);
}

/* Same accent color #acceptDrawBtn/#declineDrawBtn already use, for a
   consistent "this is the highlighted/active thing" language. */
.moveEntry.active {
    background: rgb(159, 43, 104);
    color: white;
}

/* ---- messages page ------------------------------------------------------ */

#messagesPage {
    gap: 20px;
    max-width: 1000px;
    margin: 30px auto 60px;
    padding: 0 15px;
    align-items: flex-start;
}

#messagesPage:not([hidden]) {
    display: flex;
}

#friendsColumn {
    width: 280px;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

#addFriendBox {
    display: flex;
    gap: 6px;
}

#addFriendInput {
    flex: 1;
}

#addFriendStatus {
    color: var(--muted);
    font-size: 0.85em;
    min-height: 1.2em;
    margin: 4px 0 0;
}

.requestRow {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
    font-size: 0.9em;
    border-bottom: 1px solid var(--panel-border);
}

.requestRow span {
    flex: 1;
}

#friendsColumn h3 {
    margin: 14px 0 4px;
    font-family: 'Cinzel Web', Georgia, serif;
    color: var(--muted);
    font-weight: normal;
    font-size: 0.95em;
}

.friendRow {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    border-radius: 4px;
    cursor: pointer;
    border: 1px solid transparent;
}

.friendRow:hover {
    background: var(--panel);
}

.friendRow.active {
    background: var(--panel);
    border-color: #3987e5;
}

.friendRow span:first-child {
    flex: 1;
}

.friendProfileLink {
    font-size: 0.8em;
    color: var(--muted);
    text-decoration: none;
}

.friendProfileLink:hover {
    text-decoration: underline;
}

#threadColumn {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    padding: 12px;
    min-height: 420px;
}

#threadHeader {
    margin: 0 0 10px;
    font-family: 'Cinzel Web', Georgia, serif;
    color: var(--text);
    font-weight: normal;
    border-bottom: 1px solid var(--panel-border);
    padding-bottom: 8px;
}

#threadMessages {
    flex: 1;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    max-height: 420px;
    margin-bottom: 10px;
}

.threadMsg {
    background: var(--bg);
    border-radius: 6px;
    padding: 6px 10px;
    max-width: 80%;
}

/* Staggered like an ordinary chat UI: the friend's messages stay left
   (the plain default look above), this player's own float right with a
   distinct accent background so a glance at the thread shows who said
   what without reading the timestamp/metadata. */
.threadMsgFriend {
    align-self: flex-start;
}

.threadMsgOwn {
    align-self: flex-end;
    background: rgb(90, 35, 60);
}

.threadMsgText {
    color: var(--text);
    overflow-wrap: break-word;
}

.threadMsgMeta {
    color: var(--muted);
    font-size: 0.75em;
    margin-top: 2px;
}

#threadInputRow {
    gap: 8px;
}

#threadInputRow:not([hidden]) {
    display: flex;
}

#threadInput {
    flex: 1;
    /* Text inputs have their own default min-width:auto floor (like the
       min-width:auto flex-item issue elsewhere in this file) - without
       this it wouldn't actually shrink to fit a narrow mobile row,
       pushing the Send button past the container's edge by however much
       narrower the screen is than the input's own intrinsic minimum. */
    min-width: 0;
}

#legalPage {
    max-width: 800px;
    margin: 30px auto 60px;
    padding: 0 15px;
    line-height: 1.5;
}

#legalPage h2 {
    font-family: 'Cinzel Web', Georgia, serif;
    color: var(--muted);
    border-bottom: 1px solid var(--panel-border);
    padding-bottom: 6px;
    margin-top: 32px;
}

#legalPage h3 {
    margin-bottom: 4px;
}

#legalNotice {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    padding: 10px 14px;
    color: var(--muted);
}

#rulesPage {
    max-width: 900px;
    margin: 30px auto 60px;
    padding: 0 15px;
}

#rulesPage h2 {
    font-family: 'Cinzel Web', Georgia, serif;
    color: var(--muted);
    border-bottom: 1px solid var(--panel-border);
    padding-bottom: 6px;
}

#rulesSummary li {
    margin-bottom: 10px;
    line-height: 1.4;
}

#rulebookFrame {
    width: 100%;
    height: 80vh;
    min-height: 500px;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    background: #fff;
}

/* ---- stats page --------------------------------------------------------- */

#statsPage {
    max-width: 900px;
    margin: 30px auto 60px;
    padding: 0 15px;
}

#statsUsername {
    font-family: 'Cinzel Web', Georgia, serif;
    margin-bottom: 4px;
}

#statTiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 12px;
    margin: 16px 0 28px;
}

.statTile {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 12px 14px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
}

.statLabel {
    font-size: 0.8em;
    color: var(--muted);
}

.statValue {
    font-size: 1.6em;
    font-weight: 600;
    /* proportional figures at display size, not tabular-nums - see
       marks-and-anatomy.md's stat-tile contract */
}

.viz-root {
    --chart-line: #3987e5;
    --chart-grid: var(--panel-border);
    --chart-text: var(--muted);
}

#chartHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
}

#chartHeader h3 {
    margin: 0;
    font-family: 'Cinzel Web', Georgia, serif;
    color: var(--muted);
    font-weight: normal;
}

#rangeFilters {
    display: flex;
    gap: 6px;
}

.rangeBtn {
    padding: 4px 10px;
    font-size: 0.85em;
    background: transparent;
    border: 1px solid var(--panel-border);
}

.rangeBtn.active {
    background: var(--panel);
    border-color: var(--chart-line);
    color: var(--text);
}

#chartContainer {
    position: relative;
    margin-top: 10px;
}

#ratingChart {
    width: 100%;
    height: auto;
    display: block;
}

.chartGridline {
    stroke: var(--chart-grid);
    stroke-width: 1;
}

.chartAxisLabel {
    fill: var(--chart-text);
    font-size: 11px;
}

.chartLine {
    stroke: var(--chart-line);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.chartMarkerRing {
    fill: var(--bg);
}

.chartMarkerDot {
    fill: var(--chart-line);
}

.chartEndLabel {
    fill: var(--text);
    font-size: 12px;
    font-weight: 600;
}

.chartCrosshair {
    stroke: var(--chart-grid);
    stroke-width: 1;
}

.chartHoverDot {
    fill: var(--chart-line);
    stroke: var(--bg);
    stroke-width: 2;
}

#chartTooltip {
    position: absolute;
    transform: translate(-50%, -130%);
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    padding: 4px 8px;
    pointer-events: none;
    white-space: nowrap;
    font-size: 0.85em;
}

.chartTooltipValue {
    font-weight: 600;
    color: var(--text);
}

.chartTooltipDate {
    color: var(--muted);
    font-size: 0.9em;
}

#chartEmptyState {
    color: var(--muted);
    text-align: center;
    padding: 40px 0;
}

#tableViewToggle {
    margin-top: 10px;
    font-size: 0.85em;
}

#ratingTable {
    width: 100%;
    border-collapse: collapse;
    margin-top: 10px;
    font-size: 0.9em;
}

#ratingTable th, #ratingTable td {
    text-align: left;
    padding: 6px 8px;
    border-bottom: 1px solid var(--panel-border);
    font-variant-numeric: tabular-nums;
}

#ratingTable th {
    color: var(--muted);
    font-weight: normal;
}

.placeholderPage {
    max-width: 1000px;
    margin: 60px auto;
    padding: 0 15px;
    text-align: center;
    color: var(--muted);
}

/* ---- profile page ------------------------------------------------------ */

#profilePage {
    max-width: 700px;
    margin: 30px auto 60px;
    padding: 0 15px;
}

#playerSearch {
    display: flex;
    gap: 8px;
    margin-bottom: 20px;
}

#playerSearchInput {
    flex: 1;
}

#signedOutMessage {
    text-align: center;
    color: var(--muted);
    margin-top: 60px;
}

#signedOutMessage a {
    color: var(--text);
}

#profileHeader {
    display: flex;
    align-items: center;
    gap: 20px;
    margin-bottom: 30px;
}

#avatarWrap {
    position: relative;
    width: 128px;
    height: 128px;
    flex: none;
}

#avatarImg, #avatarCanvas {
    width: 128px;
    height: 128px;
    border-radius: 50%;
    object-fit: cover;
    border: 2px solid var(--panel-border);
    /* pixelated identicon should stay crisp/blocky, not blurred when
       upscaled from its native canvas resolution */
    image-rendering: pixelated;
}

#avatarEditBtn {
    position: absolute;
    bottom: 0;
    right: 0;
    padding: 4px 10px;
    font-size: 0.8em;
    border-radius: 999px;
}

#profileInfo {
    flex: 1;
}

#profileUsername {
    margin: 0 0 6px;
    font-family: 'Cinzel Web', Georgia, serif;
    overflow-wrap: anywhere;
}

#profileRating {
    margin: 0 0 10px;
    color: var(--muted);
    font-size: 0.9em;
}

/* A plain #id{display:...} rule beats [hidden]'s built-in display:none
   regardless of specificity - see the #navMenuDropdown comment for the
   same gotcha (hit again here: this is exactly why the button kept
   showing on your own profile after a hard refresh). */
#addFriendRow {
    align-items: center;
    gap: 8px;
    margin: 0 0 10px;
}

#addFriendRow:not([hidden]) {
    display: flex;
}

#addFriendStatus {
    color: var(--muted);
    font-size: 0.85em;
}

/* Styled to match its sibling <button>s in #addFriendRow (see the
   `select, button, input` reset above) since it's a plain <a>, not a
   <button> - it needs to look identical despite navigating instead of
   firing a click handler. */
#messageFriendBtn {
    font-family: inherit;
    font-size: 0.95em;
    padding: 4px 8px;
    background: #33342a;
    color: var(--text);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    text-decoration: none;
    cursor: pointer;
}

#messageFriendBtn:hover {
    background: #40412f;
}

#bioText {
    margin: 0 0 6px;
    color: var(--muted);
    white-space: pre-wrap;
}

#bioEditor {
    flex-direction: column;
    gap: 8px;
}

#bioEditor:not([hidden]) {
    display: flex;
}

#countryRow {
    align-items: center;
    gap: 8px;
    margin: 0 0 10px;
}

#countryRow:not([hidden]) {
    display: flex;
}

#countryLabel {
    color: var(--muted);
    font-size: 0.9em;
}

#countryEditor {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    margin: 0 0 10px;
}

#countryEditor:not([hidden]) {
    display: flex;
}

#extinctStateHint {
    color: var(--muted);
    font-size: 0.85em;
    margin: 4px 0 0;
}

#countryEditorButtons {
    display: flex;
    gap: 8px;
    margin-top: 4px;
}

.extinctFlagIcon {
    height: 1em;
    width: auto;
    vertical-align: -0.15em;
}

#bioTextarea {
    width: 100%;
    resize: vertical;
    font-family: inherit;
}

#profilePage h3 {
    font-family: 'Cinzel Web', Georgia, serif;
    color: var(--muted);
    border-bottom: 1px solid var(--panel-border);
    padding-bottom: 6px;
}

#profileFriendsList {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 16px;
}

#profileFriendsList a {
    padding: 4px 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    color: var(--text);
    text-decoration: none;
    font-size: 0.9em;
}

#profileFriendsList a:hover {
    background: #40412f;
}

/* A plain div, not an <a> - it holds two SEPARATE links (opponent's
   profile vs. the game itself) side by side, since either needs to be
   independently clickable rather than one swallowing the other. */
.ledgerRow {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 8px 4px;
    border-bottom: 1px solid var(--panel-border);
}

.ledgerRow:hover {
    background: var(--panel);
}

.ledgerOpponent {
    flex: 1;
    color: var(--text);
    text-decoration: none;
}

a.ledgerOpponent:hover {
    text-decoration: underline;
}

.ledgerGameLink {
    display: flex;
    align-items: center;
    gap: 14px;
    color: var(--text);
    text-decoration: none;
}

.ledgerColor, .ledgerTimeControl, .ledgerDate {
    color: var(--muted);
    font-size: 0.9em;
}

.ledgerColor {
    display: inline-block;
    min-width: 4ch;
}

/* Reserves room for the longest current label ("1 day/move") so AI games
   (no time control at all, hence empty text here) still keep the Result
   and Date columns lined up with human games under any time control. */
.ledgerTimeControl {
    display: inline-block;
    min-width: 11ch;
}

.ledgerResult {
    font-weight: bold;
    min-width: 70px;
    text-align: center;
    padding: 2px 8px;
    border-radius: 4px;
}

.ledgerWin { background: #2d5a2d; color: #b6f2b6; }
.ledgerLoss { background: #5a2d2d; color: #f2b6b6; }
.ledgerDraw { background: #4a4a3a; color: #ddd; }
.ledgerOngoing { background: #33342a; color: var(--muted); }

/* On a touch device, a row's fixed-min-width columns (opponent + color +
   result + time-control + date) don't all fit the viewport - rather than
   let flex-shrink cram them down past legibility (the page itself can't
   scroll horizontally at all, see html's overflow-x: hidden), keep every
   column at its natural width and let them overflow. The scrollbar lives
   on #gamesLedger itself (one shared scroll position for the whole list,
   recent rows and filed year/month sections alike), not per-row - a
   scrollbar on every single row would drift independently and be far
   fiddlier to use than one shared one. #gamesLedger has no explicit
   height, so this never clips it vertically - only overflow-x actually
   triggers (see MDN's overflow-x note: setting only one axis auto still
   computes the other axis to auto too, but auto never shows a scrollbar
   on an axis with nothing to clip). Left off the desktop (mouse/trackpad)
   case entirely since nothing there reported this - .ledgerRow already
   fits comfortably at typical desktop widths. */
@media (pointer: coarse) and (hover: none) {
    #gamesLedger {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }

    .ledgerOpponent {
        flex: 0 0 auto;
        min-width: 90px;
    }

    .ledgerGameLink {
        flex-shrink: 0;
    }
}

.ledgerFiled {
    margin-top: 6px;
}

.ledgerSectionToggle {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    padding: 8px 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    color: var(--text);
    font: inherit;
    font-weight: bold;
    cursor: pointer;
}

.ledgerYearSection {
    margin-top: 6px;
}

.ledgerYearSection > .ledgerSectionBody {
    padding: 6px 0 0 14px;
}

.ledgerMonthSection {
    margin-top: 6px;
}

.ledgerMonthSection > .ledgerSectionToggle {
    font-weight: normal;
    color: var(--muted);
}

.ledgerMonthSection > .ledgerSectionBody {
    padding-top: 2px;
}

.badgeCategory {
    margin: 0 0 16px;
}

.badgeCategory h4 {
    margin: 0 0 2px;
}

.badgeCategorySubtitle {
    color: var(--muted);
    font-size: 0.85em;
    margin: 0 0 8px;
}

.badgeRow {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.badgeCell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    width: 88px;
    padding: 8px 4px;
    border-radius: 6px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    text-align: center;
}

.badgeIcon {
    font-size: 2em;
    line-height: 1;
}

.badgeIcon img {
    width: 1.6em;
    height: 1.6em;
    object-fit: contain;
}

.badgeName {
    font-size: 0.75em;
    color: var(--text);
}

.badgeLocked {
    opacity: 0.35;
    filter: grayscale(1);
}

.badgeLocked .badgeName {
    color: var(--muted);
}

/* ---- mobile/tablet layout -------------------------------------------- */
/*
   Mobile board mode is opt-in (a tapped fullscreen button, like YouTube's),
   not forced by orientation - a touch device in portrait just sees the
   ordinary page. `pointer: coarse` + `hover: none` is the "primary input is
   a finger" signal - true on phones/tablets of any size, false for a
   desktop window regardless of how it's resized (a mouse always reports
   pointer: fine, hover: hover) - so none of this can misfire for someone
   just shrinking their browser.

   #mobileFullscreenBtn/#mobileRotateOverlay/#mobileExitFullscreenBtn all
   live INSIDE #boardWrap in index.html specifically so the :fullscreen
   selector below can target them - only a fullscreened element's own
   descendants render in the fullscreen top-layer, so the rotate hint and
   exit button couldn't be siblings of #boardWrap even if that read more
   naturally in the markup.
*/

#mobileFullscreenBtn, #mobileRotateOverlay, #mobileExitFullscreenBtn, #mobileTitleBadge,
#mobileStrikeBtn, #mobileRaiseBtn, #mobilePrompt {
    display: none;
}

@media (pointer: coarse) and (hover: none) {
    #mobileFullscreenBtn {
        display: flex;
        position: absolute;
        right: 12px;
        bottom: 12px;
        z-index: 50;
        width: 40px;
        height: 40px;
        align-items: center;
        justify-content: center;
        font-size: 1.3em;
        border-radius: 50%;
        background: var(--panel);
        border: 1px solid var(--panel-border);
        color: var(--text);
    }
    /* Already fullscreen - no need to offer the button again. Matches
       both real fullscreen and the .fakeFullscreen fallback (mobile.js)
       used where the Fullscreen API itself isn't available at all
       (notably iPhone Safari - it only ever supported this API for
       <video>, on any iOS version - see mobile.js's own comment). */
    #boardWrap:fullscreen #mobileFullscreenBtn,
    #boardWrap.fakeFullscreen #mobileFullscreenBtn {
        display: none;
    }
    #boardWrap:fullscreen #mobileExitFullscreenBtn,
    #boardWrap.fakeFullscreen #mobileExitFullscreenBtn {
        display: flex;
        position: absolute;
        left: 12px;
        top: 12px;
        z-index: 50;
        width: 36px;
        height: 36px;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        background: var(--panel);
        border: 1px solid var(--panel-border);
        color: var(--text);
    }
    /* Small branding badge, top-right - the real header (with its full-
       size logo/title) isn't part of the fullscreen top-layer at all, so
       without this the board view would show no branding whatsoever. */
    #boardWrap:fullscreen #mobileTitleBadge,
    #boardWrap.fakeFullscreen #mobileTitleBadge {
        display: flex;
        position: absolute;
        right: 12px;
        top: 12px;
        z-index: 50;
        align-items: center;
        gap: 4px;
        font-size: 0.65em;
        color: var(--text);
        font-family: 'Cinzel Web', Georgia, serif;
    }
    #boardWrap:fullscreen #mobileTitleBadge img,
    #boardWrap.fakeFullscreen #mobileTitleBadge img {
        width: 18px;
        height: 18px;
    }
    /* Strike/Raise/prompt: main.js's positionMobileCastOverlay() sets the
       actual left/top/width/height on each of these directly (matching
       specific unplayable-square pixel rects from render-mobile.js's own
       cellRect()) - this block only holds what doesn't vary per-cell. */
    /* Same appearance for all three - the prompt is styled as the same
       kind of rectangle as the Strike/Raise buttons, just non-interactive.
       All three are sized/positioned by main.js's positionMobileCastOverlay
       as a padded, gapped rect within their general cluster of unplayable
       squares rather than snapped to individual cell boundaries - compare
       cellRect() usage elsewhere, which these deliberately don't use raw. */
    #boardWrap:fullscreen #mobileStrikeBtn,
    #boardWrap.fakeFullscreen #mobileStrikeBtn,
    #boardWrap:fullscreen #mobileRaiseBtn,
    #boardWrap.fakeFullscreen #mobileRaiseBtn,
    #boardWrap:fullscreen #mobilePrompt,
    #boardWrap.fakeFullscreen #mobilePrompt {
        display: flex;
        position: absolute;
        z-index: 40;
        align-items: center;
        padding: 2px 4px;
        font-size: 0.95em;
        color: var(--text);
    }
    /* A few px lower specifically on the .fakeFullscreen path (iPhone,
       where the real Fullscreen API isn't available at all - see
       mobile.js). A pure visual transform - unlike the plain JS offset
       tried before (which somehow made the whole board balloon in size,
       for a reason not yet understood), this can't feed back into any
       canvas-size measurement since it never changes what
       getBoundingClientRect() reports for layout purposes, only where the
       element paints. Real fullscreen (Android) is untouched. */
    #boardWrap.fakeFullscreen #mobileStrikeBtn,
    #boardWrap.fakeFullscreen #mobileRaiseBtn,
    #boardWrap.fakeFullscreen #mobilePrompt {
        transform: translate(-7px, 7px);
    }
    /* Strike/Raise keep their original rounded shape - lightened a step
       past var(--panel) (the same lighter olive used for ordinary
       dropdowns), and always kept lighter than #mobilePrompt below. */
    #boardWrap:fullscreen #mobileStrikeBtn,
    #boardWrap.fakeFullscreen #mobileStrikeBtn,
    #boardWrap:fullscreen #mobileRaiseBtn,
    #boardWrap.fakeFullscreen #mobileRaiseBtn {
        justify-content: center;
        text-align: center;
        white-space: nowrap;
        border-radius: 4px;
        background: rgb(88, 91, 73);
        border: 1px solid rgb(110, 113, 95);
        /* Same font/weight/color as desktop's own canvas-drawn Strike/
           Raise buttons (render.js's drawCastButtons: '600 20px "Cinzel
           Web", Georgia, serif', pure white) - main.js sets an actual px
           font-size scaled to the current cell size, same idea as
           #mobilePrompt's own dynamic sizing. */
        font-family: 'Cinzel Web', Georgia, serif;
        font-weight: 600;
        color: #fff;
    }
    /* Console-panel look: sharp corners, and left-justified text with a
       little padding after the left edge instead of centered. Lightened a
       step from its original near-black, but stays deliberately darker
       than the Strike/Raise buttons above (rgb(88,91,73)) regardless of
       any further tweak to either - that contrast is the point of the
       "console" look. */
    #boardWrap:fullscreen #mobilePrompt,
    #boardWrap.fakeFullscreen #mobilePrompt {
        justify-content: flex-start;
        text-align: left;
        padding-left: 10px;
        border-radius: 0;
        background: rgb(52, 52, 52);
        border: 1px solid rgb(80, 80, 80);
    }
    /* Shows which one is the currently-committed cast action (main.js
       toggles this from ui.committedButton) - same idea as desktop's own
       pressed/active button styling, so tapping Strike/Raise gives visible
       feedback instead of two buttons that always look identical. */
    #boardWrap:fullscreen #mobileStrikeBtn.active,
    #boardWrap.fakeFullscreen #mobileStrikeBtn.active,
    #boardWrap:fullscreen #mobileRaiseBtn.active,
    #boardWrap.fakeFullscreen #mobileRaiseBtn.active {
        background: rgb(159, 43, 104);
        border-color: rgb(159, 43, 104);
        color: #fff;
    }
    /* Status text (computeStatus() in main.js) ranges from "White to
       move." up to 62-character sentences like "Choose cards from your
       deck and from the board that sum to 21." - nothing close to that
       fits on one line at a fixed font size in a 5-cell-wide box, so this
       wraps (positionMobileCastOverlay sets an actual px font-size scaled
       to the current cell size, tight line-height, to fit up to 2 lines
       for the longest real message instead of a static em guess that
       would only happen to work at one particular cell size). overflow
       stays hidden as a last-resort clamp only. */
    #boardWrap:fullscreen #mobilePrompt,
    #boardWrap.fakeFullscreen #mobilePrompt {
        white-space: normal;
        line-height: 1.15;
        overflow: hidden;
    }

    /* Fullscreen was requested but the device is still portrait - only
       reachable on a platform without orientation-lock support (iOS
       Safari), since elsewhere the lock already forces landscape the
       instant fullscreen is entered. Applies to the .fakeFullscreen
       fallback too - there's no orientation-lock attempt at all in that
       path (mobile.js), so this rotate prompt is the PRIMARY way (not
       just a rare edge case) a fake-fullscreen player gets into landscape. */
    @media (orientation: portrait) {
        #boardWrap:fullscreen #mobileRotateOverlay,
        #boardWrap.fakeFullscreen #mobileRotateOverlay {
            display: flex;
            position: fixed;
            inset: 0;
            z-index: 9999;
            align-items: center;
            justify-content: center;
            text-align: center;
            padding: 24px;
            background: var(--bg);
            color: var(--text);
            font-size: 1.3em;
        }
    }

    /* Two rows instead of one: title centered on top, hamburger (left) and
       bell/avatar/username (right) below it - rather than all of it
       competing for one line. #navMenu is a direct child of <header> here
       (see the isTouchDevice() move in main.js) specifically so it can get
       its own named grid area independently of #headerLeft's title/logo;
       grid-area only applies to a grid container's own direct children.
       #authPanel and #accountHeader share the "account" cell since exactly
       one of them is ever visible (signed out vs. signed in). */
    header {
        display: grid;
        /* auto (not 1fr) for the nav column - the hamburger only needs its
           own small width, and the "account" column (email/password/Sign
           Up/Log In, or the signed-in avatar area) needs nearly the full
           row instead of being capped at half of it, which was forcing
           email+password+both buttons to wrap even before "Forgot
           password?" got a chance to be the one thing that wraps. */
        /* minmax(0, 1fr), not bare 1fr - a bare <flex> track has its own
           automatic minimum size (auto, i.e. the largest min-content of
           anything placed in it), completely separate from any min-width
           set on the items themselves. That's what let the "account"
           column - and everything in it, avatar included - grow past the
           header's real row width on a long username no matter what
           min-width:0 we put on the items inside it; minmax(0, 1fr) is
           the standard fix, overriding the TRACK's own floor instead of
           just the item's. */
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-areas:
            "title title"
            "nav account";
        row-gap: 6px;
        padding: 10px 16px;
    }
    #headerLeft {
        grid-area: title;
        justify-content: flex-start;
    }
    #navMenu {
        grid-area: nav;
        justify-self: start;
        align-self: center;
    }
    /* #authPanel/#accountHeader share this cell (exactly one is ever
       visible - signed out vs. signed in) rather than #authPanel getting
       its own row, so the sign-in row stays in line with the hamburger. */
    #authPanel, #accountHeader {
        grid-area: account;
        justify-self: end;
        align-self: center;
        /* Grid/flex items default to min-width:auto, which floors their
           size at their content's min-content width - for a nowrap
           username that's the FULL unwrapped text, so without this the
           "account" track (and everything in it, avatar included) was
           still being forced wider than the viewport to fit it, despite
           #avatarMenu's own max-width below; min-width:0 is what actually
           lets the grid track - and everything nested in it - shrink
           past that floor. */
        min-width: 0;
    }
    /* Email + password + Sign Up + Log In stay on one line; only "Forgot
       password?" (the widest single label) wraps onto its own line below,
       via flex-basis:100% forcing a break inside #authPanel's own wrapping
       flex row - freeing enough width on the first line for the inputs
       above to grow a bit versus trying to fit all 5 controls at once. */
    #authPanel {
        flex-wrap: wrap;
        justify-content: flex-end;
        gap: 4px;
    }
    #authEmail {
        width: 80px;
    }
    #authPassword {
        width: 65px;
    }
    #authPanel button {
        padding: 4px 6px;
        font-size: 0.8em;
    }
    #forgotPasswordBtn {
        flex-basis: 100%;
        text-align: center;
    }
    #authStatus {
        flex: 0 1 auto;
        overflow-wrap: anywhere;
    }
    /* Flag+name stay put; trophies (however many) drop to their own row
       underneath instead of competing for width on the name's line; the
       avatar becomes an absolutely-positioned circle flush against the
       menu's right edge instead of a flex sibling, so a long username
       truncates (ellipsis) at the container edge rather than pushing the
       avatar off-screen, and a short one just leaves the corner empty -
       either way nothing here can force the header wider than the
       viewport. min-height reserves room for the avatar's own height
       since pulling it out of flow (position:absolute) would otherwise
       let the row collapse to just the text's line-height and the avatar
       spill below the box. */
    #accountHeader {
        min-width: 0;
    }
    #avatarMenu {
        position: relative;
        flex-direction: column;
        align-items: stretch;
        gap: 2px;
        min-width: 0;
        max-width: 55vw;
        min-height: 46px;
    }
    #headerUserLabel {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        /* Two steps down from desktop's 1.1em base. */
        font-size: 0.9em;
        /* headerUserLabel is itself a flex item (of #avatarMenu's column),
           so it has the same min-width:auto floor problem as its
           ancestors - without this, its own nowrap text's min-content
           width wins over being stretched down to the container's capped
           width, and it overflows past #avatarMenu's edge instead of
           eliding. */
        min-width: 0;
    }
    /* The flag inherits font-size from #headerUserLabel like the rest of
       the name text (it's an emoji glyph, or - for an extinct state - an
       <img> sized via height:1em) - counteract the 0.9em reduction above
       (1.1/0.9) so the flag renders back at its original absolute size
       while the actual username text stays at the new smaller one. */
    #headerUserLabel .flagIcon {
        font-size: 1.22em;
    }
    /* Shifted left as a group - position:relative (not a margin/flow
       change) so nothing reflows and #avatarMenuBtn (positioned relative
       to #avatarMenu's own, untouched box) stays exactly where it was. */
    #notifBell, #headerUserLabel, #headerMetaRow {
        position: relative;
        right: 50px;
    }
    #avatarMenuBtn {
        position: absolute;
        top: 0;
        right: 0;
        z-index: 2;
        background: var(--panel);
    }
    #avatarMenuBtn img, #avatarMenuBtn canvas {
        width: 42px;
        height: 42px;
    }
    /* Rating + trophies share this second line, under the name/flag - see
       index.html's comment on why this wrapper exists (desktop leaves it
       as display:contents, defined near #avatarMenu). position+z-index
       above #avatarMenuBtn so if the bigger avatar's own height spills
       down into this row's space, the trophies stay visible on top of it
       rather than getting hidden behind it. */
    #headerMetaRow {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px;
        position: relative;
        z-index: 3;
    }
    #headerRatingText {
        font-size: 0.95em;
    }
    #headerTrophyRow {
        flex-wrap: wrap;
    }

    /* #displaySettings gets reparented into #navMenuDropdown by main.js on
       touch devices (see the isTouchDevice() check there) - style it here
       as a normal item in that list rather than the floating dropdown it is
       on desktop. */
    #navMenuDropdown #displaySettings {
        position: static;
        border-top: 1px solid var(--panel-border);
        margin-top: 4px;
        padding-top: 6px;
    }
    #navMenuDropdown #displaySettingsBtn {
        width: 100%;
        text-align: left;
        background: none;
        border: none;
    }
    #navMenuDropdown #displaySettingsDropdown {
        position: static;
        padding: 6px 0 0 12px;
    }
    #navMenuDropdown #displaySettingsDropdown:not([hidden]) {
        display: block;
    }

    /* The "Play vs Human" row (label + challenge form) is wide enough on
       desktop to sit on one line - on a narrow phone it needs to stack, and
       the form's own fields need to wrap rather than force a horizontal
       scroll. */
    .playModeBand {
        flex-direction: column;
        align-items: stretch;
    }
    .playModeBand h2 {
        min-width: 0;
    }
    /* "AI plays:" and its <select> were landing right on top of each
       other with zero breathing room when the label's own inline content
       wrapped (a real phone's native <select> is wider than anything
       Chrome DevTools shows, so this reliably wraps there even when it
       doesn't in DevTools) - just a small gap so the two don't visually
       touch, not a structural change to this row's layout. */
    #newGamePanel label select {
        margin-top: 4px;
    }
    #challengeSend {
        flex-wrap: wrap;
    }
    /* flex: 1 1 auto (the first pass at this) stretched every field to fill
       whatever space was left on its line, even though a <select>/<button>
       already sizes itself to its own text - that's what was reading as
       "hogging empty space". flex: 0 1 auto lets each one size to its own
       content and only shrink if truly needed, instead of growing to fill
       the row. #challengeUsername is the one exception - a plain text
       input has no content to size itself to, so it still needs an
       explicit width. */
    #challengeSend input, #challengeSend select, #challengeSend button {
        flex: 0 1 auto;
    }
    #challengeUsername {
        width: 130px;
    }

    /* Desktop's rigid 3-column grid (display/flip | resign|draw | history
       arrows) doesn't have room to also hold Display Settings on a narrow
       screen even after it moves into the nav menu above - switching to a
       wrapping flex row lets whichever groups don't fit drop to a second
       line instead of overflowing/clipping. grid-column/justify-self on the
       children are simply inert once the parent isn't display:grid, so
       nothing else needs to change here. */
    #belowBoard {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: 8px 12px;
        padding: 0 8px;
    }

    /* Profile page: avatar sat beside the username/rating/country block on
       desktop, leaving that block only ~200px wide next to a fixed 128px
       avatar - a long username had nowhere to go but off-screen. Stacking
       avatar-over-data (and centering everything under it) uses the full
       width for the text instead. */
    #profileHeader {
        flex-direction: column;
        align-items: center;
        text-align: center;
    }
    #profileInfo {
        width: 100%;
    }
    #countryRow, #addFriendRow {
        justify-content: center;
    }

    /* Messages page: #friendsColumn is a fixed 280px that refuses to
       shrink (flex-shrink:0), sitting beside #threadColumn in a side-by-
       side row - on a narrow phone that alone overflows before anything
       else even comes into it. Stacking them (friends list on top, the
       open thread below) instead of side-by-side fixes it at the source. */
    #messagesPage {
        flex-direction: column;
    }
    #friendsColumn {
        width: 100%;
    }
    #threadColumn {
        width: 100%;
        min-height: 300px;
    }

    /* #boardWrap while fullscreen (mobile board mode - see main.js's
       isMobileBoardActive()-gated rendering/hit-testing and render-
       mobile.js). Explicit rather than relying on the browser's own
       fullscreen UA defaults, which aren't consistent about whether author
       width/height rules win - main.js's own sizing math assumes this
       fills the true viewport. overflow-y:auto lets belowBoard/chat/moves/
       liveGames (still their own rows below the canvas at this stage - see
       the approved mobile plan's later phases) be reached by scrolling
       inside the fullscreen view instead of being clipped or fighting the
       outer document for scroll. */
    @media (orientation: landscape) {
        #boardWrap:fullscreen,
        #boardWrap.fakeFullscreen {
            width: 100vw;
            max-width: 100vw;
            height: 100vh;
            margin: 0;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        /* Real Fullscreen API elements are automatically pulled out of
           normal document flow and pinned to the viewport by the browser
           itself (the whole point of the "top layer" fullscreen puts an
           element in) - .fakeFullscreen never gets that for free since
           it's just an ordinary class, so without this #boardWrap would
           still sit wherever it naturally falls in the page (below the
           header, etc.) instead of at the true top of the screen, which
           throws off everything positioned relative to it (Strike/Raise/
           the console, all measured from the canvas's own on-page
           offset). */
        #boardWrap.fakeFullscreen {
            position: fixed;
            inset: 0;
            z-index: 1000;
            /* Real :fullscreen elements paint opaque by default (the "top
               layer" replaces the rest of the page entirely) - this is
               still an ordinary element in normal paint order, so without
               an explicit background whatever sits behind it on the page
               shows straight through. */
            background: var(--bg);
        }
        /* The canvas is a fixed pixel size (main.js sets it directly - see
           syncMobileCanvasResolution), so a plain block row would leave it
           hugging the left edge with the leftover width (the board is
           height-bound on a wide landscape screen) sitting unused on the
           right. Two equal flex:1 graveyard panels either side of the
           fixed-width canvas both use that space AND keep the canvas
           centered as a natural side effect - no separate centering rule
           needed on the canvas itself. */
        /* align-items:stretch (the default - listed explicitly since it's
           load-bearing) is what gives the side panels a real, definite
           height matching the canvas's own explicit pixel height. Without
           it (this used to be flex-start), .mobileGravePanel's own
           height:100% had no definite parent height to resolve against and
           silently computed to "auto" instead - sized to its own content
           regardless of the canvas, which is why the deck list had no
           real height budget to divide 13 cards into and ran past the
           bottom of the screen instead of ever being constrained to fit. */
        #boardWrap:fullscreen #mobileBoardRow,
        #boardWrap.fakeFullscreen #mobileBoardRow {
            display: flex;
            align-items: stretch;
            width: 100%;
        }
        /* Deck and graveyard sit side by side (not stacked) now that the
           deck cards are narrow - deck innermost (next to the board),
           graveyard outermost (toward the screen edge), since that reads
           more naturally than the reverse. Both panels share the same DOM
           order (deck, then grave), so which physical direction is
           "outermost" flips depending on which side of the board this is -
           row-reverse on the left panel is what achieves that without
           needing to actually reorder the DOM per side. */
        #boardWrap:fullscreen .mobileGravePanel,
        #boardWrap.fakeFullscreen .mobileGravePanel {
            display: flex;
            flex: 1;
            align-items: center;
            justify-content: center;
            gap: 6px;
            height: 100%;
            min-width: 0;
            overflow: hidden;
        }
        #boardWrap:fullscreen #mobileSideLeft,
        #boardWrap.fakeFullscreen #mobileSideLeft {
            flex-direction: row-reverse;
        }
        #boardWrap:fullscreen #mobileSideRight,
        #boardWrap.fakeFullscreen #mobileSideRight {
            flex-direction: row;
        }
        #boardWrap:fullscreen .mobileDeckList,
        #boardWrap.fakeFullscreen .mobileDeckList {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 2px;
            flex: 1;
            min-width: 0;
            /* Now resolves against .mobileGravePanel's real, definite
               height (see the #mobileBoardRow comment above) instead of
               "auto" - this is what makes each card's own flex:1 below a
               true fraction of the actual on-screen height rather than an
               unconstrained content size. */
            height: 100%;
        }
        /* flex:1 + min-height:0 divides the deck list's real height evenly
           across all 13 cards - guaranteed to fit exactly, on any device,
           rather than a fixed guessed pixel height that happened to work
           for one screen size and overflowed on another. aspect-ratio then
           derives each card's WIDTH from that computed height - matched to
           card_back.png's own real 680x336 (2.02:1) ratio specifically,
           since that's CARD_BACK_PRESETS[0], the default theme's (Green)
           card back, not an in-between guess. The other card-back art
           (dead_card0.png, 881x561, ~1.57:1 - shown on the Brown/Gray
           themes) will letterbox a bit more via object-fit:contain since
           it doesn't match this ratio as closely, but that trades off
           correctness for the theme most people will actually see by
           default against a theme most won't, rather than fitting neither
           particularly well. */
        #boardWrap:fullscreen .mobileDeckCard,
        #boardWrap.fakeFullscreen .mobileDeckCard {
            flex: 1;
            min-height: 0;
            width: auto;
            aspect-ratio: 680 / 336;
            max-width: 100%;
        }
        /* Wraps the graveyard slots + that side's emblem into one vertical
           unit (deck's sibling in the row above) - a separate element from
           #mobileGraveBlack/White themselves since mobile-panels.js
           rebuilds THEIR innerHTML wholesale on every capture, which would
           wipe out an emblem placed directly inside them. */
        #boardWrap:fullscreen .mobileGraveColumn,
        #boardWrap.fakeFullscreen .mobileGraveColumn {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 6px;
            flex: 1;
            min-width: 0;
            height: 100%;
        }
        /* Back to a plain vertical stack (not a wrapping grid) - now that
           it's a dedicated column beside (not sharing a row with) the
           deck, there's room for one straightforward file of slots. */
        #boardWrap:fullscreen .mobileGraveGrid,
        #boardWrap.fakeFullscreen .mobileGraveGrid {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 2px;
        }
        /* This rule's own align-items:center (above) sizes
           EVERY direct child to its own content width by default, not the
           full 100vw. #playerNames/#clocks are deliberately NOT listed
           here - the board itself (not the full screen) is what they
           should align with, so main.js's syncMobileCanvasResolution()
           sets their width in JS to match the canvas's own computed width
           exactly, rather than a blanket 100% here (which would stretch
           them across the whole screen, side panels included). Everything
           below genuinely does want the full row, and none of them were
           reported broken yet, but they share the identical setup, so
           there's no reason to wait for each to surface separately. */
        #boardWrap:fullscreen #belowBoard,
        #boardWrap.fakeFullscreen #belowBoard,
        #boardWrap:fullscreen #chatPanel,
        #boardWrap.fakeFullscreen #chatPanel,
        #boardWrap:fullscreen #movesPanel,
        #boardWrap.fakeFullscreen #movesPanel,
        #boardWrap:fullscreen #liveGamesPanel,
        #boardWrap.fakeFullscreen #liveGamesPanel {
            width: 100%;
        }
    }
}

/* Hidden outside mobile board mode - see the rules above for when these
   actually show. */
.mobileGravePanel {
    display: none;
}

.mobileEmblem {
    width: 40px;
    height: 40px;
    object-fit: contain;
    opacity: 0.85;
    flex-shrink: 0;
}

/* Base layout ported from the old mobile/ prototype's own .deckCard
   (mobile/style.css, since retired) - a plain rank+suit row. That
   prototype's cast interaction never went through the deck display itself
   (a separate precomputed move-list did the actual casting), so this stays
   display-only here too for now.
   Fixed narrow width (rank+suit is only ever 2-3 characters - "10♣" is the
   widest) rather than stretching to fill its column - space-between made
   sense when this was one wide row, but with a single rank+suit text node
   there was never a second thing to actually space apart, so it was a
   no-op even before this got narrower. Padding-block is generous relative
   to the font size specifically to make each row a comfortable tap target
   once these become clickable (a later change), not just readable. */
.mobileDeckCard {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    /* Actual width/height come from the mobile-only rule above (flex:1 +
       aspect-ratio, dividing the deck list's real available height) - this
       block only holds the parts that don't depend on that (this element
       is display:none outside mobile mode anyway via .mobileGravePanel's
       own default, so none of this is visible until then regardless). */
    padding: 2px 2px 2px 4px;
    /* Sharp corners, not rounded - a used slot's <img> below is set to
       fill 100%/100% of this box, but the padding above still left a
       visible strip of the slot's own background color around the image
       on every side, and the border-radius clipped its corners too,
       showing more of that background peeking through there. Corners are
       fixed here; the padding itself is zeroed out specifically for used
       slots below, since unused slots still need it to breathe around
       the rank/suit text. */
    border-radius: 0;
    font-size: 0.62em;
    /* No background here - mobile-panels.js sets it per card (white =
       theme.bgDark, black = theme.bgLight, matching desktop's drawDecks
       exactly rather than a fixed color of its own). Text is plain black
       (not var(--text), which is a light color meant for dark panels) to
       match desktop's own rank/suit text color and stay readable against
       these light theme-square backgrounds. */
    color: #000;
}

/* A used slot shows the real card-back art as a CSS background-image
   (mobile-panels.js) instead of text - background-size:100% 100% resolves
   directly against this element's own padding-box (default
   background-origin), filling it exactly regardless of any padding, with
   none of the nested-<img>-with-object-fit sizing quirks that used to
   leave (or seem to leave, depending on which browser's DevTools you
   asked) a visible gap around the art. */
.mobileDeckCard.used {
    background-repeat: no-repeat;
}

/* Same idea as desktop's drawClickedDeckCards teal glow - a card currently
   in the in-progress cast combo (main.js's clickedCards). */
.mobileDeckCard.selected {
    outline: 2px solid rgb(100, 216, 220);
    outline-offset: -2px;
}

.mobileGraveSlot {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
}

.mobileGraveSlot img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* A new chat message needs to catch your eye even while #chatPanel (an
   ordinary in-flow element) is scrolled out of view - rather than a
   separate floating element (tried first, dropped: two "is this visible"
   states to keep in sync with the board's own fullscreen/mobile logic),
   this lives right on the existing status prompt every player is already
   looking at - #statusLineText on desktop, #mobilePromptText in mobile
   board mode. Both prompts already set their OWN text via
   `.textContent =` (main.js) - putting this icon in the same DOM node
   would get wiped out on every status update, hence the separate
   permanent inner text span in each (statusLineText/mobilePromptText)
   introduced alongside this, with main.js now targeting those instead of
   the outer prompt element directly. */
.chatFlashIcon {
    font-size: 1.1em;
}

/* #statusLine keeps its existing centered text exactly as before -
   position:relative + an absolutely-positioned icon child doesn't affect
   that at all, since taking the icon out of normal flow is the whole
   point (a flex-based "push to the end" layout would also re-center the
   text off-center once a second item joins it). */
#statusLine {
    position: relative;
}

#statusLineChatFlash {
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-50%);
}

/* #mobilePrompt is already `display: flex` (see the touch-mode rules
   below) - margin-left: auto on a flex item pushes just that item to the
   far end of the row, the standard flexbox way to do this, no extra
   positioning math needed. */
#mobileChatFlash {
    margin-left: auto;
}

@keyframes chatFlashBlink {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.15; }
}

/* Three blinks, synced with the chat notification sound (main.js) - class
   is added and removed by JS rather than left on permanently, so it only
   ever plays through once per new message rather than looping. */
.chatFlashIcon.flashing {
    animation: chatFlashBlink 0.5s ease-in-out 3;
}

