/* DayRota — one stylesheet, self-hosted font, no build step.

   Inter, because the mocks are drawn in it (`mocks/board.html` loads it from Google Fonts) and the
   app was not: `-apple-system` is San Francisco on a Mac, which is wider and rounder at the same
   weight. The operator's "use the same skinny font as from the mocks".

   Self-hosted because the CSP is `default-src 'self'`: a font from fonts.googleapis.com is a request
   the browser refuses. Four weights, latin subset, ~22KB each, with the licence beside them in
   `assets/fonts/`. `swap`, so a slow font never holds the page; the fallbacks are the old stack,
   which is what most people were looking at anyway. */

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('fonts/inter-latin-400-normal.woff2') format('woff2');
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('fonts/inter-latin-500-normal.woff2') format('woff2');
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('fonts/inter-latin-600-normal.woff2') format('woff2');
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('fonts/inter-latin-700-normal.woff2') format('woff2');
}
:root {
    --indigo: #4f46e5;
    --indigo-dark: #3730a3;
    --amber: #f59e0b;
    --ink: #1f2937;
    --muted: #6b7280;
    --line: #e5e7eb;
    --bg: #f9fafb;
    --danger: #b91c1c;
    --ok: #047857;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    min-height: 100vh;
    display: grid;
    place-items: center;
    padding: 2rem 1rem;
    background: var(--bg);
    color: var(--ink);
    font: 16px/1.5 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.shell { width: 100%; max-width: 26rem; }

.brand {
    display: flex;
    align-items: center;
    gap: .625rem;
    margin-bottom: 1.5rem;
    font-weight: 600;
    letter-spacing: -.01em;
}

h1 { font-size: 1.5rem; margin: 0 0 .25rem; letter-spacing: -.02em; }
h2 { font-size: 1rem; margin: 2rem 0 .5rem; color: var(--muted); font-weight: 600; }
.lede { margin: 0 0 1.5rem; color: var(--muted); }

form { display: grid; gap: .5rem; }

label { font-size: .875rem; font-weight: 500; margin-top: .5rem; }
.hint { margin: -0.25rem 0 .25rem; font-size: .8125rem; color: var(--muted); }

/* Text-shaped inputs fill the form they are in — the auth pages and every panel want that. A
   checkbox or a radio does not: `width: 100%` on one of those stretched it to the width of the page
   and made the label look broken. */
input:not([type="checkbox"]):not([type="radio"]) {
    width: 100%;
    padding: .625rem .75rem;
    border: 1px solid var(--line);
    border-radius: .5rem;
    font: inherit;
    background: #fff;
}

/* Focus is a halo, not a second border. A 2px indigo outline inside an indigo border was two bright
   rings around every field — most obvious on the panel's title, which takes focus the moment the
   panel opens. The border still changes colour, which is the indicator; the halo is what makes it
   findable. */
input:focus-visible {
    outline: none;
    border-color: var(--indigo);
    box-shadow: 0 0 0 3px rgb(79 70 229 / .22);
}

button {
    margin-top: 1rem;
    padding: .75rem 1rem;
    border: 0;
    border-radius: .5rem;
    background: var(--indigo);
    color: #fff;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

button:hover { background: var(--indigo-dark); }
button[disabled] { opacity: .6; cursor: progress; }
button.secondary { background: transparent; color: var(--muted); border: 1px solid var(--line); }
button.secondary:hover { background: #fff; color: var(--ink); }

.error { margin: .5rem 0 0; color: var(--danger); font-size: .875rem; }
.ok { margin: .5rem 0 0; color: var(--ok); font-size: .875rem; }

.aside { margin-top: 1.5rem; font-size: .875rem; }
.aside a { color: var(--indigo); }

.people { list-style: none; margin: 0; padding: 0; display: grid; gap: .375rem; }
.people li {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .625rem .75rem;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: .5rem;
}
.people .name { flex: 1; }

.tag {
    font-size: .75rem;
    padding: .125rem .5rem;
    border-radius: 999px;
    background: #eef2ff;
    color: var(--indigo-dark);
}
.tag.quiet { background: #f3f4f6; color: var(--muted); }

.meta { margin-top: 2.5rem; font-size: .75rem; color: var(--muted); }

.hint-keys { margin-top: .25rem; }
.hint-keys kbd {
    font: inherit;
    font-weight: 600;
    padding: 0 .25rem;
    border: 1px solid var(--line);
    border-bottom-width: 2px;
    border-radius: .25rem;
    background: #fff;
    color: var(--ink);
}

/* ---- board -------------------------------------------------------------------
   Laid out to mocks/board.html: a sticky app bar, a "Today" title row with the filter on the right,
   and tinted columns holding white cards. No control appears here that the app cannot do — the
   mock's "Reset board" and theme toggle have no feature behind them yet. */
.shell.wide { max-width: 68rem; }

body.has-appbar { display: block; place-items: initial; padding: 0; background: #f8fafc; }
body.has-appbar .shell { max-width: 100rem; margin: 0 auto; padding: 1.25rem 1rem 2.5rem; }

.appbar {
    position: sticky;
    top: 0;
    z-index: 40;
    border-bottom: 1px solid #e2e8f0;
    background: rgb(255 255 255 / .92);
    backdrop-filter: blur(8px);
}
.appbar-inner {
    display: flex;
    align-items: center;
    gap: .75rem;
    height: 3.5rem;
    max-width: 100rem;
    margin: 0 auto;
    padding: 0 1rem;
}
.appbar-brand { display: flex; align-items: center; gap: .5rem; text-decoration: none; }
.appbar-name { font-size: 1.125rem; font-weight: 800; letter-spacing: -.02em; color: #0f172a; }
.beta {
    padding: .0625rem .375rem;
    border-radius: 999px;
    background: #eef2ff;
    color: #4338ca;
    font-size: .625rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
}
.appbar-divider { display: none; width: 1px; height: 1.5rem; background: #e2e8f0; }
.appbar-space { font-size: .875rem; font-weight: 600; color: #334155; white-space: nowrap; }

/* The space's kind, when it has one the product names: a small uppercase tag beside the name, as
   the family mock draws it. */
.appbar-badge {
    padding: .0625rem .4rem;
    border-radius: 999px;
    background: #fef3c7;
    color: #92400e;
    font-size: .625rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    white-space: nowrap;
}
/* On a phone the bar keeps three things: the brand, who you are, and the way out. The space's
   name, its kind and the auto-refresh note are all secondary there — the name is on the page's
   own title, which the app bar cannot show without dropping the date. */
@media (max-width: 40rem) {
    .appbar-space, .appbar-me-name, .appbar-badge { display: none; }
    /* `.live` is declared further down the file with `display: inline-flex`, so a bare class
       selector here would lose to it on source order and the note would stay in the bar. */
    .appbar-right .live { display: none; }
    .appbar-inner { gap: .5rem; }
    .appbar-right { gap: .5rem; }
}
@media (min-width: 40rem) { .appbar-divider { display: block; } }

.appbar-right { display: flex; align-items: center; gap: .75rem; margin-left: auto; }
/* "Sign out" is two words in a bar that is 100% wide on a phone: let it wrap and it takes two
   lines and pushes the bar's height off its own 3.5rem. */
.appbar-right .link { white-space: nowrap; }
.appbar-search { position: relative; display: none; }
.appbar-search svg { position: absolute; top: .5625rem; left: .625rem; color: #94a3b8; pointer-events: none; }
/* `[type="search"]` is not decoration: the global input rule is
   `input:not([type="checkbox"]):not([type="radio"])` — specificity (0,2,1) — so a bare
   `.appbar-search input` loses to it and the box loses the padding that makes room for the icon. */
.appbar-search input[type="search"] {
    width: 14rem;
    padding: .375rem .5rem .375rem 2rem;
    font-size: .875rem;
    color: #334155;
    border: 1px solid #cbd5e1;
    border-radius: .5rem;
    background: #fff;
}
.appbar-search input[type="search"]::placeholder { color: #94a3b8; }
.appbar-search input[type="search"]:focus { outline: 2px solid rgb(79 70 229 / .3); border-color: var(--indigo); }
@media (min-width: 48rem) { .appbar-search { display: block; } }

.appbar-me { display: flex; align-items: center; gap: .5rem; }
.appbar-me-name { font-size: .875rem; font-weight: 600; color: #0f172a; }

/* The mock's title row: what this is on the left, how to narrow it on the right. */
.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: .75rem;
}
.page-title h1 {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
    font-size: 1.5rem;
    font-weight: 800;
    letter-spacing: -.02em;
    color: #0f172a;
}
.bar-summary { margin: .25rem 0 0; font-size: .875rem; color: #64748b; }
.bar-summary .overdue { color: var(--danger); font-weight: 600; }
.pill.today { background: #fef3c7; color: #92400e; font-weight: 600; }
.page-tools { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }

/* Search, whose cards, and the order: a plain GET form, so the filter lives in the URL. The
   search input itself sits in the app bar and joins this form with `form="filter-form"`. */
.filters { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
/* Every select looks like the input beside it. Without a base rule, the team page's timezone, colour
   and role pickers and the day's time pickers were the browser's own controls — the only unstyled
   things left in the product. */
select {
    font: inherit;
    color: var(--ink);
    background-color: #fff;
    border: 1px solid #cbd5e1;
    border-radius: .5rem;
    /* `h-8` on the kit's scale: 2rem, the same height as a toolbar button and a segmented control.
       Left to its own padding a select came out at 34px — two pixels too tall to sit in a row. */
    height: 2rem;
    padding: 0 .5rem;
    max-width: 100%;
}
select:focus-visible { outline: none; border-color: var(--indigo); box-shadow: 0 0 0 3px rgb(79 70 229 / .22); }

.filters select { padding: 0 .25rem; font-size: .8125rem; }
.filters .sort { display: flex; align-items: center; gap: .375rem; margin: 0; }
.filters .apply { margin: 0; padding: .4375rem .75rem; font-size: .8125rem; }
.filters .apply[hidden] { display: none; }

/* One height for every control in a toolbar. A native <select>, the segmented control and a button
   each arrive with their own metrics — 31px, 32px and 36px as measured on the board — so the row
   never lines up until the height is stated. */
.page-tools { --control: 2rem; }
.page-tools .segmented,
.page-tools select,
.page-tools .apply,
.page-tools > .button { box-sizing: border-box; height: var(--control); }

/* A quiet secondary button, as in the mock's bar. */
.button {
    display: inline-flex;
    align-items: center;
    gap: .375rem;
    padding: .4375rem .75rem;
    font-size: .8125rem;
    font-weight: 600;
    color: #334155;
    text-decoration: none;
    background: #fff;
    border: 1px solid #cbd5e1;
    border-radius: .5rem;
}
.button:hover { background: #f8fafc; color: #0f172a; }

/* A button in a ROW is a row item, not a form's submit.
   The global `button` rule carries a 1rem top margin, which is right for a submit sitting under a
   stack of fields and wrong for every button that shares a line with something: it pushed the
   board's "Reset board" 8px below the filter controls, the card modal's "Add" off the todo input,
   and the add-card form's button off its own input. This is that list, in one place. */
.button,
.page-tools button,
.page-tools > a,
.filters button,
.todo-add button,
.todo-add input,
.modal-foot button,
.modal-foot a,
.hours-add-day,
.hours-window button,
.blk-tools button,
.agenda-tick { margin-top: 0; }
/* The third link in a row is the one you have to earn: no border, so the primary action still
   reads as the primary action. */
.button.quiet { border-color: transparent; background: none; color: var(--muted); }
.button.quiet:hover { background: #f1f5f9; color: #0f172a; }

/* Real radios behind a segmented control: keyboard arrows work, and it degrades to radio
   buttons if the stylesheet ever fails. */
.segmented {
    display: inline-flex;
    gap: .125rem;
    padding: .125rem;
    box-sizing: border-box;
    border-radius: .5rem;
    background: rgb(226 232 240 / .8);
}
.seg { margin: 0; display: flex; }
.seg input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.seg span {
    /* Centred inside whatever height the toolbar gives the control, rather than sized by its own
       padding — that is what made it 28px against a 36px button. */
    box-sizing: border-box;
    display: flex;
    align-items: center;
    height: 100%;
    padding: 0 .625rem;
    border-radius: .375rem;
    font-size: .8125rem;
    font-weight: 600;
    color: #64748b;
    cursor: pointer;
}
.seg input:checked + span { background: #fff; color: #0f172a; box-shadow: 0 1px 2px rgb(15 23 42 / .12); }
.seg input:focus-visible + span { outline: 2px solid rgb(79 70 229 / .45); outline-offset: 1px; }

.empty { margin: 0 0 .5rem; font-size: .8125rem; color: var(--muted); }

.empty-board {
    margin: 0 0 1rem;
    padding: .75rem 1rem;
    font-size: .875rem;
    border: 1px dashed var(--line);
    border-radius: .5rem;
    background: #fff;
    color: var(--muted);
}

/* "Someone else changed this board" — offered, never taken, while the user is mid-edit. */
.notice {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0 0 .75rem;
    padding: .5rem .75rem;
    font-size: .8125rem;
    border: 1px solid #fcd34d;
    border-radius: .5rem;
    background: #fffbeb;
    color: #92400e;
}
.notice[hidden] { display: none; }
.notice button {
    margin: 0;
    padding: .1875rem .5rem;
    font-size: .8125rem;
    font-weight: 600;
    background: #fff;
    color: #92400e;
    border: 1px solid #fcd34d;
    border-radius: .375rem;
}

.live { display: inline-flex; align-items: center; gap: .3125rem; font-size: .75rem; color: var(--muted); }
.live::before { content: ""; width: .5rem; height: .5rem; border-radius: 999px; background: var(--ok); }

/* Toast — matches mocks/assets/mock.js: a dark pill at the bottom, Undo inside it. */
.toast-host {
    position: fixed;
    inset-inline: 0;
    bottom: 1.25rem;
    z-index: 60;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
    pointer-events: none;
}
.toast {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .625rem 1rem;
    border-radius: .75rem;
    background: #1f2937;
    color: #fff;
    font-size: .875rem;
    font-weight: 500;
    box-shadow: 0 20px 40px -20px rgb(15 23 42 / .6);
    pointer-events: auto;
    animation: toast-in .16s ease-out;
}
.toast.is-going { opacity: 0; transform: translateY(6px); transition: opacity .2s ease, transform .2s ease; }
.toast button {
    margin: 0;
    padding: .25rem .5rem;
    font-size: .75rem;
    font-weight: 700;
    background: rgb(255 255 255 / .12);
    color: #fff;
    border-radius: .5rem;
}
.toast button:hover { background: rgb(255 255 255 / .22); }

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

@media (prefers-reduced-motion: reduce) {
    .toast { animation: none; }
    .toast.is-going { transition: none; }
}

/* ---- the day ------------------------------------------------------------------ */
/*
 * Blocks are positioned by slot CLASS, not by an inline `style` attribute: the page's CSP is
 * `style-src 'self'`, which blocks style attributes, and loosening it to place a rectangle would
 * be a bad trade. The grid is half-hour slots from 06:00 to 20:00 (28 of them), so `.at-n` and
 * `.len-n` below are generated by hand from one formula: n × 1.5rem.
 */
.day-nav { display: flex; gap: .75rem; font-size: .8125rem; align-items: center; }
.day-nav a { color: var(--muted); text-decoration: none; }
.day-nav a:hover { color: var(--ink); text-decoration: underline; }

.day {
    display: grid;
    grid-template-columns: 3.5rem repeat(auto-fit, minmax(9rem, 1fr));
    /* No gap: the lanes are separated by their own rules, as in the mock. `auto-fit` leaves empty
       zero-width tracks behind, and every one of those would still have been given a gap — which
       is what left a strip of nothing down the right-hand side. */
    gap: 0;
    align-items: start;
    overflow-x: auto;
    /* The head row's height, in one place: the ruler and every lane have to agree on it or the
       hour labels drift away from the gridlines. 8px padding + the avatar's 24px + the 2px gap +
       the 11px hours line + 8px padding = 58.5px; 60px leaves it room. The hours line never wraps
       (it ellipses instead), so this can be a fixed number. */
    --lane-head: 3.75rem;
}

.day-ruler { display: grid; gap: 0; border-right: 1px solid var(--line); background: #f8fafc; }
.ruler-head { height: var(--lane-head); border-bottom: 1px solid var(--line); background: #f8fafc; }

/* The mock puts each hour's label ON its line rather than under it, so the label is taken out of
   the flow and pulled up by half its height. The first one would otherwise be sliced by the top of
   the ruler, so it sits just inside its band instead. The gutter draws no hour rules of its own —
   the label is the mark, as there. */
.ruler-hour { position: relative; height: 6rem; }
.ruler-hour span {
    position: absolute;
    top: 0;
    right: .25rem;
    transform: translateY(-50%);
    font-size: .6875rem;
    color: var(--muted);
}
.ruler-hour.is-first span { transform: translateY(1px); }

/* The lane rule runs the whole height of the lane, as in the mock — heads only would leave the
   columns unseparated wherever a lane has nothing in it. The last lane needs no rule of its own:
   the panel's own edge is already there. */
.lane { min-width: 9rem; border-right: 1px solid var(--line); }
.lane:last-child { border-right: 0; }

/* The mock's lane cell: who on one line, then the hours being measured against and planned /
   available on the next. */
.lane-head {
    display: block;
    height: var(--lane-head);
    margin: 0;
    padding: .5rem .75rem;
    font-size: .75rem;
    font-weight: 600;
    text-transform: none;
    color: var(--ink);
    border-bottom: 1px solid var(--line);
}
.lane-who { display: flex; align-items: center; gap: .375rem; min-width: 0; }
.lane-name {
    font-size: .875rem;
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.lane-sub {
    display: block;
    margin-top: .125rem;
    font-size: .6875rem;
    line-height: 1.4;
    font-weight: 500;
    color: #94a3b8;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.lane-sub strong { color: #475569; font-weight: 700; }
.lane-sub.is-over strong { color: var(--danger); }

.lane-body {
    position: relative;
    /* Its own block formatting context, so a child's top margin cannot escape and push the whole
       lane down. It did: `.lane-empty`'s 4px margin lifted an *empty* lane's gridlines 4px away
       from the ruler's hour labels, while lanes with work stayed put — the columns disagreed by
       exactly the margin of a paragraph that was inside only some of them. `flow-root` rather
       than `overflow: hidden`, which would clip the last hour off every lane. */
    display: flow-root;
    /* Three rules, as the mock has them: the hour, the half hour and the quarter. A quarter-hour
       grid with only hour lines is a grid you cannot read a 15-minute block against. */
    background-image:
        repeating-linear-gradient(to bottom, rgb(148 163 184 / .20) 0 1px, transparent 1px 6rem),
        repeating-linear-gradient(to bottom, rgb(148 163 184 / .10) 0 1px, transparent 1px 3rem),
        repeating-linear-gradient(to bottom, rgb(148 163 184 / .05) 0 1px, transparent 1px 1.5rem);
    border-radius: .375rem;
}

.lane-empty { margin: 0; padding: .25rem .25rem 0; font-size: .75rem; color: var(--muted); }

.blk {
    position: absolute;
    inset-inline: 0;
    padding: .1875rem .375rem;
    border-radius: .375rem;
    background: #eef2ff;
    border-left: 3px solid var(--indigo);
    overflow: hidden;
}
/* Title and time share the first line.
   They used to be two stacked lines, which a single-slot block (22px) cannot hold: the time sat
   37px down in a 22px box and was clipped away entirely. Inline it always fits, and a long title is
   trimmed rather than pushing its own time off the block. */
.blk-head {
    display: flex;
    align-items: baseline;
    gap: .25rem;
    min-width: 0;
}
.blk-title {
    margin: 0;
    font-size: .75rem;
    font-weight: 600;
    line-height: 1.2;
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.blk-time { margin: 0; font-size: .6875rem; color: var(--muted); flex: none; white-space: nowrap; }
/* The two controls sit in this corner on `.blk`, over the block (`.blk-tools`). The head row keeps
   that square free, or the time was drawn underneath them. */
.blk.has-tools .blk-head { padding-right: 2.25rem; }

/* The rest of the card's facts, on the blocks that have a second line for them
   (`DayLanes::showsFacts`: an hour of work, and at least two slots drawn). The same chips as the
   agenda and the tiles, one size down: a 60-minute block is 46px tall. */
.blk-facts {
    display: flex;
    flex-wrap: wrap;
    gap: .0625rem .25rem;
    margin: .125rem 0 0;
    overflow: hidden;
}
.blk-facts .chip {
    padding: 0 .25rem;
    border-radius: .25rem;
    font-size: .5625rem;
    line-height: 1.35;
}
.blk.colour-yellow { border-left-color: #f59e0b; background: #fffbeb; }
.blk.colour-orange { border-left-color: #ea580c; background: #fff7ed; }
.blk.colour-green { border-left-color: #059669; background: #ecfdf5; }
.blk.colour-red { border-left-color: #dc2626; background: #fef2f2; }
.blk.is-travel {
    background: repeating-linear-gradient(45deg, #f3f4f6 0 4px, #e5e7eb 4px 8px);
    border-left: 3px dashed var(--muted);
}
.blk.is-travel .blk-time { display: none; }
.blk.is-done { opacity: .55; }
.blk.is-done .blk-title { text-decoration: line-through; }

/* --- Drag and resize (Spike A) ---------------------------------------------------------------
   `touch-action: none` on the block and the grip, never on the lane body: a finger on a block
   drags, a finger on the grid still scrolls the page. The block is moved by swapping its `at-n`
   and `len-n` classes rather than setting a style attribute, which is what keeps `style-src
   'self'` honest. */

/* A tile is a handle, not prose. Without this, a press-and-drag paints a text selection across the
   page — the card in the column, then the calendar under it — which is how a drag used to look like
   a mistake even when it worked. The rule is on the tiles themselves, not on the grid, because a
   board card is picked up outside the grid and the selection starts on the press. */
.card, .blk, .chip, .blk-grip { user-select: none; -webkit-user-select: none; }

/* ...and while any gesture is running, nothing on the page may be selected. The class is on <html>
   so it covers the distance between the two ends of the drag. */
html.is-gesturing, html.is-gesturing * { user-select: none; -webkit-user-select: none; }
html.is-gesturing { cursor: grabbing; }

.day.is-dragging, .day.is-dragging * { cursor: grabbing; user-select: none; }
.blk { touch-action: none; }
.blk:not(.is-travel) { cursor: grab; }
.blk.is-travel { cursor: default; }

/* The thing in your hand.
   A tile in a list cannot follow the pointer — it stays where it was — so a drag across the page was a
   cursor and nothing else. `drag.js` lifts a copy of it and this is that copy: the card's own title
   and facts, so it is exactly as tall as the card it stands for, raised and tilted. The original
   stays behind faded and dashed.

   No font declarations here on purpose: the copy must inherit what the card inherits, or its text
   re-flows and it is a different size from the thing it is standing in for. */
.drag-follower {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 70;
    pointer-events: none;
    overflow: hidden;
    margin: 0;
    padding: .625rem .75rem;
    border: 1px solid #e2e8f0;
    border-left: 4px solid var(--indigo);
    border-radius: .75rem;
    background: #fff;
    color: var(--ink);
    box-sizing: border-box;
    transform: rotate(-1.5deg) scale(1.03);
    transform-origin: 50% 40%;
    box-shadow: 0 0 0 2px var(--indigo), 0 1rem 2rem rgb(15 23 42 / .28);
}
.drag-follower.is-chip {
    padding: .0625rem .375rem;
    border-left-width: 3px;
    border-radius: .375rem;
    font-size: .6875rem;
    font-weight: 600;
    transform: rotate(-1deg) scale(1.04);
}
.drag-follower.is-chip p { margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drag-follower.colour-yellow { border-left-color: #f59e0b; }
.drag-follower.colour-orange { border-left-color: #ea580c; }
.drag-follower.colour-green { border-left-color: #059669; }
.drag-follower.colour-red { border-left-color: #dc2626; }
.drag-follower.colour-blue { border-left-color: #4f46e5; }

/* What is left behind. Both of these are list items, which cannot travel with the pointer; the copy
   above is the thing being carried, and this is the hole it came out of. */
.card.is-dragging,
.chip.is-dragging {
    opacity: .35;
    cursor: grabbing;
    border-style: dashed;
    box-shadow: none;
}

/* A block is its own follower: the grid moves it slot by slot as the pointer travels, so a second
   copy under the cursor would be a duplicate of the same thing. It comes forward instead. */
.blk.is-dragging {
    z-index: 30;
    cursor: grabbing;
    transform: scale(1.02);
    box-shadow: 0 0 0 2px var(--indigo), 0 .75rem 1.5rem rgb(15 23 42 / .22);
}

/* Where it would land. A dashed outline rather than a solid one: it is a question, not an answer. */
.blk.is-ghost {
    z-index: 2;
    pointer-events: none;
    border-left-style: dashed;
    background: #eef2ff;
    opacity: .7;
}
.card.is-ghost {
    pointer-events: none;
    border-style: dashed;
    border-color: var(--indigo);
    background: #eef2ff;
    box-shadow: none;
}
.ghost-title {
    margin: 0;
    font-size: .875rem;
    font-weight: 600;
    line-height: 1.3;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.lane-body.is-drop-target { background: rgb(79 70 229 / .06); }
/* A block being dragged into an empty lane sits over its "Free all day" note; the note is about the
   lane's state before the drop, so it steps aside while the lane is the target. */
.lane-body.is-drop-target .lane-empty { visibility: hidden; }
.cards.is-drop-target,
.unscheduled.is-drop-target {
    outline: 2px dashed var(--indigo);
    outline-offset: .25rem;
    border-radius: .75rem;
    background: rgb(79 70 229 / .04);
}

/* The grip is the bottom edge of the block, not a decorative line — but a 30-minute block is only
   1.375rem tall, so a grip of 0.75rem left almost nothing to *drag* the block by. Half a rem is the
   compromise: still a target, no longer most of the block. */
.blk-grip {
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    height: .5rem;
    cursor: ns-resize;
    touch-action: none;
}
.blk-grip::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: .1875rem;
    width: 1.25rem;
    height: .125rem;
    margin-left: -.625rem;
    border-radius: 1px;
    background: currentColor;
    opacity: .25;
}
.blk.is-resizing .blk-grip::after { opacity: .6; }

/* The grid is a 15-minute slot grid of *variable* length (R19), so these cover every case:
   2 slots (half an hour) to 96 (the whole day). One formula — n × 1.5rem — written out, because
   a CSP that forbids style attributes cannot take the number from the element. A day of work is
   twice as many slots as it was at half-hour resolution, so the list is twice as long. */
/* top = slot × 1.5rem */
.at-0 { top: 0rem; } .at-1 { top: 1.5rem; } .at-2 { top: 3rem; } .at-3 { top: 4.5rem; }
.at-4 { top: 6rem; } .at-5 { top: 7.5rem; } .at-6 { top: 9rem; } .at-7 { top: 10.5rem; }
.at-8 { top: 12rem; } .at-9 { top: 13.5rem; } .at-10 { top: 15rem; } .at-11 { top: 16.5rem; }
.at-12 { top: 18rem; } .at-13 { top: 19.5rem; } .at-14 { top: 21rem; } .at-15 { top: 22.5rem; }
.at-16 { top: 24rem; } .at-17 { top: 25.5rem; } .at-18 { top: 27rem; } .at-19 { top: 28.5rem; }
.at-20 { top: 30rem; } .at-21 { top: 31.5rem; } .at-22 { top: 33rem; } .at-23 { top: 34.5rem; }
.at-24 { top: 36rem; } .at-25 { top: 37.5rem; } .at-26 { top: 39rem; } .at-27 { top: 40.5rem; }
.at-28 { top: 42rem; } .at-29 { top: 43.5rem; } .at-30 { top: 45rem; } .at-31 { top: 46.5rem; }
.at-32 { top: 48rem; } .at-33 { top: 49.5rem; } .at-34 { top: 51rem; } .at-35 { top: 52.5rem; }
.at-36 { top: 54rem; } .at-37 { top: 55.5rem; } .at-38 { top: 57rem; } .at-39 { top: 58.5rem; }
.at-40 { top: 60rem; } .at-41 { top: 61.5rem; } .at-42 { top: 63rem; } .at-43 { top: 64.5rem; }
.at-44 { top: 66rem; } .at-45 { top: 67.5rem; } .at-46 { top: 69rem; } .at-47 { top: 70.5rem; }
.at-48 { top: 72rem; } .at-49 { top: 73.5rem; } .at-50 { top: 75rem; } .at-51 { top: 76.5rem; }
.at-52 { top: 78rem; } .at-53 { top: 79.5rem; } .at-54 { top: 81rem; } .at-55 { top: 82.5rem; }
.at-56 { top: 84rem; } .at-57 { top: 85.5rem; } .at-58 { top: 87rem; } .at-59 { top: 88.5rem; }
.at-60 { top: 90rem; } .at-61 { top: 91.5rem; } .at-62 { top: 93rem; } .at-63 { top: 94.5rem; }
.at-64 { top: 96rem; } .at-65 { top: 97.5rem; } .at-66 { top: 99rem; } .at-67 { top: 100.5rem; }
.at-68 { top: 102rem; } .at-69 { top: 103.5rem; } .at-70 { top: 105rem; } .at-71 { top: 106.5rem; }
.at-72 { top: 108rem; } .at-73 { top: 109.5rem; } .at-74 { top: 111rem; } .at-75 { top: 112.5rem; }
.at-76 { top: 114rem; } .at-77 { top: 115.5rem; } .at-78 { top: 117rem; } .at-79 { top: 118.5rem; }
.at-80 { top: 120rem; } .at-81 { top: 121.5rem; } .at-82 { top: 123rem; } .at-83 { top: 124.5rem; }
.at-84 { top: 126rem; } .at-85 { top: 127.5rem; } .at-86 { top: 129rem; } .at-87 { top: 130.5rem; }
.at-88 { top: 132rem; } .at-89 { top: 133.5rem; } .at-90 { top: 135rem; } .at-91 { top: 136.5rem; }
.at-92 { top: 138rem; } .at-93 { top: 139.5rem; } .at-94 { top: 141rem; } .at-95 { top: 142.5rem; }
.at-96 { top: 144rem; }

/* height = span × 1.5rem, less 2px so consecutive blocks do not touch */
.len-1 { height: 1.375rem; } .len-2 { height: 2.875rem; } .len-3 { height: 4.375rem; } .len-4 { height: 5.875rem; }
.len-5 { height: 7.375rem; } .len-6 { height: 8.875rem; } .len-7 { height: 10.375rem; } .len-8 { height: 11.875rem; }
.len-9 { height: 13.375rem; } .len-10 { height: 14.875rem; } .len-11 { height: 16.375rem; } .len-12 { height: 17.875rem; }
.len-13 { height: 19.375rem; } .len-14 { height: 20.875rem; } .len-15 { height: 22.375rem; } .len-16 { height: 23.875rem; }
.len-17 { height: 25.375rem; } .len-18 { height: 26.875rem; } .len-19 { height: 28.375rem; } .len-20 { height: 29.875rem; }
.len-21 { height: 31.375rem; } .len-22 { height: 32.875rem; } .len-23 { height: 34.375rem; } .len-24 { height: 35.875rem; }
.len-25 { height: 37.375rem; } .len-26 { height: 38.875rem; } .len-27 { height: 40.375rem; } .len-28 { height: 41.875rem; }
.len-29 { height: 43.375rem; } .len-30 { height: 44.875rem; } .len-31 { height: 46.375rem; } .len-32 { height: 47.875rem; }
.len-33 { height: 49.375rem; } .len-34 { height: 50.875rem; } .len-35 { height: 52.375rem; } .len-36 { height: 53.875rem; }
.len-37 { height: 55.375rem; } .len-38 { height: 56.875rem; } .len-39 { height: 58.375rem; } .len-40 { height: 59.875rem; }
.len-41 { height: 61.375rem; } .len-42 { height: 62.875rem; } .len-43 { height: 64.375rem; } .len-44 { height: 65.875rem; }
.len-45 { height: 67.375rem; } .len-46 { height: 68.875rem; } .len-47 { height: 70.375rem; } .len-48 { height: 71.875rem; }
.len-49 { height: 73.375rem; } .len-50 { height: 74.875rem; } .len-51 { height: 76.375rem; } .len-52 { height: 77.875rem; }
.len-53 { height: 79.375rem; } .len-54 { height: 80.875rem; } .len-55 { height: 82.375rem; } .len-56 { height: 83.875rem; }
.len-57 { height: 85.375rem; } .len-58 { height: 86.875rem; } .len-59 { height: 88.375rem; } .len-60 { height: 89.875rem; }
.len-61 { height: 91.375rem; } .len-62 { height: 92.875rem; } .len-63 { height: 94.375rem; } .len-64 { height: 95.875rem; }
.len-65 { height: 97.375rem; } .len-66 { height: 98.875rem; } .len-67 { height: 100.375rem; } .len-68 { height: 101.875rem; }
.len-69 { height: 103.375rem; } .len-70 { height: 104.875rem; } .len-71 { height: 106.375rem; } .len-72 { height: 107.875rem; }
.len-73 { height: 109.375rem; } .len-74 { height: 110.875rem; } .len-75 { height: 112.375rem; } .len-76 { height: 113.875rem; }
.len-77 { height: 115.375rem; } .len-78 { height: 116.875rem; } .len-79 { height: 118.375rem; } .len-80 { height: 119.875rem; }
.len-81 { height: 121.375rem; } .len-82 { height: 122.875rem; } .len-83 { height: 124.375rem; } .len-84 { height: 125.875rem; }
.len-85 { height: 127.375rem; } .len-86 { height: 128.875rem; } .len-87 { height: 130.375rem; } .len-88 { height: 131.875rem; }
.len-89 { height: 133.375rem; } .len-90 { height: 134.875rem; } .len-91 { height: 136.375rem; } .len-92 { height: 137.875rem; }
.len-93 { height: 139.375rem; } .len-94 { height: 140.875rem; } .len-95 { height: 142.375rem; } .len-96 { height: 143.875rem; }

/* the lane's own height */
.slots-2 { height: 3rem; } .slots-3 { height: 4.5rem; } .slots-4 { height: 6rem; } .slots-5 { height: 7.5rem; }
.slots-6 { height: 9rem; } .slots-7 { height: 10.5rem; } .slots-8 { height: 12rem; } .slots-9 { height: 13.5rem; }
.slots-10 { height: 15rem; } .slots-11 { height: 16.5rem; } .slots-12 { height: 18rem; } .slots-13 { height: 19.5rem; }
.slots-14 { height: 21rem; } .slots-15 { height: 22.5rem; } .slots-16 { height: 24rem; } .slots-17 { height: 25.5rem; }
.slots-18 { height: 27rem; } .slots-19 { height: 28.5rem; } .slots-20 { height: 30rem; } .slots-21 { height: 31.5rem; }
.slots-22 { height: 33rem; } .slots-23 { height: 34.5rem; } .slots-24 { height: 36rem; } .slots-25 { height: 37.5rem; }
.slots-26 { height: 39rem; } .slots-27 { height: 40.5rem; } .slots-28 { height: 42rem; } .slots-29 { height: 43.5rem; }
.slots-30 { height: 45rem; } .slots-31 { height: 46.5rem; } .slots-32 { height: 48rem; } .slots-33 { height: 49.5rem; }
.slots-34 { height: 51rem; } .slots-35 { height: 52.5rem; } .slots-36 { height: 54rem; } .slots-37 { height: 55.5rem; }
.slots-38 { height: 57rem; } .slots-39 { height: 58.5rem; } .slots-40 { height: 60rem; } .slots-41 { height: 61.5rem; }
.slots-42 { height: 63rem; } .slots-43 { height: 64.5rem; } .slots-44 { height: 66rem; } .slots-45 { height: 67.5rem; }
.slots-46 { height: 69rem; } .slots-47 { height: 70.5rem; } .slots-48 { height: 72rem; } .slots-49 { height: 73.5rem; }
.slots-50 { height: 75rem; } .slots-51 { height: 76.5rem; } .slots-52 { height: 78rem; } .slots-53 { height: 79.5rem; }
.slots-54 { height: 81rem; } .slots-55 { height: 82.5rem; } .slots-56 { height: 84rem; } .slots-57 { height: 85.5rem; }
.slots-58 { height: 87rem; } .slots-59 { height: 88.5rem; } .slots-60 { height: 90rem; } .slots-61 { height: 91.5rem; }
.slots-62 { height: 93rem; } .slots-63 { height: 94.5rem; } .slots-64 { height: 96rem; } .slots-65 { height: 97.5rem; }
.slots-66 { height: 99rem; } .slots-67 { height: 100.5rem; } .slots-68 { height: 102rem; } .slots-69 { height: 103.5rem; }
.slots-70 { height: 105rem; } .slots-71 { height: 106.5rem; } .slots-72 { height: 108rem; } .slots-73 { height: 109.5rem; }
.slots-74 { height: 111rem; } .slots-75 { height: 112.5rem; } .slots-76 { height: 114rem; } .slots-77 { height: 115.5rem; }
.slots-78 { height: 117rem; } .slots-79 { height: 118.5rem; } .slots-80 { height: 120rem; } .slots-81 { height: 121.5rem; }
.slots-82 { height: 123rem; } .slots-83 { height: 124.5rem; } .slots-84 { height: 126rem; } .slots-85 { height: 127.5rem; }
.slots-86 { height: 129rem; } .slots-87 { height: 130.5rem; } .slots-88 { height: 132rem; } .slots-89 { height: 133.5rem; }
.slots-90 { height: 135rem; } .slots-91 { height: 136.5rem; } .slots-92 { height: 138rem; } .slots-93 { height: 139.5rem; }
.slots-94 { height: 141rem; } .slots-95 { height: 142.5rem; } .slots-96 { height: 144rem; }

.shade {
    position: absolute;
    inset-inline: 0;
    background: repeating-linear-gradient(135deg, #f8fafc 0 6px, #f1f5f9 6px 12px);
    border-radius: .25rem;
    pointer-events: none;
}

.unscheduled { margin-top: 1.5rem; }
.unscheduled h2 { margin: 0 0 .5rem; font-size: .8125rem; text-transform: uppercase; letter-spacing: .04em; }
/* Always drawn, so it can always be dropped on: an empty list is zero pixels tall and a gesture
   that only has a destination on busy days is a gesture you cannot learn. */
.unscheduled ul { list-style: none; margin: 0; padding: .25rem 0; display: flex; flex-wrap: wrap; gap: .375rem; min-height: 2.5rem; }
.unscheduled-empty { margin: 0; font-size: .8125rem; color: var(--muted); }
.unscheduled .chip { background: #fff; border: 1px solid var(--line); color: var(--ink); }
.unscheduled .chip.colour-yellow { border-left: 3px solid #f59e0b; }
.unscheduled .chip.colour-orange { border-left: 3px solid #ea580c; }
.unscheduled .chip.colour-green { border-left: 3px solid #059669; }
.unscheduled .chip.colour-red { border-left: 3px solid #dc2626; }
.unscheduled .chip.colour-blue { border-left: 3px solid var(--indigo); }

.role { font-size: .75rem; padding: .125rem .5rem; border-radius: 999px; background: #eef2ff; color: var(--indigo-dark); }
.inline { display: inline; margin: 0; }
button.link { background: none; color: var(--muted); padding: 0; margin: 0; font-weight: 500; text-decoration: underline; }
button.link:hover { background: none; color: var(--ink); }

/* Tinted column shells with the cards stacked inside — the mock's shape. */
.board { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; align-items: start; margin-top: 1.25rem; }
@media (max-width: 48rem) { .board { grid-template-columns: 1fr; } }

/* Folded, the columns are their headers: the counts and the names, which is the context you need
   while working in the calendar, and about 370px of the page back. `.is-folded` is on `.board`,
   because they fold together — one board, not three independent lists.

   The motion is the kit's (`tailwind.css`: `.collapse` is a two-row grid, `max-content 0fr` closed
   and `max-content 1fr` open, with `transition: grid-template-rows`). `display: none` was the old
   way and cannot be animated, so the calendar under the board jumped in one frame. */
.board.is-folded .column { padding-bottom: .75rem; }
.board.is-folded .column-head { padding-bottom: 0; }

/* Clip in the collapsing row itself, not at the column's padding edge: the card list keeps its
   fixed height while the row goes to zero, and without this a sliver of it paints over the
   column's bottom padding on the way down. The kit insets `.kit-collapse-content` by 1rem because
   its own collapse is a boxed accordion; this one is a column whose `.cards` already sit inside the
   column's padding. */
.column > .kit-collapse-content { padding: 0; overflow: hidden; }

/* The kit hides its content the instant the row closes — its `transition: visibility` shorthand is
   replaced by the `transition: padding …` that follows it in the same rule, so the property never
   animates and a fold would collapse an already-empty box. Delayed instead: the content stays
   visible for the 200ms the row takes to close, then goes (which is also what keeps it out of the
   accessibility tree and off the hit-test once folded). */
.column:not(.kit-collapse-open) > .kit-collapse-content {
    visibility: hidden;
    transition: visibility 0s linear .2s;
}
.column.kit-collapse-open > .kit-collapse-content { visibility: visible; transition-delay: 0s; }

/* Motion on a press, never on load. The fold is remembered, and `board.js` applies the remembered
   state before the first paint — transitioning that would replay a 200ms collapse on every load. */
.board:not(.is-animating) .column { transition: none; }
@media (prefers-reduced-motion: reduce) {
    .board .column,
    .column > .kit-collapse-content { transition: none; }
}

/* Nothing to fold without the script, so the control does not exist until it can work. */
#board-fold { display: none; }
/* And a fixed width, because "Fold cards" and "Show cards" are not the same length: without it the
   whole toolbar shifts sideways every time it is pressed. */
.has-js #board-fold { display: inline-flex; min-width: 7rem; justify-content: center; }

.column {
    padding: .75rem;
    border: 1px solid #e2e8f0;
    border-radius: 1rem;
    background: rgb(241 245 249 / .75);
}

.column-head { display: flex; align-items: center; gap: .5rem; padding: 0 .25rem .75rem; }
.column-dot { width: .5rem; height: .5rem; border-radius: 999px; background: #94a3b8; flex: none; }
.column-done .column-dot { background: #10b981; }
.column h2 {
    margin: 0;
    font-size: .75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: #475569;
}
.column .count {
    padding: 0 .375rem;
    border-radius: 999px;
    background: #e2e8f0;
    color: #475569;
    font-size: .6875rem;
    font-weight: 700;
}
.column-add {
    margin: 0 0 0 auto;
    padding: .125rem .375rem;
    font-size: .75rem;
    font-weight: 600;
    color: #64748b;
    background: none;
    border: 0;
    border-radius: .25rem;
    cursor: pointer;
}
.column-add:hover { color: var(--indigo); background: #e2e8f0; }

.cards {
    list-style: none;
    margin: 0 0 .5rem;
    padding: 0;
    display: grid;
    gap: .5rem;
    align-content: start;
    min-height: 2rem;
    /* Five cards tall, then it scrolls. A fixed height, not a maximum: the board keeps its shape as
       cards come and go (and the calendar below it stays put), which is what makes it easy to aim at.
       `--card-h` is what a two-line card measures — padding, one line of title, one line of facts. */
    --card-h: 4.1875rem;
    --card-gap: .5rem;
    height: calc(5 * (var(--card-h) + var(--card-gap)) - var(--card-gap));
    overflow-y: auto;
    scrollbar-gutter: stable;
    padding-right: .125rem;
}
.cards::-webkit-scrollbar { width: .5rem; }
.cards::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 999px; }

.card {
    position: relative;
    padding: .625rem .75rem;
    background: #fff;
    border: 1px solid #e2e8f0;
    border-left: 4px solid var(--indigo);
    border-radius: .75rem;
    box-shadow: 0 1px 2px rgb(15 23 42 / .05);
}
.card:hover { border-color: #c7d2fe; box-shadow: 0 2px 8px rgb(15 23 42 / .08); }

/* A card is a handle: it can be dragged from its column into the calendar below, or into another
   column. The buttons and the little forms inside it keep their own behaviour, which the
   controller checks for. */
.card { cursor: grab; touch-action: none; }

/* One height for every card on a pointer device — the same `--card-h` the column is measured in, so
   five cards fill a column exactly. Without it a card is as tall as its contents (67px, 69px if a
   chip has an initials badge), and "five cards" is only true on average. A touch device keeps the
   action row in the flow, so there the card is taller and this does not apply. */
@media (hover: hover) and (pointer: fine) {
    .cards .card { height: var(--card-h); overflow: hidden; }
}

.card-top { display: flex; align-items: flex-start; gap: .5rem; }
.card-top h3 { flex: 1; margin: 0; font-size: .875rem; line-height: 1.3; min-width: 0; }
.card-minutes { flex: none; font-size: .75rem; font-weight: 600; color: #94a3b8; }
.card-tick { flex: none; color: #10b981; font-weight: 700; }

.card.colour-yellow { border-left-color: #f59e0b; }
.card.colour-orange { border-left-color: #ea580c; }
.card.colour-green { border-left-color: #059669; }
.card.colour-red { border-left-color: #dc2626; }
.card.colour-blue { border-left-color: #4f46e5; }

.card.is-done { background: #f8fafc; box-shadow: none; }
.card.is-done .card-open { text-decoration: line-through; color: #94a3b8; }
.card.is-done .chips { opacity: .75; }

/* Chips carry the card's facts, as in the mock: who, when, how soon. One line, always: the card's
   facts are unbounded and the column is not, so the server folds them into three (`CardFacts`) and
   the row refuses to wrap whatever it is given. */
.chips {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: .25rem;
    margin: .5rem 0 0;
    overflow: hidden;
    min-width: 0;
}
.chip {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: .0625rem .375rem;
    border-radius: .375rem;
    background: #f1f5f9;
    color: #475569;
    font-size: .6875rem;
    font-weight: 600;
    flex: none;
    white-space: nowrap;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}
.chip-quiet { color: #64748b; font-weight: 500; border: 1px dashed #cbd5e1; background: none; }
/* The mock's glyph beside a fact: a person, a clipboard, a car. Sized in `em` so the same rule
   works in a card's chip and in a block's, which is a size smaller. */
.chip-icon {
    width: .875rem;
    height: .875rem;
    flex: none;
    opacity: .75;
}
.chip-due { background: #fef3c7; color: #92400e; }
.chip-due.is-late { background: #fee2e2; color: #b91c1c; }


/* The card title is a button, so the whole card is reachable by keyboard — and it is where the two
   line limit lives, because the clamp has to be on the element holding the text. A title longer than
   that is not lost: the button carries it in its `title`, and the card's own panel shows all of it. */
.card-open {
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin: 0;
    padding: 0;
    background: none;
    color: inherit;
    font: inherit;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}
/* The global `button` rule gives every button an indigo background on hover. That is right for a
   primary action and wrong for a title: the ink stays inherited, so hovering a card's name put dark
   text on dark indigo. The title is a link, not a button. */
.card-open:hover { background: none; color: var(--indigo-dark); }

/* ---- card modal (matches mocks/board.html) ---------------------------------- */
body.is-modal { overflow: hidden; }

/* A card that does not exist yet: there is nothing to mark done, delete or tick off, and a
   checklist needs a card to belong to. Everything else in the panel is the same panel. */
.modal.is-new #modal-delete,
.modal.is-new #modal-done,
.modal.is-new .checklist { display: none; }

.modal { position: fixed; inset: 0; z-index: 40; display: grid; place-items: center; padding: 1rem; }
.modal[hidden] { display: none; }

.modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgb(15 23 42 / .6);
    backdrop-filter: blur(4px);
}

.modal-panel {
    position: relative;
    width: 100%;
    max-width: 48rem;
    max-height: calc(100vh - 2rem);
    display: flex;
    flex-direction: column;
    background: #fff;
    border-radius: 1rem;
    box-shadow: 0 25px 50px -12px rgb(15 23 42 / .35);
    outline: 1px solid rgb(15 23 42 / .1);
    overflow: hidden;
}

.modal-head { position: relative; padding: 1.25rem 1.5rem .75rem; border-bottom: 1px solid var(--line); }
.modal-close {
    position: absolute;
    top: .75rem;
    right: .75rem;
    width: 2rem;
    height: 2rem;
    padding: 0;
    margin: 0;
    background: none;
    color: var(--muted);
    font-size: 1rem;
    border-radius: .5rem;
}
.modal-close:hover { background: #f3f4f6; color: var(--ink); }

.dots { display: flex; gap: .375rem; margin-bottom: .5rem; }
.dot {
    width: 1rem;
    height: 1rem;
    padding: 0;
    margin: 0;
    border-radius: 999px;
    border: 2px solid transparent;
    box-shadow: 0 0 0 1px rgb(15 23 42 / .1);
    cursor: pointer;
}
.dot.is-on { border-color: #fff; box-shadow: 0 0 0 2px var(--ink); }
.dot-yellow { background: #f59e0b; }
.dot-orange { background: #ea580c; }
.dot-blue { background: var(--indigo); }
.dot-red { background: #dc2626; }
.dot-green { background: #059669; }

.modal-title {
    width: calc(100% - 2.5rem);
    padding: .25rem .375rem;
    margin-left: -.375rem;
    border: 1px solid transparent;
    border-radius: .5rem;
    background: none;
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: -.02em;
}
.modal-title:hover { border-color: var(--line); }
.modal-title:focus-visible { border-color: rgb(79 70 229 / .55); outline: none; background: #fff; box-shadow: 0 0 0 3px rgb(79 70 229 / .18); }

.modal-meta { margin: .25rem 0 0; font-size: .8125rem; color: var(--muted); }

.modal-body {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    gap: 1.5rem;
    padding: 1.25rem 1.5rem;
    overflow-y: auto;
    /* A flex item will not shrink below its content without this, so a tall panel — every field a
       new card has, on a laptop — pushed past the panel's max-height and was clipped instead of
       scrolling, with the footer drawn on top of it. */
    min-height: 0;
}
@media (max-width: 48rem) { .modal-body { grid-template-columns: 1fr; } }

.modal-left, .modal-right { display: grid; gap: .75rem; align-content: start; }
.modal-right { padding-left: 1.5rem; border-left: 1px solid var(--line); }
@media (max-width: 48rem) { .modal-right { padding-left: 0; border-left: 0; padding-top: .75rem; border-top: 1px solid var(--line); } }

.micro {
    display: block;
    font-size: .6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--muted);
    margin: 0;
}
.micro-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .25rem; }
.micro-row .pill { margin-left: .125rem; }

textarea {
    width: 100%;
    padding: .625rem .75rem;
    border: 1px solid var(--line);
    border-radius: .5rem;
    font: inherit;
    resize: vertical;
    background: #fff;
    color: inherit;
}
textarea:focus-visible { outline: none; border-color: var(--indigo); box-shadow: 0 0 0 3px rgb(79 70 229 / .22); }

.field { border: 0; padding: 0; margin: 0; display: grid; gap: .375rem; }
.field legend { padding: 0; margin-bottom: .375rem; }
.field.two-up { grid-template-columns: 1fr auto; gap: .5rem; align-items: end; }
.field-tail { display: flex; align-items: center; height: 2.5rem; }
.field input, .field select { font-size: .875rem; }
/* Direct children only: the assignee rows are labels too, and they are flex, not grid. */
.field > label { margin: 0; font-weight: 400; display: grid; gap: .25rem; }

/* `.field` rows are grids for inputs; an assignee row is a label too, so it needs its own rule
   with enough specificity to beat `.field > label`. */
.field label.who { display: flex; flex-direction: row; align-items: center; gap: .5rem; font-size: .875rem; padding: .125rem 0; }
.who input { width: auto; margin: 0; }
.who.is-inactive { opacity: .55; }

.avatar {
    display: grid;
    place-items: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 999px;
    font-size: .6875rem;
    font-weight: 700;
    color: #fff;
    background: var(--indigo);
}
.avatar.colour-yellow { background: #f59e0b; }
.avatar.colour-orange { background: #ea580c; }
.avatar.colour-green { background: #059669; }
.avatar.colour-red { background: #dc2626; }
.avatar.colour-blue { background: var(--indigo); }

.chip-btn {
    margin: 0;
    padding: .25rem .5rem;
    font-size: .75rem;
    font-weight: 500;
    background: #fff;
    color: var(--ink);
    border: 1px solid var(--line);
    border-radius: 999px;
    cursor: pointer;
}
.chip-btn:hover { background: #f9fafb; }
/* Selected, not shouted: a soft indigo edge on a tint. A full-strength indigo ring on a small pill
   was the loudest thing in the panel, and there is a row of them. */
.chip-btn.is-on { border-color: #a5b4fc; background: #eef2ff; color: var(--indigo-dark); }

.todo-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .125rem; }
.todo { display: flex; align-items: center; gap: .5rem; padding: .25rem .375rem; border-radius: .375rem; }
.todo:hover { background: #f9fafb; }
.todo input { width: auto; margin: 0; }
.todo-text { flex: 1; font-size: .875rem; }
.todo.is-done .todo-text { text-decoration: line-through; color: var(--muted); }
.todo-remove {
    margin: 0;
    padding: 0 .25rem;
    background: none;
    color: var(--muted);
    font-size: .75rem;
}
.todo-remove:hover { background: none; color: var(--danger); }

.todo-add { display: flex; gap: .375rem; margin-top: .375rem; }
.todo-add[hidden] { display: none; }
.todo-add input { flex: 1; padding: .375rem .5rem; font-size: .875rem; }
.todo-add button { margin: 0; padding: .375rem .75rem; font-size: .8125rem; }

.modal-foot {
    flex: none;
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .75rem 1.5rem;
    border-top: 1px solid var(--line);
    background: #f9fafb;
}
.modal-foot .danger { margin-right: auto; }
.modal-foot button { margin: 0; padding: .5rem .875rem; font-size: .875rem; font-weight: 500; }
/* Cancel is the quiet one: the global `button` rule made it indigo, so the footer offered two
   primaries and neither read as the way out. Every other footer button keeps its own colour. */
.modal-foot button { background: #fff; border: 1px solid var(--line); color: var(--ink); }
.modal-foot .danger { background: #fee2e2; color: var(--danger); border-color: transparent; }
.modal-foot .danger:hover { background: #fecaca; }
.modal-foot .emerald { background: #d1fae5; color: var(--ok); border-color: transparent; }
.modal-foot .emerald:hover { background: #a7f3d0; }
.modal-foot .primary { background: var(--indigo); color: #fff; border-color: transparent; }
.modal-foot .primary:hover { background: var(--indigo-dark); }
.day-link { font-size: .8125rem; color: var(--muted); text-decoration: none; }
.day-link:hover { color: var(--ink); text-decoration: underline; }

.blk.is-outside { outline: 2px dashed #d97706; outline-offset: -2px; }

/* The hours editor at the foot of the day page. Disabled fieldsets still render, because a form
   that vanishes when you pick "inherit" is more confusing than one that goes quiet. */
.hours {
    margin-top: 1.75rem;
    padding: 1rem 1.125rem 1.125rem;
    border: 1px solid var(--line);
    border-radius: .75rem;
    background: #fff;
}
.hours h2 { margin: 0 0 .375rem; font-size: .9375rem; }
.hours-what { margin: 0 0 .25rem; font-size: .875rem; }
.hours-hint { margin: 0 0 .875rem; font-size: .75rem; color: var(--muted); }
.hours-form fieldset { margin: 0 0 .75rem; padding: 0; border: 0; }
.hours-form legend { padding: 0; margin-bottom: .375rem; font-size: .6875rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.hours-form fieldset.is-muted { opacity: .45; }
.hours-choice { display: block; font-size: .875rem; }
.hours-choice input { margin-right: .375rem; }
.hours-days { display: flex; flex-wrap: wrap; gap: .5rem .875rem; margin-bottom: .625rem; }
.hours-day { display: inline-flex; align-items: center; gap: .25rem; font-size: .8125rem; }
.hours-times { display: flex; flex-wrap: wrap; gap: .75rem; font-size: .8125rem; }
.hours-times label { display: inline-flex; align-items: center; gap: .375rem; }
.hours-times input { padding: .25rem .375rem; font-size: .8125rem; }
.hours-note { margin: .625rem 0 0; font-size: .75rem; color: #b45309; }
.hours-actions { display: flex; align-items: center; gap: .625rem; margin: 0; }
.hours-actions button { margin: 0; padding: .4375rem .875rem; font-size: .8125rem; font-weight: 500; background: var(--indigo); color: #fff; border: 0; border-radius: .375rem; cursor: pointer; }
.hours-actions button:hover { background: var(--indigo-dark); }
.hours-actions button[disabled] { opacity: .6; cursor: default; }
.hours-status { font-size: .75rem; color: var(--muted); }
.hours-status.is-error { color: var(--danger); }

/* The calendar's live line, when `lanes.php` draws it.
   It used to be a paragraph under the grid, so every message the drag controller wrote grew the
   page and every silence shrank it again — the operator's "distracting and jumpy". Floated, it
   occupies no space at all: the grid below it cannot move, whatever it says. Empty, it is not
   drawn either (no background, no padding), so a quiet page looks like there is nothing there. */
.hours-status.is-toast {
    position: fixed;
    left: 50%;
    bottom: 1.25rem;
    z-index: 80;
    max-width: min(34rem, calc(100vw - 2rem));
    margin: 0;
    padding: .4375rem .875rem;
    border-radius: 999px;
    background: #1f2937;
    color: #fff;
    font-size: .8125rem;
    line-height: 1.35;
    text-align: center;
    box-shadow: 0 1.25rem 2.5rem -1.25rem rgb(15 23 42 / .6);
    transform: translateX(-50%);
}
.hours-status.is-toast.is-error { background: var(--danger); color: #fff; }
.hours-status.is-toast:empty { padding: 0; background: none; box-shadow: none; }

/* A block can be completed where it is drawn (M4b). Hidden until the script is running, because a
   button that cannot work is worse than no button — and the day view is server-rendered first. */
/* Two controls sit in the top-right corner of a card block. Hidden until the script is running,
   because neither can work without it. */
.blk-tools {
    display: none;
    position: absolute;
    top: .0625rem;
    right: .0625rem;
    gap: .0625rem;
}
.has-js .blk-tools { display: flex; }
.blk-tools button {
    width: 1.125rem;
    height: 1.125rem;
    padding: 0;
    border: 0;
    border-radius: .25rem;
    background: rgba(255, 255, 255, .8);
    color: var(--muted);
    font-size: .6875rem;
    line-height: 1;
    cursor: pointer;
}
.blk-tools button:hover { background: #fff; }
.blk-done:hover { color: var(--ok); }
.blk.is-done .blk-done:hover { color: var(--indigo); }
.blk-drop:hover { color: var(--danger); }
.blk-tools button[disabled] { opacity: .45; cursor: default; }
/* A short block cannot hold a title and two buttons side by side; the title yields. */
.blk { padding-right: 2.5rem; }

/* Blocks that share their time share the lane's width. Four is the most the layout will split into
   (see `DayLanes::MAX_COLUMNS`); a fifth collision shares the last column rather than shrinking
   below the width a title needs. Written out per combination because the CSP forbids style
   attributes — the same reason the slot grid is a table of classes. */
.blk.split-2, .blk.split-3, .blk.split-4 { inset-inline-end: auto; }
.blk.split-2 { width: calc(50% - 1px); }
.blk.split-2.col-0 { inset-inline-start: 0; }
.blk.split-2.col-1 { inset-inline-start: calc(50% + 1px); }
.blk.split-3 { width: calc(33.3333% - 1px); }
.blk.split-3.col-0 { inset-inline-start: 0; }
.blk.split-3.col-1 { inset-inline-start: calc(33.3333% + .5px); }
.blk.split-3.col-2 { inset-inline-start: calc(66.6666% + .5px); }
.blk.split-4 { width: calc(25% - 1px); }
.blk.split-4.col-0 { inset-inline-start: 0; }
.blk.split-4.col-1 { inset-inline-start: calc(25% + .25px); }
.blk.split-4.col-2 { inset-inline-start: calc(50% + .5px); }
.blk.split-4.col-3 { inset-inline-start: calc(75% + .75px); }

/* Screen-reader-only text: present for a label, invisible on screen. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* The unscheduled list. A card with no time can be given one, which is the other half of the ✕ on a
   block — without it, taking something off the day would strand it. Hidden until the script runs:
   the form talks to the JSON API, so it cannot work on its own. */
.unscheduled-hint { margin: -.25rem 0 .5rem; font-size: .75rem; color: var(--muted); }
.unscheduled li.chip { display: flex; align-items: center; gap: .5rem; width: fit-content; }
.chip-schedule { display: none; align-items: center; gap: .25rem; }
.has-js .chip-schedule { display: flex; }
/* A time picker inside a chip in the rail: short on purpose, so it does not make the chip — and the
   list — three times its own height. */
.chip-time { height: 1.5rem; padding: 0 .125rem; font-size: .6875rem; }
.chip-put {
    margin: 0;
    padding: .125rem .4375rem;
    font-size: .6875rem;
    font-weight: 600;
    border: 0;
    border-radius: .25rem;
    background: rgba(255, 255, 255, .85);
    color: var(--ink);
    cursor: pointer;
}
.chip-put:hover { background: #fff; }
.chip-put[disabled] { opacity: .5; cursor: default; }

/* R20's switch, in the space-hours form. */
.hours-strict { display: block; margin-top: .625rem; font-size: .8125rem; }
.hours-strict input { margin-right: .25rem; }
.hours-strict-note { display: block; margin-left: 1.25rem; font-size: .6875rem; color: var(--muted); }

/* The mock's "Today's plan" panel: a white card under the kanban holding the one grid. */
.panel {
    margin-top: 1.25rem;
    padding: 1rem 1.125rem 1.25rem;
    border: 1px solid #e2e8f0;
    border-radius: 1rem;
    background: #fff;
}
.panel-head { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .875rem; }
.panel-head h2 { margin: 0; font-size: 1rem; font-weight: 700; color: #0f172a; }
.panel-window { font-size: .75rem; color: #64748b; }
.panel-head .button { margin-left: auto; }
.panel .empty { margin: 0; }

/* Quiet chrome in the app bar: the mock's account area has no shouting. */
.appbar .link { text-decoration: none; font-size: .8125rem; color: #64748b; }
.appbar .link:hover { color: #0f172a; }

/* The day's date nav borrows the board's segmented control, but with links rather than radios. */
.segmented .seg-link {
    padding: .25rem .625rem;
    border-radius: .375rem;
    font-size: .8125rem;
    font-weight: 600;
    color: #475569;
    text-decoration: none;
}
.segmented .seg-link:hover { background: #fff; color: #0f172a; }

/* The day's grid inside its panel: the panel supplies the frame, so the grid needs no margin.
   No `overflow: hidden` here — it clipped the last hour and a bit off every lane. */
.panel-grid { margin-top: 1rem; padding: 0; }


/* ── Sign in: the mock's two panels ─────────────────────────────────────────────
   The brand panel is furniture, so it is built entirely in CSS — no inline style, and no
   screenshot to keep in sync with the product. */
body.has-split { display: block; place-items: initial; padding: 0; background: #fff; }
.shell-split { width: 100%; max-width: none; }

.auth { display: grid; grid-template-columns: 1fr; min-height: 100vh; min-height: 100dvh; }

.auth-brand {
    position: relative;
    display: none;
    overflow: hidden;
    padding: 3rem 2.5rem;
    background: #0b1120;
    color: #e2e8f0;
}
/* The 36px grid the mock draws with a data-URI background, as a mask so it fades out. */
.auth-brand::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(to right, rgba(255, 255, 255, .06) 1px, transparent 1px),
        linear-gradient(to bottom, rgba(255, 255, 255, .06) 1px, transparent 1px);
    background-size: 36px 36px;
    -webkit-mask-image: radial-gradient(ellipse at 25% 15%, #000 40%, transparent 85%);
    mask-image: radial-gradient(ellipse at 25% 15%, #000 40%, transparent 85%);
}
/* The blurred brand blob behind the logo. */
.auth-brand::after {
    content: "";
    position: absolute;
    top: -8rem;
    right: -8rem;
    width: 26rem;
    height: 26rem;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(79, 70, 229, .6), transparent 65%);
    filter: blur(30px);
}
.auth-brand-inner {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 2rem;
    height: 100%;
}

.auth-logo { display: inline-flex; align-items: center; gap: .6rem; }
.auth-logo svg { display: block; border-radius: .75rem; }
.auth-wordmark { font-size: 1.15rem; font-weight: 700; letter-spacing: -.02em; color: #fff; }

.auth-pill {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .3rem .7rem;
    border: 1px solid rgba(255, 255, 255, .18);
    border-radius: 999px;
    background: rgba(255, 255, 255, .06);
    font-size: .72rem;
    font-weight: 600;
    color: #c7d2fe;
}
.auth-dot { width: .4rem; height: .4rem; border-radius: 50%; background: #a5b4fc; }

.auth-pitch { margin: 1.75rem 0 0; }
.auth-pitch h2 {
    margin: 1rem 0 0;
    font-size: 2.1rem;
    line-height: 1.15;
    letter-spacing: -.03em;
    color: #fff;
}
.auth-pitch > p { max-width: 26rem; margin: .75rem 0 0; color: #94a3b8; font-size: .95rem; }

.auth-foot { margin: 0; font-size: .72rem; color: #64748b; }

/* A tiny board: three columns, tiny cards, a lane. Pure decoration. */
.auth-mini {
    margin-top: 2rem;
    border: 1px solid rgba(255, 255, 255, .1);
    border-radius: .9rem;
    background: rgba(255, 255, 255, .04);
    box-shadow: 0 20px 45px -25px rgba(0, 0, 0, .9);
    overflow: hidden;
}
.auth-mini-bar {
    display: flex;
    align-items: center;
    gap: .35rem;
    padding: .5rem .7rem;
    border-bottom: 1px solid rgba(255, 255, 255, .08);
    background: rgba(255, 255, 255, .03);
}
.auth-mini-bar span[class^="dot-"] { width: .45rem; height: .45rem; border-radius: 50%; }
.dot-red { background: #f87171; }
.dot-amber { background: #fbbf24; }
.dot-green { background: #34d399; }
.auth-mini-caption { margin-left: .4rem; font-size: .66rem; color: #64748b; }

.auth-mini-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; padding: .7rem; }
.auth-mini-col > p {
    margin: 0 0 .4rem;
    font-size: .6rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: #64748b;
}
.auth-mini-card {
    margin-bottom: .4rem;
    padding: .4rem .45rem;
    border-radius: .4rem;
    border-left: 3px solid #64748b;
    background: rgba(255, 255, 255, .07);
}
.auth-mini-card span { display: block; font-size: .64rem; font-weight: 600; color: #e2e8f0; }
.auth-mini-card em { display: block; margin-top: .1rem; font-size: .58rem; font-style: normal; color: #94a3b8; }
.auth-mini-card.is-blue { border-left-color: #6366f1; }
.auth-mini-card.is-yellow { border-left-color: #eab308; }
.auth-mini-card.is-orange { border-left-color: #f97316; }

.auth-mini-lane { display: flex; align-items: center; gap: .3rem; font-size: .6rem; color: #94a3b8; }
.auth-mini-avatar { width: .8rem; height: .8rem; border-radius: 50%; background: #475569; }
.auth-mini-block {
    height: 1.35rem;
    margin-top: .35rem;
    border-radius: .3rem;
    border-left: 3px solid #6366f1;
    background: rgba(99, 102, 241, .35);
}
.auth-mini-block.is-orange { border-left-color: #f97316; background: rgba(249, 115, 22, .3); }
.auth-mini-block.is-short { height: .9rem; }

/* The form panel. */
.auth-panel { display: flex; align-items: center; justify-content: center; padding: 2.5rem 1rem; }
.auth-card { width: 100%; max-width: 24rem; }
.auth-card .auth-logo { margin-bottom: 1.75rem; }
.auth-card .auth-wordmark { color: var(--ink); }
.auth-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.auth-label-row a { font-size: .75rem; font-weight: 600; color: var(--indigo); text-decoration: none; }
.auth-label-row a:hover { color: var(--indigo-dark); text-decoration: underline; }
.auth-card button[type="submit"] { width: 100%; margin-top: .5rem; }
.auth-note { margin: 1.5rem 0 0; font-size: .72rem; color: var(--muted); text-align: center; }

@media (min-width: 64rem) {
    .auth { grid-template-columns: 1fr 1fr; }
    .auth-brand { display: block; }
    .auth-card .auth-logo { display: none; }
    .auth-card { max-width: 26rem; }
}

/* ── The agenda: your own day as one list ───────────────────────────────────────
   The mock's phone list, at whatever width the phone actually is. Times on the left, one card per
   block, and the tick always visible because on this page ticking *is* the job. */
.agenda { list-style: none; margin: 0; padding: 0; max-width: 34rem; }
.agenda-row { display: flex; gap: .75rem; padding-bottom: .75rem; }

.agenda-when { width: 2.75rem; flex: none; padding-top: .45rem; text-align: right; }
.agenda-start { margin: 0; font-size: .72rem; font-weight: 700; color: var(--ink); }
.agenda-mins { margin: 0; font-size: .62rem; color: var(--muted); }

.agenda-card {
    flex: 1 1 auto;
    min-width: 0;
    padding: .6rem .7rem;
    border: 1px solid var(--line);
    border-left-width: 4px;
    border-radius: .7rem;
    background: #fff;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .05);
}
/* The same colour family the blocks use, so a card means the same thing on both screens. */
.agenda-card.colour-blue { border-left-color: #4f46e5; }
.agenda-card.colour-green { border-left-color: #059669; }
.agenda-card.colour-yellow { border-left-color: #f59e0b; }
.agenda-card.colour-orange { border-left-color: #ea580c; }
.agenda-card.colour-red { border-left-color: #dc2626; }

.agenda-top { display: flex; align-items: flex-start; justify-content: space-between; gap: .5rem; }
.agenda-title { margin: 0; font-size: .875rem; font-weight: 600; line-height: 1.3; }
.agenda-row.is-done .agenda-title { color: var(--muted); text-decoration: line-through; }
.agenda-row.is-travel .agenda-card { border-style: dashed; background: #f8fafc; }
.agenda-row.is-travel .agenda-title { color: var(--muted); font-style: italic; }

/* Hidden until the script that makes it work has run: a tick that cannot save is a lie. */
.agenda-tick { display: none; }
.has-js .agenda-tick {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    flex: none;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: .45rem;
    background: #fff;
    color: var(--muted);
    font-size: .8rem;
    line-height: 1;
    cursor: pointer;
}
.has-js .agenda-tick:hover { border-color: var(--indigo); color: var(--indigo); }
.has-js .agenda-tick:disabled { opacity: .5; cursor: progress; }

.agenda-now { display: flex; align-items: center; gap: .5rem; padding: 0 0 .75rem; }
.agenda-now-dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--danger); flex: none; }
.agenda-now-time { font-size: .7rem; font-weight: 700; color: var(--danger); }
.agenda-now-rule { flex: 1 1 auto; height: 1px; background: rgba(185, 28, 28, .35); }

.agenda-empty { margin: 0; padding: 1.5rem 1rem; color: var(--muted); font-size: .9rem; }

/* Ending the day: four numbers and one button. The counts are the day's, so there is nothing to
   edit here — the section exists to be read before the button is pressed. */
.eod { margin-top: 1rem; }
.eod-totals {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem 1.1rem;
    margin: 0;
    font-size: .8125rem;
    color: var(--muted);
}
.eod-totals strong { color: var(--ink); font-size: .9375rem; margin-right: .15rem; }
.eod-form { margin: .9rem 0 0; }
.eod-form button[type="submit"] { width: 100%; }
@media (min-width: 40rem) { .eod-form button[type="submit"] { width: auto; } }
.eod-form button[disabled] { opacity: .55; cursor: not-allowed; }

/* The opt-out sits under the button, quieter than it: sending is the action, this is the setting. */
.eod-opt {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    margin-top: .9rem;
    font-size: .8125rem;
    color: var(--muted);
    cursor: pointer;
}
.eod-opt input { width: auto; margin: 0; }

.eyebrow {
    margin: 0 0 .1rem;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--indigo);
}
.pill.is-done { background: #d1fae5; color: #065f46; }

/* The R20 warning on an agenda card: the same claim the grid's red block makes, worded out. */
.chip-warn { background: #fef2f2; color: #b91c1c; }

/* ── The team page ─────────────────────────────────────────────────────────────
   A table, because a roster is one: the mock draws the same five columns. Named `people-table` so
   it does not collide with the `.people` list the auth pages still use. */
.people-table { width: 100%; border-collapse: collapse; font-size: .875rem; }
.people-table th {
    padding: .5rem .75rem;
    border-bottom: 1px solid var(--line);
    color: var(--muted);
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .07em;
    text-align: left;
    text-transform: uppercase;
    white-space: nowrap;
}
.people-table td { padding: .7rem .75rem; border-bottom: 1px solid var(--line); vertical-align: middle; }
.people-table tr:last-child td { border-bottom: 0; }
.people-table tr.is-inactive td { color: var(--muted); }
.people-table tr.is-inactive .who-name { color: var(--muted); text-decoration: line-through; }
.people-table .who { display: inline-flex; align-items: center; gap: .5rem; }
.people-table .who-name { font-weight: 600; }
.people-hours { font-size: .8125rem; color: #475569; }
.people-hours .tag { margin-right: .35rem; }
.people-actions { text-align: right; white-space: nowrap; }
.people-actions .link { font-weight: 600; text-decoration: none; }
.people-actions .link:hover { color: var(--indigo); }

.tag.is-admin { background: #dcfce7; color: #166534; }
.tag.is-custom { background: #eef2ff; color: var(--indigo-dark); }

/* Active / deactivated: a dot and a word, as the mock has it. */
.state { display: inline-flex; align-items: center; gap: .35rem; font-size: .75rem; font-weight: 600; color: var(--muted); }
.state-dot { width: .35rem; height: .35rem; border-radius: 50%; background: #9ca3af; }
.state.is-on { color: #047857; }
.state.is-on .state-dot { background: #10b981; }

/* Two forms side by side on a desktop, stacked on a phone. */
.team-forms { display: grid; gap: 1rem; margin-top: 1rem; }
.team-forms .panel { margin: 0; }
@media (min-width: 56rem) { .team-forms { grid-template-columns: 1fr 1fr; } }

/* ── Settings ──────────────────────────────────────────────────────────────────
   The mock's two columns: a short list of sections on the left, the sections themselves on the
   right. The nav is in-page anchors rather than routes — one page, one read, and it works with no
   script, which a tab strip would not. Below 64rem it becomes a row of chips above the sections,
   because a 12rem sidebar on a phone is 12rem of nothing. */
.settings { display: grid; gap: 1rem; }
.settings-nav { display: flex; flex-wrap: wrap; gap: .25rem; font-size: .8125rem; }
.settings-nav a {
    padding: .35rem .6rem;
    border-radius: .5rem;
    color: #475569;
    font-weight: 600;
    text-decoration: none;
}
.settings-nav a:hover { background: #fff; color: var(--ink); box-shadow: 0 1px 2px rgb(15 23 42 / .06); }
.settings-body { display: grid; gap: 1rem; min-width: 0; }

@media (min-width: 64rem) {
    .settings { grid-template-columns: 11rem minmax(0, 1fr); align-items: start; }
    .settings-nav { position: sticky; top: 4.5rem; display: grid; gap: .125rem; }
    .settings-nav a { padding: .45rem .6rem; }
    /* The first link is where the page starts, so it reads as the current section. */
    .settings-nav a:first-child {
        background: #fff;
        color: var(--ink);
        box-shadow: 0 1px 2px rgb(15 23 42 / .06);
    }
}

.settings-panel { margin: 0; }

/* A form that is mostly two columns of label + field on a desktop, one below 40rem. */
.settings-form { display: grid; gap: .3rem; margin-top: .25rem; }
.settings-form label { margin-top: .6rem; font-size: .8125rem; font-weight: 600; color: #334155; }
.settings-form input, .settings-form select { max-width: 24rem; }
.settings-form .settings-hint, .settings-form .settings-row { max-width: 40rem; }
.settings-actions { display: flex; margin-top: 1.1rem; }

/* A value the page states rather than edits: the address, the hours, the last reset. */
.settings-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .4rem;
    margin: .35rem 0 0;
    font-size: .8125rem;
    color: #334155;
}
.settings-label {
    min-width: 8.5rem;
    font-size: .75rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
}
.settings-value code { font-size: .8125rem; }
.settings-quiet { color: var(--muted); }
.settings-hint { margin: .3rem 0 0; font-size: .75rem; line-height: 1.45; color: var(--muted); }
.settings-hint a { color: var(--indigo); }

/* A switch row: what it does on the left, the control on the right, as the mock draws it. */
.settings-switches { display: grid; gap: .25rem; margin-top: .25rem; }
.switch {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    padding: .7rem 0;
    border-bottom: 1px solid var(--line);
    cursor: pointer;
}
.switch:last-child { border-bottom: 0; }
.switch-what { display: grid; gap: .1rem; min-width: 0; }
.switch-title { font-size: .875rem; font-weight: 600; color: #0f172a; }
.switch-note { font-size: .75rem; color: var(--muted); }
.switch input { width: auto; margin: .15rem 0 0; flex: none; }

/* A vertical form: label above field, full width, a gap that does not need a wrapper div. */
.stack { display: grid; gap: .35rem; margin-top: 1rem; }
.stack label { font-size: .8125rem; font-weight: 600; color: #334155; margin-top: .5rem; }
.stack label:first-child { margin-top: 0; }
.stack input, .stack select { width: 100%; }
.stack button[type="submit"] { margin-top: .85rem; }

/* ── A split shift: more than one set of times in a day ─────────────────────────
   One row per window, in the order they were saved. The rows share `name="start"`/`name="end"`
   because they are one fieldset, not one field: the script reads the rows, never the names. */
.hours-windows { display: grid; gap: .375rem; }
.hours-window { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; font-size: .8125rem; }
.hours-window label { display: inline-flex; align-items: center; gap: .375rem; }
.hours-window input { padding: .25rem .375rem; font-size: .8125rem; }
.hours-window-what { color: var(--muted); min-width: 2.25rem; }
.hours-window-remove {
    border: 1px solid var(--line);
    border-radius: .375rem;
    background: #fff;
    color: var(--muted);
    font-size: .6875rem;
    line-height: 1;
    padding: .3rem .4rem;
    cursor: pointer;
}
.hours-window-remove:hover { border-color: var(--danger); color: var(--danger); }
.hours-window-remove[hidden] { display: none; }
.hours-add { margin: .5rem 0 0; font-size: .8125rem; }
.hours-add button[hidden] { display: none; }

/* A week of hours: one row per day, each with its own sets of times. */
.hours-week { display: grid; gap: .5rem; margin-top: .5rem; }
.hours-day-row { display: flex; align-items: flex-start; gap: .75rem; }
.hours-day-row > .hours-day { flex: none; width: 3.25rem; padding-top: .3rem; }
.hours-day-row.is-off { opacity: .5; }
.hours-day-windows { display: grid; gap: .3rem; }
.hours-add-day {
    justify-self: start;
    border: 1px dashed #cbd5e1;
    border-radius: .375rem;
    background: none;
    color: var(--muted);
    font-size: .75rem;
    line-height: 1;
    padding: .25rem .45rem;
    cursor: pointer;
}
.hours-add-day:hover { border-color: var(--indigo); color: var(--indigo); }
.hours-day-row:not(.is-on) .hours-day-windows,
.hours-day-row:not(.is-on) .hours-add-day { display: none; }

/* ── Offline ───────────────────────────────────────────────────────────────────
   The banner the service worker injects when it serves a cached page, and the offline page's
   furniture. Classes only: `style-src 'self'` blocks an inline style, so an injected attribute
   would arrive invisible — which is the one thing an offline banner must not do. */
.offline-banner {
    position: sticky;
    top: 0;
    z-index: 50;
    padding: .5rem 1rem;
    background: #fef3c7;
    color: #92400e;
    font-size: .8125rem;
    font-weight: 600;
    text-align: center;
    border-bottom: 1px solid #fcd34d;
}
