/* ═══════════════════════════════════════════════════════════
   Ponente — "Warm Studio"
   A tactile, inviting workspace that feels like a shared
   corkboard in a sunlit room. No hard edges, no cold
   surfaces. Friendly, approachable, human.
   ═══════════════════════════════════════════════════════════ */

/* Self-hosted variable fonts (latin subsets, vendored — no CDN, no IP leak
   to Google, works offline). Source: Google Fonts, SIL OFL. */
@font-face {
    font-family: 'Bricolage Grotesque';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('fonts/bricolage-grotesque.woff2') format('woff2');
}

@font-face {
    font-family: 'Figtree';
    font-style: normal;
    font-weight: 300 900;
    font-display: swap;
    src: url('fonts/figtree.woff2') format('woff2');
}

@font-face {
    font-family: 'Figtree';
    font-style: italic;
    font-weight: 300 900;
    font-display: swap;
    src: url('fonts/figtree-italic.woff2') format('woff2');
}

@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400 500;
    font-display: swap;
    src: url('fonts/jetbrains-mono.woff2') format('woff2');
}

/* ── Design tokens ─────────────────────────────────────── */
/* Every color is written ONCE as light-dark(light, dark) — which side
   renders is steered purely by color-scheme below. This is the single
   source of truth; never add a separate per-theme token block (the old
   duplicated blocks drifted apart). */
:root {
    --font-display: 'Bricolage Grotesque', 'Georgia', serif;
    --font-body: 'Figtree', 'Segoe UI', system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, monospace;

    /* Brand purple — kept warm */
    --brand: light-dark(#7c3aed, #b59cff);
    --brand-bright: light-dark(#ab7ef6, #d2c4ff);
    --brand-dim: light-dark(#6d2ad9, #8b6cff);
    --brand-glow: light-dark(rgba(124, 58, 237, 0.12), rgba(181, 156, 255, 0.10));
    --brand-surface: light-dark(#f3eeff, #2a2340);
    /* Text/icons sitting ON a --brand fill. Dark brand is a light lavender,
       so it needs ink, not white. */
    --brand-contrast: light-dark(#ffffff, #221733);

    /* Warm neutrals — fg-2 carries the Warm Studio sage tone.
       Surfaces climb with elevation: page (bg-0) < column/inset (bg-2)
       < card (bg-1); bg-3 is for pressed/raised accents. */
    --fg: light-dark(#2c2418, #f2ece2);
    --fg-2: light-dark(#6b7e6f, #b5a896);
    --fg-3: light-dark(#a39684, #7d6f5e);
    --bg-0: light-dark(#f7f6ef, #161311);
    --bg-1: light-dark(#ffffff, #2e2823);
    --bg-2: light-dark(#f2eee5, #221d1a);
    --bg-3: light-dark(#e6e2d6, #3a342e);

    --border-1: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.05));
    --border-2: light-dark(rgba(0, 0, 0, 0.10), rgba(255, 255, 255, 0.09));
    --border-3: light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.16));

    --success: light-dark(#16a36a, #36d399);
    --success-bg: light-dark(#eefff5, #142b20);
    --danger: light-dark(#fc2626, #f87171);
    --danger-bg: light-dark(#fef2f2, #2d1515);
    /* Text/icons sitting ON a --danger fill — dark danger is a light red. */
    --danger-contrast: light-dark(#ffffff, #2d0f0f);
    --info: light-dark(#2563eb, #7eb8ff);

    /* Toasts stay dark in light theme (intentional contrast pop) but
       lift to a raised surface in dark, where near-black would vanish. */
    --toast-bg: light-dark(#1a1613, #3a342e);
    --toast-fg: #f2ede6;

    --radius: 20px;
    --radius-lg: 24px;
    --radius-sm: 14px;
    --radius-xs: 10px;
    --radius-btn: 12px;
    --radius-pill: 999px;

    --shadow-xs: 0 1px 2px light-dark(rgba(44, 36, 24, 0.04), rgba(0, 0, 0, 0.4));
    --shadow-sm: 0 2px 8px light-dark(rgba(44, 36, 24, 0.06), rgba(0, 0, 0, 0.3)), 0 1px 2px light-dark(rgba(44, 36, 24, 0.04), rgba(0, 0, 0, 0.2));
    --shadow-md: 0 6px 24px light-dark(rgba(44, 36, 24, 0.08), rgba(0, 0, 0, 0.35)), 0 2px 6px light-dark(rgba(44, 36, 24, 0.04), rgba(0, 0, 0, 0.2));
    --shadow-lg: 0 16px 48px light-dark(rgba(44, 36, 24, 0.10), rgba(0, 0, 0, 0.4)), 0 4px 12px light-dark(rgba(44, 36, 24, 0.05), rgba(0, 0, 0, 0.25));
    --shadow-xl: 0 24px 64px light-dark(rgba(44, 36, 24, 0.14), rgba(0, 0, 0, 0.5)), 0 8px 20px light-dark(rgba(44, 36, 24, 0.06), rgba(0, 0, 0, 0.3));
    --shadow-card: 0 2px 8px light-dark(rgba(44, 36, 24, 0.05), rgba(0, 0, 0, 0.25)), 0 0 0 1px light-dark(rgba(44, 36, 24, 0.03), rgba(255, 255, 255, 0.035));
    --shadow-card-hover: 0 14px 36px light-dark(rgba(44, 36, 24, 0.10), rgba(0, 0, 0, 0.45)), 0 3px 8px light-dark(rgba(44, 36, 24, 0.04), rgba(0, 0, 0, 0.25));
    --shadow-brand: 0 8px 24px light-dark(rgba(124, 58, 237, 0.18), rgba(181, 156, 255, 0.22));

    --ease: cubic-bezier(0.22, 0.68, 0, 1);
    --ease-out: cubic-bezier(0, 0, 0.2, 1);
    --duration: 200ms;

    /* Auto: follow the OS preference */
    color-scheme: light dark;
}

/* Explicit user choice pins the scheme — every light-dark() above follows,
   as do UA-rendered parts (scrollbars, form controls, <dialog> backdrops). */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

/* Switching the theme changes every colour token at once, and a good part of
   the page carries a transition — so without this the swap is several hundred
   elements each animating background, colour, border and shadow for 200 ms.
   That is the lag on a phone, and it is animating a change nobody asked to see
   animated. The head script in App.razor sets this attribute, flushes the new
   colours under it, and drops it again in the same frame, which turns the swap
   into one repaint. It is never on for longer than that, so this is not a
   general-purpose "no transitions" switch. */
:root[data-theme-switching] *,
:root[data-theme-switching] *::before,
:root[data-theme-switching] *::after {
    transition: none !important;
}

/* ── Base reset ────────────────────────────────────────── */

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

h1:focus { outline: none; }

body {
    font-family: var(--font-body);
    margin: 0;
    background: var(--bg-0);
    color: var(--fg);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    line-height: 1.6;
    font-size: 15px;
}

code, pre, .admin-did code, .member-preview-did {
    font-family: var(--font-mono);
}

::selection {
    background: var(--brand);
    color: var(--brand-contrast);
}

/* ── Blazor scaffolding ────────────────────────────────── */

.valid.modified:not([type=checkbox]) {
    outline: 2px solid var(--success);
    outline-offset: -1px;
}

.invalid {
    outline: 2px solid var(--danger);
    outline-offset: -1px;
}

.validation-message {
    color: var(--danger);
    font-size: 0.85rem;
    margin-top: 0.25rem;
}

.blazor-error-boundary {
    background: var(--danger);
    padding: 1rem 1.25rem;
    color: white;
    border-radius: var(--radius-sm);
    margin: 1rem;
    font-weight: 600;
}

.blazor-error-boundary::after {
    content: "An error has occurred."
}

.darker-border-checkbox.form-check-input {
    border-color: var(--border-3);
}

/* ── Shared button system ──────────────────────────────── */

.btn-primary,
.btn-danger,
.btn-secondary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    border-radius: var(--radius-pill);
    padding: 0.65rem 1.5rem;
    cursor: pointer;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.9rem;
    font-weight: 600;
    transition: all var(--duration) var(--ease);
    position: relative;
    white-space: nowrap;
    letter-spacing: 0.01em;
}

.btn-primary {
    background: var(--brand);
    border: none;
    color: var(--brand-contrast);
    box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.2);
}

.btn-primary:hover:not(:disabled) {
    background: var(--brand-bright);
    box-shadow: var(--shadow-brand);
    transform: translateY(-2px);
}

.btn-primary:active:not(:disabled) {
    transform: translateY(0);
    box-shadow: var(--shadow-xs);
}

.btn-primary:disabled,
.btn-danger:disabled,
.btn-secondary:disabled {
    opacity: 0.35;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

.btn-danger {
    background: transparent;
    border: 2px solid var(--danger);
    color: var(--danger);
}

.btn-danger:hover:not(:disabled) {
    background: var(--danger);
    color: var(--danger-contrast);
    box-shadow: 0 6px 16px rgba(220, 38, 38, 0.2);
    transform: translateY(-2px);
}

.btn-secondary {
    background: var(--bg-2);
    border: 1px solid var(--border-2);
    color: var(--fg);
}

.btn-secondary:hover:not(:disabled) {
    background: var(--bg-3);
    border-color: var(--border-3);
}

/* ── Board header ──────────────────────────────────────── */

.board-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 1.75rem;
    height: 64px;
    background: var(--bg-1);
    color: var(--fg);
    position: sticky;
    top: 0;
    z-index: 100;
    box-shadow: 0 1px 0 var(--border-1);
}

.board-header h1 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.4rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--fg);
}

.board-user {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    font-size: 0.88rem;
    color: var(--fg-2);
    position: relative;
}

/* Default styles for *text* buttons inside .board-user. The icon-only
   round buttons (.nav-hamburger) own their own
   padding/border-radius and are excluded so the text-button defaults
   don't override their fixed-aspect 40×40 sizing. */
.board-user button:not(.nav-hamburger) {
    border: 1px solid var(--border-2);
    background: var(--bg-2);
    color: var(--fg-2);
    border-radius: var(--radius-pill);
    padding: 0.4rem 1rem;
    cursor: pointer;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.85rem;
    font-weight: 500;
    transition: all var(--duration) var(--ease);
}

.board-user button:not(.nav-hamburger):hover {
    background: var(--bg-3);
    border-color: var(--border-3);
    color: var(--fg);
}

.board-nav-link {
    color: var(--fg-2);
    text-decoration: none;
    font-size: 0.9rem;
    font-weight: 500;
    padding: 0.45rem 0.85rem;
    border-radius: var(--radius-pill);
    transition: all var(--duration) var(--ease);
}

.board-nav-link:hover {
    background: var(--brand-glow);
    color: var(--brand);
    text-decoration: none;
}

.board-nav-link-active {
    background: var(--brand-surface);
    color: var(--brand);
    font-weight: 600;
    text-decoration: none;
}

.board-title-link {
    text-decoration: none;
    color: inherit;
}

.board-title-link:hover { opacity: 0.8; }

.admin-page-title {
    padding: 1.5rem 1.75rem 0;
}

.admin-page-title h2 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.4rem;
    font-weight: 700;
    color: var(--fg);
    letter-spacing: -0.02em;
}

/* ── Board layout ──────────────────────────────────────── */

.board {
    display: flex;
    /* Stretch, not flex-start: columns sized to their content ended at three
       different heights, and the emptiest column — the one you most want to
       drop onto — offered the smallest drop target. */
    align-items: stretch;
    /* Once the columns hit their max width, whatever is still left over gets
       split either side rather than pooling on the right. `safe` matters: a
       plain `center` on an overflowing scroll container pushes the first
       column past the scroll origin and puts it out of reach. */
    justify-content: safe center;
    gap: 1.25rem;
    padding: 1.5rem;
    overflow-x: auto;
    overflow-y: hidden;
    /* Definite height (not min-height): columns scroll internally, so the
       page itself must never gain a scrollbar. dvh where supported. */
    height: calc(100vh - 64px);
    height: calc(100dvh - 64px);
    scroll-behavior: smooth;
    /* Sky wash: a whisper of brand at the top, like high haze. */
    background: linear-gradient(180deg, var(--brand-glow), transparent 45%);
    scrollbar-width: thin;
    scrollbar-color: var(--border-2) transparent;
}

.column {
    background: var(--bg-2);
    border: none;
    border-radius: var(--radius);
    padding: 0;
    width: 320px;
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    /* Fill the board's content box — automatically accounts for its padding
       and the horizontal scrollbar, unlike a hand-computed vh offset. */
    max-height: 100%;
    box-shadow: none;
    overflow: hidden;
}


.column-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem 1.15rem;
    border-bottom: none;
    background: transparent;
}

.column-name {
    margin: 0;
    font-family: var(--font-display);
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--fg);
    letter-spacing: -0.01em;
}

/* Count and filter button travel together at the header's right end. Pointedly
   *not* positioned: the flyout anchors on .column-header instead, because a
   percentage width here would resolve against these two chips — see
   .column-filter-panel. */
.column-tools {
    display: flex;
    align-items: center;
    gap: 0.3rem;
}

.column-count {
    font-size: 0.78rem;
    color: var(--fg-3);
    background: var(--bg-1);
    padding: 0.2rem 0.6rem;
    border-radius: var(--radius-pill);
    font-weight: 600;
    border: none;
    box-shadow: var(--shadow-xs);
    min-width: 1.7rem;
    text-align: center;
}

/* Filtered, the badge counts what is on screen rather than what the column
   holds — two different facts that must not look like the same one. */
.column-count-filtered {
    color: var(--brand);
    background: var(--brand-glow);
    box-shadow: none;
}

/* ── Column tag filter (button + flyout) ───────────────── */

.column-filter-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: none;
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--fg-3);
    cursor: pointer;
    transition: background var(--duration) var(--ease),
                color var(--duration) var(--ease);
}

.column-filter-toggle svg {
    width: 0.95rem;
    height: 0.95rem;
}

.column-filter-toggle:hover {
    background: var(--bg-1);
    color: var(--fg);
}

/* Filtering is a state the column is left in, and the flyout that says so is
   shut by then — so the button carries it, in the same brand tint the count
   badge takes. */
.column-filter-toggle-active,
.column-filter-toggle-active:hover {
    background: var(--brand-glow);
    color: var(--brand);
}

/* Invisible full-screen click-catcher, as the nav menu has. It covers the
   button that opened it too, so that button is lifted over it while the panel
   is open — otherwise the way you opened the flyout is the one place a click
   cannot close it again. */
.column-filter-backdrop {
    position: fixed;
    inset: 0;
    z-index: 190;
    background: transparent;
}

.column-filter-toggle[aria-expanded="true"] {
    position: relative;
    z-index: 200;
}

/* Anchored on .column-header — which is already positioned, for the hairline —
   rather than on .column-tools, so a percentage width resolves against the
   column rather than against the count badge and the button. It is deliberately
   sized to fit *inside* the column: .column clips at its own rounded edges, so
   a panel that outgrew it would be sliced rather than overflow. Hence the width
   under the 320px column's content box, the max-width for the narrower column a
   tablet gets, and a tag list that scrolls rather than growing. */
.column-filter-panel {
    position: absolute;
    top: calc(100% + 0.55rem);
    right: 1.15rem;
    z-index: 200;
    width: 15rem;
    max-width: calc(100% - 2.3rem);
    background: var(--bg-1);
    border: 1px solid var(--border-1);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-lg);
    padding: 0.7rem;
    animation: column-filter-in var(--duration) var(--ease) both;
}

@keyframes column-filter-in {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: none; }
}

.column-filter-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.55rem;
}

.column-filter-title {
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--fg-3);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.column-filter-clear {
    border: none;
    background: transparent;
    color: var(--brand);
    font-family: var(--font-body);
    font-size: 0.72rem;
    font-weight: 600;
    padding: 0.2rem 0.35rem;
    margin: -0.2rem -0.35rem;
    border-radius: var(--radius-xs);
    cursor: pointer;
    transition: background var(--duration) var(--ease);
}

.column-filter-clear:hover {
    background: var(--brand-glow);
}

/* Wraps rather than scrolling sideways: inside a panel the height is free, and
   a wrapped row shows every tag at once instead of hiding some off an edge. */
.column-filter-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    max-height: min(18rem, 40vh);
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-2) transparent;
}

/* The inline padding is the column's rail: it puts the card edges on the same
   1.15rem line as the header label and the hairline under it (which cards at
   0.65rem missed by 8px), and leaves the hover shadow room before the clip. */
.column-cards {
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    padding: 0.5rem 1.15rem 0.75rem;
    flex: 1;
    gap: 0.7rem;
    scrollbar-width: thin;
    scrollbar-color: var(--border-2) transparent;
}

/* Auto block margins centre it: the columns are full height now, so pinned to
   the top the doodle sat alone above a lot of empty panel. */
.column-empty {
    margin: auto 0;
    padding: 2.5rem 1.25rem;
    color: var(--fg-3);
    font-size: 0.88rem;
    text-align: center;
    font-style: italic;
    line-height: 1.6;
}

/* ── Cards — borderless, shadow-elevated ──────────────── */

.card {
    background: var(--bg-1);
    border: none;
    border-radius: var(--radius-sm);
    padding: 1rem 1.15rem;
    cursor: pointer;
    transition: all var(--duration) var(--ease);
    position: relative;
    box-shadow: var(--shadow-card);
}

.card:hover {
    box-shadow: var(--shadow-card-hover);
    transform: translateY(-3px);
}

.card:active {
    transform: translateY(-1px);
    box-shadow: var(--shadow-sm);
}

.card-title {
    margin: 0;
    font-family: var(--font-display);
    font-size: 0.95rem;
    font-weight: 600;
    line-height: 1.45;
    color: var(--fg);
    letter-spacing: -0.01em;
    /* A pasted URL is one unbreakable word and would run past the card. */
    overflow-wrap: anywhere;
}

/* Clamp on a line boundary. A max-height cut wherever it landed — 3.2rem
   over a 1.55 line-height showed 2.43 lines, slicing the third row of
   letters through the x-height on every card that had one. */
.card-description {
    margin: 0.45rem 0 0;
    font-size: 0.85rem;
    color: var(--fg-2);
    line-height: 1.55;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
    overflow-wrap: anywhere;
}

.column-add {
    /* Bottom inset matches the header's 1rem top inset. */
    margin: 0.35rem 1.15rem 1rem;
    border: 2px dashed var(--border-2);
    background: transparent;
    color: var(--fg-3);
    text-align: center;
    padding: 0.75rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 600;
    font-family: var(--font-body);
    transition: all var(--duration) var(--ease);
}

.column-add:hover {
    border-color: var(--brand);
    color: var(--brand);
    background: var(--brand-glow);
    border-style: solid;
}

/* ── Modal ─────────────────────────────────────────────── */

.modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(25, 21, 19, 0.45);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    padding: 1rem;
    animation: fadeIn var(--duration) var(--ease-out);
}

@keyframes fadeIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes slideUp {
    from { opacity: 0; transform: translateY(20px) scale(0.96); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}

.modal {
    background: var(--bg-1);
    border: none;
    border-radius: var(--radius);
    width: min(720px, 100%);
    max-height: calc(100vh - 2rem);
    display: flex;
    flex-direction: column;
    box-shadow: var(--shadow-xl);
    animation: slideUp 300ms var(--ease-out);
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1.25rem 1.5rem;
    border-bottom: 1px solid var(--border-1);
    flex-shrink: 0;
}

.modal-header h2 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: -0.02em;
}

.modal-close {
    background: none;
    border: none;
    font-size: 1.4rem;
    cursor: pointer;
    color: var(--fg-3);
    width: 2.5rem;
    height: 2.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    transition: all var(--duration) var(--ease);
}

.modal-close:hover {
    background: var(--bg-2);
    color: var(--fg);
}

.modal-body {
    padding: 1.35rem 1.5rem 1.5rem;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
    flex: 1;
    min-height: 0;
    scrollbar-width: thin;
    scrollbar-color: var(--border-2) transparent;
}

.field {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.field-label {
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--fg-2);
}

.field input,
.field textarea,
.field select {
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.65rem 0.85rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    resize: vertical;
    background: var(--bg-0);
    color: var(--fg);
    transition: all var(--duration) var(--ease);
}

.field input:focus,
.field textarea:focus,
.field select:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
    background: var(--bg-1);
}

.field input::placeholder,
.field textarea::placeholder {
    color: var(--fg-3);
    opacity: 0.7;
}

/* .atproto-login-error is the SDK LoginForm's own error block. It ships with
   Bootstrap class names we don't have, so give it the same look as ours. */
.modal-error,
.atproto-login-error {
    margin: 0;
    color: var(--danger);
    font-size: 0.88rem;
    font-weight: 500;
    padding: 0.75rem 1rem;
    background: var(--danger-bg);
    border-radius: var(--radius-xs);
    border: none;
    box-shadow: var(--shadow-xs);
}

.modal-footer {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    padding: 1rem 1.5rem;
    border-top: 1px solid var(--border-1);
    flex-shrink: 0;
}

.modal-footer-spacer { flex: 1; }

.delete-confirm-label {
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--danger);
}

.modal-footer button {
    border: 1px solid var(--border-2);
    background: var(--bg-1);
    border-radius: var(--radius-pill);
    padding: 0.5rem 1.15rem;
    cursor: pointer;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.88rem;
    font-weight: 500;
    color: var(--fg-2);
    transition: all var(--duration) var(--ease);
}

.modal-footer button:hover:not(:disabled) {
    background: var(--bg-2);
    border-color: var(--border-3);
}

.modal-footer button:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.modal-footer .btn-primary {
    background: var(--brand);
    border-color: transparent;
    color: var(--brand-contrast);
}

.modal-footer .btn-primary:hover:not(:disabled) {
    background: var(--brand-bright);
    box-shadow: var(--shadow-brand);
    transform: translateY(-1px);
}

.modal-footer .btn-danger {
    background: transparent;
    border-color: var(--danger);
    color: var(--danger);
}

.modal-footer .btn-danger:hover:not(:disabled) {
    background: var(--danger);
    color: var(--danger-contrast);
}

/* ── Card modal: composer / details / discussion ───────── */
/* The card dialog is three groups, not six stacked form rows: what the card
   says (composer), what it is filed under (details), and what people said
   about it (discussion). Group headings reuse the column header's language —
   display face plus a pill count — so the dialog reads like the board it
   belongs to, and the labelled-input list disappears: the title is set in the
   type it will be read in, and the description carries its own Write/Preview
   switch instead of a second, permanently-open preview block below it. */

.section-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 2rem;
}

.section-label {
    font-family: var(--font-display);
    font-size: 0.9rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--fg);
}

.section-count {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--fg-3);
    background: var(--bg-2);
    border-radius: var(--radius-pill);
    padding: 0.1rem 0.55rem;
    box-shadow: var(--shadow-xs);
}

.section-note {
    margin: 0;
    color: var(--fg-3);
    font-size: 0.88rem;
}

/* — Composer — */

.composer {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1.1rem;
    background: var(--bg-2);
    border-radius: var(--radius-sm);
}

/* The headline is an input that looks like the heading it becomes. It carries
   no box of its own; the underline only shows up on hover and focus, which is
   also the whole focus indicator (the generic ring covers buttons, not
   fields). */
.composer-title {
    width: 100%;
    border: none;
    border-bottom: 2px solid transparent;
    background: transparent;
    padding: 0.1rem 0 0.25rem;
    font-family: var(--font-display);
    font-size: 1.4rem;
    font-weight: 700;
    letter-spacing: -0.025em;
    line-height: 1.3;
    color: var(--fg);
    transition: border-color var(--duration) var(--ease);
}

.composer-title::placeholder {
    color: var(--fg-3);
    opacity: 0.6;
}

.composer-title:hover:not(:focus) {
    border-bottom-color: var(--border-2);
}

.composer-title:focus {
    outline: none;
    border-bottom-color: var(--brand);
}

.composer-error {
    padding: 0.55rem 0.85rem;
    font-size: 0.85rem;
}

.composer-desc {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
}

/* — Write / Preview switch — */

.segmented {
    margin-left: auto;
    display: inline-flex;
    gap: 2px;
    padding: 3px;
    background: var(--bg-1);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-xs);
}

.segmented-tab {
    border: none;
    background: transparent;
    border-radius: var(--radius-pill);
    padding: 0.28rem 0.85rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--fg-3);
    cursor: pointer;
    transition: all var(--duration) var(--ease);
}

.segmented-tab:hover:not(.segmented-tab-on) {
    color: var(--fg);
}

.segmented-tab-on {
    background: var(--brand);
    color: var(--brand-contrast);
    box-shadow: var(--shadow-xs);
}

/* Both tabs render on the same footprint, so flipping between them never
   resizes the dialog under the pointer. */
.composer-panel {
    display: flex;
    flex-direction: column;
    min-height: 12rem;
}

.composer-body,
.composer-preview,
.composer-empty {
    flex: 1;
    border-radius: var(--radius-xs);
    background: var(--bg-1);
    padding: 0.75rem 0.9rem;
    min-height: 12rem;
}

.composer-body {
    width: 100%;
    border: 1px solid var(--border-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    line-height: 1.65;
    color: var(--fg);
    resize: vertical;
    transition: all var(--duration) var(--ease);
}

.composer-body:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
}

.composer-body::placeholder {
    color: var(--fg-3);
    opacity: 0.7;
}

.composer-preview {
    border: 1px solid transparent;
    overflow-wrap: anywhere;
}

.composer-empty {
    margin: 0;
    border: 1px dashed var(--border-2);
    color: var(--fg-3);
    font-size: 0.88rem;
}

/* — Details: tags and assignees, two up — */

.card-details {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: 0.75rem;
}

.detail-panel {
    display: flex;
    flex-direction: column;
    gap: 0.65rem;
    align-content: start;
    padding: 0.9rem 1rem 1rem;
    background: var(--bg-2);
    border-radius: var(--radius-sm);
}

/* — Discussion — */

/* The rule separates the card from the talk about it; everything above it is
   the card itself, everything below is other people. */
.discussion {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--border-2);
}

/* ── Markdown ──────────────────────────────────────────── */

.markdown {
    font-size: 0.93rem;
    line-height: 1.7;
    color: var(--fg);
}

.markdown p { margin: 0 0 0.5rem; }
.markdown p:last-child { margin-bottom: 0; }
.markdown ul, .markdown ol { padding-left: 1.25rem; margin: 0 0 0.5rem; }

.markdown code {
    font-family: var(--font-mono);
    background: var(--bg-2);
    padding: 0.15rem 0.45rem;
    border-radius: 6px;
    font-size: 0.82em;
    border: none;
}

.markdown pre {
    background: var(--bg-2);
    padding: 1rem;
    border-radius: var(--radius-xs);
    overflow-x: auto;
    border: none;
}

.markdown pre code {
    background: transparent;
    padding: 0;
}

.markdown a {
    color: var(--brand);
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, var(--brand) 35%, transparent);
    text-underline-offset: 2px;
}

.markdown a:hover {
    text-decoration-color: var(--brand);
}

/* ── SortableJS drag states ────────────────────────────── */

.card-ghost {
    opacity: 0.25;
    background: var(--brand-surface);
    box-shadow: none;
}

.card-chosen {
    cursor: grabbing;
    box-shadow: var(--shadow-lg);
}

.card-dragging {
    box-shadow: var(--shadow-brand), var(--shadow-lg);
    z-index: 10;
    rotate: 2.5deg;
}

/* ── Tag chips — solid, pill-shaped ──────────────────── */

.card-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-bottom: 0.5rem;
}

.tag-chip {
    --tag-color: #888;
    display: inline-flex;
    align-items: center;
    padding: 0.22rem 0.65rem;
    border-radius: var(--radius-pill);
    border: none;
    background: var(--tag-color);
    color: #fff;
    font-size: 0.72rem;
    font-weight: 600;
    cursor: pointer;
    line-height: 1.3;
    transition: all var(--duration) var(--ease);
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
    letter-spacing: 0.02em;
}

.tag-chip-static { cursor: default; }

.tag-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.45rem;
}

.tag-chip:not(.tag-chip-selected):not(.tag-chip-static) {
    background: color-mix(in srgb, var(--tag-color) 15%, transparent);
    /* Tag colors are user-picked and often mid-bright: ink them down on
       light paper, use them straight on dark. */
    color: light-dark(color-mix(in srgb, var(--tag-color) 80%, #2c2418), var(--tag-color));
    text-shadow: none;
    border: 1px solid color-mix(in srgb, var(--tag-color) 25%, transparent);
}

.tag-chip-selected {
    background: var(--tag-color);
    color: #fff;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
    border: none;
}

/* Picked vs. not-picked was carried by fill alone, which in a row of
   user-chosen tag colours reads as two more colours. The tick says which is
   which. Cards get no tick — their chips are labels, not choices, and the
   filter flyout's are choices again. */
.tag-picker .tag-chip-selected::before,
.column-filter-tags .tag-chip-selected::before,
.assignee-picker .assignee-chip-selected::after {
    content: "";
    display: inline-block;
    flex: 0 0 auto;
    width: 0.85em;
    height: 0.85em;
    background: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' d='M3 8.5 6.5 12 13 4.5'/%3E%3C/svg%3E") no-repeat center / contain;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' d='M3 8.5 6.5 12 13 4.5'/%3E%3C/svg%3E") no-repeat center / contain;
}

.tag-picker .tag-chip-selected::before,
.column-filter-tags .tag-chip-selected::before {
    margin-right: 0.35em;
    vertical-align: -0.05em;
}

.assignee-picker .assignee-chip-selected::after {
    background: var(--brand);
    width: 0.95rem;
    height: 0.95rem;
    margin-left: 0.05rem;
}

.tag-picker-empty {
    margin: 0;
    color: var(--fg-3);
    font-size: 0.88rem;
}

/* ── Assignees ─────────────────────────────────────────── */

.card-meta {
    display: flex;
    align-items: center;
    margin-top: 0.6rem;
    gap: 0.4rem;
}

.card-assignees {
    display: flex;
    flex-wrap: wrap;
    gap: 0.2rem;
}

/* Always trails the row. Under `space-between` the badge sat hard left on a
   card with no assignees and hard right on one with them, so the same badge
   changed sides card to card. */
.card-meta .card-comment-count {
    margin-left: auto;
}

.card-comment-count {
    font-size: 0.78rem;
    color: var(--fg-3);
    background: var(--bg-2);
    padding: 0.18rem 0.55rem;
    border-radius: var(--radius-pill);
    border: none;
    white-space: nowrap;
    box-shadow: var(--shadow-xs);
}

/* Speech-bubble glyph drawn with the same currentColor-mask technique as
   the wind doodles — crisp and identical on every platform, unlike emoji. */
.card-comment-count::before {
    content: "";
    display: inline-block;
    width: 0.85em;
    height: 0.85em;
    margin-right: 0.35em;
    vertical-align: -0.1em;
    background: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='1.8' stroke-linejoin='round' d='M14 7.4c0 2.9-2.7 5.2-6 5.2-.7 0-1.4-.1-2-.3L2.6 13.5l.9-2.6C2.6 10 2 8.8 2 7.4 2 4.5 4.7 2.2 8 2.2s6 2.3 6 5.2Z'/%3E%3C/svg%3E") no-repeat center / contain;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='1.8' stroke-linejoin='round' d='M14 7.4c0 2.9-2.7 5.2-6 5.2-.7 0-1.4-.1-2-.3L2.6 13.5l.9-2.6C2.6 10 2 8.8 2 7.4 2 4.5 4.7 2.2 8 2.2s6 2.3 6 5.2Z'/%3E%3C/svg%3E") no-repeat center / contain;
}

.assignee-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.7rem;
    height: 1.7rem;
    border-radius: 50%;
    background: var(--brand-surface);
    color: var(--brand);
    font-size: 0.65rem;
    font-weight: 700;
    border: 2px solid var(--bg-1);
    box-shadow: 0 0 0 1px var(--border-1);
}

.assignee-avatar-img {
    object-fit: cover;
    background: var(--bg-2);
    padding: 0;
}

.assignee-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.45rem;
}

.assignee-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.3rem 0.7rem 0.3rem 0.3rem;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-2);
    background: var(--bg-0);
    cursor: pointer;
    font-size: 0.82rem;
    transition: all var(--duration) var(--ease);
}

.assignee-chip:hover {
    border-color: var(--brand);
    background: var(--brand-glow);
    box-shadow: var(--shadow-xs);
}

.assignee-chip-selected {
    background: var(--brand-surface);
    border-color: var(--brand);
}

.assignee-chip .assignee-name {
    color: var(--fg);
    font-weight: 500;
}

/* ── Comments ──────────────────────────────────────────── */

.comment-list {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
}

/* Same inset surface as the composer and the detail panels — inside the
   dialog, anything that groups content sits on bg-2 and anything you type
   into sits on bg-1. */
.comment {
    border: none;
    border-radius: var(--radius-sm);
    padding: 0.8rem 1rem;
    background: var(--bg-2);
}

.comment-header {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.45rem;
}

.comment-author {
    font-weight: 600;
    font-size: 0.88rem;
    color: var(--brand);
}

.comment-time {
    color: var(--fg-3);
    font-size: 0.78rem;
    margin-left: auto;
}

/* Wherever you write in this dialog it is the same object: a bg-1 field on a
   bg-2 panel. Composer and comment box are built from that one rule. */
.comment-form {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
    margin-top: 0.2rem;
    padding: 0.75rem;
    background: var(--bg-2);
    border-radius: var(--radius-sm);
}

.comment-form textarea {
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.65rem 0.85rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    line-height: 1.6;
    resize: vertical;
    background: var(--bg-1);
    color: var(--fg);
    transition: all var(--duration) var(--ease);
}

.comment-form textarea:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
    background: var(--bg-1);
}

.comment-form .btn-primary {
    align-self: flex-end;
}

/* ── Login page ────────────────────────────────────────── */

.login-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    min-height: 100dvh;
    padding: 2rem;
    /* Warm sun glow upper-right, brand haze lower-left — echoes the mark. */
    background:
        radial-gradient(480px circle at 86% 8%, rgba(255, 198, 107, 0.12), transparent 70%),
        radial-gradient(640px circle at 8% 96%, var(--brand-glow), transparent 70%),
        var(--bg-0);
    position: relative;
}

.login-card {
    background: var(--bg-1);
    border: none;
    border-radius: var(--radius-lg);
    padding: 2.25rem 2.25rem 2rem;
    width: min(456px, 100%);
    box-shadow: var(--shadow-lg);
    text-align: center;
    position: relative;
}

.login-card h1 {
    margin: 0 0 0.5rem;
    font-family: var(--font-display);
    font-size: 1.75rem;
    font-weight: 800;
    letter-spacing: -0.03em;
    background: linear-gradient(135deg, var(--brand-dim), var(--brand-bright));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}

.login-card > p {
    color: var(--fg-2);
    font-size: 0.93rem;
    margin: 0 0 1.5rem;
    line-height: 1.55;
}

/* The gap here separates whole fields; label, input and hint sit closer
   together inside their .field, so the form reads as groups rather than as
   one evenly spaced ladder. */
.login-card form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    text-align: left;
}

.login-card .field {
    gap: 0.3rem;
}

.login-card form label {
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--fg-2);
}

.login-card form input:not([type="checkbox"]):not([type="radio"]),
.login-card form textarea {
    width: 100%;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.7rem 0.9rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 1rem;
    background: var(--bg-0);
    color: var(--fg);
    box-sizing: border-box;
    /* Named properties, not `all`: password managers write inline styles on
       these inputs (Proton Pass reserves room for its icon with padding), and
       `all` animated every one of those writes. */
    transition: border-color var(--duration) var(--ease),
                box-shadow var(--duration) var(--ease),
                background var(--duration) var(--ease);
}

.login-card form input:not([type="checkbox"]):not([type="radio"]):focus,
.login-card form textarea:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
    background: var(--bg-1);
}

.login-card form input::placeholder,
.login-card form textarea::placeholder {
    color: var(--fg-3);
    opacity: 0.6;
}

/* The handle field's placeholder cycles through example handles
   (js/handle-placeholder.js). The text on screen is a ghost laid over the
   field, because only part of it moves — the shared "alice." holds still while
   the domain rolls over. The real placeholder stays put underneath for
   assistive tech, painted transparent so the two don't stack up. */
.handle-ghost-field {
    position: relative;
}

.login-card form input.has-handle-ghost::placeholder {
    color: transparent;
}

.handle-ghost {
    position: absolute;
    display: flex;
    align-items: center;
    /* Clips the roll at the field's edges, exactly like a real placeholder. */
    overflow: hidden;
    pointer-events: none;
    white-space: nowrap;
    color: var(--fg-3);
    opacity: 0.6;
}

.handle-ghost.is-hidden {
    display: none;
}

.handle-ghost-reel {
    position: relative;
}

.handle-ghost-domain {
    display: inline-block;
    /* The incoming domain waits out most of the outgoing one's fade before
       showing up, so the two are never both legible. 420ms has to match
       SWAP_MS in js/handle-placeholder.js. */
    transition: transform 420ms var(--ease), opacity 280ms var(--ease) 140ms;
}

.handle-ghost-domain.is-entering {
    transform: translateY(0.75em);
    opacity: 0;
}

.handle-ghost-domain.is-leaving {
    position: absolute;
    left: 0;
    top: 0;
    transform: translateY(-0.75em);
    opacity: 0;
    transition: transform 420ms var(--ease), opacity 200ms var(--ease);
}

@media (prefers-reduced-motion: reduce) {
    .handle-ghost-domain {
        transition: none;
    }
}

.login-card form textarea {
    resize: vertical;
    min-height: 6rem;
    line-height: 1.5;
}

.login-card form button[type="submit"] {
    margin-top: 0.35rem;
    background: var(--brand);
    border: none;
    color: var(--brand-contrast);
    border-radius: var(--radius-pill);
    padding: 0.9rem 1.25rem;
    cursor: pointer;
    font: inherit;
    font-family: var(--font-body);
    font-size: 1rem;
    font-weight: 600;
    transition: all var(--duration) var(--ease);
    box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.2);
    letter-spacing: 0.01em;
}

.login-card form button[type="submit"]:hover {
    background: var(--brand-bright);
    box-shadow: var(--shadow-brand);
    transform: translateY(-2px);
}

.login-card form button[type="submit"]:active {
    transform: translateY(0);
}

.login-card form small,
.login-card form .hint {
    font-size: 0.82rem;
    color: var(--fg-3);
    text-align: left;
}

.login-card form input[type="checkbox"] {
    accent-color: var(--brand);
    margin-right: 0.3rem;
}

/* ── Standalone pages (Access Denied, Not Found, Error) ── */

.standalone-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    padding: 2rem;
    text-align: center;
}

.standalone-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: var(--brand-glow);
    color: var(--brand);
    margin-bottom: 1.25rem;
}

.standalone-icon svg {
    width: 32px;
    height: 32px;
    stroke-width: 1.75;
}

.standalone-icon.standalone-icon-danger {
    background: var(--danger-bg);
    color: var(--danger);
}

.standalone-page h1 {
    font-family: var(--font-display);
    font-size: 1.6rem;
    font-weight: 700;
    margin: 0 0 0.6rem;
    color: var(--fg);
    letter-spacing: -0.02em;
}

.standalone-page p {
    color: var(--fg-2);
    font-size: 0.95rem;
    margin: 0 0 0.35rem;
    max-width: 380px;
    line-height: 1.6;
}

.standalone-page button,
.standalone-page a.btn {
    margin-top: 1.5rem;
    background: var(--brand);
    border: none;
    color: var(--brand-contrast);
    border-radius: var(--radius-pill);
    padding: 0.7rem 1.5rem;
    cursor: pointer;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    font-weight: 600;
    text-decoration: none;
    transition: all var(--duration) var(--ease);
    box-shadow: var(--shadow-sm);
}

.standalone-page button:hover,
.standalone-page a.btn:hover {
    background: var(--brand-bright);
    box-shadow: var(--shadow-brand);
    transform: translateY(-2px);
}

/* ── Admin pages ───────────────────────────────────────── */

.admin-page {
    padding: 1.5rem 1.75rem;
    max-width: 880px;
    margin: 0 auto;
}

.admin-section {
    background: var(--bg-1);
    border: none;
    border-radius: var(--radius);
    padding: 1.75rem;
    margin-bottom: 1.5rem;
    box-shadow: var(--shadow-card);
}

.admin-section h2 {
    margin: 0 0 1.15rem;
    font-family: var(--font-display);
    font-size: 1.1rem;
    font-weight: 700;
    letter-spacing: -0.01em;
}

.admin-form {
    display: flex;
    gap: 0.65rem;
    align-items: center;
}

.admin-form input[type="text"] {
    flex: 1;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.6rem 0.85rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    background: var(--bg-0);
    color: var(--fg);
    transition: all var(--duration) var(--ease);
}

.admin-form input[type="text"]:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
    background: var(--bg-1);
}

.admin-form input[type="text"]::placeholder {
    color: var(--fg-3);
    opacity: 0.6;
}

.admin-form select {
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.6rem 0.85rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    background: var(--bg-0);
    color: var(--fg);
}

.admin-table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: 0.9rem;
}

.admin-table th,
.admin-table td {
    padding: 0.75rem 0.65rem;
    border-bottom: 1px solid var(--border-1);
    text-align: left;
}

.admin-table th {
    color: var(--fg-3);
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding-bottom: 0.6rem;
}

.admin-table tbody tr {
    transition: background var(--duration) var(--ease);
}

.admin-table tbody tr:hover {
    background: var(--brand-glow);
}

.admin-table tbody tr:last-child td {
    border-bottom: none;
}

.admin-table input[type="text"] {
    width: 100%;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.4rem 0.65rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.9rem;
    background: var(--bg-0);
    color: var(--fg);
}

.admin-row-actions {
    display: flex;
    gap: 0.45rem;
    justify-content: flex-end;
}

/* As a table cell this class must stay a real cell: display:flex detaches
   the cell from the row box, so its border-bottom stops lining up with the
   row separator whenever a sibling cell wraps to two lines. */
td.admin-row-actions {
    display: table-cell;
    text-align: right;
    white-space: nowrap;
    vertical-align: middle;
}

td.admin-row-actions button + button {
    margin-left: 0.45rem;
}

.admin-row-actions button {
    border: 1px solid var(--border-2);
    border-radius: var(--radius-pill);
    padding: 0.38rem 0.85rem;
    cursor: pointer;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.82rem;
    font-weight: 600;
    background: var(--bg-0);
    color: var(--fg-2);
    transition: all var(--duration) var(--ease);
}

.admin-row-actions button:hover:not(:disabled) {
    background: var(--bg-2);
    border-color: var(--border-3);
}

.admin-row-actions .btn-primary {
    background: var(--brand);
    border-color: transparent;
    color: var(--brand-contrast);
}

.admin-row-actions .btn-primary:hover:not(:disabled) {
    background: var(--brand-bright);
    box-shadow: var(--shadow-brand);
}

.admin-row-actions .btn-danger {
    background: transparent;
    border-color: var(--danger);
    color: var(--danger);
}

.admin-row-actions .btn-danger:hover:not(:disabled) {
    background: var(--danger);
    color: var(--danger-contrast);
}

.admin-empty {
    margin: 0;
    color: var(--fg-3);
    font-style: italic;
}

.admin-did code {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--fg-3);
    word-break: break-all;
}

/* ── Admin sortable list (column reorder) ──────────────── */

.admin-sortable-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.admin-sortable-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.7rem 0.85rem;
    background: var(--bg-0);
    border: none;
    border-radius: var(--radius-xs);
    transition: all var(--duration) var(--ease);
    box-shadow: var(--shadow-xs);
}

.admin-sortable-item:hover {
    box-shadow: var(--shadow-sm);
}

.admin-sortable-handle {
    cursor: grab;
    color: var(--fg-3);
    font-size: 1.2rem;
    line-height: 1;
    user-select: none;
    padding: 0 0.2rem;
}

.admin-sortable-handle:active { cursor: grabbing; }

.admin-sortable-pos {
    font-size: 0.82rem;
    color: var(--fg-3);
    min-width: 1.5rem;
    text-align: center;
    font-weight: 600;
}

.admin-sortable-input {
    flex: 1;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.4rem 0.65rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.9rem;
    background: var(--bg-1);
    color: var(--fg);
}

.admin-sortable-input:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
}

.admin-sortable-ghost {
    opacity: 0.3;
    background: var(--brand-surface);
}

.admin-sortable-chosen {
    box-shadow: var(--shadow-md);
}

/* ── Member preview (handle lookup) ────────────────────── */

.member-preview {
    margin-top: 1.25rem;
    border: none;
    border-radius: var(--radius);
    padding: 1.5rem;
    background: var(--bg-0);
    box-shadow: var(--shadow-sm);
}

.member-preview-card {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
}

.member-preview-avatar {
    width: 60px;
    height: 60px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    border: 3px solid var(--bg-1);
    box-shadow: var(--shadow-sm);
}

.member-preview-avatar-fallback {
    width: 60px;
    height: 60px;
    border-radius: 50%;
    background: var(--brand-surface);
    color: var(--brand);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.15rem;
    font-weight: 700;
    flex-shrink: 0;
    border: 3px solid var(--bg-1);
}

.member-preview-info {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}

.member-preview-info strong {
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--fg);
}

.member-preview-handle {
    font-size: 0.88rem;
    color: var(--fg-2);
}

.member-preview-did {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--fg-3);
    word-break: break-all;
    margin-top: 0.15rem;
}

.member-preview-bio {
    margin: 0.4rem 0 0;
    font-size: 0.88rem;
    color: var(--fg-2);
    line-height: 1.6;
}

.member-preview-note {
    margin: 1rem 0 0;
    font-size: 0.88rem;
    color: var(--fg-3);
    padding: 0.65rem 0.9rem;
    background: var(--brand-glow);
    border-radius: var(--radius-xs);
    border: none;
    font-style: italic;
}

.member-preview-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 1rem;
}

.member-preview-actions select {
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.55rem 0.85rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.88rem;
    background: var(--bg-1);
    color: var(--fg);
}

/* ── Scrollbars ────────────────────────────────────────── */

.column-cards::-webkit-scrollbar {
    width: 5px;
}

.column-cards::-webkit-scrollbar-track {
    background: transparent;
}

.column-cards::-webkit-scrollbar-thumb {
    background: var(--border-2);
    border-radius: 3px;
}

.column-cards::-webkit-scrollbar-thumb:hover {
    background: var(--border-3);
}

/* ── Reconnect modal ───────────────────────────────────── */

#components-reconnect-modal {
    border: none;
    border-radius: var(--radius);
    padding: 0;
    background: var(--bg-1);
    box-shadow: var(--shadow-xl);
}

#components-reconnect-modal::backdrop {
    background: rgba(25, 21, 19, 0.5);
    backdrop-filter: blur(10px);
}

.components-reconnect-container {
    padding: 2.25rem;
    text-align: center;
    color: var(--fg);
    font-family: var(--font-body);
}

.components-reconnect-container p {
    margin: 0.5rem 0;
    color: var(--fg-2);
    font-size: 0.93rem;
}

.components-reconnect-container button {
    margin-top: 0.85rem;
    background: var(--brand);
    border: none;
    color: var(--brand-contrast);
    border-radius: var(--radius-pill);
    padding: 0.6rem 1.5rem;
    cursor: pointer;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    font-weight: 600;
    transition: all var(--duration) var(--ease);
}

.components-reconnect-container button:hover {
    background: var(--brand-bright);
}

/* ── Nav admin links wrapper ──────────────────────────── */

.nav-admin-links {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

/* ── Nav account (avatar + handle) ────────────────────── */

.nav-account {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.25rem 0.85rem 0.25rem 0.3rem;
    background: var(--bg-2);
    border: 1px solid var(--border-1);
    border-radius: var(--radius-pill);
    transition: all var(--duration) var(--ease);
}

.nav-account:hover {
    background: var(--bg-3);
    border-color: var(--border-2);
}

.nav-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    object-fit: cover;
    background: var(--brand-surface);
    border: none;
    flex-shrink: 0;
    box-shadow: 0 0 0 1px var(--border-2);
}

.nav-avatar-fallback {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--brand);
    font-size: 0.72rem;
    font-weight: 700;
    font-family: var(--font-display);
}

.board-user-handle {
    color: var(--fg);
    font-weight: 500;
    font-size: 0.88rem;
    line-height: 1;
}

/* ── Sign-out icon button ─────────────────────────────── */

/* ── Hamburger menu (prefs popover) ───────────────────── */

.nav-hamburger {
    display: inline-flex;
    position: relative;
    background: var(--bg-2);
    border: 1px solid var(--border-1);
    color: var(--fg-2);
    cursor: pointer;
    padding: 0;
    width: 40px;
    height: 40px;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    transition: all var(--duration) var(--ease);
    flex-shrink: 0;
}

.nav-hamburger svg {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
}

.nav-hamburger:hover {
    background: var(--bg-3);
    border-color: var(--border-2);
    color: var(--fg);
}

/* Nav menu popover (all viewports) */
.nav-menu {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 1rem;
    background: var(--bg-1);
    border: 1px solid var(--border-1);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    padding: 0.65rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    transform: translateY(-8px);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--duration) var(--ease),
                transform var(--duration) var(--ease),
                visibility 0s var(--duration);
    z-index: 200;
    min-width: 260px;
}

/* Invisible full-screen click-catcher. Lives inside the sticky header's
   stacking context (z 100): negative z keeps it below the header's own
   children — so nav links stay one-click while the menu is open (navigation
   closes it) — yet the header context still floats above all page content,
   so clicks anywhere else land here and dismiss. The menu itself is z 200. */
.nav-menu-backdrop {
    position: fixed;
    inset: 0;
    z-index: -1;
    background: transparent;
}

.nav-menu-open {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    pointer-events: auto;
    transition: opacity var(--duration) var(--ease),
                transform var(--duration) var(--ease),
                visibility 0s 0s;
}

/* On desktop+tablet, hide the duplicated mobile admin links inside the menu */
.nav-menu-mobile-links { display: none; }

/* ── Prefs (theme + lang) inside menu ─────────────────── */

.nav-menu-prefs {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 0.5rem 0.5rem 0.65rem;
}

.nav-pref-row {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.nav-pref-label {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--fg-3);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.nav-pref-options {
    display: flex;
    gap: 0.3rem;
    background: var(--bg-2);
    padding: 0.25rem;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-1);
}

/* Specificity is bumped with `.board-user` so these pill styles win over
   the generic `.board-user button:not(...)` defaults (which would otherwise
   paint a surface background on top of the active pill's brand fill). */
.board-user .nav-pref-options button {
    flex: 1;
    border: none;
    background: transparent;
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.82rem;
    font-weight: 500;
    padding: 0.45rem 0.75rem;
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: all var(--duration) var(--ease);
    white-space: nowrap;
}

.board-user .nav-pref-options button:hover {
    background: var(--bg-1);
    color: var(--fg);
}

/* The language pill is told which one is active by the render that translated
   it. The theme pill is not: the theme is a browser-only concern, and reading
   it back off :root is what lets the tap land without waiting for a round-trip
   — no data-theme at all is "auto", the attribute's value is a pinned choice. */
.board-user .nav-pref-options button.active,
:root:not([data-theme]) .board-user .nav-pref-options button[data-theme-set="auto"],
:root[data-theme="light"] .board-user .nav-pref-options button[data-theme-set="light"],
:root[data-theme="dark"] .board-user .nav-pref-options button[data-theme-set="dark"] {
    background: var(--brand);
    color: var(--brand-contrast);
    box-shadow: var(--shadow-xs);
}

/* Sign-out inside menu */
.nav-menu-signout {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
    border: none;
    background: none;
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.92rem;
    font-weight: 500;
    padding: 0.7rem 1rem;
    cursor: pointer;
    border-top: 1px solid var(--border-1);
    margin-top: 0.25rem;
    border-radius: var(--radius-xs);
    transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}

.nav-menu-signout:hover {
    background: var(--danger-bg);
    color: var(--danger);
}

/* ── Install as an app ────────────────────────────────── */

/* The one hint outside the menu that installing is possible: a dot on the
   hamburger, shown until the menu has been opened once (js/pwa-install.js
   remembers), and never on a browser that cannot install. */
.nav-hamburger-dot {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--brand);
    /* Punches a ring out of the button so the dot reads at any header color. */
    box-shadow: 0 0 0 2px var(--bg-2);
}

/* Specificity bumped the same way the pref pills are, so the generic
   `.board-user button:not(...)` pill defaults don't paint over this. */
.board-user button.nav-menu-install {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    width: 100%;
    text-align: left;
    border: none;
    background: var(--brand-surface);
    color: var(--brand);
    font: inherit;
    font-family: var(--font-body);
    padding: 0.6rem 0.75rem;
    cursor: pointer;
    border-radius: var(--radius-xs);
    transition: background var(--duration) var(--ease);
}

.board-user button.nav-menu-install:hover {
    background: var(--brand-glow);
    color: var(--brand);
}

.nav-menu-install svg {
    flex-shrink: 0;
}

.nav-menu-install-text {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
}

.nav-menu-install-label {
    font-size: 0.92rem;
    font-weight: 600;
}

.nav-menu-install-sub {
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--fg-2);
}

.nav-install-hint {
    margin: 0.15rem 0 0;
    padding: 0 0.75rem 0.35rem;
    font-size: 0.78rem;
    line-height: 1.45;
    color: var(--fg-2);
}

/* ── Loading spinner ──────────────────────────────────── */

.spinner {
    position: relative;
    width: 2.6rem;
    height: 1.5rem;
}

.spinner {
    --windbar: var(--brand);
}

.spinner::before,
.spinner::after {
    content: "";
    position: absolute;
    left: 0;
    height: 4px;
    border-radius: 999px;
    background: var(--windbar);
    animation: wind-drift 1.1s ease-in-out infinite;
}

.spinner::before {
    top: 0;
    width: 100%;
}

.spinner::after {
    top: 10px;
    width: 70%;
    left: 8%;
    opacity: 0.7;
    animation-delay: 0.18s;
}

@keyframes wind-drift {
    0%, 100% { transform: translateX(0); opacity: 0.35; }
    50%      { transform: translateX(-7px); opacity: 1; }
}

.board-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.25rem;
    min-height: calc(100vh - 64px);
    color: var(--fg-3);
    font-size: 0.93rem;
}

/* ── Empty states ─────────────────────────────────────── */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: calc(100vh - 64px);
    padding: 2rem;
    text-align: center;
}

.empty-state-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: var(--brand-glow);
    color: var(--brand);
    margin-bottom: 1.25rem;
}

.empty-state-icon svg {
    width: 32px;
    height: 32px;
    stroke-width: 1.75;
}

.empty-state-title {
    margin: 0 0 0.5rem;
    font-family: var(--font-display);
    font-size: 1.3rem;
    font-weight: 700;
    color: var(--fg-2);
    letter-spacing: -0.02em;
}

.empty-state-hint {
    margin: 0;
    font-size: 0.93rem;
    color: var(--fg-3);
    max-width: 360px;
    line-height: 1.6;
}

/* ── Focus-visible ────────────────────────────────────── */
/* One ring for everything — see the generic rule near the end of the file. */

/* ── Toast notifications ──────────────────────────────── */

.toast-container {
    position: fixed;
    bottom: 1.75rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 2000;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    align-items: center;
    pointer-events: none;
}

.toast {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    background: var(--toast-bg);
    color: var(--toast-fg);
    padding: 0.75rem 1.35rem 0.75rem 1.1rem;
    border-radius: var(--radius-pill);
    font-size: 0.9rem;
    font-weight: 600;
    box-shadow: var(--shadow-lg);
    border: 1px solid rgba(255, 255, 255, 0.06);
    animation: toastIn 380ms cubic-bezier(0.34, 1.56, 0.64, 1);
    pointer-events: auto;
    white-space: nowrap;
}

.toast-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: 0 0 8px;
    box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.16);
}

.toast-success .toast-dot {
    background: var(--success);
    box-shadow: 0 0 0 3px rgba(54, 211, 153, 0.18);
}

.toast-danger .toast-dot {
    background: #f87171;
    box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.18);
}

.toast-exit {
    animation: toastOut 200ms var(--ease) forwards;
}

@keyframes toastIn {
    from { opacity: 0; transform: translateY(16px) scale(0.92); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes toastOut {
    from { opacity: 1; transform: translateY(0) scale(1); }
    to   { opacity: 0; transform: translateY(-10px) scale(0.92); }
}

/* ── Drag state: disable scroll-snap during drag ──────── */

.board-dragging {
    scroll-snap-type: none !important;
}

/* ── Board pagination dots (phone only) ──────────────── */

.board-pagination {
    display: none;
}

.board-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    border: none;
    padding: 0;
    background: var(--fg-3);
    opacity: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    box-sizing: border-box;
    transition: background 240ms var(--ease-out),
                width 320ms var(--ease-out),
                height 320ms var(--ease-out),
                flex 320ms var(--ease-out),
                min-height 320ms var(--ease-out),
                border-radius 320ms var(--ease-out),
                padding 240ms var(--ease-out);
}

.board-dot::before {
    content: attr(data-label);
    display: block;
    max-width: 0;
    max-height: 0;
    opacity: 0;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
    transition: opacity 200ms var(--ease-out) 80ms,
                max-width 320ms var(--ease-out),
                max-height 320ms var(--ease-out);
}

.board-dot-active {
    background: var(--brand);
    width: 24px;
    border-radius: 4px;
}

/* ══════════════════════════════════════════════════════════
   RESPONSIVE — Phone (max-width: 599px)
   ══════════════════════════════════════════════════════════ */

@media (max-width: 599px) {

    .board-header { padding: 0 0.75rem; height: 56px; }

    .board-user-handle { display: none; }

    .nav-admin-links { display: none; }

    .nav-account { padding: 0.2rem; gap: 0; }

        .nav-menu {
        position: fixed;
        top: calc(56px + 0.5rem);
        right: 0.75rem;
        left: 0.75rem;
        min-width: 0;
        max-height: calc(100dvh - 56px - 1rem);
        overflow-y: auto;
    }

    .nav-menu-mobile-links {
        display: flex;
        flex-direction: column;
        gap: 0.15rem;
        padding-bottom: 0.25rem;
        border-bottom: 1px solid var(--border-1);
        margin-bottom: 0.25rem;
    }

    .nav-menu-mobile-links .board-nav-link {
        color: var(--fg);
        font-size: 0.95rem;
        font-weight: 500;
        min-height: 44px;
        display: flex;
        align-items: center;
        padding: 0.5rem 1rem;
        width: 100%;
        text-align: left;
        box-sizing: border-box;
        border-radius: var(--radius-xs);
    }

    .nav-menu-mobile-links .board-nav-link:hover {
        background: var(--brand-glow);
        color: var(--brand);
    }

    /* ── Board: single column with scroll-snap ── */

    .board {
        padding: 0.75rem;
        gap: 0.75rem;
        scroll-snap-type: x mandatory;
        scroll-padding-inline: 0.75rem;
        -webkit-overflow-scrolling: touch;
        /* Phone header is 56px, not the desktop 64px. */
        height: calc(100vh - 56px);
        height: calc(100dvh - 56px);
    }

    .column {
        width: calc(100vw - 1.5rem);
        min-width: calc(100vw - 1.5rem);
        flex: 0 0 auto;
        scroll-snap-align: start;
        max-height: calc(100dvh - 56px - 3rem);
    }

    /* Thumb-sized, and the panel widens to the column it hangs in: a phone
       column is most of the viewport, so there is no reason to keep the
       desktop panel's width. */
    .column-filter-toggle {
        width: 2.25rem;
        height: 2.25rem;
    }

    .column-filter-toggle svg {
        width: 1.05rem;
        height: 1.05rem;
    }

    .column-filter-panel {
        width: 17rem;
    }

    .board-pagination {
        display: flex;
        position: fixed;
        bottom: calc(0.6rem + env(safe-area-inset-bottom, 0px));
        left: 50%;
        transform: translateX(-50%);
        gap: 0.4rem;
        padding: 0.5rem 0.75rem;
        background: var(--bg-1);
        border: 1px solid var(--border-1);
        border-radius: 999px;
        box-shadow: var(--shadow-md);
        z-index: 20;
        align-items: center;
        transition: left 320ms var(--ease-out),
                    right 320ms var(--ease-out),
                    transform 320ms var(--ease-out),
                    padding 320ms var(--ease-out),
                    gap 320ms var(--ease-out),
                    border-radius 320ms var(--ease-out),
                    box-shadow 320ms var(--ease-out);
    }

    /* Expanded drop-tile bar: activated while a card is being dragged.
       Each dot morphs into a full drop target with the column name visible,
       so the user can release the card on any column without first scrolling
       to reach it. */
    .board-dragging ~ .board-pagination,
    body:has(.board-dragging) .board-pagination {
        left: 0.6rem;
        right: 0.6rem;
        transform: none;
        gap: 0.5rem;
        padding: 0.6rem;
        border-radius: var(--radius);
        box-shadow: var(--shadow-lg);
    }

    .board-dragging ~ .board-pagination .board-dot,
    body:has(.board-dragging) .board-pagination .board-dot {
        flex: 1 1 0;
        width: auto;
        min-height: 64px;
        height: auto;
        border-radius: var(--radius-sm);
        background: var(--bg-2);
        border: 2px dashed var(--border-2);
        padding: 0.5rem 0.6rem;
    }

    .board-dragging ~ .board-pagination .board-dot-active,
    body:has(.board-dragging) .board-pagination .board-dot-active {
        background: var(--bg-2);
        border-color: var(--brand);
    }

    .board-dragging ~ .board-pagination .board-dot::before,
    body:has(.board-dragging) .board-pagination .board-dot::before {
        max-width: 100%;
        max-height: 2.4em;
        opacity: 1;
    }

    /* Highlight the tile the card is hovering over. SortableJS inserts a
       `.card-ghost`-classed clone into whichever drop target the user is
       currently dragging over. */
    .board-pagination .board-dot:has(.card-ghost) {
        background: var(--brand-glow) !important;
        border-color: var(--brand) !important;
        transform: translateY(-2px);
    }

    /* board-pagination.js.onAdd moves the dropped card back to its
       source column before Blazor re-renders, so the .card normally
       never sits inside a .board-dot. This rule is a belt-and-braces
       hide in case any code path leaves one stranded there. */
    .board-pagination .board-dot .card {
        display: none;
    }

    /* ── Modal: full screen slide-up ── */

    .modal-backdrop {
        padding: 0;
        align-items: flex-end;
        /* No blur on a phone. `backdrop-filter` re-filters the whole viewport —
           at a phone's pixel ratio that is millions of pixels — and it has to do
           it while the sheet below slides up over it, so the entrance dropped
           frames on exactly the devices with the least GPU to spare. A sheet
           that covers most of the screen leaves almost nothing to blur anyway,
           so the scrim just darkens a little further to keep the separation. */
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(25, 21, 19, 0.58);
    }

    .modal {
        width: 100%;
        max-height: 100vh;
        border-radius: var(--radius) var(--radius) 0 0;
        animation: slideUpMobile 300ms var(--ease-out);
        /* A full-height sheet is a big layer to rasterize; asking for it before
           the first frame keeps the slide on the compositor from the start. */
        will-change: transform;
    }

    .modal-body {
        padding: 1.15rem;
    }

    .modal-header {
        padding: 1rem 1.15rem;
    }

    .composer {
        padding: 0.9rem;
        gap: 0.85rem;
    }

    .composer-title {
        font-size: 1.2rem;
    }

    /* 16px keeps iOS from zooming the viewport on focus; the panel shrinks
       with the field so the preview keeps standing on the same footprint. */
    .composer-body {
        font-size: 16px;
    }

    .composer-panel,
    .composer-body,
    .composer-preview,
    .composer-empty {
        min-height: 9.5rem;
    }

    .segmented-tab {
        min-height: 36px;
        padding: 0.3rem 0.8rem;
    }

    .detail-panel {
        padding: 0.85rem 0.9rem 0.9rem;
    }

    .modal-footer {
        padding: 0.9rem 1.15rem;
        padding-bottom: calc(0.9rem + env(safe-area-inset-bottom, 0px));
        flex-wrap: wrap;
        gap: 0.5rem;
    }

    .modal-footer button,
    .modal-footer .btn-primary,
    .modal-footer .btn-danger {
        min-height: 48px;
        padding: 0.65rem 1.15rem;
    }

    .modal-close {
        width: 44px;
        height: 44px;
    }

    .field input,
    .field textarea,
    .field select {
        font-size: 16px;  /* prevents iOS zoom on focus */
        min-height: 48px;
        padding: 0.7rem 0.9rem;
    }

    /* ── Admin pages ── */

    .admin-page {
        padding: 1rem;
    }

    .admin-page-title {
        padding: 0.85rem 1rem 0;
    }

    .admin-section {
        padding: 1.25rem;
    }

    .admin-form {
        flex-direction: column;
        align-items: stretch;
    }

    .admin-form input[type="text"],
    .admin-form input[type="color"],
    .admin-form select,
    .admin-form button {
        min-height: 48px;
        font-size: 16px;
    }

    /* Tables collapse to stacked cards on phone. Each row becomes a bordered
       block with `data-label` captions rendered as ::before so the column
       meaning is preserved without the header row. */
    .admin-table,
    .admin-table thead,
    .admin-table tbody,
    .admin-table tr,
    .admin-table td {
        display: block;
        width: 100%;
        min-width: 0;
    }

    .admin-table thead {
        display: none;
    }

    .admin-table tbody tr {
        border: 1px solid var(--border-1);
        border-radius: var(--radius);
        padding: 0.75rem 0.9rem;
        margin-bottom: 0.75rem;
        background: var(--bg-1);
    }

    .admin-table tbody tr:hover {
        background: var(--bg-1);
    }

    .admin-table tbody tr:last-child {
        margin-bottom: 0;
    }

    .admin-table tbody td {
        border: none;
        padding: 0.35rem 0;
        display: flex;
        flex-direction: column;
        gap: 0.25rem;
        word-break: normal;
        overflow-wrap: anywhere;
    }

    .admin-table tbody td::before {
        content: attr(data-label);
        font-size: 0.7rem;
        font-weight: 600;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--fg-3);
    }

    .admin-table tbody td:empty,
    .admin-table tbody td[data-label=""]:not(.admin-row-actions) {
        display: none;
    }

    .admin-table .admin-did code {
        font-size: 0.75rem;
        word-break: break-all;
        white-space: normal;
    }

    .admin-table .admin-row-actions {
        flex-direction: row;
        flex-wrap: wrap;
        gap: 0.5rem;
        padding-top: 0.6rem;
        margin-top: 0.35rem;
        border-top: 1px solid var(--border-1);
    }

    .admin-table .admin-row-actions::before {
        display: none;
    }

    .admin-table .admin-row-actions button {
        flex: 1 1 auto;
        min-height: 44px;
    }

    .admin-table td input[type="text"] {
        width: 100%;
        min-height: 44px;
    }

    .admin-sortable-item {
        flex-wrap: wrap;
    }

    .admin-sortable-input {
        min-width: 0;
        flex: 1 1 100%;
        order: 1;
        margin-top: 0.35rem;
    }

    .admin-row-actions {
        order: 2;
        width: 100%;
        margin-top: 0.45rem;
    }

    .admin-row-actions button {
        min-height: 48px;
        flex: 1;
    }

    /* The request card is the same shape on every width — only the footer
       splits, because the phone rule above gives the buttons the full row. */
    .request-card {
        padding: 0.9rem 1rem;
    }

    .request-foot {
        flex-wrap: wrap;
    }

    .request-message {
        max-width: none;
    }

    .member-preview-card {
        flex-direction: column;
        align-items: center;
        text-align: center;
    }

    .member-preview-actions {
        flex-direction: column;
        width: 100%;
    }

    .member-preview-actions select,
    .member-preview-actions button {
        width: 100%;
        min-height: 48px;
    }

    /* ── Touch targets ── */

    .column-add {
        min-height: 48px;
        padding: 0.8rem;
    }

    .card {
        padding: 1.1rem 1.15rem;
    }

    /* Only the pickable chips in the card modal are touch targets. The static
       chips on a card are labels, not controls — sizing them to 34px made a
       one-word tag the loudest thing on the card. */
    .tag-chip:not(.tag-chip-static) {
        padding: 0.3rem 0.7rem;
        min-height: 34px;
        font-size: 0.8rem;
    }

    .assignee-chip {
        min-height: 44px;
        padding: 0.35rem 0.7rem 0.35rem 0.35rem;
    }

    /* Touch-target sizing for text buttons inside .board-user — the
       icon-only round buttons (.nav-hamburger) are
       excluded so the 44px min-height doesn't stretch them into ovals. */
    .board-user button:not(.nav-hamburger) {
        min-height: 44px;
        padding: 0.55rem 1rem;
    }

    .nav-hamburger {
        width: 44px;
        height: 44px;
        padding: 0;
    }

    .comment-form textarea {
        font-size: 16px;
        min-height: 48px;
    }

    .comment-form .btn-primary {
        min-height: 48px;
    }

    /* ── Toast ── */

    .toast-container {
        bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
        left: 1rem;
        right: 1rem;
        transform: none;
    }

    .toast {
        width: 100%;
        text-align: center;
    }

    /* ── Login page ── */

    .login-page {
        padding: 1rem;
    }
}

@keyframes slideUpMobile {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
}

/* ══════════════════════════════════════════════════════════
   RESPONSIVE — Tablet (600px – 959px)
   ══════════════════════════════════════════════════════════ */

@media (min-width: 600px) and (max-width: 959px) {

    .nav-menu-mobile-links { display: none; }

    .board {
        padding: 1.15rem;
        gap: 1rem;
    }

    .column {
        width: calc(50vw - 1.75rem);
        min-width: calc(50vw - 1.75rem);
    }
}

/* ══════════════════════════════════════════════════════════
   RESPONSIVE — Desktop (960px and up)
   ══════════════════════════════════════════════════════════ */

@media (min-width: 960px) {

    /* Share the spare width out instead of leaving it beside the board — at
       320px fixed, three columns on a 1080p screen left more empty page than
       board. The floor keeps a crowded board scrolling horizontally rather
       than squeezing its columns; the ceiling stops at roughly a 70-character
       description line, past which the text stops being comfortable to read. */
    .column {
        flex: 1 1 320px;
        width: auto;
        min-width: 320px;
        max-width: 520px;
    }
}

/* ── Signup / local login / invites ───────────────────── */

.signup-handle-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.signup-handle-row input {
    flex: 1;
    min-width: 0;
}

.signup-handle-domain {
    color: var(--fg-2);
    font-size: 0.88rem;
    font-weight: 600;
    white-space: nowrap;
}

.login-links {
    margin-top: 1.2rem;
    padding-top: 1.05rem;
    border-top: 1px solid var(--border-1);
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.login-links .login-alt {
    margin: 0;
}

.login-alt {
    margin: 1.5rem 0 0;
    font-size: 0.88rem;
    color: var(--fg-2);
}

.login-alt-link {
    color: var(--brand-bright);
    font-weight: 600;
    text-decoration: none;
}

.login-alt-link:hover {
    text-decoration: underline;
}

.invite-created {
    margin-top: 1rem;
    padding: 1rem 1.25rem;
    border: 1px solid var(--border-1);
    border-radius: var(--radius-sm);
    background: var(--bg-2);
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.invite-created p {
    margin: 0;
    font-size: 0.88rem;
    color: var(--fg-2);
}

.invite-code {
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: 0.02em;
}

.invite-link {
    width: 100%;
    font-size: 0.82rem;
    color: var(--fg-2);
}

.invite-status {
    font-size: 0.8rem;
    font-weight: 600;
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
}

.invite-status-active {
    background: color-mix(in srgb, var(--success) 18%, transparent);
    color: var(--success);
}

.invite-status-used {
    background: color-mix(in srgb, var(--fg-2) 14%, transparent);
    color: var(--fg-2);
}

.invite-status-disabled {
    background: color-mix(in srgb, var(--danger) 16%, transparent);
    color: var(--danger);
}

/* ── Access requests ──────────────────────────────────── */

.hp-field {
    position: absolute !important;
    left: -9999px !important;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.request-keep-link {
    font-size: 0.85rem;
    color: var(--fg-2);
}

/* A request is a decision unit, not a row: five short fields and a message of
   up to 2000 characters. As a table column that message lost every time —
   `overflow-wrap: anywhere` puts its min-content width at one character, so
   the auto layout starved it to a ribbon while the fields either side took
   what they wanted. As a card it gets the width, and the phone layout stops
   being a separate stacked-table rendering of the same thing. */
.request-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.request-card {
    background: var(--bg-2);
    border: 1px solid var(--border-1);
    border-radius: var(--radius-sm);
    padding: 1rem 1.15rem;
    display: flex;
    flex-direction: column;
    gap: 0.8rem;
}

.request-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem 0.9rem;
}

.request-who {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.15rem 0.6rem;
    min-width: 0;
}

.request-handle {
    font-size: 0.85rem;
    overflow-wrap: anywhere;
}

.request-name {
    font-weight: 600;
    font-size: 0.95rem;
}

/* Sits with the handle rather than with the name: it is an identifier, and it
   answers the same question — where this person can be reached. */
.request-email {
    font-size: 0.85rem;
    color: var(--fg-3);
    overflow-wrap: anywhere;
}

/* Not a warning. It only says the code will need carrying by hand. */
.request-email-none {
    font-style: italic;
}

.request-badges {
    display: flex;
    align-items: center;
    gap: 0.65rem;
}

/* Which kind it is decides what approving *does* (allowlist vs. mint an
   invite), so it stays legible — but the pill next to it is the state, and
   two pills would fight. */
.request-kind {
    font-size: 0.74rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--fg-3);
    white-space: nowrap;
}

/* Someone else's words, so they read as quoted — and capped at a line length
   that can be read, rather than run to the full width of the panel. */
.request-message {
    margin: 0;
    padding-left: 0.9rem;
    border-left: 2px solid var(--border-2);
    max-width: 62ch;
    font-size: 0.93rem;
    line-height: 1.6;
    color: var(--fg-2);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.request-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.9rem;
}

.request-date {
    font-size: 0.82rem;
    color: var(--fg-3);
}

/* ── Auth tabs (sign in / create account switcher) ───── */

.auth-tabs {
    display: flex;
    background: var(--bg-2);
    border: 1px solid var(--border-1);
    border-radius: var(--radius-pill);
    padding: 4px;
    margin: 0 0 1.15rem;
    gap: 4px;
}

.auth-tab {
    flex: 1;
    text-align: center;
    padding: 0.55rem 0.75rem;
    border-radius: var(--radius-pill);
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--fg-2);
    text-decoration: none;
    transition: all var(--duration) var(--ease);
}

.auth-tab:hover {
    color: var(--fg);
}

.auth-tab-active {
    background: var(--brand);
    color: var(--brand-contrast);
    box-shadow: var(--shadow-sm);
}

.auth-tab-active:hover {
    color: var(--brand-contrast);
}

/* ── Auth page polish ────────────────────────────────── */

/* The OAuth login form is a centered composition — its hint text was the
   only left-aligned element on the card. */
.atproto-login-form .atproto-login-hint {
    display: block;
    text-align: center;
    margin-top: 0.6rem;
    line-height: 1.55;
}

/* The "you probably already have an account" note, above the form on the two
   pages that hand out a new one. Brand-tinted rather than red: it offers a
   better path, it doesn't report a mistake. Left-aligned inside a card that
   centres everything else, because it is a sentence to read rather than a
   heading to glance at — and that misalignment is also what stops it reading
   as more intro copy. */
.auth-note {
    display: flex;
    gap: 0.7rem;
    align-items: flex-start;
    text-align: left;
    margin: 0 0 1.35rem;
    padding: 0.85rem 0.95rem;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-sm);
    background: var(--brand-glow);
}

.auth-note-icon {
    flex-shrink: 0;
    margin-top: 0.1rem;
    color: var(--brand-bright);
}

.auth-note-body {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    min-width: 0;
}

.auth-note-title {
    margin: 0;
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--fg);
}

.auth-note-text {
    margin: 0;
    font-size: 0.86rem;
    line-height: 1.5;
    color: var(--fg-2);
}

.auth-note .login-alt-link {
    margin-top: 0.15rem;
    font-size: 0.86rem;
}

/* Signup form: numbered step groups so the field list has visual guidance. */
.form-step {
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    border: none;
    margin: 0;
    padding: 0.95rem 0 0;
    border-top: 1px solid var(--border-1);
}

.form-step:first-of-type {
    border-top: none;
    padding-top: 0;
}

.form-step-title {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    margin: 0;
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--fg);
    font-family: var(--font-body);
}

.form-step-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--brand-glow);
    color: var(--brand-bright);
    font-size: 0.85rem;
    font-weight: 700;
}

/* Auth page switch animation. Plays on first load AND on every
   Sign in ↔ Create account ↔ request switch, but exactly once each time —
   js/auth-enter.js drives the data-auth state machine that gates it, because
   the card's nodes get created more than once per load and a plain
   `.login-card { animation }` replayed on every one of them. See that file. */
@keyframes auth-card-in {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
}

/* Before the entrance: hidden, so the prerendered card Blazor is about to throw
   away is never seen animating. Conditional on `js` so that without JavaScript
   there is no state machine and no hidden card — just a visible, unanimated one.
   data-auth="done" matches nothing here, which is what leaves the card visible
   with no animation for any later node churn to restart. */
html.js[data-auth="pending"] .login-card,
html.js[data-auth="pending"] .login-card > * {
    opacity: 0;
}

html[data-auth="playing"] .login-card {
    animation: auth-card-in 0.28s var(--ease) both;
}

@media (prefers-reduced-motion: reduce) {
    html[data-auth="playing"] .login-card {
        animation: none;
    }
}

/* Section heading with an inline action (e.g. Invites + Create button). */
.admin-section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.admin-section-header h2 {
    margin: 0;
}

/* Link-style toggle revealing resolved requests / spent invites. */
.admin-history-toggle {
    align-self: flex-start;
    margin-top: 0.75rem;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--brand-bright);
}

.admin-history-toggle:hover {
    text-decoration: underline;
}

/* ── Brand mark (Ponente west-wind squircle) ─────────── */

.brand-mark {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    display: block;
}

.board-title-link {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
}

.login-card .brand-mark-lg {
    width: 48px;
    height: 48px;
    margin: 0 auto 0.7rem;
}

/* ── Ponente polish pass: atmosphere, wind details, focus ─ */

/* Empty columns get a little wind doodle, idly drifting. */
.column-empty::before {
    content: "";
    display: block;
    width: 44px;
    height: 24px;
    margin: 0 auto 0.8rem;
    background: currentColor;
    opacity: 0.55;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 44 24'%3E%3Cg fill='none' stroke='black' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M40 7 H12 a3.5 3.5 0 1 0 3.5 -6'/%3E%3Cpath d='M34 16 H6'/%3E%3Cpath d='M28 22 H14'/%3E%3C/g%3E%3C/svg%3E") no-repeat center / contain;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 44 24'%3E%3Cg fill='none' stroke='black' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M40 7 H12 a3.5 3.5 0 1 0 3.5 -6'/%3E%3Cpath d='M34 16 H6'/%3E%3Cpath d='M28 22 H14'/%3E%3C/g%3E%3C/svg%3E") no-repeat center / contain;
    animation: idle-drift 4s ease-in-out infinite alternate;
}

@keyframes idle-drift {
    from { transform: translateX(4px); }
    to   { transform: translateX(-4px); }
}

/* The big auth-card mark breathes: streamlines sway in the wind. */
.brand-mark .brand-wind path {
    animation: wind-sway 3.4s ease-in-out infinite alternate;
    transform-box: fill-box;
}

.brand-mark .brand-wind path:nth-child(2) { animation-delay: 0.5s; }
.brand-mark .brand-wind path:nth-child(3) { animation-delay: 1s; }

@keyframes wind-sway {
    from { transform: translateX(1.6px); }
    to   { transform: translateX(-1.6px); }
}

/* Keyboard focus: one consistent brand ring for interactive elements.
   No border-radius here — it would reshape pill and circle controls. */
button:focus-visible,
a:focus-visible,
[role="button"]:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .column-empty::before,
    .brand-mark .brand-wind path,
    .spinner::before,
    .spinner::after {
        animation: none;
    }
}

/* ── Second design pass: texture, entrances, wind language ─ */

/* Fine grain over everything — kills the flat-gradient feel. Fixed and
   non-interactive; barely-there in light, a touch more present in dark. */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 9000;
    pointer-events: none;
    opacity: 0.022;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

[data-theme="dark"] body::before {
    opacity: 0.04;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) body::before {
        opacity: 0.04;
    }
}

/* Column headers carry the wind: a hairline trailing off westward,
   waking up on hover. */
.column-header {
    position: relative;
}

.column-header::after {
    content: "";
    position: absolute;
    left: 1.15rem;
    right: 1.15rem;
    bottom: 0;
    height: 2px;
    border-radius: 999px;
    background: linear-gradient(270deg, var(--brand) 0%, transparent 90%);
    opacity: 0.35;
    transition: opacity var(--duration) var(--ease);
}

.column:hover .column-header::after {
    opacity: 0.8;
}

/* Orchestrated entrances: columns rise in sequence when the board mounts;
   the auth card's contents follow its entrance in a quick cascade. */
@keyframes rise-in {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}

/* The columns are animated from JS, not by a rule keyed to element creation.
   That keying is what made this undeployable: the board is built twice on every
   load — the prerender paints columns, the circuit's first render replaces them
   with Board.razor's spinner, and the data builds them again — so a CSS
   animation fires on whichever sets happen to exist, and every attempt to gate
   it was a timer racing that churn. Board.razor calls the entrance itself, from
   the render where its data has actually arrived, and js/auth-enter.js animates
   exactly those elements once. Nothing created afterwards can animate, because
   there is no rule left to fire.

   All this does is keep the columns from being seen before that call. Scoped to
   `js` so a visitor without JavaScript gets a plain visible board, and dropped
   the moment the entrance runs. The timings live here so CSS stays the source of
   truth; the JS reads them back off :root. */
:root {
    --entrance-duration: 450ms;
    --entrance-lead: 30ms;
    --entrance-stagger: 60ms;
    --entrance-rise: 14px;
}

html.js:not([data-board-entered]) .board > .column {
    opacity: 0;
}


/* The card's contents cascade in behind it — gated on the same state as the
   card itself, so the two halves of the entrance can never get out of step. */
html[data-auth="playing"] .login-card > * {
    animation: rise-in 0.4s var(--ease-out) both;
}

html[data-auth="playing"] .login-card > :nth-child(1) { animation-delay: 0.05s; }
html[data-auth="playing"] .login-card > :nth-child(2) { animation-delay: 0.1s; }
html[data-auth="playing"] .login-card > :nth-child(3) { animation-delay: 0.15s; }
html[data-auth="playing"] .login-card > :nth-child(4) { animation-delay: 0.2s; }
html[data-auth="playing"] .login-card > :nth-child(n+5) { animation-delay: 0.25s; }

@media (prefers-reduced-motion: reduce) {
    html[data-auth="playing"] .login-card > * {
        animation: none;
    }
}
