: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);
    --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;
    --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;

    color-scheme: light dark;
}

@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);
        --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;
        --overlay-bg: rgba(0, 0, 0, 0.7);
    }
}

[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);
    --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;
    --overlay-bg: rgba(0, 0, 0, 0.7);
}

*, *::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);
    min-height: 100vh;
    -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: 100vh;
}

.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; }

.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;
    padding-bottom: 72px;
}

.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));
    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; }

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

.editor-toolbar.docked-bottom {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    border-radius: 0;
    padding-bottom: max(8px, env(safe-area-inset-bottom));
    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(.editor-toolbar.docked-bottom) .wall-page {
    padding-bottom: calc(120px + 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(.editor-toolbar.docked-bottom) .editor-bottom {
    min-height: 42px;
    scroll-margin-bottom: calc(120px + 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); }

/* ---------------------------------------------------------------------------
   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));
    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 {
    --wall-stats-h: 96px;                                    /* ~64px stat card + 20px margin + 12px column gap */
    --wall-photo-chrome: calc(140px + var(--wall-stats-h));  /* carousel padding + card padding + dim bar + dots + gaps (toggle row removed) + stats */

    /* 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 .editor-toolbar:not(.docked-bottom) { 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)); }
