/* ========== Home / Landing ========== */

.landing {
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
    color: white;
    padding: 20px;
}

.landing-content { text-align: center; max-width: 400px; }
.landing-content h1 { font-size: 3rem; font-weight: 700; margin-bottom: 1rem; letter-spacing: -0.02em; }
.landing-content p { font-size: 1.1rem; opacity: 0.8; margin-bottom: 2rem; line-height: 1.6; }

/* Momentary /home redirect placeholder — theme-aware so a light-mode user never sees a dark flash. */
.home-redirect {
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    color: var(--text);
    padding: 20px;
}

.home-redirect p { font-size: 1.1rem; color: var(--text-secondary); }

/* Inline save/validation error — visually distinct from muted helper text. Theme-aware. */
.field-error { color: var(--danger-text); font-size: 0.85rem; margin-top: 6px; }

/* ========== OAuth Select ========== */

.oauth-container {
    background: var(--bg-surface);
    border-radius: 16px;
    box-shadow: 0 20px 40px var(--shadow-lg);
    padding: 40px 24px;
    max-width: 420px;
    width: 100%;
    margin: 60px auto;
}

.oauth-container .header { text-align: center; margin-bottom: 32px; }
.oauth-container .header h1 { color: var(--text-heading); font-size: 2rem; font-weight: 700; margin-bottom: 8px; }
.oauth-container .header p { color: var(--text-secondary); font-size: 1rem; }
.oauth-container .providers { display: flex; flex-direction: column; gap: 12px; }

.oauth-error {
    background: var(--danger-bg, rgba(198, 40, 40, 0.12));
    color: var(--danger, #c62828);
    border: 1px solid var(--danger, #c62828);
    border-radius: 10px;
    padding: 12px 14px;
    margin-bottom: 20px;
    font-size: 0.9rem;
    text-align: center;
}

.provider-button {
    display: flex; align-items: center; padding: 14px 16px;
    border: 2px solid var(--border); border-radius: 10px;
    text-decoration: none; font-size: 1rem; font-weight: 500;
    transition: all 0.2s; background: var(--bg-surface); color: var(--text);
    cursor: pointer;
}
.provider-button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px var(--shadow-lg); }
.github:hover { border-color: #333; background: #333; color: white; }
.microsoft:hover { border-color: #0078d4; background: #0078d4; color: white; }
.google:hover { border-color: #db4437; background: #db4437; color: white; }

.provider-icon { width: 24px; height: 24px; margin-right: 12px; flex-shrink: 0; }
.provider-text { flex: 1; text-align: left; }

.remember-method {
    display: flex; align-items: center; gap: 10px;
    margin-top: 20px; cursor: pointer;
    color: var(--text-secondary); font-size: 0.9rem;
}
.remember-method input { width: 18px; height: 18px; flex-shrink: 0; accent-color: var(--accent); cursor: pointer; }

.oauth-container .footer { text-align: center; margin-top: 24px; color: var(--text-secondary); font-size: 0.85rem; }

/* Password sign-in form: "or" divider, labelled inputs and a primary-styled submit, all matching
   the OAuth provider buttons above it. */
.password-login { margin-top: 20px; }
.password-divider {
    display: flex; align-items: center; text-align: center;
    color: var(--text-secondary); font-size: 0.85rem; margin: 0 0 16px;
}
.password-divider::before, .password-divider::after {
    content: ""; flex: 1; height: 1px; background: var(--border);
}
.password-divider span { padding: 0 12px; white-space: nowrap; }
.password-login .field-label { color: var(--text); }
.password-login .text-input {
    width: 100%; padding: 12px 14px; font-size: 0.95rem;
    border: 1px solid var(--border); border-radius: 10px;
    background: var(--bg-surface); color: var(--text);
    margin-bottom: 14px;
}
.password-login .text-input:focus {
    outline: none; border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}
.password-submit {
    justify-content: center; width: 100%; margin-top: 4px;
    background: var(--accent); border-color: var(--accent); color: white;
}
.password-submit:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: white; }
.password-remember {
    display: flex; align-items: center; gap: 8px; margin: 12px 0 4px;
    font-size: 0.85rem; color: var(--text-secondary); cursor: pointer;
}
.password-remember input { width: auto; margin: 0; flex: none; }

/* OAuth page needs dark background even in light mode */
body:has(.oauth-container) { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); }

/* ========== Wall List ========== */

.wall-list { padding: 16px; max-width: var(--page-max, 600px); margin: 0 auto; }

.list-header {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 16px;
}
.list-header h1 { font-size: 1.5rem; font-weight: 700; color: var(--text-heading); }

.btn-create {
    padding: 10px 20px; background: var(--accent); color: white;
    text-decoration: none; border-radius: 8px; font-weight: 600;
    font-size: 0.9rem;
}

.join-section { display: flex; gap: 8px; margin-bottom: 20px; }
.join-input {
    flex: 1; padding: 10px 14px; border: 2px solid var(--border);
    border-radius: 8px; font-size: 0.9rem; outline: none;
    background: var(--input-bg); color: var(--text);
}
.join-input:focus { border-color: var(--secondary); }
.btn-join {
    padding: 10px 20px; background: var(--secondary); color: white;
    border: none; border-radius: 8px; font-weight: 600; cursor: pointer;
}
.btn-join:disabled { opacity: 0.5; cursor: not-allowed; }

.wall-cards { display: flex; flex-direction: column; gap: 12px; }

.wall-card {
    display: flex; gap: 12px; padding: 12px; background: var(--bg-surface);
    border-radius: 12px; box-shadow: 0 2px 8px var(--shadow-md);
    text-decoration: none; color: inherit; transition: transform 0.15s;
}
.wall-card:hover { transform: translateY(-2px); box-shadow: 0 4px 16px var(--shadow-lg); }

.wall-card-photo {
    width: 80px; height: 80px; border-radius: 8px; overflow: hidden;
    background: var(--bg-surface-alt); flex-shrink: 0;
}
.wall-card-photo img { width: 100%; height: 100%; object-fit: cover; }
.no-photo {
    width: 100%; height: 100%; display: flex; align-items: center;
    justify-content: center; color: var(--text-secondary); font-size: 0.75rem;
}

.wall-card-info { display: flex; flex-direction: column; justify-content: center; }
.wall-card-info h3 { font-size: 1.1rem; font-weight: 600; color: var(--text-heading); margin: 0; }
.member-count { font-size: 0.85rem; color: var(--text-secondary); margin-top: 4px; }

.empty-state {
    text-align: center; padding: 48px 20px; color: var(--text-secondary);
    background: var(--bg-surface); border-radius: 12px;
}

/* ========== Image Stitcher ========== */

.stitcher-page {
    padding: 12px;
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 12px;
    height: calc(100dvh - var(--topbar-h) - var(--bottomnav-h));
    min-height: 500px;
}
.stitcher-toolbar {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}
.stitcher-toolbar .stitcher-spacer { flex: 1; }
.stitcher-upload {
    width: auto;
    padding: 10px 16px;
    margin: 0;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.stitcher-upload input[type="file"] { display: none; }
.stitcher-viewport {
    flex: 1 1 0;
    min-height: 0;
    position: relative;
    background: var(--bg-surface-alt);
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
    touch-action: none;
    user-select: none;
    cursor: grab;
}
.stitcher-viewport:active { cursor: grabbing; }
/* The transform itself is written by wwwroot/js/viewport.js (transform model) and
   deliberately not rendered by Blazor, so a late re-render can't stomp a gesture. */
.stitcher-world {
    position: absolute;
    left: 0;
    top: 0;
    width: 0;
    height: 0;
    transform-origin: 0 0;
}
.stitcher-layer {
    pointer-events: auto;
    -webkit-user-drag: none;
    user-select: none;
}
.stitcher-layer.selected { outline: none; }
.stitcher-overlay {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: visible;
}

.stitcher-zoom {
    position: absolute;
    right: 10px;
    bottom: 10px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    z-index: 5;
}
.stitcher-zoom .fab-btn {
    width: 36px;
    height: 36px;
    background: rgba(22, 22, 42, 0.85);
    color: white;
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 50%;
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(0,0,0,0.25);
    display: flex;
    align-items: center;
    justify-content: center;
}
.stitcher-zoom .fab-btn:hover { background: rgba(22, 22, 42, 1); }

.stitcher-layer-controls {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}
.stitcher-layer-controls .stitcher-spacer { flex: 1; }

.opacity-slider {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 12px;
    background: var(--bg-surface-alt);
    border-radius: var(--r-pill);
    border: 1px solid var(--border);
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--text-secondary);
}
.opacity-slider input[type="range"] {
    width: 120px;
    accent-color: var(--accent);
}
.opacity-value {
    font-variant-numeric: tabular-nums;
    color: var(--text-heading);
    min-width: 3em;
    text-align: right;
}

/* ========== Wall Detail ========== */

.wall-detail { padding: 16px; max-width: var(--page-max, 600px); margin: 0 auto; }

/* ========== Wall page carousel ========== */
.wall-carousel-root {
    max-width: var(--page-max, 600px);
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    height: calc(100dvh - var(--topbar-h) - var(--bottomnav-h));
    min-height: 360px;
}
.wall-carousel {
    flex: 1 1 0;
    min-height: 0;
    display: flex;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    /* No scroll-behavior here on purpose: a CSS-level "smooth" makes every scrollLeft assignment
       animate, including the initial positioning. wall-carousel.js asks for smooth or instant per
       call instead, so user-driven page changes glide and programmatic ones jump. */
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
.wall-carousel::-webkit-scrollbar { display: none; }
/* Hidden until the initial page is positioned (see wall-carousel.js initPage), so the first
   painted frame is already the target page instead of a flash of page 1. */
.wall-carousel.carousel-pending { visibility: hidden; }
.wall-page {
    flex: 0 0 100%;
    width: 100%;
    scroll-snap-align: start;
    overflow-y: auto;
    padding: 12px 16px;
    box-sizing: border-box;
}
.wall-page.no-pad { padding: 0; }

.carousel-dots {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    padding: 10px 0 6px;
    flex: 0 0 auto;
}
.carousel-dots .dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--text-secondary);
    opacity: 0.4;
    transition: width 0.18s, height 0.18s, opacity 0.18s, background 0.18s;
    border: none;
    padding: 0;
    cursor: pointer;
}
.carousel-dots .dot.active {
    width: 10px;
    height: 10px;
    opacity: 1;
    background: var(--accent);
}

/* Collapsible wall-settings cards: the <summary> reads as the card header. */
.settings-card > .settings-card-summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.settings-card > .settings-card-summary::-webkit-details-marker { display: none; }
.settings-card > .settings-card-summary h2 { margin-bottom: 0; }
.settings-card > .settings-card-summary::after {
    content: "▾";
    color: var(--text-secondary);
    font-size: 0.85rem;
    transition: transform 0.18s;
}
.settings-card[open] > .settings-card-summary::after { transform: rotate(180deg); }
.settings-card > .settings-card-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.settings-card > .settings-card-body { margin-top: 14px; }
/* The API-key panel supplies its own settings-panel chrome; drop the nested inset inside its card. */
.settings-card > .settings-card-body > .wall-apikey-panel {
    margin-top: 0;
    padding: 0;
    background: none;
    border: none;
    box-shadow: none;
}

.app-shell:has(.wall-carousel-root.editing) .carousel-dots { display: none; }
.app-shell:has(.wall-carousel-root.editing) .wall-carousel {
    overflow-x: hidden;
    scroll-snap-type: none;
}

.detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.detail-header h1 {
    font-family: var(--font-display);
    font-size: 1.8rem;
    font-weight: 700;
    letter-spacing: -0.03em;
    color: var(--text-heading);
}
.header-actions { display: flex; gap: 4px; }

.btn-icon { background: none; border: none; cursor: pointer; color: var(--text-secondary); padding: 4px; }
.btn-icon:hover { color: var(--text-heading); }
.btn-icon.editing { color: var(--accent); }

.description { color: var(--text-secondary); margin-bottom: 16px; font-size: 0.95rem; }

.wall-photo-container { margin-bottom: 16px; }

/* Grow-with-content, not viewport-locked: `.wall-page` (overflow-y:auto) is the vertical scroller,
   so the column and its card must be free to exceed the viewport slice and let the page scroll
   instead of being height-locked and clipped. `min-height:100%` still fills a short viewport. */
/* Two-column on the desktop tier, one column on the phone tier — ONE block, switched by
   --wall-main-display (see the layout-tier section at the bottom of this file). In the flex
   (phone) polarity the grid properties below are inert, so the phone layout is byte-identical
   to what it was; in the grid polarity `flex-direction` is the inert one. Column 1 is the photo
   card, column 2 the .wall-side rail (stats, invite, panel-link callout — and, later, a docked
   toolbar). `align-items: start` keeps both columns content-height instead of stretching the
   rail to the photo's height; it is inert in the column-flex polarity (the children are
   grid polarity `flex-direction` is the inert one. `align-items` has to stay on the switch: in
   the grid polarity `start` is what keeps the rail content-height instead of stretched to the
   photo's height, but in the column-flex polarity it would shrink-wrap the phone's full-width
   cards, so the phone keeps `stretch`. */
.wall-main {
    display: var(--wall-main-display, flex);
    flex-direction: column;
    gap: 12px;
    min-height: 100%;
    max-width: var(--page-max, none);
    /* `auto` rather than a fixed second track: the rail is only rendered when the wall is NOT in
       edit mode, and an auto track with no item in it collapses to zero, so the editor gets the
       whole width back instead of staring at a 340px hole. The rail carries its own width. */
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: var(--wall-main-align, stretch);
}
/* The rail. `display: contents` on the phone tier makes the wrapper vanish entirely — its
   children are laid out exactly as if it were not in the markup, which is what keeps the phone
   pixel-identical; max-height/overflow are inert on a contents box. On the desktop tier it
   becomes a real column that scrolls on its own so nothing in it can ever be unreachable. */
.wall-side {
    display: var(--wall-side-display, contents);
    flex-direction: column;
    gap: 0;
    width: var(--wall-side-w, 340px);   /* inert on the phone tier's `contents` box */
    min-width: 0;
    max-height: var(--wall-side-max-h, none);
    overflow-y: auto;
    overscroll-behavior: contain;
}
.wall-main .wall-detail-card {
    display: flex;
    flex-direction: column;
    background: var(--bg-surface);
    border: 1px solid var(--border-light);
    border-radius: var(--r-lg);
    padding: 12px;
    gap: 10px;
    box-shadow: 0 1px 2px var(--shadow-sm), 0 8px 32px -18px var(--shadow-md);
}
.wall-main .wall-photo-container { display: flex; flex-direction: column; margin-bottom: 0; }
/* The card no longer supplies a locked flex height for the photo to fill, so bound the photo box to
   a viewport slice directly (fullscreen overrides this with its own aspect cap below). Rounded via
   the container: the base .photo-viewport overflow (auto) clips the width-scaled/scrolled content to
   this radius, so the wall photo has rounded corners without the removed .photo-editor band-aid. */
.wall-main .photo-viewport,
.wall-main .schematic-viewport {
    flex: 0 1 auto;
    height: auto;
    min-height: 0;
    max-height: calc(100dvh - var(--topbar-h) - var(--bottomnav-h) - var(--wall-photo-chrome));
    border-radius: 12px;
}

/* Edit mode renders NEITHER of the two things the chrome budget reserves room for under the
   photo: WallDetail drops the .wall-side rail (so no stat cards), and WallPhotoEditor renders
   WallDimControl only on its read-only arms (so no dim bar). Budgeting for both anyway shrank the
   photo's cap by --wall-stats-h + --wall-dim-bar-h for nothing. Declared on .wall-main so it beats
   BOTH the inherited `:root:root` block in app.css and the desktop tier's body-level payload; the
   desktop tier's own override keys on `:has(> .wall-side)`, which edit mode never matches, so the
   two rules cannot both apply. Read-only keeps the full budget unchanged. */
.wall-carousel-root.editing .wall-main {
    --wall-photo-chrome: calc(var(--wall-photo-chrome-base) - var(--wall-dim-bar-h));
}

/* Close the dead strip between the editor card and the docked toolbar (phone tier only, where the
   bar is actually fixed to the bottom — above 720px components.css returns it to normal flow and
   this whole problem does not exist).
   The photo is WIDTH-bound at phone widths: on a 390px screen it renders 332x249 against a 616px
   height cap, so no chrome re-budgeting can grow it and the card shrink-wrapped it, leaving ~296px
   of page background above the bar. `.wall-main` already fills the scroller's content box
   (min-height: 100%), so all that is missing is a stretch chain from it down to the stage.
   `.photo-viewport` is deliberately NOT in that chain: it keeps `flex: 0 1 auto`, so the image is
   letterboxed at its natural size rather than upscaled or cropped, and — more importantly — its box
   dimensions stay exactly as they were. viewport.js owns inline styles on that element and the
   browser re-clamps scroll offsets when its size changes, which is what makes the view pan by
   itself; centring it inside a taller parent moves it without ever resizing it. */
@media (max-width: 720px) {
    .wall-carousel-root.editing .wall-detail-card,
    .wall-carousel-root.editing .wall-photo-container,
    .wall-carousel-root.editing .editor-shell,
    .wall-carousel-root.editing .editor-stage {
        flex: 1 1 auto;
        min-height: 0;
    }

    /* Letterbox: the photo (and the lifecycle bar that belongs under it) centre in the free space
       as one group, so the bar stays attached to the image instead of floating away from it. */
    .wall-carousel-root.editing .editor-stage { justify-content: center; }
}

/* When the wall photo is framed to its wall region, size the box to the region aspect (published
   as --fit-aspect by bwViewport.fitBox) instead of stretching to fill the card — this is what
   actually trims the floor/ceiling dead band. Only the read-only view is framed; the editor keeps
   the intrinsic-aspect box. Centred, and capped to a viewport slice so the rest of the page flows
   below and scrolls. */
.wall-main .photo-viewport.bw-framed {
    flex: 0 1 auto;
    aspect-ratio: var(--fit-aspect, 1.4);
    height: auto;
    max-height: calc(100dvh - var(--topbar-h) - var(--bottomnav-h) - var(--wall-photo-chrome));
    margin-inline: auto;
}

.wall-top-row { display: flex; align-items: center; gap: 8px; }
.wall-top-row .view-toggle { flex: 1; margin-bottom: 0; }
.wall-top-row > .btn-icon { color: var(--text); }

.upload-prompt {
    padding: 40px 20px; text-align: center; background: var(--bg-surface-alt);
    border-radius: 12px; border: 2px dashed var(--border);
}
.upload-prompt p { color: var(--text-secondary); margin-bottom: 12px; }

/* --stats-wrap/--stat-basis are the tier switch: on the phone the bar is one nowrap row of
   flex:1 0-basis cards (unchanged); in the narrow desktop rail the cards keep a 130px basis and
   wrap into a 2x2 block instead of shrinking to unreadable slivers. */
.stats-bar { display: flex; flex-wrap: var(--stats-wrap, nowrap); gap: 10px; margin-bottom: 20px; }
.stat {
    flex: 1 1 var(--stat-basis, 0); text-align: center;
    padding: 14px 10px 12px;
    background: var(--bg-surface);
    border: 1px solid var(--border-light);
    border-radius: var(--r-lg);
    box-shadow: 0 1px 2px var(--shadow-sm);
    transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.stat.clickable { cursor: pointer; }
.stat.clickable:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 14px -4px var(--shadow-md), 0 1px 2px var(--shadow-sm);
    border-color: var(--border);
}
.stat-value {
    display: block;
    font-family: var(--font-display);
    font-size: 1.75rem;
    font-weight: 600;
    letter-spacing: -0.04em;
    color: var(--text-heading);
    line-height: 1;
    margin-bottom: 4px;
}
svg.stat-value { margin-inline: auto; }
/* "View in 3D" under the wall stats (Wall3DLink's default btn-secondary look, as a link). */
a.wall3d-link {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    box-sizing: border-box; text-decoration: none; margin-bottom: 20px;
}
.stat-label {
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}

/* Collapsible boulder-filter card: reuses the settings-card chrome; the summary is its header. */
.filter-card { margin-top: 0; margin-bottom: 12px; padding: 14px 16px; }
.filter-card .filter-card-title {
    display: inline-flex; align-items: center; gap: 8px;
    font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.08em; color: var(--text-secondary);
}
.filter-card .filter-card-title svg { color: var(--text-secondary); }
.filter-card .filter-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
    background: var(--accent); color: #fff; font-size: 0.68rem; font-weight: 700;
    letter-spacing: 0;
}
.filter-card > .settings-card-body { margin-top: 12px; }
.filter-card .filter-bar { margin-bottom: 0; }

.filter-bar { display: flex; flex-direction: column; gap: 9px; margin-bottom: 12px; }
.filter-input {
    flex: 1; padding: 8px 12px; border: 2px solid var(--border);
    border-radius: 8px; font-size: 0.9rem; outline: none; box-sizing: border-box;
    background: var(--input-bg); color: var(--text);
}
.filter-input:focus { border-color: var(--secondary); }
.filter-select {
    padding: 8px 10px; border: 2px solid var(--border); border-radius: 8px;
    font-size: 0.9rem; background: var(--input-bg); color: var(--text);
}
.sort-row, .grade-range-row { display: flex; align-items: center; gap: 8px; }
.sort-row .filter-select { flex: 1; }
.sort-label {
    font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.08em; color: var(--text-secondary); min-width: 46px;
}
.filter-checks { display: flex; flex-wrap: wrap; gap: 7px 14px; }
.filter-check {
    display: flex; align-items: center; gap: 5px;
    font-size: 0.8rem; color: var(--text-secondary); cursor: pointer;
}
.filter-check input { accent-color: var(--accent); }

.grade-range-picker { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.grade-range-picker .grade-select { min-width: 62px; font-size: 0.85rem; font-weight: 600; padding: 6px 8px; }
.range-dash { color: var(--text-secondary); }

.archive-section { margin-top: 16px; border-top: 1px dashed var(--border); padding-top: 12px; }
.archive-summary {
    cursor: pointer; font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.08em; color: var(--text-secondary); margin-bottom: 12px; user-select: none;
}

.boulder-rating { display: inline-flex; align-items: center; gap: 5px; margin-top: 3px; }
.boulder-rating .rating-count { font-size: 0.72rem; color: var(--text-secondary); }
.fav-icon { color: #f5b301; }
.card-archive {
    background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border);
    border-radius: 6px; padding: 4px 10px; font-size: 0.72rem; font-weight: 600; cursor: pointer;
}
.card-archive:hover { border-color: var(--accent); color: var(--accent); }

.star-rating { display: inline-flex; align-items: center; line-height: 1; }
.star-rating.readonly .star-track {
    position: relative; display: inline-block; font-size: 1.05rem; letter-spacing: 1px;
}
.star-rating.small.readonly .star-track { font-size: 0.85rem; }
.star-back { color: var(--border); }
.star-fill {
    position: absolute; top: 0; left: 0; overflow: hidden; white-space: nowrap; color: #f5b301;
}
.star-rating.interactive { gap: 3px; }
.star-btn {
    background: none; border: none; padding: 0 1px; cursor: pointer;
    font-size: 1.5rem; line-height: 1; color: var(--border); transition: color 0.1s ease;
}
.star-rating.small .star-btn { font-size: 1.05rem; }
.star-btn.on, .star-btn:hover { color: #f5b301; }
/* Offline-wired stars render their lit state AS `bw-opt-on` (see StarRating.razor), so the
   delegated DOM handler can lower a rating as well as raise it with the circuit down. This
   has to beat the generic `.bw-opt-on { color: var(--accent) }` rule in app.css, hence the
   container qualifier. Gold reads correctly against both themes' surfaces. */
.star-rating .star-btn.bw-opt-on { color: #f5b301; opacity: 1; }
/* Thumb-friendly hit area without inflating the glyph. */
.star-rating.interactive .star-btn { min-width: 34px; min-height: 40px; }

.fav-toggle {
    background: none; border: none; cursor: pointer; font-size: 1.5rem;
    line-height: 1; color: var(--text-secondary); padding: 0 4px;
    min-width: 40px; min-height: 40px;
    transition: color 0.12s ease, transform 0.12s ease;
}
.fav-toggle.on { color: #f5b301; }
/* Clear tap/hover affordance on the star, gold like its lit state. */
.fav-toggle:hover, .fav-toggle:focus-visible { color: #f5b301; transform: scale(1.15); }
.fav-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }

/* Both directions of the favourite are always in the DOM; CSS picks the visible one from
   `bw-opt-on`, which offline-actions.js can add and remove without a circuit. See
   Components/Shared/BoulderFavoriteToggle.razor for why one button cannot work offline. */
.fav-wrap { display: inline-flex; align-items: center; color: inherit; opacity: 1; }
.fav-wrap .fav-add { display: inline-flex; align-items: center; justify-content: center; }
.fav-wrap .fav-remove { display: none; }
.fav-wrap.bw-opt-on .fav-add { display: none; }
.fav-wrap.bw-opt-on .fav-remove {
    display: inline-flex; align-items: center; justify-content: center;
}
.archived-badge {
    font-size: 0.65rem; padding: 1px 6px; background: var(--border-light); color: var(--text-secondary);
    border-radius: 4px; font-weight: 600;
}

.rating-section {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px;
    padding: 12px 14px; background: var(--bg-surface-alt); border-radius: 10px; margin: 12px 0;
}
.rating-summary { display: flex; align-items: center; gap: 8px; }
.rating-avg { font-weight: 700; color: var(--text-heading); }
.rating-count, .rating-empty { font-size: 0.8rem; color: var(--text-secondary); }
.rating-mine { display: flex; align-items: center; gap: 8px; }
.rating-mine-label { font-size: 0.78rem; color: var(--text-secondary); font-weight: 600; }
.rating-clear {
    background: none; border: none; color: var(--text-secondary);
    text-decoration: underline; font-size: 0.72rem; cursor: pointer;
}
.revision-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-archive {
    background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border);
    border-radius: 7px; padding: 7px 14px; font-size: 0.82rem; font-weight: 600; cursor: pointer;
}
.btn-archive:hover { border-color: var(--accent); color: var(--accent); }

.boulder-section { margin-bottom: 20px; }

/* ---------- Boulder list as its own surface (carousel page 4) ----------
 *
 * The list is USER-generated and unbounded, so it keeps scrolling — but it was scrolling inside a
 * slide sized for the photo pages: 690px of an 844px phone screen, with the section header and the
 * filter card eating the top of it and no sign that 4+ viewports of cards continued below.
 *
 * Three changes, all scoped to `.boulders-page` so no other carousel page is touched:
 *   1. the slide goes full-bleed (padding moves onto the section) so the list owns the whole slice;
 *   2. the header + filter card become ONE sticky head, so "+ New" and Filters stay reachable
 *      however far down the list you are — they used to scroll away and never come back;
 *   3. a sticky fade at the bottom edge marks the fold, shown only while there IS more below
 *      (wall-carousel.js toggles `has-more` on the slide).
 *
 * Nothing here changes the WIDTH of a slide or the height of the flex track, so the `x mandatory`
 * snap container is undisturbed; `has-more` only animates an opacity. */
.wall-page.boulders-page { padding: 0; }
/* One block, switched by --boulder-list-display (see the layout-tier section at the bottom of this
   file), exactly like .wall-main. In the flex (phone) polarity the grid properties are inert, so the
   phone layout is byte-identical to the one-column list; in the grid (desktop) polarity
   `flex-direction` is the inert one and the cards flow into as many columns as the width affords.
   Only the COLUMN gap comes from `gap` — rows keep the 8px margin every .boulder-card already
   carries, so a card looks the same in both polarities. */
.boulders-page .boulder-section {
    display: var(--boulder-list-display, flex);
    flex-direction: column;
    grid-template-columns: var(--boulder-list-columns, none);
    gap: 0 12px;
    align-content: start;
    min-height: 100%;
    margin-bottom: 0;
    padding: 0 16px 12px;
    box-sizing: border-box;
}
/* Everything that is not a card spans the full width of the grid. Inert in the flex polarity. */
.boulders-page .boulder-list-head,
.boulders-page .empty-boulders,
.boulders-page .archive-section,
.boulders-page .list-fade { grid-column: 1 / -1; }
.boulders-page .boulder-list-head {
    position: sticky;
    top: 0;
    z-index: 3;
    /* Full-bleed background: the cards scroll UNDER the head, so it has to cover the slide edge to
       edge or a sliver of card shows past the section padding. */
    margin: 0 -16px;
    padding: 12px 16px 8px;
    background: var(--bg);
}
/* One compact row: title · count — filter chip — "+ New". `display: contents` dissolves the
   section-header wrapper so its two children join this flex row directly; the filter card sits
   between them as a chip. Two stacked rows cost 140px of a 690px slide, which is a fifth of the
   list; this is 54px. */
.boulders-page .boulder-list-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 10px;
}
.boulders-page .boulder-list-head .section-header { display: contents; }
.boulders-page .boulder-list-head .section-header h2 { flex: 1 1 auto; }
.boulders-page .boulder-list-head .filter-card {
    order: 1;
    flex: 0 0 auto;
    margin: 0;
    padding: 7px 12px;
}
/* The chip is a real <button> now, not a <summary>, so it needs the button chrome stripped back to
   what .settings-card-summary already draws. `justify-content` is overridden because a summary
   fills its card and pushes the chevron to the far edge, while the chip is only as wide as its own
   label. */
.boulders-page .boulder-list-head .filter-card > .settings-card-summary {
    justify-content: flex-start;
    gap: 6px;
    padding: 0;
    border: none;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
}
.boulders-page .boulder-list-head .filter-card.open > .settings-card-summary::after {
    transform: rotate(180deg);
}
.boulders-page .boulder-list-head .btn-add-boulder { order: 2; }
/* Open, it is a panel rather than a chip: it hangs OFF the bar (out of flow, anchored under it)
   instead of growing it. That is what keeps "+ New" and the Filters chip on screen while the card
   is open — the earlier version un-stuck the whole head, which took "+ New" off-screen with it, and
   the bar cannot simply grow because a 290px panel inside a 690px slide is most of the list. Out of
   flow, the sticky box stays one bar tall whatever the panel does.
   `position: absolute` resolves against the sticky head itself (sticky is a positioned ancestor).
   The max-height is a safety valve for a very short window, not an everyday scroller.
   The alternative — a panel in the list's own flow that scrolls away with the cards — was tried and
   rejected: opened part-way down the list it is inserted ABOVE the viewport, so the tap looks like
   a no-op and the controls cannot be reached without scrolling the reader back to the top. */
.boulders-page .boulder-list-head .filter-card.open {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin: 0;
    padding: 16px;
    max-height: calc(100dvh - var(--topbar-h) - var(--bottomnav-h) - 120px);
    overflow-y: auto;
    box-shadow: 0 10px 24px -12px var(--shadow-md);
}
/* Count badge next to the "Boulders" heading: the length of the list is the other half of "there is
   more below" — the fade says THAT there is more, this says how much. */
.boulders-page .section-header h2 .list-count {
    margin-left: 6px;
    padding: 1px 7px;
    border-radius: var(--r-pill);
    background: var(--bg-surface-alt);
    border: 1px solid var(--border-light);
    color: var(--text-secondary);
    font-size: 0.72rem;
    letter-spacing: 0;
}
/* The fold marker. Last child of the section, pulled back over the final card by its negative
   margin so it costs no layout height, and sticky to the bottom of the slide so it rides the scroll.
   Invisible unless the slide actually has more content below the fold. */
.boulders-page .list-fade {
    position: sticky;
    bottom: 0;
    flex: 0 0 auto;
    height: 48px;
    margin: -48px -16px 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.18s ease;
    /* Two layers, because one is not enough in BOTH themes: the card surface and the page
       background are nearly the same colour in light mode, so a plain fade-to-background is
       invisible there — the scrim (a neutral darkening, the same direction as every other shadow in
       the app) is what actually reads. In dark mode the background fade carries it and the scrim
       only deepens the edge. */
    background:
        linear-gradient(to top, var(--bg) 0%, color-mix(in srgb, var(--bg) 70%, transparent) 42%, transparent 100%),
        linear-gradient(to top, var(--scrim) 0%, transparent 72%);
}
.wall-page.boulders-page.has-more .list-fade { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
    .boulders-page .list-fade { transition: none; }
}
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.section-header h2 {
    font-family: var(--font-body);
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-secondary);
}
.btn-add-boulder {
    padding: 7px 14px;
    background: var(--accent); color: white;
    text-decoration: none;
    border-radius: var(--r-pill);
    font-weight: 600; font-size: 0.85rem;
    letter-spacing: -0.005em;
    box-shadow: 0 2px 8px -3px var(--accent-soft);
    transition: background 0.15s ease, transform 0.08s ease;
}
.btn-add-boulder:hover { background: var(--accent-hover); }
.btn-add-boulder:active { transform: translateY(1px); }

.boulder-card {
    display: flex; justify-content: space-between; align-items: center;
    padding: 14px 16px;
    background: var(--bg-surface);
    border: 1px solid var(--border-light);
    border-left: 3px solid transparent;
    border-radius: var(--r-lg);
    box-shadow: 0 1px 2px var(--shadow-sm);
    margin-bottom: 8px;
    text-decoration: none; color: inherit;
    transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
}
.boulder-card:hover {
    transform: translateY(-1px);
    box-shadow: 0 6px 18px -6px var(--shadow-md), 0 1px 2px var(--shadow-sm);
    border-left-color: var(--accent);
}
.boulder-card-left { display: flex; flex-direction: column; gap: 3px; }
.boulder-card-right { display: flex; align-items: center; gap: 10px; }
.boulder-name {
    font-family: var(--font-display);
    font-weight: 600;
    letter-spacing: -0.015em;
    color: var(--text-heading);
    font-size: 1.02rem;
}
.boulder-meta { font-size: 0.78rem; color: var(--text-secondary); letter-spacing: 0.005em; }
.boulder-grade {
    padding: 4px 12px;
    background: var(--accent); color: white;
    border-radius: var(--r-pill);
    font-family: var(--font-display);
    font-weight: 700; font-size: 0.85rem;
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 2px 6px -3px var(--accent-soft);
}
.historic-badge {
    font-size: 0.65rem; padding: 1px 6px; background: var(--warning-bg); color: var(--warning-text);
    border-radius: 4px; font-weight: 600; margin-left: 6px; vertical-align: middle;
}
/* Shares the badge shape with .historic-badge but takes the danger colours on purpose: historic is
   a statement about the past, review is a job somebody still has to do. Without a rule here the
   badge renders as bare body text and reads as part of the boulder's name. */
.review-badge {
    font-size: 0.65rem; padding: 1px 6px; background: var(--danger-bg); color: var(--danger-text);
    border-radius: 4px; font-weight: 600; margin-left: 6px; vertical-align: middle;
}
.status-icon { font-size: 1rem; }
.flash-icon { color: #FF5722; }
.send-icon { color: #4CAF50; font-weight: 700; }
.try-icon {
    font-size: 0.75rem; font-weight: 700; color: #FF9800;
    background: var(--warning-bg); padding: 2px 6px; border-radius: 4px;
}
.empty-boulders {
    text-align: center; padding: 24px; color: var(--text-secondary); background: var(--bg-surface);
    border-radius: 10px; font-size: 0.9rem;
}

.settings-panel {
    margin-top: 16px;
    padding: 20px;
    background: var(--bg-surface);
    border: 1px solid var(--border-light);
    border-radius: var(--r-lg);
    box-shadow: 0 1px 2px var(--shadow-sm);
}
.settings-panel + .settings-panel { margin-top: 12px; }
.settings-panel h2 {
    font-family: var(--font-display);
    font-size: 1.1rem;
    font-weight: 600;
    letter-spacing: -0.02em;
    color: var(--text-heading);
    margin-bottom: 14px;
}

.share-link { margin-bottom: 12px; }

/* Cross-panel linking, said out loud on the wall page: the tool itself is otherwise a bare icon
   buried in per-panel edit mode, so a multi-panel wall could be built with zero links. */
.panel-link-callout {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin-bottom: 12px;
    padding: 10px 12px;
    border-radius: var(--r-sm);
    border: 1px solid var(--border);
    background: var(--bg-surface);
}
.panel-link-callout.warn {
    border-color: var(--status-warning);
    background: color-mix(in srgb, var(--status-warning) 10%, var(--bg-surface));
}
.panel-link-callout-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1 1 220px;
}
.panel-link-callout .btn-secondary { width: auto; margin-bottom: 0; }

.member-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 10px 12px; background: var(--bg-surface); border-radius: 8px;
    margin-bottom: 6px; box-shadow: 0 1px 3px var(--shadow-sm);
}
.member-name-link {
    font-weight: 600; font-size: 0.9rem; color: var(--secondary);
    text-decoration: underline; padding: 4px 0; min-height: 24px; display: inline-flex; align-items: center; gap: 8px;
}
.member-name-link:hover { text-decoration: underline; }
.member-name-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.member-role { font-size: 0.8rem; font-weight: 600; padding: 3px 10px; border-radius: 4px; }
.member-role.owner { background: var(--success-bg); color: var(--success-text); }
.role-toggle {
    padding: 3px 10px; border: 1px solid var(--border); background: var(--bg-surface); border-radius: 4px;
    font-size: 0.8rem; font-weight: 600; cursor: pointer; color: var(--text);
}
.role-toggle.admin { background: var(--info-bg); color: var(--info-text); border-color: var(--info-text); }
.role-toggle.member { color: var(--text-secondary); }

/* Wall member leaderboard (WallLeaderboardCard) */
.leaderboard-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.leaderboard-table {
    width: 100%; min-width: 340px; border-collapse: collapse; font-size: 0.9rem;
}
.leaderboard-table th, .leaderboard-table td {
    padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--border-light);
}
.leaderboard-table tbody tr:last-child td { border-bottom: none; }
.leaderboard-table th.sortable { padding: 0; white-space: nowrap; }
.sort-btn {
    display: inline-flex; align-items: center;
    width: 100%; padding: 8px 10px; border: none; background: none;
    cursor: pointer; user-select: none;
    font: inherit; font-weight: 600; color: var(--text-secondary);
    text-align: inherit;
}
.leaderboard-table th.num .sort-btn { justify-content: flex-end; }
.sort-btn:hover { color: var(--text-heading); }
.sort-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sort-btn.active { color: var(--text-heading); font-weight: 700; }
.leaderboard-table th.num, .leaderboard-table td.num {
    text-align: right; font-variant-numeric: tabular-nums;
}
.leaderboard-table td.num { color: var(--text); }
/* Fixed-width slot so an inactive→active arrow does not nudge the header text. */
.leaderboard-sort-ind {
    display: inline-block; width: 0.9em; margin-left: 4px;
    font-size: 0.75rem; color: var(--accent); text-align: center;
}
.leaderboard-empty { text-align: center; padding: 16px; color: var(--text-secondary); font-size: 0.9rem; }

.activity-row {
    display: flex; align-items: center; gap: 8px; padding: 8px 10px;
    background: var(--bg-surface); border-radius: 6px; margin-bottom: 4px;
    box-shadow: 0 1px 2px var(--shadow-sm); font-size: 0.85rem;
}
.activity-icon {
    width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
    background: var(--bg-surface-alt); border-radius: 4px; font-size: 0.7rem; font-weight: 700; color: var(--text-secondary);
}
.activity-text { flex: 1; color: var(--text); }
.activity-time { font-size: 0.75rem; color: var(--text-secondary); white-space: nowrap; }
.activity-pager { display: flex; gap: 6px; margin-top: 8px; }
.empty-activity { text-align: center; color: var(--text-secondary); font-size: 0.85rem; padding: 12px; }

/* ========== Boulder Detail ========== */

/* Same one-block two-polarity switch as .wall-main: block flow on the phone tier (every grid
   property below is inert on a block box, so the phone is unchanged), a two-column grid on the
   desktop tier. Column 1 is the header + photo stack, column 2 the interaction rail (rules,
   ascent buttons, rating, backdating, videos), and a third full-width row under both carries the
   sections with no natural length — activity, comments, history. The desktop cap is its own var
   rather than --page-max: this page WANTS a bounded width so no comment or history row ever
   runs the full 1440px, while --page-max: none still frees the pages that want the window. */
.boulder-detail {
    padding: 16px;
    max-width: var(--boulder-page-max, var(--page-max, 600px));
    margin: 0 auto;
}
/* The two columns live in their own row rather than directly in .boulder-detail, because the
   photo column is sticky on the desktop tier and a sticky box sticks for as long as its PARENT is
   on screen. With .boulder-detail as the grid, that parent also held the full-width row below, so
   the photo stayed pinned over activity/comments/history all the way to the bottom of the page.
   Wrapped here, the photo sticks only while the rail beside it scrolls, then leaves with the row.
   Transparent on the phone tier (display: contents), so the stack there is unchanged. */
.boulder-main-row {
    display: var(--boulder-detail-display, contents);
    grid-template-columns: minmax(0, 1fr) var(--boulder-side-w, 360px);
    gap: 0 20px;
    align-items: start;
}
/* Both wrappers are transparent on the phone tier (display: contents), so their children stay
   direct children of the block flow and nothing moves. On the desktop tier they are the two
   grid columns; the photo column sticks under the top bar while the rail scrolls past it. */
.boulder-photo-col, .boulder-side-col {
    display: var(--boulder-col-display, contents);
    flex-direction: column;
    min-width: 0;
}
.boulder-photo-col { position: var(--boulder-photo-col-position, static); top: calc(var(--topbar-h) + 8px); }

/* The long-form row. Transparent on the phone tier like the other two wrappers, so its three
   sections stay where they always were in the single stack. On the desktop tier it is a block
   under .boulder-main-row spanning its full width, and an auto-fit grid in its own right: each section gets at
   least 340px and the row uses as many columns as the page width affords (three at 1280+, two at
   1024, one at the bottom of the tier). align-items: start so a short Activity list does not
   stretch to the height of a long comment thread. */
.boulder-wide-col {
    display: var(--boulder-wide-display, contents);
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    align-items: start;
    gap: 0 20px;
    min-width: 0;
}

.page-header { margin-bottom: 12px; }
.title-row { display: flex; align-items: center; gap: 10px; margin-top: 4px; flex-wrap: wrap; }
.title-row h1 { font-size: 1.4rem; font-weight: 700; color: var(--text-heading); min-width: 0; }
/* Right-aligned action cluster (favourite, meta-edit pencil, Revise) — stays grouped and
   pushes to the right edge, wrapping as a unit on narrow screens. */
.title-actions { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }
.grade-badge {
    padding: 4px 12px; background: var(--accent); color: white;
    border-radius: 6px; font-weight: 700; font-size: 0.95rem;
}
.meta { color: var(--text-secondary); font-size: 0.85rem; margin-top: 2px; }

.bw-revision-notice {
    padding: 12px 14px; background: var(--warning-bg); border-radius: 8px; margin-bottom: 12px;
}
.bw-revision-notice p { color: var(--warning-text); font-size: 0.9rem; margin-bottom: 8px; }
.btn-revise {
    display: inline-block; padding: 8px 16px; background: #E65100; color: white;
    text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 0.85rem;
}
/* "3D" next to Revise on the boulder page (Wall3DLink): an outlined pill, secondary to Revise. */
.btn-view3d {
    display: inline-flex; align-items: center; gap: 5px; min-height: 36px; padding: 6px 12px;
    background: var(--bg-surface); color: var(--text-heading); border: 1px solid var(--border);
    text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 0.85rem;
}
.btn-view3d:hover { background: var(--bg-surface-alt); }

.bw-grade-proposal-notice {
    padding: 12px 14px; border-radius: 8px; margin-bottom: 12px; font-size: 0.9rem;
}
.bw-grade-proposal-notice.creator { background: var(--info-bg); color: var(--info-text); }
.bw-grade-proposal-notice.viewer { background: var(--warning-bg); color: var(--warning-text); }
.bw-grade-proposal-notice p { margin: 0 0 8px 0; }
.proposal-actions { display: flex; gap: 8px; }
.proposal-btn {
    padding: 6px 16px; border: none; border-radius: 6px;
    font-weight: 600; font-size: 0.85rem; cursor: pointer;
}
.proposal-btn.accept { background: #4CAF50; color: white; }
.proposal-btn.accept:hover { background: #43A047; }
.proposal-btn.accept:disabled { opacity: 0.5; cursor: not-allowed; }
.proposal-btn.reject { background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border); }
.proposal-btn.reject:hover { background: var(--bg-surface-alt); }

.btn-propose-grade {
    display: block; width: 100%; padding: 10px; background: var(--bg-surface);
    border: 2px dashed var(--border); border-radius: 8px; color: var(--text-secondary);
    font-size: 0.85rem; cursor: pointer; margin-bottom: 16px; text-align: center;
}
.btn-propose-grade:hover { border-color: var(--text-secondary); color: var(--text); }

.grade-propose-form {
    padding: 12px 14px; background: var(--bg-surface-alt); border-radius: 8px;
    margin-bottom: 16px;
}
.grade-propose-actions { display: flex; gap: 8px; margin-top: 10px; }

/* Creator inline rename / regrade, shown from the pencil next to the title. */
.boulder-edit-btn { color: var(--text-secondary); }
.boulder-edit-btn:hover { color: var(--accent); }
.boulder-meta-edit {
    display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
    padding: 12px 14px; background: var(--bg-surface-alt); border-radius: 8px; margin: 8px 0 12px;
}
.boulder-name-input {
    flex: 1 1 200px; min-width: 160px; padding: 8px 10px; font-size: 1rem;
    border: 1px solid var(--border); border-radius: var(--r-sm);
    background: var(--input-bg); color: var(--text);
}
.meta-edit-actions { display: flex; gap: 8px; }

/* Backdated logging: a collapsed card with a time picker and the three ascent buttons. */
.backdate-card {
    margin-bottom: 16px; border: 1px solid var(--border); border-radius: 12px;
    background: var(--bg-surface); overflow: hidden;
}
.backdate-card > summary {
    cursor: pointer; padding: 12px 14px; font-weight: 600; color: var(--text-heading);
}
.backdate-body { display: flex; flex-direction: column; gap: 10px; padding: 0 14px 14px; }
.backdate-label { font-size: 0.85rem; color: var(--text-secondary); }
.backdate-input {
    padding: 8px 10px; font-size: 0.95rem; border: 1px solid var(--border);
    border-radius: var(--r-sm); background: var(--input-bg); color: var(--text);
}

.photo-section {
    position: relative; border-radius: 12px; overflow: hidden; margin-bottom: 12px;
}
.photo-section .wall-photo { width: 100%; display: block; }
.photo-section .hold-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }

.legend-bar { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
.legend-chip {
    padding: 4px 10px; border-radius: 6px; font-size: 0.8rem; font-weight: 600;
}
.legend-chip.start { background: var(--success-bg); color: var(--success-text); }
.legend-chip.normal { background: var(--info-bg); color: var(--info-text); }
.legend-chip.top { background: var(--danger-bg); color: var(--danger-text); }
.legend-chip.usage { background: var(--bg-surface); color: var(--text-muted); border: 1px solid var(--border); }
.legend-chip.foot { background: var(--foot-hold-soft); color: var(--foot-hold-text); }

/* Informational rule banners above the boulder image (feet-of-a-colour, no-match). */
.boulder-info-banners { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.boulder-info-banner {
    background: var(--info-bg); color: var(--info-text);
    border: 1px solid var(--border-light);
    padding: 10px 14px; border-radius: var(--r-md); font-size: 0.88rem; line-height: 1.35;
}

/* Rule chips under the boulder view. */
.rules-bar { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
.rule-chip {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 10px; border-radius: var(--r-pill); font-size: 0.78rem;
    background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border);
}
.rule-chip.on { color: var(--text); border-color: var(--border); }
.rule-chip.off { opacity: 0.85; }
.rule-chip.foot { background: var(--foot-hold-soft); color: var(--foot-hold-text); border-color: transparent; }
.rule-chip .rule-warn { color: var(--warning-text); }
.view-toggle .toggle-sep { width: 1px; align-self: stretch; background: var(--border); margin: 0 4px; }

.attempt-section { margin-bottom: 16px; }
.attempt-section h2 { font-size: 1.1rem; font-weight: 600; color: var(--text-heading); margin-bottom: 10px; }

.attempt-buttons { display: flex; gap: 10px; }
.attempt-btn {
    flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: 14px 8px; border: 2px solid var(--border); border-radius: 12px;
    background: var(--bg-surface); cursor: pointer; font-weight: 600; font-size: 0.85rem;
    transition: all 0.15s;
}
.attempt-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.attempt-btn.try { color: #FF9800; }
.attempt-btn.try:hover:not(:disabled) { border-color: #FF9800; background: var(--warning-bg); }
.attempt-btn.send { color: #4CAF50; }
.attempt-btn.send:hover:not(:disabled) { border-color: #4CAF50; background: var(--success-bg); }
.attempt-btn.flash { color: #FF5722; }
.attempt-btn.flash:hover:not(:disabled) { border-color: #FF5722; background: #FBE9E7; }

.my-stats {
    display: flex; align-items: center; gap: 8px; padding: 10px 14px;
    background: var(--bg-surface-alt); border-radius: 8px; font-size: 0.9rem;
    color: var(--text-secondary); margin-bottom: 16px;
}
.chip { padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: 600; }
.flash-chip { background: #FBE9E7; color: #D84315; }
.send-chip { background: var(--success-bg); color: var(--success-text); }

/* ========== Beta videos ========== */
/* Collapsed by default (see BoulderBetaVideos.razor): beta is a spoiler, so the section has to
   be opened deliberately. The strip scrolls horizontally rather than wrapping — one row keeps
   the "+" tile permanently in reach on a phone, however many clips a boulder collects. */
.beta-section {
    margin-bottom: 20px; border: 1px solid var(--border); border-radius: 12px;
    background: var(--bg-surface); overflow: hidden;
}
.beta-summary {
    cursor: pointer; padding: 12px 14px; display: flex; align-items: center; gap: 8px;
    list-style: none;
}
.beta-summary::-webkit-details-marker { display: none; }

/* Own disclosure triangle: the native marker cannot be positioned and disappears under
   `display: flex` in some engines. */
.beta-summary::before {
    content: ''; width: 0; height: 0; flex: none;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent; border-bottom: 4px solid transparent;
    color: var(--text-secondary); transition: transform 0.15s ease;
}
.beta-section[open] > .beta-summary::before { transform: rotate(90deg); }
.beta-title { font-weight: 600; color: var(--text-heading); font-size: 1.1rem; }
.beta-count {
    font-size: 0.75rem; font-weight: 600; padding: 1px 7px; border-radius: var(--r-pill);
    background: var(--accent-soft); color: var(--accent);
}
.beta-body { padding: 0 14px 14px; }
.beta-strip {
    display: flex; gap: 10px; overflow-x: auto; padding-bottom: 4px;
    scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
}
.beta-strip::-webkit-scrollbar { display: none; }
.beta-strip { scrollbar-width: none; }

.beta-tile {
    position: relative; flex: none; width: 108px; display: flex; flex-direction: column; gap: 6px;
    padding: 0; border: none; background: none; cursor: pointer; scroll-snap-align: start;
    font: inherit; color: inherit; text-align: center;
}
.beta-thumb {
    position: relative; display: block; width: 108px; height: 144px;
    border-radius: var(--r-md); overflow: hidden; background: var(--bg-surface-alt);
    border: 1px solid var(--border);
}
.beta-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.beta-thumb-fallback {
    display: block; width: 100%; height: 100%;
    background: linear-gradient(150deg, var(--bg-surface-alt), var(--border-light));
}
.beta-play {
    position: absolute; right: 6px; bottom: 6px; display: grid; place-items: center;
    width: 28px; height: 28px; border-radius: 50%;
    background: rgba(0, 0, 0, 0.55); color: #fff;
}
.beta-uploader {
    font-size: 0.78rem; color: var(--text-secondary); line-height: 1.2;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.beta-add .beta-add-mark {
    display: grid; place-items: center; width: 108px; height: 144px;
    border: 2px dashed var(--border); border-radius: var(--r-md);
    background: var(--bg-surface-alt); color: var(--accent);
    font-size: 2rem; font-weight: 300; line-height: 1;
}
.beta-add:hover .beta-add-mark { border-color: var(--accent); }
.beta-add.busy { pointer-events: none; opacity: 0.6; }

/* The file input is driven by the tile it sits in; only the label is ever visible. */
.beta-file-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

.beta-lightbox {
    position: fixed; inset: 0; z-index: 1000; background: var(--overlay-bg);
    display: grid; place-items: center; padding: 16px;
}
.beta-lightbox-inner {
    width: min(720px, 100%); max-height: 100%; display: flex; flex-direction: column; gap: 8px;
}
.beta-lightbox-bar { display: flex; align-items: center; gap: 10px; color: #fff; }
.beta-lightbox-title { flex: 1; font-weight: 600; font-size: 0.9rem; }
.beta-lightbox-delete {
    background: none; border: none; color: #fff; opacity: 0.8; font-size: 0.85rem;
    cursor: pointer; padding: 4px 6px;
}
.beta-lightbox-close {
    background: none; border: none; color: #fff; font-size: 1.6rem; line-height: 1;
    cursor: pointer; padding: 0 6px;
}
.beta-player {
    width: 100%; max-height: 78vh; border-radius: var(--r-md); background: #000; display: block;
}

.activity-feed { margin-bottom: 20px; }
.activity-feed h2 { font-size: 1.1rem; font-weight: 600; color: var(--text-heading); margin-bottom: 8px; }

.comments-section { margin-bottom: 20px; }
.comments-section h2 { font-size: 1.1rem; font-weight: 600; color: var(--text-heading); margin-bottom: 8px; }
.comment-form { display: flex; gap: 8px; margin-bottom: 12px; }
.comment-input {
    flex: 1; padding: 8px 12px; border: 2px solid var(--border); border-radius: 8px;
    font-size: 0.9rem; outline: none; box-sizing: border-box;
    background: var(--input-bg); color: var(--text);
}
.comment-input:focus { border-color: var(--secondary); }
.comment-submit {
    padding: 8px 16px; background: var(--secondary); color: white; border: none;
    border-radius: 8px; font-weight: 600; font-size: 0.85rem; cursor: pointer;
}
.comment-submit:disabled { opacity: 0.5; cursor: not-allowed; }
/* The Post button is never `disabled` — a disabled element is skipped by the delegated
   offline handler, and the flag driving it could not be updated with the circuit down.
   `:placeholder-shown` is true exactly while the (unbound) input is empty, so the
   greyed-out affordance is pure CSS and needs no round-trip. Clicking it while empty is
   already a no-op in offline-actions.js. */
.comment-input:placeholder-shown + .comment-submit { opacity: 0.5; }
.comment-submit { min-height: 40px; }
.comment-row {
    padding: 10px 12px; background: var(--bg-surface); border-radius: 8px; margin-bottom: 6px;
    box-shadow: 0 1px 3px var(--shadow-sm);
}
.comment-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.comment-author { font-weight: 600; color: var(--text); font-size: 0.85rem; }
.comment-time { font-size: 0.75rem; color: var(--text-secondary); }
.comment-text { font-size: 0.9rem; color: var(--text); margin: 0; }

.history-section { margin-bottom: 20px; }
.history-section h2 { font-size: 1.1rem; font-weight: 600; color: var(--text-heading); margin-bottom: 8px; }
.attempt-row {
    display: flex; align-items: center; gap: 8px; padding: 10px 12px;
    background: var(--bg-surface); border-radius: 8px; margin-bottom: 6px;
    box-shadow: 0 1px 3px var(--shadow-sm);
}
.attempt-info { flex: 1; display: flex; align-items: center; gap: 8px; }
.attempt-type {
    padding: 2px 8px; border-radius: 4px; font-size: 0.75rem;
    font-weight: 700; text-transform: uppercase;
}
.attempt-type.attempt { background: var(--warning-bg); color: var(--warning-text); }
.attempt-type.send { background: var(--success-bg); color: var(--success-text); }
.attempt-type.flash { background: var(--danger-bg); color: var(--danger-text); }
.attempt-user { font-size: 0.9rem; color: var(--text); }
.attempt-time { font-size: 0.8rem; color: var(--text-secondary); }

/* ========== Wall Create ========== */

.create-wall { padding: 16px; max-width: var(--page-max, 500px); margin: 0 auto; }
.create-wall h1 { font-size: 1.5rem; font-weight: 700; color: var(--text-heading); margin-bottom: 20px; }

.hint { font-size: 0.8rem; color: var(--text-secondary); margin-top: 4px; }

.angle-slider { width: 100%; accent-color: var(--accent); }
.angle-labels { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--text-secondary); }

.photo-preview { margin-bottom: 16px; border-radius: 12px; overflow: hidden; max-height: 300px; }
.photo-preview img { width: 100%; object-fit: contain; }

.detect-choice { margin-bottom: 16px; }
.radio-group { display: flex; flex-direction: column; gap: 8px; }
.radio-label {
    display: flex; align-items: center; gap: 8px; padding: 10px 14px;
    background: var(--bg-surface); border: 2px solid var(--border); border-radius: 8px;
    cursor: pointer; font-size: 0.95rem; color: var(--text);
}
/* Same selected-not-invalid treatment as .checkbox-label below. */
.radio-label:has(input:checked) {
    border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
    background: var(--accent-soft);
}
.radio-label:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.radio-label input[type=radio] { accent-color: var(--accent); }

/* ========== Boulder Create ========== */

.boulder-create { padding: 16px; max-width: var(--page-max, 600px); margin: 0 auto; }
.boulder-create .page-header h1 { font-size: 1.4rem; font-weight: 700; color: var(--text-heading); margin-top: 4px; }
.boulder-create .form-group > label { font-size: 0.85rem; }

.form-section { margin-bottom: 12px; }

/* Name + "Random" name generator. The generator is the point of the field on a keyboard-less kiosk
   tablet, so it is a full-height, comfortably tappable button sitting LEFT of the input, not a
   tiny icon. align-items: stretch keeps it exactly as tall as the input at any font size. */
.name-row { display: flex; align-items: stretch; gap: 8px; }
.name-row > .form-input { flex: 1 1 auto; min-width: 0; }
.name-row > .name-shuffle {
    flex: 0 0 auto; width: auto; margin-bottom: 0;
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    padding: 0 16px; min-height: 44px; font-size: 0.9rem; white-space: nowrap;
}
.name-shuffle-icon { font-size: 1.15rem; line-height: 1; }

.checkbox-label, .toggle-label {
    display: flex; align-items: center; gap: 8px; padding: 10px 14px;
    background: var(--bg-surface); border: 2px solid var(--border); border-radius: 8px;
    cursor: pointer; font-size: 0.9rem; font-weight: 500;
    color: var(--text);
}
/* Checked reads as SELECTED, not as invalid. The fill used to be a literal #fff5f6, which
   in dark mode put near-white label text on a pale pink ground — illegible, and a pale
   fill inside a full-strength --accent border is exactly how this app draws a validation
   error. --accent-soft is a translucent tint defined in all three theme blocks, so it
   composites over whatever the card ground is, and the border is mixed back toward
   --border so the row reads as emphasis rather than alarm. */
.checkbox-label:has(input:checked), .toggle-label:has(input:checked) {
    border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
    background: var(--accent-soft);
    color: var(--text);
}
.checkbox-label input, .toggle-label input { accent-color: var(--accent); }
.checkbox-label:has(input:focus-visible), .toggle-label:has(input:focus-visible) {
    outline: 2px solid var(--accent); outline-offset: 2px;
}

.submit-row { display: flex; gap: 8px; margin-top: 12px; }
.submit-row > .btn-secondary { flex: 0 1 auto; width: auto; margin-bottom: 0; }
.submit-row > .btn-submit { flex: 1; margin-top: 0; }
.boulder-create .hint { color: var(--text-muted); font-size: 0.8rem; margin-top: 6px; text-align: center; }

/* ========== Boulder Revise ========== */

.revision-hint { color: var(--warning-text); font-size: 0.9rem; background: var(--warning-bg); padding: 10px 14px; border-radius: 8px; margin-bottom: 12px; }
.reference-panel { margin-bottom: 12px; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: var(--bg-surface); }
.reference-panel > summary { padding: 10px 14px; cursor: pointer; font-size: 0.85rem; font-weight: 600; color: var(--text-heading); }
.reference-photo { position: relative; }
.reference-photo img { width: 100%; display: block; }
.reference-photo .hold-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
.reference-panel .hint { padding: 8px 14px; font-size: 0.78rem; color: var(--text-muted); margin: 0; }

/* Press-and-hold original reference (BoulderRevise). */
.reference-hold { margin-bottom: 12px; }
.btn-reference { width: 100%; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px;
    background: var(--bg-surface); color: var(--text-heading); font-size: 0.9rem; font-weight: 600;
    cursor: pointer; touch-action: none; -webkit-user-select: none; user-select: none; }
.btn-reference:active { background: var(--bg-surface-alt); border-color: var(--top-hold); }
.reference-hold .hint { margin-top: 6px; }
.reference-overlay { position: fixed; inset: 0; z-index: 1000; background: var(--overlay-bg);
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 16px; overflow: auto; }
.reference-overlay .reference-photo { max-width: 100%; max-height: 88vh; }
.reference-overlay .reference-photo img { width: auto; max-width: 100%; max-height: 88vh; }
.reference-note { margin-top: 10px; font-size: 0.78rem; color: #fff; opacity: 0.85; text-align: center; }

/* ========== Draft & staleness badges ========== */

.draft-badge { background: var(--warning-bg); color: var(--warning-text); font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 7px; border-radius: 5px; }
.bw-draft-notice { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; background: var(--warning-bg); color: var(--warning-text); padding: 12px 14px; border-radius: 10px; margin-bottom: 12px; font-size: 0.88rem; }
.bw-draft-notice .draft-actions { display: flex; gap: 8px; }
.btn-publish { background: var(--accent); color: #fff; border: none; border-radius: 7px; padding: 7px 14px; font-size: 0.82rem; font-weight: 600; cursor: pointer; }
.btn-publish:disabled { opacity: 0.5; cursor: not-allowed; }
.card-publish { background: var(--accent); color: #fff; border: none; border-radius: 6px; padding: 4px 10px; font-size: 0.72rem; font-weight: 600; cursor: pointer; }

/* ========== Join ========== */

.join-page { padding: 48px 20px; text-align: center; max-width: 400px; margin: 0 auto; }
.error-state h2 { color: var(--text-heading); margin-bottom: 8px; }
.error-state p { color: var(--text-secondary); margin-bottom: 20px; }

/* ========== Activity ========== */

.activity-page { padding: 16px; max-width: var(--page-max, 600px); margin: 0 auto; }

.activity-page .section { margin-bottom: 24px; }
.activity-page .section h2 { font-size: 1.1rem; font-weight: 600; color: var(--text-heading); margin-bottom: 10px; }
.profile-page .section h2 { font-size: 1.1rem; font-weight: 600; color: var(--text-heading); margin-bottom: 10px; }
.activity-page .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.activity-page .section-header h2 { margin-bottom: 0; }

.training-menu { position: relative; }
.btn-add-training {
    width: 36px; height: 36px; border-radius: 50%; background: var(--accent); color: white;
    border: none; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.btn-add-training:hover { background: var(--accent-hover); }
.training-dropdown {
    position: absolute; right: 0; top: 42px; background: var(--bg-surface); border-radius: 8px;
    box-shadow: 0 4px 16px var(--shadow-lg); overflow: hidden; z-index: 10; min-width: 140px;
}
.dropdown-item {
    display: block; width: 100%; text-align: left; padding: 10px 16px; color: var(--text);
    text-decoration: none; font-size: 0.9rem; font-weight: 500;
    background: none; border: none; cursor: pointer;
}
.dropdown-item:hover { background: var(--bg-surface-alt); }
.dropdown-item.danger { color: var(--danger-text); }

/* Simple centred modal (session wall picker). */
.bw-modal-backdrop {
    position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center;
    background: rgba(0, 0, 0, 0.45); padding: 20px;
}
.bw-modal {
    width: 100%; max-width: 360px; background: var(--bg-surface); border-radius: 14px;
    padding: 18px; box-shadow: 0 10px 40px var(--shadow-lg); display: flex; flex-direction: column; gap: 12px;
}
.bw-modal h3 { font-size: 1.05rem; font-weight: 600; color: var(--text-heading); }
.wall-pick-list { display: flex; flex-direction: column; gap: 8px; max-height: 50vh; overflow-y: auto; }
.wall-pick-item {
    text-align: left; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px;
    background: var(--bg-surface); color: var(--text); font-weight: 600; font-size: 0.95rem; cursor: pointer;
}
.wall-pick-item:hover { border-color: var(--accent); background: var(--accent-soft); }

/* Consenting-member list inside the kiosk "who did this?" dialog. Rows reuse .kiosk-row from the
   kiosk picker further down, so only the scroll container is new here. */
.kiosk-pick-list { display: flex; flex-direction: column; gap: 8px; max-height: 50vh; overflow-y: auto; }

/* GitHub-style contribution graph: fixed-metric cells that can be wider than the viewport, with the
   grid area scrolling horizontally (swipe / trackpad / overflow) so the page body never scrolls sideways.
   A vertical year selector sits to its left; it is omitted entirely when only one year has data. */
.contribution-section { display: flex; align-items: flex-start; gap: 10px; }
.year-selector { display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; }
.year-btn {
    padding: 4px 10px; border: 1px solid var(--border); border-radius: 6px;
    background: var(--input-bg); color: var(--text-secondary);
    font-size: 0.8rem; font-weight: 600; cursor: pointer; text-align: center;
}
.year-btn:hover { border-color: var(--accent); }
.year-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.contribution-scroll { flex: 1; min-width: 0; overflow-x: auto; }
.contribution-inner { display: inline-block; }
.contribution-grid { display: flex; gap: 3px; }
.grid-column { display: flex; flex-direction: column; gap: 3px; }
.grid-cell { width: 11px; height: 11px; border-radius: 2px; cursor: pointer; }
.grid-cell.intensity-0 { background: #ebedf0; }
.grid-cell.intensity-1 { background: #9be9a8; }
.grid-cell.intensity-2 { background: #40c463; }
.grid-cell.intensity-3 { background: #30a14e; }
.grid-cell.intensity-4 { background: #216e39; }

[data-theme="dark"] .grid-cell.intensity-0 { background: #2d2d3a; }

.month-labels { position: relative; width: 100%; height: 18px; margin-top: 4px; }
/* `left` is the month's FIRST column, so the label starts there rather than being centred on it:
   centring pushed the leftmost label half outside the scroll box, which clipped it. */
.month-label { position: absolute; font-size: 0.65rem; color: var(--text-secondary); white-space: nowrap; }

.grid-legend { display: flex; align-items: center; gap: 3px; margin-top: 2px; justify-content: flex-end; }
.legend-text { font-size: 0.7rem; color: var(--text-secondary); }
.legend-cell { width: 10px; height: 10px; cursor: default; }

.day-summary {
    padding: 16px; background: var(--bg-surface); border-radius: 12px;
    box-shadow: 0 2px 8px var(--shadow-md); margin-bottom: 16px;
}
.summary-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.summary-header h3 { font-size: 1rem; font-weight: 600; color: var(--text-heading); flex: 1; margin: 0; }
.duration-badge {
    padding: 2px 8px; background: var(--info-bg); color: var(--info-text);
    border-radius: 4px; font-size: 0.75rem; font-weight: 600;
}
.close-btn { background: none; border: none; color: var(--text-secondary); font-size: 1.1rem; cursor: pointer; padding: 4px 8px; }
.day-summary h4 { font-size: 0.85rem; font-weight: 600; color: var(--text-secondary); margin: 10px 0 6px; }
.summary-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 6px 8px; background: var(--bg-surface-alt); border-radius: 6px; margin-bottom: 4px;
}
.summary-name { font-size: 0.85rem; color: var(--text); font-weight: 500; }
.summary-meta { display: flex; align-items: center; gap: 6px; font-size: 0.75rem; }
.mini-grade { padding: 1px 6px; background: var(--accent); color: white; border-radius: 3px; font-weight: 700; }
.result-badge { padding: 1px 6px; border-radius: 3px; font-weight: 600; text-transform: uppercase; font-size: 0.65rem; }
.result-badge.flash { background: #FBE9E7; color: #D84315; }
.result-badge.send { background: var(--success-bg); color: var(--success-text); }
.result-badge.attempt { background: var(--warning-bg); color: var(--warning-text); }
.attempt-count { color: var(--text-secondary); }
.empty-day { text-align: center; color: var(--text-secondary); font-size: 0.85rem; padding: 12px; }

.progression-header { display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.progression-header h2 { margin-bottom: 0; }
.progression-title { display: flex; align-items: center; gap: 6px; }

/* Small round "(i)" button that opens the score-explanation dialog. */
.info-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0; border: none; border-radius: 50%;
    background: transparent; color: var(--text-secondary); cursor: pointer;
}
.info-btn:hover { background: var(--bg-surface-alt); color: var(--accent); }

/* Score-explanation dialog (reuses .bw-modal / .bw-modal-backdrop). */
.score-info-modal { max-width: 440px; max-height: 85vh; }
.score-info-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.score-info-head h3 { margin: 0; }
.score-info-body { overflow-y: auto; }
.score-info-body h4 { font-size: 0.9rem; color: var(--text-heading); margin: 12px 0 4px; }
.score-info-body h4:first-child { margin-top: 0; }
.score-info-body p { font-size: 0.88rem; line-height: 1.55; color: var(--text-secondary); margin-bottom: 8px; }
.window-select {
    padding: 4px 8px; border: 1px solid var(--border); border-radius: 6px;
    font-size: 0.8rem; background: var(--input-bg); color: var(--text);
}

.score-cards { display: flex; gap: 12px; margin-bottom: 16px; }
.score-card {
    flex: 1; padding: 16px; background: var(--bg-surface); border-radius: 12px;
    box-shadow: 0 2px 8px var(--shadow-md); text-align: center;
}
.score-label { display: block; font-size: 0.75rem; color: var(--text-secondary); font-weight: 600; text-transform: uppercase; }
.score-value { display: block; font-size: 1.6rem; font-weight: 700; color: var(--text-heading); margin: 4px 0; }
.score-grade { display: block; font-size: 0.85rem; color: var(--accent); font-weight: 600; }

.chart-section { margin-bottom: 12px; }
.chart-section h4 { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 6px; }
.mini-chart {
    height: 60px; background: var(--bg-surface); border-radius: 8px;
    box-shadow: 0 1px 4px var(--shadow-sm); padding: 8px;
}
.chart-svg { width: 100%; height: 100%; }

.day-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 10px 12px; background: var(--bg-surface); border-radius: 8px;
    margin-bottom: 6px; box-shadow: 0 1px 3px var(--shadow-sm);
    cursor: pointer;
}
.day-row:hover { box-shadow: 0 2px 8px var(--shadow-md); }
.day-date { font-size: 0.9rem; color: var(--text); font-weight: 500; }
.day-dots { display: flex; gap: 3px; }
.dot { width: 8px; height: 8px; border-radius: 50%; }
.dot.intensity-1 { background: #9be9a8; }
.dot.intensity-2 { background: #40c463; }
.dot.intensity-3 { background: #30a14e; }
.dot.intensity-4 { background: #216e39; }

/* Activity page inside a carousel: let .wall-carousel-root own width/height. */
.activity-page { padding: 0; max-width: none; }
.window-control { display: flex; gap: 8px; }

/* The "N activities" score card doubles as a button that swipes to the list page. */
.score-card-action { border: none; cursor: pointer; font: inherit; }
.score-card-action:hover { box-shadow: 0 3px 12px var(--shadow-md); }

/* ---- Progression line charts (ProgressionChart.razor) ---- */
.prog-chart { margin-bottom: 16px; }
.prog-chart-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.prog-chart-title { font-size: 0.85rem; color: var(--text-secondary); margin: 0; }
.prog-chart-caption { font-size: 0.7rem; color: var(--text-secondary); opacity: 0.75; }
.prog-chart-empty {
    height: 72px; display: flex; align-items: center; justify-content: center;
    background: var(--bg-surface); border-radius: 8px; color: var(--text-secondary); font-size: 0.8rem;
    box-shadow: 0 1px 4px var(--shadow-sm);
}
.prog-chart-body {
    position: relative; height: 128px; background: var(--bg-surface); border-radius: 8px;
    box-shadow: 0 1px 4px var(--shadow-sm); padding: 10px 10px 10px 36px;
    touch-action: pan-y; cursor: crosshair;
    user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
}
.prog-chart-svg { width: 100%; height: 100%; display: block; }
.prog-grid { stroke: var(--border); opacity: 0.45; }
.prog-trend { stroke-width: 1.5; stroke-dasharray: 4 3; opacity: 0.65; }
.prog-chart-yaxis { position: absolute; left: 6px; top: 10px; bottom: 10px; width: 30px; pointer-events: none; }
.prog-ytick { position: absolute; left: 0; transform: translateY(-50%); font-size: 0.6rem; color: var(--text-secondary); white-space: nowrap; }
.prog-chart-xaxis { position: relative; height: 14px; margin: 2px 10px 0 36px; }
.prog-xtick { position: absolute; transform: translateX(-50%); font-size: 0.6rem; color: var(--text-secondary); white-space: nowrap; }
.prog-chart-guide { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--text-secondary); opacity: 0.5; display: none; pointer-events: none; }
.prog-chart-dot { position: absolute; width: 9px; height: 9px; border-radius: 50%; transform: translate(-50%, -50%); display: none; pointer-events: none; box-shadow: 0 0 0 2px var(--bg-surface); }
.prog-chart-tip {
    position: absolute; top: 6px; background: var(--text-heading); color: var(--bg-surface);
    font-size: 0.7rem; font-weight: 600; padding: 3px 7px; border-radius: 6px; white-space: nowrap;
    display: none; pointer-events: none; z-index: 3;
}

/* ---- Activity rows (recent + full list) ---- */
.activity-row {
    display: flex; justify-content: space-between; align-items: center; gap: 12px;
    padding: 10px 12px; background: var(--bg-surface); border-radius: 8px; margin-bottom: 6px;
    box-shadow: 0 1px 3px var(--shadow-sm); text-decoration: none; color: inherit;
}
.activity-row:hover { box-shadow: 0 2px 8px var(--shadow-md); }
.activity-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.activity-date { font-size: 0.9rem; font-weight: 600; color: var(--text-heading); }
.activity-meta { font-size: 0.75rem; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.activity-duration { font-size: 0.9rem; font-weight: 600; color: var(--accent); flex: 0 0 auto; }
.list-hint { font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 10px; }

/* ---- Activity detail page ---- */
.activity-detail { padding: 16px; max-width: var(--page-max, 600px); margin: 0 auto; }
.activity-detail .page-header { margin: 8px 0 16px; }
.activity-detail .page-header h1 { font-size: 1.4rem; font-weight: 700; color: var(--text-heading); }
.detail-sub { font-size: 0.85rem; color: var(--text-secondary); }
.duration-editor {
    background: var(--bg-surface); border-radius: 12px; padding: 14px 16px; margin-bottom: 20px;
    box-shadow: 0 1px 4px var(--shadow-sm);
}
.duration-label { display: block; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; color: var(--text-secondary); margin-bottom: 8px; }
.duration-row { display: flex; align-items: center; gap: 8px; }
.duration-input {
    width: 90px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px;
    background: var(--input-bg); color: var(--text); font-size: 1rem;
}
.duration-unit { font-size: 0.85rem; color: var(--text-secondary); }
.duration-saved { font-size: 0.8rem; color: var(--success-text, #2e9e6b); font-weight: 600; }
.duration-hint { display: block; margin-top: 8px; font-size: 0.72rem; color: var(--text-secondary); }

/* ========== Profile ========== */

.profile-page { padding: 16px; max-width: var(--page-max, 500px); margin: 0 auto; }

.profile-card {
    text-align: center; padding: 32px 20px; background: var(--bg-surface);
    border-radius: 16px; box-shadow: 0 2px 12px var(--shadow-md);
    margin-bottom: 20px;
}
.avatar {
    width: 64px; height: 64px; border-radius: 50%; background: var(--secondary);
    color: white; font-size: 1.8rem; font-weight: 700; display: flex;
    align-items: center; justify-content: center; margin: 0 auto 12px;
    text-transform: uppercase;
}
img.avatar { object-fit: cover; }
/* Compact avatar for inline author/member rows next to a name. */
.avatar-sm {
    width: 26px; height: 26px; font-size: 0.8rem; margin: 0;
    display: inline-flex; vertical-align: middle; flex-shrink: 0;
}
.profile-card h1 { font-size: 1.3rem; font-weight: 700; color: var(--text-heading); margin-bottom: 6px; }
.role-badge {
    display: inline-block; padding: 4px 12px; background: var(--info-bg);
    color: var(--secondary); border-radius: 12px; font-size: 0.8rem; font-weight: 600;
}
.member-since { color: var(--text-secondary); font-size: 0.85rem; margin-top: 8px; }

/* Profile edit fields (display name + avatar) */
.field-group { margin-bottom: 18px; }
.field-label { display: block; font-size: 0.85rem; font-weight: 600; color: var(--text-heading); margin-bottom: 6px; }
.field-group .text-input {
    width: 100%; padding: 10px 12px; font-size: 0.95rem;
    border: 1px solid var(--border); border-radius: var(--r-md);
    background: var(--bg-surface); color: var(--text);
}
.field-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
.field-actions .btn-primary { padding: 10px 20px; font-size: 0.95rem; }
.field-actions .btn-secondary { width: auto; margin-bottom: 0; padding: 10px 16px; font-size: 0.95rem; }
.field-actions .btn-primary[disabled],
.field-actions .btn-secondary[disabled] { opacity: 0.5; cursor: not-allowed; }

/* Consistent vertical rhythm for every profile section, matching the collapsible cards' gap. */
.profile-page .section { margin-bottom: 16px; }

/* Non-collapsible section cards (Home wall, TopLogger import) — same surface/border/radius as
   the .profile-collapsible cards so the whole page reads as one consistent stack. */
.profile-section-card {
    background: var(--bg-surface); border: 1px solid var(--border);
    border-radius: var(--r-md); box-shadow: 0 1px 2px var(--shadow-sm);
    padding: 16px;
}

/* ---- Staged profile shell -------------------------------------------------
   /profile is content the app authors end to end, so it is a fixed composition sized to the viewport
   instead of a 3.4-viewport scroll: a persistent identity/links side column plus four tabbed screens.
   Scoped to .profile-shell so the settings pages that reuse the plain .profile-page shell (API keys,
   delete account) keep their narrow single-column layout. ---- */
.profile-shell { max-width: var(--profile-max, 1180px); }

.profile-columns { display: flex; flex-direction: column; gap: 16px; align-items: stretch; }
.profile-shell .profile-side > .profile-card { margin-bottom: 16px; }
.profile-shell .profile-side .section:last-child { margin-bottom: 0; }
.profile-main { min-width: 0; }

/* Tab rail. Horizontal, scrollable on a phone rather than wrapping into two rows. */
.profile-tabs {
    display: flex; gap: 6px; margin-bottom: 14px; padding-bottom: 2px;
    overflow-x: auto; scrollbar-width: none;
}
.profile-tabs::-webkit-scrollbar { display: none; }
.profile-tab {
    flex: 0 0 auto; padding: 9px 16px; border-radius: 999px; cursor: pointer;
    background: var(--bg-surface); color: var(--text-secondary);
    border: 1px solid var(--border); font-size: 0.9rem; font-weight: 600;
}
.profile-tab:hover { color: var(--text-heading); border-color: var(--text-secondary); }
.profile-tab.is-active {
    background: var(--secondary); border-color: var(--secondary); color: #fff;
}

/* Cards inside a screen flow into as many columns as the width allows, so a 1440px desktop never
   shows a full-width row holding one dropdown. */
.profile-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
    gap: 14px; align-items: start;
}
.profile-shell .profile-grid > .section { margin-bottom: 0; }
.profile-shell .profile-grid > .profile-collapsible { margin-bottom: 0; }
.profile-grid-wide { grid-column: 1 / -1; }
.profile-notify-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 2px 18px; }

/* The two progression charts sit side by side once there is room for both. */
.profile-chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; }

/* Display-name editor inside the centred identity card: a disclosure whose summary reads as a
   normal field label, so the card stays short enough to keep the section tabs above the fold. */
.profile-shell .profile-name-edit { text-align: left; margin: 18px 0 0; }
.profile-shell .profile-name-edit > summary {
    cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px;
    font-size: 0.9rem; font-weight: 600; color: var(--text-heading);
}
.profile-shell .profile-name-edit > summary::-webkit-details-marker { display: none; }
.profile-shell .profile-name-edit > summary::before {
    content: ''; width: 0; height: 0; flex: none;
    border-left: 6px solid currentColor;
    border-top: 5px solid transparent; border-bottom: 5px solid transparent;
    transition: transform 0.15s ease;
}
.profile-shell .profile-name-edit[open] > summary::before { transform: rotate(90deg); }
.profile-shell .profile-name-edit[open] > summary { margin-bottom: 10px; }
.profile-shell .profile-name-edit > summary + .field-label { display: none; }

/* Inline "go to the Account tab" affordance where a cross-tab anchor used to point. */
.profile-link-button {
    background: none; border: none; padding: 0; font: inherit; cursor: pointer;
    color: var(--secondary); font-weight: 600; text-decoration: underline;
}

.profile-shell .profile-danger-card h2 { color: var(--danger-text); }

/* Below the two-column breakpoint the identity card goes horizontal — a phone's first viewport has
   to reach the section tabs, and a 300px-tall centred header card eats it. */
@media (max-width: 899px) {
    .profile-shell .profile-side .profile-card {
        display: grid; grid-template-columns: 64px 1fr; gap: 2px 14px;
        text-align: left; padding: 16px; align-items: center;
    }
    .profile-shell .profile-side .profile-card > .avatar-edit,
    .profile-shell .profile-side .profile-card > .avatar {
        grid-column: 1; grid-row: 1 / span 3; margin: 0; align-self: center;
    }
    .profile-shell .profile-side .profile-card > h1 { grid-column: 2; margin-bottom: 0; }
    .profile-shell .profile-side .profile-card > .role-badge { grid-column: 2; justify-self: start; }
    .profile-shell .profile-side .profile-card > .member-since { grid-column: 2; margin-top: 2px; }
    .profile-shell .profile-side .profile-card > .profile-name-edit,
    .profile-shell .profile-side .profile-card > .field-error { grid-column: 1 / -1; }
    .profile-shell .profile-side .profile-card > .profile-name-edit { margin-top: 12px; }
}

@media (max-width: 480px) {
    .profile-tab { padding: 9px 12px; font-size: 0.85rem; }
}

/* Two columns from tablet up: the identity/links rail keeps its place while the screens swap. */
@media (min-width: 900px) {
    .profile-columns { flex-direction: row; align-items: flex-start; }
    .profile-side { flex: 0 0 300px; max-width: 300px; }
    .profile-main { flex: 1 1 auto; }
}

/* Home wall dropdown left-aligned like a normal form control (was right-aligned in a flex header). */
.home-wall-select { display: block; margin-bottom: 4px; }

.profile-page .chart-section h2 { font-size: 1rem; font-weight: 600; color: var(--text-heading); margin-bottom: 8px; }
.chart-box {
    height: 80px; background: var(--bg-surface); border-radius: 12px;
    box-shadow: 0 2px 8px var(--shadow-md); padding: 12px;
}
.chart-empty { text-align: center; color: var(--text-secondary); font-size: 0.85rem; margin: 0; line-height: 56px; }

.activity-hint { text-align: center; color: var(--text-secondary); font-size: 0.85rem; margin-top: 8px; }
.activity-hint a { color: var(--secondary); font-weight: 600; text-decoration: underline; }

/* ========== Privacy / About ========== */

.legal-page { padding: 24px 16px; max-width: var(--page-max, 600px); margin: 0 auto; }
.legal-page h1 { font-size: 1.5rem; font-weight: 700; color: var(--text-heading); margin-bottom: 16px; }
.legal-page h2 { font-size: 1.1rem; font-weight: 600; color: var(--text-heading); margin: 20px 0 8px; }
.legal-page p { color: var(--text); font-size: 0.95rem; line-height: 1.6; margin-bottom: 12px; }
.legal-page ul { padding-left: 20px; margin-bottom: 12px; }
.legal-page li { color: var(--text); font-size: 0.95rem; line-height: 1.6; margin-bottom: 4px; }

/* ========== Layout tier (desktop breakpoint + manual override) ==========
 *
 * ONE breakpoint defines the desktop tier: at >= 900px the app drops the ~600px phone column and
 * uses the window. 900px is chosen off the evidence rather than a device list — the de-facto
 * column is 600px plus 16px of padding a side (632px of content), so anything under ~900px has
 * no meaningful room to spend and would only trade a comfortable reading column for a stretched
 * one. It also clears tablet portrait (768px), where the phone column still reads correctly, and
 * sits well above the only other breakpoint in this file (560px), so the two never interact.
 *
 * The manual toggle (theme.js / TopBarActions) is still honoured at BOTH ends, so it never
 * becomes a no-op: `html.bw-fullscreen` forces the desktop tier on a narrow window, and
 * `html.bw-compact` forces the phone column back on a wide one.
 *
 * Nothing below is written twice. The tier only flips switch custom properties; the layout rules
 * that read them stay where they already live (the page rules keep their own cap as the var
 * fallback, the photo boxes keep their single sizing block). Two polarities, for one reason:
 *   - Page width caps are WIDE-triggered, because every page's narrow cap differs (600/500/none)
 *     and `var(--page-max, <that page's cap>)` keeps it in exactly one place.
 *   - The wall photo box is NARROW-triggered, because its desktop values contain `--fit-aspect`,
 *     which bwViewport publishes per ELEMENT — so it cannot be hoisted to a :root switch and has
 *     to be the fallback written at the use site.
 */

/* Desktop tier: lift the page column cap, drop the short-window floor under the photo box, and
 * switch the two detail pages from one stacked column to photo-left / rail-right (the layout
 * rules themselves live with their pages above and read these switches; nothing is written
 * twice). Both selectors are :root-bumped so they outrank the `:root:root` chrome block in
 * app.css, which is where --wall-stats-h and --boulder-photo-chrome are defined: with the stats
 * and the rows under the photo moved into the rail, the photo no longer has to budget for them
 * and grows into the height they used to take. */
/* Step 1 — earn the marker. Two ways in (a wide window, or the manual toggle), one word out.
 * These two lines are the ONLY thing written twice, and they carry no layout. */
@media (min-width: 900px) {
    html:not(.bw-compact) { --tier: desktop; }
}
html.bw-fullscreen { --tier: desktop; }

/* Step 2 — spend it, once. A style container query: every element is a style container, so the
 * <html> that carries --tier is the query container for <body>, and the whole payload lands on
 * body and inherits down to every page. Setting it on body (not :root) is also what lets it
 * beat the `:root:root` chrome block in app.css — --wall-photo-chrome and
 * --boulder-photo-chrome are defined there, and a declaration on the element itself outranks
 * anything inherited.
 * A browser without style queries simply keeps the phone tier, which is the safe degradation. */
@container style(--tier: desktop) {
    body {
        --page-max: none;
        --photo-fit-floor: 0px;
    }
}

/* Step 3 — the two-column payload, which needs actual width and not just the marker. Wrapping
 * the same style query in the width media query gives exactly the three cases we want:
 *   wide window                -> two columns;
 *   wide window + .bw-compact  -> no marker, so one column (the toggle still means something);
 *   narrow + .bw-fullscreen    -> marker but no width, so the caps lift as they always did and
 *                                 the page stays one column instead of collapsing the photo
 *                                 into a 0px grid track next to a 340px rail.
 * With the stats bar and the rows under the boulder photo moved into the right-hand rail, the
 * photos no longer have to budget for them and grow into the height they used to take. */
@media (min-width: 900px) {
    @container style(--tier: desktop) {
        body {
            /* Wall detail: photo left, rail right. */
            --wall-main-display: grid;
            --wall-main-align: start;
            --wall-side-display: flex;
            --wall-side-max-h: calc(100dvh - var(--topbar-h) - var(--bottomnav-h) - 48px);
            --stats-wrap: wrap;
            --stat-basis: 130px;

            /* Boulder LIST (wall carousel page 4): the wall page loses --page-max on this tier,
               so a 31-card list was one 600px-ish column of cards in a 1440px slide and nearly
               four viewports of scroll. Flow it into as many ~360px columns as fit instead.
               Read only by .boulders-page, so no other list is affected. */
            --boulder-list-display: grid;
            --boulder-list-columns: repeat(auto-fill, minmax(360px, 1fr));

            /* Boulder detail: header + photo left, interaction rail right. */
            --boulder-detail-display: grid;
            --boulder-col-display: block;
            --boulder-wide-display: grid;
            --boulder-photo-col-position: sticky;
            --boulder-page-max: 1320px;
            --boulder-photo-chrome: 190px;
        }

        /* The stats term of the wall photo's chrome (app.css) pays for the stat cards UNDER the
           photo. On the desktop tier they sit beside it instead, so the photo gets that height
           back — but only while the rail is actually there: in edit mode the rail is not
           rendered, the editor has the full width, and its docked toolbar still needs the room
           the stats used to take. Hence the :has() rather than a flat override on body. */
        .wall-main:has(> .wall-side) { --wall-photo-chrome: var(--wall-photo-chrome-base); }
    }
}

/* Phone tier: neutralise the desktop aspect-capping on the wall photo box (see below). */
@media (max-width: 899.98px) {
    html:not(.bw-fullscreen) {
        --wall-photo-aspect: auto;
        --wall-photo-cap: none;
        --wall-photo-center: 0;
    }
}
html.bw-compact {
    --wall-photo-aspect: auto;
    --wall-photo-cap: none;
    --wall-photo-center: 0;
}

/* Contain the photo/overlay views within the window on the desktop tier.
 *
 * Desktop removes the 600px width cap above, so on a wide window the photo box would grow
 * full-width and — because its height follows the image aspect ratio — become far taller than
 * the window, forcing a scroll. We instead cap the box WIDTH at `availableHeight * aspect` (the
 * image aspect published as `--fit-aspect` by bwViewport.fitBox). `width: auto` then settles at
 * `min(container width, availableHeight * aspect)`, so the box uses as much width as it can while
 * its height stays within the window. Purely CSS + `100dvh`, so it re-fits on resize on its own. */
/* Scale the boulder image so the header + view + tags + Attempt/Send/Flash buttons all fit one
 * screen. bwViewport.fitBox pins this box's aspect-ratio inline and publishes it as --fit-aspect,
 * so capping the HEIGHT (and the matching WIDTH via that aspect) shrinks the image to make room
 * for the rows below instead of pushing the buttons off-screen — the same trick the wall photo
 * uses. --boulder-photo-chrome (app.css) budgets everything else in the slice; --bottomnav-h
 * collapses when the tab bar hides, reclaiming that space automatically. The phone tier floors
 * the box at 180px so a very short window shrinks it gracefully instead of to nothing (the
 * description/lower sections then become the scrollable overflow); the desktop tier sets
 * --photo-fit-floor to 0 so the box is free to use the whole height budget. That floor is the
 * ONLY thing the two tiers disagree on here, which is why this is one block and not two. */
.boulder-detail .photo-viewport.photo-section {
    --avail-h: calc(100dvh - var(--topbar-h) - var(--bottomnav-h) - var(--boulder-photo-chrome));
    --fit-h: max(var(--photo-fit-floor, 180px), var(--avail-h));
    width: auto;
    height: auto;
    max-height: var(--fit-h);
    max-width: min(100%, calc(var(--fit-h) * var(--fit-aspect, 1)));
    margin-inline: auto;
}

/* Wall photo box. The width cap / centring / aspect sizing are desktop-only; the phone tier
 * switches them off through the three --wall-photo-* vars above, so the box falls back to the
 * flex-driven, full-width behaviour of the base rule further up this file. --avail-h is the same
 * viewport slice that base rule caps to, so max-height is identical in both tiers.
 * Note: `.wall-main .photo-viewport.bw-framed` (higher specificity, further up) keeps owning
 * aspect-ratio/max-height for the framed read-only view in BOTH tiers — with the same values —
 * while the desktop-only width cap below still applies to it. */
.wall-main .photo-viewport {
    --avail-h: calc(100dvh - var(--topbar-h) - var(--bottomnav-h) - var(--wall-photo-chrome));
    flex: 0 1 auto;                        /* size to the contained photo, don't stretch the column */
    aspect-ratio: var(--wall-photo-aspect, var(--fit-aspect, 1.4));
    height: auto;
    width: auto;
    max-height: var(--avail-h);            /* hard safety cap: never exceed the card height */
    max-width: var(--wall-photo-cap, min(100%, calc(var(--avail-h) * var(--fit-aspect, 1.4))));
    margin-inline: var(--wall-photo-center, auto);  /* centre within the full-width card */
}

/* The remaining manual-override rules below are deliberately NOT on the tier switch: they widen
 * an already desktop-sized dialog (1100px), so the desktop tier changes nothing for them and the
 * explicit toggle stays the only way to go edge to edge. */

/* ========== Administration ========== */

/* The dashboard is the one page container without a gutter of its own, so on the desktop tier the
 * heading and the wall table sit flush against the window edge. Give it the same 16px side gutter
 * every other page container uses (.wall-list, .activity-page, .profile-page, …).
 *
 * Desktop-only, and through the --tier style switch rather than a fresh media query, so the phone
 * tier keeps the edge-to-edge rows it has today. --tier inherits from <html>, so the parent of
 * .admin-dashboard is a valid query container for it, exactly as <body> is for the block above. */
@container style(--tier: desktop) {
    .admin-dashboard { padding-inline: 16px; }
}

/* ========== Personal API Keys ========== */

.apikey-page { padding: 16px; max-width: 640px; margin: 0 auto; }
.apikey-page h1 { font-size: 1.4rem; color: var(--text-heading); margin-bottom: 10px; }
.apikey-page h2 { font-size: 1rem; color: var(--text-heading); margin-bottom: 10px; }
.apikey-blurb { font-size: 0.85rem; color: var(--text-secondary); line-height: 1.6; margin-bottom: 18px; }
.apikey-blurb code { font-size: 0.8rem; background: var(--bg-muted, rgba(127, 127, 127, 0.12)); border-radius: 4px; padding: 1px 4px; }

.apikey-create, .apikey-list { margin-top: 22px; }

.apikey-reveal {
    background: var(--bg-surface);
    border: 2px solid var(--accent, #e94560);
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 18px;
    box-shadow: 0 6px 18px var(--shadow-lg, rgba(0, 0, 0, 0.18));
}

.apikey-reveal-warning { font-size: 0.8rem; font-weight: 600; color: var(--accent, #e94560); line-height: 1.5; margin-bottom: 10px; }

.apikey-token {
    display: block;
    word-break: break-all;
    user-select: all;
    font-size: 0.85rem;
    background: var(--bg-muted, rgba(127, 127, 127, 0.12));
    border-radius: 8px;
    padding: 10px 12px;
    margin-bottom: 12px;
}

.apikey-reveal-actions { display: flex; flex-wrap: wrap; gap: 8px; }

.apikey-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 10px 12px;
    margin-bottom: 8px;
}

.apikey-row.revoked, .apikey-row.expired { opacity: 0.6; }
.apikey-row.revoked { border-style: dashed; }
.apikey-row-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.apikey-name { font-size: 0.92rem; font-weight: 600; color: var(--text-heading); }
.apikey-row.revoked .apikey-name, .apikey-row.expired .apikey-name { text-decoration: line-through; }
.apikey-prefix { font-size: 0.78rem; color: var(--text-secondary); }
.apikey-meta { font-size: 0.72rem; color: var(--text-secondary); }
.apikey-row-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.apikey-confirm-text { font-size: 0.78rem; font-weight: 600; color: var(--text-heading); }

/* Per-tablet anonymous-setting opt-out; only kiosk keys in the wall panel render it. Own full-width
   line inside the wrapping row, so the checkbox and its hint never squeeze the name or the actions. */
.apikey-row-option { flex: 1 1 100%; order: 3; margin-top: 2px; }
.apikey-option-label { display: flex; align-items: center; gap: 7px; font-size: 0.8rem; cursor: pointer; }
.apikey-row-option .hint { margin: 3px 0 0 22px; font-size: 0.72rem; }

/* Scope badge next to a key's name; only kiosk keys carry one today. */
.apikey-scope {
    font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
    border-radius: 999px; padding: 1px 7px; margin-left: 6px; vertical-align: middle;
}
.apikey-scope.kiosk { background: var(--info-bg); color: var(--info-text); }

/* Kiosk consent, shown under the current user's own row in the members list. */
.kiosk-consent-line {
    display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
    margin: -2px 0 8px; padding: 0 12px 0 12px;
}
.kiosk-consent-state { font-size: 0.78rem; font-weight: 600; color: var(--text-secondary); }
.kiosk-consent-confirm { font-size: 0.78rem; font-weight: 600; color: var(--text-heading); }
.kiosk-consent-btn {
    padding: 4px 10px; border: 1px solid var(--border); background: var(--bg-surface); color: var(--text);
    border-radius: 6px; font-size: 0.78rem; font-weight: 600; cursor: pointer; min-height: 30px;
}
.kiosk-consent-btn:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-soft); }
.kiosk-consent-btn:disabled { opacity: 0.55; cursor: default; }
.kiosk-consent-btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.kiosk-consent-btn.danger { color: var(--danger-text); border-color: var(--danger-text); }

.kiosk-modal-text { font-size: 0.85rem; line-height: 1.6; color: var(--text-secondary); }
.kiosk-modal-warning {
    font-size: 0.82rem; line-height: 1.55; color: var(--warning-text, var(--text-heading));
    background: var(--warning-bg, var(--bg-surface-alt)); border-radius: 8px; padding: 10px 12px;
}
.kiosk-pin-toggle {
    display: flex; align-items: center; gap: 8px; font-size: 0.88rem; font-weight: 600;
    color: var(--text); cursor: pointer;
}
.kiosk-modal-actions { display: flex; justify-content: flex-end; gap: 8px; }

.apikey-state {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-secondary);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 2px 8px;
}

.apikey-btn {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-heading);
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 6px 10px;
    cursor: pointer;
}

.apikey-btn:disabled { opacity: 0.5; cursor: default; }
.apikey-btn.primary { background: var(--accent, #e94560); border-color: var(--accent, #e94560); color: #fff; }
.apikey-btn.danger { background: var(--danger, #c62828); border-color: var(--danger, #c62828); color: #fff; }
.apikey-empty { font-size: 0.85rem; color: var(--text-secondary); }

.apikey-docs-toggle { margin-bottom: 8px; }
.apikey-docs {
    margin-bottom: 18px;
    padding: 14px;
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: 12px;
}

.apidocs-surface { margin-top: 20px; }
.apidocs-surface:first-of-type { margin-top: 12px; }
.apidocs-surface-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.apidocs-surface-title { font-size: 1rem; color: var(--text-heading); margin: 0; }
.apidocs-scope {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--accent, #e94560);
    border: 1px solid var(--accent, #e94560);
    border-radius: 999px;
    padding: 1px 8px;
}
.apidocs-surface-desc { font-size: 0.8rem; color: var(--text-secondary); line-height: 1.6; margin: 6px 0 4px; }

.apidocs-endpoint {
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 10px 12px;
    margin-top: 10px;
    background: var(--bg-muted, rgba(127, 127, 127, 0.06));
}
.apidocs-endpoint-soon { border-style: dashed; }
.apidocs-endpoint-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.apidocs-method {
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: #fff;
    background: var(--text-secondary);
    border-radius: 6px;
    padding: 2px 7px;
    min-width: 3.2em;
    text-align: center;
}
.apidocs-method.get { background: #2e7d32; }
.apidocs-method.post { background: #1565c0; }
.apidocs-method.delete { background: var(--danger, #c62828); }
.apidocs-method.soon { background: var(--text-secondary); }
.apidocs-path { font-size: 0.82rem; word-break: break-all; color: var(--text-heading); }
.apidocs-summary { font-size: 0.8rem; color: var(--text-secondary); line-height: 1.55; margin: 6px 0 0; }

.apidocs-params { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.apidocs-params li { font-size: 0.76rem; color: var(--text-secondary); line-height: 1.5; }
.apidocs-params code { font-size: 0.76rem; background: var(--bg-muted, rgba(127, 127, 127, 0.12)); border-radius: 4px; padding: 1px 4px; }
.apidocs-param-loc {
    font-size: 0.64rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-secondary);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 0 6px;
    margin: 0 4px;
}

.apidocs-block-label {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-secondary);
    margin: 10px 0 4px;
}
.apidocs-pre {
    font-size: 0.76rem;
    line-height: 1.5;
    background: var(--bg-muted, rgba(127, 127, 127, 0.12));
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 10px 12px;
    margin: 0;
    overflow-x: auto;
    white-space: pre;
}
.apidocs-note { font-size: 0.74rem; color: var(--text-secondary); line-height: 1.5; margin: 8px 0 0; font-style: italic; }

.profile-links { text-align: center; margin-top: 12px; margin-bottom: 24px; font-size: 0.85rem; }

/* ========== Wall details page (temperature, gallery, wall API keys) ========== */

.wall-panel-head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
}
.wall-panel-head h2 { margin-bottom: 0; }
.wall-panel-hint { font-size: 0.78rem; color: var(--text-secondary); }

.wall-range-row { display: flex; gap: 4px; }
.wall-range-btn {
    font-size: 0.75rem; font-weight: 600; padding: 4px 9px; cursor: pointer;
    color: var(--text-secondary); background: var(--bg-surface);
    border: 1px solid var(--border); border-radius: 999px;
}
.wall-range-btn.active { background: var(--accent, #e94560); border-color: var(--accent, #e94560); color: #fff; }
.wall-range-btn:disabled { opacity: 0.5; cursor: default; }

.wall-temp-latest { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.wall-temp-value {
    font-family: var(--font-display); font-size: 2rem; font-weight: 700;
    letter-spacing: -0.03em; color: var(--text-heading);
}
.wall-temp-when { font-size: 0.8rem; color: var(--text-secondary); }
.wall-temp-empty { font-size: 0.85rem; color: var(--text-secondary); line-height: 1.6; }
.wall-temp-empty code {
    font-size: 0.78rem; border-radius: 4px; padding: 1px 4px;
    background: var(--bg-muted, rgba(127, 127, 127, 0.12));
}

.wall-gallery-grid {
    display: grid; gap: 6px; margin-bottom: 10px;
    grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
}
.wall-gallery-thumb {
    position: relative; padding: 0; border: none; cursor: pointer; overflow: hidden;
    aspect-ratio: 1 / 1; border-radius: 8px; background: var(--bg-muted, rgba(127, 127, 127, 0.12));
}
.wall-gallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wall-gallery-badge {
    position: absolute; top: 4px; left: 4px; font-size: 0.62rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 6px; border-radius: 4px;
    color: #fff; background: rgba(0, 0, 0, 0.55);
}
.wall-gallery-badge.uploaded { background: rgba(46, 158, 107, 0.85); }
.wall-gallery-badge.current { background: rgba(233, 69, 96, 0.85); }
.wall-gallery-badge.historic { background: rgba(15, 52, 96, 0.85); }
.wall-gallery-when {
    position: absolute; bottom: 0; left: 0; right: 0; padding: 3px 5px;
    font-size: 0.65rem; color: #fff; background: rgba(0, 0, 0, 0.45); text-align: right;
}
.wall-gallery-more { display: block; margin: 0 auto; }
.wall-gallery-empty { font-size: 0.85rem; color: var(--text-secondary); }

.wall-lightbox {
    position: fixed; inset: 0; z-index: 60; outline: none;
    display: flex; flex-direction: column; background: rgba(0, 0, 0, 0.92);
}
.wall-lightbox-bar {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; padding: 12px 16px; color: #fff;
}
.wall-lightbox-title { font-size: 0.9rem; font-weight: 600; }
.wall-lightbox-close {
    font-size: 1.6rem; line-height: 1; color: #fff; background: none;
    border: none; cursor: pointer; padding: 0 4px;
}
.wall-lightbox-stage {
    flex: 1 1 auto; min-height: 0; display: flex; align-items: center;
    justify-content: center; gap: 6px; padding: 0 6px;
}
.wall-lightbox-img { max-width: 100%; max-height: 100%; object-fit: contain; }
.wall-lightbox-nav {
    flex: 0 0 auto; font-size: 2rem; line-height: 1; color: #fff; cursor: pointer;
    background: rgba(255, 255, 255, 0.12); border: none; border-radius: 999px; padding: 6px 14px;
}
.wall-lightbox-nav:disabled { opacity: 0.25; cursor: default; }
.wall-lightbox-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; flex-wrap: wrap; padding: 12px 16px; color: rgba(255, 255, 255, 0.85);
}
.wall-lightbox-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 0.78rem; }
.wall-lightbox-meta .wall-gallery-badge { position: static; }
.wall-lightbox-actions { display: flex; align-items: center; gap: 8px; }
.wall-lightbox-actions .apikey-confirm-text { color: #fff; font-size: 0.78rem; }

.wall-apikey-panel .apikey-blurb { margin-bottom: 14px; }
.wall-apikey-list { margin-top: 16px; }

/* ── Big-wall multi-image panel grid + overlap confirmation ───────────────── */
.multi-image-toggle { margin-bottom: 10px; font-weight: 600; }

.panel-grid {
    display: grid;
    gap: 6px;
    justify-content: center;
    margin: 8px 0 4px;
}
.panel-cell {
    position: relative;
    aspect-ratio: 1 / 1;
    border-radius: var(--r-md);
    overflow: hidden;
    min-width: 64px;
}
.panel-thumb {
    border: 1px solid var(--border);
    background: var(--bg-surface-alt);
}
.panel-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.panel-add-card {
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px dashed var(--border);
    background: var(--bg-surface-alt);
    color: var(--text-secondary);
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s;
}
.panel-add-card:hover { border-color: var(--accent); color: var(--accent); }
.panel-add-plus { font-size: 1.8rem; font-weight: 300; line-height: 1; }
.panel-add-input {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
    width: 100%;
    height: 100%;
}

.panel-staged-card {
    border: 2px solid var(--accent);
    background: var(--bg-surface-alt);
}
.panel-staged-card img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    opacity: 0.35;
}
.panel-staged-badge {
    position: absolute;
    top: 6px;
    left: 6px;
    padding: 2px 8px;
    border-radius: var(--r-sm);
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: #fff;
    background: var(--accent);
}
.panel-staged-actions {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 6px;
}
.panel-staged-actions button {
    min-width: 84px;
}

/* Per-panel "needs update" chip: a panel lags the wall's current generation (a sibling was updated
   ahead of it). Used by the read-only MultiPanelViewer and the admin panel grid. Amber, with a dark
   label so the meaning survives without colour. Pinned top-left, above the panel image. */
.panel-outdated-badge {
    position: absolute;
    top: 6px;
    left: 6px;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: calc(100% - 12px);
    padding: 3px 8px;
    border-radius: var(--r-sm);
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1.2;
    color: #241a00;
    background: var(--status-warning);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
    pointer-events: none;
}
.panel-outdated-badge svg {
    flex: none;
}

/* Subtle in-context note on the boulder photo when one of its holds sits on an outdated panel. */
.boulder-outdated-note {
    margin: 0 0 6px;
    padding: 6px 10px;
    border-left: 3px solid var(--status-warning);
    border-radius: var(--r-sm);
    background: color-mix(in srgb, var(--status-warning) 14%, transparent);
    color: var(--text-secondary);
    font-size: 0.82rem;
}

/* Multi-image read-only viewer: one live panel at a time, with directional step arrows
   overlaid at the edges. The arrows sit OUTSIDE the scrolling .photo-viewport so they stay
   pinned on screen while the panel image pans/zooms inside it. */
.panel-viewer { position: relative; }
/* Read-only multi-image wall view: usage toggle (magnifier). When active every hold is drawn
   green with a small boulder-count badge, mirroring WallPhotoEditor's usage tool. Floats over
   the panel, top-right, outside the scrolling image. Only shown to non-anonymous viewers. */
.panel-usage-toggle {
    position: absolute;
    z-index: 3;
    top: 10px;
    right: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    padding: 0;
    border: none;
    border-radius: var(--r-pill);
    color: #fff;
    background: rgba(0, 0, 0, 0.5);
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
    cursor: pointer;
    transition: background 0.15s, transform 0.1s;
}
.panel-usage-toggle:hover { background: rgba(0, 0, 0, 0.65); }
.panel-usage-toggle.active { background: #4CAF50; }
.panel-arrow {
    position: absolute;
    z-index: 3;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    padding: 0;
    border: none;
    border-radius: var(--r-pill);
    color: #fff;
    background: rgba(0, 0, 0, 0.5);
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
    cursor: pointer;
    transition: background 0.15s, transform 0.1s;
}
.panel-arrow:hover { background: var(--accent); }
.panel-arrow:active { transform: scale(0.92); }
.panel-arrow.left { left: 8px; top: 50%; transform: translateY(-50%); }
.panel-arrow.right { right: 8px; top: 50%; transform: translateY(-50%); }
.panel-arrow.up { top: 8px; left: 50%; transform: translateX(-50%); }
.panel-arrow.down { bottom: 8px; left: 50%; transform: translateX(-50%); }
.panel-arrow.left:active { transform: translateY(-50%) scale(0.92); }
.panel-arrow.right:active { transform: translateY(-50%) scale(0.92); }
.panel-arrow.up:active { transform: translateX(-50%) scale(0.92); }
.panel-arrow.down:active { transform: translateX(-50%) scale(0.92); }

/* Overlap confirmation */
.panel-overlap { margin: 8px 0; }
.panel-overlap-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.panel-overlap-title {
    font-family: var(--font-display);
    font-size: 1rem;
    font-weight: 600;
    color: var(--text-heading);
    margin-bottom: 4px;
}
.panel-cmp {
    border: 1px solid var(--border-light);
    border-radius: var(--r-md);
    padding: 10px;
    margin-bottom: 12px;
    background: var(--bg-surface-alt);
}
.panel-cmp-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
}
.panel-cmp-img { margin: 0; }
.panel-cmp-img figcaption {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-secondary);
    margin-bottom: 4px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.panel-cmp-frame {
    position: relative;
    border-radius: var(--r-sm);
    overflow: hidden;
    border: 1px solid var(--border);
    background: #000;
}
.panel-cmp-frame img { width: 100%; display: block; }
.panel-cmp-frame .hold-overlay {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}
.panel-hold-badge {
    position: absolute;
    transform: translate(-50%, -50%);
    min-width: 15px;
    height: 15px;
    padding: 0 3px;
    border-radius: var(--r-pill);
    color: #fff;
    font-size: 0.62rem;
    font-weight: 700;
    line-height: 15px;
    text-align: center;
    box-shadow: 0 0 0 1.5px #fff;
    pointer-events: none;
}

.panel-proposal-list { list-style: none; margin: 10px 0 0; padding: 0; }
.panel-proposal-row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 6px 4px;
    border-top: 1px solid var(--border-light);
}
.panel-proposal-row.accepted { background: var(--success-bg); border-radius: var(--r-sm); }
.panel-proposal-num {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    border-radius: var(--r-pill);
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 20px;
    text-align: center;
}
.panel-proposal-accept { display: inline-flex; align-items: center; gap: 6px; font-size: 0.85rem; }
.panel-proposal-accept input { accent-color: var(--accent); }
.panel-proposal-band { font-weight: 600; }
.panel-proposal-band.high { color: var(--success-text); }
.panel-proposal-band.mid { color: var(--warning-text); }
.panel-proposal-band.low { color: var(--text-secondary); }
.panel-proposal-conf { font-size: 0.78rem; color: var(--text-secondary); }
.panel-proposal-moved {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-left: auto;
}
.panel-proposal-moved input { accent-color: var(--accent); }

/* ---- Big-wall overlap stepper (one hold-match at a time) ---- */
.panel-stepper { margin: 8px 0; outline: none; }
.panel-stepper-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.panel-stepper-head .btn-secondary { width: auto; margin-bottom: 0; }
.panel-stepper-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}
.panel-stepper-progress {
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--text-heading);
}
.panel-stepper-band { font-weight: 600; font-size: 0.85rem; }
.panel-stepper-band.high { color: var(--success-text); }
.panel-stepper-band.mid { color: var(--warning-text); }
.panel-stepper-band.low { color: var(--text-secondary); }
.panel-stepper-decided { font-size: 0.8rem; color: var(--success-text); font-weight: 600; }
.panel-stepper-toggle {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 0.9rem;
    font-weight: 600;
    cursor: pointer;
    user-select: none;
}
.panel-stepper-toggle input[type="checkbox"] { width: auto; margin: 0; cursor: pointer; }

.maintenance-notice {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 8px;
    max-width: 520px;
    margin: 12vh auto;
    padding: 32px 24px;
    border: 1px solid var(--border);
    border-radius: var(--r-md, 12px);
    background: var(--surface-2, rgba(120,190,255,0.08));
    color: var(--text-secondary);
}
.maintenance-notice h2 { margin: 0; color: var(--text-primary); }
.maintenance-notice p { margin: 0; }
.maintenance-icon { font-size: 2.4rem; line-height: 1; }

/* Prev/next selector shown above the per-panel hold editor on a big wall. */
.panel-selector {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    margin-bottom: 8px;
}
.panel-selector-label {
    min-width: 84px;
    text-align: center;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}

.panel-stepper-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    /* wallEditor.revealElement scrolls the panes to the top: keep them clear of the fixed top bar. */
    scroll-margin-top: calc(var(--topbar-h, 56px) + 8px);
}
/* Single full-width panel (the touch-up step): the lone PanelImageView spans the whole width
   instead of sitting half-width in the left column of the side-by-side comparison grid. */
.panel-step-solo { display: block; width: 100%; }
.panel-step-solo .panel-step-view { width: 100%; }
.panel-step-view { margin: 0; }
.panel-step-cap {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-secondary);
    margin-bottom: 4px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
/* Per-image overlay show/hide. Not uppercase — it is a control, not the caption label. */
.panel-step-toggle {
    flex: none;
    padding: 2px 8px;
    border: 1px solid var(--border);
    border-radius: var(--r-pill);
    background: var(--surface-2, rgba(120,190,255,0.12));
    color: var(--text-secondary);
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0;
    cursor: pointer;
}
.panel-step-toggle:hover { color: var(--text-primary); border-color: var(--accent, #4aa8ff); }
.panel-step-toggle.off { background: transparent; color: var(--text-tertiary, var(--text-secondary)); }
/* A scroll-model viewport (see viewport.js): the .photo-editor child is widened by --zoom
   and scrolls inside this box. The box is sized to the image's aspect ratio — bwViewport.fitBox
   publishes --fit-aspect and pins an inline aspect-ratio once the photo loads — so a landscape
   wall photo leaves NO black letterbox band, while a portrait one is capped by max-height and
   scrolls. Background is a theme surface (not #000) so any sliver stays theme-aware.
   touch-action mirrors the other viewports. */
.panel-step-viewport {
    position: relative;
    height: auto;
    aspect-ratio: var(--fit-aspect, 1.4);
    /* Published as a custom property, not a literal, so the desktop-mode width cap below can
       derive the box's widest allowed width from the SAME budget this height cap uses. */
    --panel-step-max-h: 70vh;
    max-height: var(--panel-step-max-h);
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-y;
    overscroll-behavior: contain;
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-surface-alt);
}
/* HTML number badge pinned to the highlighted hold; lives inside the zoomed .photo-editor
   so it tracks the image on pan/zoom, but keeps a constant pixel size (no SVG stretch). */
.panel-step-badge {
    position: absolute;
    transform: translate(-50%, -50%);
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: var(--r-pill);
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 17px;
    text-align: center;
    box-shadow: 0 0 0 2px #fff;
    pointer-events: none;
    z-index: 2;
}

.panel-stepper-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 10px;
}
.panel-stepper-actions .btn-primary,
.panel-stepper-actions .btn-secondary,
.panel-stepper-actions .btn-danger-soft {
    width: auto;
    margin-bottom: 0;
    flex: 1 1 auto;
    padding: 12px 18px;
    text-align: center;
}
.panel-stepper-actions .btn-secondary[disabled] { opacity: 0.45; cursor: not-allowed; }
.panel-stepper-actions .btn-primary[disabled] { opacity: 0.5; cursor: not-allowed; }
.btn-danger-soft {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 10px 16px;
    font-family: var(--font-body);
    font-weight: 600;
    border-radius: var(--r-md);
    background: var(--bg-surface);
    color: var(--warning-text, #b3541e);
    border: 1px solid var(--border);
    cursor: pointer;
    transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, opacity 0.18s ease;
}
.btn-danger-soft:hover:not(:disabled) { background: var(--warning-bg, #fbeee4); border-color: var(--warning-text, #b3541e); }
/* Disabled: read as a greyed-out button, not the bare orange text it fell back to without padding. */
.btn-danger-soft:disabled,
.btn-danger-soft[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
    color: var(--text-muted, var(--text-secondary));
    background: var(--bg-surface);
    border-color: var(--border);
}
.panel-stepper-hint { flex: 1 1 100%; font-size: 0.82rem; color: var(--text-secondary); }

.panel-stepper-foot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 8px;
}
.panel-stepper-legend { font-size: 0.76rem; color: var(--text-muted); }
.panel-stepper-legend kbd {
    display: inline-block;
    padding: 1px 5px;
    border-radius: 4px;
    border: 1px solid var(--border);
    background: var(--bg-surface-alt);
    font-family: var(--font-mono, monospace);
    font-size: 0.72rem;
    color: var(--text-secondary);
}
.btn-link {
    background: none;
    border: none;
    padding: 4px 2px;
    color: var(--accent);
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    text-decoration: underline;
}
.btn-link:hover { color: var(--accent-hover); }

@media (max-width: 560px) {
    .panel-stepper-pair { grid-template-columns: 1fr; }
    /* Keep the aspect-fit sizing on mobile; just tighten the portrait cap so a tall photo
       does not eat the whole screen. No fixed height, so landscape still hugs the image. */
    .panel-step-viewport { --panel-step-max-h: 60vh; }
}

/* ── Cross-panel hold link tool (PanelLinkTool.razor) ──────────────────────── */
/* The overlay itself is a full-view takeover (it is also in the keyboard dispatcher's
   takeover-surface selector, so the class name must stay); only the dialog inside it is
   width-capped. The cap is a comfortable reading width in normal (mobile-ish) layout, and
   desktop mode — html.bw-fullscreen, the same toggle the pages use — lifts it so the two
   photos get the whole window, which is the entire point of comparing them side by side. */
.panel-link-dialog {
    width: 100%;
    max-width: 1100px;
    background: var(--bg-surface);
    border-radius: 14px;
    padding: 16px;
    box-shadow: 0 10px 40px var(--shadow-lg);
}
html.bw-fullscreen .panel-link-dialog { max-width: none; }
/* With the cap lifted, each half of the pair can become wide enough that the box's
   aspect-driven height would exceed its height budget and the photo would only be reachable by
   scrolling. Cap the WIDTH at `budget * --fit-aspect` instead (the same trick the fullscreen
   photo views use), so the image grows into all the width it can actually use and still shows
   whole. Pure CSS off the JS-published aspect, so the zoom/pan model is untouched. */
html.bw-fullscreen .panel-link-dialog .panel-step-viewport {
    max-width: min(100%, calc(var(--panel-step-max-h) * var(--fit-aspect, 1.4)));
    margin-inline: auto;
}

/* ── Big-wall update flow (upload → carryover → overlaps → apply) ──────────── */
.bigupdate-entry { width: auto; margin-top: 10px; }

.bigupdate { margin: 8px 0; }
.bigupdate-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}
.bigupdate-head h2 { margin: 0; }
.bigupdate-step-label {
    font-weight: 600;
    color: var(--text-secondary);
    margin-bottom: 8px;
}

/* An update session belongs to the wall, so the flow names who started it and who touched it last. */
.bigupdate-owner {
    margin: 0 0 10px;
    font-size: 0.85rem;
}
.bigupdate-owner-last { opacity: 0.8; }

/* Uploader: one centre slot + four direction slots */
.bigupdate-slots {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 8px;
    margin: 10px 0;
}
.bigupdate-slot {
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--bg-surface-alt);
    padding: 10px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-height: 92px;
}
.bigupdate-slot.required { border-color: var(--accent); }
.bigupdate-slot.filled { border-style: solid; border-color: var(--success-text, #2f8f4e); }
.bigupdate-slot-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 6px;
}
.bigupdate-slot-label { font-weight: 600; color: var(--text-heading); }
.bigupdate-slot-req {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--accent);
}
.bigupdate-slot-file {
    font-size: 0.78rem;
    color: var(--text-secondary);
    word-break: break-all;
}
.bigupdate-slot-pick {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    min-height: 40px;
    border: 2px dashed var(--border);
    border-radius: var(--r-sm);
    color: var(--text-secondary);
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s;
}
.bigupdate-slot-pick:hover { border-color: var(--accent); color: var(--accent); }
/* A slot whose pick failed (e.g. too large and could not be resized): flagged so the safety-net
   Start-disable has a visible cause. */
.bigupdate-slot.invalid { border-color: var(--danger-text, #c0392b); }
.bigupdate-slot-error {
    font-size: 0.76rem;
    color: var(--danger-text, #c0392b);
    word-break: break-word;
}

/* Touch-up editing tools: a wrapping row under the single panel — the hint spans, the buttons
   sit auto-width beside each other instead of the full-width default. */
.bigupdate-edit-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 10px 0;
}
.bigupdate-edit-tools .hint { flex: 1 1 100%; margin: 0; }
.bigupdate-edit-tools .btn-secondary,
.bigupdate-edit-tools .btn-danger-soft { width: auto; margin-bottom: 0; flex: 0 1 auto; }
/* The shared HoldTouchupToolbar reuses the wall editor's toolbar chrome; here it is a standalone row
   rather than the top edge of an editor, so it gets all four corners rounded. */
.bigupdate-edit-tools .hold-touchup-toolbar { flex: 1 1 100%; border-radius: 10px; }

/* Phase overview: summary chips + colour legend */
.bigupdate-phase { margin: 4px 0; }
.bigupdate-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 6px 0 10px;
}
.bigupdate-chip {
    padding: 3px 10px;
    border-radius: var(--r-pill);
    font-size: 0.82rem;
    font-weight: 600;
    border: 1px solid var(--border-light);
    background: var(--bg-surface-alt);
    color: var(--text-secondary);
}
.bigupdate-chip.carry { color: var(--success-text, #2f8f4e); }
.bigupdate-chip.changed { color: var(--warning-text, #b3541e); }
.bigupdate-chip.remove { color: var(--danger-text, #c0392b); }
.bigupdate-chip.new { color: var(--accent); }
.bigupdate-chip.link { color: var(--text-secondary); }

.bigupdate-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin: 10px 0;
    font-size: 0.8rem;
    color: var(--text-secondary);
}
.bigupdate-legend span { display: inline-flex; align-items: center; gap: 5px; }
.bigupdate-dot {
    display: inline-block;
    width: 11px;
    height: 11px;
    border-radius: 50%;
}

.bigupdate-review-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 10px 0;
}
.bigupdate-review-actions .btn-secondary { width: auto; margin-bottom: 0; flex: 1 1 auto; }

/* Attention-focused headline: "N of {total} need review", the rest auto-carried. */
.bigupdate-attention {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 12px 14px;
    margin: 6px 0 10px;
    border-radius: var(--r-sm);
    border: 1px solid var(--status-warning);
    background: color-mix(in srgb, var(--status-warning) 12%, var(--bg-surface));
}
.bigupdate-attention.clear {
    border-color: var(--status-success);
    background: color-mix(in srgb, var(--status-success) 12%, var(--bg-surface));
}
/* A banner the user must not miss (failed alignment, reset verdicts): the danger tone, unlike the
   warning-toned attention headline, because what it describes happened without the user's say. */
.bigupdate-banner {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 12px 14px;
    margin: 6px 0 10px;
    border-radius: var(--r-sm);
    border: 1px solid var(--status-danger);
    background: color-mix(in srgb, var(--status-danger) 10%, var(--bg-surface));
    color: var(--text-primary);
}
.bigupdate-banner span { font-size: 0.88rem; color: var(--text-secondary); }
.bigupdate-banner .btn-secondary { width: auto; align-self: flex-start; margin-bottom: 0; }
.bigupdate-attention-head { font-size: 1.02rem; color: var(--text-primary); }
.bigupdate-attention-sub { font-size: 0.85rem; color: var(--text-secondary); }

.bigupdate-primary-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 10px;
}
.bigupdate-primary-actions .btn-primary,
.bigupdate-primary-actions .btn-secondary { width: auto; margin-bottom: 0; flex: 1 1 auto; }

.bigupdate-more-review { margin: 10px 0; }
.bigupdate-more-review > summary {
    cursor: pointer;
    font-size: 0.85rem;
    color: var(--text-secondary);
    padding: 4px 0;
}
.bigupdate-more-review[open] > summary { margin-bottom: 6px; }

.panel-stepper-reviewed {
    font-size: 0.82rem;
    color: var(--text-secondary);
}

/* Reviewed holds: hidden from the queues by default, force-shown with attribution + un-review. */
.bigupdate-reviewed { margin: 0 0 10px; }
.bigupdate-reviewed-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}
.bigupdate-reviewed-head .btn-secondary { width: auto; margin-bottom: 0; }
.bigupdate-reviewed-list {
    list-style: none;
    margin: 8px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.bigupdate-reviewed-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    border-radius: var(--r-sm);
    border: 1px solid var(--border);
    background: var(--bg-surface);
}
.bigupdate-reviewed-row .btn-secondary { width: auto; margin-bottom: 0; margin-left: auto; }
.bigupdate-reviewed-hold { font-weight: 600; color: var(--text-primary); }

/* Cross-gen side-by-side confirm tool */
.crossgen-tool { margin: 4px 0; }
.crossgen-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 6px 0 10px;
}
.crossgen-focusbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
}
.crossgen-focusbar .btn-secondary { width: auto; margin-bottom: 0; }
.crossgen-focus-state { font-size: 0.85rem; font-weight: 600; }
.crossgen-focus-state.linked { color: var(--status-success); }
.crossgen-focus-state.unlinked { color: var(--status-warning); }

/* TopLogger import section */
.tl-card .tl-meta { font-size: 0.85rem; color: var(--text-secondary); margin-top: 2px; }
.tl-outcome { font-weight: 600; }
.tl-outcome-ok { color: var(--success-text, #2e9e6b); }
.tl-outcome-failed { color: var(--danger-text); }
.tl-reauth {
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--accent);
    background: var(--input-bg);
    border: 1px solid var(--accent);
    border-radius: 8px;
    padding: 10px 12px;
    margin-bottom: 10px;
}
.tl-token { font-family: var(--font-mono, monospace); font-size: 0.8rem; resize: vertical; }
.tl-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.tl-actions .btn-primary, .tl-actions .btn-secondary { width: auto; margin-bottom: 0; }
.tl-confirm { margin-top: 12px; }
.tl-check { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: var(--text-secondary); }
.tl-check input { width: auto; }

/* Two-phase busy indicator: a small rotating circle beside the "Connecting…" / "Syncing…" text. */
.tl-busy {
    display: flex; align-items: center; gap: 8px; margin-top: 12px;
    font-size: 0.85rem; color: var(--text-secondary);
}
.tl-spinner {
    width: 16px; height: 16px; flex: none; border-radius: 50%;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    animation: tl-spin 0.7s linear infinite;
}
@keyframes tl-spin {
    to { transform: rotate(360deg); }
}

/* Collapsed "Update tokens" disclosure shown once connected, so the token inputs stop cluttering
   the connected view. The summary reads as a small clickable link row. */
.tl-token-edit { margin-top: 14px; }
.tl-token-edit summary {
    cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px;
    font-size: 0.85rem; font-weight: 600; color: var(--accent); padding: 2px 0;
}
.tl-token-edit summary::-webkit-details-marker { display: none; }
.tl-token-edit summary::before {
    content: ''; width: 0; height: 0; flex: none;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent; border-bottom: 4px solid transparent;
    transition: transform 0.15s ease;
}
.tl-token-edit[open] > summary::before { transform: rotate(90deg); }

/* Gym points→grade calibration disclosure: matches the "Update tokens" summary, with a scrollable
   grade→points table and compact numeric inputs. */
.tl-calibration { margin-top: 14px; }
.tl-calibration > summary {
    cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px;
    font-size: 0.85rem; font-weight: 600; color: var(--accent); padding: 2px 0;
}
.tl-calibration > summary::-webkit-details-marker { display: none; }
.tl-calibration > summary::before {
    content: ''; width: 0; height: 0; flex: none;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent; border-bottom: 4px solid transparent;
    transition: transform 0.15s ease;
}
.tl-calibration[open] > summary::before { transform: rotate(90deg); }
.tl-calib-grid { max-height: 260px; overflow-y: auto; margin: 8px 0; }
.tl-calib-table { width: 100%; border-collapse: collapse; }
.tl-calib-table th, .tl-calib-table td { text-align: left; padding: 3px 8px 3px 0; }
.tl-calib-table th { font-size: 0.8rem; color: var(--text-secondary); font-weight: 600; }
.tl-calib-input { width: 100px; margin-bottom: 0; }

/* ---- Header avatar with overlaid edit/remove icon controls (self view). Centered inside the
   text-align:center profile card; the 12px gap below matches the avatar's normal bottom margin. ---- */
.avatar-edit { position: relative; display: inline-block; width: 64px; margin: 0 auto 12px; }
.avatar-edit .avatar { margin: 0; }
.avatar-file-input { display: none; }
.avatar-icon-btn {
    position: absolute; bottom: -2px;
    width: 26px; height: 26px; border-radius: 50%; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--bg-surface); color: var(--text-heading);
    border: 1px solid var(--border); box-shadow: 0 1px 2px var(--shadow-sm);
    cursor: pointer;
}
.avatar-icon-btn:hover { background: var(--bg-surface-alt); border-color: var(--text-secondary); }
.avatar-edit-btn { right: -6px; }
.avatar-delete-btn { left: -6px; color: var(--danger-text); }
.avatar-icon-btn.is-disabled { opacity: 0.5; pointer-events: none; }
.avatar-icon-btn[disabled] { opacity: 0.5; cursor: not-allowed; }

/* ---- Collapsible profile cards (password sign-in, 2FA, linked accounts). Collapsed by default;
   the summary is the whole clickable header row, mirroring the tl-token-edit disclosure. ---- */
.profile-collapsible {
    background: var(--bg-surface); border: 1px solid var(--border);
    border-radius: var(--r-md); box-shadow: 0 1px 2px var(--shadow-sm);
    padding: 0 16px; margin-bottom: 16px;
}
.profile-collapsible > summary {
    cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px;
    font-size: 1.05rem; font-weight: 600; color: var(--text-heading); padding: 16px 0;
}
.profile-collapsible > summary::-webkit-details-marker { display: none; }
.profile-collapsible > summary::before {
    content: ''; width: 0; height: 0; flex: none;
    border-left: 6px solid currentColor;
    border-top: 5px solid transparent; border-bottom: 5px solid transparent;
    transition: transform 0.15s ease;
}
.profile-collapsible[open] > summary::before { transform: rotate(90deg); }
.profile-collapsible[open] > summary { padding-bottom: 8px; }

/* API keys link styled to match the app's secondary buttons instead of a bare underlined link.
   Wraps as a row so the moved-off-the-tab-bar entries (Tools, Administration) sit side by side. */
.profile-links { display: flex; flex-wrap: wrap; gap: 10px; }
.profile-links a.btn-secondary {
    display: inline-block; width: auto; margin-bottom: 0;
    padding: 10px 16px; font-size: 0.95rem; text-decoration: none;
}

/* ============================================================================
   Kiosk mode (Phase 3 UI)
   Everything here is sized for a wall-mounted tablet read at arm's length and
   tapped with a chalky finger: 64px rows, 1.15rem names, no dense list.
   ============================================================================ */

/* Avatar standing in for the tab icon while somebody is acting as a member. Matches the 22px svg
   the other tabs use so the bar's height never shifts between anonymous and acting-as. */
.nav-item .nav-avatar {
    width: 24px; height: 24px; font-size: 0.75rem; margin: 0; flex: none;
}

.kiosk-picker { max-width: 560px; margin: 0 auto; padding: 20px 16px 32px; }
.kiosk-picker-head { text-align: center; margin-bottom: 20px; }
.kiosk-picker-head h1 { font-size: 1.5rem; font-weight: 700; color: var(--text-heading); margin-bottom: 6px; }
.kiosk-picker-head p { color: var(--text-secondary); font-size: 0.95rem; }

.kiosk-user-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.kiosk-user-list form { margin: 0; }

/* One row shape for links and submit buttons alike, so a PIN user and a no-PIN user are visually
   identical even though one is an <a> and the other a <button>. */
.kiosk-row {
    display: flex; align-items: center; gap: 14px; width: 100%;
    min-height: 64px; padding: 12px 16px; box-sizing: border-box;
    background: var(--bg-surface); border: 1px solid var(--border);
    border-radius: var(--r-md); box-shadow: 0 1px 2px var(--shadow-sm);
    color: var(--text-heading); font-family: inherit; font-size: 1.15rem; font-weight: 600;
    text-align: left; text-decoration: none; cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.kiosk-row:hover, .kiosk-row:active { background: var(--bg-surface-alt); border-color: var(--accent); }
.kiosk-row-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kiosk-row-hint {
    flex: none; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.06em;
    color: var(--text-secondary); border: 1px solid var(--border);
    border-radius: 999px; padding: 3px 10px;
}
.kiosk-row-primary { justify-content: center; background: var(--accent); border-color: var(--accent); color: #fff; }
.kiosk-row-primary:hover { background: var(--accent); border-color: var(--accent); filter: brightness(1.08); }
.kiosk-row-secondary { justify-content: center; margin-top: 12px; color: var(--text-secondary); font-weight: 500; }
.kiosk-row-danger { justify-content: center; color: var(--danger-text); }

.kiosk-avatar { width: 44px; height: 44px; font-size: 1.25rem; margin: 0; flex: none; }

.kiosk-current {
    display: flex; align-items: center; gap: 14px; justify-content: center;
    margin-bottom: 20px; padding: 16px;
    background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r-md);
}
.kiosk-current-name { font-size: 1.25rem; font-weight: 700; color: var(--text-heading); }

.kiosk-pin-form { display: block; margin: 0; }

/* The field that actually posts. Off-screen rather than display:none so it stays a real form control
   and the browser still submits it; readonly + inputmode="none" in the markup are what keep the OS
   keyboard down on a touch-only tablet. */
.kiosk-pin-value {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Masked read-out: one dot per expected digit, filled as they are tapped. The count of entered
   digits is the ONLY thing visible from a couple of metres away. */
.kiosk-pin-display {
    display: flex; justify-content: center; align-items: center; gap: 14px;
    min-height: 28px; margin-bottom: 6px;
}
.kiosk-pin-dot {
    width: 16px; height: 16px; border-radius: 50%;
    border: 2px solid var(--border); background: transparent;
    transition: background-color 0.1s ease, border-color 0.1s ease;
}
.kiosk-pin-dot.is-filled { background: var(--accent); border-color: var(--accent); }
.kiosk-pin-count {
    text-align: center; color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 14px;
}

/* Wall-mounted tablet, used at arm's length with a whole hand: keys are sized for a thumb, not a
   cursor. 72px is the floor; the grid grows the keys on anything roomier. */
.kiosk-numpad {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
}
.kiosk-numpad-key {
    min-height: 72px; padding: 0;
    display: flex; align-items: center; justify-content: center;
    font-size: 1.75rem; font-weight: 600; font-variant-numeric: tabular-nums;
    color: var(--text-heading); background: var(--bg-surface);
    border: 1px solid var(--border); border-radius: var(--r-md);
    cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
}
.kiosk-numpad-key:active { background: var(--accent-soft); border-color: var(--accent); }
.kiosk-numpad-key:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.kiosk-numpad-key:disabled { opacity: 0.5; cursor: default; }
.kiosk-numpad-back { font-size: 1.5rem; color: var(--text-secondary); }
.kiosk-numpad-gap { display: block; }

/* Only rendered for a PIN whose length predates the stored length, so there is nothing to
   auto-submit on. */
.kiosk-pin-submit { margin-top: 14px; }

/* Subordinate on purpose: an escape hatch from a mistap, not a second primary action. */
.kiosk-pin-back {
    display: block; margin-top: 18px; text-align: center;
    color: var(--text-secondary); font-size: 0.9rem; text-decoration: underline;
}

/* The PIN step has to fit without scrolling on a tablet in either orientation, so the header block
   above the pad shrinks before the keys do. */
.kiosk-current-compact { padding: 10px; margin-bottom: 14px; gap: 10px; }
@media (max-height: 700px) {
    .kiosk-picker { padding-top: 10px; padding-bottom: 16px; }
    .kiosk-picker-head { margin-bottom: 10px; }
    .kiosk-current-compact { padding: 6px; margin-bottom: 10px; }
    .kiosk-current-compact .kiosk-avatar { width: 36px; height: 36px; font-size: 1rem; }
    .kiosk-numpad { gap: 10px; }
    .kiosk-numpad-key { min-height: 60px; font-size: 1.5rem; }
}

/* Says what the tablet is about to log. Informational, not an alert: it must be obvious without
   competing with the pick itself. */
.kiosk-pending-note {
    max-width: 560px; margin: 0 auto 14px; padding: 10px 14px; text-align: center;
    background: var(--surface-2, var(--surface)); color: var(--text-secondary);
    border: 1px solid var(--border); border-radius: var(--r-md); font-size: 0.95rem;
}
.kiosk-pending-note strong { color: var(--text-primary); }

.kiosk-note { margin-top: 20px; text-align: center; color: var(--text-secondary); font-size: 0.85rem; }
.kiosk-empty { text-align: center; color: var(--text-secondary); font-size: 0.95rem; padding: 24px 8px; }

/* Shared by the picker page and the layout banner that catches ?kiosk_pin_error=1 wherever act-as
   bounced the tablet to. */
.kiosk-banner-error {
    max-width: 560px; margin: 12px auto; padding: 12px 16px;
    background: var(--danger-bg); color: var(--danger-text);
    border: 1px solid var(--danger-border, var(--border)); border-radius: var(--r-md);
    font-size: 0.95rem; text-align: center;
}

/* Register-kiosk disclosure on the sign-in page: deliberately quiet, below the human options. */
.kiosk-register { margin-top: 24px; font-size: 0.9rem; }
.kiosk-register > summary {
    cursor: pointer; list-style: none; color: var(--text-secondary);
    text-align: center; padding: 8px 0;
}
.kiosk-register > summary::-webkit-details-marker { display: none; }
.kiosk-register > summary:hover { color: var(--text-heading); }
.kiosk-register form { margin-top: 12px; }
.kiosk-register .text-input {
    width: 100%; box-sizing: border-box; margin-bottom: 12px; padding: 12px 14px;
    background: var(--bg-surface); color: var(--text-heading);
    border: 1px solid var(--border); border-radius: var(--r-md); font-size: 1rem;
}
.kiosk-register .text-input:focus { outline: none; border-color: var(--accent); }
.kiosk-register-submit { justify-content: center; }
.kiosk-register-note { margin-top: 10px; color: var(--text-secondary); font-size: 0.8rem; line-height: 1.5; }

/* ============================================================================
   Kiosk device pairing
   The tablet's /kiosk/pair page is read from a couple of metres away and its QR
   is photographed off a glossy screen, so the code and the QR are both large and
   both deliberately high-contrast rather than themed.
   ============================================================================ */

.kiosk-pair { max-width: 560px; margin: 0 auto; padding: 20px 16px 32px; text-align: center; }

/* The QR keeps its own white ground (the SVG paints one) so it stays scannable in the dark theme;
   the padding and radius stop it looking like an unstyled image dropped on the page. */
.kiosk-pair-qr {
    width: min(320px, 70vw); margin: 20px auto 16px; padding: 12px;
    background: #ffffff; border-radius: var(--r-md);
}
.kiosk-pair-qr .kiosk-pair-qr-svg { display: block; width: 100%; height: auto; }

/* Two groups of three, spaced, tabular so the digits do not shuffle as the page redraws. */
.kiosk-pair-code {
    display: flex; justify-content: center; gap: 18px;
    font-size: clamp(2rem, 12vw, 3rem); font-weight: 700; letter-spacing: 0.12em;
    font-variant-numeric: tabular-nums; color: var(--text-heading);
}

.kiosk-pair-countdown {
    margin-top: 10px; color: var(--text-secondary); font-size: 0.9rem;
    font-variant-numeric: tabular-nums;
}

/* Present from the first paint so its antiforgery token is server-rendered, but only visible once
   the approval has landed — at which point it is the manual fallback behind the scripted submit. */
.kiosk-pair-complete-form { display: none; }
.kiosk-pair-complete-form.is-ready { display: block; margin-top: 20px; }

/* The pair-with-a-code link inside the register-kiosk disclosure: the primary of the two paths. */
.kiosk-pair-link { justify-content: center; }

.kiosk-approve { max-width: 560px; margin: 0 auto; padding: 20px 16px 32px; }
.kiosk-approve-blurb { color: var(--text-secondary); font-size: 0.95rem; margin-bottom: 16px; }
.kiosk-approve .form-group { margin-bottom: 16px; }

.wall-kiosk-pair-ok {
    padding: 10px 14px; margin-bottom: 12px; border-radius: var(--r-md);
    background: rgba(46, 160, 67, 0.15); color: var(--text);
    border: 1px solid rgba(46, 160, 67, 0.4); font-size: 0.9rem;
}

/* ========== Delete account ========== */

/* Reuses the .profile-page shell (same max-width, same .section cards) so the settings pages read
   as one family; only the pieces this page needs that no other page has are defined here. */
.delete-account-page .delete-account-subhead {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-heading);
    margin: 14px 0 6px;
}

.delete-account-page .delete-account-list {
    margin: 0 0 6px 18px;
    padding: 0;
    color: var(--text-secondary);
    font-size: 0.9rem;
    line-height: 1.55;
}

.delete-account-page .delete-account-list li { margin-bottom: 4px; }

.delete-account-page .delete-account-warning {
    margin-top: 14px;
    padding: 10px 12px;
    border-radius: 8px;
    background: var(--danger-bg);
    color: var(--danger-text);
    font-size: 0.9rem;
    line-height: 1.5;
}

.delete-account-page .field-label { display: block; margin-bottom: 6px; font-size: 0.9rem; }

/* Each step-up provider is its own POST form (a GET link here was CSRF-able), so the forms have to
   sit side by side the way the links they replaced did. */
.delete-account-page .reauth-start-form { display: inline-block; margin: 0 8px 8px 0; }

/* ── Keyboard shortcuts help (/help/keyboard) ─────────────────────────────────
   A `kbd` style already existed but was scoped to `.panel-stepper-legend kbd`;
   this page needs a slightly larger, page-level variant. Everything is expressed
   in theme custom properties so light and dark both read correctly. Note that
   --text-muted / --font-mono are NOT defined in app.css, so the fallbacks here
   are what actually apply — keep them. */
.keyshort-page {
    max-width: 780px;
    margin: 0 auto;
    padding: 0 16px 32px;
}

.keyshort-section { margin-top: 28px; }

.keyshort-section h2 {
    font-size: 1.15rem;
    margin: 0 0 6px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--border);
    color: var(--text-heading);
}

.keyshort-section h3 {
    font-size: 0.95rem;
    margin: 18px 0 6px;
    color: var(--text-heading);
}

.keyshort-note {
    margin: 0 0 10px;
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--text-secondary);
}

.keyshort-list { margin: 0; }

/* Two columns on desktop; the row collapses to a stacked block on narrow phones
   so long descriptions are not squeezed into a sliver of width. */
.keyshort-row {
    display: grid;
    grid-template-columns: 170px 1fr;
    gap: 4px 14px;
    align-items: baseline;
    padding: 7px 0;
    border-bottom: 1px solid var(--border-light);
}

.keyshort-row:last-child { border-bottom: none; }

.keyshort-row dt {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    margin: 0;
}

.keyshort-row dd {
    margin: 0;
    font-size: 0.9rem;
    line-height: 1.5;
    color: var(--text);
}

.keyshort-plus {
    font-size: 0.78rem;
    color: var(--text-secondary);
}

.keyshort-page kbd {
    display: inline-block;
    min-width: 1.5em;
    padding: 2px 7px;
    border-radius: var(--r-sm, 6px);
    border: 1px solid var(--border);
    border-bottom-width: 2px;
    background: var(--bg-surface-alt);
    box-shadow: 0 1px 1px var(--shadow-sm);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
    font-size: 0.8rem;
    line-height: 1.4;
    text-align: center;
    white-space: nowrap;
    color: var(--text);
}

@media (max-width: 560px) {
    .keyshort-row {
        grid-template-columns: 1fr;
        gap: 5px;
    }
}

/* ========== Short viewport (landscape phone) ==========================================
 *
 * A phone turned sideways is ~390-430px TALL. The vertical budget is entirely token-driven
 * (--topbar-h + --bottomnav-h + a per-surface chrome term, all subtracted from 100dvh), and
 * those tokens are sized for a ~850px portrait window: on the boulder page they alone came to
 * 56 + 72 + 330 = 458px, i.e. more than the whole landscape window. --avail-h went negative,
 * the 180px floor under the photo box won, the photo overflowed, and the action rows and the
 * fixed tab bar were pushed out of reach — the "weird, unresponsive, zoomed" state.
 *
 * So shrink the BUDGET, not the components: every rule below either re-sizes a token or makes
 * the real chrome match the token it just shrank (a token that lies is worse than a big one).
 * 560px is the breakpoint because every phone in landscape is under it and every tablet in
 * portrait is over it; it matches the existing max-height: 700px convention above.
 *
 * The WIDTH guard is not optional: height alone also matches a short desktop window and a landscape
 * kiosk display, both of which would get the phone-sized tab bar and 19px icons for nothing. The
 * cap is 960 rather than the desktop tier's 900 on purpose: a large phone in landscape is WIDER
 * than the tier line (iPhone 16 Pro Max is 932px that way) and is exactly the case this block
 * exists for. A real desktop window that is both under 960px wide and under 560px tall is rare
 * enough to accept.
 *
 * `html body` rather than `:root`: --boulder-photo-chrome and friends are set on `body` by the
 * desktop tier's style container query above and on `:root:root` in app.css, and a selector
 * matching the same element with higher specificity is the only thing that reliably beats both.
 */
@media (max-height: 560px) and (max-width: 959.98px) {
    html body {
        --topbar-h: 40px;
        --bottomnav-h: calc(44px + max(2px, env(safe-area-inset-bottom, 0px)));
        --fold-peek: 44px;
        /* The stat cards under the wall photo are the first thing worth sacrificing height for;
           they are still there, just no longer budgeted as if the photo must clear them. */
        --wall-stats-h: 0px;
        /* BOTH, and in this order: `.wall-carousel-root.editing .wall-main` re-derives
           --wall-photo-chrome from --wall-photo-chrome-base on a DESCENDANT, so overriding only the
           computed token left wall EDIT mode budgeting the full 140px on a 400px-tall window. */
        --wall-photo-chrome-base: 88px;
        --wall-photo-chrome: 88px;
        /* Title row + dim bar + chips + the Attempt/Send/Flash row, at their shrunken sizes. */
        --boulder-photo-chrome: 150px;
        /* No floor: on a window this short the photo must be free to become small so the
           buttons below it stay on screen. The desktop tier already does exactly this. */
        --photo-fit-floor: 0px;
    }

    /* Make the chrome actually fit the budget above. */
    .bw-topbar { padding-top: 4px; padding-bottom: 4px; }
    .bw-topbar-title { font-size: 0.95rem; }

    .bw-tabbar { padding-top: 2px; padding-bottom: max(2px, env(safe-area-inset-bottom)); }
    .nav-item { gap: 1px; padding: 2px 0; font-size: 0.6rem; }
    .nav-item svg { width: 19px; height: 19px; }

    /* 360px of first-fold on a 390px window left nothing for the rest of the page. */
    .wall-fold { min-height: 200px; }
}

/* ---- Wall update: optional shape recognition + shape review ------------------------------------ */
.shape-optional {
    margin-left: 6px;
    padding: 1px 8px;
    border-radius: var(--r-pill);
    border: 1px solid var(--border-light);
    font-size: 0.75rem;
    font-weight: 500;
}
/* Large tap targets: this step is used on a phone at the wall. */
.shape-big-btn { min-height: 48px; width: auto; flex: 1 1 auto; margin-bottom: 0; }
.shape-progress { width: 100%; height: 14px; margin: 6px 0 2px; accent-color: var(--accent); }
.shape-scope { border: 1px solid var(--border-light); border-radius: var(--r-sm); padding: 8px 12px; margin: 0 0 12px; }
.shape-scope legend { font-weight: 600; padding: 0 4px; }
.shape-radio { display: flex; align-items: center; gap: 10px; min-height: 44px; cursor: pointer; }
.shape-radio input { width: 22px; height: 22px; flex: 0 0 auto; margin: 0; }
.shape-radio small { color: var(--text-secondary); }

.shape-review-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 12px;
    margin: 6px 0 12px;
}
.shape-slider { display: flex; flex-direction: column; gap: 4px; flex: 1 1 220px; font-size: 0.88rem; }
.shape-slider input[type=range] { width: 100%; min-height: 32px; accent-color: var(--accent); }

.shape-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 210px), 1fr));
    gap: 12px;
    margin: 0 0 12px;
}
.shape-card {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border-light);
    border-radius: var(--r-sm);
    background: var(--bg-surface);
    overflow: hidden;
}
.shape-card.decision-accepted, .shape-card.decision-adjusted { border-color: var(--status-success); }
.shape-card.decision-circle, .shape-card.decision-keepprevious { border-color: var(--text-secondary); }
.shape-crop { width: 100%; aspect-ratio: 1; display: block; background: #111; image-rendering: auto; }
.shape-recognised { fill: rgba(233, 69, 96, 0.08); stroke: #e94560; vector-effect: none; }
/* Nothing was recognised: an unfilled dashed ring, so no tint suggests a colour the hold does not have. */
.shape-recognised.shape-none { fill: none; stroke: #fff; stroke-dasharray: 6 4; }
.shape-previous { fill: none; stroke: #ffd700; stroke-dasharray: 4 3; }
.shape-card-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 8px;
    padding: 6px 8px 0;
    font-size: 0.78rem;
    color: var(--text-secondary);
}
.shape-conf { font-weight: 700; }
.shape-conf.low { color: var(--danger-text, #c0392b); }
.shape-conf.mid { color: var(--warning-text, #b3541e); }
.shape-conf.high { color: var(--success-text, #2f8f4e); }
.shape-decision { margin-left: auto; font-weight: 600; }
.shape-card-actions {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
    gap: 6px;
    padding: 8px;
}
.shape-act {
    min-height: 44px;
    border-radius: var(--r-sm);
    border: 1px solid var(--border-light);
    background: var(--bg-surface-alt);
    color: var(--text-primary);
    font-weight: 600;
    cursor: pointer;
}
.shape-act.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.shape-act:disabled { opacity: 0.45; cursor: not-allowed; }

.shape-adjust-overlay {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    flex-direction: column;
    background: var(--bg-surface);
}
.shape-adjust-head {
    flex: 0 0 auto;
    padding: 8px 14px;
    border-bottom: 1px solid var(--border-light);
}
.shape-adjust-head .hint { margin: 2px 0 0; }
.shape-adjust-editor { flex: 1 1 auto; min-height: 0; overflow: auto; display: flex; flex-direction: column; }
/* Save/Cancel belong to this overlay's footer; the editor's own batch bar would duplicate them off-screen. */
.shape-adjust-editor .batch-bar { display: none; }
.shape-adjust-footer {
    flex: 0 0 auto;
    display: flex;
    gap: 10px;
    padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--border-light);
    background: var(--bg-surface);
}
