/* ==========================================================================
   Project Manager — application stylesheet

   Plain CSS3, no framework. Organised as:
     0. Typefaces
     1. Design tokens
     2. Reset and base elements
     3. Layout primitives
     4. Application shell (header, navigation)
     5. Components (card, button, field, alert, badge, avatar)
     6. Authentication screens
     7. Utilities
     8. Accessibility and polish
   ========================================================================== */

/* 0. Typefaces =============================================================

   Two self-hosted monospaced families: the interface stays terminal-flavoured,
   with one face to read in and one to look at. Nothing is requested from a
   third party.

     Google Sans Code  text and UI     open, rounded letters that stay readable
                                       in long task titles and comments; the
                                       four weights the interface uses
     Geist Mono        display & data  crisp and geometric: headings get their
                                       voice from a different shape rather than
                                       just more weight, and the running timer
                                       and the code input read as instruments

   Built from the Nerd Font downloads (both SIL OFL 1.1, licences beside the
   files) by subsetting to Latin, punctuation and the arrows and shapes the app
   draws, which drops the thousands of icon glyphs: about 15 MB of .ttf/.otf
   becomes 84 KB of .woff2. The recipe, per file:

     python -m fontTools.subset <font> --flavor=woff2 --drop-tables+=BASE
       --unicodes=U+0020-007E,U+00A0-017F,U+0131,U+0152-0153,U+02C6,U+02DA,
         U+02DC,U+2010-2027,U+2030-203A,U+2044,U+20AC,U+20B9,U+2122,
         U+2190-2193,U+2212,U+2215,U+2260,U+2264-2265,U+25A0-25AA,U+25B2-25BC
       --layout-features=kern,liga,calt,ccmp,locl,mark,mkmk,tnum,case,zero

   (BASE is dropped because the Nerd Font patch leaves Google Sans Code's
   malformed; browsers don't use it.) Symbols neither face has (the ticks, the
   theme switch's moon and sun, the warning sign) come from the system's
   symbol font, as they always have.

   font-display: swap means text paints immediately in the fallback and
   re-renders when the font arrives, rather than being invisible.
   ========================================================================== */

/* Google Sans Code: text and UI */
@font-face {
    font-family: "Google Sans Code Web";
    src: url("../fonts/google-sans-code-400.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Google Sans Code Web";
    src: url("../fonts/google-sans-code-500.woff2") format("woff2");
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Google Sans Code Web";
    src: url("../fonts/google-sans-code-600.woff2") format("woff2");
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Google Sans Code Web";
    src: url("../fonts/google-sans-code-700.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* Geist Mono: headings, the running timer and the verification code input */
@font-face {
    font-family: "Geist Mono Web";
    src: url("../fonts/geist-mono-400.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Geist Mono Web";
    src: url("../fonts/geist-mono-700.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* 1. Design tokens ========================================================= */

:root {
    /* Achromatic base. Colour is reserved for meaning — a status, a warning, a
       destructive action — so anything coloured on screen is telling you
       something rather than decorating. */
    --bg: #fafafa;
    --surface: #ffffff;
    --surface-2: #f2f2f2;
    --border: #eaeaea;
    --border-strong: #d4d4d4;

    --text: #000000;
    --text-muted: #666666;
    --text-subtle: #8f8f8f;

    /* The accent is black, not a brand hue: primary buttons, the brand mark and
       progress fills read as structure rather than decoration. */
    --primary: #000000;
    --primary-hover: #383838;
    --primary-soft: #f2f2f2;
    --on-primary: #ffffff;

    /* Links keep a blue, because an achromatic link inside body text is
       indistinguishable from the text around it. */
    --link: #0070f3;
    --focus: #0070f3;

    --danger: #e5484d;
    --danger-soft: #fff0f0;
    --success: #45a557;
    --success-soft: #f1fdf4;
    --warning: #f5a623;
    --warning-soft: #fff9ed;
    --info: #0070f3;
    --info-soft: #f0f7ff;

    --radius-sm: 6px;
    --radius: 10px;
    --radius-lg: 14px;

    --shadow-sm: 0 1px 2px rgb(16 24 40 / 6%);
    --shadow: 0 4px 16px rgb(16 24 40 / 8%);
    --shadow-lg: 0 12px 40px rgb(16 24 40 / 12%);

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;
    --space-7: 48px;

    /* Every role is monospaced, so each stack falls back to the generic
       monospace family rather than a proportional one — a fallback should
       change the typeface, not the whole layout. */
    --font: "Google Sans Code Web", ui-monospace, SFMono-Regular, Consolas, monospace;
    --font-display: "Geist Mono Web", "Google Sans Code Web", ui-monospace, Consolas, monospace;
    --font-mono: "Geist Mono Web", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    --header-height: 60px;
}

/* Dark theme. The <head> of every page sets data-theme before first paint
   (saved choice, else the system preference); the top-bar switch flips it. */
:root[data-theme="dark"] {
        color-scheme: dark;

        /* True black ground, with surfaces stepping up in near-neutral greys.
           The inversion is deliberate: in dark mode the accent becomes white,
           so a primary button stays the highest-contrast thing on screen. */
        --bg: #000000;
        --surface: #0a0a0a;
        --surface-2: #171717;
        --border: #262626;
        --border-strong: #404040;

        --text: #ededed;
        --text-muted: #a1a1a1;
        --text-subtle: #737373;

        --primary: #ffffff;
        --primary-hover: #cccccc;
        --primary-soft: #1a1a1a;
        --on-primary: #000000;

        --link: #3291ff;
        --focus: #3291ff;

        --danger: #ff6369;
        --danger-soft: #2a1215;
        --success: #62c073;
        --success-soft: #0f2417;
        --warning: #f7b955;
        --warning-soft: #2a1f0d;
        --info: #3291ff;
        --info-soft: #0d1f33;

        --shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
        --shadow: 0 4px 16px rgb(0 0 0 / 45%);
        --shadow-lg: 0 12px 40px rgb(0 0 0 / 55%);
}

/* 2. Reset and base ======================================================== */

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

* {
    margin: 0;
}

html {
    -webkit-text-size-adjust: 100%;
}

/* The hidden attribute must win over a component's own display (an .inline-form
   is inline-flex, so without this a hidden one stayed on screen). */
[hidden] {
    display: none !important;
}

body {
    min-height: 100vh;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    /* 14px rather than 15: a monospaced face sets wider than a proportional
       one at the same size, and 15px cost too much density in the task lists
       and Kanban cards. */
    font-size: 14px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
    /* Coding faces can join -> and != into ligatures. Welcome in a code block,
       wrong in a task title like "Fix != comparison", so they are switched off
       everywhere except the .font-mono slots below. */
    font-variant-ligatures: none;
    font-feature-settings: "liga" 0, "calt" 0;
}

h1, h2, h3, h4 {
    font-family: var(--font-display);
    line-height: 1.25;
    /* 700 rather than 650: the supplied faces are static, so a non-standard
       weight would be snapped by the browser anyway. */
    font-weight: 700;
    /* Negative tracking suited the previous proportional face. Monospace is
       already evenly spaced and reads cramped when pulled tighter. */
    letter-spacing: 0;
}

h1 { font-size: 1.6rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.05rem; }

a {
    color: var(--link);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

img, svg {
    display: block;
    max-width: 100%;
}

button, input, select, textarea {
    font: inherit;
    color: inherit;
}

:focus-visible {
    /* Two rings: an inner one in the surface colour holding the outer ring
       clear of the control, so focus is legible against a card, a coloured
       button or the page background alike. */
    outline: none;
    box-shadow:
        0 0 0 2px var(--surface),
        0 0 0 4px var(--focus);
    border-radius: var(--radius-sm);
}

/* Windows high-contrast mode ignores box-shadow, so keep a real outline there. */
@media (forced-colors: active) {
    :focus-visible {
        outline: 2px solid Highlight;
        outline-offset: 2px;
    }
}

/* 3. Layout primitives ===================================================== */

.container {
    width: 100%;
    max-width: 1140px;
    margin-inline: auto;
    padding-inline: var(--space-5);
}

.stack > * + * {
    margin-top: var(--space-4);
}

.row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.row-between {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

/* 4. Application shell ===================================================== */

.app-header {
    position: sticky;
    top: 0;
    z-index: 20;
    height: var(--header-height);
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.app-header > .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    height: 100%;
}

.brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text);
    font-weight: 700;
    letter-spacing: 0;
}

.brand:hover {
    text-decoration: none;
}

.brand-mark {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    border-radius: 8px;
    background: var(--primary);
    color: var(--on-primary);
    font-size: 0.8rem;
    font-weight: 700;
}

.app-nav {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    margin-left: var(--space-5);
}

.app-nav a {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-weight: 500;
}

.app-nav a:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}

.app-nav a[aria-current="page"] {
    background: var(--primary-soft);
    color: var(--primary);
}

.app-header-actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-left: auto;
}

.app-main {
    padding-block: var(--space-6) var(--space-7);
}

.app-footer {
    padding-block: var(--space-5);
    border-top: 1px solid var(--border);
    color: var(--text-subtle);
    font-size: 0.85rem;
}

/* 5. Components ============================================================ */

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}

.card-body {
    padding: var(--space-5);
}

.card-title {
    margin-bottom: var(--space-1);
    font-size: 1.3rem;
}

.card-subtitle {
    color: var(--text-muted);
    font-size: 0.925rem;
}

/* Buttons ------------------------------------------------------------------ */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: 0 var(--space-4);
    height: 40px;
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--text);
    font-size: 0.925rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 120ms ease, border-color 120ms ease, opacity 120ms ease;
}

.btn:hover {
    text-decoration: none;
}

.btn-primary {
    background: var(--primary);
    color: var(--on-primary);
}

.btn-primary:hover {
    background: var(--primary-hover);
}

.btn-secondary {
    background: var(--surface);
    border-color: var(--border-strong);
    color: var(--text);
}

.btn-secondary:hover {
    background: var(--surface-2);
}

.btn-ghost {
    background: transparent;
    color: var(--text-muted);
}

.btn-ghost:hover {
    background: var(--surface-2);
    color: var(--text);
}

.btn-block {
    width: 100%;
}

.btn:disabled,
.btn[aria-busy="true"] {
    opacity: 0.65;
    cursor: not-allowed;
}

/* Form fields -------------------------------------------------------------- */

.field + .field {
    margin-top: var(--space-4);
}

.field label {
    display: block;
    margin-bottom: var(--space-2);
    font-size: 0.875rem;
    font-weight: 600;
}

.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="date"],
.field input[type="time"],
.field input[type="number"],
.field select,
.field textarea {
    width: 100%;
    padding: 0 var(--space-3);
    height: 42px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    transition: border-color 120ms ease, box-shadow 120ms ease;
}

.field textarea {
    height: auto;
    min-height: 110px;
    padding-block: var(--space-3);
    resize: vertical;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-soft);
}

.field input::placeholder,
.field textarea::placeholder {
    color: var(--text-subtle);
}

.field .hint {
    margin-top: var(--space-2);
    color: var(--text-subtle);
    font-size: 0.8rem;
}

/* Invalid state is applied by JS on blur and by PHP on redisplay. */
.field.is-invalid input,
.field.is-invalid select,
.field.is-invalid textarea {
    border-color: var(--danger);
}

.field.is-invalid input:focus {
    box-shadow: 0 0 0 3px var(--danger-soft);
}

.field-error {
    display: none;
    margin-top: var(--space-2);
    color: var(--danger);
    font-size: 0.8rem;
}

.field.is-invalid .field-error {
    display: block;
}

.field-with-toggle {
    position: relative;
}

.field-with-toggle input {
    padding-right: 68px;
}

.password-toggle {
    position: absolute;
    top: 50%;
    right: var(--space-2);
    transform: translateY(-50%);
    padding: var(--space-1) var(--space-2);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
}

.password-toggle:hover {
    background: var(--surface-2);
    color: var(--text);
}

.checkbox {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: 0.9rem;
    cursor: pointer;
}

/* The real checkbox stays (keyboard, screen readers, clicks anywhere on the
   label) but is invisible and stretched over the label; the .checkmark next
   to it is what you see. */
.checkbox {
    position: relative;
}

/* Inside forms, .field label would make this a block; keep it a row. */
.field .checkbox {
    display: inline-flex;
    margin-bottom: 0;
}

.checkbox input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

/* Alerts -------------------------------------------------------------------- */

.alert {
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-left: 2px solid transparent;
    border-radius: 0 var(--radius) var(--radius) 0;
    font-size: 0.82rem;
}

/* Page messages sit above the heading; keep them clear of it. */
#alerts:not(:empty) {
    margin-bottom: var(--space-4);
}

.alert + .alert {
    margin-top: var(--space-3);
}

.alert ul {
    margin: 0;
    padding-left: var(--space-5);
}

.alert-error {
    background: var(--danger-soft);
    border-color: var(--danger);
    color: var(--danger);
}

.alert-success {
    background: var(--success-soft);
    border-color: var(--success);
    color: var(--success);
}

.alert-info {
    background: var(--info-soft);
    border-color: var(--info);
    color: var(--info);
}

.alert-warning {
    background: var(--warning-soft);
    border-color: var(--warning);
    color: var(--warning);
}

/* Badges and avatars --------------------------------------------------------- */

.badge {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--space-2);
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: 0.75rem;
    font-weight: 600;
    white-space: nowrap;
}

.avatar {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--primary-soft);
    color: var(--primary);
    font-size: 0.8rem;
    font-weight: 700;
    text-transform: uppercase;
}

.user-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: 0.9rem;
    font-weight: 600;
}

/* Empty state ---------------------------------------------------------------- */

.empty-state {
    padding: var(--space-7) var(--space-5);
    text-align: center;
    color: var(--text-muted);
}

.empty-state h2 {
    margin-bottom: var(--space-2);
    color: var(--text);
}

/* Page header ---------------------------------------------------------------- */

.page-header {
    margin-bottom: var(--space-5);
}

.page-header p {
    margin-top: var(--space-1);
    color: var(--text-muted);
}

/* Statistic tiles ------------------------------------------------------------ */

.stat-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-4);
    margin-bottom: var(--space-6);
}

.stat {
    padding: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

.stat-value {
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: 0;
}

/* Numbers are data, so every standalone number takes the data face: counts,
   percentages, dates, times and durations. Containers that only ever hold a
   number are listed here; a number sitting among words is marked .num where
   it is rendered. A number inside a sentence or a title stays in the text face.
   Geist Mono ships 400 and 700, so a 600 here renders at 700. */
.num,
.stat-value,
.panel-head > .badge,
.board-column-head > .badge,
.activity-time,
.meta-list dd,
.member-bars__value {
    font-family: var(--font-mono);
}

.stat-label {
    margin-top: var(--space-1);
    color: var(--text-muted);
    font-size: 0.825rem;
}

/* Draws the eye to figures that need action rather than to every tile. */
.stat.is-alert .stat-value { color: var(--danger); }
.stat.is-warn .stat-value { color: var(--warning); }

/* Two-column panel layout ----------------------------------------------------- */

.panels {
    display: grid;
    grid-template-columns: 3fr 2fr;
    gap: var(--space-5);
    align-items: start;
}

.panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--border);
}

.panel-head h2 {
    font-size: 1rem;
}

/* Lists ----------------------------------------------------------------------- */

.list {
    list-style: none;
    padding: 0;
    margin: 0;
}

.list-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-5);
}

.list-item + .list-item {
    border-top: 1px solid var(--border);
}

.list-item-title {
    font-weight: 600;
}

.list-item-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-1);
    color: var(--text-muted);
    font-size: 0.825rem;
}

.list-item-aside {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
}

.is-overdue {
    color: var(--danger);
    font-weight: 600;
}

.is-due-today {
    color: var(--warning);
    font-weight: 600;
}

/* Progress -------------------------------------------------------------------- */

.progress {
    display: block; /* a <span>: without this its width/height (and the fill's) are ignored */
    width: 120px;
    height: 6px;
    background: var(--surface-2);
    border-radius: 999px;
    overflow: hidden;
}

.progress-bar {
    display: block;
    height: 100%;
    background: var(--primary);
    border-radius: 999px;
}

/* Status and priority badges --------------------------------------------------- */

.badge-status-todo,
.badge-priority-low {
    background: var(--surface-2);
    color: var(--text-muted);
}

.badge-status-in_progress,
.badge-priority-medium,
.badge-status-planning {
    background: var(--info-soft);
    color: var(--info);
}

.badge-status-review,
.badge-priority-high,
.badge-status-on_hold {
    background: var(--warning-soft);
    color: var(--warning);
}

.badge-status-completed,
.badge-status-active {
    background: var(--success-soft);
    color: var(--success);
}

.badge-priority-critical {
    background: var(--danger-soft);
    color: var(--danger);
}

.badge-status-archived {
    background: var(--surface-2);
    color: var(--text-subtle);
}

/* Toolbar and page actions ------------------------------------------------------ */

.page-header-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-bottom: var(--space-5);
}

.toolbar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

/* Forms laid out in two columns ---------------------------------------------------- */

.form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-4);
}

.form-grid .field {
    margin-top: 0;
}

.form-grid .field + .field {
    margin-top: 0;
}

.form-full {
    grid-column: 1 / -1;
}

.form-actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-5);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

/* Definition-style metadata --------------------------------------------------------- */

.meta-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--space-4);
}

.meta-list dt {
    color: var(--text-subtle);
    font-size: 0.775rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.meta-list dd {
    margin: var(--space-1) 0 0;
    font-weight: 600;
}

/* Activity feed ---------------------------------------------------------------------- */

.activity-item {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
}

.activity-item + .activity-item {
    border-top: 1px solid var(--border);
}

.activity-body {
    font-size: 0.9rem;
}

.activity-time {
    color: var(--text-subtle);
    font-size: 0.775rem;
}

.activity-diff {
    margin-top: var(--space-1);
    /* Old and new values are data, so they take the data face: Geist Mono stays
       crisp at this small size, where the earlier bitmap-derived face blurred. */
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    color: var(--text-muted);
}

/* Inline forms inside list rows -------------------------------------------------------- */

.inline-form {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

.inline-form select {
    height: 32px;
    padding: 0 var(--space-2);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    font-size: 0.825rem;
}

.btn-sm {
    height: 32px;
    padding: 0 var(--space-3);
    font-size: 0.825rem;
}

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

.btn-danger:hover {
    background: var(--danger-soft);
}

/* Tag chips ------------------------------------------------------------------------------ */

.tag-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-4) var(--space-5);
}

.tag {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 2px var(--space-3);
    border-radius: 999px;
    background: var(--surface-2);
    font-size: 0.8rem;
    font-weight: 600;
}

.tag-swatch {
    width: 8px;
    height: 8px;
    border-radius: 50%;
}

/* Checkbox grids in forms ------------------------------------------------------------ */

.check-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: var(--space-2) var(--space-4);
}

/* Checklists --------------------------------------------------------------------------- */

.checklist {
    padding: var(--space-4) var(--space-5);
}

.checklist + .checklist {
    border-top: 1px solid var(--border);
}

.checklist-title {
    margin-bottom: var(--space-3);
    font-size: 0.95rem;
}

.checklist-items {
    list-style: none;
    margin: 0;
    padding: 0;
}

.checklist-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-1) 0;
}

.checklist-item.is-done .checklist-text {
    color: var(--text-subtle);
    text-decoration: line-through;
}

.checklist-box {
    display: grid;
    place-items: center;
    flex-shrink: 0;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
}

.checklist-add {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

/* Checklist rename / edit / delete: quiet until the row is hovered or focused. */
.checklist-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.checklist-head .checklist-title {
    flex: 1;
    margin-bottom: 0;
}

.checklist-item .checklist-text {
    flex: 1;
}

.checklist-tools {
    display: inline-flex;
    gap: var(--space-1);
    opacity: 0.45;
    transition: opacity 120ms ease;
}

.checklist-head:hover .checklist-tools,
.checklist-item:hover .checklist-tools,
.checklist-tools:focus-within {
    opacity: 1;
}

.checklist-tools button {
    padding: 0 var(--space-1);
    background: none;
    border: 0;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font: inherit;
    font-size: 0.78rem;
    cursor: pointer;
}

.checklist-tools button:hover {
    color: var(--text);
}

.checklist-tools [data-delete]:hover {
    color: var(--danger);
}

.checklist-edit {
    display: flex;
    flex: 1;
    gap: var(--space-2);
}

.checklist-add input,
.checklist-edit input {
    flex: 1;
    height: 32px;
    padding: 0 var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    font-size: 0.875rem;
}

/* Kanban board -------------------------------------------------------------------------- */

.board {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-4);
    align-items: start;
}

.board-column {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

.board-column-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border);
}

.board-column-head h2 {
    font-size: 0.9rem;
}

.board-dropzone {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    /* Keeps an empty column a valid drop target. */
    min-height: 90px;
    padding: var(--space-3);
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    transition: background-color 120ms ease;
}

.board-dropzone.is-over {
    background: var(--primary-soft);
}

.board-card {
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    cursor: grab;
}

.board-card.is-dragging {
    opacity: 0.45;
    cursor: grabbing;
}

.board-card-title {
    display: block;
    color: var(--text);
    font-size: 0.9rem;
    font-weight: 600;
}

.board-card-title:hover {
    color: var(--primary);
    text-decoration: none;
}

.board-card-tags,
.board-card-foot,
.board-card-people {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-2);
    font-size: 0.775rem;
}

.avatar-sm {
    width: 22px;
    height: 22px;
    font-size: 0.65rem;
}

.board-status {
    margin-top: var(--space-4);
    color: var(--text-subtle);
    font-size: 0.825rem;
    min-height: 1.2em;
}

.board-status.is-error {
    color: var(--danger);
}

/* Notifications ------------------------------------------------------------------------- */

.notification-link {
    position: relative;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 0.9rem;
    font-weight: 500;
}

.notification-link:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}

.notification-count {
    display: inline-grid;
    place-items: center;
    min-width: 18px;
    height: 18px;
    margin-left: var(--space-1);
    padding: 0 5px;
    border-radius: 999px;
    background: var(--danger);
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
}

.list-item.is-unread {
    background: var(--primary-soft);
}

/* A highlighted row at the top or bottom of a card follows the card's rounded
   corners instead of poking out square. */
.card > .list:first-child > .list-item:first-child {
    border-top-left-radius: var(--radius-lg);
    border-top-right-radius: var(--radius-lg);
}

.card > .list:last-child > .list-item:last-child {
    border-bottom-left-radius: var(--radius-lg);
    border-bottom-right-radius: var(--radius-lg);
}

.notification-dot.is-read {
    background: var(--border-strong);
}

/* Unread notification marker ---------------------------------------------------- */

.notification-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--primary);
    flex-shrink: 0;
}

/* 6. Authentication screens ================================================= */

.auth-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    padding: var(--space-5);
}

.auth-brand {
    display: flex;
    justify-content: center;
    padding-block: var(--space-6) var(--space-5);
}

.auth-body {
    display: flex;
    flex: 1;
    align-items: flex-start;
    justify-content: center;
}

.auth-card {
    width: 100%;
    max-width: 420px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
}

.auth-card .card-body {
    padding: var(--space-6);
}

.auth-header {
    margin-bottom: var(--space-5);
}

.auth-header h1 {
    font-size: 1.4rem;
}

.auth-header p {
    margin-top: var(--space-1);
    color: var(--text-muted);
    font-size: 0.925rem;
}

.auth-form {
    margin-top: var(--space-5);
}

.auth-form .btn-block {
    margin-top: var(--space-5);
}

.auth-meta {
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
    color: var(--text-muted);
    font-size: 0.9rem;
    text-align: center;
}

/* Desktop app only: "Sign in with your browser", then the in-app form as the fallback. */
.auth-hint {
    margin-top: var(--space-2);
    color: var(--text-muted);
    font-size: 0.85rem;
    text-align: center;
}

.auth-divider {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-5);
    color: var(--text-muted);
    font-size: 0.85rem;
}

.auth-divider::before,
.auth-divider::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--border);
}

.auth-meta + .auth-meta {
    margin-top: var(--space-2);
    padding-top: 0;
    border-top: 0;
}

.auth-footer {
    padding-top: var(--space-5);
    color: var(--text-subtle);
    font-size: 0.825rem;
    text-align: center;
}

.token-input input {
    font-family: var(--font-mono);
    font-variant-ligatures: normal;
    font-feature-settings: normal;
    font-size: 1.4rem;
    letter-spacing: 0.2em;
    text-align: center;
}

/* 7. Utilities ============================================================== */

.text-muted { color: var(--text-muted); }
.text-center { text-align: center; }
.mt-0 { margin-top: 0; }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mb-4 { margin-bottom: var(--space-4); }
/* User text rendered with textContent keeps its line breaks (was nl2br). */
.pre-line { white-space: pre-line; }

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

/* Responsive ---------------------------------------------------------------- */

@media (max-width: 980px) {
    .stat-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .panels {
        grid-template-columns: 1fr;
    }

    .form-grid {
        grid-template-columns: 1fr;
    }

    /* Columns become a horizontally scrolling strip rather than shrinking to
       an unusable width. */
    .board {
        grid-template-columns: repeat(4, minmax(230px, 1fr));
        overflow-x: auto;
        padding-bottom: var(--space-3);
    }
}

@media (max-width: 720px) {
    .container {
        padding-inline: var(--space-4);
    }

    .list-item {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-2);
    }

    .progress {
        width: 100%;
    }

    .app-nav {
        margin-left: var(--space-2);
        gap: 0;
    }

    .app-nav a {
        padding-inline: var(--space-2);
        font-size: 0.9rem;
    }

    .auth-card .card-body {
        padding: var(--space-5);
    }

    /* Hide only the username, never the avatar — which is also a span. */
    .user-chip > span:not(.avatar) {
        display: none;
    }
}

/* Tag delete control ---------------------------------------------------------- */

.tag-remove {
    padding: 0 2px;
    border: 0;
    background: transparent;
    color: var(--text-subtle);
    font-size: 0.9rem;
    line-height: 1;
    cursor: pointer;
}

.tag-remove:hover {
    color: var(--danger);
}

/* Tags under a list row's meta line (dashboard open tasks). */
.list-item-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin-top: var(--space-2);
}

/* Quick add: each suggested tag in its own small container, with the
   animated plus button beside its name. Greys only, from the theme tokens;
   the dot keeps the tag's own colour. */
.tag-options {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: var(--space-2);
    margin-top: var(--space-2);
}

.tag-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-1) var(--space-1) var(--space-3);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 150ms ease;
}

.tag-option:hover,
.tag-option:focus-within {
    border-color: var(--border-strong);
}

.tag-option-name {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    font-size: 0.8rem;
    font-weight: 600;
    overflow-wrap: anywhere;
}


.field input[type="color"] {
    width: 100%;
    height: 42px;
    padding: 4px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    cursor: pointer;
}

/* Pagination ------------------------------------------------------------------- */

.pagination {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1);
    list-style: none;
    margin: 0;
    padding: 0;
}

.pagination li a,
.pagination li span {
    display: inline-flex;
    align-items: center;
    height: 34px;
    padding: 0 var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 0.875rem;
}

.pagination li a:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}

.pagination li.active span {
    background: var(--primary);
    border-color: var(--primary);
    color: var(--on-primary);
    font-weight: 600;
}

.pagination li.disabled span {
    opacity: 0.5;
}

/* ==========================================================================
   8. Accessibility and polish
   ========================================================================== */

/* Skip link: hidden until focused, then pinned to the top-left.
   Fixed rather than absolute so it is unaffected by page scroll. */
.skip-link {
    position: fixed;
    top: -60px;
    left: var(--space-3);
    z-index: 100;
    padding: var(--space-2) var(--space-4);
    background: var(--primary);
    color: var(--on-primary);
    border-radius: 0 0 var(--radius) var(--radius);
    font-weight: 600;
    transition: top 120ms ease;
}

.skip-link:focus {
    top: 0;
    text-decoration: none;
}

/* The main region is focusable only as a skip-link target, so it must not draw
   a focus ring when jumped to. */
#main-content:focus {
    outline: none;
}

/* Respect a reduced-motion preference: transitions are decoration here, and
   the drag-and-drop board is the one place motion is meaningful. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* Touch targets: keep small controls tappable on a phone. */
@media (pointer: coarse) {
    .btn-sm,
    .checklist-box,
    .tag-remove {
        min-height: 36px;
        min-width: 36px;
    }
}

/* Header below 1024px (phones and tablets, either orientation): the one-row bar
   needs ~950px, so go to two rows rather than overflow. Row 1: brand + account
   links. Row 2: tabs, with the theme switch at the right end. */
@media (max-width: 1023px) {
    .app-header {
        position: static;
        height: auto;
    }

    .app-header > .container {
        flex-wrap: wrap;
        gap: var(--space-2);
        padding-block: var(--space-3);
    }

    .brand {
        white-space: nowrap;
    }

    /* The basis leaves just the theme switch beside the tabs, so the tabs
       always start row 2 instead of joining row 1 when it has room. */
    .app-nav {
        order: 3;
        flex: 1 0 calc(100% - 90px);
        margin-left: 0;
        gap: var(--space-1);
    }

    .app-header-actions {
        gap: var(--space-2);
        white-space: nowrap;
    }

    .app-header > .container > .rocker-header {
        order: 4;
    }

    .app-header .rocker-header + .app-header-actions {
        margin-left: auto;
    }
}

@media (max-width: 620px) {
    .page-header-row {
        flex-direction: column;
        align-items: stretch;
    }

    .toolbar .btn {
        flex: 1;
    }

    /* Keep brand and account actions on one row: the PM mark is enough. */
    .brand > span:last-child {
        display: none;
    }

    .app-header > .container > .rocker-header {
        font-size: 7.5px;
    }

    /* Meta lines wrap here, which left "·" separators dangling at line ends;
       the flex gap already spaces the items. */
    .list-item-meta > span[aria-hidden="true"] {
        display: none;
    }

    /* Tighter stats, so the real content starts sooner. */
    .stat-grid {
        gap: var(--space-2);
    }

    .stat {
        padding: var(--space-3);
    }

    .stat-value {
        font-size: 1.35rem;
    }

    /* Stacked list rows: the aside spans the row so progress bars keep a width. */
    .list-item-aside {
        width: 100%;
        flex-wrap: wrap;
    }

    .list-item-aside .progress {
        flex: 1;
    }

    /* Board: one near-full-width column at a time, swiped with snapping. */
    .board {
        grid-template-columns: repeat(4, 85%);
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
    }

    .board-column {
        scroll-snap-align: start;
    }
}

/* Long words and pasted URLs must not push a card wider than its column. */
.list-item-title,
.board-card-title,
.activity-body {
    overflow-wrap: anywhere;
}

/* Tables and wide blocks scroll inside themselves rather than the page. */
.scroll-x {
    overflow-x: auto;
}

/* Theme switch ----------------------------------------------------------------- */
/* Rocker switch from Uiverse.io by bandirevanth. Left (☾) = dark, right (☀) = light;
   the checkbox is checked in dark mode. */
.rocker {
  display: inline-block;
  position: relative;
  /* All sizes are in em, so font-size sets the size of the switch. */
  font-size: 2em;
  font-weight: bold;
  text-align: center;
  text-transform: uppercase;
  color: #888;
  width: 7em;
  height: 4em;
  overflow: hidden;
  border-bottom: 0.5em solid #eee;
}

.rocker-small {
  font-size: 0.75em;
  margin: 1em;
}

/* Sized to sit in the top bar, just left of the account links. */
.rocker-header {
  flex-shrink: 0;
  font-size: 10px;
  margin: 0;
  line-height: 1;
}

.app-header > .container > .rocker-header {
  margin-left: auto;
}

.rocker-header + .app-header-actions {
  margin-left: var(--space-4);
}

.rocker::before {
  content: "";
  position: absolute;
  top: 0.5em;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #999;
  border: 0.5em solid #eee;
  border-bottom: 0;
}

.rocker input {
  opacity: 0;
  width: 0;
  height: 0;
}

.switch-left,
.switch-right {
  cursor: pointer;
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 2.5em;
  width: 3em;
  transition: 0.2s;
  user-select: none;
}

.switch-left {
  height: 2.4em;
  width: 2.75em;
  left: 0.85em;
  bottom: 0.4em;
  background-color: #ddd;
  transform: rotate(15deg) skewX(15deg);
}

.switch-right {
  right: 0.5em;
  bottom: 0;
  background-color: #bd5757;
  color: #fff;
}

.switch-left::before,
.switch-right::before {
  content: "";
  position: absolute;
  width: 0.4em;
  height: 2.45em;
  bottom: -0.45em;
  background-color: #ccc;
  transform: skewY(-65deg);
}

.switch-left::before {
  left: -0.4em;
}

.switch-right::before {
  right: -0.375em;
  background-color: transparent;
  transform: skewY(65deg);
}

/* Drawn moon and sun (Heroicons) in place of the ☾ and ☀ characters, which
   neither typeface has. Sized like the glyphs they replace. */
.switch-left .icon,
.switch-right .icon {
  width: 1.35em;
  height: 1.35em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
}

input:checked + .switch-left {
  background-color: #0084d0;
  color: #fff;
  bottom: 0px;
  left: 0.5em;
  height: 2.5em;
  width: 3em;
  transform: rotate(0deg) skewX(0deg);
}

input:checked + .switch-left::before {
  background-color: transparent;
  width: 3.0833em;
}

input:checked + .switch-left + .switch-right {
  background-color: #ddd;
  color: #888;
  bottom: 0.4em;
  right: 0.8em;
  height: 2.4em;
  width: 2.75em;
  transform: rotate(-15deg) skewX(-15deg);
}

input:checked + .switch-left + .switch-right::before {
  background-color: #ccc;
}

/* Keyboard users */
input:focus + .switch-left {
  color: #333;
}

input:checked:focus + .switch-left {
  color: #fff;
}

input:focus + .switch-left + .switch-right {
  color: #fff;
}

input:checked:focus + .switch-left + .switch-right {
  color: #333;
}

/* The input is invisible, so show keyboard focus on the switch itself. */
.rocker:has(input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* On the sign-in pages the brand is centred, so the clock and switch sit top-right. */
.auth-shell {
  position: relative;
}

.auth-corner {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* Analogue clock ----------------------------------------------------------------- */
/* From Uiverse.io by escannord, redrawn in SVG and recoloured with the site's
   tokens (light and dark). Floats in the bottom-right corner of every page so
   it is always in view; the outer red arc fills as the day passes. The hour and
   minute hands and the arc are set by tickClocks() in app.js. */
.clock {
  position: fixed;
  right: var(--space-4);
  bottom: var(--space-4);
  z-index: 50;
  width: 96px;
  height: 96px;
  padding: 4px;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: var(--shadow);
  line-height: 0;
  pointer-events: auto;
}

.clock svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Keep the last bit of every page clear of the floating clock. */
body {
  padding-bottom: 120px;
}

.clock__day-track {
  fill: none;
  stroke: var(--border);
  stroke-width: 8;
}

.clock__day {
  fill: none;
  stroke: var(--danger);
  stroke-width: 8;
  stroke-linecap: round;
}

.clock__face {
  fill: var(--surface-2);
  stroke: var(--border-strong);
  stroke-width: 10;
}

@media (max-width: 620px) {
  .clock {
    right: var(--space-3);
    bottom: var(--space-3);
    width: 72px;
    height: 72px;
    padding: 3px;
  }

  body {
    padding-bottom: 96px;
  }
}

.clock__mark {
  fill: var(--text-muted);
}

.clock__mark--major {
  fill: var(--text);
}

.clock__hand--h {
  fill: var(--text);
}

.clock__hand--m {
  fill: var(--text-muted);
}

.clock__hand--s {
  fill: var(--danger);
  /* One turn a minute in 480 steps: reads as a sweep, costs no JavaScript.
     tickClocks() sets a negative animation-delay so it starts at the real second. */
  transform-origin: 100px 100px;
  animation: clock-sweep 60s steps(480) infinite;
}

@keyframes clock-sweep {
  to { transform: rotate(360deg); }
}

.clock__cap {
  fill: var(--surface);
  stroke: var(--danger);
  stroke-width: 4;
}

/* Very small phones (≤380px): tighten the account links so they stay beside
   the PM mark even with an unread badge. */
@media (max-width: 380px) {
  .app-header-actions {
    gap: var(--space-1);
    font-size: 0.85rem;
  }

  .app-header-actions .btn,
  .app-header-actions .notification-link {
    padding-inline: var(--space-1);
  }
}

/* Dark mode: the switch's light frame melts into whatever it sits on
   (header or sign-in page), leaving just the rocker. */
:root[data-theme="dark"] .rocker {
  border-bottom-color: transparent;
}

:root[data-theme="dark"] .rocker::before {
  border-color: transparent;
  background-color: transparent;
}

/* Animated checkbox ------------------------------------------------------------ */
/* From Uiverse.io by SelfMadeSystem: an outlined rounded square that fills blue
   while the outline rotates into a white tick. Used by form checkboxes
   (.checkbox input:checked ~ .checkmark) and checklist ticks
   (.checklist-box[aria-pressed="true"] or .is-checked). The outline uses the
   text colour instead of black so it shows in dark mode too. */
.checkmark {
  position: relative;
  display: inline-block;
  top: 0;
  left: 0;
  flex-shrink: 0;
  font-size: 16px; /* sizes the box: 1.3em */
  height: 1.3em;
  width: 1.3em;
  background-color: #2196F300;
  border-radius: 0.25em;
  transition: all 0.25s;
}

.checkmark:after {
  content: "";
  position: absolute;
  transform: rotate(0deg);
  border: 0.1em solid var(--text);
  left: 0;
  top: 0;
  width: 1.05em;
  height: 1.05em;
  border-radius: 0.25em;
  transition: all 0.25s, border-width 0.1s;
}

.checkbox input:checked ~ .checkmark,
.checklist-box[aria-pressed="true"] .checkmark,
.checklist-box.is-checked .checkmark {
  background-color: #2196F3;
}

.checkbox input:checked ~ .checkmark:after,
.checklist-box[aria-pressed="true"] .checkmark:after,
.checklist-box.is-checked .checkmark:after {
  left: 0.45em;
  top: 0.25em;
  width: 0.25em;
  height: 0.5em;
  border-color: #fff0 white white #fff0;
  border-width: 0 0.15em 0.15em 0;
  border-radius: 0em;
  transform: rotate(45deg);
}

/* The Uiverse geometry assumes content-box sizing; the site-wide border-box
   reset would squeeze the tick (its border counted inside 0.25em x 0.5em). */
.checkmark,
.checkmark:after {
  box-sizing: content-box;
}

/* Keyboard focus on the hidden input shows on the visible box. */
.checkbox input:focus-visible ~ .checkmark,
.checklist-box:focus-visible .checkmark {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.checklist-box:focus-visible {
  outline: none;
}

/* Animated plus button ---------------------------------------------------------- */
/* From Uiverse.io by mRcOol7 (converted from Tailwind), in the site's greys:
   turns 90deg and fills on hover, flashes the primary colour while pressed.
   Follows light/dark through the theme tokens. */
.plus-btn {
  display: inline-grid;
  place-items: center;
  flex-shrink: 0;
  padding: 2px;
  border: 0;
  border-radius: 50%;
  background: none;
  line-height: 0;
  cursor: pointer;
  outline: none;
  transition: transform 300ms;
}

.plus-btn:hover {
  transform: rotate(90deg);
}

.plus-icon {
  width: 26px;
  height: 26px;
  stroke-width: 1.5; /* Heroicons outline weight */
  stroke: var(--text-muted);
  fill: none;
  transition: fill 300ms, stroke 300ms;
}

.plus-btn:hover .plus-icon {
  stroke: var(--text);
  fill: var(--border);
}

.plus-btn:active .plus-icon {
  stroke: var(--on-primary);
  fill: var(--primary);
  transition-duration: 0s;
}

.plus-btn:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* Overdue ------------------------------------------------------------------------ */
/* Anything past its deadline and unfinished must read as urgent at a glance:
   a red bar down the left of its row or card, a faint red wash, a solid red
   "Overdue" badge with a pulsing dot, and a banner on its own page. */
.list-item.is-late,
.board-card.is-late {
    background: var(--danger-soft);
    box-shadow: inset 3px 0 0 var(--danger);
}

.board-card.is-late {
    border-color: var(--danger);
}

.badge-overdue {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    background: var(--danger);
    color: #fff;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

.badge-overdue::before {
    content: "";
    width: 0.45em;
    height: 0.45em;
    border-radius: 50%;
    background: #fff;
    animation: overdue-pulse 1.4s ease-in-out infinite;
}

@keyframes overdue-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.25; }
}

.alert-urgent {
    background: var(--danger-soft);
    border-left-color: var(--danger);
    color: var(--danger);
    font-size: 0.9rem;
}

/* A drawn warning sign rather than the ⚠ character, which neither typeface
   has and the system would fill in from a third font. */
.alert-urgent .icon {
    display: inline-block;
    width: 1.15em;
    height: 1.15em;
    margin-right: 0.2em;
    vertical-align: -0.22em;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
}

/* Phones: list rows stack, so pin an unread dot to the row's corner rather
   than letting it drop onto a line of its own. */
@media (max-width: 720px) {
    .list-item {
        position: relative;
    }

    .list-item > .notification-dot {
        position: absolute;
        top: var(--space-4);
        right: var(--space-4);
    }
}

/* Sliding action button ---------------------------------------------------------- */
/* From Uiverse.io by andrew-demchenk0, adapted: an icon panel on the right
   that slides across the whole button on hover or keyboard focus, hiding the
   label. The original was fixed at 150px and green; this works at any width
   and takes each button's own site colours (primary / secondary / danger),
   so it follows light and dark mode. */
.btn-slide {
  --slide-width: 38px;
  position: relative;
  overflow: hidden;
  padding-right: calc(var(--slide-width) + var(--space-3));
}

.btn-sm.btn-slide {
  --slide-width: 30px;
  padding-right: calc(var(--slide-width) + var(--space-2));
}

.btn-slide__text {
  white-space: nowrap;
  transition: color 300ms;
}

/* The panel spans the whole button but is clipped to the right-hand strip;
   hover unclips it (clip-path) while the icon glides to the centre
   (transform). Neither animates a layout property, so no reflow per frame. */
.btn-slide__icon {
  position: absolute;
  inset: 0;
  background-color: var(--slide-panel);
  color: var(--slide-ink);
  clip-path: inset(0 0 0 calc(100% - var(--slide-width)));
  transition: clip-path 300ms, background-color 300ms;
}

/* Full-width box with the icon centred; shifted right so the icon starts
   centred in the strip (50% of this box = half the button's width). */
.btn-slide__glyph {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: translateX(calc(50% - var(--slide-width) / 2));
  transition: transform 300ms;
}

.btn-slide__icon svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5; /* Heroicons outline weight */
  stroke-linecap: round;
  stroke-linejoin: round;
}

.btn-sm .btn-slide__icon svg {
  width: 15px;
  height: 15px;
}

.btn-slide:not(:disabled):hover .btn-slide__text,
.btn-slide:not(:disabled):focus-visible .btn-slide__text {
  color: transparent;
}

.btn-slide:not(:disabled):hover .btn-slide__icon,
.btn-slide:not(:disabled):focus-visible .btn-slide__icon {
  clip-path: inset(0);
}

.btn-slide:not(:disabled):hover .btn-slide__glyph,
.btn-slide:not(:disabled):focus-visible .btn-slide__glyph {
  transform: translateX(0);
}

.btn-slide:not(:disabled):active .btn-slide__icon {
  filter: brightness(0.88);
}

/* Colours per kind: the panel is a shade of the button's own colour. */
.btn-primary.btn-slide {
  --slide-panel: var(--primary-hover);
  --slide-ink: var(--on-primary);
}

.btn-primary.btn-slide:hover {
  background: var(--primary);
}

.btn-secondary.btn-slide {
  --slide-panel: var(--surface-2);
  --slide-ink: var(--text);
}

.btn-danger.btn-slide {
  --slide-panel: var(--danger-soft);
  --slide-ink: var(--danger);
  border-color: var(--danger-soft);
}

.btn-danger.btn-slide:hover {
  background: transparent;
}

/* Members: when the role/remove controls don't fit beside a name, they wrap
   underneath rather than squeezing the name until it breaks mid-word. */
.member-list .list-item {
  flex-wrap: wrap;
  row-gap: var(--space-2);
}

.member-list .list-item-title {
  overflow-wrap: normal;
  word-break: keep-all;
}

.member-list .list-item-aside {
  margin-left: auto;
}

/* 3D box button ------------------------------------------------------------------ */
/* From Uiverse.io by arthur_6104, scaled down for checklist rows and recoloured
   with the site's tokens (light and dark): a raised face on a darker base
   that sinks into it when pressed. Used for each checklist item's edit and
   delete. */
.checklist-tools {
  gap: var(--space-2);
  opacity: 1; /* real buttons now; no longer faded until hover */
}

.checklist-tools .box-button {
  --box-depth: 3px;
  display: inline-block;
  padding: 0 0 var(--box-depth);
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background-color: var(--border-strong);
  color: var(--text);
  line-height: 0;
  cursor: pointer;
  user-select: none;
  transition: 0.1s ease-in-out;
}

.box-button__face {
  display: grid;
  place-items: center;
  width: 26px;
  height: 24px;
  border: 1.5px solid var(--surface);
  border-radius: 3px;
  background-color: var(--surface-2);
}

.box-button__face .icon {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5; /* Heroicons outline weight */
  stroke-linecap: round;
  stroke-linejoin: round;
}

.checklist-tools .box-button--danger {
  color: var(--danger);
}

.checklist-tools .box-button:hover .box-button__face {
  background-color: var(--surface);
}

.checklist-tools .box-button:active {
  padding: 0;
  margin-bottom: var(--box-depth);
  transform: translateY(var(--box-depth));
}

.checklist-tools .box-button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Top-bar pills with rolling letters ---------------------------------------------- */
/* From Uiverse.io by KINGFRESS, adapted: every top-bar button is a rounded
   pill in the site's greys, the current page filled with the primary colour
   (light and dark via tokens), sized to its label. On hover or keyboard focus
   each letter rolls down and out while its twin rolls in from above, staggered
   for any label length (the original handled six letters at a fixed width).
   Selectors are prefixed with .app-header so they win over the older
   .app-nav a / .notification-link rules. */
.app-header .top-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: 36px;
  padding: 0 var(--space-4);
  border: 0;
  border-radius: 2rem;
  background-color: var(--surface-2);
  color: var(--text-muted);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 150ms ease, color 150ms ease;
}

.app-header .top-btn:hover,
.app-header .top-btn:focus-visible {
  background-color: var(--border);
  color: var(--text);
  text-decoration: none;
}

.app-header .top-btn[aria-current="page"],
.app-header .top-btn--primary {
  background-color: var(--primary);
  color: var(--on-primary);
}

.app-header .top-btn:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.roll {
  display: inline-grid;
  overflow: hidden;
  height: 1.2em;
  line-height: 1.2em;
}

.roll__row {
  grid-area: 1 / 1;
  display: flex;
  white-space: pre;
}

.roll__row > span {
  display: inline-block;
  transition: transform calc(0.2s + var(--i) * 0.05s);
}

.roll__row--in > span {
  transform: translateY(-1.2em);
}

.top-btn:hover .roll__row > span,
.top-btn:focus-visible .roll__row > span {
  transform: translateY(1.2em);
}

.top-btn:hover .roll__row--in > span,
.top-btn:focus-visible .roll__row--in > span {
  transform: translateY(0);
}

/* Phones: slimmer pills so the two header rows still fit at 360px. */
@media (max-width: 620px) {
  .app-header .top-btn {
    height: 32px;
    padding: 0 var(--space-3);
    font-size: 0.8rem;
  }
}

@media (max-width: 380px) {
  .app-header .top-btn {
    padding: 0 var(--space-2);
  }
}

/* Search pill (icon only) --------------------------------------------------------- */
.app-header .top-btn--icon {
  width: 36px;
  padding: 0;
}

.top-btn__icon {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

@media (max-width: 620px) {
  .app-header .top-btn--icon {
    width: 32px;
    padding: 0;
  }
}

/* Sync pill ------------------------------------------------------------------------ */
/* Where the local copy stands (Synced / Offline / N waiting / Problem). Tinted by state,
   with the words and the icon saying the same thing, so colour is never the only signal. */
.app-header .sync-pill[data-state="waiting"] {
  --tint: var(--warning);
  color: var(--tint);
}

.app-header .sync-pill[data-state="problem"] {
  --tint: var(--danger);
  color: var(--tint);
}

.app-header .sync-pill[data-state="offline"] {
  --tint: var(--text-subtle);
  color: var(--tint);
}

.sync-pill__count {
  display: none; /* the wide pill says "N waiting" in words; the badge is for the icon-only pill */
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  /* A disc in the pill's own tint, with the number cut out of it in the surface colour. */
  background: var(--tint, currentColor);
  color: var(--surface);
  font-size: 0.7rem;
  font-weight: 700;
}

.sync-panel {
  position: fixed;
  right: var(--space-4);
  z-index: 60;
  width: min(380px, calc(100vw - 2 * var(--space-4)));
  max-height: 70vh;
  overflow-y: auto;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  color: var(--text);
}

.sync-panel[hidden] {
  display: none;
}

.sync-panel__title {
  margin: 0 0 var(--space-3);
  font-size: 0.95rem;
}

.sync-panel__empty,
.sync-panel__note {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.85rem;
}

.sync-panel__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.sync-panel__list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
}

.sync-panel__list .is-failed .sync-panel__note {
  color: var(--danger);
}

/* The word beside the icon only fits a wide header; otherwise the pill is icon-only (with its
   count), so the one-row header at 1024px and the two-row one below it both still fit. */
@media (max-width: 1279px) {
  .app-header .sync-pill {
    padding: 0 var(--space-3);
  }

  .sync-pill__label {
    display: none;
  }

  .sync-pill__count:not([hidden]) {
    display: inline-grid;
  }
}

@media (max-width: 620px) {
  .app-header .sync-pill {
    padding: 0 var(--space-2);
  }
}

/* Time tracking ------------------------------------------------------------------- */
.time-meter .progress {
  width: 100%;
  height: 8px;
}

.time-meter.is-over .progress-bar {
  background: var(--danger);
}

.time-meter.is-over .hint {
  color: var(--danger);
}

.time-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.time-running {
  font-family: var(--font-mono);
  font-size: 1.25rem;
  font-variant-numeric: tabular-nums;
  color: var(--danger);
}

.time-log {
  display: grid;
  grid-template-columns: 1fr 1.4fr auto;
  align-items: end;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.time-log .field {
  margin: 0;
}

.time-entries {
  border-top: 1px solid var(--border);
}

@media (max-width: 620px) {
  .time-log {
    grid-template-columns: 1fr;
  }
}

/* Analytics charts ---------------------------------------------------------------- */
.panels--even {
  grid-template-columns: 1fr 1fr;
}

@media (max-width: 980px) {
  .panels--even {
    grid-template-columns: 1fr;
  }
}

.chart {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.chart-grid {
  stroke: var(--border);
  stroke-width: 1;
}

.chart-axis {
  fill: var(--text-muted);
  font-size: 11px;
  font-family: var(--font-mono); /* axis labels are numbers and dates: data */
}

.chart-bar {
  fill: var(--text);
}

.chart-line {
  fill: none;
  stroke: var(--text);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.chart-dot {
  fill: var(--text);
  stroke: var(--surface);
  stroke-width: 2;
  opacity: 0;
}

.chart-hit:hover .chart-bar {
  fill: var(--text-muted);
}

.chart-hit:hover .chart-dot {
  opacity: 1;
}

.chart-table {
  margin-top: var(--space-3);
  font-size: 0.825rem;
  color: var(--text-muted);
}

.chart-table summary {
  cursor: pointer;
}

.chart-table table {
  margin-top: var(--space-2);
  border-collapse: collapse;
}

.chart-table th,
.chart-table td {
  padding: 2px var(--space-3) 2px 0;
  text-align: left;
}

.member-bars {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
}

.member-bars li {
  display: grid;
  grid-template-columns: minmax(90px, 160px) 1fr auto;
  align-items: center;
  gap: var(--space-3);
  font-size: 0.875rem;
}

.member-bars__track {
  height: 10px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}

.member-bars__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--text);
}

.member-bars__value {
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* Search ------------------------------------------------------------------------- */
.search-form {
  position: relative;
  margin-bottom: var(--space-4);
}

.search-form input {
  width: 100%;
  height: 48px;
  padding: 0 var(--space-4) 0 44px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 1rem;
}

.search-form__icon {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  line-height: 0;
  color: var(--text-muted);
}

.search-form__icon .icon {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

.search-result__snippet {
  margin: var(--space-2) 0 0;
  color: var(--text-muted);
  font-size: 0.875rem;
}

.search-result__snippet mark {
  background: var(--warning-soft);
  color: var(--text);
  border-radius: 2px;
  padding: 0 2px;
}

/* Wandering eyes loader ------------------------------------------------------ */
/* Port of the WanderingEyes component the user supplied: two eyes glance left,
   right and down, blinking now and then. Sized in em (font-size sets the eye
   size) and drawn in currentColor, so it follows light and dark themes. Made by
   loader() in app.js; forms.js adds the inline size to busy submit buttons. */
.eyes {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.145em;
  height: 1em;
  color: var(--text-muted);
  vertical-align: middle;
}

/* Before any script has run the body holds no elements (only whitespace, so it
   is not :empty): draw the eyes in CSS alone, so they show from first paint. */
body:not(:has(*)) {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.145em;
  padding: 0;
  font-size: 44px;
  color: var(--text-muted);
  animation: eyes-in 0.2s 0.15s both;
}

.eyes__eye,
body:not(:has(*))::before,
body:not(:has(*))::after {
  content: "";
  display: inline-block;
  width: 1em;
  height: 1em;
  border-radius: 50%;
  background-color: color-mix(in srgb, currentColor 16%, transparent);
  background-image: radial-gradient(circle 0.32em, currentColor 100%, transparent 0);
  background-repeat: no-repeat;
  animation: eyes-move 4s infinite, eyes-blink 4s infinite;
}

.eyes--page {
  display: flex;
  min-height: 40vh;
  height: auto;
  font-size: 44px;
  /* Fast loads never show it: it only fades in after 150ms. */
  animation: eyes-in 0.2s 0.15s both;
}

.eyes--inline {
  font-size: 0.9em;
  margin-right: 0.4em;
  color: inherit;
}

@keyframes eyes-in {
  from { opacity: 0; }
}

@keyframes eyes-move {
  0%, 10% { background-position: 0 0; }
  13%, 40% { background-position: -0.3125em 0; }
  43%, 70% { background-position: 0.3125em 0; }
  73%, 90% { background-position: 0 0.3125em; }
  93%, 100% { background-position: 0 0; }
}

@keyframes eyes-blink {
  0%, 10%, 12%, 20%, 22%, 40%, 42%, 60%, 62%, 70%, 72%, 90%, 92%, 98%, 100% { height: 1em; }
  11%, 21%, 41%, 61%, 71%, 91%, 99% { height: 0.375em; }
}
