
/* 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 colour is defined ONCE here as light-dark(light, dark), steered by
   color-scheme below. Never add a per-theme token block: the old ones drifted. */
:root {
    --font-display: 'Bricolage Grotesque', 'Georgia', serif;
    --font-body: 'Figtree', 'Segoe UI', system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, monospace;

    --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);
    /* On a --brand fill. Dark brand is a light lavender, so it takes ink, not white. */
    --brand-contrast: light-dark(#ffffff, #221733);

    /* Surfaces climb with elevation: page (bg-0) < column (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);
    --danger: light-dark(#fc2626, #f87171);
    --danger-bg: light-dark(#fef2f2, #2d1515);
    /* On a --danger fill; dark danger is a light red. */
    --danger-contrast: light-dark(#ffffff, #2d0f0f);

    /* Dark in both themes, but raised 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));

    /* Casts upwards only: a phone sheet rests on the nav bar, and a downward half
       would read as the bar tucked under it. */
    --shadow-sheet: 0 -14px 40px light-dark(rgba(44, 36, 24, 0.13), rgba(0, 0, 0, 0.45)), 0 -2px 8px light-dark(rgba(44, 36, 24, 0.06), rgba(0, 0, 0, 0.3));

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

    /* The column's one inset: header label, hairline, cards and add button all
       start on it, or the column reads as having two left edges. */
    --column-rail: 1.15rem;

    /* The column's second line: where the words inside a card start. */
    --card-inset: 1.15rem;

    /* The fixed bottom nav bar on a phone, safe area included; everything that
       stops at the bar subtracts this. */
    --phone-nav-h: calc(56px + env(safe-area-inset-bottom, 0px));

    /* .board-phone-bar, subtracted like the nav bar. */
    --phone-board-title-h: 56px;

    /* The strip the fixed pagination pill stands in: the collapsed pill's height
       (8px dot, 0.5rem padding, 1px border, twice) plus one gap each side, so the
       space above and below it is equal. mobile.spec.js measures both gaps. */
    --phone-pager-h: 1.625rem;
    --phone-pager-gap: 0.3125rem;

    color-scheme: light dark;
}

/* A pinned choice. UA parts (scrollbars, controls, dialog backdrops) follow too. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

/* Set for one frame by App.razor's theme switch, so the swap is one repaint
   rather than hundreds of transitions. Not a general "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;
}

/* The browser's own ticks (a hand-drawn one needs telling assistive tech about
   twice), in the app's colour and size everywhere. */
input[type="checkbox"],
input[type="radio"] {
    accent-color: var(--brand);
    width: 1.05rem;
    height: 1.05rem;
    flex: 0 0 auto;
    cursor: pointer;
}

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

/* ── 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;
    /* Some are <a>, and there is no global reset on `a`. */
    text-decoration: none;
}

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

/* No default style for buttons in .board-user on purpose: each control styles
   itself, so nothing has to out-specify anything. */

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

/* Repeats .admin-page's width and padding (desktop and phone), because the
   sticky header sits between the two blocks. */
.admin-page-title {
    max-width: 880px;
    margin: 0 auto;
    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-phone-bar { display: none; }

.board {
    display: flex;
    /* Stretch, so the emptiest column still offers a full-height drop target. */
    align-items: stretch;
    /* `safe`: a plain center on an overflowing scroll container pushes the
       first column out of reach. */
    justify-content: safe center;
    gap: 1.25rem;
    padding: 1.5rem;
    overflow-x: auto;
    overflow-y: hidden;
    /* Definite height: columns scroll internally, the page never does. */
    height: calc(100vh - 64px);
    height: calc(100dvh - 64px);
    scroll-behavior: smooth;
    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;
    /* Fills the board's content box, padding and scrollbar included. */
    max-height: 100%;
    box-shadow: none;
    overflow: hidden;
}

.column-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem var(--column-rail);
    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;
}

/* Not positioned: the filter panel anchors on .column-header, so its
   percentage width resolves against the column. */
.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, not what the column holds. */
.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 outlives the flyout, so the button carries the state. */
.column-filter-toggle-active,
.column-filter-toggle-active:hover {
    background: var(--brand-glow);
    color: var(--brand);
}

/* Invisible click-catcher. It covers the button that opened it, so that button
   is lifted over it, or it could not close the panel. */
.column-filter-backdrop {
    position: fixed;
    inset: 0;
    z-index: 190;
    background: transparent;
}

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

/* Sized to fit inside the column, which clips at its rounded edges; the tag
   list scrolls rather than growing. */
.column-filter-panel {
    position: absolute;
    top: calc(100% + 0.55rem);
    right: var(--column-rail);
    z-index: 200;
    width: 15rem;
    max-width: calc(100% - 2 * var(--column-rail));
    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);
}

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

/* Inline padding is the rail, so card edges line up with the header label. */
.column-cards {
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    padding: 0.5rem var(--column-rail) 0.75rem;
    flex: 1;
    gap: 0.7rem;
    scrollbar-width: thin;
    scrollbar-color: var(--border-2) transparent;
}

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

/* A card in here is one being dragged in. display:none, not opacity: only then
   does SortableJS's lastChild() skip the placeholder; otherwise the drop lands
   after it, at the bottom of the column. */
.column-cards:has(.card) .column-empty {
    display: none;
}

/* ── Cards ── */

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

/* Hover lift only for a real pointer: on touch, :hover sticks to the tapped card
   and its shadow repainted the board under the dialog's entrance. Touch gets a
   press on `scale` instead. Must stay before .card-chosen. */
@media (hover: hover) {
    .card:hover {
        box-shadow: var(--shadow-card-hover);
        transform: translateY(-3px);
    }

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

@media (hover: none) {
    .card:active {
        scale: 0.98;
    }
}

/* Baseline keeps the due pill on the title's first line when it wraps. */
.card-title-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

.card-title-row .card-due { flex: 0 0 auto; }

.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. */
    overflow-wrap: anywhere;
    /* Lets a long word wrap inside the flex row. */
    min-width: 0;
}

/* Clamp on a line boundary; a max-height sliced a line through its letters. */
.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 {
    /* On the column rail. Its padding keeps a 44px touch target and does not shrink. */
    margin: 0.35rem var(--column-rail) 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 ── */

/* The scrim fades on ::before, behind the dialog: an opacity animated on the
   dialog's parent composites the whole dialog offscreen every frame, which was
   what made it sluggish on phones. */
.modal-backdrop {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    padding: 1rem;
}

/* -1 in the backdrop's stacking context: under the dialog, over the page. */
.modal-backdrop::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: rgba(25, 21, 19, 0.45);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    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 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 error block, which ships
   Bootstrap classes we don't have. */
.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 ── */

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

/* An input styled as the heading it becomes. The underline on hover/focus is its
   whole focus indicator. */
.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;
}

/* A title you may read but not change: same type, no field affordances. */
.composer-title-static {
    margin: 0;
    padding: 0.1rem 0 0.25rem;
    border-bottom: 2px solid transparent;
}

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

/* — Board settings tabs — */

.settings-tabs {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 2px;
    margin: 0 0 1.25rem;
    padding: 3px;
    background: var(--bg-1);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-xs);
}

.settings-tab {
    border-radius: var(--radius-pill);
    padding: 0.36rem 1rem;
    font-family: var(--font-body);
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--fg-3);
    text-decoration: none;
    transition: color var(--duration) var(--ease), background var(--duration) var(--ease);
}

.settings-tab:hover:not(.settings-tab-active) {
    color: var(--fg);
}

.settings-tab:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

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

/* — 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);
}

/* Beside the view control, so it drops the auto margin the segmented control
   uses when alone on its row. */
.segmented-scale {
    margin-left: 0;
    background: var(--bg-2);
}

.segmented-scale .segmented-tab {
    padding: 0.28rem 0.65rem;
    font-size: 0.76rem;
}

/* Both tabs share one footprint, so switching never resizes the dialog. */
.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 — */

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

/* ── Sharing ── */

/* No layout of its own; each surface places it. */
.share-button {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    background: none;
    border: none;
    color: var(--fg-3);
    cursor: pointer;
    padding: 0.5rem;
    border-radius: 50%;
    font: inherit;
    font-family: var(--font-body);
    transition: all var(--duration) var(--ease);
}

.share-button:hover:not(:disabled) {
    background: var(--bg-2);
    color: var(--fg);
}

.share-button:disabled {
    opacity: 0.55;
    cursor: default;
}

.share-button svg {
    flex-shrink: 0;
}

/* Beside the close button in the card dialog, so it must not claim the whole
   space-between row. */
.modal-header h2 {
    margin-right: auto;
}

/* In the nav menu it is a row (.nav-menu .nav-menu-row), not an icon button. */
.nav-menu-share {
    justify-content: flex-start;
}

/* ── Link cards ── */

/* Deliberately the board's own card: same surface, radius and shadow. */
.link-card {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    background: var(--bg-1);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-card);
    padding: 0.7rem 0.85rem;
    margin: 0.5rem 0;
    text-decoration: none;
    color: var(--fg);
    transition: box-shadow var(--duration) var(--ease), transform var(--duration) var(--ease);
    /* An anchor in a flex column still has to be told not to stretch its text. */
    align-items: stretch;
}

.link-card:hover {
    box-shadow: var(--shadow-card-hover);
    transform: translateY(-1px);
    text-decoration: none;
}

.link-card-kind {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--fg-3);
    min-width: 0;
}

.link-card-board,
.link-card-column {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.link-card-sep {
    flex-shrink: 0;
    opacity: 0.7;
}

.link-card-title {
    font-family: var(--font-display);
    font-size: 0.98rem;
    font-weight: 600;
    line-height: 1.3;
    color: var(--fg);
}

.link-card-excerpt {
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--fg-2);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.link-card-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-top: 0.15rem;
}

.link-card-count {
    font-size: 0.78rem;
    color: var(--fg-3);
}

/* A link we could not resolve is shown as the plain link. */
.link-card-plain {
    margin: 0.5rem 0;
    word-break: break-word;
}

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

/* ── SortableJS drag states ── */

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

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

/* Picked up, not moved yet (on touch, the end of a 200ms press; see
   sortable-interop.js). `scale`, because :hover and :active own `transform`;
   .card:active is earlier on purpose so a held press ends grown. Not on the
   ghost, which SortableJS leaves chosenClass on. */
.card-chosen:not(.card-ghost) {
    scale: 1.03;
}

/* A touch drag moves a clone by writing `transform` on every touchmove;
   .card's transition lagged each write and SortableJS compounds the lag, so no
   transition here. will-change moved ~26-30% of the drag's paint time off the
   main thread, and the class only exists while a card is in the air. */
.card-dragging {
    box-shadow: var(--shadow-brand), var(--shadow-lg);
    z-index: 10;
    rotate: 2.5deg;
    transition: none;
    will-change: transform;
}

/* ── Tag chips ── */

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

.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);
    /* User-picked colours: inked down on light paper, 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;
}

/* The tick marks a picked tag, since fill alone reads as another colour. Only on
   choices: card chips are labels and get none. */
.tag-picker .tag-chip-selected::before,
.column-filter-tags .tag-chip-selected::before,
.calendar-legend .tag-chip-selected::before,
.assignee-option-on::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,
.calendar-legend .tag-chip-selected::before {
    margin-right: 0.35em;
    vertical-align: -0.05em;
}

.assignee-option-on::after {
    background: var(--brand);
    width: 0.95rem;
    height: 0.95rem;
    margin-left: auto;
}

.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;
    /* A tag-heavy card wraps rather than squeezing out the faces. */
    flex-wrap: wrap;
}

/* Overlapping faces; each avatar's 2px --bg-1 ring keeps them legible. CardView
   caps the stack at three. */
.card-assignees {
    display: flex;
}

.card-assignees .assignee-avatar:not(:first-child) {
    margin-left: -0.45rem;
}

/* Always trails the row, so the badge doesn't change 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);
}

/* currentColor mask, like the wind doodles: identical on every platform. */
.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;
}

/* After the .assignee-avatar base on purpose: same specificity, and the base's
   fill would win from below. */
.assignee-overflow {
    background: var(--brand);
    color: var(--brand-contrast);
}

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

.assignee-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.3rem 0.6rem 0.3rem 0.3rem;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-2);
    background: var(--bg-0);
    color: var(--fg);
    font: inherit;
    font-family: var(--font-body);
    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-x {
    color: var(--fg-3);
    font-size: 1rem;
    line-height: 1;
    transition: color var(--duration) var(--ease);
}

.assignee-chip:hover .assignee-chip-x {
    color: var(--danger);
}

.assignee-chip-static {
    cursor: default;
    background: var(--bg-2);
}

.assignee-chip-static:hover {
    background: var(--bg-2);
    border-color: var(--border-2);
    box-shadow: none;
}

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

/* Dashed: a place a chip could go. Stretched to the chips' height. */
.assignee-add {
    display: inline-flex;
    align-items: center;
    align-self: stretch;
    gap: 0.3rem;
    padding: 0.3rem 0.85rem;
    border-radius: var(--radius-pill);
    border: 1.5px dashed var(--brand);
    background: transparent;
    color: var(--brand);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    transition: background var(--duration) var(--ease);
}

/* A glyph in the markup rather than ::before: the canvas tracer (design/trace)
   reads text from elements. */
.assignee-add-plus {
    font-size: 1rem;
    line-height: 1;
}

.assignee-add:hover,
.assignee-add[aria-expanded="true"] {
    background: var(--brand-glow);
    border-style: solid;
}

/* Anchored on the panel, not the button: the button moves along the row, and a
   menu following it would slide past the dialog's edge. */
.detail-panel-assignees {
    position: relative;
}

/* Invisible click-catcher: a click elsewhere shuts the menu rather than hitting
   what was under it. */
.assignee-menu-catcher {
    position: fixed;
    inset: 0;
    z-index: 20;
}

.assignee-menu {
    position: absolute;
    top: calc(100% - 0.5rem);
    left: 0.5rem;
    right: 0.5rem;
    z-index: 30;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding: 0.5rem;
    background: var(--bg-1);
    border: 1px solid var(--border-1);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-lg);
    animation: column-filter-in var(--duration) var(--ease) both;
}

.assignee-search {
    width: 100%;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.5rem 0.75rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.9rem;
    background: var(--bg-2);
    color: var(--fg);
    transition: all var(--duration) var(--ease);
}

.assignee-search::placeholder { color: var(--fg-3); }

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

.assignee-options {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.assignee-option {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.35rem 0.5rem;
    border: none;
    border-radius: var(--radius-xs);
    background: transparent;
    color: var(--fg);
    font: inherit;
    font-family: var(--font-body);
    text-align: left;
    cursor: pointer;
    transition: background var(--duration) var(--ease);
}

.assignee-option:hover { background: var(--bg-2); }

.assignee-option:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--brand);
}

.assignee-option-on,
.assignee-option-on:hover {
    background: var(--brand-glow);
}

.assignee-option-text {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
}

.assignee-option-name,
.assignee-option-handle {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.assignee-option-name {
    font-size: 0.86rem;
    font-weight: 500;
}

.assignee-option-handle {
    font-size: 0.74rem;
    color: var(--fg-3);
}

.assignee-menu-note {
    margin: 0.15rem 0.5rem 0.1rem;
    font-size: 0.78rem;
    color: var(--fg-3);
}

/* ── Due dates on a card ── */

/* Overdue is the one state that earns colour. */
.card-due {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--fg-2);
    background: var(--bg-2);
    padding: 0.18rem 0.55rem;
    border-radius: var(--radius-pill);
    white-space: nowrap;
    box-shadow: var(--shadow-xs);
}

.card-due::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%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.2' y='3.2' width='11.6' height='10.6' rx='2'/%3E%3Cpath d='M2.2 6.4h11.6M5.6 2v2.4M10.4 2v2.4'/%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 16 16'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.2' y='3.2' width='11.6' height='10.6' rx='2'/%3E%3Cpath d='M2.2 6.4h11.6M5.6 2v2.4M10.4 2v2.4'/%3E%3C/g%3E%3C/svg%3E") no-repeat center / contain;
}

.card-due-overdue {
    color: var(--danger);
    background: var(--danger-bg);
}

/* — The due-date field in the card dialog — */

.due-picker {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.due-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.55rem 0.8rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    background: var(--bg-1);
    color: var(--fg);
    transition: all var(--duration) var(--ease);
}

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

.due-clear {
    border: none;
    background: transparent;
    color: var(--fg-3);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.82rem;
    font-weight: 600;
    padding: 0.3rem 0.5rem;
    border-radius: var(--radius-btn);
    cursor: pointer;
    transition: color var(--duration) var(--ease);
}

.due-clear:hover { color: var(--danger); }

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

/* — Moving a card to another board — */

.move-picker {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.move-select {
    flex: 1 1 8rem;
    min-width: 0;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.55rem 0.8rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    background: var(--bg-1);
    color: var(--fg);
    transition: all var(--duration) var(--ease);
}

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

.move-go {
    border: 1px solid var(--border-2);
    background: var(--bg-1);
    border-radius: var(--radius-pill);
    padding: 0.45rem 1rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--fg-2);
    cursor: pointer;
    transition: all var(--duration) var(--ease);
}

.move-go:hover:not(:disabled) {
    border-color: var(--border-3);
    background: var(--bg-2);
}

.move-go:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.move-note {
    margin: 0;
    font-size: 0.82rem;
    line-height: 1.45;
    color: var(--fg-3);
}

.move-note-drops {
    display: block;
    color: var(--danger);
}

/* ── Calendar (the board read by date) ── */

.calendar-page {
    /* Chip row height; .calendar-day-cards' auto-fill track reads it. */
    --cal-chip-h: 1.4rem;
    --cal-chip-gap: 0.15rem;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1.25rem 1.75rem 1.25rem;
    /* Wider than the app's reading measure, since these views answer by how much
       you see at once; capped so ultrawide day cells stay squares. */
    max-width: 1800px;
    margin: 0 auto;
    /* Fallback; the :has() rule below derives the height instead. */
    height: calc(100vh - 64px);
    height: calc(100dvh - 64px);
    overflow: hidden;
}

/* Height measured, not subtracted: calc(100dvh - 64px) fits with no slack, so a
   browser off by a pixel (Firefox RDM) hid the last week. Body is a flex column
   and the page takes the rest. :has() keeps it off every other page. */
html:has(.calendar-page) { height: 100%; }

body:has(.calendar-page) {
    height: 100%;
    display: flex;
    flex-direction: column;
    /* Every other child is fixed or display:none, so nothing new becomes a flex item. */
    overflow: hidden;
}

body:has(.calendar-page) > .calendar-page {
    height: auto;
    flex: 1;
    min-height: 0;
    /* A flex item's min-width is its content: the timeline pushed the page past the
       viewport and body's overflow clipped the toolbar. */
    min-width: 0;
    width: 100%;
}

/* min-height: 0 is load-bearing, or the grid grows the page. */
.calendar-body {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.calendar-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    /* Allowed to shrink, the toolbar would wrap. */
    flex: 0 0 auto;
}

.calendar-nav {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.calendar-step:disabled {
    opacity: 0.35;
    cursor: default;
    box-shadow: none;
}

.calendar-step:disabled:hover { color: var(--fg-2); }

.calendar-heading {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.4rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--fg);
    /* So the arrows either side of the month name don't shuffle. */
    min-width: 10.5rem;
    text-align: center;
}

.calendar-step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    border: none;
    border-radius: var(--radius-pill);
    background: var(--bg-1);
    color: var(--fg-2);
    cursor: pointer;
    box-shadow: var(--shadow-xs);
    transition: all var(--duration) var(--ease);
}

.calendar-step svg { width: 16px; height: 16px; }

.calendar-step:hover {
    color: var(--brand);
    box-shadow: var(--shadow-sm);
}

.calendar-today,
.calendar-legend-clear {
    border: none;
    background: transparent;
    color: var(--brand);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.84rem;
    font-weight: 600;
    padding: 0.3rem 0.6rem;
    border-radius: var(--radius-btn);
    cursor: pointer;
}

.calendar-today:hover,
.calendar-legend-clear:hover { background: var(--brand-glow); }

.calendar-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* Same look, not a shared class: .calendar-export identifies the download. */
.calendar-export,
.calendar-subscribe {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    text-decoration: none;
    border: none;
    border-radius: var(--radius-pill);
    padding: 0.4rem 0.9rem;
    background: var(--bg-1);
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    /* Otherwise "Export .ics" wraps and heightens the phone toolbar. */
    white-space: nowrap;
    box-shadow: var(--shadow-xs);
    transition: all var(--duration) var(--ease);
}

.calendar-export svg,
.calendar-subscribe svg { width: 15px; height: 15px; flex: 0 0 auto; }

.calendar-export:hover,
.calendar-subscribe:hover {
    color: var(--brand);
    box-shadow: var(--shadow-sm);
}

/* Legend and count on one row; see Calendar.razor. */
.calendar-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex: 0 0 auto;
    min-height: 1.5rem;
}

.calendar-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.45rem;
    min-width: 0;
}

/* Phone only; the legend is inline at other widths. */
.calendar-filter-toggle,
.calendar-filter-backdrop { display: none; }

.calendar-summary {
    margin: 0;
    margin-left: auto;
    font-size: 0.85rem;
    color: var(--fg-3);
    flex: 0 0 auto;
    white-space: nowrap;
}

/* Both classes on purpose: .empty-state is declared later at equal specificity
   and would win min-height and padding. Here the flex parent sets the height. */
.empty-state.calendar-empty {
    flex: 1;
    min-height: 0;
    justify-content: center;
    /* Lifted off geometric centre, since the eye takes the heading for the middle.
       Taken off the bottom (half the difference is the lift; a matching top
       padding would cancel it) and capped in vh. */
    padding: 1rem 2rem min(6rem, 14vh);
}

/* — Month grid — */

/* A grid, not flex: six flex:1 rows round independently and could overflow by
   a pixel, cutting the last week. minmax(0, 1fr) so a full cell can't push its
   row. overflow: clip with a clip margin, not hidden: hidden cut the cells'
   shadow and today's ring, and ignores overflow-clip-margin. */
.calendar-grid {
    display: grid;
    grid-template-rows: auto repeat(var(--weeks, 6), minmax(0, 1fr));
    gap: 0.35rem;
    flex: 1;
    min-height: 0;
    overflow: clip;
    overflow-clip-margin: 12px;
}

.calendar-weekdays,
.calendar-week {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 0.35rem;
}

.calendar-weekdays { flex: 0 0 auto; }

.calendar-week {
    /* Low on purpose: the grid clips, so a higher floor lost the last week (e.g. at
       175% zoom). The cell adapts; this keeps the day number legible. */
    min-height: 2rem;
}

.calendar-weekday {
    padding: 0.2rem 0.5rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--fg-3);
}

.calendar-weekend { color: var(--brand); }

.calendar-day {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    /* Exactly its share of the row; a floor here would fight .calendar-week's. */
    min-height: 0;
    padding: 0.45rem;
    border-radius: var(--radius-sm);
    background: var(--bg-1);
    box-shadow: var(--shadow-card);
    /* Clips rather than stretching the row; .calendar-day-more reaches the rest. */
    overflow: hidden;
}

/* Chips render whole or not at all: auto-fill makes only complete rows, and
   grid-auto-rows: 0 collapses the rest. */
.calendar-day-cards {
    display: grid;
    grid-template-rows: repeat(auto-fill, var(--cal-chip-h));
    grid-auto-rows: 0;
    row-gap: var(--cal-chip-gap);
    align-content: start;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

.calendar-day-cards .cal-card {
    /* No vertical padding: in a collapsed 0px row it drew a sliced bar. */
    min-height: 0;
    height: 100%;
    padding-block: 0;
    display: flex;
    align-items: center;
}

.calendar-day-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.25rem;
    flex: 0 0 auto;
}

/* How many fit changes with the window; the count is true at every height. */
.calendar-day-count {
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1;
    padding: 0.15rem 0.35rem;
    border-radius: var(--radius-pill);
    background: var(--bg-2);
    color: var(--fg-3);
}

.calendar-day-open { cursor: pointer; }

.calendar-day-open:hover {
    box-shadow: var(--shadow-card), 0 0 0 1px var(--border-2);
}

.calendar-day-today.calendar-day-open:hover {
    box-shadow: var(--shadow-card), 0 0 0 2px var(--brand);
}

/* Phones only; see the phone block. */
.calendar-day-dots { display: none; }
.calendar-agenda { display: none; }

/* — One day in full, for the cards the cell could not fit — */

/* Flex centring on the overlay: slideUp animates `transform`, which would
   replace a centring translate on the panel. Below the card dialog's 1000; both
   exist for the frame of the swap. */
.calendar-overlay {
    position: fixed;
    inset: 0;
    background: rgba(25, 21, 19, 0.45);
    z-index: 900;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    animation: fadeIn var(--duration) var(--ease-out);
}

.calendar-day-sheet {
    width: min(380px, 100%);
    max-height: min(70vh, 32rem);
    display: flex;
    flex-direction: column;
    background: var(--bg-1);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    padding: 1rem;
    gap: 0.75rem;
    animation: slideUp var(--duration) var(--ease-out);
}

.calendar-day-sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex: 0 0 auto;
}

.calendar-day-sheet-head h3 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1rem;
    font-weight: 700;
    color: var(--fg);
}

.calendar-day-sheet-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    border: none;
    border-radius: var(--radius-pill);
    background: var(--bg-2);
    color: var(--fg-2);
    cursor: pointer;
    transition: all var(--duration) var(--ease);
}

.calendar-day-sheet-close svg { width: 14px; height: 14px; }
.calendar-day-sheet-close:hover { color: var(--brand); }

/* The one chip list allowed to scroll: nothing in the sheet is hidden. */
.calendar-day-sheet-cards {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-2) transparent;
}

.calendar-day-sheet-cards .cal-card {
    font-size: 0.85rem;
    padding: 0.45rem 0.6rem;
}

/* — The subscription link — */

.calendar-subscribe-panel {
    /* Centred by .calendar-overlay. */
    width: min(460px, 100%);
    max-height: calc(100dvh - 2rem);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    background: var(--bg-1);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    padding: 1.25rem;
    animation: slideUp var(--duration) var(--ease-out);
}

.calendar-subscribe-blurb {
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--fg-2);
}

.calendar-subscribe-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 0.6rem 0 0;
}

.calendar-subscribe-tag {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.2rem 0.6rem;
    border-radius: var(--radius-pill);
    background: var(--bg-2);
    color: var(--fg-2);
    font-size: 0.75rem;
    font-weight: 600;
}

.calendar-subscribe-tag svg {
    width: 13px;
    height: 13px;
    flex: 0 0 auto;
    color: var(--fg-3);
}

.calendar-subscribe-loading {
    display: flex;
    justify-content: center;
    padding: 1.5rem 0;
}

/* A string to copy, not read; the ellipsis keeps the panel one height. */
.calendar-subscribe-url {
    width: 100%;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.75rem;
    padding: 0.55rem 0.65rem;
    border: 1px solid var(--border-1);
    border-radius: var(--radius-sm);
    background: var(--bg-2);
    color: var(--fg-2);
    text-overflow: ellipsis;
}

.calendar-subscribe-url:focus {
    outline: 2px solid var(--brand);
    outline-offset: 1px;
}

.calendar-subscribe-actions {
    display: flex;
    align-items: stretch;
    gap: 0.6rem;
}

/* One size for both, not .btn-primary's; `stretch` on the row evens out the
   link's and the button's line boxes. */
.calendar-subscribe-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    text-decoration: none;
    border-radius: var(--radius-pill);
    padding: 0.5rem 1.1rem;
    background: var(--bg-2);
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.85rem;
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    white-space: nowrap;
    transition: all var(--duration) var(--ease);
}

.calendar-subscribe-action-primary {
    background: var(--brand);
    color: var(--brand-contrast);
}

.calendar-subscribe-action:hover { color: var(--brand); }
.calendar-subscribe-action-primary:hover {
    color: var(--brand-contrast);
    filter: brightness(1.08);
}

/* Not .modal-error's red: nothing has gone wrong. */
.calendar-subscribe-warning {
    margin: 0;
    font-size: 0.78rem;
    line-height: 1.45;
    color: var(--fg-3);
    padding: 0.5rem 0.65rem;
    border-radius: var(--radius-sm);
    background: var(--brand-glow);
}

.calendar-subscribe-regen {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    flex-wrap: wrap;
    border-top: 1px solid var(--border-1);
    padding-top: 0.75rem;
}

.calendar-subscribe-hint {
    font-size: 0.75rem;
    color: var(--fg-3);
}

/* Dimmed, not dropped: a grid with holes stops reading as weeks. */
.calendar-day-outside {
    background: var(--bg-2);
    box-shadow: none;
}

.calendar-day-outside .calendar-daynum { color: var(--fg-3); }

.calendar-day-today {
    box-shadow: var(--shadow-card), 0 0 0 2px var(--brand);
}

.calendar-daynum {
    font-size: 0.78rem;
    font-weight: 700;
    color: var(--fg-2);
}

/* — A card as one line in a day cell — */

.cal-card {
    --tag-color: var(--fg-3);
    display: block;
    width: 100%;
    text-align: left;
    border: none;
    border-left: 3px solid var(--tag-color);
    border-radius: var(--radius-xs);
    background: color-mix(in srgb, var(--tag-color) 12%, transparent);
    color: var(--fg);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.35;
    padding: 0.25rem 0.4rem;
    cursor: pointer;
    /* One line: a wrapping title would push the week's rows apart. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: all var(--duration) var(--ease);
}

.cal-card:hover {
    background: color-mix(in srgb, var(--tag-color) 22%, transparent);
}

.cal-card-overdue {
    border-left-color: var(--danger);
}

/* Only where a chip has a second line to give; day-cell chips are one line. */
.cal-card-board {
    display: block;
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--fg-2);
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The sheet is a list, not a grid, so its chips may wrap. */
.calendar-day-sheet-cards .cal-card { white-space: normal; }

/* — Timeline: one lane per category, days across — */

.calendar-timeline-scroll {
    flex: 1;
    min-height: 0;
    overflow: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-2) transparent;
    padding-bottom: 0.5rem;
}

.calendar-timeline {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    /* Sized to fit the whole range on a desktop; scrolls only when narrower. Never
       width: max-content: with 1fr tracks it sized every column to the widest chip. */
    min-width: calc(var(--label-w) + var(--cols) * var(--col-w));
    --label-w: 9.5rem;
    --col-w: 2.6rem;
}

/* A coarser axis gets wider columns. */
.calendar-timeline-quarter { --col-w: 4.5rem; }
.calendar-timeline-all { --col-w: 5rem; }

.timeline-head,
.timeline-row {
    display: grid;
    grid-template-columns: var(--label-w) 1fr;
    gap: 0.5rem;
    align-items: start;
}

.timeline-days,
.timeline-track {
    display: grid;
    /* minmax(0, 1fr): an auto track lets one long nowrap chip widen the day. */
    grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
    gap: 2px;
}

.timeline-day {
    text-align: center;
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--fg-3);
    padding: 0.15rem 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.timeline-day-today {
    color: var(--brand-contrast);
    background: var(--brand);
    border-radius: var(--radius-pill);
}

/* The month or year a run of columns belongs to; coarser scales only. */
.timeline-head-groups .timeline-days { gap: 2px; }

.timeline-group {
    text-align: center;
    font-family: var(--font-display);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--fg-2);
    padding: 0.1rem 0 0.25rem;
    border-bottom: 1px solid var(--border-1);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.timeline-track {
    /* Chips carry an explicit grid-row per packed sub-row. */
    grid-auto-rows: minmax(2.15rem, auto);
    min-height: 2.5rem;
    padding: 0.25rem 0;
    border-radius: var(--radius-xs);
    background:
        repeating-linear-gradient(to right,
            var(--border-1) 0 1px,
            transparent 1px calc(100% / var(--cols)));
}

.timeline-label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    /* Keeps the lane's label in view while the columns scroll. */
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--bg-0);
    padding: 0.5rem 0.3rem;
    font-size: 0.82rem;
    font-weight: 700;
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.timeline-label-empty { background: transparent; }

.timeline-swatch {
    flex: 0 0 auto;
    width: 0.7rem;
    height: 0.7rem;
    border-radius: var(--radius-pill);
    background: var(--tag-color);
}

/* Reaches past its column so the title has room (see CalendarTimeline.Pack).
   The solid left border pins it to its date: a due date is a point, not a span. */
.timeline-card {
    align-self: center;
    /* No horizontal margin: on top of the track's gap it rounded the last chip past
       the axis, into a scrollbar. */
    padding: 0.35rem 0.5rem;
    background: linear-gradient(
        to right,
        color-mix(in srgb, var(--tag-color) 20%, transparent),
        color-mix(in srgb, var(--tag-color) 7%, transparent));
}

.timeline-card:hover {
    background: linear-gradient(
        to right,
        color-mix(in srgb, var(--tag-color) 32%, transparent),
        color-mix(in srgb, var(--tag-color) 14%, transparent));
}

/* The column is the date here; the phone's vertical timeline shows it. */
.timeline-card-date { display: none; }

/* ── The calendar on a phone ──
   A ~45px day cell can't hold a chip, so it shows the date, a count and a dot
   per card; tapping it opens the day. */
@media (max-width: 599px) {
    .calendar-page {
        padding: 0.75rem 0.75rem 0.6rem;
        /* Whatever the bottom bar leaves, as for .board. */
        height: calc(100vh - var(--phone-nav-h));
        height: calc(100dvh - var(--phone-nav-h));
        gap: 0.5rem;
    }

    /* Heading and controls on separate centred rows; at 390px one would wrap. */
    .calendar-toolbar {
        flex-direction: column;
        align-items: stretch;
        gap: 0.5rem;
    }

    .calendar-nav { justify-content: center; }

    /* Left, not centred. justify-content alone won't do it: .segmented's auto
       margin swallows the free space. */
    .calendar-actions {
        justify-content: flex-start;
        gap: 0.5rem;
    }

    .calendar-actions > .segmented { margin-left: 0; }

    /* The exports go to the far end; the auto margin carries the pair. */
    .calendar-actions > .calendar-export { margin-left: auto; }

    .calendar-heading {
        font-size: 1.1rem;
        min-width: 0;
        flex: 1;
    }

    /* Labels stay (these two act beyond the page, worth the width); tightened. */
    .calendar-export,
    .calendar-subscribe {
        padding: 0.4rem 0.7rem;
        font-size: 0.76rem;
        gap: 0.3rem;
    }

    .calendar-export svg,
    .calendar-subscribe svg { width: 14px; height: 14px; }

    .segmented-tab { font-size: 0.76rem; }

    .calendar-meta {
        flex-wrap: nowrap;
        gap: 0.5rem;
        justify-content: flex-start;
        overflow: visible;
    }

    .calendar-meta > .segmented-scale { flex: 0 0 auto; }

    /* The tag chips go behind a button with a count and a click-away layer, like
       the board's column filter. */
    .calendar-filter-toggle {
        display: inline-flex;
        align-items: center;
        gap: 0.35rem;
        flex: 0 0 auto;
        border: none;
        border-radius: var(--radius-pill);
        padding: 0.4rem 0.7rem;
        background: var(--bg-1);
        color: var(--fg-2);
        font: inherit;
        font-family: var(--font-body);
        font-size: 0.76rem;
        font-weight: 600;
        cursor: pointer;
        box-shadow: var(--shadow-xs);
        white-space: nowrap;
    }

    .calendar-filter-toggle svg { width: 14px; height: 14px; }

    .calendar-filter-toggle-on {
        background: var(--brand-glow);
        color: var(--brand);
    }

    .calendar-filter-count {
        min-width: 1.05rem;
        text-align: center;
        border-radius: var(--radius-pill);
        background: var(--brand);
        color: var(--brand-contrast);
        font-size: 0.68rem;
        line-height: 1.35;
        padding: 0 0.25rem;
    }

    .calendar-filter-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 880;
        background: rgba(25, 21, 19, 0.35);
        animation: fadeIn var(--duration) var(--ease-out);
    }

    /* Fixed, so .calendar-page's overflow:hidden can't slice it. top/left are
       written by calendar-interop.js's anchorFlyout; these are the fallback. */
    .calendar-legend {
        display: none;
        position: fixed;
        z-index: 881;
        top: 8rem;
        left: 0.75rem;
        width: min(20rem, calc(100vw - 1.5rem));
        max-height: 60dvh;
        overflow-y: auto;
        flex-wrap: wrap;
        gap: 0.4rem;
        padding: 0.75rem;
        border-radius: var(--radius);
        background: var(--bg-1);
        box-shadow: var(--shadow-lg);
    }

    /* Safe to animate transform here: the panel is placed with top/left. */
    .calendar-legend-open {
        display: flex;
        transform-origin: top left;
        animation: flyoutIn 140ms var(--ease-out);
    }

    @media (prefers-reduced-motion: reduce) {
        .calendar-legend-open { animation: none; }
    }

    .calendar-legend::-webkit-scrollbar { display: none; }
    .calendar-legend .tag-chip { flex: 0 0 auto; }

    /* The agenda below lists them already. */
    .calendar-summary { display: none; }
    /* Nothing left on the row: no tags to filter by and the grid's own view. */
    .calendar-meta:not(:has(.calendar-filter-toggle)):not(:has(.segmented-scale)) { display: none; }

    .calendar-weekdays,
    .calendar-week { gap: 0.25rem; }

    .calendar-weekday {
        padding: 0.15rem 0;
        text-align: center;
        font-size: 0.62rem;
    }

    /* Compact; the agenda takes the rest. grid-template-rows: none lets rows size
       to content. */
    .calendar-grid {
        flex: 0 0 auto;
        grid-template-rows: none;
        overflow: visible;
    }

    .calendar-week { min-height: 2.6rem; }

    .calendar-day {
        padding: 0.25rem 0.15rem;
        gap: 0.15rem;
        align-items: center;
    }

    /* The dots already say how many. */
    .calendar-day-head { justify-content: center; }
    .calendar-day-count { display: none; }
    .calendar-daynum { font-size: 0.8rem; }

    .calendar-day-cards { display: none; }

    .calendar-day-dots {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        align-content: flex-start;
        gap: 0.15rem;
        flex: 1;
        min-height: 0;
        overflow: hidden;
    }

    .calendar-day-dot {
        width: 0.35rem;
        height: 0.35rem;
        border-radius: var(--radius-pill);
        background: var(--tag-color, var(--fg-3));
    }

    /* Scrolls on its own so the month above stays put. */
    .calendar-agenda {
        display: flex;
        flex-direction: column;
        gap: 0.5rem;
        flex: 1;
        min-height: 0;
        overflow-y: auto;
        margin-top: 0.35rem;
        padding-top: 0.6rem;
        border-top: 1px solid var(--border-1);
        scrollbar-width: none;
    }

    .calendar-agenda::-webkit-scrollbar { display: none; }

    .calendar-agenda-day {
        display: grid;
        grid-template-columns: 2.4rem 1fr;
        gap: 0.5rem;
        align-items: start;
    }

    .calendar-agenda-date {
        display: flex;
        flex-direction: column;
        align-items: center;
        line-height: 1.1;
        padding-top: 0.15rem;
    }

    .calendar-agenda-weekday {
        font-size: 0.6rem;
        font-weight: 700;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--fg-3);
    }

    .calendar-agenda-daynum {
        font-family: var(--font-display);
        font-size: 1.05rem;
        font-weight: 700;
        color: var(--fg);
    }

    .calendar-agenda-today .calendar-agenda-daynum,
    .calendar-agenda-today .calendar-agenda-weekday { color: var(--brand); }

    .calendar-agenda-cards {
        display: flex;
        flex-direction: column;
        gap: 0.3rem;
        min-width: 0;
    }

    /* Its own class rather than .cal-card; see Calendar.razor. */
    .agenda-card {
        --tag-color: var(--fg-3);
        display: block;
        width: 100%;
        text-align: left;
        border: none;
        border-left: 3px solid var(--tag-color);
        border-radius: var(--radius-xs);
        background: color-mix(in srgb, var(--tag-color) 12%, transparent);
        color: var(--fg);
        font: inherit;
        font-family: var(--font-body);
        font-size: 0.82rem;
        font-weight: 600;
        line-height: 1.35;
        white-space: normal;
        padding: 0.45rem 0.6rem;
        cursor: pointer;
    }

    .agenda-card-overdue { border-left-color: var(--danger); }

    .calendar-day-sheet {
        width: 100%;
        max-height: 80dvh;
    }

    .calendar-day-sheet-cards .cal-card { font-size: 0.9rem; }

    /* ── The timeline runs down the page here ──
       Sideways, a month is three phone screens. Same markup: lanes stack, cards
       list in date order, and each chip carries its date. */
    .calendar-timeline-scroll { overflow-x: hidden; }

    .calendar-timeline {
        min-width: 0;
        gap: 0;
    }

    .timeline-head { display: none; }

    .timeline-row {
        display: block;
        padding-bottom: 0.75rem;
    }

    .timeline-label {
        position: static;
        padding: 0.4rem 0 0.3rem;
        background: transparent;
        font-size: 0.85rem;
    }

    /* A lane is a list here. */
    .timeline-track {
        display: flex;
        flex-direction: column;
        gap: 0.3rem;
        background: none;
        min-height: 0;
        padding: 0;
        /* Flex ignores the chips' inline grid placement, but the width has to be
           taken back. */
        width: 100%;
    }

    .timeline-card {
        grid-column: auto !important;
        grid-row: auto !important;
        width: 100%;
        display: flex;
        align-items: baseline;
        gap: 0.5rem;
        padding: 0.45rem 0.6rem;
        font-size: 0.82rem;
        background: color-mix(in srgb, var(--tag-color) 12%, transparent);
    }

    /* Fixed width so the dates form a column; wide enough for the longest
       ("Nov 2, 2027"), since a flex item's auto min-width would let one grow. */
    .timeline-card-date {
        display: block;
        flex: 0 0 5rem;
        min-width: 0;
        overflow: hidden;
        font-size: 0.7rem;
        font-weight: 700;
        color: var(--fg-3);
        white-space: nowrap;
    }

    .timeline-card-title {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .calendar-subscribe-panel { padding: 1rem; }

    /* Stacked: side by side at this width they wrap mid-label. */
    .calendar-subscribe-actions { flex-direction: column; }
    .calendar-subscribe-action { width: 100%; }
}

/* ── Comments ── */

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

/* In the dialog, groups sit on bg-2 and anything you type into 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;
}

.comment-actions {
    display: flex;
    align-items: center;
    gap: 0.15rem;
}

/* Quiet until hovered or focused, but never display:none, which would take them
   off the keyboard and touch. */
.comment-action {
    border: none;
    background: transparent;
    color: var(--fg-3);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.78rem;
    font-weight: 600;
    padding: 0.2rem 0.4rem;
    border-radius: var(--radius-btn);
    cursor: pointer;
    opacity: 0.55;
    transition: opacity var(--duration) var(--ease), color var(--duration) var(--ease);
}

.comment:hover .comment-action,
.comment-action:focus-visible { opacity: 1; }

.comment-action:hover { color: var(--fg); }
.comment-action-danger:hover { color: var(--danger); }

@media (hover: none) {
    /* No hover to reveal them with. */
    .comment-action { opacity: 1; }
}

.comment-edited {
    color: var(--fg-3);
    font-size: 0.72rem;
    font-style: italic;
}

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

.comment-edit textarea {
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    padding: 0.6rem 0.8rem;
    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-edit textarea:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
}

.comment-edit-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
}

.comment-delete-confirm {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.6rem;
    font-size: 0.85rem;
    color: var(--fg-2);
}

/* Composer and comment box share this: a bg-1 field on a bg-2 panel. */
.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;
    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;
}

.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 here
       (Proton Pass pads for its icon), and `all` animated every write. */
    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 placeholder cycles examples (js/handle-placeholder.js) through a ghost
   over the field, since only the domain rolls. The real placeholder stays
   underneath, transparent, for assistive tech. */
.handle-ghost-field {
    position: relative;
}

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

.handle-ghost {
    position: absolute;
    display: flex;
    align-items: center;
    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 the fade. 420ms must 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 {
    font-size: 0.82rem;
    color: var(--fg-3);
    text-align: left;
}

.login-card form input[type="checkbox"] {
    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);
}

/* ── Home ── */

/* Tiles show structure only (counts, columns, people), which needs no
   decrypting on every visit. */
.home {
    padding: 2.5rem 1.75rem 3rem;
    max-width: 1140px;
    margin: 0 auto;
}

.home-title {
    margin: 0 0 1.25rem;
    font-family: var(--font-display);
    font-size: 1.45rem;
    font-weight: 700;
    letter-spacing: -0.02em;
}

/* auto-fill, not auto-fit: auto-fit stretches two boards into banners. */
.home-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
    gap: 1.5rem;
}

.board-tile {
    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--bg-1);
    border: none;
    border-radius: var(--radius);
    padding: 1.25rem;
    box-shadow: var(--shadow-card);
    transition: box-shadow var(--duration) var(--ease), transform var(--duration) var(--ease);
}

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

/* The link covers the tile from underneath and the controls sit above it: an
   anchor can't contain the settings button. Spacing is margins so the phone
   grid can place the parts. */
.board-tile-link {
    display: flex;
    flex-direction: column;
    text-decoration: none;
    color: inherit;
}

.board-tile-link::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
}

.board-tile-name {
    margin-top: 0.9rem;
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    /* Cut, not wrapped, so one long name can't set its row's height. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A track per column and a dash per card, in the board's BoardPalette colour. */
.board-mini {
    display: flex;
    gap: 6px;
    height: 56px;
}

/* The gap fits five 2px dashes and the faded one in 40px (38 on a phone); at 6px
   flex shrank the dashes to uneven fractional widths. */
.board-mini-column {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    padding: 8px 7px;
    border-radius: var(--radius-xs);
    background: var(--bg-2);
    overflow: hidden;
}

.board-mini-card {
    flex: none;
    width: 72%;
    height: 2px;
    border-radius: 1px;
    /* Mid-tone board colours, as for tag chips: inked down on light, straight on dark. */
    background: light-dark(
        color-mix(in srgb, var(--board-color, var(--fg-3)) 55%, transparent),
        color-mix(in srgb, var(--board-color, var(--fg-3)) 75%, transparent));
}

.board-mini-card-more {
    opacity: 0.4;
}

/* Above the link's cover, so a face's name still shows on hover. */
.board-tile-foot {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

.board-tile-count {
    font-size: 0.82rem;
    color: var(--fg-2);
}

.board-tile-who {
    font-size: 0.78rem;
    color: var(--fg-3);
}

.board-tile-everyone::before {
    content: "";
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-right: 0.4rem;
    border-radius: 50%;
    background: currentColor;
    vertical-align: 0.05em;
}

/* One positioned row, so adding or dropping a control does not move the other. */
.board-tile-controls {
    position: absolute;
    top: 0.85rem;
    right: 0.85rem;
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

/* Shown on hover; always there for the keyboard via :focus-visible. */
.board-tile-settings,
.board-tile-pin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border: none;
    padding: 0;
    cursor: pointer;
    border-radius: 50%;
    color: var(--fg-3);
    background: var(--bg-2);
    opacity: 0;
    transition: opacity var(--duration) var(--ease), color var(--duration) var(--ease);
}

.board-tile:hover .board-tile-settings,
.board-tile:hover .board-tile-pin,
.board-tile-settings:focus-visible,
.board-tile-pin:focus-visible {
    opacity: 1;
}

.board-tile-settings:hover,
.board-tile-pin:hover {
    color: var(--brand);
}

/* Visible unhovered: the pin explains the order. */
.board-tile-pin-on {
    opacity: 1;
    color: var(--brand);
    background: var(--brand-surface);
}

.board-tile-new {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    min-height: 168px;
    background: transparent;
    border: 2px dashed var(--border-2);
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.93rem;
    font-weight: 600;
    cursor: pointer;
}

.board-tile-new:hover {
    border-color: var(--brand);
    color: var(--brand);
    box-shadow: none;
}

.board-tile-plus {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--brand-surface);
    color: var(--brand);
}

.board-tile-form {
    gap: 0.6rem;
    justify-content: center;
    min-height: 168px;
}

.board-tile-input {
    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);
}

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

.board-tile-hint {
    margin: 0;
    font-size: 0.78rem;
    color: var(--fg-2);
    line-height: 1.5;
}

.board-tile-actions {
    display: flex;
    gap: 0.5rem;
}

.board-tile-actions button {
    padding: 0.5rem 1.1rem;
    font-size: 0.85rem;
}

.home-empty-new {
    margin-top: 1.5rem;
}

.home-empty-note {
    margin: 1.75rem 0 0;
    font-size: 0.82rem;
    color: var(--fg-3);
}

/* ── Admin: a board's record ── */

.board-record-name {
    margin: 0 0 0.25rem;
    overflow-wrap: anywhere;
}

.board-record-facts {
    display: grid;
    grid-template-columns: minmax(8rem, max-content) 1fr;
    gap: 0.45rem 1.25rem;
    margin: 1rem 0 0;
    font-size: 0.9rem;
}

.board-record-facts dt {
    color: var(--fg-3);
}

.board-record-facts dd {
    margin: 0;
    color: var(--fg);
}

.board-record-admins {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 0.75rem;
}

/* ── Members: a banned row ── */

/* Stays in the list (the row holds the ban and the way back) but reads as out. */
.member-row-banned td:not(.admin-row-actions) {
    color: var(--fg-3);
}

.member-banned {
    display: inline-block;
    margin-left: 0.5rem;
    padding: 0.05rem 0.45rem;
    border-radius: 999px;
    background: var(--bg-2);
    color: var(--fg-2);
    font-size: 0.72rem;
    font-weight: 600;
    vertical-align: 0.1em;
}

.member-ban-reason {
    min-width: 10rem;
    padding: 0.35rem 0.55rem;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    background: var(--bg-1);
    color: var(--fg);
    font: inherit;
    font-size: 0.85rem;
}

/* ── Home: recently deleted ── */

/* The page's column, not the tile grid: these aren't boards to open. */
.home-deleted {
    padding-top: 0;
}

.home-deleted-title {
    margin: 0 0 0.6rem;
    font-size: 0.82rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--fg-3);
}

.home-deleted-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid var(--border-1);
}

.home-deleted-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.55rem 0;
    border-bottom: 1px solid var(--border-1);
    font-size: 0.88rem;
}

.home-deleted-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--fg-2);
}

.home-deleted-until {
    flex: none;
    color: var(--fg-3);
    font-size: 0.8rem;
}

.home-deleted-restore {
    flex: none;
}

/* ── Home: the "for you" strip ── */

/* Capped at six: the one place on the page that decrypts a card's words (two
   passes, ForYouService). */
.home-foryou {
    margin-bottom: 2rem;
}

/* A row that scrolls, not a grid that wraps; snap points land a thumb on a card. */
.foryou-strip {
    display: flex;
    gap: 0.75rem;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    /* Room on every side for a hovered card's lift and shadow, since overflow-x
       clips overflow-y too. The negative margins give it back; scroll padding
       keeps a snapped card on the page's column. */
    padding: 1rem 1rem 1.75rem;
    margin: -1rem -1rem -1.4rem;
    scroll-padding-inline: 1rem;
    /* Shown only while wanted. `thin` keeps the room constant, so it never moves
       the row. */
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
}

.foryou-strip:hover,
.foryou-strip:focus-within {
    scrollbar-color: var(--border-3) transparent;
}

.foryou-card {
    flex: 0 0 auto;
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    width: 190px;
    padding: 0.7rem 0.8rem;
    background: var(--bg-1);
    /* A tick, not a wash: six washed cards would be a paint chart. */
    border-left: 3px solid var(--board-color);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-card);
    text-decoration: none;
    color: inherit;
    transition: box-shadow var(--duration) var(--ease), transform var(--duration) var(--ease);
}

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

/* Both optional, so the row collapses instead of leaving a gap. */
.foryou-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem;
    min-height: 20px;
}

/* The face always sits top-right, even with no date beside it. */
.foryou-top > .assignee-avatar { margin-left: auto; }

.foryou-title {
    font-weight: 600;
    font-size: 0.9rem;
    line-height: 1.3;
    /* Two lines then cut, so one title can't set the row's height. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.foryou-board {
    font-size: 0.75rem;
    color: var(--fg-3);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* On a phone a tile becomes a row: the picture beside the name. */
@media (max-width: 599px) {
    /* The bar's inset is already on the body (body:has(.board-header)). */
    .home {
        padding: 1.25rem 1rem 2rem;
    }

    .home-tiles {
        grid-template-columns: 1fr;
        gap: 0.75rem;
    }

    /* The strip stays a row, narrower, so a half card always shows it scrolls. */
    .home-foryou {
        margin-bottom: 1.25rem;
    }

    .foryou-card {
        width: 168px;
    }

    .board-tile {
        padding: 0.9rem;
    }

    /* Picture on the left across both lines, words in one column beside it. The
       name keeps clear of the pin and the cog. */
    .board-tile-link {
        display: grid;
        grid-template-columns: 104px minmax(0, 1fr);
        grid-template-areas: "mini name" "mini foot";
        column-gap: 0.9rem;
        row-gap: 0.3rem;
        align-items: center;
    }

    .board-mini {
        grid-area: mini;
        height: 52px;
    }

    .board-mini-column { padding: 7px 5px; }

    .board-tile-name {
        grid-area: name;
        align-self: end;
        margin-top: 0;
        padding-right: 4rem;
    }

    .board-tile-foot {
        grid-area: foot;
        align-self: start;
        margin-top: 0;
    }

    .board-tile-new,
    .board-tile-form {
        min-height: 0;
    }

    /* No hover, so both controls are always shown, clear of the row's tap target. */
    .board-tile-controls {
        top: 0.9rem;
        right: 0.9rem;
    }

    .board-tile-settings,
    .board-tile-pin {
        opacity: 1;
    }
}

/* ── 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);
}

/* The vendor pseudo-elements strip the swatch's own box so the colour fills the
   input's shape. */
.admin-form input[type="color"],
.admin-table input[type="color"] {
    -webkit-appearance: none;
    appearance: none;
    flex: 0 0 auto;
    width: 3rem;
    height: 2.5rem;
    padding: 3px;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    background: var(--bg-0);
    cursor: pointer;
    transition: all var(--duration) var(--ease);
}

.admin-form input[type="color"]:hover,
.admin-table input[type="color"]:hover {
    border-color: var(--border-3);
}

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

input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }

input[type="color"]::-webkit-color-swatch {
    border: none;
    border-radius: calc(var(--radius-xs) - 4px);
}

input[type="color"]::-moz-color-swatch {
    border: none;
    border-radius: calc(var(--radius-xs) - 4px);
}

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

/* Takes its content's width, so the spare goes to the field a member types into. */
.admin-table .admin-cell-fit {
    width: 1%;
    white-space: nowrap;
}

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

/* center, not stretch: a stretched <a> keeps its text at the top, off the
   buttons' line. */
.admin-row-actions {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    justify-content: flex-end;
}

/* Must stay a real table cell: display:flex detaches it from the row box and
   misaligns its border when a sibling cell wraps. */
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;
}

/* The links take the buttons' type size and box (transparent border included),
   so the row is one height. */
.admin-row-actions .board-nav-link {
    font-size: 0.82rem;
    padding: 0.38rem 0.85rem;
    border: 1px solid transparent;
}

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

/* A note states the state of what it sits above (a mark and one line on a tinted
   strip); .admin-hint says what a control does. */
.admin-note {
    display: flex;
    /* Top, not centre: beside a wrapped note the mark would read as a bullet. */
    align-items: flex-start;
    gap: 0.55rem;
    margin: 0 0 0.9rem;
    padding: 0.6rem 0.85rem;
    border-radius: var(--radius-xs);
    background: var(--bg-0);
    color: var(--fg-2);
    font-size: 0.87rem;
    line-height: 1.45;
}

.admin-note svg {
    width: 17px;
    height: 17px;
    flex: 0 0 auto;
    /* Half the difference between the mark and the line, so it sits on the first line. */
    margin-top: 0.1rem;
    color: var(--fg-3);
}

.admin-note-warn {
    background: var(--danger-bg);
    color: var(--fg-2);
}

.admin-note-warn svg { color: var(--danger); }

.admin-hint {
    margin: 0.5rem 0 0;
    color: var(--fg-3);
    font-size: 0.85rem;
}

/* ── A choice between two things ── */

/* Options as cards, each with a line saying what it means. */
.choice-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    /* Usable as a <fieldset> without its box. */
    margin: 0;
    padding: 0;
    border: 0;
}

.choice-list legend {
    padding: 0;
    margin-bottom: 0.5rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--fg-2);
}

.choice-option {
    display: flex;
    align-items: flex-start;
    gap: 0.7rem;
    padding: 0.8rem 0.9rem;
    background: var(--bg-0);
    border: 1px solid var(--border-2);
    border-radius: var(--radius-xs);
    cursor: pointer;
    transition: border-color var(--duration) var(--ease), background var(--duration) var(--ease);
}

.choice-option:hover { border-color: var(--border-3); }

.choice-option-on {
    border-color: var(--brand);
    background: var(--brand-glow);
}

/* Dimmed rather than removed, so it can be judged against the others. */
.choice-option-off {
    opacity: 0.55;
    cursor: not-allowed;
}

.choice-option-off:hover { border-color: var(--border-2); }

.choice-option input { margin-top: 0.15rem; }

.choice-option-text { display: flex; flex-direction: column; gap: 0.15rem; }

.choice-option-label { font-weight: 600; color: var(--fg); }

.choice-option-hint { color: var(--fg-3); font-size: 0.85rem; }

/* ── The profile page ── */

/* An <a> wearing a button needs the block layout buttons get for free. */
.profile-reauthorize {
    display: inline-block;
    margin-bottom: 1.25rem;
}

.profile-target {
    margin-bottom: 1rem;
    max-width: 34rem;
}

.profile-network {
    margin: 0 0 1.25rem;
    max-width: 60ch;
}

.profile-field {
    margin-bottom: 1.25rem;
}

.profile-field label {
    display: block;
    margin-bottom: 0.4rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--fg-2);
}

.profile-field input[type="text"] {
    width: 100%;
    max-width: 28rem;
    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);
}

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

/* Only the button half of a file input can be styled, so this restates the
   secondary button on ::file-selector-button. */
.profile-file {
    display: block;
    max-width: 28rem;
    font-size: 0.9rem;
    color: var(--fg-3);
}

.profile-file::file-selector-button {
    margin-right: 0.75rem;
    border: 1px solid var(--border-2);
    border-radius: var(--radius-pill);
    padding: 0.5rem 1.1rem;
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.85rem;
    font-weight: 600;
    background: var(--bg-2);
    color: var(--fg);
    cursor: pointer;
    transition: all var(--duration) var(--ease);
}

.profile-file::file-selector-button:hover {
    background: var(--bg-3);
    border-color: var(--border-3);
}

.profile-file:disabled { opacity: 0.5; }

.profile-file:disabled::file-selector-button { cursor: not-allowed; }

.profile-field .btn-secondary {
    margin-top: 0.6rem;
}

.profile-actions {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    flex-wrap: wrap;
    margin-top: 1rem;
}

.profile-actions .admin-hint {
    margin: 0;
}

.profile-locked {
    margin: 0 0 1rem;
    max-width: 60ch;
    padding: 0.85rem 1rem;
    border-radius: var(--radius-xs);
    background: var(--brand-surface);
    color: var(--fg-2);
    font-size: 0.9rem;
    line-height: 1.5;
}

.profile-permission {
    margin-top: 1.25rem;
    max-width: 60ch;
}

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

/* ── Board access ── */

/* auto-fill collapses it to one column on a phone without a media query. */
.access-members {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 0.5rem;
    margin-top: 0.75rem;
}

/* Open to everyone: the ticks stay editable but decide nothing. Dimmed, not
   hidden, so restricting the board again keeps them. */
.access-members-moot { opacity: 0.55; }

.access-member {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.55rem 0.6rem 0.55rem 0.7rem;
    background: var(--bg-0);
    border-radius: var(--radius-xs);
    box-shadow: var(--shadow-xs);
    min-width: 0;
}

.access-member:hover { box-shadow: var(--shadow-sm); }

.access-member-pick {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex: 1;
    min-width: 0;
    cursor: pointer;
}

/* Faded while the member isn't on the board: the role lives on the grant. */
.access-member-admin {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    flex: none;
    padding: 0.2rem 0.55rem 0.2rem 0.4rem;
    border-radius: var(--radius-pill);
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--fg-3);
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}

.access-member-admin:hover { color: var(--fg-2); }

.access-member-admin:has(input:checked) {
    background: var(--brand-surface);
    color: var(--brand);
}

.access-member-admin:has(input:disabled) {
    opacity: 0.45;
    cursor: default;
}

.access-member-admin input { margin: 0; }

.access-member-avatar {
    width: 28px;
    height: 28px;
    border-radius: var(--radius-pill);
    object-fit: cover;
    flex: 0 0 auto;
}

.access-member-avatar-fallback {
    display: grid;
    place-items: center;
    background: var(--brand-surface);
    color: var(--brand);
    font-size: 0.7rem;
    font-weight: 700;
}

.access-member-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.access-member-name {
    font-size: 0.9rem;
    color: var(--fg);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.access-member-note {
    font-size: 0.75rem;
    color: var(--fg-3);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.access-save { margin-top: 1rem; align-items: center; }

/* ── 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;
}

/* min-width: 0: an input's content minimum is the browser's field width, which
   pushed Delete past the row's edge. */
.admin-sortable-input {
    flex: 1;
    min-width: 0;
    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 ── */

/* The only styles for it. Don't add a ReconnectModal.razor.css: its scoped
   [b-…] selectors outrank these, as the template's once did. */

/* Nothing here may decide whether the dialog is on screen: that is showModal(),
   which ReconnectModal.razor.js holds back so a quick reconnect shows nothing. A
   display rule on a state class painted it outside the top layer. The state
   classes only choose which line an open dialog shows. */

#components-reconnect-modal {
    /* A modal <dialog> centres on auto margins; never a transform, which the
       entrance animates. */
    margin: auto;
    width: min(23rem, calc(100vw - 2rem));
    padding: 0;
    border: none;
    border-radius: var(--radius);
    background: var(--bg-1);
    color: var(--fg);
    box-shadow: var(--shadow-xl);
    font-family: var(--font-body);
}

#components-reconnect-modal[open] {
    animation: slideUp 300ms var(--ease-out);
}

/* The browser focuses the dialog itself; keep the focus, drop the ring. The
   buttons keep theirs. */
#components-reconnect-modal:focus,
#components-reconnect-modal:focus-visible {
    outline: none;
}

#components-reconnect-modal::backdrop {
    /* Literal, not a token: ::backdrop inheriting custom properties is too recent.
       .modal-backdrop uses the same value. */
    background: rgba(25, 21, 19, 0.45);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    animation: fadeIn var(--duration) var(--ease-out);
}

.components-reconnect-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.9rem;
    padding: 2rem 1.75rem;
    text-align: center;
}

/* Every line starts hidden and the state class picks one; the id outranks
   .btn-primary. */
#components-reconnect-modal .components-reconnect-container > * {
    display: none;
}

#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible {
    display: block;
}

/* The buttons are .btn-primary, a flex box. */
#components-reconnect-modal.components-reconnect-failed button.components-reconnect-failed-visible,
#components-reconnect-modal.components-reconnect-paused button.components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed button.components-resume-failed-visible {
    display: inline-flex;
}

#components-reconnect-modal p {
    margin: 0;
    font-size: 0.95rem;
    line-height: 1.5;
    color: var(--fg);
}

#components-reconnect-modal .reconnect-hint {
    display: inline-block;
    margin-top: 0.15rem;
    font-size: 0.85rem;
    color: var(--fg-2);
}

@media (max-width: 599px) {
    /* No blur on a phone, for .modal-backdrop's reason. */
    #components-reconnect-modal::backdrop {
        background: rgba(25, 21, 19, 0.6);
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    #components-reconnect-modal[open],
    #components-reconnect-modal::backdrop {
        animation: none;
    }
}

/* ── Nav links (views + admin) ── */

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

.nav-views {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
}

/* ── The phone's tabs ── */

/* Phone only. Hidden in CSS rather than left out of the render: _isPhone takes
   a round trip, and a bar that arrives late rearranges under a thumb. */
.nav-tabs { display: none; }

.nav-tab-label { display: none; }

/* ── Header lead: the brand, then which board ── */

/* min-width: 0 is load-bearing: without it a long board name pushes the account
   and menu off the row. Above 960px the search block overrides it. */
.nav-lead {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-width: 0;
    flex: 1 1 auto;
}

.nav-lead-sep {
    width: 1px;
    height: 22px;
    background: var(--border-2);
    flex: 0 0 auto;
}

/* ── Board switcher ── */

/* Anchor for the absolutely positioned flyout. */
.nav-boards {
    position: relative;
    display: flex;
    min-width: 0;
}

/* Board names are user-supplied and can be sentences, so it truncates. */
.nav-board-title,
.nav-boards-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-width: 0;
    max-width: 24rem;
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: -0.015em;
    color: var(--fg);
}

.nav-board-title {
    padding: 0.3rem 0.2rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav-boards-toggle {
    padding: 0.3rem 0.5rem 0.3rem 0.7rem;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: all var(--duration) var(--ease);
}

.nav-boards-toggle:hover {
    background: var(--bg-2);
    border-color: var(--border-1);
}

/* Over the click-catcher (z-index -1 in the header's stacking context), so the
   button that opened the flyout can close it. */
.nav-boards-toggle[aria-expanded="true"] {
    background: var(--brand-surface);
    border-color: transparent;
    color: var(--brand);
    position: relative;
    z-index: 1;
}

.nav-boards-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav-boards-toggle > svg {
    flex: 0 0 auto;
    opacity: 0.6;
}

/* ── The header's flyout ── */

.nav-boards-menu {
    position: absolute;
    top: calc(100% + 0.4rem);
    z-index: 200;
    min-width: 15rem;
    max-height: 60vh;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.4rem;
    background: var(--bg-1);
    border: 1px solid var(--border-1);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    animation: flyoutIn 160ms var(--ease-out);
    left: 0;
}

@keyframes flyoutIn {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: translateY(0); }
}

.nav-flyout-head {
    margin: 0.15rem 0.35rem 0.3rem;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--fg-3);
}

.nav-board-option {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.7rem;
    border-radius: var(--radius-xs);
    color: var(--fg);
    text-decoration: none;
    font-size: 0.92rem;
    font-weight: 500;
    transition: all var(--duration) var(--ease);
}

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

.nav-board-option-name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav-board-option-active {
    background: var(--brand-surface);
    color: var(--brand);
    font-weight: 600;
}

/* A tick as well as the fill, so the current board doesn't rest on a tint. */
.nav-board-option-tick {
    flex: 0 0 auto;
    opacity: 0;
}

.nav-board-option-active .nav-board-option-tick { opacity: 1; }

/* The general calendar picks all the rows under it, so a hairline starts the
   list proper. */
.nav-board-option-all {
    border-bottom: 1px solid var(--border);
    border-radius: var(--radius-xs) var(--radius-xs) 0 0;
    margin-bottom: 0.15rem;
    padding-bottom: 0.55rem;
}

/* Below a laptop, drop the wordmark (the mark stays) and the handle (the avatar
   stands for it). Not in the phone/tablet blocks: this width spans both. */
@media (max-width: 959px) {
    .board-header h1 { display: none; }

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

/* ── The account menu ── */

.nav-menu-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    position: relative;
    padding: 0.25rem 0.6rem 0.25rem 0.3rem;
    background: var(--bg-2);
    border: 1px solid var(--border-1);
    border-radius: var(--radius-pill);
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.88rem;
    font-weight: 500;
    cursor: pointer;
    flex-shrink: 0;
    transition: all var(--duration) var(--ease);
}

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

/* Over the click-catcher, as the switcher's. */
.nav-menu-toggle[aria-expanded="true"] {
    background: var(--bg-3);
    border-color: var(--border-2);
    position: relative;
    z-index: 1;
}

.nav-menu-toggle-caret {
    flex: 0 0 auto;
    opacity: 0.6;
    transition: transform var(--duration) var(--ease);
}

.nav-menu-toggle[aria-expanded="true"] .nav-menu-toggle-caret {
    transform: rotate(180deg);
}

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

.nav-avatar-lg {
    width: 38px;
    height: 38px;
}

.nav-avatar-lg.nav-avatar-fallback {
    font-size: 0.92rem;
}

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

/* One dot for anything new in the menu (install offer, unread release), inside
   the pill's curve: at (0,0) it would sit outside the radius. */
.nav-menu-dot {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--brand);
    /* Punches a ring out of the button so the dot reads at any header colour. */
    box-shadow: 0 0 0 2px var(--bg-1);
}

/* ── The search box ── */

/* A flex item between the two sides, not a centred overlay, which would cover a
   long board name. */
.nav-search {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 1 26rem;
    min-width: 0;
    margin: 0 1rem;
}

/* Centres the box on the row: both sides get flex: 1 1 0 and split the leftover
   in half (an auto basis would inherit their width difference). If the account
   side needs more than half, its floor wins and the box drifts left rather than
   the links wrapping. The lead's min-width keeps the wordmark from being clipped:
   the mark, the wordmark and enough board name to tell which. */
@media (min-width: 960px) {
    .nav-lead {
        flex: 1 1 0;
        min-width: 13rem;
    }

    .board-user {
        flex: 1 1 0;
        justify-content: flex-end;
    }
}

/* Above 960px the field itself is on screen. */
.nav-search-compact { display: none; }

.nav-search-field {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    min-width: 0;
    padding: 0 0.7rem;
    height: 38px;
    background: var(--bg-2);
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    transition: background var(--duration) var(--ease), border-color var(--duration) var(--ease);
}

.nav-search-open .nav-search-field {
    background: var(--bg-1);
    border-color: var(--border-2);
    box-shadow: var(--shadow-sm);
}

.nav-search-mark {
    display: flex;
    flex: 0 0 auto;
    color: var(--fg-3);
}

.nav-search-mark svg { width: 16px; height: 16px; }

.nav-search-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    background: none;
    color: var(--fg);
    font: inherit;
    font-size: 0.9rem;
    outline: none;
    padding: 0;
}

.nav-search-input::placeholder { color: var(--fg-3); }

.nav-search-clear {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-pill);
    background: var(--bg-3);
    color: var(--fg-2);
    cursor: pointer;
}

.nav-search-clear:hover { color: var(--fg); }

/* Only the phone's sheet needs a way out in words; see the phone block. */
.nav-search-close { display: none; }

.nav-search-panel {
    position: absolute;
    top: calc(100% + 0.5rem);
    left: 0;
    right: 0;
    z-index: 200;
    max-height: min(60vh, 30rem);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.4rem;
    background: var(--bg-1);
    border: 1px solid var(--border-1);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    animation: flyoutIn 160ms var(--ease-out);
}

.nav-search-tips {
    margin: 0;
    padding: 0 0.35rem 0.3rem;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.nav-search-tips li {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    font-size: 0.85rem;
    color: var(--fg-2);
}

.nav-search-tips code {
    flex: 0 0 auto;
    min-width: 8.5rem;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--brand);
}

.nav-search-empty {
    margin: 0;
    padding: 0.9rem 0.7rem;
    font-size: 0.88rem;
    color: var(--fg-2);
    text-align: center;
}

.nav-search-hit {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 0.5rem 0.7rem;
    border-radius: var(--radius-xs);
    color: var(--fg);
    text-decoration: none;
}

.nav-search-hit:hover,
.nav-search-hit-active {
    background: var(--brand-glow);
    text-decoration: none;
}

.nav-search-hit-title {
    font-size: 0.92rem;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav-search-hit-preview {
    font-size: 0.82rem;
    color: var(--fg-2);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav-search-hit-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.4rem;
    font-size: 0.74rem;
    color: var(--fg-3);
}

.nav-search-hit-place {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav-search-hit-tag {
    padding: 0.05rem 0.4rem;
    border-radius: var(--radius-pill);
    background: var(--bg-2);
    color: var(--fg-2);
}

/* Only on a hit found in its discussion: says why it is here. */
.nav-search-hit-comment {
    padding: 0.05rem 0.4rem;
    border-radius: var(--radius-pill);
    background: var(--brand-surface);
    color: var(--brand);
}

/* The field folds to the magnifier: a 26rem box pushed the admin links off the row. */
@media (max-width: 959px) {
    .nav-search {
        flex: 0 0 auto;
        margin: 0;
    }

    /* The same box as .nav-menu-toggle beside it: a bare glyph didn't read as
       pressable. */
    .nav-search-compact {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 46px;
        height: 40px;
        padding: 0;
        border: 1px solid var(--border-1);
        border-radius: var(--radius-pill);
        background: var(--bg-2);
        color: var(--fg-2);
        cursor: pointer;
        transition: all var(--duration) var(--ease);
    }

    /* Larger when alone in its button; the one in the field stays 16px. */
    .nav-search-compact svg { width: 20px; height: 20px; }

    .nav-search-compact:hover { background: var(--bg-3); color: var(--fg); }

    /* Over the click-catcher, like the board switcher. */
    .nav-search-compact[aria-expanded="true"] {
        background: var(--brand-surface);
        border-color: transparent;
        color: var(--brand);
        position: relative;
        z-index: 1;
    }

    .nav-search-field { display: none; }

    /* A flyout under the header, anchored to the window rather than the trigger: a
       26rem panel hung off the trigger overflowed a 620px window's left edge.
       Fixed, so it restates the header's 64px. */
    .nav-search-open .nav-search-field {
        display: flex;
        position: fixed;
        inset: calc(64px + 0.5rem) 0.75rem auto auto;
        z-index: 300;
        width: min(26rem, calc(100vw - 1.5rem));
        animation: flyoutIn 160ms var(--ease-out);
    }
}

/* ── The menu ── */

.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.4rem;
    display: flex;
    flex-direction: column;
    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: 19rem;
}

/* Invisible click-catcher at negative z inside the sticky header's stacking
   context (z 100): below the header's own children, so nav links stay one click,
   but above all page content. The menu 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;
}

/* Only the phone's sheet shows one. */
.nav-menu-sheet-handle { display: none; }

/* Hairlines between groups, not boxes around them, so the menu is one surface. */
.nav-menu-group {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.3rem 0;
    border-top: 1px solid var(--border-1);
}

.nav-menu-account {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.5rem 0.6rem 0.7rem;
    min-width: 0;
}

/* The identity block links to /profile and borrows the rows' hover. */
a.nav-menu-account {
    color: inherit;
    text-decoration: none;
    border-radius: var(--radius-xs);
    transition: background var(--duration) var(--ease);
}

a.nav-menu-account:hover { background: var(--brand-glow); }

.nav-menu-account-go {
    flex: 0 0 auto;
    margin-left: auto;
    color: var(--fg-3);
    transition: color var(--duration) var(--ease);
}

a.nav-menu-account:hover .nav-menu-account-go { color: var(--brand); }

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

.nav-menu-account-name {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--fg);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav-menu-account-role {
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--brand);
}

/* ── Menu rows ── */

.nav-menu .nav-menu-row {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    width: 100%;
    min-height: 40px;
    padding: 0.5rem 0.6rem;
    border: none;
    background: none;
    color: var(--fg);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.92rem;
    font-weight: 500;
    text-align: left;
    border-radius: var(--radius-xs);
    cursor: pointer;
    transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}

.nav-menu .nav-menu-row:hover:not(:disabled) {
    background: var(--brand-glow);
    color: var(--brand);
}

.nav-menu .nav-menu-row:disabled {
    opacity: 0.55;
    cursor: default;
}

.nav-menu .nav-menu-row svg {
    flex: 0 0 auto;
    color: var(--fg-3);
    transition: color var(--duration) var(--ease);
}

/* One icon column: ShareButton ships a 17px mark, and a pixel of icon is a pixel
   of indent. :first-child, so the fold's caret keeps its size. */
.nav-menu .nav-menu-row > svg:first-child,
.nav-menu-link > svg:first-child {
    width: 18px;
    height: 18px;
}

.nav-menu .nav-menu-row:hover:not(:disabled) svg { color: var(--brand); }

.nav-menu-row-label,
.nav-menu .share-button-label {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The sparkle pictures its row, so it keeps its colour. */
.nav-menu .nav-menu-whatsnew svg {
    fill: currentColor;
    color: var(--brand);
}

.nav-menu-badge {
    flex: 0 0 auto;
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 0.15rem 0.45rem;
    border-radius: var(--radius-pill);
    background: var(--brand);
    color: var(--brand-contrast);
}

.nav-menu .nav-menu-install svg { color: var(--brand); }

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

/* ── Where to go: the Members row ── */

/* The same box as .nav-menu-row, gap included, so the sheet has one left margin. */
.nav-menu-link {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    min-height: 40px;
    padding: 0.5rem 0.6rem;
    border-radius: var(--radius-xs);
    color: var(--fg);
    text-decoration: none;
    font-size: 0.92rem;
    font-weight: 500;
    transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}

.nav-menu-link > svg {
    flex: 0 0 auto;
    color: var(--fg-3);
    transition: color var(--duration) var(--ease);
}

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

.nav-menu-link:hover > svg,
.nav-menu-link-active > svg { color: var(--brand); }

.nav-menu-link-active {
    background: var(--brand-surface);
    color: var(--brand);
    font-weight: 600;
}

/* ── Prefs (theme + language) ── */

.nav-menu-prefs {
    gap: 0.35rem;
    padding: 0.45rem 0;
}

.nav-pref-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0 0.6rem;
}

.nav-pref-label {
    font-size: 0.88rem;
    font-weight: 500;
    color: var(--fg-2);
    white-space: nowrap;
}

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

.nav-pref-options button {
    border: none;
    background: transparent;
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.8rem;
    font-weight: 500;
    padding: 0.32rem 0.6rem;
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: all var(--duration) var(--ease);
    white-space: nowrap;
}

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

/* The language pill gets .active from the render. The theme pill reads :root
   (no data-theme is auto), so the tap needs no round trip. */
.nav-pref-options button.active,
:root:not([data-theme]) .nav-pref-options button[data-theme-set="auto"],
:root[data-theme="light"] .nav-pref-options button[data-theme-set="light"],
:root[data-theme="dark"] .nav-pref-options button[data-theme-set="dark"] {
    background: var(--brand);
    color: var(--brand-contrast);
    box-shadow: var(--shadow-xs);
}

/* ── The foot ── */

.nav-menu-foot {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0 0;
    border-top: 1px solid var(--border-1);
}

.nav-menu-signout {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    flex: 1 1 auto;
    min-height: 40px;
    border: none;
    background: none;
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.92rem;
    font-weight: 500;
    padding: 0.5rem 0.6rem;
    text-align: left;
    cursor: pointer;
    border-radius: var(--radius-xs);
    transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}

.nav-menu-signout svg {
    flex: 0 0 auto;
    color: var(--fg-3);
    transition: color var(--duration) var(--ease);
}

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

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

/* The one place naming the running version: a footnote, for bug reports. */
.nav-menu-version {
    flex: 0 0 auto;
    padding-right: 0.6rem;
    font-size: 0.72rem;
    font-weight: 500;
    color: var(--fg-3);
    font-variant-numeric: tabular-nums;
}

/* ── What's new: the dialog ── */

.changelog-modal {
    width: min(560px, 100%);
}

.changelog-modal .modal-header h2 {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.changelog-sparkle {
    fill: var(--brand);
    flex-shrink: 0;
}

.changelog-body {
    gap: 1rem;
}

.changelog-intro {
    margin: 0;
    font-size: 0.88rem;
    color: var(--fg-2);
}

.changelog-timeline {
    display: flex;
    flex-direction: column;
    position: relative;
    padding-left: 1.15rem;
}

/* On the container, so the line runs behind the marks and stops at the last. */
.changelog-timeline::before {
    content: "";
    position: absolute;
    left: 3px;
    top: 0.6rem;
    bottom: 0.6rem;
    width: 2px;
    border-radius: 1px;
    background: var(--border-1);
}

.changelog-release {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
    position: relative;
    padding: 0.15rem 0 1.15rem;
}

.changelog-release:last-child { padding-bottom: 0.15rem; }

.changelog-dot {
    position: absolute;
    left: -1.15rem;
    top: 0.5rem;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--border-3);
    box-shadow: 0 0 0 3px var(--bg-1);
}

/* The top of the story is filled; the unreleased block is hollow instead, said
   here rather than left to rule order. */
.changelog-release-top:not(.changelog-release-unreleased) .changelog-dot { background: var(--brand); }

/* Only on the dev stack: a heading, not a date, with a hollow mark. */
.changelog-release-unreleased .changelog-dot {
    background: var(--bg-1);
    border: 2px solid var(--brand);
}

.changelog-release-unreleased .changelog-release-date {
    color: var(--fg-2);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 0.78rem;
}

.changelog-release-new {
    background: var(--brand-surface);
    border-radius: var(--radius-sm);
    padding: 0.7rem 0.9rem 0.9rem;
    margin: -0.1rem -0.9rem 0.4rem;
}

.changelog-release-new .changelog-dot {
    left: -0.25rem;
    top: 1.05rem;
    box-shadow: 0 0 0 3px var(--brand-surface);
}

.changelog-release-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    flex-wrap: wrap;
}

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

.changelog-release:not(.changelog-release-top) .changelog-release-date {
    font-size: 0.88rem;
    color: var(--fg-2);
}

.changelog-badge {
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 0.12rem 0.5rem;
    border-radius: var(--radius-pill);
    background: var(--brand);
    color: var(--brand-contrast);
}

.changelog-version {
    margin-left: auto;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--fg-3);
    font-variant-numeric: tabular-nums;
}

.changelog-list {
    margin: 0;
    padding-left: 1.1rem;
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
}

.changelog-list li {
    font-size: 0.92rem;
    line-height: 1.55;
    color: var(--fg-2);
}

.changelog-list li::marker {
    color: var(--brand);
}

.changelog-more {
    align-self: flex-start;
    margin-left: 1.15rem;
    padding: 0.4rem 0.75rem;
    border: 1px solid var(--border-1);
    border-radius: var(--radius-pill);
    background: none;
    color: var(--fg-2);
    font: inherit;
    font-family: var(--font-body);
    font-size: 0.82rem;
    font-weight: 500;
    cursor: pointer;
    transition: all var(--duration) var(--ease);
}

.changelog-more:hover {
    background: var(--brand-glow);
    border-color: transparent;
    color: var(--brand);
}

/* ── Report a problem: the dialog ── */
/* Narrower: a 720px dialog around one textarea reads as a form missing fields. */

.feedback-modal {
    width: min(480px, 100%);
}

.feedback-text {
    min-height: 8rem;
    line-height: 1.55;
}

.feedback-note {
    margin: 0;
    font-size: 0.78rem;
    color: var(--fg-3);
    line-height: 1.5;
}

.feedback-thanks {
    margin: 0;
    font-size: 0.95rem;
    line-height: 1.55;
}

/* ── Loading spinner ── */

.spinner {
    position: relative;
    width: 2.6rem;
    height: 1.5rem;
    --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-action {
    margin: 1rem 0 0.25rem;
}

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

/* ── 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);
    /* Click-through: a toast has nothing to press, and it covered the "+ Add card"
       below it. */
    pointer-events: none;
    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: no scroll-snap during a 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;
}

/* ── Phone (max-width: 599px) ── */

@media (max-width: 599px) {
    /* ── The nav is a bar on the bottom edge ──
       Its sheets open there too, so nothing is out of a thumb's reach. Out of
       flow: pages that size themselves subtract it, and body pads for the rest. */
    .board-header {
        position: fixed;
        inset: auto 0 0 0;
        height: var(--phone-nav-h);
        padding: 0 0.5rem env(safe-area-inset-bottom, 0px);
        gap: 0.25rem;
        /* The hairline moves to the top edge with the bar. */
        box-shadow: 0 -1px 0 var(--border-1);
    }

    /* The bar is four destinations (Home, Calendar, Search, account), the same
       everywhere; a board page names its board in .board-phone-bar. */
    .nav-lead { display: none; }

    /* Four cells of one shape (the search and account triggers are not .nav-tab),
       48px tall with the label inside the touch target. */
    .nav-tabs {
        display: flex;
        align-items: stretch;
    }

    .nav-tab,
    .nav-search-compact,
    .nav-menu-toggle {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 1px;
        width: 100%;
        min-width: 52px;
        height: 48px;
        padding: 0;
        background: none;
        border: 0;
        border-radius: var(--radius-sm);
        color: var(--fg-3);
        text-decoration: none;
        font-family: var(--font-body);
        font-size: 0.625rem;
        font-weight: 500;
        letter-spacing: -0.005em;
        line-height: 1;
        cursor: pointer;
        transition: color var(--duration) var(--ease);
    }

    /* 1 1 0, not auto, so a longer word doesn't get a wider target. .nav-tabs holds
       two of the cells, so two shares; search and account take theirs on the
       wrapper their sheet hangs off, and the button fills it. */
    .nav-tabs { flex: 2 1 0; }

    .nav-tab,
    .nav-search,
    .board-user { flex: 1 1 0; }

    .nav-tab svg,
    .nav-search-compact svg {
        width: 20px;
        height: 20px;
        flex: 0 0 auto;
    }

    .nav-tab-label {
        display: block;
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* The current page in brand colour, no pill: a filled cell is a fifth surface. */
    .nav-tab-active,
    .nav-search-compact[aria-expanded="true"],
    .nav-menu-toggle[aria-expanded="true"] {
        background: none;
        border: 0;
        color: var(--brand);
        font-weight: 600;
    }

    /* The word under it replaces the tablet's filled pill. */
    .nav-search-compact:hover { background: none; }

    .nav-menu-toggle-caret { display: none; }

    /* 20px like the other marks, so the words line up. Scoped to the trigger, or it
       would also shrink .nav-avatar-lg in the sheet. */
    .nav-menu-toggle .nav-avatar {
        width: 20px;
        height: 20px;
    }

    .nav-menu-toggle .nav-avatar-fallback { font-size: 0.6rem; }

    /* Measured from the centred face; the cell's corner floats between tabs. */
    .nav-menu-dot {
        top: 6px;
        right: auto;
        left: calc(50% + 5px);
        width: 8px;
        height: 8px;
    }

    /* Only where the bar exists; sign-in, sign-up and /request have no header. */
    body:has(.board-header) { padding-bottom: var(--phone-nav-h); }

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

    /* A sheet on the bar, like the two menus. The field sits at the bottom with
       results growing upward, so typing never moves it; the markup keeps the
       listbox after the combobox for screen readers. */
    .nav-search-open .nav-search-field {
        /* On the bar, or on the keyboard: the sheet is fixed to the layout viewport,
           which the keyboard isn't part of. search-interop.js measures it; the
           fallback is the bar. */
        --search-sheet-bottom: max(var(--phone-nav-h), var(--keyboard-inset, 0px));
        display: grid;
        position: fixed;
        inset: auto 0 var(--search-sheet-bottom) 0;
        z-index: 200;
        width: auto;
        grid-template-columns: auto minmax(0, 1fr) auto auto;
        grid-template-rows: minmax(0, auto) auto;
        align-items: center;
        column-gap: 0.5rem;
        row-gap: 0.4rem;
        height: auto;
        max-height: calc(100dvh - var(--search-sheet-bottom) - 2rem);
        padding: 0.5rem 0.75rem 0.7rem;
        border-color: var(--border-1);
        border-bottom: none;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
        /* Upwards only — see --shadow-sheet. */
        box-shadow: var(--shadow-sheet);
        animation: sheetRise 200ms var(--ease-out);
    }

    .nav-search-open .nav-search-mark { grid-area: 2 / 1; }

    /* A grid item: align-items: center applies, so it needs a height. */
    .nav-search-open .nav-search-input {
        grid-area: 2 / 2;
        height: 44px;
        padding: 0 1rem;
        font-size: 1rem;
        background: var(--bg-2);
        border: 1px solid var(--border-2);
        border-radius: var(--radius-pill);
    }

    .nav-search-open .nav-search-clear {
        grid-area: 2 / 3;
        width: 30px;
        height: 30px;
    }

    /* With the keyboard up the sheet stands on it, hiding the trigger, so this is
       the way out. */
    .nav-search-open .nav-search-close {
        display: block;
        grid-area: 2 / 4;
        height: 44px;
        padding: 0 0.4rem;
        border: 0;
        background: none;
        color: var(--brand);
        font: inherit;
        font-size: 0.92rem;
        font-weight: 600;
        cursor: pointer;
    }

    /* Scrolls on its own above the field, which never moves. */
    .nav-search-open .nav-search-panel {
        grid-area: 1 / 1 / 2 / -1;
        /* Fills its track (the row below centres); min-height: 0 lets it shrink below
           its content so it scrolls. */
        align-self: stretch;
        min-height: 0;
        position: static;
        max-height: min(58dvh, 26rem);
        overflow-y: auto;
        padding: 0.2rem 0 0.1rem;
        background: none;
        border: 0;
        border-radius: 0;
        box-shadow: none;
        animation: none;
    }

    /* A sheet on the bar. It rises on the transform .nav-menu-open already
       animates, since the menu is always in the DOM and an animation would play on
       load. A short rise, not from under the bar: inside the header's stacking
       context it would be drawn over the bar. */
    .nav-menu {
        position: fixed;
        inset: auto 0 var(--phone-nav-h) 0;
        top: auto;
        min-width: 0;
        /* The bar's height comes off the old ceiling, so the strip above stays the same. */
        max-height: calc(85dvh - var(--phone-nav-h));
        overflow-y: auto;
        padding: 0.5rem 0.75rem 0.85rem;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
        border-bottom: none;
        /* Upwards only — see --shadow-sheet. */
        box-shadow: var(--shadow-sheet);
        transform: translateY(10px);
    }

    .nav-menu-open { transform: translateY(0); }

    .nav-menu-sheet-handle {
        display: block;
        width: 36px;
        height: 4px;
        flex: 0 0 auto;
        margin: 0.1rem auto 0.5rem;
        border-radius: 2px;
        background: var(--border-3);
    }

    /* Visible here, since the sheet covers the board. It stops at the bar: at
       z-index -1 inside the header's context it would tint the bar under its
       still-crisp buttons. */
    .nav-menu-backdrop {
        inset: 0 0 var(--phone-nav-h) 0;
        background: rgba(25, 21, 19, 0.45);
        animation: fadeIn var(--duration) var(--ease-out);
    }

    /* Thumb-sized rows; the account reads as the sheet's heading. */
    .nav-menu-account { padding: 0.35rem 0.6rem 0.75rem; }

    .nav-menu .nav-menu-row,
    .nav-menu-link,
    .nav-menu-signout {
        min-height: 44px;
        font-size: 0.95rem;
    }

    .nav-menu-group { padding: 0.2rem 0; }

    .nav-pref-options button {
        min-height: 38px;
        padding: 0.4rem 0.75rem;
        font-size: 0.85rem;
    }

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

    /* Board padding, column rail and card padding each shrink to ~2/3 here: at
       desktop values the three cost 98px of a 412px phone. None goes to zero,
       since the steps between page, column and card keep a board legible. */
    .board {
        --column-rail: 0.7rem;
        --card-inset: 0.85rem;
        padding: 0.5rem;
        gap: 0.5rem;
        scroll-snap-type: x mandatory;
        scroll-padding-inline: 0.5rem;
        -webkit-overflow-scrolling: touch;
        /* Whatever the two bars leave; body pads by the bottom one. */
        height: calc(100vh - var(--phone-nav-h) - var(--phone-board-title-h));
        height: calc(100dvh - var(--phone-nav-h) - var(--phone-board-title-h));
    }

    /* The board's own bar (BoardPhoneBar), since the bottom one no longer names
       it; it takes the bottom bar's height, surface and hairline. */
    .board-phone-bar {
        display: flex;
        align-items: center;
        gap: 0.35rem;
        height: var(--phone-board-title-h);
        padding: 0 0.4rem;
        background: var(--bg-1);
        box-shadow: 0 1px 0 var(--border-1);
    }

    /* The board's home tile in miniature, in its colour. */
    .board-phone-mark {
        display: flex;
        align-items: flex-start;
        justify-content: center;
        gap: 2px;
        flex: none;
        width: 30px;
        height: 30px;
        padding-top: 8px;
        border-radius: 9px;
        background: color-mix(in srgb, var(--board-color) 16%, transparent);
    }

    .board-phone-mark > span {
        width: 4px;
        border-radius: 2px;
        background: var(--board-color);
    }

    .board-phone-mark > span:nth-child(1) { height: 13px; }
    .board-phone-mark > span:nth-child(2) { height: 9px; }
    .board-phone-mark > span:nth-child(3) { height: 5px; }

    .board-phone-title {
        flex: 1;
        min-width: 0;
        margin: 0 0 0 0.15rem;
        font-family: var(--font-display);
        font-size: 1.1rem;
        font-weight: 700;
        letter-spacing: -0.015em;
        color: var(--fg);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* A 36px disc inside a 44px target; the disc is a gradient so the target can
       be larger than what is drawn. */
    .board-phone-action {
        display: grid;
        place-items: center;
        flex: none;
        width: 44px;
        height: 44px;
        border-radius: 50%;
        color: var(--fg-2);
        background: radial-gradient(circle closest-side, var(--bg-2) calc(100% - 4px), transparent calc(100% - 3.5px));
        transition: color var(--duration) var(--ease);
    }

    .board-phone-action:active { color: var(--brand); }

    .column {
        width: calc(100vw - 1rem);
        min-width: calc(100vw - 1rem);
        flex: 0 0 auto;
        scroll-snap-align: start;
        /* The board's top padding, then the pagination pill's strip: the pill is
           fixed, so without the strip it covers the last card. */
        max-height: calc(100dvh - var(--phone-nav-h) - var(--phone-board-title-h) - 0.5rem
                         - var(--phone-pager-h) - 2 * var(--phone-pager-gap));
    }

    /* The filter button sets the height. The left inset is rail plus card inset,
       so the label lines up with the card titles; the right stays on the rail. */
    .column-header {
        padding: 0.6rem var(--column-rail) 0.4rem
                 calc(var(--column-rail) + var(--card-inset));
    }

    .column-cards {
        padding: 0.35rem var(--column-rail) 0.5rem;
        gap: 0.5rem;
    }

    /* The card's own step, clear of the shadow; the header label lines up with it. */
    .card { padding: 0.7rem var(--card-inset); }

    /* Matches the header's top inset, as at every width. */
    .column-add { margin: 0.3rem var(--column-rail) 0.6rem; }

    /* Sized against the desktop header; here body already pads for the bar. */
    .board-loading,
    .empty-state { min-height: calc(100dvh - var(--phone-nav-h)); }

    /* Thumb-sized; the panel widens to the phone's column. */
    .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;
        /* One gap above the bar; see --phone-pager-gap. */
        bottom: calc(var(--phone-pager-gap) + var(--phone-nav-h));
        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);
    }

    /* While dragging, each dot becomes a drop target named after its column. */
    .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;
    }

    /* SortableJS puts the .card-ghost into whichever target is under the drag. */
    .board-pagination .board-dot:has(.card-ghost) {
        background: var(--brand-glow) !important;
        border-color: var(--brand) !important;
        transform: translateY(-2px);
    }

    /* board-pagination.js's onAdd moves a dropped card back before Blazor
       re-renders; this hides one if any path strands it here. */
    .board-pagination .board-dot .card {
        display: none;
    }

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

    .modal-backdrop {
        padding: 0;
        align-items: flex-end;
    }

    .modal-backdrop::before {
        /* No blur on a phone: backdrop-filter re-filters the whole viewport during the
           slide and dropped frames on weak GPUs. The scrim darkens a little more. */
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(25, 21, 19, 0.58);
    }

    .modal {
        width: 100%;
        /* Not 100vh, which excludes the browser toolbar and pushed the sheet's header
           above the top edge. */
        max-height: 100%;
        border-radius: var(--radius) var(--radius) 0 0;
        animation: slideUpMobile 300ms var(--ease-out);
        /* Promoted before the first frame, so the slide is on the compositor from the start. */
        will-change: transform;
    }

    /* Full screen from its first frame: sized to content, it grew as the thread
       loaded mid-slide, changing the distance slideUpMobile travels. Shorter
       dialogs stay sheets. */
    .card-modal {
        height: 100%;
    }

    .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 on focus; the panel shrinks with the field so
       both tabs keep one 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;
    }

    .move-select {
        flex: 1 1 100%;
        min-height: 44px;
    }

    .move-go {
        flex: 1 1 100%;
        min-height: 44px;
    }

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

    /* Full width in the stacked form. */
    .admin-form input[type="color"] { width: 100%; }

    /* Tables become stacked cards on a phone; data-label captions (::before)
       replace 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 .admin-cell-fit { width: 100%; }

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

    /* Five controls don't fit, and flex-end pushed the overflow off the left edge.
       Wrapped from the left; the basis forces Save/Delete onto their own line. */
    .admin-sortable-item .admin-row-actions {
        flex-wrap: wrap;
        justify-content: flex-start;
    }

    .admin-sortable-item .admin-row-actions button {
        flex: 1 1 45%;
    }

    .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 is not padded here: the whole tile is the target, and its padding
       lives in the board block above. */

    /* Only pickable chips are touch targets; the chips on a card are labels. */
    .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;
    }

    .assignee-add,
    .assignee-option {
        min-height: 44px;
    }

    /* 16px, or iOS zooms on focus. */
    .assignee-search {
        min-height: 44px;
        font-size: 16px;
    }

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

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

    /* ── Toast ── */

    .toast-container {
        bottom: calc(1rem + var(--phone-nav-h));
        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); }
}

/* The nav sheets rest on the bar, so they rise a little rather than slide up
   from below it (see .nav-menu above). The card dialog keeps slideUpMobile. */
@keyframes sheetRise {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}

/* ── Tablet (600px – 959px) ── */

@media (min-width: 600px) and (max-width: 959px) {
    .board {
        padding: 1.15rem;
        gap: 1rem;
    }

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

/* ── Desktop (960px and up) ── */

@media (min-width: 960px) {
    /* Columns share the spare width. The floor keeps a crowded board scrolling; the
       ceiling stops near a 70-character description line. */
    .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);
}

/* Three paragraphs sit above the form, so tighter than .login-card > p's
   1.5rem, which pushed the field below the fold on a phone. */
.login-card > .access-identity,
.login-card > .access-ask,
.login-card > .access-pending {
    margin-bottom: 0.75rem;
}

.login-card > .access-ask {
    margin-bottom: 1.25rem;
}

.access-identity {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: center;
    gap: 0.4rem;
    font-size: 0.88rem;
}

.access-identity code {
    font-family: var(--font-mono);
    font-size: 0.88rem;
    color: var(--fg);
    background: var(--bg-2);
    border-radius: var(--radius-xs);
    padding: 0.1rem 0.45rem;
}

.access-pending {
    background: var(--brand-glow);
    color: var(--fg);
    border-radius: var(--radius-xs);
    padding: 0.75rem 1rem;
    font-size: 0.9rem;
    line-height: 1.5;
}

/* A card, not a table row: as a column, the message (overflow-wrap: anywhere,
   so a one-character min-content) was starved to a ribbon. */
.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;
}

.request-verified {
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--success) 16%, transparent);
    color: var(--success);
    white-space: nowrap;
}

.request-email {
    font-size: 0.85rem;
    color: var(--fg-3);
    overflow-wrap: anywhere;
}

.request-email-none {
    font-style: italic;
}

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

.request-kind {
    font-size: 0.74rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--fg-3);
    white-space: nowrap;
}

.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 ── */

.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 ── */

/* Otherwise the only left-aligned element on the centred card. */
.atproto-login-form .atproto-login-hint {
    display: block;
    text-align: center;
    margin-top: 0.6rem;
    line-height: 1.55;
}

/* Left-aligned on purpose in the centred card: a sentence to read, not 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;
}

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

/* Plays once per load and per auth page switch. js/auth-enter.js drives the
   data-auth state machine: the card's nodes are created more than once, and a
   plain animation replayed on each. */
@keyframes auth-card-in {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
}

/* Hidden before the entrance, so the discarded prerendered card never animates;
   only with js, so without it the card is simply visible. data-auth="done"
   matches nothing, so later node churn can't restart it. */
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;
    }
}

.admin-section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

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

.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 ── */

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

/* ── Wind details and focus ── */

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

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

/* One brand focus ring. No border-radius: 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;
    }
}

/* ── Texture and entrances ── */

/* Fine grain over everything; fixed and non-interactive. */
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-header {
    position: relative;
}

.column-header::after {
    content: "";
    position: absolute;
    left: var(--column-rail);
    right: var(--column-rail);
    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);
}

/* Hover-only for .card:hover's reason: the tap that opens a card hovers its
   column, and the fade repainted the board under the dialog's entrance. */
@media (hover: hover) {
    .column:hover .column-header::after {
        opacity: 0.8;
    }
}

@keyframes rise-in {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}

/* The columns are animated from JS (the board page calls it once its data has
   arrived), never by a CSS rule: the board is built up to three times per load,
   and a creation-keyed animation raced that churn. This only hides them until
   then, and only with js. The timings live here; the JS reads them off :root. */
:root {
    --entrance-duration: 450ms;
    --entrance-lead: 30ms;
    --entrance-stagger: 60ms;
    --entrance-rise: 14px;
    /* The home page may use a spring; the board may not. */
    --home-stagger: 35ms;
    --home-pop-duration: 420ms;
    --ease-spring: cubic-bezier(0.34, 1.45, 0.64, 1);
}

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

/* Home and calendar parts, hidden until auth-enter.js animates them like the
   columns; there is no CSS entrance rule for them on purpose. */
html.js:not([data-home-entered]) .home-title,
html.js:not([data-home-entered]) .home-deleted-title,
html.js:not([data-home-entered]) .foryou-card,
html.js:not([data-home-entered]) .home-tiles > .board-tile,
html.js:not([data-home-entered]) .home-empty,
html.js:not([data-home-entered]) .home-deleted-row {
    opacity: 0;
}

html.js:not([data-calendar-entered]) .calendar-week,
html.js:not([data-calendar-entered]) .timeline-row,
html.js:not([data-calendar-entered]) .calendar-agenda-day {
    opacity: 0;
}

/* Gated on the card's own state, so both halves of the entrance stay in 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;
    }
}
