:root {
    --bg: #f5f4f1;
    --bg-surface: #ffffff;
    --bg-surface-alt: #f8f7f4;
    --text: #2b2b33;
    --text-secondary: #8a8a95;
    --text-heading: #16162a;
    --accent: #e94560;
    --accent-hover: #d63851;
    --accent-soft: rgba(233, 69, 96, 0.12);
    --secondary: #0f3460;
    --secondary-hover: #0a2847;
    --border: #e3e0d9;
    --border-light: #ecebe4;
    /* Warm-tinted shadows read as more tactile than neutral grey */
    --shadow-sm: rgba(30, 20, 10, 0.05);
    --shadow-md: rgba(30, 20, 10, 0.08);
    --shadow-lg: rgba(30, 20, 10, 0.14);
    /* Edge scrim for fade-out affordances. A pure fade to --bg is invisible in light
       mode, where card and page are nearly the same colour. Shadow tokens are
       alpha-blacks tuned for drop shadows and read far too heavy here in dark. */
    --scrim: rgba(20, 14, 8, 0.10);
    --topbar-bg: #16162a;
    --topbar-text: #ffffff;
    --nav-bg: #ffffff;
    --nav-border: #ecebe4;
    --input-bg: #ffffff;
    --warning-bg: #FFF3E0;
    --warning-text: #E65100;
    --success-bg: #E8F5E9;
    --success-text: #2E7D32;
    --info-bg: #E3F2FD;
    --info-text: #0D47A1;
    --danger-bg: #FFEBEE;
    --danger-text: #C62828;
    /* Solid status/indicator colors for dots, strokes and legend swatches —
       distinct from the soft --*-bg / --*-text pairs above. */
    --status-success: #33cc66;
    --status-warning: #ffb020;
    --status-danger: #ff5555;
    --status-info: #4aa8ff;
    --status-review: #ff3db4;
    --overlay-bg: rgba(0, 0, 0, 0.5);

    /* Typography */
    --font-display: 'Bricolage Grotesque', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
    --font-body: 'Manrope', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;

    /* Consistent radius scale */
    --r-sm: 6px;
    --r-md: 10px;
    --r-lg: 14px;
    --r-pill: 999px;

    /* Wall photo dim level, 0 = untouched .. 1 = strongly dimmed. Owned by
       wwwroot/js/wall-dim.js (localStorage key `blocwerk-wall-dim`); this is only
       the fallback for a first visit / disabled storage. Theme-independent: it is a
       relative filter, so it reads correctly in both light and dark. */
    --wall-dim: 0;

    /* Native range slider (WallDimControl). The unfilled track has no themed
       system colour that follows --data-theme, so it is a token like everything
       else; the thumb rides --accent. */
    --range-track: #dcd8cf;
    --range-thumb: var(--accent);

    /* Explicit, NOT `light dark`: with `light dark` every native control follows the
       OS preference, so an explicit [data-theme="dark"] on a light-OS machine left
       range sliders and scrollbars rendering light inside dark chrome. Each theme
       block below restates it. */
    color-scheme: light;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #0f0f18;
        --bg-surface: #1a1a28;
        --bg-surface-alt: #22222f;
        --text: #e5e3dd;
        --text-secondary: #96939d;
        --text-heading: #f4f2ec;
        --accent: #ff5a75;
        --accent-hover: #ff7288;
        --accent-soft: rgba(255, 90, 117, 0.14);
        --secondary: #6ea3ff;
        --secondary-hover: #4e8ae6;
        --border: #2f2f42;
        --border-light: #28283a;
        --shadow-sm: rgba(0, 0, 0, 0.30);
        --shadow-md: rgba(0, 0, 0, 0.42);
        --shadow-lg: rgba(0, 0, 0, 0.58);
        --scrim: rgba(0, 0, 0, 0.45);
        --topbar-bg: #0a0a15;
        --topbar-text: #ffffff;
        --nav-bg: #14141f;
        --nav-border: #262637;
        --input-bg: #22222f;
        --warning-bg: #3d2e1a;
        --warning-text: #FFB74D;
        --success-bg: #1a3d1a;
        --success-text: #81C784;
        --info-bg: #1a2a3d;
        --info-text: #64B5F6;
        --danger-bg: #3d1a1a;
        --danger-text: #EF9A9A;
        --status-success: #3ddc84;
        --status-warning: #ffc04d;
        --status-danger: #ff6b6b;
        --status-info: #6ab4ff;
        --status-review: #ff66c4;
        --overlay-bg: rgba(0, 0, 0, 0.7);
        --range-track: #3a3a50;
        --range-thumb: var(--accent);

        color-scheme: dark;
    }
}

[data-theme="dark"] {
    --bg: #0f0f18;
    --bg-surface: #1a1a28;
    --bg-surface-alt: #22222f;
    --text: #e5e3dd;
    --text-secondary: #96939d;
    --text-heading: #f4f2ec;
    --accent: #ff5a75;
    --accent-hover: #ff7288;
    --accent-soft: rgba(255, 90, 117, 0.14);
    --secondary: #6ea3ff;
    --secondary-hover: #4e8ae6;
    --border: #2f2f42;
    --border-light: #28283a;
    --shadow-sm: rgba(0, 0, 0, 0.30);
    --shadow-md: rgba(0, 0, 0, 0.42);
    --shadow-lg: rgba(0, 0, 0, 0.58);
    --scrim: rgba(0, 0, 0, 0.45);
    --topbar-bg: #0a0a15;
    --topbar-text: #ffffff;
    --nav-bg: #14141f;
    --nav-border: #262637;
    --input-bg: #22222f;
    --warning-bg: #3d2e1a;
    --warning-text: #FFB74D;
    --success-bg: #1a3d1a;
    --success-text: #81C784;
    --info-bg: #1a2a3d;
    --info-text: #64B5F6;
    --danger-bg: #3d1a1a;
    --danger-text: #EF9A9A;
    --status-success: #3ddc84;
    --status-warning: #ffc04d;
    --status-danger: #ff6b6b;
    --status-info: #6ab4ff;
    --status-review: #ff66c4;
    --overlay-bg: rgba(0, 0, 0, 0.7);
    --range-track: #3a3a50;
    --range-thumb: var(--accent);

    color-scheme: dark;
}

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

html, body {
    font-family: var(--font-body);
    font-feature-settings: 'ss01', 'cv11'; /* Manrope stylistic set + alt digits */
    background: var(--bg);
    color: var(--text);
    /* dvh, not vh: everything INSIDE the shell already sizes off 100dvh, and mixing the two
       units meant the outer box claimed the tall (URL-bar-hidden) height while its contents
       claimed the short one. On a landscape phone that mismatch is a visible dead strip. */
    min-height: 100dvh;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    letter-spacing: -0.005em;
    /* Kill iOS Safari's double-tap-to-zoom on rapid button taps.
       Panning + pinch-zoom remain permitted; the wall photo viewport uses
       touch-action: none locally to run its own gesture code. */
    touch-action: manipulation;
}

h1, h2, h3, h4 {
    font-family: var(--font-display);
    font-weight: 600;
    letter-spacing: -0.025em;
    color: var(--text-heading);
    line-height: 1.15;
}
h1 { font-size: 1.9rem; font-weight: 700; letter-spacing: -0.03em; }
h2 { font-size: 1.15rem; }

/* Tabular numerics for stat values + counts */
.stat-value, .hold-count, .pick-count, .stitcher-toolbar .option-hint {
    font-variant-numeric: tabular-nums;
}

/* Global focus ring — consistent, accessible, doesn't disturb layout */
:where(button, a, input, select, textarea, [tabindex]):focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--r-sm);
}

.app-shell {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
    /* Landscape on a notched phone puts the notch on one SIDE. With viewport-fit=cover the page
       now extends under it, so the shell keeps its content clear; the fixed bars below do the
       same for themselves, since they are positioned against the viewport, not this box. */
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
}

.bw-topbar {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    padding: 12px 16px;
    background: var(--topbar-bg);
    color: var(--topbar-text);
    position: sticky;
    top: 0;
    z-index: 50;
    gap: 12px;
    /* Subtle bottom hairline that reads like a horizon line under the topbar. */
    box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.06);
}

.bw-topbar > .app-title { grid-column: 1; justify-self: start; }
/* Pin the actions to the last column so they stay hard right-aligned even when the middle title cell
   is empty and collapses (/walls) — otherwise the two 1fr columns would recenter them. margin-left:auto
   is a belt-and-braces fallback inside the cell. */
.bw-topbar > .header-actions { grid-column: 3; justify-self: end; margin-left: auto; }
.bw-topbar-title {
    justify-self: center;
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 600;
    letter-spacing: -0.02em;
    color: var(--topbar-text);
    max-width: 60vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-align: center;
}
.bw-topbar-title:empty { display: none; }

/* On a phone the centred 1fr/auto/1fr layout lets a long page title (a wall name) grow wider than the
   space between the brand and the actions, so the three drew over each other. There the title takes
   whatever is left between them and ellipsizes instead. */
@media (max-width: 720px) {
    .bw-topbar { grid-template-columns: auto minmax(0, 1fr) auto; }
    .bw-topbar-title { max-width: none; min-width: 0; justify-self: stretch; }
}

.app-title {
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 700;
    color: white;
    text-decoration: none;
    letter-spacing: -0.035em;
}

.header-actions {
    display: flex;
    align-items: center;
    gap: 12px;
}

.header-link {
    color: rgba(255, 255, 255, 0.8);
    text-decoration: none;
    font-size: 0.9rem;
    font-weight: 500;
}

.header-link:hover {
    color: white;
}

.header-icon-btn {
    background: none;
    border: none;
    color: rgba(255, 255, 255, 0.8);
    cursor: pointer;
    padding: 4px;
    display: flex;
    align-items: center;
    position: relative;
}

.header-icon-btn:hover {
    color: white;
}

.header-icon-btn.active {
    color: var(--accent, #4aa8ff);
}

.theme-toggle {
    background: none;
    border: none;
    color: rgba(255, 255, 255, 0.8);
    cursor: pointer;
    padding: 4px;
    display: flex;
    align-items: center;
}

.theme-toggle:hover {
    color: white;
}

.info-popover {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 8px;
    background: var(--bg-surface);
    border-radius: 8px;
    box-shadow: 0 4px 16px var(--shadow-lg);
    overflow: hidden;
    min-width: 160px;
    z-index: 100;
}

.info-popover a {
    display: block;
    padding: 10px 16px;
    color: var(--text);
    text-decoration: none;
    font-size: 0.9rem;
    font-weight: 500;
}

.info-popover a:hover {
    background: var(--bg-surface-alt);
}

.info-version {
    display: block;
    padding: 8px 16px;
    border-top: 1px solid var(--border);
    color: var(--text-secondary);
    font-size: 0.78rem;
    font-variant-numeric: tabular-nums;
}

.content {
    flex: 1;
    /* The tab bar's own height token rather than a second copy of the number, so the safe-area
       term and the short-viewport override below can never drift apart from it. */
    padding-bottom: var(--bottomnav-h);
}

.bw-tabbar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    background: var(--nav-bg);
    border-top: 1px solid var(--nav-border);
    padding: 6px 0;
    padding-bottom: max(6px, env(safe-area-inset-bottom));
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
    z-index: 50;
    /* Faint upward glow so the tab bar reads like it's floating above content */
    box-shadow: 0 -1px 0 var(--nav-border), 0 -12px 24px -14px var(--shadow-md);
}

.nav-item {
    flex: 1;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    text-decoration: none;
    color: var(--text-secondary);
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    padding: 6px 0 4px;
    transition: color 0.18s ease;
}

/* Keep every tab label on a single line so a five-item bar never wraps or grows taller than its
   72px content reservation on a narrow (~360px) phone. */
.nav-item span {
    white-space: nowrap;
}

/* Accent bar that slides in above the active tab */
.nav-item::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%) scaleX(0.2);
    width: 28px;
    height: 2px;
    border-radius: 0 0 var(--r-sm) var(--r-sm);
    background: var(--accent);
    opacity: 0;
    transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}

.nav-item:hover { color: var(--text-heading); }
.nav-item.active {
    color: var(--accent);
}
.nav-item.active::before {
    opacity: 1;
    transform: translateX(-50%) scaleX(1);
}

.nav-item svg {
    width: 22px;
    height: 22px;
    stroke-width: 1.9;
}

/* Live session: the Activity tab becomes "Session" with a slowly breathing dot, so a session in
   progress is always visible from anywhere in the app. */
.nav-item.session { color: var(--accent); }
.nav-item.session .session-icon {
    display: inline-flex; width: 22px; height: 22px;
    animation: bw-session-breathe 2.6s ease-in-out infinite;
    transform-origin: center;
}
.nav-item.session .session-icon svg { width: 22px; height: 22px; }

@keyframes bw-session-breathe {
    0%, 100% { transform: scale(0.72); opacity: 0.55; }
    50% { transform: scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .nav-item.session .session-icon { animation: none; opacity: 1; }
}

/* Small chevron button that hides the tab bar to reclaim screen space. Sits flush right, not a flex:1
   tab, so it never steals width from the labelled nav items. */
.tabbar-toggle {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-secondary);
    padding: 6px 12px;
    transition: color 0.18s ease;
}
.tabbar-toggle:hover { color: var(--text-heading); }
.tabbar-toggle svg { width: 22px; height: 22px; }

/* Collapsed state lives as a class on <body>, toggled by wwwroot/js/nav.js (bwNav) — MainLayout is
   static, so a Blazor field can't drive this. Mirror the edit-mode approach (display:none) so the
   bar leaves no reserved space; its down-chevron sits inside the bar and disappears with it. */
body.bw-tabbar-collapsed .bw-tabbar { display: none; }

/* Floating '^' handle that brings the bar back — shown only while collapsed. */
.tabbar-restore { display: none; }
body.bw-tabbar-collapsed .tabbar-restore { display: flex; }

.tabbar-restore {
    position: fixed;
    right: 12px;
    bottom: max(12px, env(safe-area-inset-bottom));
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--nav-border);
    border-radius: 50%;
    background: var(--nav-bg);
    color: var(--text-secondary);
    cursor: pointer;
    z-index: 50;
    box-shadow: 0 -1px 0 var(--nav-border), 0 6px 16px -8px var(--shadow-md);
    transition: color 0.18s ease;
}
.tabbar-restore:hover { color: var(--text-heading); }
.tabbar-restore svg { width: 22px; height: 22px; }

/* An anonymous visitor gets no tab bar at all (MainLayout renders none), so drop the reservation
   the bar would have needed — same shape as the docked-editor rule below. */
.app-shell:not(:has(.bw-tabbar)) .content { padding-bottom: 0; }

/* When the wall is in edit mode, the docked editor toolbar replaces the bottom nav. */
.app-shell:has(.bw-toolbar.docked-bottom) .bw-tabbar { display: none; }
.app-shell:has(.bw-toolbar.docked-bottom) .content { padding-bottom: 0; }

.bw-toolbar.docked-bottom .editor-toolbar { border-radius: 0; }
.bw-toolbar.docked-bottom {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    border-radius: 0;
    padding-bottom: max(8px, env(safe-area-inset-bottom));
    /* Fixed elements are positioned against the viewport, so the shell's side insets do not
       reach them — with viewport-fit=cover they would sit under a landscape notch otherwise. */
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
    z-index: 60;
    background: var(--topbar-bg);
    border-top: 1px solid var(--nav-border);
    /* Cap the fixed tool row so it never eats the whole screen; extra tools scroll within it. */
    max-height: 45vh;
    overflow-y: auto;
}

/* The tool row is docked (fixed, z-index 60) at the viewport bottom, so the option bars + Save
   batch-bar in normal flow would scroll underneath it. Reserve the clearance on the ACTUAL vertical
   scroller (.wall-page) — the old margin-bottom lived inside the height-locked card that clipped it,
   which only buried Save deeper. As bottom padding on the scroll container it reliably extends the
   scrollable range so Save comes to rest ABOVE the toolbar. Tune the clearance to taste on-device. */
.app-shell:has(.bw-toolbar.docked-bottom) .wall-page {
    padding-bottom: calc(180px + env(safe-area-inset-bottom));
}
/* Keep a small floor so (de)selecting a hold doesn't jump the layout, and leave room above the
   fixed toolbar when a control is scrolled into view. */
.app-shell:has(.bw-toolbar.docked-bottom) .editor-bottom {
    min-height: 42px;
    scroll-margin-bottom: calc(180px + env(safe-area-inset-bottom));
}

.loading { text-align: center; color: var(--text-secondary); padding: 48px; }
.loading-small { text-align: center; color: var(--text-secondary); font-size: 0.85rem; }

.toast-msg {
    position: fixed; bottom: 88px; left: 50%; transform: translateX(-50%);
    background: var(--topbar-bg); color: white;
    padding: 12px 20px;
    border-radius: var(--r-pill);
    font-size: 0.88rem;
    font-weight: 500;
    letter-spacing: -0.005em;
    z-index: 100;
    box-shadow: 0 12px 28px -8px rgba(0, 0, 0, 0.35), 0 2px 6px var(--shadow-md);
    animation: toast-in 0.24s cubic-bezier(0.2, 0, 0, 1);
}
@keyframes toast-in {
    from { opacity: 0; transform: translateX(-50%) translateY(6px); }
    to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* The app-wide "your TopLogger session expired" banner. Sits a little higher than a transient
   toast, stays until dismissed, and carries the reconnect link + a dismiss button. */
.toast-reauth {
    display: flex;
    align-items: center;
    gap: 14px;
    background: var(--accent);
}
.toast-reauth a {
    color: white;
    font-weight: 600;
    text-decoration: underline;
}
.toast-reauth-dismiss {
    background: none;
    border: none;
    color: white;
    font-size: 1.2rem;
    line-height: 1;
    cursor: pointer;
    padding: 0 2px;
}

.error-toast {
    position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
    background: var(--accent); color: white; padding: 12px 24px;
    border-radius: 8px; font-size: 0.9rem; z-index: 100;
}

.error-msg { margin-top: 12px; color: var(--accent); text-align: center; }

.btn-primary,
.btn-submit,
.btn-secondary,
.btn-danger {
    font-family: var(--font-body);
    font-weight: 600;
    letter-spacing: -0.005em;
    border-radius: var(--r-md);
    transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.08s ease, color 0.18s ease, border-color 0.18s ease;
    -webkit-tap-highlight-color: transparent;
}
.btn-primary:active,
.btn-submit:active,
.btn-secondary:active,
.btn-danger:active { transform: translateY(1px); }

.btn-primary {
    display: inline-block; padding: 13px 28px;
    background: var(--accent); color: white;
    text-decoration: none;
    font-size: 1.02rem;
    /* Subtle inner highlight + drop for tactile feel */
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.18),
        0 1px 2px var(--shadow-sm),
        0 4px 12px -6px var(--accent-soft);
}
.btn-primary:hover { background: var(--accent-hover); box-shadow: inset 0 1px 0 rgba(255,255,255,0.22), 0 6px 18px -6px var(--accent-soft); }

.btn-submit {
    width: 100%; padding: 14px;
    background: var(--accent); color: white;
    border: none; font-size: 1rem;
    cursor: pointer;
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.18),
        0 1px 2px var(--shadow-sm),
        0 4px 12px -6px var(--accent-soft);
}
.btn-submit:hover:not(:disabled) { background: var(--accent-hover); box-shadow: inset 0 1px 0 rgba(255,255,255,0.22), 0 6px 18px -6px var(--accent-soft); }
.btn-submit:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }

.btn-secondary {
    width: 100%; padding: 12px;
    background: var(--bg-surface); color: var(--text-heading);
    border: 1px solid var(--border);
    cursor: pointer;
    margin-bottom: 8px;
    box-shadow: 0 1px 2px var(--shadow-sm);
}
.btn-secondary:hover {
    background: var(--bg-surface-alt);
    border-color: var(--text-secondary);
}

.btn-danger {
    width: 100%; padding: 12px;
    background: transparent; color: var(--accent);
    border: 1.5px solid var(--accent);
    cursor: pointer;
}
.btn-danger:hover { background: var(--accent); color: white; }

.staged-hint { margin: 8px 0; color: var(--text-secondary); font-size: 0.9rem; }
.staged-hint.danger { color: var(--warning-text, #b3541e); }
.staged-actions { display: flex; gap: 8px; margin-top: 8px; }
.staged-actions > .btn-danger,
.staged-actions > .btn-primary,
.staged-actions > .btn-secondary {
    flex: 1;
    width: auto;
    padding: 12px 16px;
    font-size: 1rem;
    margin: 0;
    line-height: 1.2;
    text-align: center;
}

.form-group { margin-bottom: 12px; }
.form-group label, .form-group > label { display: block; font-weight: 600; color: var(--text); margin-bottom: 6px; font-size: 0.9rem; }

.form-input {
    width: 100%; padding: 11px 14px;
    border: 1.5px solid var(--border);
    border-radius: var(--r-md); font-size: 0.95rem;
    outline: none; box-sizing: border-box;
    background: var(--input-bg); color: var(--text);
    font-family: var(--font-body);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.form-input:hover { border-color: var(--text-secondary); }
.form-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.back-link { color: var(--text-secondary); text-decoration: none; font-size: 0.9rem; }

.bw-anon-notice {
    padding: 10px 14px; background: var(--warning-bg); border-radius: 8px; margin-bottom: 12px;
    font-size: 0.9rem; color: var(--warning-text);
}
.bw-anon-notice a { color: var(--secondary); font-weight: 600; text-decoration: underline; }

.empty-msg { text-align: center; color: var(--text-secondary); font-size: 0.85rem; padding: 8px; }

.delete-btn {
    background: none; border: none; color: #ccc; cursor: pointer;
    font-size: 0.85rem; padding: 2px 6px;
}
.delete-btn:hover { color: var(--accent); }

.pager-btn {
    padding: 4px 12px; background: var(--bg-surface); border: 1px solid var(--border);
    border-radius: 4px; font-size: 0.8rem; cursor: pointer; color: var(--text);
}
.pager-btn:hover { background: var(--bg-surface-alt); }

.view-toggle {
    display: flex;
    background: var(--bg-surface-alt);
    padding: 3px;
    border-radius: var(--r-pill);
    border: 1px solid var(--border);
    margin-bottom: 8px;
    gap: 0;
}
.toggle-btn {
    flex: 1;
    padding: 8px 14px;
    border: none;
    background: transparent;
    border-radius: var(--r-pill);
    font-family: var(--font-body);
    font-weight: 600; font-size: 0.85rem;
    cursor: pointer; text-align: center;
    color: var(--text-secondary);
    transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.toggle-btn:hover { color: var(--text-heading); }
.toggle-btn.active {
    background: var(--bg-surface);
    color: var(--text-heading);
    box-shadow: 0 1px 3px var(--shadow-sm), 0 1px 2px var(--shadow-md);
}

.bw-consent {
    position: fixed; bottom: 0; left: 0; right: 0;
    background: var(--topbar-bg); color: white;
    padding: 16px 20px; z-index: 200;
    display: flex; justify-content: space-between; align-items: center; gap: 16px;
    box-shadow: 0 -2px 12px var(--shadow-lg);
}
.bw-consent p { font-size: 0.9rem; flex: 1; margin: 0; }
.bw-consent a { color: rgba(255,255,255,0.8); text-decoration: underline; }
.bw-consent-accept {
    padding: 8px 20px; background: var(--accent); color: white;
    border: none; border-radius: 6px; font-weight: 600; cursor: pointer;
    white-space: nowrap;
}
.bw-consent-accept:hover { background: var(--accent-hover); }

/* ---------------------------------------------------------------------------
   "Install as an app" banner (Components/Shared/InstallPrompt.razor)
   Sits ABOVE the tab bar rather than over it — 74px mirrors the connection
   pill's clearance — and above the carousel dots on the pages that have them,
   so it never covers navigation. Both offsets collapse with the tab bar, which
   wwwroot/js/nav.js flags on <body>. Under .bw-consent's z-index: consent is
   the one banner that has to win.
   --------------------------------------------------------------------------- */
.bw-install {
    position: fixed;
    left: 12px; right: 12px;
    bottom: calc(74px + env(safe-area-inset-bottom));
    z-index: 150;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    max-width: 560px;
    margin: 0 auto;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--bg-surface);
    box-shadow: 0 6px 20px -8px var(--shadow-lg);
}
.bw-install-body { flex: 1 1 200px; min-width: 0; }
.bw-install-title { margin: 0; font-size: 0.92rem; font-weight: 600; color: var(--text-heading); }
.bw-install-hint { margin: 4px 0 0; font-size: 0.85rem; color: var(--text-secondary); }
.bw-install-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.bw-install-accept {
    padding: 8px 16px; background: var(--accent); color: white;
    border: none; border-radius: 6px; font-weight: 600; cursor: pointer;
    white-space: nowrap;
}
.bw-install-accept:hover { background: var(--accent-hover); }
.bw-install-accept[disabled] { opacity: 0.5; cursor: not-allowed; }
.bw-install-dismiss {
    padding: 8px 6px; background: none; border: none;
    color: var(--text-secondary); font-size: 0.85rem; cursor: pointer;
    white-space: nowrap;
}
.bw-install-dismiss:hover { color: var(--text-heading); }

/* Carousel dots are in-flow at the bottom of the wall carousel, just above the
   tab bar; clear them too where they exist. */
.app-shell:has(.carousel-dots) .bw-install { bottom: calc(102px + env(safe-area-inset-bottom)); }

/* ---------------------------------------------------------------------------
   Offline queue + connection status
   All colours come from the token set above, so light/dark and the
   [data-theme] override both work with no extra rules.
   --------------------------------------------------------------------------- */

/* Blazor's own reconnect element. We only use it as an event source; the visible
   affordance is the status pill, so it must never paint or catch a tap. */
.bw-reconnect-sentinel {
    position: fixed;
    width: 0; height: 0;
    overflow: hidden;
    pointer-events: none;
    opacity: 0;
}

/* The slot is only an anchor; the pill positions itself, so a Blazor re-render
   rewriting the slot's class attribute cannot disturb the layout. */
.bw-conn-slot { display: contents; }

.bw-conn-fallback { display: contents; }

.bw-conn-pill {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    /* Clears the bottom tab bar (and its safe-area padding) so it never covers navigation. */
    bottom: calc(74px + env(safe-area-inset-bottom));
    z-index: 90;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    max-width: min(92vw, 560px);
    padding: 6px 14px;
    border: 1px solid var(--border);
    border-radius: var(--r-pill);
    background: var(--bg-surface);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    box-shadow: 0 2px 10px var(--shadow-md);
}

.bw-conn-pill.bw-conn-warn {
    background: var(--warning-bg);
    color: var(--warning-text);
    border-color: color-mix(in srgb, var(--warning-text) 30%, transparent);
}

.bw-conn-pill.bw-conn-info {
    background: var(--info-bg);
    color: var(--info-text);
    border-color: color-mix(in srgb, var(--info-text) 30%, transparent);
}

.bw-conn-dot {
    width: 7px; height: 7px;
    border-radius: 50%;
    background: currentColor;
    flex: 0 0 auto;
    animation: bw-conn-pulse 1.6s ease-in-out infinite;
}

.bw-conn-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

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

@media (prefers-reduced-motion: reduce) {
    .bw-conn-dot { animation: none; }
}

.bw-conn-panel {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(112px + env(safe-area-inset-bottom));
    z-index: 91;
    width: min(92vw, 420px);
    max-height: 45vh;
    overflow-y: auto;
    padding: 14px 16px;
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--bg-surface);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 0.82rem;
    box-shadow: 0 6px 24px var(--shadow-lg);
}

.bw-conn-panel-text { margin: 0 0 8px; }

.bw-conn-panel-error {
    margin: 0 0 6px;
    padding: 6px 8px;
    border-radius: var(--r-sm);
    background: var(--danger-bg);
    color: var(--danger-text);
    font-size: 0.78rem;
}

.bw-conn-panel-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }

.bw-conn-btn {
    padding: 6px 14px;
    border: 1px solid var(--border);
    border-radius: var(--r-pill);
    background: var(--bg-surface-alt);
    color: var(--text-heading);
    font-family: var(--font-body);
    font-size: 0.78rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}

.bw-conn-btn:hover { background: var(--accent-soft); border-color: var(--accent); }

/* Marks a queued, not-yet-synced action. Created by offline-actions.js, so it is outside
   Blazor's render tree and survives a re-render until the queue confirms the action landed. */
.bw-queued-pip {
    position: absolute;
    top: 3px; right: 3px;
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--warning-text);
    box-shadow: 0 0 0 2px var(--bg-surface);
    pointer-events: none;
}

/* Any element carrying a pip needs to be a positioning context. */
[data-bw-action] { position: relative; }

/* Optimistic pre-echo of server state. Blazor rewrites `class` on re-render, so this
   disappears by itself once the server's version of the truth arrives. */
.bw-opt-on { color: var(--accent); opacity: 1; }

/* Blazor toggles this element with an inline `display: block`, so the flex layout has to
   live on an inner wrapper rather than on #blazor-error-ui itself. */
#blazor-error-ui {
    display: none;
    position: fixed;
    left: 0; right: 0;
    bottom: 0;
    z-index: 300;
    padding: 12px 16px;
    padding-bottom: max(12px, env(safe-area-inset-bottom));
    padding-left: calc(16px + env(safe-area-inset-left, 0px));
    padding-right: calc(16px + env(safe-area-inset-right, 0px));
    background: var(--danger-bg);
    color: var(--danger-text);
    font-family: var(--font-body);
    font-size: 0.85rem;
    box-shadow: 0 -2px 12px var(--shadow-lg);
}

.bw-error-ui-inner { display: flex; align-items: center; gap: 12px; }

.bw-error-ui-text { flex: 1; }

.bw-error-ui-action,
.bw-error-ui-dismiss {
    color: inherit;
    font-weight: 700;
    text-decoration: underline;
    cursor: pointer;
}

.bw-error-ui-dismiss { text-decoration: none; font-size: 1.3rem; line-height: 1; }

/* ===========================================================================
   Wall-detail layout: fit-to-viewport photo + overlaid edit pencil + nav reclaim
   These live here (app.css) as intentional overrides of the base wall rules in
   pages.css / components.css. app.css loads FIRST, so every selector below is
   specificity-bumped (e.g. :root:root, .wall-main prefixes) to win the cascade.
   =========================================================================== */

/* TASK 3 — Re-budget the wall photo chrome so the image AND the three stat cards
   fit one viewport with no scroll. The Photo/Schematic toggle row is gone, which
   frees ~40px above the image; the stat-card block below the photo (card height +
   its margin + the wall-main column gap) must now share the same viewport slice,
   so fold it into the chrome the photo box subtracts from 100dvh. Deliberately
   generous so the image is guaranteed contained on a phone; desktop is width-
   bounded by the content column, so a slightly smaller cap looks fine there too.
   Only wall photo boxes read --wall-photo-chrome (see pages.css), so this is scoped. */
:root:root {
    /* Measured, not estimated: .stats-bar is 82px tall on the phone tier with a 20px bottom
       margin, and .wall-main adds its own 12px column gap under it. (The old 96px assumed a
       64px card and under-budgeted the slice by 18px.) */
    --wall-stats-h: 114px;                                   /* 82px stats bar + 20px margin + 12px column gap */
    /* The WallDimControl row under the photo. Broken out of the base below because
       WallPhotoEditor renders it on its READ-ONLY arms only (`@if (!CanEdit && !BorderOnly)`),
       so edit mode has to be able to subtract it again — see pages.css. */
    --wall-dim-bar-h: 40px;
    /* Split in two so the desktop tier can drop the stats term without restating the base: there
       the stat cards live in the right-hand rail beside the photo, not under it, so the photo has
       no reason to budget for them (pages.css, @container style(--tier: desktop)). */
    --wall-photo-chrome-base: 140px;                         /* carousel padding + card padding + --wall-dim-bar-h + dots + gaps (toggle row removed) */
    --wall-photo-chrome: calc(var(--wall-photo-chrome-base) + var(--wall-stats-h));

    /* Re-budget the boulder photo chrome so the whole top of the boulder detail page — title
       row, view/historic toggle, the image, the tags/rule chips, AND the Attempt/Send/Flash
       buttons — fits one viewport with no scroll (the base 96px in components.css only covered
       the bits ABOVE the image). Everything the photo box must leave room for, summed:
         32  .boulder-detail padding (16 top + 16 bottom)
         98  page-header: back link (~24) + title row (~40) + setter/date meta (~22) + margin (12)
         40  WallDimControl dim bar
         12  photo-section margin-bottom
         36  legend-bar chips (~28) + margin (8)
         44  rules-bar chips (~28) + margin (16)
         68  attempt buttons (~52) + attempt-section margin (16)
       ≈ 330px, kept generous so the image is guaranteed contained. THIS is the knob to tune:
       nudge it DOWN if the image ends up too small on your device, UP if the buttons ever get
       pushed off-screen. Only boulder photo boxes read the token, so nothing else is affected;
       --bottomnav-h collapses when the tab bar hides, reclaiming that space automatically. */
    --boulder-photo-chrome: 330px;
}

/* TASK 2 — Edit pencil overlaid on the image's top-right, co-located with the
   editor's usage "lens" button. Absolutely positioned inside the photo container
   so it floats over whatever renderer is inside (single-image editor, multi-panel
   viewer, or schematic) and shows whenever _canEditHolds, independent of the lens. */
.wall-main .wall-photo-container { position: relative; }
.btn-icon.wall-edit-fab {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 20;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--bg-surface);
    color: var(--text);
    box-shadow: 0 2px 10px var(--shadow-md);
}
.btn-icon.wall-edit-fab:hover { color: var(--text-heading); }
.btn-icon.wall-edit-fab.editing { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }

/* Keep the FAB from covering the editor's own top toolbar buttons (the lens sits at
   its right edge in the read-only view): reserve room on the right of the non-docked
   toolbar so the lens/name toggles shift clear of the floated pencil. The docked
   (bottom) editing toolbar is untouched — the FAB then floats over the image alone. */
.wall-main .wall-photo-container .bw-toolbar:not(.docked-bottom) .editor-toolbar { padding-right: 54px; }

/* TASK 4 — Reclaim the space freed when the bottom nav is hidden. Mirrors the
   docked-editor precedent above: zero the .content reservation and collapse the
   bottom-nav height token so every dvh-based view (incl. the Task 3 photo cap)
   reclaims the ~72px. The token is inherited from :root, so redefining it on
   <body> wins for the whole subtree without a specificity fight. */
body.bw-tabbar-collapsed { --bottomnav-h: max(8px, env(safe-area-inset-bottom)); }
body.bw-tabbar-collapsed .content { padding-bottom: 0; }

/* Drop the connection pill/panel down to where the (now-hidden) tab bar used to sit
   so they don't float in a dead gap. Their +74/112px offsets cleared the tab bar. */
body.bw-tabbar-collapsed .bw-conn-pill { bottom: calc(16px + env(safe-area-inset-bottom)); }
body.bw-tabbar-collapsed .bw-conn-panel { bottom: calc(54px + env(safe-area-inset-bottom)); }
body.bw-tabbar-collapsed .bw-install { bottom: calc(16px + env(safe-area-inset-bottom)); }
body.bw-tabbar-collapsed .app-shell:has(.carousel-dots) .bw-install { bottom: calc(44px + env(safe-area-inset-bottom)); }

/* Admin maintenance panel: long-running, admin-triggered jobs and their live log. */
.maintenance { margin-top: 32px; }
.maintenance h2 { font-size: 1.1rem; margin-bottom: 12px; }
.maintenance-job {
    display: flex; align-items: flex-start; gap: 16px;
    padding: 14px 0; border-top: 1px solid var(--border);
}
.maintenance-job-text { flex: 1; min-width: 0; }
.maintenance-job-text p { margin: 4px 0 0; color: var(--text-secondary); font-size: 0.88rem; }
.maintenance-job-actions { display: flex; flex-direction: column; gap: 6px; }
.maintenance-job > .btn-secondary,
.maintenance-job-actions > .btn-secondary,
.maintenance-job-actions > .btn-danger {
    width: auto; margin: 0; white-space: nowrap;
}
.maintenance-status { margin-top: 16px; border-top: 1px solid var(--border); padding-top: 12px; }
.maintenance-status-line {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 0.9rem;
}
.maintenance-elapsed { color: var(--text-secondary); }
.maintenance-stop {
    background: none; border: 1px solid var(--border); border-radius: var(--r-md);
    padding: 2px 10px; cursor: pointer; color: var(--accent); font-size: 0.85rem;
}
.maintenance-log {
    margin-top: 10px; max-height: 260px; overflow: auto;
    background: var(--bg-surface-alt); border-radius: var(--r-md); padding: 10px;
    font-size: 0.78rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word;
}
