/* ==========================================================================
   Touchpad design system — "Signal Orange on graphite"
   --------------------------------------------------------------------------
   Spider-Man tech x Watch Dogs ctOS, deliberately restrained: the interface is
   graphite and type, and orange only ever marks *state* and *action*. If more
   than about a tenth of the screen is orange, something is wrong.

   Everything visual lives here. Templates use these classes and nothing else —
   no per-page <style> blocks, so alignment and colour stay consistent by
   construction instead of by tuning each page.
   ========================================================================== */

:root {
    /* --- surface ramp ---------------------------------------------------- */
    --ink:        #0B0B0D;  /* page base */
    --ink-2:      #141417;  /* panel surface */
    --ink-3:      #1B1B1F;  /* raised: input wells, hover */
    --ink-4:      #232328;  /* pressed / selected */
    --line:       #26262B;  /* hairline border */
    --line-soft:  #1D1D21;  /* internal dividers */

    /* --- text ------------------------------------------------------------ */
    --text:       #E8E6E3;  /* warm off-white */
    --text-dim:   #B4B2AE;
    --muted:      #8A8A93;  /* labels, meta, timestamps */
    --muted-deep: #5C5C64;  /* disabled */

    /* --- accents --------------------------------------------------------- */
    --signal:     #FF6A1F;  /* THE accent */
    --signal-hot: #FF8341;  /* hover */
    --signal-dim: #C24E14;  /* pressed / borders */
    --amber:      #FFA92B;  /* secondary state, warnings */
    --alert:      #E5484D;  /* errors and alerts ONLY */
    --ok:         #3DD68C;  /* connected / live */

    /* Translucent accent washes — pre-mixed so they stay consistent. */
    --signal-a08: rgba(255, 106, 31, 0.08);
    --signal-a16: rgba(255, 106, 31, 0.16);
    --signal-a32: rgba(255, 106, 31, 0.32);
    --signal-a60: rgba(255, 106, 31, 0.60);

    /* --- glass ----------------------------------------------------------- */
    /* Panels float over a live map, so they need real opacity to stay legible
       while still letting the city read through. */
    --glass:      rgba(17, 17, 20, 0.82);
    --glass-soft: rgba(20, 20, 24, 0.66);
    --glass-blur: blur(20px) saturate(120%);

    /* --- 8px baseline grid ----------------------------------------------- */
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 24px;
    --sp-6: 32px;
    --sp-7: 48px;
    --sp-8: 64px;

    /* --- geometry -------------------------------------------------------- */
    --r-sm:  6px;
    --r-md:  8px;
    --r-lg:  12px;
    --r-pill: 999px;
    --notch: 9px;  /* corner cut on primary actions */

    /* Single source of truth for horizontal alignment. Every top-level row —
       top bar, toolbar, widget grid — uses this, so nothing can drift. */
    --gutter: 24px;
    --shell-max: 1440px;

    /* --- elevation ------------------------------------------------------- */
    --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-2: 0 4px 16px rgba(0, 0, 0, 0.45);
    --shadow-3: 0 12px 40px rgba(0, 0, 0, 0.55);
    --hairline-top: inset 0 1px 0 rgba(255, 255, 255, 0.06);

    /* --- motion ---------------------------------------------------------- */
    --t-fast: 120ms cubic-bezier(0.2, 0, 0.2, 1);
    --t-mid:  200ms cubic-bezier(0.2, 0, 0.2, 1);
    --t-slow: 320ms cubic-bezier(0.16, 1, 0.3, 1);

    /* --- type ------------------------------------------------------------ */
    --font-display: 'Rajdhani', system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

    --z-map: 0;
    --z-content: 10;
    --z-header: 100;
    --z-overlay: 1000;
    --z-modal: 4000;
}

@media (min-width: 768px) {
    :root { --gutter: 32px; }
}

/* ==========================================================================
   Reset / base
   ========================================================================== */

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

html, body {
    height: 100%;
    margin: 0;
    padding: 0;
}

body {
    background: var(--ink);
    color: var(--text);
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 15px;
    line-height: 1.45;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

/* Orange selection instead of the browser's blue — small detail, sells it. */
::selection { background: var(--signal-a32); color: #fff; }

a { color: inherit; text-decoration: none; }

:focus-visible {
    outline: 2px solid var(--signal);
    outline-offset: 2px;
    border-radius: 2px;
}

/* Scrollbars, quietly themed. */
* { scrollbar-width: thin; scrollbar-color: var(--ink-4) transparent; }
*::-webkit-scrollbar { width: 6px; height: 6px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--ink-4); border-radius: var(--r-pill); }
*::-webkit-scrollbar-thumb:hover { background: var(--signal-dim); }

/* ==========================================================================
   App shell
   --------------------------------------------------------------------------
   header / main / footer as a fixed vertical stack. `main` is the only thing
   that scrolls, so the map behind it can stay pinned to the viewport.
   ========================================================================== */

body {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

main {
    flex: 1 1 auto;
    position: relative;
    overflow: hidden;
    min-height: 0;
}

#spa-content {
    position: absolute;
    inset: 0;
    z-index: var(--z-content);
    overflow-y: auto;
    overscroll-behavior: contain;
}

#app-footer {
    flex-shrink: 0;
    height: 44px;
    background: var(--ink);
    border-top: 1px solid var(--line);
    z-index: var(--z-header);
    padding-bottom: env(safe-area-inset-bottom);
}
#app-footer a { transition: color var(--t-fast); }
#app-footer a:hover { color: var(--signal); }

/* Hold the page back until the first tiles land, so nothing paints over a
   blank map. */
body > main, body > header, body > footer { opacity: 0; transition: opacity var(--t-slow); }
body.app-ready > main, body.app-ready > header, body.app-ready > footer { opacity: 1; }

/* ==========================================================================
   Header
   ========================================================================== */

.hdr-row { height: 100%; gap: var(--sp-5); }

#app-header {
    flex-shrink: 0;
    height: 56px;
    background: var(--ink);
    border-bottom: 1px solid var(--line);
    z-index: var(--z-header);
    padding-top: env(safe-area-inset-top);
}

.hdr-brand { display: inline-flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }
.hdr-brand__mark {
    width: 10px; height: 10px;
    background: var(--signal);
    /* Angular mark rather than a circle — the Spider-tech note, at 10px. */
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
.hdr-brand__text {
    font-family: var(--font-display);
    font-size: 15px; font-weight: 700;
    letter-spacing: 0.16em;
    color: var(--text);
}

.hdr-nav { display: flex; align-items: center; gap: var(--sp-1); height: 100%; }
.hdr-tab {
    position: relative;
    display: inline-flex; align-items: center;
    height: 100%;
    padding: 0 var(--sp-3);
    font-family: var(--font-mono);
    font-size: 11px; font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--muted);
    transition: color var(--t-fast);
}
.hdr-tab:hover { color: var(--text); }
.hdr-tab.is-active { color: var(--text); }
/* Underline marks the active tab — the accent doing structural work. */
.hdr-tab.is-active::after {
    content: '';
    position: absolute;
    left: var(--sp-3); right: var(--sp-3); bottom: -1px;
    height: 2px;
    background: var(--signal);
}

.hdr-loc {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    height: 32px;
    padding: 0 10px;
    max-width: 240px;
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    color: var(--text-dim);
    cursor: pointer;
    transition: border-color var(--t-fast), color var(--t-fast);
}
.hdr-loc:hover { border-color: var(--signal-a60); color: var(--text); }
.hdr-loc > i { color: var(--signal); font-size: 11px; }
.hdr-loc__text {
    font-family: var(--font-mono);
    font-size: 11px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hdr-loc__badge {
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--amber);
    border: 1px solid rgba(255, 169, 43, 0.3);
    border-radius: var(--r-sm);
    padding: 1px 4px;
    flex-shrink: 0;
}

.hdr-user {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    padding: 4px 10px 4px 4px;
    border-radius: var(--r-pill);
    max-width: 180px;
    transition: background var(--t-fast);
}
.hdr-user:hover { background: var(--ink-2); }
.hdr-user__name { font-family: var(--font-mono); font-size: 11px; color: var(--text-dim); }

/* ==========================================================================
   Layout
   ========================================================================== */

/* The one container. Centres content and owns the gutters, so the top bar,
   toolbar and widget grid share an identical left edge on every page. */
.tp-container {
    width: 100%;
    max-width: var(--shell-max);
    margin-inline: auto;
    padding-inline: var(--gutter);
}

.tp-stack   { display: flex; flex-direction: column; gap: var(--sp-3); }
.tp-row     { display: flex; align-items: center; gap: var(--sp-2); }
.tp-row-wrap{ display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.tp-spacer  { flex: 1 1 auto; }

/* ==========================================================================
   Type
   ========================================================================== */

/* The ctOS voice: tiny, tracked-out, monospaced. Used for every label,
   readout and timestamp in the app. */
.tp-label {
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
    line-height: 1;
}

.tp-meta {
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 400;
    color: var(--muted);
    line-height: 1.3;
}

.tp-title {
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 700;
    letter-spacing: 0.01em;
    color: var(--text);
    line-height: 1.2;
}

.tp-display {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: clamp(28px, 5vw, 44px);
    line-height: 1.05;
    letter-spacing: -0.02em;
    color: var(--text);
}

.tp-readout {
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 18px;
    color: var(--text);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.tp-readout .unit { font-size: 11px; color: var(--muted); margin-left: 2px; }

.tp-accent { color: var(--signal); }
.tp-dim    { color: var(--muted); }

/* ==========================================================================
   Panel — the single glass recipe
   ========================================================================== */

.tp-panel {
    position: relative;
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-2), var(--hairline-top);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* One corner bracket, not four. Suggests the HUD without cosplaying it. */
.tp-panel::after {
    content: '';
    position: absolute;
    top: -1px; left: -1px;
    width: 14px; height: 14px;
    border-top: 2px solid var(--signal);
    border-left: 2px solid var(--signal);
    border-top-left-radius: var(--r-lg);
    opacity: 0.55;
    pointer-events: none;
}
.tp-panel--plain::after { display: none; }

.tp-panel__head {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--line-soft);
    flex-shrink: 0;
    min-height: 44px;
}

.tp-panel__head i { color: var(--signal); font-size: 12px; }

.tp-panel__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: var(--sp-3) var(--sp-4);
}
.tp-panel__body--flush { padding: 0; }

.tp-panel__foot {
    flex-shrink: 0;
    padding: var(--sp-3);
    border-top: 1px solid var(--line-soft);
    background: rgba(0, 0, 0, 0.22);
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.tp-btn {
    appearance: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    min-height: 36px;
    padding: 0 var(--sp-3);
    background: var(--ink-3);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.02em;
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--t-fast), border-color var(--t-fast),
                color var(--t-fast), transform var(--t-fast);
}
.tp-btn:hover { background: var(--ink-4); border-color: #33333A; }
.tp-btn:active { transform: translateY(1px); }
.tp-btn i { font-size: 12px; }

.tp-btn.is-active,
.tp-btn[aria-pressed="true"] {
    background: var(--signal-a16);
    border-color: var(--signal-a60);
    color: var(--signal-hot);
}

/* Primary action — the notched corner is the one overt Spider-tech gesture. */
.tp-btn--primary {
    background: var(--signal);
    border-color: var(--signal);
    color: #140A04;
    font-weight: 700;
    clip-path: polygon(
        0 0, calc(100% - var(--notch)) 0, 100% var(--notch),
        100% 100%, var(--notch) 100%, 0 calc(100% - var(--notch))
    );
}
.tp-btn--primary:hover { background: var(--signal-hot); border-color: var(--signal-hot); }

.tp-btn--ghost {
    background: transparent;
    border-color: var(--line);
    color: var(--text-dim);
}
.tp-btn--ghost:hover { background: var(--ink-3); color: var(--text); }

.tp-btn--danger { color: var(--alert); border-color: rgba(229, 72, 77, 0.35); }
.tp-btn--danger:hover { background: rgba(229, 72, 77, 0.14); border-color: var(--alert); }

.tp-btn--icon { width: 36px; min-width: 36px; padding: 0; }

.tp-btn--sm { min-height: 28px; font-size: 12px; padding: 0 var(--sp-2); }
.tp-btn--lg { min-height: 46px; font-size: 15px; padding: 0 var(--sp-5); }

.tp-btn:disabled,
.tp-btn[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
    pointer-events: none;
}

/* ==========================================================================
   Fields
   ========================================================================== */

.tp-field {
    width: 100%;
    min-height: 36px;
    padding: var(--sp-2) var(--sp-3);
    background: var(--ink-3);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 500;
    transition: border-color var(--t-fast), background var(--t-fast);
}
.tp-field::placeholder { color: var(--muted-deep); }
.tp-field:hover:not(:disabled) { border-color: #33333A; }
.tp-field:focus {
    outline: none;
    border-color: var(--signal);
    background: var(--ink-2);
    box-shadow: 0 0 0 3px var(--signal-a08);
}
.tp-field:disabled { color: var(--muted-deep); cursor: not-allowed; }

textarea.tp-field { min-height: 84px; resize: vertical; line-height: 1.4; }

/* Input with a leading icon. */
.tp-field-wrap { position: relative; display: flex; align-items: center; width: 100%; }
.tp-field-wrap > i {
    position: absolute;
    left: var(--sp-3);
    font-size: 12px;
    color: var(--muted);
    pointer-events: none;
}
.tp-field-wrap > .tp-field { padding-left: 32px; }
.tp-field-wrap:focus-within > i { color: var(--signal); }

/* ==========================================================================
   Chips, badges, avatars
   ========================================================================== */

.tp-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    height: 26px;
    padding: 0 10px;
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
}
.tp-chip--signal { color: var(--signal); border-color: var(--signal-a32); background: var(--signal-a08); }
.tp-chip--alert  { color: var(--alert);  border-color: rgba(229, 72, 77, 0.3); background: rgba(229, 72, 77, 0.08); }

/* Live indicator — the only thing allowed to pulse. */
.tp-live { display: inline-flex; align-items: center; gap: 6px; }
.tp-live::before {
    content: '';
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--ok);
    box-shadow: 0 0 0 0 rgba(61, 214, 140, 0.5);
    animation: tp-pulse 2.4s ease-out infinite;
}
.tp-live--off::before { background: var(--muted-deep); animation: none; box-shadow: none; }
@keyframes tp-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(61, 214, 140, 0.45); }
    70%  { box-shadow: 0 0 0 6px rgba(61, 214, 140, 0); }
    100% { box-shadow: 0 0 0 0 rgba(61, 214, 140, 0); }
}

.tp-avatar {
    width: 36px; height: 36px;
    flex-shrink: 0;
    border-radius: 50%;
    overflow: hidden;
    background: var(--ink-4);
    border: 1px solid var(--line);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    font-size: 13px;
}
.tp-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tp-avatar--sm { width: 26px; height: 26px; font-size: 11px; }
.tp-avatar--lg { width: 72px; height: 72px; font-size: 24px; }
.tp-avatar--me { border-color: var(--signal-a60); }

/* ==========================================================================
   Radius slider
   ========================================================================== */

.tp-range {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 4px;
    background: var(--ink-4);
    border-radius: var(--r-pill);
    outline: none;
    cursor: pointer;
    margin: 0;
}
.tp-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px; height: 14px;
    border-radius: 50%;
    background: var(--signal);
    border: 2px solid var(--ink);
    cursor: grab;
    box-shadow: 0 0 0 1px var(--signal-a60);
    transition: transform var(--t-fast);
}
.tp-range::-webkit-slider-thumb:active { cursor: grabbing; transform: scale(1.15); }
.tp-range::-moz-range-thumb {
    width: 14px; height: 14px;
    border-radius: 50%;
    background: var(--signal);
    border: 2px solid var(--ink);
    cursor: grab;
}

/* ==========================================================================
   The guest gate
   --------------------------------------------------------------------------
   Guests see every message and post in full. Only the composer is gated, and
   it is gated by *replacing* it with an invitation rather than by disabling a
   dead input — a disabled textbox reads as broken, an invitation reads as a
   door.
   ========================================================================== */

.tp-gate {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3);
    background: linear-gradient(180deg, var(--signal-a08), transparent);
    border: 1px dashed var(--signal-a32);
    border-radius: var(--r-md);
}
.tp-gate__icon {
    width: 30px; height: 30px;
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    border-radius: var(--r-sm);
    background: var(--signal-a16);
    color: var(--signal);
    font-size: 12px;
}
.tp-gate__text { flex: 1 1 auto; min-width: 0; }
.tp-gate__title { font-size: 13px; font-weight: 600; color: var(--text); line-height: 1.25; }
.tp-gate__sub   { font-family: var(--font-mono); font-size: 10px; color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; margin-top: 3px; }

/* ==========================================================================
   Modal
   ========================================================================== */

.tp-modal {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: none;
    align-items: center;
    justify-content: center;
    padding: var(--sp-4);
    background: rgba(5, 5, 6, 0.78);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    opacity: 0;
    transition: opacity var(--t-mid);
    /* Taller-than-viewport dialogs (the location picker, the news composer)
       must be reachable rather than clipped. The backdrop scrolls, and the box
       scrolls inside itself — so the actions at the bottom are always usable. */
    overflow-y: auto;
    overscroll-behavior: contain;
}
.tp-modal.is-open { display: flex; opacity: 1; }

.tp-modal__box {
    width: 100%;
    max-width: 440px;
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3);
    padding: var(--sp-5);
    transform: translateY(8px) scale(0.985);
    transition: transform var(--t-slow);
    /* dvh, not vh: mobile browser chrome otherwise pushes the footer of the
       dialog under the address bar. */
    max-height: calc(100dvh - var(--sp-6));
    overflow-y: auto;
    /* Don't let flex centring squash a tall box. */
    margin: auto;
    flex-shrink: 0;
}
.tp-modal.is-open .tp-modal__box { transform: none; }

.tp-modal__box--wide { max-width: 560px; }

/* --- location picker ----------------------------------------------------- */

.loc-picker__map {
    /* Flexible: on a short laptop viewport the map gives way first so the
       whole dialog fits without scrolling. */
    height: clamp(150px, 24dvh, 240px);
    width: 100%;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    overflow: hidden;
    background: var(--ink);
    cursor: crosshair;
}
.loc-picker__map .leaflet-container { background: var(--ink); }

.loc-places {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    max-height: 92px;
    overflow-y: auto;
}
.loc-place {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 28px;
    padding: 0 10px;
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    color: var(--text-dim);
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
}
.loc-place:hover { border-color: var(--signal-a60); color: var(--text); }
.loc-place.is-active {
    background: var(--signal-a16);
    border-color: var(--signal);
    color: var(--signal-hot);
}

.loc-picker__foot {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin-top: var(--sp-4);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--line-soft);
    flex-wrap: wrap;
}

/* The draggable pin shown while choosing a point. */
.loc-pin {
    position: absolute;
    top: 50%; left: 50%;
    width: 14px; height: 14px;
    margin: -7px 0 0 -7px;
    border-radius: 50%;
    background: var(--signal);
    border: 2px solid var(--ink);
    box-shadow: 0 0 0 1px var(--signal-a60), 0 0 14px var(--signal-a60);
}

.tp-modal__title {
    font-family: var(--font-display);
    font-size: 22px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--text);
    margin: 0 0 var(--sp-2);
}
.tp-modal__body { color: var(--text-dim); font-size: 14px; line-height: 1.5; }
.tp-modal__actions {
    display: flex;
    gap: var(--sp-2);
    margin-top: var(--sp-5);
    flex-wrap: wrap;
}
.tp-modal__actions .tp-btn { flex: 1 1 auto; }

/* Fine print under a privacy-sensitive action. */
.tp-fineprint {
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1.5;
    color: var(--muted-deep);
    letter-spacing: 0.03em;
    margin-top: var(--sp-3);
}

/* ==========================================================================
   Tutorial
   ========================================================================== */

.tut { position: fixed; inset: 0; z-index: var(--z-modal); display: none; }
.tut.is-open { display: block; }
/* body never scrolls in this app — #spa-content is the scroller, so that is
   what has to be frozen while the tour is pinning things to the viewport. */
body.tut-active #spa-content { overflow: hidden; }

/* The dim layer is the spotlight's own huge box-shadow, which leaves the
   target punched out and fully visible. */
.tut__spot {
    position: absolute;
    border-radius: var(--r-md);
    box-shadow: 0 0 0 9999px rgba(5, 5, 6, 0.78);
    border: 1px solid var(--signal);
    pointer-events: none;
    transition: all var(--t-slow);
    opacity: 0;
}

.tut__card {
    position: absolute;
    width: 360px;
    max-width: calc(100vw - var(--sp-6));
    max-height: calc(100dvh - var(--sp-6));
    overflow-y: auto;
    padding: var(--sp-4);
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3);
    transition: left var(--t-slow), top var(--t-slow);
}

.tut__head { display: flex; align-items: center; margin-bottom: var(--sp-3); }
.tut__skip {
    background: none; border: 0; cursor: pointer;
    font-family: var(--font-mono); font-size: 10px;
    letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--muted);
}
.tut__skip:hover { color: var(--text); }

.tut__title {
    font-family: var(--font-display);
    font-size: 18px; font-weight: 700;
    color: var(--text);
    margin: 0 0 var(--sp-2);
    line-height: 1.25;
}
.tut__body { font-size: 14px; line-height: 1.55; color: var(--text-dim); }
.tut__body p { margin: 0 0 var(--sp-2); }
.tut__body b { color: var(--text); font-weight: 600; }

.tut__cookies {
    list-style: none;
    margin: var(--sp-2) 0;
    padding: 0;
}
.tut__cookies li {
    position: relative;
    padding: 6px 0 6px var(--sp-4);
    font-size: 13px;
    line-height: 1.5;
    border-bottom: 1px solid var(--line-soft);
}
.tut__cookies li:last-child { border-bottom: 0; }
.tut__cookies li::before {
    content: '';
    position: absolute; left: 4px; top: 14px;
    width: 4px; height: 4px;
    border-radius: 50%;
    background: var(--signal);
}
.tut__cookies b {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--signal);
    font-weight: 500;
}

.tut__foot {
    display: flex; align-items: center; gap: var(--sp-2);
    margin-top: var(--sp-4);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--line-soft);
}
.tut__dots { display: flex; gap: 5px; }
.tut__dot {
    width: 5px; height: 5px;
    border-radius: 50%;
    background: var(--ink-4);
    transition: background var(--t-fast);
}
.tut__dot.is-active { background: var(--signal); }

/* ==========================================================================
   Feed / chat rows — shared list vocabulary
   ========================================================================== */

.tp-item {
    display: block;
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--line-soft);
    border-left: 2px solid transparent;
    transition: background var(--t-fast), border-color var(--t-fast);
}
.tp-item:last-child { border-bottom: 0; }
.tp-item:hover { background: rgba(255, 255, 255, 0.02); border-left-color: var(--signal-a60); }
.tp-item--pinned { border-left-color: var(--signal); background: var(--signal-a08); }

.tp-item__head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: 5px; }
.tp-item__who  { font-size: 13px; font-weight: 700; color: var(--text); }
.tp-item__body { font-size: 13.5px; color: var(--text-dim); line-height: 1.45; overflow-wrap: anywhere; }
.tp-item__image {
    display: block;
    width: 100%;
    max-height: 160px;
    object-fit: cover;
    border-radius: var(--r-sm);
    border: 1px solid var(--line);
    margin: var(--sp-2) 0;
}

/* A message that just arrived over the socket, briefly marked. */
.tp-item--fresh { animation: tp-arrive var(--t-slow) ease-out; }
@keyframes tp-arrive {
    from { background: var(--signal-a16); transform: translateY(-3px); }
    to   { background: transparent; transform: none; }
}

/* Distance readout — the app's signature data point, so it gets the accent. */
.tp-dist {
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
    color: var(--signal);
    letter-spacing: 0.04em;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.tp-empty {
    padding: var(--sp-6) var(--sp-4);
    text-align: center;
    color: var(--muted-deep);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.8;
}
.tp-empty i { display: block; font-size: 20px; margin-bottom: var(--sp-3); color: var(--ink-4); }

/* ==========================================================================
   Map background
   --------------------------------------------------------------------------
   One style, dark, always. The grade below is what turns stock Carto tiles
   into something that reads as a ctOS surveillance grid: darken, drop most of
   the remaining colour, then lay a whisper of orange over it.
   ========================================================================== */

#bg-map {
    position: absolute;
    inset: 0;
    z-index: var(--z-map);
    background: var(--ink);
}

/* The dark basemap is graded down further so the glass panels stay legible
   over it. Light and satellite are chosen precisely to be read, so neither the
   darkening nor the orange vignette applies to them. */
#bg-map .leaflet-tile-pane {
    filter: brightness(0.72) contrast(1.12) saturate(0.35);
}
/* Voyager is a cheerful map. Pull the saturation back and lift it a touch so
   it reads as clean daylight rather than a competing palette — the pins and
   the radius ring have to stay the brightest things on it. */
body.tp-map--light #bg-map .leaflet-tile-pane {
    filter: saturate(0.62) brightness(1.04) contrast(0.96);
}
body.tp-map--satellite #bg-map .leaflet-tile-pane { filter: none; }
body.tp-map--light .tp-map-grade,
body.tp-map--satellite .tp-map-grade { opacity: 0; }
/* Place labels over imagery need a dark backing, not a light one. */
body.tp-map--satellite #bg-map { background: #101418; }
body.tp-map--light #bg-map { background: #F2F1ED; }

/* 3D mode — a MapLibre map in the same box as #bg-map, one visible at a time.
   Leaflet has no camera pitch, so standing the buildings up means a second
   engine rather than a setting; see js/map3d.js. The flat map keeps running
   underneath and stays the source of truth for the ring and your point. */
#bg-map-3d {
    position: absolute;
    inset: 0;
    z-index: var(--z-map);
    background: var(--ink);
    display: none;
}
body.is-3d #bg-map-3d { display: block; }
body.is-3d #bg-map { visibility: hidden; }
/* The style is already graphite; the grade would only mud it. */
body.is-3d .tp-map-grade { opacity: 0; }

/* MapLibre's own chrome, dressed down to a footnote. */
#bg-map-3d .maplibregl-ctrl-attrib {
    background: rgba(11, 11, 13, 0.6);
    border-radius: var(--r-sm) 0 0 0;
}
#bg-map-3d .maplibregl-ctrl-attrib,
#bg-map-3d .maplibregl-ctrl-attrib a {
    color: var(--muted-deep);
    font-family: var(--font-mono);
    font-size: 9px;
}
#bg-map-3d .maplibregl-ctrl-attrib-button { filter: invert(1) opacity(0.4); }

/* Orange wash + vignette, painted above the tiles but below all content. */
.tp-map-grade {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background:
        radial-gradient(120% 90% at 50% 50%, transparent 40%, rgba(5, 5, 6, 0.72) 100%),
        linear-gradient(180deg, rgba(255, 106, 31, 0.05), rgba(255, 106, 31, 0.02));
    mix-blend-mode: normal;
}

.leaflet-container { background: var(--ink) !important; font-family: var(--font-display) !important; }
.leaflet-control-attribution { display: none !important; }

/* NB: the background map runs Leaflet's canvas renderer (preferCanvas), so the
   radius ring is not an SVG path and cannot be faded with CSS. Its fade is
   tweened in JS instead — see __dashRing in dashboard.html. */

/* "You are here", wearing your face. Anchored so the circle's centre sits on
   the actual coordinate, not its top-left corner. */
.tp-me {
    position: absolute;
    top: 50%; left: 50%;
    width: 34px; height: 34px;
    margin: -17px 0 0 -17px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--ink-3);
    border: 2px solid var(--signal);
    box-shadow: 0 0 0 2px var(--ink), 0 2px 10px rgba(0, 0, 0, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
}
.tp-me img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tp-me__initial {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    color: var(--signal);
    line-height: 1;
}
/* A small pointer so it reads as a pin rather than a floating avatar. */
.tp-me::after {
    content: '';
    position: absolute;
    bottom: -6px; left: 50%;
    margin-left: -4px;
    width: 8px; height: 8px;
    background: var(--signal);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
}

/* "You are here" — the exact centre of the radius ring. */
.tp-here {
    position: absolute;
    top: 50%; left: 50%;
    width: 12px; height: 12px;
    margin: -6px 0 0 -6px;
    border-radius: 50%;
    background: var(--signal);
    border: 2px solid var(--ink);
    box-shadow: 0 0 0 1px var(--signal-a60), 0 0 12px var(--signal-a60);
}

/* Radio-wave ripple, shown at the radius centre while the slider moves. */
.radiowave { position: relative; width: 0; height: 0; pointer-events: none; }
.radiowave span {
    position: absolute;
    top: 50%; left: 50%;
    width: 40px; height: 40px;
    margin: -20px 0 0 -20px;
    border-radius: 50%;
    border: 2px solid var(--signal);
    opacity: 0;
    animation: radiowave-pulse 1.6s cubic-bezier(0.22, 1, 0.36, 1) infinite;
}
.radiowave span:nth-child(2) { animation-delay: 0.4s; }
.radiowave span:nth-child(3) { animation-delay: 0.8s; }
.radiowave span:nth-child(4) { animation-delay: 1.2s; }
@keyframes radiowave-pulse {
    0%   { transform: scale(0.2); opacity: 0.85; border-width: 3px; }
    70%  { opacity: 0.35; }
    100% { transform: scale(8); opacity: 0; border-width: 1px; }
}
.radiowave-core {
    position: absolute;
    top: 50%; left: 50%;
    width: 8px; height: 8px;
    margin: -4px 0 0 -4px;
    background: var(--signal);
    border-radius: 50%;
    box-shadow: 0 0 12px var(--signal);
}

/* ==========================================================================
   Dashboard
   --------------------------------------------------------------------------
   The dashboard floats over the live map, so its root does NOT capture
   pointer events — only the actual controls do. That's what lets you drag the
   city around by grabbing the gaps between widgets.
   ========================================================================== */

.dash-root {
    position: relative;
    min-height: 100%;
    padding: var(--sp-4) 0 var(--sp-8);
    pointer-events: none;
}
.dash-root > * { pointer-events: auto; }

/* The one way back after wandering off. Centred at the top of the map so it
   reads as a status ("you are looking somewhere else") rather than a tool. */
.dash-home {
    position: absolute;
    top: var(--sp-3);
    left: 50%;
    transform: translateX(-50%);
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 8px var(--sp-4);
    background: var(--ink-2);
    border: 1px solid var(--signal-a60);
    border-radius: var(--r-pill);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: var(--shadow-3);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.dash-home i { color: var(--signal); font-size: 11px; }
.dash-home:hover { background: var(--signal-a16); border-color: var(--signal-hot); }

/* Appearance menu — basemap and 2D/3D behind one control at the top right.
   Off it is furniture; it only wears orange once the buildings are up. */
.dash-look {
    position: absolute;
    top: var(--sp-3);
    right: var(--gutter);
}
.dash-look__btn {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 8px var(--sp-3);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    color: var(--muted);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.dash-look__btn:hover { color: var(--text); border-color: var(--text-dim); }
/* The orange sits ON the glass rather than replacing it: a translucent accent
   alone washes out over a light or satellite basemap. */
.dash-look.is-3d .dash-look__btn {
    background: linear-gradient(var(--signal-a16), var(--signal-a16)), var(--glass);
    border-color: var(--signal);
    color: var(--signal-hot);
}
/* MapLibre is most of a megabyte; on a slow line the control has to say so. */
.dash-look.is-loading .dash-look__btn { color: var(--muted-deep); pointer-events: none; }
.dash-look.is-loading .dash-look__btn > i { animation: tp-spin 0.9s linear infinite; }

.dash-look__menu {
    position: absolute;
    top: calc(100% + var(--sp-2));
    right: 0;
    min-width: 200px;
    padding: var(--sp-3);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3);
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
}
.dash-look__group { display: flex; flex-direction: column; gap: 2px; }
.dash-look__group .tp-label { margin-bottom: var(--sp-1); }
.dash-look__opt {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    width: 100%;
    padding: 7px var(--sp-2);
    background: none;
    border: 0;
    border-radius: var(--r-sm);
    color: var(--text-dim);
    font-family: var(--font-display);
    font-size: 13px;
    text-align: left;
    cursor: pointer;
    transition: background var(--t-fast), color var(--t-fast);
}
.dash-look__opt > i { width: 14px; text-align: center; color: var(--muted); font-size: 11px; }
.dash-look__opt:hover { background: var(--ink-3); color: var(--text); }
.dash-look__opt.is-on { color: var(--signal-hot); }
.dash-look__opt.is-on > i { color: var(--signal); }
.dash-look__note {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 9px;
    color: var(--muted-deep);
}

@media (max-width: 480px) {
    .dash-home span { display: none; }
    .dash-home { padding: 8px 12px; }
    .dash-look { right: var(--sp-3); }
    .dash-look__btn span { display: none; }
}

/* Top identity row and the toolbar below it share .tp-container, so their
   left edges are identical by construction — not by matching two paddings. */
.dash-bar {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
    margin-bottom: var(--sp-3);
}

.dash-card {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
    height: 52px;
    padding: 0 var(--sp-4) 0 var(--sp-3);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-2), var(--hairline-top);
}
button.dash-card { cursor: pointer; transition: border-color var(--t-fast); }
button.dash-card:hover { border-color: var(--signal-a60); }

.dash-card__meta { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.dash-card__value { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--text); line-height: 1; }

.dash-stat__icon {
    width: 30px; height: 30px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: var(--r-sm);
    background: var(--signal-a08);
    border: 1px solid var(--signal-a32);
    color: var(--signal);
    font-size: 12px;
    flex-shrink: 0;
}

/* The single control bar. Everything sits in one glass strip so the map keeps
   as much of the viewport as possible. */
.dash-toolbar {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
    margin-bottom: var(--sp-3);
    padding: var(--sp-2);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-2), var(--hairline-top);
}

/* Inside the bar the controls are flush — the bar itself is the container. */
.dash-toolbar .dash-radius {
    background: transparent;
    border-color: transparent;
    padding: 0 var(--sp-2);
    border-left: 1px solid var(--line);
    border-radius: 0;
}
.dash-toolbar .dash-search .tp-field {
    background: var(--ink-3);
    backdrop-filter: none;
}

/* Radius control — the app's primary instrument, so it reads as one unit. */
.dash-radius {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
    height: 36px;
    padding: 0 var(--sp-4);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
}
.dash-radius .tp-range { width: 150px; }
.dash-radius__val { min-width: 62px; text-align: right; }

.dash-search { position: relative; min-width: 220px; flex: 0 1 300px; }
.dash-search .tp-field { background: var(--glass); backdrop-filter: var(--glass-blur); }

.dash-search__results {
    position: absolute;
    top: calc(100% + 6px); left: 0; right: 0;
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-3);
    max-height: 320px;
    overflow-y: auto;
    z-index: var(--z-overlay);
    display: none;
}
.dash-search__results.is-open { display: block; }
.dash-search__result { display: block; padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--line-soft); cursor: pointer; }
.dash-search__result:last-child { border-bottom: 0; }
.dash-search__result:hover { background: var(--signal-a08); }
.dash-search__result .t { font-size: 13px; font-weight: 600; color: var(--text); }
.dash-search__result .d { font-family: var(--font-mono); font-size: 10px; color: var(--muted); margin-top: 2px; }
.dash-search__empty { padding: var(--sp-3); font-family: var(--font-mono); font-size: 11px; color: var(--muted); text-align: center; }

/* --- widget grid --------------------------------------------------------- */

/* Gridstack adds its own item margin; the negative wrapper margin cancels it
   so widget edges line up flush with the container gutter. */
.dash-grid-wrap { margin: 0 -6px; }

.widget-frame { height: 100%; position: relative; }
.widget-slot { height: 100%; }
.widget-slot > * { height: 100%; }

/* htmx marks the element it is fetching for with .htmx-request. A widget that
   already has content shouldn't blank out mid-refresh — it just dims. */
.widget-slot {
    transition: opacity var(--t-fast);
    position: relative;
}
.widget-slot.htmx-request { opacity: 0.55; }

/* An empty slot on first load gets a placeholder panel so the grid doesn't
   visibly reflow when the content arrives. */
.widget-slot:empty::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: var(--r-lg);
    background: var(--glass);
    border: 1px solid var(--line);
    box-shadow: var(--shadow-2);
}

.dash-search__results.htmx-request { opacity: 0.6; }

/* ==========================================================================
   Standalone widget pages (/chat/, /news/, and their /<place>/ variants)
   --------------------------------------------------------------------------
   The same widget partials the dashboard grid loads, but as the centrepiece
   of a full page. The wrapper supplies the height the grid cell would have.
   ========================================================================== */

/* These pages are one instrument with a label on it: a compact heading block
   and then the widget, filling the window. #spa-content is already exactly the
   space between the app header and the footer, so 100% here is the whole of it
   — no viewport arithmetic to keep in step with the chrome. */
.page-fill {
    height: 100%;
    min-height: 0;
    padding: var(--sp-4) 0;
    display: flex;
}
.page-fill > .tp-stack {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

/* One heading block: what the page is, and where else to read it. The place
   chips used to be a second panel BELOW the widget, which cost a panel's worth
   of height and put the switcher out of sight. */
.page-head { flex: 0 0 auto; padding: var(--sp-4) var(--sp-5); }
.page-head__title { font-size: 26px; margin-top: var(--sp-2); }
.page-head__sub {
    margin-top: var(--sp-2);
    max-width: 64ch;
}
/* One row that scrolls sideways, not four that push the widget off-screen. */
.page-head__places {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin-top: var(--sp-3);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--line-soft);
}
.page-head__places h2 { margin: 0; flex: 0 0 auto; }
.page-head__places .tp-row-wrap {
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
    padding-bottom: 2px;
}
.page-head__places .tp-chip { flex: 0 0 auto; }

/* The widget IS the page: it gets everything the heading block doesn't use. */
.page-widget {
    flex: 1 1 auto;
    min-height: 300px;
    transition: opacity var(--t-fast);
}
.page-widget .tp-panel { height: 100%; }
.page-widget.htmx-request { opacity: 0.55; }

@media (max-width: 700px) {
    .page-head__title { font-size: 22px; }
    .page-head__places { display: block; }
    .page-head__places .tp-row-wrap { margin-top: var(--sp-2); }
}

/* /places/ — one card per popular place. */
.places-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--sp-4);
}

.widget-edit-bar {
    position: absolute;
    top: 6px; right: 6px;
    z-index: 5;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-fast);
}
.grid-stack-item:hover .widget-edit-bar { opacity: 1; pointer-events: auto; }
.widget-edit-btn {
    width: 24px; height: 24px;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    color: var(--muted);
    font-size: 10px;
    cursor: pointer;
    transition: background var(--t-fast), color var(--t-fast);
}
.widget-edit-btn:hover { background: var(--alert); border-color: var(--alert); color: #fff; }

/* Widget size and position are derived from how many widgets are on the
   board, so a resize grip would be advertising something that isn't possible.
   Gridstack still emits the handle elements even with disableResize, so
   they're hidden here. */
.grid-stack-item > .ui-resizable-handle { display: none !important; }

.grid-stack-item-content { overflow: visible !important; }

/* --- widget library drawer ----------------------------------------------- */

#widget-library {
    position: fixed;
    top: 0; right: 0; bottom: 0;
    width: 300px;
    max-width: 88vw;
    background: var(--ink-2);
    border-left: 1px solid var(--line);
    box-shadow: var(--shadow-3);
    z-index: var(--z-overlay);
    transform: translateX(100%);
    transition: transform var(--t-slow);
    display: flex;
    flex-direction: column;
}
#widget-library.is-open { transform: none; }

.wl-header {
    display: flex; align-items: center;
    padding: var(--sp-4);
    border-bottom: 1px solid var(--line);
    flex-shrink: 0;
}
.wl-body { flex: 1 1 auto; overflow-y: auto; padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); }

.wl-card {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-3);
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    cursor: pointer;
    transition: border-color var(--t-fast), background var(--t-fast);
}
.wl-card:hover { border-color: var(--signal-a60); background: var(--ink-4); }
.wl-card.is-used { opacity: 0.5; cursor: default; }
.wl-card.is-used:hover { border-color: var(--line); background: var(--ink-3); }
.wl-icon {
    width: 34px; height: 34px;
    display: flex; align-items: center; justify-content: center;
    border-radius: var(--r-sm);
    background: var(--signal-a08);
    border: 1px solid var(--signal-a32);
    color: var(--signal);
    flex-shrink: 0;
}
.wl-meta { flex: 1 1 auto; min-width: 0; }
.wl-name { font-size: 13px; font-weight: 600; color: var(--text); }
.wl-desc { font-family: var(--font-mono); font-size: 10px; color: var(--muted); margin-top: 2px; }
.wl-add { color: var(--muted); font-size: 11px; }

/* --- map overlay --------------------------------------------------------- */

#map-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    background: var(--ink);
    display: none;
    flex-direction: column;
}
#map-overlay.is-open { display: flex; }

/* A real bar rather than a floating button, so the editor's own panel starts
   below it and the two can never overlap. It is also lifted above the loader,
   which covers the whole overlay — otherwise Back is unclickable until the
   iframe finishes loading, which is exactly when you want to bail out. */
.map-overlay__bar {
    position: relative;
    z-index: 1;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    background: var(--ink);
    border-bottom: 1px solid var(--line);
    padding-top: calc(var(--sp-2) + env(safe-area-inset-top));
}

#map-overlay iframe { flex: 1 1 auto; width: 100%; border: 0; display: block; min-height: 0; }

#map-overlay-loader {
    position: absolute;
    inset: 0;
    display: flex; align-items: center; justify-content: center;
    background: var(--ink);
}
#map-overlay-loader {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    background: var(--ink);
}
#map-overlay-loader .spinner {
    width: 34px; height: 34px;
    border: 2px solid var(--ink-4);
    border-top-color: var(--signal);
    border-radius: 50%;
    animation: tp-spin 0.8s linear infinite;
}

/* --- map widget ---------------------------------------------------------- */

.mw-map-wrap { position: relative; flex: 1 1 auto; min-height: 0; }
.mw-map { position: absolute; inset: 0; background: var(--ink); }

.mw-select {
    flex: 1 1 auto;
    min-width: 0;
    max-width: 220px;
    height: 28px;
    padding: 0 var(--sp-2);
    background: var(--ink-3);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.mw-select:focus { outline: none; border-color: var(--signal); }

.mw-tools {
    position: absolute;
    top: var(--sp-2); left: var(--sp-2);
    z-index: 500;
    display: flex;
    gap: 2px;
    padding: 3px;
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
}
.mw-tool {
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
    background: none;
    border: 0;
    border-radius: var(--r-sm);
    color: var(--muted);
    font-size: 12px;
    cursor: pointer;
    transition: background var(--t-fast), color var(--t-fast);
}
.mw-tool:hover { background: var(--ink-3); color: var(--text); }
.mw-tool.is-active { background: var(--signal-a16); color: var(--signal-hot); }
.mw-tools__sep { width: 1px; align-self: stretch; background: var(--line); margin: 2px 2px; }

.mw-compose {
    position: absolute;
    left: var(--sp-2); right: var(--sp-2); bottom: var(--sp-2);
    z-index: 500;
    display: flex;
    gap: var(--sp-2);
    padding: var(--sp-2);
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-2);
}
.mw-compose .tp-field { min-height: 30px; font-size: 13px; }

.mw-pin {
    position: absolute;
    top: 50%; left: 50%;
    width: 16px; height: 16px;
    margin: -8px 0 0 -8px;
    border-radius: 50%;
    border: 2px solid var(--ink);
    box-shadow: 0 0 0 1px rgba(255, 106, 31, 0.55);
}

/* --- map cards ----------------------------------------------------------- */

.maps-grid {
    display: grid;
    gap: var(--sp-3);
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

.map-card {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-4);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-left: 2px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-2), var(--hairline-top);
    transition: border-color var(--t-fast), transform var(--t-fast);
}
.map-card:hover { border-left-color: var(--signal); transform: translateX(2px); }
.map-card__foot {
    display: flex; align-items: center; gap: var(--sp-2);
    margin-top: auto;
    padding-top: var(--sp-2);
    border-top: 1px solid var(--line-soft);
}

/* --- profile view (swaps the grid) --------------------------------------- */

.dash-profile-view { display: none; }
.dash-profile-view.is-open { display: block; }
.dash-root.is-profile .dash-grid-wrap { display: none; }
.dash-profile-stack {
    display: grid;
    gap: var(--sp-3);
    grid-template-columns: minmax(0, 1fr);
    max-width: 640px;
}

/* ==========================================================================
   Document pages (about, terms, privacy, contact)
   ========================================================================== */

.doc-wrap { padding: var(--sp-6) 0 var(--sp-8); }
.doc-card { max-width: 760px; margin-inline: auto; padding: var(--sp-6); }

.doc-card h1 { margin: 0 0 var(--sp-2); }
.doc-card h2 {
    font-family: var(--font-display);
    font-size: 17px; font-weight: 700;
    color: var(--text);
    margin: var(--sp-6) 0 var(--sp-3);
    padding-left: var(--sp-3);
    border-left: 2px solid var(--signal);
    line-height: 1.3;
}
.doc-card h3 {
    font-family: var(--font-display);
    font-size: 14px; font-weight: 600;
    color: var(--text);
    margin: var(--sp-4) 0 var(--sp-2);
}
.doc-card p, .doc-card li {
    font-size: 14px;
    line-height: 1.65;
    color: var(--text-dim);
}
.doc-card p { margin: 0 0 var(--sp-3); }
.doc-card ul { margin: 0 0 var(--sp-3); padding-left: var(--sp-5); }
.doc-card li { margin-bottom: var(--sp-2); }
.doc-card a { color: var(--signal); }
.doc-card a:hover { color: var(--signal-hot); }
.doc-card strong { color: var(--text); font-weight: 600; }

.doc-features {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--sp-3);
    margin-bottom: var(--sp-3);
}
.doc-feature {
    padding: var(--sp-3);
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
}
.doc-feature h4 {
    font-family: var(--font-mono);
    font-size: 10px; font-weight: 500;
    letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--signal);
    margin: 0 0 6px;
}
.doc-feature p { font-size: 13px; margin: 0; color: var(--muted); }

/* ==========================================================================
   Editor
   ========================================================================== */

.editor-toggle {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    padding: var(--sp-3);
    margin-top: var(--sp-4);
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    cursor: pointer;
}
.editor-toggle input { margin-top: 2px; accent-color: var(--signal); width: 16px; height: 16px; }
.editor-toggle__title { display: block; font-size: 13px; font-weight: 600; color: var(--text); }
.editor-toggle__sub {
    display: block;
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--muted);
    margin-top: 3px;
}

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

.landing {
    min-height: 100%;
    display: flex;
    align-items: center;
    padding: var(--sp-7) 0;
}
.landing__inner { max-width: 780px; }
.landing__title { margin: var(--sp-5) 0 0; font-size: clamp(32px, 6vw, 56px); }
.landing__lede {
    margin-top: var(--sp-4);
    max-width: 52ch;
    font-size: 17px;
    line-height: 1.55;
    color: var(--text-dim);
}
.landing__features {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--sp-3);
    margin-top: var(--sp-7);
}

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

/* --- profile form rhythm -------------------------------------------------
   A label sitting almost on top of its input reads as one blurry block. These
   give the label room to breathe and separate one field from the next by more
   than the label-to-input gap, so the pairing is obvious. */

.pf-form { margin-top: var(--sp-4); }

.pf-field { margin-bottom: var(--sp-5); }
.pf-field:last-of-type { margin-bottom: 0; }
.pf-field .tp-label {
    display: block;
    margin-bottom: var(--sp-2);
    color: var(--text-dim);
}
.pf-field .tp-field { margin-top: 0; }

.pf-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--sp-3) var(--sp-4);
    margin-bottom: var(--sp-5);
}
.pf-grid .pf-field { margin-bottom: 0; }

.pf-error {
    margin-top: 6px;
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1.5;
    color: var(--alert);
}

.profile-hero { padding: var(--sp-5); overflow: visible; }
.profile-hero::after { display: none; }  /* no corner bracket on the hero */

.profile-hero__top {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-4);
    flex-wrap: wrap;
}

.profile-hero__avatar {
    width: 96px; height: 96px;
    flex-shrink: 0;
    border-radius: var(--r-lg);
    overflow: hidden;
    background: var(--ink-3);
    border: 1px solid var(--line);
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-2);
}
.profile-hero__avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.profile-hero__initial {
    font-family: var(--font-display);
    font-size: 40px;
    font-weight: 700;
    color: var(--signal);
    line-height: 1;
}

.profile-hero__id { flex: 1 1 240px; min-width: 0; }
.profile-hero__name {
    font-family: var(--font-display);
    font-size: 28px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--text);
    margin: 0 0 4px;
    line-height: 1.15;
}
.profile-hero__bio {
    margin: var(--sp-3) 0 0;
    font-size: 14px;
    line-height: 1.55;
    color: var(--text-dim);
    max-width: 56ch;
}
.profile-hero__actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

.profile-stats {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1px;
    margin-top: var(--sp-5);
    background: var(--line-soft);
    border: 1px solid var(--line-soft);
    border-radius: var(--r-md);
    overflow: hidden;
}
.profile-stat {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: var(--sp-3);
    background: var(--ink-2);
}

/* --- people nearby ------------------------------------------------------- */

.nearby { display: inline-flex; align-items: center; gap: var(--sp-2); }
.nearby__faces { display: inline-flex; }
.nearby__face {
    width: 20px; height: 20px;
    margin-left: -6px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%;
    overflow: hidden;
    background: var(--signal-a16);
    border: 1.5px solid var(--ink-2);
    color: var(--signal-hot);
    font-family: var(--font-display);
    font-size: 9px;
    font-weight: 700;
}
.nearby__face:first-child { margin-left: 0; }
.nearby__face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.nearby__count {
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

.hdr-nearby {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 26px;
    padding: 0 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.06em;
    color: var(--text-dim);
}
.hdr-nearby__label { color: var(--muted); text-transform: uppercase; }

/* --- live collaboration presence ----------------------------------------- */

/* Positioned by the editor's .ed-corner--tr, not by itself. */
.collab-bar {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 6px var(--sp-3);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    box-shadow: var(--shadow-2);
}
.collab-people { display: inline-flex; gap: -4px; }
.collab-dot {
    width: 24px; height: 24px;
    margin-left: -6px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: var(--signal-a16);
    border: 2px solid var(--ink-2);
    color: var(--signal-hot);
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 700;
}
.collab-dot:first-child { margin-left: 0; }
.collab-note {
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--muted);
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --- maps widget rows ---------------------------------------------------- */

.cw-row { display: flex; align-items: flex-start; gap: var(--sp-2); }
.cw-row__main { flex: 1 1 auto; min-width: 0; }
.cw-copy { flex: 0 0 auto; align-self: center; }

/* --- share dialog -------------------------------------------------------- */

.share-people {
    margin-top: var(--sp-4);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    overflow: hidden;
}
.share-person {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    background: var(--ink-3);
    border-bottom: 1px solid var(--line-soft);
}
.share-person:last-child { border-bottom: 0; }
.share-person__name {
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    color: var(--text);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.share-person .tp-avatar i { font-size: 10px; color: var(--signal); }

/* --- map cards on the profile ------------------------------------------- */

.pmaps {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--sp-3);
}

.pmap {
    display: flex;
    flex-direction: column;
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    overflow: hidden;
    box-shadow: var(--shadow-2), var(--hairline-top);
    transition: border-color var(--t-fast), transform var(--t-fast);
}
.pmap:hover { border-color: var(--signal-a60); transform: translateY(-2px); }

/* A single basemap tile of the map's own centre — see the map_thumb filter. */
.pmap__thumb {
    position: relative;
    display: block;
    height: 104px;
    background: var(--ink-3);
    border-bottom: 1px solid var(--line);
    overflow: hidden;
}
.pmap__thumb img {
    width: 100%; height: 100%;
    object-fit: cover;
    display: block;
    /* Match the app's basemap grade so thumbnails belong to the same world. */
    filter: brightness(0.75) contrast(1.1) saturate(0.35);
}
/* Marks the centre the thumbnail is of. */
.pmap__pin {
    position: absolute;
    top: 50%; left: 50%;
    width: 10px; height: 10px;
    margin: -5px 0 0 -5px;
    border-radius: 50%;
    background: var(--signal);
    border: 2px solid var(--ink);
    box-shadow: 0 0 0 1px var(--signal-a60);
}

.pmap__body {
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: var(--sp-3);
    flex: 1 1 auto;
}
.pmap__name { font-size: 14px; font-weight: 700; color: var(--text); }
.pmap__desc {
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--muted);
    flex: 1 1 auto;
}
.pmap__foot {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-top: var(--sp-2);
    padding-top: var(--sp-2);
    border-top: 1px solid var(--line-soft);
}

/* ==========================================================================
   Auth pages
   ========================================================================== */

.auth-wrap {
    min-height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-5) var(--gutter);
}

.auth-card {
    width: 100%;
    max-width: 400px;
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3), var(--hairline-top);
    padding: var(--sp-6);
}

.auth-brand {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    margin-bottom: var(--sp-5);
}
.auth-brand__mark {
    width: 10px; height: 10px;
    background: var(--signal);
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
.auth-brand__text {
    font-size: 14px; font-weight: 700;
    letter-spacing: 0.16em;
    color: var(--text);
}

.auth-divider {
    display: flex; align-items: center; gap: var(--sp-3);
    margin: var(--sp-5) 0;
}
.auth-divider::before, .auth-divider::after {
    content: ''; flex: 1 1 auto; height: 1px; background: var(--line);
}

/* Google's brand guidelines require their own button treatment, so this is
   the one control that deliberately sits outside the design system. */
.auth-google {
    display: flex; align-items: center; justify-content: center; gap: var(--sp-2);
    width: 100%;
    min-height: 42px;
    background: #fff;
    color: #1f1f1f;
    border-radius: var(--r-md);
    font-family: var(--font-display);
    font-size: 14px; font-weight: 600;
    transition: opacity var(--t-fast);
}
.auth-google:hover { opacity: 0.9; }
.auth-google img { width: 18px; height: 18px; }

.auth-error {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-3);
    background: rgba(229, 72, 77, 0.1);
    border: 1px solid rgba(229, 72, 77, 0.35);
    border-left: 2px solid var(--alert);
    border-radius: var(--r-md);
    color: #F5B5B7;
    font-size: 13px;
    margin-bottom: var(--sp-4);
}
.auth-error i { color: var(--alert); }

.auth-field + .auth-field { margin-top: var(--sp-4); }
.auth-field .tp-label { display: block; margin-bottom: var(--sp-2); }

/* Contact form: the name/email pair lives inside a grid wrapper, so the
   adjacent-sibling rule above skips the field that follows it. Space every
   row of the form evenly instead (the send button's larger inline margin
   still wins over this). The hidden csrf input is the form's first child,
   so the row after it is really the first visible one — keep it flush. */
.contact-form > * + * { margin-top: var(--sp-4); }
.contact-form > input[type="hidden"] + * { margin-top: 0; }
.contact-form .auth-field .tp-label { margin-bottom: var(--sp-3); }
.auth-field__hint { font-family: var(--font-mono); font-size: 10px; color: var(--muted-deep); margin-top: 6px; line-height: 1.5; }

/* ==========================================================================
   Loader
   ========================================================================== */

#app-loader {
    position: fixed;
    inset: 0;
    z-index: 9999;
    background: var(--ink);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: opacity var(--t-slow);
}
#app-loader .spinner {
    width: 34px; height: 34px;
    border: 2px solid var(--ink-4);
    border-top-color: var(--signal);
    border-radius: 50%;
    animation: tp-spin 0.8s linear infinite;
}
@keyframes tp-spin { to { transform: rotate(360deg); } }

/* ==========================================================================
   Utilities
   ========================================================================== */

.tp-hide { display: none !important; }

.tp-sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.tp-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

/* ==========================================================================
   Mobile
   --------------------------------------------------------------------------
   One breakpoint, 768px — the same number gridstack switches to a single
   column at, so the layout and the grid never disagree about what "mobile"
   means.
   ========================================================================== */

#mobile-nav { display: none; }

@media (max-width: 768px) {

    /* --- app shell --------------------------------------------------- */

    /* dvh, not vh: iOS Safari's collapsing toolbar makes vh taller than the
       visible viewport, which would hide the bottom nav behind browser chrome. */
    html, body { height: 100dvh; }

    /* Tabs move to the bottom bar; the header keeps identity and position.
       Its height and two-row layout live with the radius control further
       down — one place, rather than two rules quietly overriding each other. */
    #app-header .hdr-nav { display: none; }
    .hdr-brand__text { display: none; }
    .hdr-user__name { display: none; }
    /* Four 24px gaps plus 48px of gutter overflow a 360px phone. */
    .hdr-row { gap: var(--sp-2); }

    /* The footer's link row is redundant next to the bottom nav. */
    #app-footer { display: none; }

    #mobile-nav {
        display: flex;
        flex-shrink: 0;
        align-items: stretch;
        background: var(--ink);
        border-top: 1px solid var(--line);
        z-index: var(--z-header);
        /* Sit above the home indicator on notched devices. */
        padding-bottom: env(safe-area-inset-bottom);
    }
    .mnav__item {
        flex: 1 1 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 3px;
        /* 56px clears the 44px minimum target with room for the label. */
        min-height: 56px;
        padding: 6px 4px;
        background: none;
        border: 0;
        color: var(--muted);
        cursor: pointer;
        transition: color var(--t-fast);
    }
    .mnav__item i { font-size: 16px; }
    .mnav__item span {
        font-family: var(--font-mono);
        font-size: 9px;
        letter-spacing: 0.08em;
        text-transform: uppercase;
    }
    .mnav__item.is-active { color: var(--signal); }
    .mnav__item:active { background: var(--ink-2); }

    /* --- controls ------------------------------------------------------ */

    .dash-root { padding-top: var(--sp-3); }

    /* The toolbar becomes two rows: actions, then a full-width search. */
    .dash-toolbar { gap: var(--sp-2); }
    .dash-toolbar .dash-search { flex: 1 1 100%; min-width: 0; }
    .dash-toolbar .dash-radius {
        flex: 1 1 100%;
        border-left: 0;
        border-top: 1px solid var(--line);
        padding: var(--sp-2) 0 0;
        justify-content: space-between;
    }
    .dash-radius .tp-range { flex: 1 1 auto; width: auto; }

    /* NB: do NOT reset .dash-grid-wrap's negative margin here. It cancels
       gridstack's per-item margin, and without it every widget sits 6px inside
       the gutter while the toolbar above it sits on it. */

    /* --- touch targets -------------------------------------------------- */
    /* Sized at the breakpoint rather than only under `pointer: coarse`: that
       query doesn't fire in every browser or emulator, and a control that is
       28px on a phone is a miss waiting to happen. */

    .tp-btn { min-height: 40px; }
    .tp-btn--sm { min-height: 34px; }
    .tp-btn--icon { width: 40px; min-width: 40px; }
    .tp-field { min-height: 40px; font-size: 16px; }  /* 16px stops iOS zooming on focus */

    /* The brand collapses to a 10px mark once its text is hidden — still a
       link to the dashboard, so it needs a body to hit. */
    .hdr-brand {
        min-width: 40px;
        min-height: 40px;
        justify-content: center;
    }

    /* Map widget controls */
    .mw-tool { width: 38px; height: 38px; }
    .mw-select { height: 36px; font-size: 14px; }
    .mw-compose .tp-field { min-height: 38px; }

    /* The slider's visible track is 4px — fine to look at, impossible to grab.
       Pad the input itself so the hit area is finger-sized while the track
       stays thin. */
    .tp-range {
        height: 36px;
        background: transparent;
        padding: 0;
    }
    .tp-range::-webkit-slider-runnable-track {
        height: 4px;
        background: var(--ink-4);
        border-radius: var(--r-pill);
    }
    .tp-range::-moz-range-track {
        height: 4px;
        background: var(--ink-4);
        border-radius: var(--r-pill);
    }
    .tp-range::-webkit-slider-thumb {
        width: 22px; height: 22px;
        margin-top: -9px;   /* centre the thumb on the 4px track */
    }
    .tp-range::-moz-range-thumb { width: 22px; height: 22px; }

    /* Remove-widget lives behind :hover, which a touch screen doesn't have.
       Show it, and make it big enough to press. */
    .widget-edit-bar { opacity: 1; pointer-events: auto; }
    .widget-edit-btn { width: 32px; height: 32px; font-size: 12px; }

    /* --- dialogs --------------------------------------------------------- */

    .tp-modal { padding: var(--sp-3); align-items: flex-end; }
    .tp-modal__box {
        max-width: none;
        max-height: calc(100dvh - var(--sp-6));
        margin-bottom: env(safe-area-inset-bottom);
    }
    .tp-modal__actions { flex-direction: column-reverse; }
    .tp-modal__actions .tp-btn { width: 100%; }

    /* min-height too, not just height: the markup carries an inline
       min-height:240px as a guard against a stale stylesheet, and that would
       otherwise win here. */
    .loc-picker__map { height: 200px; min-height: 200px !important; }
    .loc-places { max-height: 76px; }

    /* --- tour ------------------------------------------------------------ */

    /* Anchoring a card next to a spotlit element doesn't work on a narrow
       screen — pin it to the bottom instead. The transform reset matters: the
       centred step sets translate(-50%,-50%) inline, which would drag the card
       half its width off the left edge once left/top are overridden here. */
    .tut__card {
        left: var(--sp-3) !important;
        right: var(--sp-3);
        top: auto !important;
        bottom: calc(var(--sp-3) + env(safe-area-inset-bottom));
        width: auto;
        transform: none !important;
        max-height: 60dvh;
    }

    /* --- content --------------------------------------------------------- */

    .maps-grid { grid-template-columns: 1fr; }
    .doc-card { padding: var(--sp-4); }
    .profile-head { gap: var(--sp-3); }
    .tp-display { font-size: clamp(24px, 7vw, 32px); }
}

/* Anything with a coarse pointer gets full-size targets, even on a big
   screen — a touch laptop is still touched. */
@media (pointer: coarse) {
    .tp-btn { min-height: 44px; }
    .tp-btn--icon { width: 44px; min-width: 44px; }
    .mw-tool { width: 38px; height: 38px; }
    .tp-range::-webkit-slider-thumb { width: 20px; height: 20px; }
}

/* ==========================================================================
   Accessibility
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* --- live geolocation ---------------------------------------------------- */
/* The follow toggle in the location modal. Built as a full-width row rather
   than a bare checkbox so the whole thing is a comfortable tap target. */
.loc-follow {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin-top: var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
    min-height: 44px;
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    cursor: pointer;
    transition: border-color .15s ease, background .15s ease;
}
.loc-follow:hover { border-color: var(--signal-a60); }
.loc-follow:has(input:checked) {
    border-color: var(--signal);
    background: var(--signal-a08);
}
.loc-follow input {
    width: 16px; height: 16px;
    accent-color: var(--signal);
    flex: none;
    cursor: pointer;
}
.loc-follow__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.loc-follow__title {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 14px;
    color: var(--text);
}
.loc-follow__sub { font-size: 11px; color: var(--muted); line-height: 1.4; }
.loc-follow .tp-live { margin-left: auto; flex: none; }

/* LIVE marker on the header's location chip. */
.hdr-loc__live {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 6px;
    border-radius: var(--r-sm);
    background: var(--signal-a16);
    color: var(--signal);
    font-family: var(--font-mono);
    font-size: 9px;
    font-weight: 500;
    letter-spacing: .08em;
    flex: none;
}
.hdr-loc:has(.hdr-loc__live:not(.tp-hide)) { border-color: var(--signal-a60); }

@media (max-width: 768px) {
    /* On a phone the dot alone carries the meaning — the word doesn't fit. */
    .hdr-loc__live { font-size: 0; gap: 0; padding: 2px 4px; }
}

/* ==========================================================================
   Marker pins and detail cards — shared by the map editor and the dashboard's
   connected-maps overlay, so a pin looks the same wherever it is drawn.
   ========================================================================== */

/* Classic diamond pin: a rotated square with a dark ring so it stays visible
   over both pale streets and dark parkland. */
.ed-marker {
    width: 22px; height: 22px;
    border: 2px solid var(--ink);
    outline: 1px solid rgba(255, 255, 255, 0.65);
    transform: rotate(45deg);
    border-radius: 3px;
}

/* Markers with a chosen icon: a coloured round badge on a short tail; the
   glyph stays dark ink for contrast. */
.ed-pin { position: relative; width: 30px; height: 38px; }
.ed-pin__head {
    width: 30px; height: 30px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%;
    border: 2px solid var(--ink);
    outline: 1px solid rgba(255, 255, 255, 0.55);
    color: #140A04;
    font-size: 13px;
}
.ed-pin__tail {
    position: absolute;
    left: 50%; top: 27px;
    transform: translateX(-50%);
    width: 0; height: 0;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 9px solid var(--ink);
}

/* The Leaflet popup dressed as a Touchpad card. */
.marker-card-popup .leaflet-popup-content-wrapper {
    background: var(--ink-2);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    padding: 0;
    overflow: hidden;
    box-shadow: var(--shadow-3);
}
.marker-card-popup .leaflet-popup-content { margin: 0; width: 260px !important; }
.marker-card-popup .leaflet-popup-tip { background: var(--ink-2); border: 1px solid var(--line); }

/* Marker cards sit above every other map layer — pins, tooltips, live
   cursors — so an open card is never threaded under its neighbours. */
.leaflet-popup-pane { z-index: 1200; }

/* A real close control instead of Leaflet's bare x: a round hit target that
   stays legible even over a photo header. */
.marker-card-popup a.leaflet-popup-close-button {
    top: var(--sp-2);
    right: var(--sp-2);
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: rgba(11, 11, 13, 0.72);
    border: 1px solid var(--line);
    border-radius: 50%;
    color: var(--text-dim);
    font: 400 15px/1 var(--font-display);
    transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.marker-card-popup a.leaflet-popup-close-button:hover {
    color: var(--signal);
    border-color: var(--signal);
    background: var(--ink-2);
}

/* The same card, seated by MapLibre in 3D mode. Different engine, different
   class names, identical result — the card must not change when the buildings
   stand up. */
.maplibregl-popup.marker-card-popup .maplibregl-popup-content {
    background: var(--ink-2);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    padding: 0;
    overflow: hidden;
    box-shadow: var(--shadow-3);
    width: 260px;
}
.maplibregl-popup.marker-card-popup .maplibregl-popup-tip { display: none; }
.maplibregl-popup.marker-card-popup .maplibregl-popup-close-button {
    top: var(--sp-2);
    right: var(--sp-2);
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: rgba(11, 11, 13, 0.72);
    border: 1px solid var(--line);
    border-radius: 50%;
    color: var(--text-dim);
    font: 400 15px/1 var(--font-display);
}
.maplibregl-popup.marker-card-popup .maplibregl-popup-close-button:hover {
    color: var(--signal);
    border-color: var(--signal);
    background: var(--ink-2);
}

.marker-card { font-family: var(--font-display); }
.marker-card-img {
    width: 100%; max-height: 160px;
    overflow: hidden;
    background: var(--ink);
    border-bottom: 1px solid var(--line);
}
.marker-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.marker-card-body { padding: var(--sp-3) var(--sp-4); }
.marker-card-head {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: 4px;
}
.marker-card-head i { font-size: 12px; }
.marker-card-title {
    font-weight: 700; font-size: 15px;
    color: var(--text);
}
.marker-card-desc {
    font-size: 13px;
    color: var(--text-dim);
    line-height: 1.45;
    white-space: pre-wrap;
    /* Imported pins can carry a whole guidebook entry. Let the description
       scroll rather than growing a card taller than the window — the notes
       thread underneath has to stay reachable. */
    max-height: 150px;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.marker-card-meta {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    border-top: 1px solid var(--line-soft);
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--muted);
}
.marker-card-meta .tp-spacer { flex: 1; }
.marker-card-meta a { color: var(--signal); }

/* Marker notes — the thread at the foot of a card. Shared by the editor and
   the dashboard; the markup comes from js/marker-comments.js. */
.marker-comments { border-top: 1px solid var(--line-soft); }
.marker-comments__list {
    max-height: 180px;
    overflow-y: auto;
    padding: var(--sp-2) var(--sp-4);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}
.marker-comments__empty {
    font-size: 12px;
    color: var(--muted);
    padding: 2px 0;
}
.mc-item { display: flex; gap: var(--sp-2); align-items: flex-start; }
.mc-avatar {
    flex: 0 0 auto;
    width: 22px; height: 22px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--ink-3);
    display: flex; align-items: center; justify-content: center;
    font-family: var(--font-display);
    font-weight: 700; font-size: 11px;
    color: var(--muted);
}
.mc-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mc-body { flex: 1 1 auto; min-width: 0; }
.mc-head {
    display: flex; align-items: baseline; gap: var(--sp-2);
    font-size: 11px;
}
.mc-who { font-weight: 700; color: var(--text-dim); }
.mc-when { font-family: var(--font-mono); font-size: 9px; color: var(--muted-deep); }
.mc-del {
    background: none; border: 0; padding: 0 2px;
    color: var(--muted-deep); cursor: pointer; font-size: 10px;
}
.mc-del:hover { color: var(--alert); }
.mc-text {
    font-size: 12px; color: var(--text-dim); line-height: 1.4;
    white-space: pre-wrap; overflow-wrap: break-word;
}

.marker-comments__form {
    display: flex;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4) var(--sp-3);
}
.marker-comments__form input {
    flex: 1 1 auto;
    min-width: 0;
    padding: 6px var(--sp-3);
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    color: var(--text);
    font-family: var(--font-display);
    font-size: 12px;
}
.marker-comments__form input:focus { outline: none; border-color: var(--signal); }
.marker-comments__form button {
    flex: 0 0 auto;
    width: 30px; height: 30px;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--signal);
    border: 0;
    border-radius: var(--r-md);
    color: #140A04;
    cursor: pointer;
    font-size: 11px;
}
.marker-comments__form button:hover { background: var(--signal-hot); }
.marker-comments__signin {
    display: block;
    padding: var(--sp-2) var(--sp-4) var(--sp-3);
    font-size: 11px;
    color: var(--muted);
}
.marker-comments__signin a { color: var(--signal); }

/* Ambient OpenStreetMap places — the city that is already there. Quieter than
   the app's own pins by construction: half the size, no glow, and a dark disc
   so a few hundred of them read as texture rather than as a wall of colour. */
.tp-place-icon { pointer-events: auto; }
.tp-place {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 18px; height: 18px;
    border-radius: 50%;
    background: rgba(11, 11, 13, 0.82);
    border: 1px solid var(--place, var(--muted));
    color: var(--place, var(--muted));
    font-size: 8px;
    transition: transform var(--t-fast), box-shadow var(--t-fast);
}
.tp-place-icon:hover .tp-place {
    transform: scale(1.35);
    box-shadow: 0 0 10px var(--place, var(--muted));
    z-index: 2;
}
/* Over daylight and imagery the disc inverts, or it disappears into them. */
body.tp-map--light .tp-place { background: rgba(255, 255, 255, 0.9); }
body.tp-map--satellite .tp-place { background: rgba(11, 11, 13, 0.9); }

/* Waiting on a fresh lookup. The previous places stay put — they are mostly
   the right ones — and only say so by going quiet. */
body.tp-places-stale .tp-place-icon {
    opacity: 0.45;
    transition: opacity var(--t-mid);
}

/* Per-map connect toggle on the maps page cards. */
.map-connect {
    position: absolute;
    top: var(--sp-3); right: var(--sp-3);
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 4px 10px;
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    color: var(--muted);
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.map-connect:hover { color: var(--text); border-color: var(--text-dim); }
.map-connect.is-on {
    background: var(--signal-a16);
    border-color: var(--signal);
    color: var(--signal-hot);
}
.map-card { position: relative; }

/* Map-first dashboard: the page content is one floating toolbar, so the
   scroller stands down and lets the wheel and the mouse through to the map.
   The dashboard script adds the class; bgMapReset removes it on navigation. */
#spa-content.is-mapfirst { pointer-events: none; overflow-y: hidden; }

/* Radius control inside the app header: flush with the bar, compact, and on
   every chrome page — one instrument instead of a per-page toolbar. */
#app-header .dash-radius {
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border: 0;
    padding: 0 var(--sp-2);
    gap: var(--sp-2);
    height: auto;
    min-width: 0;
}
#app-header .dash-radius .tp-range { width: 140px; }
#app-header .dash-radius__val { min-width: 52px; }

@media (max-width: 900px) {
    #app-header .dash-radius .tp-label { display: none; }
    #app-header .dash-radius .tp-range { width: 96px; }
}

/* ---- Mobile header ------------------------------------------------------
   Identity and the app's one real input were competing for 375px on a single
   line: the slider was squeezed to 72px, its readout clipped to "2" with the
   unit cut off, and the location shortened to "38.5…". None of it was usable.

   Two rows instead. The top one is who and where — brand, presence, position,
   account — and the radius gets a line of its own underneath, full width,
   with its label and readout back. The header is no longer a fixed height;
   body is a flex column, so main simply takes what is left. */
@media (max-width: 768px) {
    #app-header { height: auto; }

    .hdr-row {
        height: auto;
        flex-wrap: wrap;
        align-items: center;
        column-gap: var(--sp-2);
        row-gap: 0;
        padding: 5px var(--sp-4);
    }

    /* Sign-out goes. The bottom nav's You tab reaches the profile page, which
       has its own; keeping a second one here cost ~50px of a 375px row and
       forced the position — the thing you actually read — down to "40.71,
       -74…". Dropping it is what makes the rest of this fit comfortably. */
    .hdr-signout { display: none; }

    /* Explicit order, so the row reads brand | where you are | who is near |
       you. Source order puts the radius and a spacer in the middle of that,
       which only made sense when it was all one line. */
    .hdr-brand { order: 0; }
    .hdr-loc { order: 1; }
    .hdr-nearby { order: 2; }
    .hdr-user { order: 3; }
    .hdr-auth { order: 4; }
    #app-header .dash-radius { order: 10; }

    /* Signed out, two full-size text buttons sat where the avatar does when
       you are signed in — roughly three times the width — and left the position
       chip nothing. They are the reason to be here, so they stay visible and
       labelled; they just stop being full-size controls in a 375px row. */
    .hdr-auth {
        height: 30px;
        padding: 0 10px;
        font-size: 12px;
        white-space: nowrap;
        flex: 0 0 auto;
    }
    /* The spacer's job was to push the right-hand group over. The position
       chip does that now by growing, and two flexible items would just split
       the slack between them. */
    .hdr-row > .tp-spacer { display: none; }

    /* Position takes the room sign-out gave up: it is the one thing in the
       header with something to say.

       flex-basis 0, not auto. A wrapping flex container decides line breaks
       from each item's basis and only shrinks WITHIN a line afterwards — so at
       `auto` the chip's full text counted against the row and pushed itself
       onto a line of its own on a 320px phone (worst case: signed out, where
       Sign in and Join are there too). At 0 it contributes nothing to that
       decision and simply fills whatever the row has left, ellipsing when
       that is not much. */
    .hdr-loc {
        flex: 1 1 0;
        min-width: 0;
        max-width: none;
    }

    /* Second row: the radius, full width, with its label and readout back.
       No divider — the header already has a bottom border, and a second
       hairline 30px above it just reads as clutter. */
    #app-header .dash-radius {
        flex: 1 0 100%;
        padding: 0;
        margin-top: 2px;
        gap: var(--sp-3);
    }
    #app-header .dash-radius > i { display: inline; }
    #app-header .dash-radius .tp-label { display: inline; }
    #app-header .dash-radius .tp-range {
        width: auto;
        flex: 1 1 auto;
        min-width: 0;
        /* A comfortable thumb target on glass. */
        height: 28px;
    }
    #app-header .dash-radius__val { min-width: 54px; text-align: right; }
}

/* Very narrow: drop the two decorative glyphs rather than the readouts. */
@media (max-width: 380px) {
    #app-header .dash-radius > i { display: none; }
    .hdr-nearby__label { display: none; }
}

/* Side-seated marker cards (see TouchpadMap.enableSidePopups): the card sits
   beside the pin, so the bottom tip would point at nothing — drop it. */
.marker-card-popup .leaflet-popup-tip-container { display: none; }


/* The location dialog must fit a laptop viewport WHOLE: hunting for the
   "Use this point" button below the fold made picking a point feel broken.
   Every vertical gap gives a little, and the map above yields first. */
#location-modal .tp-modal__box { padding: var(--sp-4) var(--sp-5); }
#location-modal .tp-modal__body { font-size: 13px; }
#location-modal .auth-divider { margin: var(--sp-3) 0; }
#location-modal .loc-follow { margin-top: var(--sp-3); padding: var(--sp-2) var(--sp-3); }
#location-modal .loc-places { max-height: 64px; }
#location-modal .loc-picker__foot { margin-top: var(--sp-3); }
#location-modal .tp-fineprint { margin-top: var(--sp-3); }

/* ==========================================================================
   Map editor
   --------------------------------------------------------------------------
   The editor is the same app as the dashboard, drawn full-bleed: a live map
   with glass panels floating over it. It used to carry its own <style> block,
   which is exactly how it drifted away from the rest of the app — so it lives
   here now, on the same tokens as everything else.

   Selector names are unchanged: the editor's JS keys off #drawing-controls,
   .tool, .tool-active, #district-summary, .create-shape-flow and friends.
   ========================================================================== */

body.is-editor { overflow: hidden; }


#map-area { position: absolute; inset: 0; z-index: 1; }
#map { height: 100%; width: 100%; background: var(--ink); }

/* --- Summary panel (top-left) ------------------------------------- */
#district-summary {
    width: 320px;
    max-width: 100%;
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3), var(--hairline-top);
    overflow: hidden;
    pointer-events: auto;
}

.summary-header {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--line-soft);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    color: var(--text);
    white-space: nowrap;
}
/* Real targets: these were 24x16 boxes, unusable on a phone. */
.summary-header button,
.summary-header a {
    flex: 0 0 auto;
    width: 30px; height: 30px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: none;
    border: 0;
    border-radius: var(--r-sm);
    color: var(--muted);
    cursor: pointer;
    font-size: 12px;
    transition: color var(--t-fast), background var(--t-fast);
}
.summary-header button:hover,
.summary-header a:hover { color: var(--signal); background: var(--ink-3); }

@media (pointer: coarse) {
    .summary-header button,
    .summary-header a { width: 40px; height: 40px; }
}

.district-progress-bar { padding: var(--sp-4); border-bottom: 1px solid var(--line-soft); }
.progress-title {
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: var(--sp-3);
}
.progress-nodes {
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    padding: 0 var(--sp-4);
}
.progress-nodes::before {
    content: '';
    position: absolute; top: 50%; left: var(--sp-4); right: var(--sp-4);
    height: 1px; background: var(--line); z-index: 1;
}
.node {
    width: 24px; height: 24px;
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: 50%;
    z-index: 2;
    display: flex; align-items: center; justify-content: center;
    font-size: 10px;
    color: var(--muted);
}
.node.active {
    background: var(--signal);
    border-color: var(--signal);
    color: #140A04;
}

/* --- what's on the map -------------------------------------------- */
/* Four counts across one row: the numbers are the content, the icons
   carry the meaning, and nothing needs a full labelled line of its own. */
.ed-stats {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1px;
    background: var(--line-soft);
    border-bottom: 1px solid var(--line-soft);
}
.ed-stat {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: var(--sp-3) var(--sp-2);
    background: var(--ink-2);
}
.ed-stat i { font-size: 11px; color: var(--muted-deep); }
.ed-stat__n {
    font-family: var(--font-mono);
    font-size: 17px;
    font-weight: 500;
    color: var(--text);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.ed-stat__l {
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
}

.ed-desc {
    margin: 0;
    padding: var(--sp-3) var(--sp-4);
    font-size: 13px;
    line-height: 1.5;
    color: var(--text-dim);
    border-bottom: 1px solid var(--line-soft);
}

.ed-meta {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--muted);
    border-bottom: 1px solid var(--line-soft);
}
.ed-meta__owner { color: var(--muted); transition: color var(--t-fast); }
.ed-meta__owner:hover { color: var(--signal); }
.ed-meta__dot { width: 2px; height: 2px; border-radius: 50%; background: var(--muted-deep); }

.ed-units {
    display: flex;
    align-items: center;
    padding: var(--sp-2) var(--sp-4) var(--sp-3);
}
.ed-seg {
    display: inline-flex;
    padding: 2px;
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}
.ed-seg .unit-btn {
    min-width: 34px;
    padding: 4px 8px;
    background: none;
    border: 0;
    border-radius: 3px;
    color: var(--muted);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background var(--t-fast), color var(--t-fast);
}
.ed-seg .unit-btn:hover { color: var(--text); }
.ed-seg .unit-btn.is-active { background: var(--signal); color: #140A04; }

/* The details drawer, collapsed by default.
   Not animated via grid-template-rows: that recipe needs the row's
   automatic min-content floor, which the child's `min-height: 0` removes,
   so `1fr` resolved to 0 on a cold load and the drawer never opened. A
   drawer that reliably opens beats one that animates. */
.ed-details.is-collapsed { display: none; }

#hud-toggle i { transition: transform var(--t-mid); display: inline-block; }
#hud-toggle[aria-expanded="true"] i { transform: rotate(180deg); }

/* --- Read-only banner ---------------------------------------------- */
.editor-readonly {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: 10px var(--sp-4);
    background: var(--signal-a08);
    border-bottom: 1px solid var(--signal-a32);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--amber);
}

/* --- Toggle -------------------------------------------------------- */
/* ---- the overlay that owns every floating control ------------------
   One gutter, one z-index, three anchors. Panels no longer carry their
   own top/left/bottom values, which is what made them drift apart. */
.ed-ui {
    position: absolute;
    inset: 0;
    /* The overlay must never exceed the map it floats over. */
    max-width: 100%;
    overflow: hidden;
    z-index: 1000;
    /* The editor is normally chromeless, but it must not *assume* that:
       if a header or footer is ever rendered above/below the map, these
       offsets keep the panel out from under them. They are measured at
       runtime (see the script below) and default to 0. */
    padding: calc(var(--sp-4) + var(--ed-top, 0px))
             var(--sp-4)
             calc(var(--sp-4) + var(--ed-bottom, 0px) + env(safe-area-inset-bottom))
             var(--sp-4);
    display: grid;
    grid-template-columns: auto 1fr auto;
    grid-template-rows: auto 1fr auto;
    gap: var(--sp-3);
    pointer-events: none;   /* the map stays draggable between panels */
}
/* min-width: 0 on every grid child is load-bearing: the tool dock's
   intrinsic width would otherwise push the whole overlay wider than the
   viewport, dragging the panels off-screen with it. */
.ed-corner { display: flex; flex-direction: column; gap: var(--sp-3);
             min-height: 0; min-width: 0; max-width: 100%; }
.ed-corner > * { pointer-events: auto; }
.ed-corner--tl { grid-column: 1; grid-row: 1; align-items: flex-start; max-height: 100%; overflow-y: auto; }
.ed-corner--tr { grid-column: 3; grid-row: 1; align-items: flex-end; }
.ed-dock {
    position: relative;   /* anchors the transient toast above the dock */
    grid-column: 1 / -1;
    grid-row: 3;
    display: flex;
    justify-content: center;
    align-items: flex-end;
    pointer-events: none;
    min-width: 0;
}
.ed-dock > * { pointer-events: auto; }

/* --- Bottom tool dock ---------------------------------------------- */
#drawing-controls {
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    height: 60px;
    padding: 0 var(--sp-3);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3), var(--hairline-top);
    max-width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    white-space: nowrap;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
#drawing-controls::-webkit-scrollbar { display: none; }

.tool {
    flex: 0 0 auto;
    width: 42px; height: 42px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: var(--r-md);
    color: var(--muted);
    cursor: pointer;
    transition: background var(--t-fast), color var(--t-fast);
}
.tool i { font-size: 16px; }
.tool:hover { background: var(--ink-3); color: var(--text); }
.tool.tool-active {
    background: var(--signal-a16);
    color: var(--signal-hot);
    box-shadow: inset 0 0 0 1px var(--signal-a60);
}
#locate-tool { color: var(--amber); }
#locate-tool:hover { color: var(--amber); background: rgba(255, 169, 43, 0.12); }

.vertical-divider {
    flex: 0 0 auto;
    width: 1px; height: 24px;
    background: var(--line);
    margin: 0 var(--sp-2);
}

/* --- Shape creation flyout ----------------------------------------- */
.create-shape-flow {
    position: fixed;
    /* Same left gutter as the summary panel above it, so the two read as
       one column rather than two floating cards. */
    top: calc(var(--sp-4) + 56px);
    left: var(--sp-4);
    width: 320px;
    max-width: calc(100vw - var(--sp-6));
    padding: var(--sp-5);
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-left: 2px solid var(--signal);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3);
    color: var(--text);
    z-index: 2100;
}
.create-shape-flow h3,
.create-shape-flow h4 {
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 700;
    color: var(--text);
    margin: 0 0 var(--sp-3);
}

/* Scoped so these don't fight .tp-field elsewhere on the page. */
.create-shape-flow input,
.create-shape-flow textarea {
    width: 100%;
    box-sizing: border-box;
    margin-bottom: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    color: var(--text);
    font-family: var(--font-display);
    font-size: 14px;
}
.create-shape-flow input:focus,
.create-shape-flow textarea:focus {
    outline: none;
    border-color: var(--signal);
    box-shadow: 0 0 0 3px var(--signal-a08);
}
/* A file input is the one control browsers still draw themselves, and the
   stock button is a light-grey slab that undoes the whole panel around it.
   The field keeps its well; the button becomes an ordinary .tp-btn. */
.create-shape-flow input[type="file"] {
    padding: var(--sp-2);
    font-family: var(--font-display);
    font-size: 12px;
    color: var(--muted);
}
.create-shape-flow input[type="file"]::file-selector-button {
    margin-right: var(--sp-3);
    padding: 6px var(--sp-3);
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    color: var(--text-dim);
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 12px;
    cursor: pointer;
    transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.create-shape-flow input[type="file"]::file-selector-button:hover {
    background: var(--ink-3);
    border-color: var(--text-dim);
    color: var(--text);
}

#inner-color {
    height: 26px; width: 26px;
    border-radius: var(--r-sm);
    border: 1px solid var(--line);
    cursor: pointer;
}
.color-option {
    height: 34px;
    border-radius: var(--r-sm);
    border: 2px solid transparent;
    cursor: pointer;
    transition: border-color var(--t-fast);
}
.color-option:hover { border-color: var(--text); }

.save-button {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
    min-height: 38px;
    padding: 0 var(--sp-4);
    background: var(--signal);
    color: #140A04;
    border: 1px solid var(--signal);
    border-radius: var(--r-md);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    cursor: pointer;
    transition: background var(--t-fast);
}
.save-button:hover { background: var(--signal-hot); }

.cancel-button {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 38px;
    margin-right: var(--sp-2);
    padding: 0 var(--sp-3);
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 13px;
    cursor: pointer;
}
.cancel-button:hover { background: var(--ink-3); color: var(--text); }

.hidden { display: none !important; }

#menu-backdrop {
    position: fixed; inset: 0;
    background: rgba(5, 5, 6, 0.5);
    backdrop-filter: blur(2px);
    z-index: 2050;
}

/* Placed markers, pins and the popup card chrome live in touchpad.css —
   the dashboard's connected-maps overlay draws the same pins. */

/* --- Measurements ---------------------------------------------------- */
.measure-info {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--muted);
    margin-top: var(--sp-2);
    line-height: 1.6;
}
.measure-info i { width: 16px; color: var(--signal); }
.measure-info b { color: var(--text); font-weight: 500; }

.leaflet-tooltip.measure-tooltip {
    background: var(--ink-2);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 11px;
    box-shadow: var(--shadow-2);
}
.leaflet-tooltip.measure-tooltip:before { display: none; }

/* --- View-only dock note -------------------------------------------- */
.dock-note {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 0 var(--sp-3);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--amber);
    white-space: nowrap;
}

/* Transient dock message ("GPS unavailable — …"). */
.ed-toast {
    position: absolute;
    bottom: calc(100% + var(--sp-2));
    left: 50%;
    transform: translateX(-50%) translateY(4px);
    padding: var(--sp-2) var(--sp-4);
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-left: 2px solid var(--amber);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-2);
    color: var(--text);
    font-size: 13px;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-mid), transform var(--t-mid);
}
.ed-toast.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }

/* --- Marker icon picker --------------------------------------------- */
.icon-grid {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: var(--sp-1);
}
.icon-option {
    height: 38px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--ink-3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    color: var(--muted);
    font-size: 14px;
    cursor: pointer;
    transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.icon-option:hover { color: var(--text); border-color: var(--text-dim); }
.icon-option.is-active {
    background: var(--signal-a16);
    border-color: var(--signal);
    color: var(--signal-hot);
}
.icon-option__diamond {
    width: 12px; height: 12px;
    background: currentColor;
    transform: rotate(45deg);
    border-radius: 2px;
}

/* --- Live peers ------------------------------------------------------ */
.peer-cursor { position: relative; width: 36px; }
.peer-cursor__avatar {
    width: 32px; height: 32px;
    border-radius: 50%;
    border: 2px solid var(--peer-color, var(--signal));
    box-shadow: 0 0 12px rgba(0, 0, 0, 0.6);
    overflow: hidden;
    background: var(--ink-2);
}
.peer-cursor__avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.peer-cursor__initial {
    width: 100%; height: 100%;
    display: flex; align-items: center; justify-content: center;
    font-family: var(--font-display);
    font-weight: 700; font-size: 14px;
    color: var(--text);
    background: var(--ink-3);
}
.peer-cursor__name {
    position: absolute;
    left: 50%; top: 34px;
    transform: translateX(-50%);
    padding: 1px 7px;
    background: var(--glass);
    border: 1px solid var(--peer-color, var(--signal));
    border-radius: var(--r-pill);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
}
.collab-dot img {
    width: 100%; height: 100%;
    border-radius: 50%;
    object-fit: cover;
    display: block;
}

/* --- Marker detail card (editor-only bits; base card in touchpad.css) - */
.marker-card-edit {
    background: none;
    border: 0;
    color: var(--muted);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    padding: 2px 4px;
}
.marker-card-edit:hover { color: var(--signal); }

/* The marker-notes thread is shared with the dashboard's cards, so its
   styles live in touchpad.css next to .marker-card. */

@media (max-width: 700px) {
    /* One column: the panel spans the width and the dock sits under it,
       both inside the same overlay padding. */
    .ed-ui { padding: var(--sp-3); grid-template-columns: 1fr; }
    .ed-corner--tl { grid-column: 1; grid-row: 1; align-self: start; width: 100%; }
    .ed-corner--tr { grid-column: 1; grid-row: 2; align-items: flex-end; align-self: start; }
    #district-summary { width: 100%; }
    #drawing-controls { width: 100%; }
    /* Same reasoning as touchpad.css: size for touch at the breakpoint,
       not only under `pointer: coarse`. */
    .summary-header button,
    .summary-header a { width: 40px; height: 40px; }
    .tool { width: 46px; height: 46px; }
    .ed-seg .unit-btn { min-width: 44px; padding: 8px 10px; }
    .create-shape-flow {
        top: auto;
        bottom: calc(84px + env(safe-area-inset-bottom));
        left: var(--sp-3); right: var(--sp-3);
        width: auto;
    }
}

/* --- 3D in the editor ------------------------------------------------
   Same trick as the dashboard: a MapLibre map in the same box, one visible
   at a time. It has to be declared here rather than reusing the dashboard's
   rules, because in the editor the box is #map-area, not #bg-map — and the
   editor page still renders base.html's background map underneath, which
   must stay hidden either way. */
#map-area #bg-map-3d {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: var(--ink);
    display: none;
}
body.is-editor.is-3d #map-area #bg-map-3d { display: block; }
body.is-editor.is-3d #map { visibility: hidden; }

/* The editor's Appearance control is the dashboard's, seated in the overlay
   grid instead of floating at a fixed corner — the top-right cell already
   holds the collaborator strip, and the two stack. */
.ed-corner--tr .dash-look { position: relative; top: auto; right: auto; }
.ed-corner--tr .dash-look__menu { z-index: 2200; }

/* Drawing is a flat-map gesture: Leaflet owns the handlers and there is no
   pitched equivalent. Rather than grey the tools out, picking one drops back
   to flat and says so — so the dock keeps working and 3D stays a view. */
body.is-3d #drawing-controls .tool[data-flat-only] { opacity: 0.55; }
