html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
html {
    background: var(--ds-surface);
}

:root {
    --topbar-h: calc(56px + env(safe-area-inset-top, 0px));
}

a, .btn-link {
    color: #006bb7;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.content {
    padding: 2rem 2.5rem !important;
    max-width: 1280px;
}

/* Responsive content padding — much tighter on phones to reclaim horizontal space */
@media (max-width: 768px) {
    .content {
        padding: 1.25rem 1rem !important;
    }
}
@media (max-width: 480px) {
    .content {
        padding: 1rem 0.75rem !important;
    }
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

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

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}
/* Global pill-shaped buttons */
.btn,
.btn-group > .btn,
.btn-group-vertical > .btn {
    border-radius: 50rem;
}

/* Apply Lato font globally */
html, body, .btn, .form-control, .form-select, .dropdown-menu, input, textarea, select, button {
    font-family: 'Lato', system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
}

/* Dark mode adaptations for hardcoded utility classes */
[data-bs-theme="dark"] .bg-light {
    background-color: var(--bs-tertiary-bg) !important;
    color: var(--bs-body-color) !important;
}
[data-bs-theme="dark"] .bg-white {
    background-color: var(--bs-body-bg) !important;
    color: var(--bs-body-color) !important;
}
[data-bs-theme="dark"] pre.bg-light,
[data-bs-theme="dark"] .alert-light {
    background-color: var(--bs-tertiary-bg) !important;
    color: var(--bs-body-color) !important;
}

/* ==========================================================================
   Design system tokens & components
   ========================================================================== */
[data-bs-theme="dark"] {
    --ds-bg: #0e1116;
    --ds-surface: #161b22;
    --ds-surface-2: #1c222b;
    --ds-surface-hover: #21283086;
    --ds-hairline: rgba(255,255,255,.08);
    --ds-hairline-strong: rgba(255,255,255,.14);
    --ds-text: #e6edf3;
    --ds-text-muted: #7d8590;
    /* Bumped from #5d6571 (2.7:1 vs --ds-surface) to meet WCAG AA on small text. */
    --ds-text-faint: #9098a3;
    --ds-accent: #2f81f7;
    --ds-accent-hover: #4493f8;
    --ds-accent-bg: rgba(47,129,247,.14);
    --ds-success: #3fb950;
    --ds-success-bg: rgba(63,185,80,.14);
    --ds-warning: #d29922;
    --ds-warning-bg: rgba(210,153,34,.14);
    --ds-danger: #f85149;
    --ds-danger-bg: rgba(248,81,73,.14);
    --ds-shadow-2: 0 8px 24px rgba(0,0,0,.30);
    /* Modal/dialog backdrop, themed so it doesn't look harsh in light mode. */
    --ds-scrim: rgba(0,0,0,.55);
}
[data-bs-theme="light"] {
    --ds-bg: #fbfbfd;
    --ds-surface: #ffffff;
    --ds-surface-2: #f5f5f7;
    --ds-surface-hover: #f0f0f3;
    --ds-hairline: rgba(0,0,0,.08);
    --ds-hairline-strong: rgba(0,0,0,.14);
    --ds-text: #1d1d1f;
    --ds-text-muted: #6e6e73;
    /* Bumped from #86868b (3.3:1 vs --ds-surface) to meet WCAG AA on small text. */
    --ds-text-faint: #6c6c70;
    --ds-accent: #0066ff;
    --ds-accent-hover: #0052cc;
    --ds-accent-bg: rgba(0,102,255,.10);
    --ds-success: #1f7a3c;
    --ds-success-bg: rgba(31,122,60,.10);
    --ds-warning: #bf6900;
    --ds-warning-bg: rgba(191,105,0,.10);
    --ds-danger: #d12a2a;
    --ds-danger-bg: rgba(209,42,42,.10);
    --ds-shadow-2: 0 8px 24px rgba(0,0,0,.10);
    --ds-scrim: rgba(0,0,0,.32);
}

html, body { background: var(--ds-bg); color: var(--ds-text); -webkit-font-smoothing: antialiased; }
*:focus-visible { outline: 3px solid var(--ds-accent); outline-offset: 2px; border-radius: 8px; }
.attachment-paste-zone:focus-visible { outline: none; }
/* 📋 Paste image or the words "or paste image", never both (e7f25630). js/paste-upload.js puts
   html.td-can-read-clipboard on where the browser lets a page read a picture off the clipboard on a
   press, and keeps it there across page changes; elsewhere the words stay and ⌘V is the way. */
html:not(.td-can-read-clipboard) [data-td-paste-picture] { display: none; }
html.td-can-read-clipboard .td-paste-hint { display: none; }

/* Form-control focus halo. Lots of inputs across the app do `outline: none; border-color:
   var(--ds-accent);` — that's a focus indicator but a subtle one. The box-shadow halo here
   stacks on top so keyboard users always see a clearly visible focus ring without changing
   layout. Applied only on :focus-visible so mouse clicks don't get the halo too. */
input:focus-visible, textarea:focus-visible, select:focus-visible {
    box-shadow: 0 0 0 3px var(--ds-accent-bg);
}

/* Page header */
.ds-page-head {
    display: flex; align-items: flex-end; justify-content: space-between; gap: 16px;
    padding-bottom: 24px; margin-bottom: 24px;
    border-bottom: 1px solid var(--ds-hairline);
    flex-wrap: wrap;
}
.ds-page-head h1 { margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.02em; }
.ds-page-head .ds-subtitle { margin-top: 4px; font-size: 14px; color: var(--ds-text-muted); }
.ds-page-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* Page header tightens on mobile so the heading + action buttons don't crowd the page */
@media (max-width: 768px) {
    .ds-page-head {
        padding-bottom: 16px; margin-bottom: 16px; gap: 10px;
        align-items: flex-start;
    }
    .ds-page-head h1 { font-size: 22px; letter-spacing: -0.01em; }
    .ds-page-head .ds-subtitle { font-size: 13px; }
    .ds-page-actions { width: 100%; }
}

/* ==========================================================================
   Hover styling is for pointers only
   ==========================================================================
   Every :hover rule in this app that DECORATES — here and in the page components — sits inside
   `@media (hover: hover) and (pointer: fine)`.

   iOS does not clear :hover when you lift your finger. It leaves the tapped element hovered
   until you tap somewhere else, so any decoration hover applies is a highlight that stays put.
   Worse when the list under it changes: tick a task, the row collapses out, the next row takes
   its place — and the leftover green ring from `.pd-circle:hover` is now sitting on a task that
   is not complete. That is the bug this guard fixes; the general "leftover highlights on a
   phone" feeling is the same cause everywhere else.

   Three things this is NOT allowed to do:

   - Take a focus style with it. :focus / :focus-visible must keep working on every device, so
     where a selector list carried both they are split and only the hover half is guarded.

   - Cost a control its only way of being reached. Five rules REVEAL a button rather than
     decorate one — .pd-row-rename in ProjectDetail, .sn-kebab in StickyNotes, .grp-cat-actions
     and .grp-msg-actions in Groups, .te-history-revert in TaskEditor — and each is left
     UNGUARDED on purpose. Where there is no pointer, iOS's sticky :hover is the affordance that
     uncovers them, and it always has been. A revealed button that lingers tells no lie about the
     row it sits on; a leftover highlight does. Pinning them permanently visible instead was
     tried and is worse: .grp-msg-actions is positioned over the message bubble's own first line,
     and .grp-cat-actions puts a one-tap unconfirmed delete inside the row you tap to collapse a
     category. Only .pd-row-rename keeps an `@media (hover: none)` always-visible rule, which is
     the trade it already shipped with.

   - Stop at this codebase. bootstrap.min.css loads before this file (App.razor) and carries its
     own unguarded `.btn:hover`, `.btn-close:hover` and `.dropdown-item:hover`, which no guard
     here can reach. See the neutralising block immediately below.

   In a .razor file the escaped form `@@media` is required; plain `@media` there is a Razor
   directive and will not compile. This file is plain CSS, so `@media` is correct here. */

/* Bootstrap's own hover rules, cancelled where there is no pointer.
   `.btn:hover` alone paints over 70-odd buttons in this app, so without this the stuck-highlight
   symptom survives the whole change on the most-tapped control there is. app.css is the later
   sheet, so the fix belongs here rather than in the vendored file.

   `not all and (...)` is the exact negation of the guard above, so every device lands in one
   branch or the other — `(hover: none)` alone would miss a hover-capable coarse pointer.

   The .btn rule works by pointing each variant's hover token back at its own resting token, which
   covers every btn-* at once. The two rules after it exist because `.btn:hover` (0,2,0) still
   outranks this file's own `.btn-primary` / `.btn-link` (0,1,0), so those resting colours have to
   be restated at hover specificity or the button would settle on Bootstrap's blue instead of
   ours. Nothing here changes what a pointer device does. */
@media not all and (hover: hover) and (pointer: fine) {
    .btn:hover {
        --bs-btn-hover-color: var(--bs-btn-color);
        --bs-btn-hover-bg: var(--bs-btn-bg);
        --bs-btn-hover-border-color: var(--bs-btn-border-color);
    }
    .btn-primary:hover { color: #fff; background-color: #1b6ec2; border-color: #1861ac; }
    .btn-link:hover { color: #006bb7; }
    .btn-close:hover { opacity: var(--bs-btn-close-opacity); }
    /* This one is a regression as well as a leftover: guarding our own `.dropdown-item:hover`
       below hands the touch case straight to Bootstrap's, which paints --bs-tertiary-bg on menus
       drawn on --ds-surface. Resting values, restated. */
    .dropdown-item:hover { color: var(--ds-text); background-color: transparent; }
}

/* Buttons (design-system pills) */
.ds-btn {
    border: 1px solid transparent;
    border-radius: 999px;
    padding: 8px 16px;
    font-size: 14px; font-weight: 600;
    background: transparent; color: var(--ds-text);
    transition: background .12s ease, transform .04s ease;
    cursor: pointer;
}
.ds-btn:active { transform: scale(.98); }
.ds-btn-primary { background: var(--ds-accent); color: #fff; border-color: var(--ds-accent); }
@media (hover: hover) and (pointer: fine) {
    .ds-btn-primary:hover { background: var(--ds-accent-hover); border-color: var(--ds-accent-hover); }
}
.ds-btn-ghost { background: var(--ds-surface-2); border-color: var(--ds-hairline); color: var(--ds-text); }
@media (hover: hover) and (pointer: fine) {
    .ds-btn-ghost:hover { background: var(--ds-surface-hover); }
}
/* A switch that is ON (Show completed): the same words, lit in the accent with a ✓, like the
   Timeline's Day / Week / Month picks. Off, it is the plain ghost button. */
.ds-btn-ghost[aria-pressed="true"] { background: var(--ds-accent); border-color: var(--ds-accent); color: #fff; }
@media (hover: hover) and (pointer: fine) {
    .ds-btn-ghost[aria-pressed="true"]:hover { background: var(--ds-accent-hover); border-color: var(--ds-accent-hover); }
}
.ds-btn-tick { margin-right: 4px; }
.ds-btn-danger-text { color: var(--ds-danger); background: transparent; }
@media (hover: hover) and (pointer: fine) {
    .ds-btn-danger-text:hover { background: var(--ds-danger-bg); }
}
.ds-btn-icon {
    width: 32px; height: 32px; padding: 0;
    border-radius: 999px; border: none;
    background: transparent; color: var(--ds-text-muted);
    display: inline-grid; place-items: center;
    font-size: 22px; font-weight: 900; line-height: 1;
    cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
    .ds-btn-icon:hover { background: transparent; color: var(--ds-text); }
}

/* Cards / pills / meta */
.ds-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
@media (max-width: 1100px) { .ds-cards { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 760px) { .ds-cards { grid-template-columns: 1fr; } }

.ds-card {
    background: var(--ds-surface);
    border: 1px solid var(--ds-hairline);
    border-radius: 12px;
    padding: 18px 18px 16px;
    cursor: pointer;
    display: flex; flex-direction: column; gap: 14px;
    transition: border-color .12s ease, transform .12s ease, box-shadow .12s ease;
    color: var(--ds-text);
}
@media (hover: hover) and (pointer: fine) {
    .ds-card:hover { border-color: var(--ds-hairline-strong); transform: translateY(-1px); box-shadow: var(--ds-shadow-2); }
}
.ds-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.ds-card-title { font-size: 15px; font-weight: 600; line-height: 1.3; letter-spacing: -0.01em; }
.ds-card-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 12px; border-top: 1px solid var(--ds-hairline); color: var(--ds-text-muted); font-size: 12px; }

.ds-pill {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 10px; font-size: 12px; font-weight: 600;
    border-radius: 999px; white-space: nowrap;
}
.ds-pill-success { color: var(--ds-success); background: var(--ds-success-bg); }
.ds-pill-warning { color: var(--ds-warning); background: var(--ds-warning-bg); }
.ds-pill-danger  { color: var(--ds-danger);  background: var(--ds-danger-bg);  }
.ds-pill-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

.ds-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; font-size: 13px; }
.ds-meta dt { color: var(--ds-text-muted); font-weight: 500; }
.ds-meta dd { margin: 0; color: var(--ds-text); font-weight: 500; }

.ds-assignee { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ds-avatar {
    width: 22px; height: 22px; border-radius: 50%;
    background: linear-gradient(135deg, #8b5cf6, var(--ds-accent));
    color: white; font-size: 11px; font-weight: 700;
    display: grid; place-items: center;
    border: 1px solid var(--ds-hairline);
    flex: 0 0 22px;
}
.ds-avatar-stack { display: inline-flex; }
.ds-avatar-stack .ds-avatar:not(:first-child) { margin-left: -6px; }

/* Category headings */
.ds-category { margin-bottom: 28px; }
.ds-category-head {
    display: flex; align-items: center; gap: 10px; padding: 4px 0 14px;
    color: var(--ds-text);
}
.ds-category-head h2 { font-size: 17px; font-weight: 600; margin: 0; letter-spacing: -0.01em; }
.ds-category-head .ds-count {
    color: var(--ds-text-muted); font-size: 13px; font-weight: 500;
    background: var(--ds-surface-2); padding: 2px 8px; border-radius: 999px;
}
.ds-subcategory { padding-left: 20px; border-left: 1px solid var(--ds-hairline); margin-left: 4px; margin-top: 14px; }
.ds-subcategory .ds-category-head h2 { font-size: 14px; color: var(--ds-text-muted); }

/* Detail view */
.ds-detail { background: var(--ds-surface); border: 1px solid var(--ds-hairline); border-radius: 16px; overflow: hidden; }
.ds-detail-head {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 16px 20px; border-bottom: 1px solid var(--ds-hairline);
}
.ds-detail-title { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }
.ds-detail-section { padding: 18px 20px; border-bottom: 1px solid var(--ds-hairline); }
.ds-detail-section:last-child { border-bottom: 0; }
.ds-detail-section h3 {
    margin: 0 0 12px; font-size: 13px; font-weight: 600; color: var(--ds-text-muted);
    text-transform: uppercase; letter-spacing: 0.04em;
}

/* Checklist (intervals + linked tasks) */
.ds-checklist { display: flex; flex-direction: column; }
.ds-chk {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--ds-hairline);
}
.ds-chk:last-child { border-bottom: 0; }
.ds-chk-box {
    width: 18px; height: 18px; border-radius: 6px;
    border: 1px solid var(--ds-hairline-strong);
    display: grid; place-items: center;
    flex: 0 0 18px; cursor: pointer; background: transparent;
    color: white;
}
.ds-chk-box.done { background: var(--ds-success); border-color: var(--ds-success); }
.ds-chk-box[disabled] { opacity: .4; cursor: not-allowed; }
.ds-chk-grow { flex: 1; min-width: 0; }
.ds-chk .ds-pill { font-size: 11px; padding: 2px 8px; }

/* Inputs in dark theme: tonal */
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select,
[data-bs-theme="dark"] textarea {
    background-color: var(--ds-surface-2);
    border-color: var(--ds-hairline);
    color: var(--ds-text);
}

/* Sidebar (overrides MainLayout default) */
.page > .sidebar { background-image: none !important; background: var(--ds-surface) !important; border-right: 1px solid var(--ds-hairline); }

/* Dropdown menu adapts to theme */
.dropdown-menu {
    background-color: var(--ds-surface);
    border: 1px solid var(--ds-hairline);
    color: var(--ds-text);
    border-radius: 12px;
    padding: 6px;
}
.dropdown-item { color: var(--ds-text); border-radius: 8px; padding: 8px 14px; font-size: 14px; width: 100%; text-align: left; background: transparent; border: 0; }
@media (hover: hover) and (pointer: fine) {
    .dropdown-item:hover { background-color: var(--ds-surface-2); color: var(--ds-text); }
}

/* Sidebar / NavMenu (global so it overrides default <a> link styles) */
.ds-nav { padding: 20px 12px; display: flex; flex-direction: column; gap: 4px; height: 100%; }
.ds-brand {
    display: flex; align-items: center; gap: 10px;
    padding: 6px 12px 18px;
    font-weight: 700; font-size: 17px; letter-spacing: -0.01em;
    color: var(--ds-text);
}
.ds-brand-mark {
    width: 28px; height: 28px;
    background: linear-gradient(135deg, var(--ds-accent), #8b5cf6);
    border-radius: 8px; display: grid; place-items: center;
    color: #fff; font-weight: 800; font-size: 14px;
}
.ds-nav-group {
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--ds-text-faint);
    padding: 14px 14px 6px; font-weight: 600;
}
a.ds-nav-item, .ds-nav-item {
    display: flex; align-items: center; gap: 12px;
    padding: 8px 12px;
    border-radius: 8px;
    color: var(--ds-text-muted) !important;
    text-decoration: none !important;
    font-size: 14px; font-weight: 500;
    border-left: 3px solid transparent;
    transition: background .12s ease, color .12s ease;
}
@media (hover: hover) and (pointer: fine) {
    a.ds-nav-item:hover, .ds-nav-item:hover {
        background: var(--ds-surface-2);
        color: var(--ds-text) !important;
    }
}
a.ds-nav-item.active, .ds-nav-item.active {
    background: var(--ds-accent-bg);
    color: var(--ds-accent) !important;
    border-left-color: var(--ds-accent);
    font-weight: 600;
}
.ds-nav-icon { width: 18px; height: 18px; flex: 0 0 18px; }

/* Settings sections */
.ds-section {
    background: var(--ds-surface);
    border: 1px solid var(--ds-hairline);
    border-radius: 12px;
    padding: 20px 22px;
    margin-bottom: 16px;
}
.ds-section.ds-section-danger { border-color: rgba(248,81,73,.30); }
[data-bs-theme="light"] .ds-section.ds-section-danger { border-color: rgba(209,42,42,.30); }
.ds-section-head {
    display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
    margin-bottom: 16px;
}
.ds-section-title { font-size: 17px; font-weight: 600; margin: 0; letter-spacing: -0.01em; color: var(--ds-text); }
.ds-section-desc { color: var(--ds-text-muted); font-size: 13px; margin: 4px 0 0; }
.ds-section-row {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
}

/* Form fields */
.ds-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.ds-field-label { font-size: 13px; font-weight: 600; color: var(--ds-text); }
.ds-field-help { font-size: 12px; color: var(--ds-text-muted); margin-top: 4px; }
.ds-input,
.ds-section input[type="text"],
.ds-section input[type="email"],
.ds-section input[type="password"],
.ds-section input[type="number"],
.ds-section select,
.ds-section textarea {
    background: var(--ds-surface-2);
    border: 1px solid var(--ds-hairline);
    border-radius: 8px;
    padding: 8px 12px;
    color: var(--ds-text);
    font-size: 14px;
    width: 100%;
    font-family: inherit;
}
.ds-section input:focus,
.ds-section select:focus,
.ds-section textarea:focus {
    outline: 2px solid var(--ds-accent);
    outline-offset: 1px;
    border-color: var(--ds-accent);
}

/* Switch (toggle) */
.ds-switch { position: relative; width: 44px; height: 26px; flex: 0 0 44px; display: inline-block; }
.ds-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
.ds-switch-slider {
    position: absolute; inset: 0;
    background: var(--ds-surface-2); border: 1px solid var(--ds-hairline);
    border-radius: 999px; transition: background .15s, border-color .15s; cursor: pointer;
}
.ds-switch-slider::before {
    content: ''; position: absolute; top: 2px; left: 2px;
    width: 20px; height: 20px; background: #fff; border-radius: 50%;
    transition: transform .15s;
    box-shadow: 0 1px 2px rgba(0,0,0,.25);
}
.ds-switch input:checked ~ .ds-switch-slider { background: var(--ds-accent); border-color: var(--ds-accent); }
.ds-switch input:checked ~ .ds-switch-slider::before { transform: translateX(18px); }

/* Badges (time zone, etc.) */
.ds-badge {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 12px;
    background: var(--ds-surface-2);
    border: 1px solid var(--ds-hairline);
    border-radius: 999px;
    font-size: 13px; color: var(--ds-text); font-weight: 500;
}
/* Semantic status badges. Translucent bg + bright fg color always meets WCAG AA against the
   page surface in both themes, unlike the previous solid-fill / white-text badges which
   fell to ~1.9:1 on cyan ("Triaged") and ~3.5:1 on green / blue. */
.ds-badge-open { background: var(--ds-surface-2); color: var(--ds-text-muted); border-color: var(--ds-hairline); }
.ds-badge-triaged { background: var(--ds-warning-bg); color: var(--ds-warning); border-color: rgba(210,153,34,.30); }
[data-bs-theme="light"] .ds-badge-triaged { border-color: rgba(191,105,0,.20); }
.ds-badge-ongithub { background: var(--ds-accent-bg); color: var(--ds-accent); border-color: rgba(47,129,247,.30); }
[data-bs-theme="light"] .ds-badge-ongithub { border-color: rgba(0,102,255,.20); }
.ds-badge-resolved { background: var(--ds-success-bg); color: var(--ds-success); border-color: rgba(63,185,80,.30); }
[data-bs-theme="light"] .ds-badge-resolved { border-color: rgba(31,122,60,.20); }

/* Task status pills. One class per TONE in the TaskStatuses catalogue, not one per status, so a
   new status needs a catalogue row and nothing here. Same recipe as the semantic badges above —
   translucent tint, bright text, a faint border — so they read at AA in both themes, and the same
   shape as the due-date chip they sit next to on a project row. "done" is the completion tick
   wearing the pill's shape: a completed task says Done whatever its status. */
.td-status {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 2px 8px;
    border-radius: 999px;
    border: 1px solid transparent;
    font-size: 12px; font-weight: 600; line-height: 1.4;
    white-space: nowrap;
    max-width: 100%;
}
.td-status-neutral { background: var(--ds-surface-2); color: var(--ds-text-muted); border-color: var(--ds-hairline); }
.td-status-info    { background: var(--ds-accent-bg);  color: var(--ds-accent);  border-color: rgba(47,129,247,.30); }
.td-status-go      { background: var(--ds-success-bg); color: var(--ds-success); border-color: rgba(63,185,80,.30); }
.td-status-wait    { background: var(--ds-warning-bg); color: var(--ds-warning); border-color: rgba(210,153,34,.30); }
.td-status-warn    { background: rgba(255,123,0,.14);  color: #ff9a3c;           border-color: rgba(255,154,60,.30); }
.td-status-stop    { background: var(--ds-danger-bg);  color: var(--ds-danger);  border-color: rgba(248,81,73,.30); }
.td-status-hold    { background: rgba(163,113,247,.14); color: #b48cf5;          border-color: rgba(163,113,247,.30); }
.td-status-done    { background: var(--ds-success);    color: #fff;              border-color: var(--ds-success); }
/* "Claude's turn": a MARK beside the pill, never a status (15 Sep 2026) — a person wrote after
   Claude's last word. Its own colour, neither the amber of "waiting on you" nor the green of "go",
   because it is neither: the move is Claude's. Triage wore it until Triage was retired. */
.td-status-ask     { background: rgba(56,189,248,.14);  color: #38bdf8;           border-color: rgba(56,189,248,.30); }
[data-bs-theme="light"] .td-status-info { border-color: rgba(0,102,255,.20); }
[data-bs-theme="light"] .td-status-go   { border-color: rgba(31,122,60,.20); }
[data-bs-theme="light"] .td-status-wait { border-color: rgba(191,105,0,.20); }
[data-bs-theme="light"] .td-status-warn { background: rgba(204,85,0,.10);  color: #b34a00; border-color: rgba(204,85,0,.20); }
[data-bs-theme="light"] .td-status-stop { border-color: rgba(209,42,42,.20); }
[data-bs-theme="light"] .td-status-hold { background: rgba(110,60,200,.10); color: #5b2fb3; border-color: rgba(110,60,200,.20); }
[data-bs-theme="light"] .td-status-ask  { background: rgba(2,132,199,.10);  color: #0369a1; border-color: rgba(2,132,199,.20); }
/* The bundle tag at the end of the task panel header's project line (TaskEditor.StatusPill): the
   project, the status pill, the marks, then the tag, on one line that wraps (.te-context-line). IT
   SHOWS ITS WHOLE NAME, never "…" (1dc0afd8): no width cap of its own, only the line's width. A name
   that fits sits on that line; one that does not wraps to the next, and a name longer than the whole
   line wraps inside the tag. A 10px corner rather than a full
   pill, so a two-line tag is a rounded box and not a lozenge; on one line it reads the same. */
.te-bundle {
    display: inline-flex; align-items: center; gap: 4px;
    flex: 0 1 auto; max-width: 100%; min-width: 0;
    font-size: 11.5px; font-weight: 600; line-height: 1.4;
    padding: 2px 8px; border-radius: 10px;
    color: var(--ds-accent); background: var(--ds-accent-bg);
    border: 1px solid rgba(47,129,247,.30);
    white-space: normal; overflow-wrap: anywhere;
    text-decoration: none;
}
/* It is a link to the bundle when it has one (d6847fb8): it opens the project showing only the
   bundle's tasks. A tap target, so a hover and a keyboard focus show it. */
/* Above the status pill's taller tap area (.te-status-pill::after reaches 8px up and down): when the
   line wraps on a phone and the tag lands under the pill, a tap on the tag's top edge opens the bundle,
   not the status sheet. */
a.te-bundle[href] { cursor: pointer; position: relative; z-index: 1; }
a.te-bundle:hover { color: var(--ds-accent); border-color: var(--ds-accent); text-decoration: none; }
a.te-bundle:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }

/* ----- an agent's hand in the task panel: "Claude · via Josh" comments, the via-agent chip, split links -----
   The conversation's own rules (.cmt-*) live in TaskEditor.razor; these are the additions for the
   other hand. Tokens carry the theme; the light block below only tightens the borders the same
   way the status pills do. */
.te-comment-agent .cmt-avatar.te-comment-agent-avatar {
    background: var(--ds-success-bg); color: var(--ds-success);
    border: 1px solid rgba(63,185,80,.30); font-size: 13px;
}
/* A comment written on GitHub by somebody with no TDPro account ("octo-cat · via GitHub"): a grey
   circle with GH in it and a dashed edge, so it never passes for a teammate's initials. */
.cmt-avatar.te-comment-github-avatar {
    background: var(--ds-surface-2); color: var(--ds-text-muted);
    border: 1px dashed var(--ds-text-muted); font-size: 11px;
}
/* Claude's row in the task comment box's @ list: the same green C, on the list's small avatar. Its
   own selector because the rule above is scoped to a comment by the agent. */
.mention-item .st-avatar-sm.mention-agent-avatar {
    background: var(--ds-success-bg); color: var(--ds-success);
    border: 1px solid rgba(63,185,80,.30); font-size: 11px;
}
/* ----- a person's photo, in place of their initials (e7d860cb) -----
   PersonFace puts an <img class="person-photo"> inside whatever circle the page draws, after the
   letters: a person's photo, the Claude logo, or a GitHub writer's picture. The circle keeps its own
   size, colour and shape; the picture covers it, clipped to its round edge. The letters stay under
   it while it loads, and show if it never does (js/photo-crop.js marks it pc-failed). The picture
   takes no clicks, so a circle that opens something still does. */
:has(> img.person-photo) { position: relative; overflow: hidden; }
/* In the stacks where circles overlap, every circle is positioned, with a photo or without. A
   positioned circle paints after every unpositioned one, so with only the photo circles positioned a
   photo would sit on top of the letters circle after it; this way each circle still covers the one
   before it, in order, as it did before there were photos. */
.pd-collab-stack > *, .te-collab-stack > *, .ds-avatar-stack > *, .pp-collab-stack > * { position: relative; }
img.person-photo {
    position: absolute; inset: 0;
    width: 100%; height: 100%; max-width: none;
    margin: 0; padding: 0; border: 0;
    object-fit: cover; border-radius: inherit;
    display: block;
    pointer-events: none; user-select: none; -webkit-user-select: none;
}
img.person-photo.pc-failed { display: none; }
/* Markdown, not pre-wrapped text: the renderer's <p> and <ul> carry the line structure, and
   pre-wrap on top of them doubled every blank line. */
.cmt-text.cmt-text-md { white-space: normal; line-height: 1.5; }
.cmt-text-md > :first-child { margin-top: 0; }
.cmt-text-md > :last-child { margin-bottom: 0; }
/* A reply's own words sit in blocks of their own between its quotes (CommentReply.Html), so the
   same rule holds inside each block, and no block ends with a paragraph's gap under it. */
.cmt-text-md [data-cmt-own] > :first-child { margin-top: 0; }
.cmt-text-md [data-cmt-own] > :last-child { margin-bottom: 0; }
.cmt-text-md p { margin: 0 0 8px; }
.cmt-text-md ul, .cmt-text-md ol { margin: 0 0 8px; padding-left: 22px; }
.cmt-text-md li { margin: 2px 0; }
.cmt-text-md h1, .cmt-text-md h2, .cmt-text-md h3, .cmt-text-md h4 { font-size: 14px; font-weight: 600; margin: 10px 0 4px; }
.cmt-text-md code { font-size: .92em; background: var(--ds-surface-2); border: 1px solid var(--ds-hairline); border-radius: 4px; padding: 1px 4px; }
.cmt-text-md pre { background: var(--ds-surface-2); border: 1px solid var(--ds-hairline); border-radius: 8px; padding: 8px 10px; overflow-x: auto; font-size: 12.5px; margin: 0 0 8px; }
.cmt-text-md pre code { background: transparent; border: none; padding: 0; }
.cmt-text-md a { color: var(--ds-accent); text-decoration: none; }
/* A short task code in a document that opens its task (MarkdownRenderer.LinkTaskCodes): underlined, so it reads as a link and not as a grey label. */
.cmt-text-md a.task-code-link { text-decoration: underline; text-underline-offset: 2px; }
.cmt-text-md blockquote { margin: 0 0 8px; padding: 2px 10px; border-left: 3px solid var(--ds-hairline-strong); color: var(--ds-text-muted); }
/* A TABLE TOO WIDE FOR ITS CARD SCROLLS SIDEWAYS INSIDE IT, and its words stay whole.
   Both places that show a comment (.cmt-text in the panel, .msg-text in Messages) set
   word-break: break-word, which is right for a long web address in a sentence. Cells inherit it,
   though, and it tells the browser any column can shrink to one letter wide, so a table squeezes
   itself to fit instead of scrolling. Josh's screenshot on 18 Sep ("This looks terrible
   (messages)") had a five-column table cut to "Lan|e", "Effo|rt", "tria|ge", "stron|gest". A cell
   now breaks only between words. What still doesn't fit makes the table wider than the card, and
   the display: block + overflow-x: auto above turns that into a scroll inside the card instead of
   a page that scrolls sideways. */
.cmt-text-md table { border-collapse: collapse; margin: 0 0 8px; max-width: 100%; display: block; overflow-x: auto; }
.cmt-text-md th, .cmt-text-md td { border: 1px solid var(--ds-hairline); padding: 3px 8px; font-size: 13px; word-break: normal; overflow-wrap: normal; }
/* A PICTURE IN A PERSON'S WORDS (2c9ac4a5): pasted into Notes or a comment, drawn where it was put.
   On its own line whatever surrounds it, never wider than the card, and capped in height so one tall
   screenshot does not become the whole thread; a tap opens it full size (js/formatting.js), which
   is what the zoom cursor says. Only this app's own uploads are ever drawn (MarkdownRenderer).
   In Messages a tap opens the task, like the rest of the row, so it is smaller there and says nothing. */
.cmt-text-md img.cmt-pic { display: block; max-width: 100%; max-height: 320px; width: auto; height: auto; margin: 6px 0; border-radius: 8px; border: 1px solid var(--ds-hairline); cursor: zoom-in; }
.msg-text.cmt-text-md img.cmt-pic { max-height: 160px; cursor: inherit; }
.cmt-attachments { margin-top: 8px; }

/* ---- attachments on a comment, and on the one being written ------------------------------------
   ONE LOOK, TWO PLACES. The chips under the composer and the chips on a posted comment are the same
   rules on purpose: what you are about to send should look like what you have already sent, or the
   strip under the box reads as a staging area rather than as the message.

   Chips WRAP rather than scroll. A horizontal scroller inside a panel that is itself inside a
   scrolling thread is two nested gestures on a phone, and the outer one always wins. */
.cmt-attach-strip {
    display: flex; flex-wrap: wrap; gap: 6px;
    margin-top: 8px;
}
.cmt-attach-chip {
    display: inline-flex; align-items: center; gap: 6px;
    max-width: 100%; min-width: 0;
    padding: 3px 4px 3px 8px;
    border: 1px solid var(--ds-hairline); border-radius: 999px;
    background: var(--ds-surface);
    color: var(--ds-text);
    font-size: 12.5px; line-height: 1.5;
    /* A button chip must not be mistaken for the page's own text. */
    text-align: left;
}
/* The posted-comment chip IS the button; a pending one holds two — .cmt-attach-open to look at the
   file and the ✕ to take it off — so on that one the chip is a span and the name is what gets
   pressed. Both carry the same padding on the right when nothing follows the name. */
button.cmt-attach-chip { cursor: pointer; padding-right: 10px; font-family: inherit; }
/* LOOK AT WHAT YOU JUST PASTED (a5ce4fd2). It was an unpressable label, so the only way to check a
   screenshot before sending it was to post the comment and open it from the thread. Borderless and
   background-free: the chip around it is the shape, and a button inside a button-shaped pill
   reading as a second button is worse than a label was. */
.cmt-attach-open {
    display: inline-flex; align-items: center; gap: 6px;
    min-width: 0; max-width: 100%;
    border: 0; background: transparent; padding: 0;
    color: inherit; font: inherit; text-align: left;
    cursor: pointer;
    /* IT FILLS THE CHIP RATHER THAN SITTING INSIDE IT. At its text height this was 19px tall — the
       smallest target in the strip, and half the size of the ✕ 6px to its right, which is the one
       control that throws the upload away. A thumb aiming at the tail of a long filename and
       missing low hit "take this file off the comment" instead. align-self:stretch takes the
       chip's full content height and min-height holds the floor the ✕ below already sets, so the
       two are the same size and the dead band between them is gone. */
    align-self: stretch;
    min-height: 24px;
}
@media (hover: hover) and (pointer: fine) {
    .cmt-attach-open:hover { color: var(--ds-accent); }
}
.cmt-attach-icon { flex: 0 0 auto; }
.cmt-attach-name {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cmt-attach-x {
    flex: 0 0 auto;
    border: none; background: transparent; color: var(--ds-text-muted);
    cursor: pointer; font-size: 12px; line-height: 1;
    /* 24px is the smallest thing a thumb can find, and these sit shoulder to shoulder. */
    width: 24px; height: 24px; border-radius: 999px;
    display: grid; place-items: center; padding: 0;
}
@media (hover: hover) and (pointer: fine) {
    button.cmt-attach-chip:hover { border-color: var(--ds-accent); color: var(--ds-accent); }
    .cmt-attach-x:hover { background: var(--ds-danger-bg, var(--ds-surface-2)); color: var(--ds-danger, #ef4444); }
}
.cmt-attach-chip:focus-within { border-color: var(--ds-accent); }

/* The upload's progress line and its refusal. Under the box, where the paste happened — not in the
   panel footer, which is about SAVING and is a different question. */
.cmt-attach-note { margin-top: 6px; font-size: 12px; color: var(--ds-text-muted); }
.cmt-attach-error { margin-top: 6px; font-size: 12px; color: var(--ds-danger, #ef4444); }
/* The comment box's line keeps each thing that went wrong on its own line, with a ✕ to put it away
   (24 Sep, CommentUploadLines). The ✕ is the chips' 24px round button. */
.cmt-composer .cmt-attach-error { display: flex; align-items: flex-start; gap: 6px; }
.cmt-attach-error-text { flex: 1; min-width: 0; white-space: pre-line; overflow-wrap: anywhere; padding-top: 4px; }
.cmt-attach-error-x {
    flex: 0 0 auto;
    border: none; background: transparent; color: inherit;
    cursor: pointer; font-size: 12px; line-height: 1;
    width: 24px; height: 24px; border-radius: 999px;
    display: grid; place-items: center; padding: 0;
}
@media (hover: hover) and (pointer: fine) {
    .cmt-attach-error-x:hover { background: var(--ds-danger-bg, var(--ds-surface-2)); }
}

/* THE FILE INPUT IS NEVER TAKEN OUT OF THE DOM — see the note in the markup and in
   AttachmentUploader. It is hidden the way an accessible control is hidden (clipped, not
   display:none) so it keeps a focus ring of its own and the <label> can still open the picker. */
.cmt-file-input {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
label.cmt-attach-btn { cursor: pointer; }
label.cmt-attach-btn.is-busy { cursor: progress; opacity: 0.5; }
/* Keyboard: the input is what focus lands on, so the ring has to be drawn on the label beside it. */
.cmt-file-input:focus-visible + label.cmt-attach-btn {
    outline: 2px solid var(--ds-accent); outline-offset: 2px;
}
.te-history-via.te-history-via-agent {
    color: var(--ds-success); background: var(--ds-success-bg);
    text-transform: none; letter-spacing: 0;
}
.te-history-link {
    background: transparent; border: none; padding: 0; margin: 0;
    color: var(--ds-accent); cursor: pointer; font: inherit;
}
/* "Deleted by Josh" / "Restored by Joel". The verb carries the weight the name carries on every
   other line, because on these two the verb is the news. */
.te-history-life { font-weight: 600; color: var(--ds-text); }
[data-bs-theme="light"] .te-comment-agent .cmt-avatar.te-comment-agent-avatar,
[data-bs-theme="light"] .mention-item .st-avatar-sm.mention-agent-avatar { border-color: rgba(31,122,60,.20); }
[data-bs-theme="dark"] .cmt-text-md pre,
[data-bs-theme="dark"] .cmt-text-md code { background: rgba(255,255,255,.04); }
@media (hover: hover) and (pointer: fine) {
    .cmt-text-md a:hover { text-decoration: underline; }
    .te-history-link:hover { text-decoration: underline; color: var(--ds-accent-hover); }
}

/* Member row (used in Settings team list) */
.ds-member-row {
    display: flex; align-items: center; gap: 10px;
    padding: 8px 0;
    border-bottom: 1px solid var(--ds-hairline);
}
.ds-member-row:last-child { border-bottom: 0; }
.ds-member-row .ds-avatar { flex: 0 0 28px; width: 28px; height: 28px; font-size: 12px; }

/* Small inline tag (used on Manage task rows) */
.ds-tag {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 2px 8px;
    background: var(--ds-surface-2);
    border: 1px solid var(--ds-hairline);
    border-radius: 999px;
    font-size: 12px; color: var(--ds-text-muted); font-weight: 500;
}
.ds-tag-accent { color: var(--ds-accent); border-color: rgba(47,129,247,.30); background: var(--ds-accent-bg); }
[data-bs-theme="light"] .ds-tag-accent { border-color: rgba(0,102,255,.20); }
.ds-tag-warning { color: var(--ds-warning); border-color: rgba(210,153,34,.30); background: var(--ds-warning-bg); }
.ds-tag-danger { color: var(--ds-danger); border-color: rgba(248,81,73,.30); background: var(--ds-danger-bg); }
.ds-tag-success { color: var(--ds-success); border-color: rgba(63,185,80,.30); background: var(--ds-success-bg); }

/* Segmented toggle (used for day-of-week + form flags) */
.ds-toggle-row { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.ds-toggle {
    padding: 6px 12px;
    border: 1px solid var(--ds-hairline);
    background: var(--ds-surface-2);
    color: var(--ds-text-muted);
    border-radius: 999px;
    font-size: 13px; font-weight: 600;
    cursor: pointer;
    transition: background .12s, color .12s, border-color .12s;
}
@media (hover: hover) and (pointer: fine) {
    .ds-toggle:hover { color: var(--ds-text); }
}
.ds-toggle.active {
    background: var(--ds-accent-bg);
    color: var(--ds-accent);
    border-color: rgba(47,129,247,.40);
}
/* A task panel you can only read (TaskEditor's <fieldset disabled>): its switches do nothing, so
   they look like it. The High priority switch always shows since 46c104a3 (21 Sep), where before a
   read-only viewer never got past the Flags +; dimmed, it still shows whether it is on. Scoped to
   the fieldset so no other page's switches change. */
fieldset:disabled .ds-toggle { opacity: .5; cursor: not-allowed; }
@media (hover: hover) and (pointer: fine) {
    fieldset:disabled .ds-toggle:not(.active):hover { color: var(--ds-text-muted); }
}

/* Manage task row */
.ds-task-row {
    display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
    padding: 14px 18px;
    border-top: 1px solid var(--ds-hairline);
}
@media (hover: hover) and (pointer: fine) {
    .ds-task-row:hover { background: var(--ds-surface-2); }
}
.ds-task-row.editing { background: var(--ds-surface-2); border-radius: 10px; }
.ds-task-row .ds-task-title { font-weight: 600; font-size: 15px; color: var(--ds-text); }
.ds-task-row .ds-task-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

/* The Dashboard's task rows used to be built here: a one-line clipped title, a grey notes line under
   it, and two halves (.ds-row-main, .ds-row-actions) of which only one could shrink. They are the
   project page's row now (49cb51ad), in the task row block at the end of this file, and what they
   needed of their own went with them. */

/* ============================================================
   Layout: page / sidebar / mobile drawer
   ============================================================ */
:root { --ds-sidebar-w: 0px; }

/* 100vh IS A LIE ON IOS, and this is the second place it bit. The drawer learned it first (see the
   sidebar block further down): on iOS Safari 100vh means the viewport with the toolbar HIDDEN, so
   while the toolbar is showing this box is about 110px taller than what you can actually see. The
   scroller inside it therefore ends underneath the toolbar, and the last item on a list cannot be
   reached — "scrolling down the first time lands part of the way and not FULLY at the bottom".
   Scrolling up and back down hides the toolbar, the visible viewport finally matches 100vh, and
   the missing rows appear, which is exactly the workaround that was being used.

   dvh tracks the toolbar as it comes and goes. The plain vh line above it is the fallback for
   anything too old to know dvh, and must stay first. */
.page {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
}
.page main {
    flex: 1;
    min-width: 0;
    overflow-y: auto;
    overflow-x: hidden;
    /* And on a notched phone the last row still sits under the home indicator without this. Same
       line the drawer carries, for the same reason. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* ROOM UNDER THE +, ON EVERY PAGE THAT HAS ONE (358f4a86).
   The + (.td-fab, the PageAddButton component) is fixed to the bottom-right corner of the screen.
   Passing over the page mid-scroll is fine. But at the very bottom it came to rest on whatever was
   last, and there was no more scroll to move that out from under it. Josh's picture: Habits
   scrolled as far as it goes, the + parked on today's square of the last habit, which could not be
   ticked. Only People left room for it (an 84px spacer of its own, now gone in favour of this).

   THE NUMBERS. The + reaches 18px + 52px = 70px up from the bottom of the screen, plus the home
   indicator's inset on an iPhone (about 34px), so about 104px there. What is under the last row at
   the end of the scroll is .content's bottom padding (16px on a phone), this, and main's own
   safe-area padding above: 16 + 84 + 34 = 134px on an iPhone, 32 + 84 = 116px on a desktop. Both
   clear the + with a finger's width to spare. TaskRPG.Tests/RoomUnderThePlusTests.cs does this
   sum from the real rules, so changing the + or this number without the other fails there.

   ::after ON main, NOT PADDING ON A PAGE'S OWN LIST. It has to be the true bottom of the scroll
   whatever the page draws last. People learned that: padding on its grid opened a hole in the
   middle of the page whenever the detail panel was showing under it.

   :has(.td-fab), so the room appears only where the + actually rendered. A page with nothing to
   offer right now (Inbox with no mailbox, a group's conversation) draws no button and gets no
   extra scroll.

   NOT GROUPS. That page is a fixed-height box with its own scrolling list inside it, so room here
   would only add scroll to the page around the box and leave the list's last row under the +.
   Groups gives its list the same --td-fab-room instead. */
:root { --td-fab-room: 84px; }
.page main:has(.td-fab):not(:has(.grp-shell))::after {
    content: "";
    display: block;
    height: var(--td-fab-room);
}

.page > .topbar { display: none; }
/* The topbar brand is an anchor now (it goes to the Dashboard, see MainLayout.razor), so it needs
   telling not to look like one. The phone block below still hides it; this only says what it is
   when it is shown. */
.page > .topbar a.topbar-brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; color: inherit; text-decoration: none; }

@media (min-width: 641px) {
    /* Width of the in-flow sidebar, published as a variable so anything that has to escape the
       flex flow (a position:fixed full-page view, a floating action bar) can start where the
       content area starts instead of underneath the nav. The sidebar itself reads it too, so the
       two can't drift apart. Stays 0 on mobile, where the sidebar is an off-canvas drawer and
       full-bleed is correct. */
    :root { --ds-sidebar-w: 240px; }
    .page { flex-direction: row; }
    .page > .sidebar {
        width: var(--ds-sidebar-w);
        height: 100vh;
        position: relative;
        z-index: 1045;
        border-right: 1px solid var(--ds-hairline);
        transition: width .2s ease;
        overflow-y: auto;
        flex-shrink: 0;
    }

    /* Collapsed sidebar (desktop only) */
    html.sidebar-collapsed { --ds-sidebar-w: 64px; }
    html.sidebar-collapsed .page > .sidebar .ds-nav { padding: 20px 8px; }
    html.sidebar-collapsed .page > .sidebar .ds-brand { padding: 6px 0 18px; justify-content: center; }
    html.sidebar-collapsed .page > .sidebar .ds-brand-name { display: none; }
    html.sidebar-collapsed .page > .sidebar .ds-collapse-btn { transform: rotate(180deg); }
    html.sidebar-collapsed .page > .sidebar .ds-nav-group { display: none; }
    html.sidebar-collapsed .page > .sidebar a.ds-nav-item,
    html.sidebar-collapsed .page > .sidebar .ds-nav-item {
        justify-content: center !important;
        padding: 10px 0 !important;
        border-left: 0 !important;
        border-radius: 8px !important;
        gap: 0 !important;
    }
    html.sidebar-collapsed .page > .sidebar a.ds-nav-item.active {
        background: var(--ds-accent-bg) !important;
        color: var(--ds-accent) !important;
    }
    /* Hide nav item labels when collapsed */
    html.sidebar-collapsed .page > .sidebar .ds-nav-label { display: none; }
    /* Reposition badges as a small dot when collapsed */
    html.sidebar-collapsed .page > .sidebar .ds-nav-badge {
        position: absolute;
        top: 4px; right: 8px;
        margin-left: 0;
        min-width: 8px; height: 8px;
        padding: 0;
        font-size: 0;
        line-height: 0;
        border-radius: 999px;
    }
    html.sidebar-collapsed .page > .sidebar a.ds-nav-item { position: relative; }

    /* User switcher: shrink to avatar only when collapsed */
    html.sidebar-collapsed .page > .sidebar .ds-user-switcher { padding-top: 10px; }
    html.sidebar-collapsed .page > .sidebar .ds-user-button {
        justify-content: center;
        padding: 6px;
        gap: 0;
    }
    html.sidebar-collapsed .page > .sidebar .ds-user-button .ds-user-info,
    html.sidebar-collapsed .page > .sidebar .ds-user-button .ds-user-chev { display: none; }
}

/* Collaborators "+" toggle button — circular, expands list of potential collaborators */
.ds-collab-toggle {
    width: 22px;
    height: 22px;
    border-radius: 999px;
    border: 1px solid var(--ds-hairline);
    background: var(--ds-surface-2);
    color: var(--ds-text-muted);
    display: inline-grid;
    place-items: center;
    cursor: pointer;
    padding: 0;
    transition: background .12s ease, color .12s ease, transform .18s ease, border-color .12s ease;
}
@media (hover: hover) and (pointer: fine) {
    .ds-collab-toggle:hover {
        background: var(--ds-accent-bg);
        color: var(--ds-accent);
        border-color: var(--ds-accent);
    }
}
.ds-collab-toggle:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }
.ds-collab-toggle.open {
    transform: rotate(45deg);
    background: var(--ds-accent-bg);
    color: var(--ds-accent);
    border-color: var(--ds-accent);
}

/* Collapse-toggle button (desktop only) */
.ds-collapse-btn {
    margin-left: auto;
    width: 26px; height: 26px;
    display: inline-grid;
    place-items: center;
    background: transparent;
    border: 1px solid var(--ds-hairline);
    border-radius: 6px;
    color: var(--ds-text-muted);
    cursor: pointer;
    transition: background .12s ease, color .12s ease, transform .2s ease;
    padding: 0;
}
@media (hover: hover) and (pointer: fine) {
    .ds-collapse-btn:hover { background: var(--ds-surface-2); color: var(--ds-text); }
}
.ds-collapse-btn:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }
@media (max-width: 640px) {
    /* Mobile uses the hamburger drawer; hide the desktop collapse button */
    .ds-collapse-btn { display: none; }
}

/* BACK AND FORWARD AT THE TOP OF THE SIDEBAR (NavMenu.razor).
   The installed Mac app is a WebView with no browser toolbar, so these are the only ‹ and › there
   are — see the header of js/history-nav.js for the whole story, including why neither is ever
   dimmed. Filed here rather than with the other html.sidebar-collapsed rules above so that this
   control is described in ONE place, the way .ds-collapse-btn immediately above it is.
   Two buttons sharing the row equally: on a desktop there is width to spare, and a pair of wide
   targets is easier to hit than a pair of 26px squares. */
.ds-histnav { display: flex; gap: 6px; padding: 0 12px 12px; }
.ds-histnav-btn {
    flex: 1 1 0;
    height: 30px;
    display: inline-grid;
    place-items: center;
    background: transparent;
    border: 1px solid var(--ds-hairline);
    border-radius: 8px;
    color: var(--ds-text-muted);
    cursor: pointer;
    padding: 0;
    transition: background .12s ease, color .12s ease;
}
@media (hover: hover) and (pointer: fine) {
    .ds-histnav-btn:hover { background: var(--ds-surface-2); color: var(--ds-text); }
}
.ds-histnav-btn:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }
@media (min-width: 641px) {
    /* The collapsed rail is 64px with 8px of padding each side, which two buttons will not fit
       across. They stack rather than disappear: the point of the pair is that it is always
       reachable, and a collapsed rail is still a rail somebody navigates from. */
    html.sidebar-collapsed .page > .sidebar .ds-histnav {
        flex-direction: column;
        gap: 4px;
        padding: 0 8px 12px;
    }
}
@media (max-width: 640px) {
    /* The phone has the pair in the topbar instead (MainLayout.razor). Here the sidebar IS the
       drawer, and a press inside an open drawer would change the page behind it and leave the
       drawer sitting over the result — the drawer only closes itself on a link click. */
    .ds-histnav { display: none; }
}

@media (max-width: 640px) {
    /* Shrink the mobile topbar — brand is hidden (see below), so only the hamburger needs
       to fit. The spacer below auto-tracks via --topbar-h, redeclared for this breakpoint. */
    :root {
        --topbar-h: calc(44px + env(safe-area-inset-top, 0px));
    }
    .page > .topbar {
        display: flex;
        align-items: center;
        gap: 12px;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 100;
        background: var(--ds-surface);
        border-bottom: 1px solid var(--ds-hairline);
        padding: calc(6px + env(safe-area-inset-top, 0px)) 12px 6px;
    }
    .topbar-spacer {
        height: var(--topbar-h);
        flex-shrink: 0;
    }
    .page > .topbar .hamburger {
        background: transparent;
        border: 1px solid var(--ds-hairline);
        color: var(--ds-text);
        padding: 6px 8px;
        border-radius: 8px;
        cursor: pointer;
        display: inline-grid;
        place-items: center;
        line-height: 0;
    }
    @media (hover: hover) and (pointer: fine) {
        .page > .topbar .hamburger:hover { background: var(--ds-surface-2); }
    }
    /* Back and forward, beside the hamburger. The home-screen app has no browser toolbar at all,
       so this strip is the only place they can be on a phone — MainLayout.razor and the header of
       js/history-nav.js carry the reasoning. Their own flex row with a 4px gap so the pair reads as
       one control, rather than being spread by the topbar's 12px gap.
       Same box as the hamburger deliberately: three buttons in one strip should be one shape. */
    .page > .topbar .topbar-histnav { display: flex; align-items: center; gap: 4px; }
    .page > .topbar .topbar-histbtn {
        background: transparent;
        border: 1px solid var(--ds-hairline);
        color: var(--ds-text);
        padding: 6px 8px;
        border-radius: 8px;
        cursor: pointer;
        display: inline-grid;
        place-items: center;
        line-height: 0;
    }
    @media (hover: hover) and (pointer: fine) {
        .page > .topbar .topbar-histbtn:hover { background: var(--ds-surface-2); }
    }
    .page > .topbar .topbar-histbtn:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }
    /* Brand hidden on phones to reclaim the top strip — only the hamburger remains, and
       each page's own heading (e.g. Dashboard h1) sits immediately below. */
    .page > .topbar .topbar-brand { display: none; }
    .page > .topbar .topbar-brand-mark {
        width: 26px; height: 26px;
        background: linear-gradient(135deg, var(--ds-accent), #8b5cf6);
        border-radius: 7px;
        display: grid; place-items: center;
        color: #fff; font-weight: 800; font-size: 13px;
    }

    /* THE DRAWER HAS TO SCROLL, AND 100vh IS A LIE ON IOS.
       Two separate faults put Sign out permanently out of reach on an iPhone. overflow-y is
       declared only in the desktop block above, so the drawer had no scrolling at all down here;
       and 100vh on iOS Safari means the viewport with the toolbar HIDDEN, so the last ~110px of a
       full-height drawer sits behind the address bar with no way to reach it. The footer is pinned
       with margin-top:auto, which put Sign out squarely in that dead zone.
       dvh tracks the toolbar as it comes and goes; the plain vh line above it is the fallback for
       anything too old to know dvh. */
    .page > .sidebar {
        position: fixed !important;
        top: 0; left: 0;
        width: 260px;
        height: 100vh;
        height: 100dvh;
        overflow-y: auto;
        /* Scrolling to the end of the drawer must not start scrolling the page underneath it. */
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
        padding-top: env(safe-area-inset-top, 0px);
        padding-bottom: env(safe-area-inset-bottom, 0px);
        transform: translateX(-100%);
        transition: transform .22s ease;
        z-index: 1100;
        box-shadow: 4px 0 12px rgba(0,0,0,0.25);
        border-right: 1px solid var(--ds-hairline);
    }
    /* Fills the drawer when the list is short, so Sign out still sits at the bottom — and grows
       past it when the list is long, so there is something to scroll to. height:100% did only the
       first of those and clipped the rest. */
    .page > .sidebar .ds-nav { height: auto; min-height: 100%; }
    html.drawer-open .page > .sidebar {
        transform: translateX(0);
    }
    .page > .drawer-backdrop {
        display: none;
        position: fixed;
        inset: 0;
        background: rgba(0,0,0,0.45);
        z-index: 1090;
    }
    html.drawer-open .page > .drawer-backdrop {
        display: block;
    }
}

/* ============================================================
   THE TOP LAYER — where a panel a page opens for itself lives
   ============================================================
   74e15914. Every page renders inside `.page main`, and that is the app's scroller (see the .page
   block above). So a panel a PAGE opens is a fixed-position element inside a scrolling box — which
   on a Mac is invisible, because position:fixed there still means the viewport, and in Safari on an
   iPhone is the bug: a fixed element inside a scrolling container is treated as part of it, so a
   sheet told "cover the screen" lands at the BOTTOM OF THE LIST. All three of Josh's pictures show
   that — the filter pills and the header fully visible above a sheet covering only the task list.

   <TopLayer /> (Components/Shared) wraps each of those panels in a real modal <dialog> and
   js/top-layer.js calls showModal on it. The top layer is outside the page's paint order entirely,
   so no ancestor's overflow, transform, contain or z-index reaches it: the panel covers the header
   and the filter row, the page behind goes inert, and the browser keeps it there.

   THE DIALOG IS A PANE OF GLASS, nothing more. Every rule here undoes a UA default; the panels
   inside are untouched, and each still carries its own scrim, its own click-outside and its own ✕.
   ============================================================ */
dialog.td-top {
    /* The UA gives a dialog fit-content sizing, a margin:auto, a border, 1em of padding, a Canvas
       background and a max-width/height of the viewport less 6px. All of it has to go: what the
       panels inside expect is a full-bleed box at the viewport's own size, which is what they had
       as ordinary fixed overlays. width/height stay auto so `inset` alone decides the box — a
       percentage here would raise the "100% of what" question that dvh exists to answer. */
    position: fixed;
    inset: 0;
    width: auto;
    height: auto;
    max-width: none;
    max-height: none;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    /* The panels inside do their own scrolling (a sheet caps itself and scrolls its body). */
    overflow: visible;
    /* Reaching the end of a scrolling sheet must not start scrolling whatever is behind it. */
    overscroll-behavior: contain;
}
/* NO SECOND DIM. Every panel already paints its own scrim — var(--ds-scrim), or an rgba of its
   own — so the browser's backdrop stays out of the way rather than darkening the page twice. */
dialog.td-top::backdrop { background: transparent; }

/* ONE CONSEQUENCE, AND IT IS WORTH KNOWING BEFORE YOU LOOK FOR THE BUG. Nothing outside the top
   layer can paint above something inside it, whatever its z-index — that is what makes the fix a
   fix. So the app's own floating surfaces (the undo toast at 1200, the habit card at 1150, the
   install prompt at 1160) now sit BEHIND a page's open panel rather than over it. For the panels
   that were already above them — every sheet at 1250, the picture viewer at 2000 — nothing changed.
   For the four that were below (the Inbox composer and the Calendar event panel at 1100, the
   project assistant at 1040, the two report modals at 1000) a toast raised while one is open is now
   dimmed behind it.

   The one case that was ever load-bearing is already handled and was handled BEFORE this: the
   Inbox's closeout defers a habit celebration and says it once the screen is gone
   (DrainOwedCelebrations, and the note above it explains why raising the card instead would be
   worse). If another one turns up, that is the pattern — hold the sentence, not the write. */

/* THE PAGE BEHIND HOLDS STILL, AND KEEPS ITS PLACE. js/top-layer.js counts the open panels and puts
   this class on <html> while there is at least one.

   overflow:hidden ON THE SCROLLER, and not a position:fixed body. The body trick is the usual one
   and it is the one that loses your place: taking the scroller out of flow resets the offset, so
   closing a sheet drops you at the top of a 94-row board. overflow:hidden leaves scrollTop exactly
   where it was — the page simply stops responding to a finger — which is what `keep-your-place`
   means here as much as it does for a reload.

   EVERY SCROLLER THE APP OWNS, not just `.page main`. Two of the panels that moved are not drawn
   over the page's list at all: the `+ Add to…` sheet, the closeout, the composer and the mail
   attachment viewer all sit over `.td-mail-reader`, which is its own fixed, own-scrolling surface;
   and the draft-picture viewer (a5ce4fd2) sits over `.task-panel-body`. Neither is a descendant of
   `.page main`, so the one rule above left the thing actually under the panel free to be dragged.
   The dialog's own `overscroll-behavior: contain` does not help either: a top-layer element's
   scroll chain goes to the viewport, so neither box is ever in it.

   Kept to the surfaces the APP draws. A scroller inside a panel (a sheet's own body, the message
   HTML's sideways box) has to keep working while the panel is up, which is why this is a list and
   not `* { overflow: hidden }`. */
html.td-top-open .page main,
html.td-top-open .td-mail-reader,
html.td-top-open .task-panel-body { overflow: hidden; }

/* THE ONE WAY A PANEL COULD GO MISSING, closed off. A dialog with no [open] draws nothing, so if
   js/top-layer.js never loaded, every one of these surfaces would be invisible — worse than the bug
   above, because at least the bottom of the list is somewhere. The script adds .td-top-ready to
   <html> the moment it runs; until then a panel that WANTS to be open is drawn the old way, as an
   ordinary overlay inside the page. */
html:not(.td-top-ready) dialog.td-top.td-top-want { display: block; }

/* ============================================================
   Mobile responsive baseline
   ============================================================ */
/* Prevent accidental horizontal scroll caused by overflowing children */
html, body { overflow-x: hidden; }

@media (max-width: 768px) {
    /* Touch-target floor on buttons. Doesn't affect tightly-packed icon-only
       buttons explicitly sized via inline style — only buttons that don't
       opt out via their own dimensions. */
    .btn, .ds-btn {
        min-height: 38px;
    }
    /* Bootstrap btn-sm is 31px by default — bumping to 34px for a friendlier
       tap target while keeping it visually a "small" button. */
    .btn-sm { min-height: 34px; }

    /* Forms grow to full width by default and gain a bit of breathing room */
    .form-control, .form-select { font-size: 15px; }

    /* Modals shouldn't bleed past the viewport on tiny screens */
    .modal-dialog { margin: 0.5rem; max-width: calc(100vw - 1rem); }
}


/* ============================================================
   Login page
   ============================================================ */
.login-shell {
    min-height: 100vh;
    display: grid;
    place-items: center;
    background: var(--ds-bg);
    padding: 24px;
}
.login-card {
    width: 100%;
    max-width: 400px;
    background: var(--ds-surface);
    border: 1px solid var(--ds-hairline);
    border-radius: 14px;
    padding: 32px 28px;
    box-shadow: var(--ds-shadow-2);
}
.login-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 18px;
    font-weight: 700;
    font-size: 16px;
    color: var(--ds-text);
}
.login-mark {
    width: 28px; height: 28px;
    background: linear-gradient(135deg, var(--ds-accent), #8b5cf6);
    border-radius: 8px;
    display: grid; place-items: center;
    color: #fff; font-weight: 800; font-size: 14px;
}
.login-title {
    margin: 0 0 4px;
    font-size: 22px;
    font-weight: 600;
    color: var(--ds-text);
    letter-spacing: -0.01em;
}
.login-sub {
    margin: 0 0 24px;
    color: var(--ds-text-muted);
    font-size: 13px;
}
.login-error {
    background: var(--ds-danger-bg);
    color: var(--ds-danger);
    border: 1px solid var(--ds-danger);
    border-radius: 8px;
    padding: 8px 12px;
    margin-bottom: 16px;
    font-size: 13px;
}
/* Not an error and not a caption: something the app got wrong, being owned up to. Reads as
   information rather than alarm — the person has done nothing that warrants a red box. */
.login-notice {
    background: var(--ds-surface-2);
    color: var(--ds-text);
    border: 1px solid var(--ds-hairline);
    border-left: 3px solid var(--ds-accent);
    border-radius: 8px;
    padding: 10px 12px;
    margin-bottom: 16px;
    font-size: 13px;
    line-height: 1.5;
    text-align: left;
}
/* Only the opening phrase is a heading. Scoped to the first child because a later <strong> is
   emphasis inside a sentence — as a block it broke the line and stranded the full stop on its own. */
.login-notice > strong:first-child { display: block; margin-bottom: 2px; }

/* The same idea inside the app shell, for things worth saying once after a redirect. */
.app-notice {
    display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap;
    background: var(--ds-surface-2);
    color: var(--ds-text);
    border: 1px solid var(--ds-hairline);
    border-left: 3px solid var(--ds-accent);
    border-radius: 10px;
    padding: 10px 14px;
    margin: 0 0 14px;
    font-size: 13px;
    line-height: 1.5;
}
.app-notice-dismiss {
    margin-left: auto; border: none; background: transparent; cursor: pointer;
    color: var(--ds-text-faint); font-size: 15px; line-height: 1; padding: 2px 4px;
}

.login-form { display: flex; flex-direction: column; gap: 14px; }
.login-field { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ds-text-muted); }
.login-field input {
    padding: 9px 12px;
    border: 1px solid var(--ds-hairline);
    border-radius: 8px;
    background: var(--ds-surface-2);
    color: var(--ds-text);
    font-size: 14px;
}
.login-field input:focus {
    outline: 2px solid var(--ds-accent);
    outline-offset: 1px;
    background: var(--ds-surface);
}
.login-remember {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    color: var(--ds-text-muted);
    cursor: pointer;
}
.login-submit {
    margin-top: 6px;
    background: var(--ds-accent);
    color: #fff;
    border: 1px solid var(--ds-accent);
    border-radius: 999px;
    padding: 10px 16px;
    font-weight: 600;
    font-size: 14px;
    cursor: pointer;
    transition: background .12s ease;
}
@media (hover: hover) and (pointer: fine) {
    .login-submit:hover { background: var(--ds-accent-hover); border-color: var(--ds-accent-hover); }
}

/* ============================================================
   Blazor circuit error banner
   ============================================================
   Hidden by default; Blazor's JS toggles display:block when an
   unhandled circuit exception fires. Without this rule the banner
   would always be visible at the bottom of every page. */
#blazor-error-ui {
    color-scheme: light only;
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
    color: #333;
}
#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

/* ============================================================
   Notification preferences (Settings)
   ============================================================
   A three-column matrix on a desktop and a stacked list on a phone. The columns collapse rather
   than scroll because there are only three of them and a horizontally scrolling settings form is
   a settings form people give up on. */
.td-notif { display: flex; flex-direction: column; gap: 16px; margin-top: 4px; }

.td-notif-panel {
    display: flex; flex-direction: column; gap: 10px; align-items: flex-start;
    background: var(--ds-surface-2);
    border: 1px solid var(--ds-hairline);
    border-radius: 10px;
    padding: 14px 16px;
}

.td-notif-panel-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 14px; width: 100%; flex-wrap: wrap;
}

.td-notif-label { font-size: .9rem; font-weight: 600; color: var(--ds-text); }
.td-notif-hint { color: var(--ds-text-muted); font-size: .78rem; line-height: 1.45; margin-top: 2px; }

.td-notif-note {
    background: var(--ds-accent-bg);
    border: 1px solid var(--ds-hairline);
    border-radius: 8px;
    padding: 9px 12px;
    font-size: .8rem; line-height: 1.5;
    color: var(--ds-text);
}

.td-notif-grid {
    border: 1px solid var(--ds-hairline);
    border-radius: 10px;
    overflow: hidden;
}

.td-notif-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 72px 72px 72px;
    align-items: center;
    gap: 8px;
    padding: 11px 14px;
    border-top: 1px solid var(--ds-hairline);
}

.td-notif-row:first-child { border-top: none; }

.td-notif-head {
    background: var(--ds-surface-2);
    font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
    color: var(--ds-text-muted);
    padding-top: 8px; padding-bottom: 8px;
}

.td-notif-col { text-align: center; }
.td-notif-col input[type="checkbox"] { width: 17px; height: 17px; cursor: pointer; }
.td-notif-col input[type="checkbox"]:disabled { cursor: not-allowed; opacity: .35; }
.td-notif-col-empty { color: var(--ds-text-faint); font-size: .8rem; }
.td-notif-locked { color: var(--ds-text-faint); font-size: .72rem; }

@media (max-width: 640px) {
    /* Stack: the row label goes full width and the three switches sit under it with their own
       captions, since a 72px column with no header is unreadable once the header row is gone. */
    .td-notif-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .td-notif-row > :first-child { grid-column: 1 / -1; }
    .td-notif-head { display: none; }
    .td-notif-col::before {
        content: attr(data-label);
        display: block;
        font-size: .68rem; text-transform: uppercase; letter-spacing: .04em;
        color: var(--ds-text-muted);
        margin-bottom: 4px;
    }
}

/* ---------------------------------------------------------------- refused-write banner

   ONE page-level slot for "the ownership rule said no", for the pages that had no error
   affordance of any kind before the gate landed (Manage, Habits, Sticky notes). Every other page
   already owned somewhere to put a message and uses that instead — this is not a fourth style of
   error box, it is the same visual language as Home's .dashboard-load-error, in one place rather
   than pasted into three .razor files.

   The text inside is OwnershipDeniedException.Message verbatim: plain prose, already written for
   the person reading it ("Only the person who created this task can delete it."). */
.gate-error {
    display: flex; align-items: flex-start; gap: 8px;
    font-size: 13px;
    color: var(--ds-danger);
    background: var(--ds-danger-bg);
    border: 1px solid var(--ds-danger);
    border-radius: 8px;
    padding: 8px 12px;
    margin-bottom: 12px;
}
.gate-error button {
    margin-left: auto;
    background: none; border: 0; padding: 0 2px;
    color: inherit; cursor: pointer; font-size: 15px; line-height: 1;
}

/* ---------------------------------------------------------------- a ticked task leaving a list

   THE COMPLETION ANIMATION, in one place. A task you tick is crossed out, holds there long enough
   to be read, and then gets out of the way. It was written for the project board and the owner
   asked for it everywhere ("I just did this on dashboard for one of my tasks and fully expected
   the sick animation"). It lives here rather than in four page <style> blocks because it is one
   760ms three-stage animation with a matching C# constant, and four copies of that is four things
   that drift apart.

   Three stages, and the first one is the whole point: for the first ~360ms the row sits at FULL
   OPACITY while the check draws itself in and the line lands on the title, so what you see is a
   task being completed. Only then does it fade, and only then does its box close. Opacity goes
   BEFORE height, which is what stops the rows below snapping upward.

   It used to be one 240ms animation that reached opacity 0 at the 55% stop — 132ms — under an
   ease-out that spends most of its distance in the first third of that. The strike-through was
   therefore on screen for a handful of frames at best, so the row read as vanishing rather than
   completing. The hold went from 260ms to 360ms after he watched it: "maybe a tiny bit fast if I
   am being picky". The hold IS the effect, so that is the number that grows, not the fade or the
   close.

   A page opts a row in with three classes:
     .ds-leaving       on the row, for as long as the page holds it in the list (RowLeaveHold)
     .ds-leave-strike  on the element carrying the line-through — the title
     .ds-leave-check   on the tick control, whose <svg><polyline> is the check that draws
   The two inner ones do nothing on their own: everything is scoped to .ds-leaving, so a row that
   is merely already done — a "show completed" view — never re-animates on an ordinary render.
   They are therefore safe to write unconditionally into a row's markup.

   The 760ms below is RowLeaveHold.DurationMs. Change one and change the other, or the row is
   dropped from the list mid-animation and there is nothing to watch. RowLeaveHoldTests reads this
   file and fails if the two numbers ever part company.

   --ds-leave-max is a CEILING on the row's height, not the row's height. An explicit max-height at
   the 0% stop overrides the natural height outright, so a value under one row's height clips the
   row and the rows below jump up on the first frame — the exact snap this animation exists to
   avoid. The default clears a four-line project row on a 390px viewport; A LIST WITH TALLER ROWS
   MUST RAISE IT on its own rows (the dashboard does). The only cost of raising it is that the
   visible close starts a little later inside the last stage, which the easing hides.

   One trap if you use this on a row built out of Bootstrap utilities: an author !important beats
   an animation in the cascade, so a row wearing .py-3 or .opacity-75 will keep that padding or
   that opacity right through the animation and then snap. Give the row plain classes instead.
   .text-decoration-line-through is the same trap in disguise and it caught the dashboard: it is a
   SHORTHAND, so its !important also lands on text-decoration-color, and .ds-leave-strike below
   could never fade the line in — the line was stamped on at full strength on frame 0. Strike a
   leaving title with a plain `text-decoration-line: line-through` of your own instead.

   Second trap, about the space AROUND the row: a list that separates its rows with `gap` keeps
   that gap when a row collapses, because the gap belongs to the list and nothing in the row's
   own animation can reach it. Space such rows with margin-bottom instead — the 100% stop zeroes
   it, so the last few pixels close with the row rather than snapping shut after it. */
:root { --ds-leave-max: 320px; }

.ds-leaving {
    pointer-events: none;
    overflow: hidden;
    animation: ds-row-leave 760ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes ds-row-leave {
    0%   { opacity: 1; max-height: var(--ds-leave-max, 320px); }
    47%  { opacity: 1; max-height: var(--ds-leave-max, 320px); }  /* 357ms: hold, read the tick and the line */
    71%  { opacity: 0; max-height: var(--ds-leave-max, 320px); }  /* 182ms: fade, height untouched */
    /* Every way a collapsed row can still be taking up space. Both borders, not just the bottom
       one: the project board's rows are bottom-bordered, but the group task list boxes each row
       on all four sides and its 1px top edge was surviving the close. */
    100% { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; border-top-width: 0; border-bottom-width: 0; margin-bottom: 0; }
}

/* The line is drawn while the row holds instead of being stamped on with the class.
   text-decoration itself cannot be animated, but its colour can, and fading the colour up keeps
   the line on the glyphs. A background-gradient strike — the usual trick for a left-to-right draw
   — would run the full width of the grid cell rather than the text, and on the project board that
   put a line through the rename pencil, which lives inside the same span. */
.ds-leaving .ds-leave-strike {
    animation: ds-strike-on 200ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes ds-strike-on {
    from { text-decoration-color: transparent; }
    to   { text-decoration-color: currentColor; }
}

/* The check draws itself in and the circle gives once. The dash length is the polyline's own:
   20,6 to 9,17 to 4,12 measures just under 23 units, and 26 covers it with no gap left at the end.
   Every tick control in the app draws that same polyline, which is why one number does. */
.ds-leaving .ds-leave-check {
    animation: ds-check-pop 300ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes ds-check-pop {
    0%   { transform: scale(1); }
    45%  { transform: scale(1.18); }
    100% { transform: scale(1); }
}
.ds-leaving .ds-leave-check svg polyline {
    stroke-dasharray: 26;
    stroke-dashoffset: 26;
    animation: ds-check-draw 200ms cubic-bezier(0.4, 0, 0.2, 1) 60ms forwards;
}
@keyframes ds-check-draw {
    to { stroke-dashoffset: 0; }
}

/* No motion: the row still leaves, it just goes at once instead of over 760ms. It needs the CLOSED
   state, not merely opacity — a transparent row still occupies its full height, which left a blank
   gap in the middle of the list for the length of the hold and then snapped shut. display:none is
   what says "closed" here rather than a pile of zeroed box properties, and it is !important for
   the same reason the note above warns about: the dashboard's rows wear Bootstrap's .d-flex, whose
   own !important would otherwise keep an invisible row taking up its full height. The page holds
   the row for the same 760ms either way, so what is hidden and what is counted stay identical. */
@media (prefers-reduced-motion: reduce) {
    .ds-leaving { animation: none; display: none !important; }
    .ds-leaving .ds-leave-strike,
    .ds-leaving .ds-leave-check,
    .ds-leaving .ds-leave-check svg polyline { animation: none; }
    .ds-leaving .ds-leave-check svg polyline { stroke-dashoffset: 0; }
}

/* ================================================================================================
   THE ATTACHMENT VIEWER — moved here from Inbox.razor's own <style> block.

   A <style> inside a .razor is rendered when THAT component renders, so every rule below existed
   only while the mail reader was on screen. The task panel's comment thread now opens the same
   overlay for a comment's attachment, and a second copy of a 170-line viewer is two viewers that
   drift — so there is one copy, on the app's own stylesheet, and js/attachment-viewer.js (already
   loaded on every page from App.razor) drives whichever one is up.

   NOTHING IN THESE RULES CHANGED IN THE MOVE. Order is not load-bearing either: every rule that
   could collide with a page's own (.td-nav-btn, .td-attach-saving, .ds-btn) is written here at a
   higher specificity than the page rule it overrides, so it wins wherever it is loaded from.
   ============================================================================================== */
/* ---- the attachment viewer ---------------------------------------------------------------
   Tap an attachment and it opens here, over the message, with the arrows to its neighbours and
   Save in reach. Above everything else on the page (the reader is 1000, the compose overlay
   1100) because it is the only surface that can be opened over either of them — and at or above
   1000 is also what makes js/swipe-nav.js stand its drawer gesture down over this, which is how
   the viewer's own swipe and the nav drawer's manage to coexist without either knowing about
   the other. */
.td-av {
    position: fixed; inset: 0; z-index: 2000;
    display: flex;
    overflow: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
    /* BOTH viewers take focus when they open and on every step, so the arrow keys have somewhere
       to land that is not the composer behind them (the reply box on the mail page, the comment
       box in the task panel) — js/attachment-viewer.js stands its keys down for a focused text
       field. It is a surface, not a control, and a focus ring around the whole screen says
       nothing. */
    outline: none;
    /* How much of the screen the chrome at each end is holding, in one place: the picture is
       sized against it and the SCROLLER is padded by it, so a picture is never under the Close
       button or behind the arrows. Measured against what those bars actually come out at — 60px
       for the top row of chips and 109px for the caption plus the 54px arrows — rather than
       guessed, with a few pixels of air on each so a longer filename wrapping cannot close the
       gap. */
    --td-av-top: calc(env(safe-area-inset-top, 0px) + 68px);
    --td-av-bottom: calc(env(safe-area-inset-bottom, 0px) + 118px);
    /* THE RESERVE IS HERE, NOT ON THE STAGE. As the stage's padding it made the stage the full
       width and height of the screen with the picture floating in the middle of it — and the
       stage paints above the backdrop and has no click handler, so the black gutter immediately
       above, below and beside a photo was DEAD: tapping there did nothing, while tapping a
       little further out closed. Measured at 500px wide with a fitted photo, all four of "20px
       above", "20px below", "2px in from the left" and "2px in from the right" hit
       .td-av-stage. On the scroller the stage shrink-wraps the picture instead, so everything
       around it is backdrop and "tap outside to close" is true everywhere it looks true. The
       picture lands in exactly the same place either way — same fit, same centring. */
    padding: var(--td-av-top) 14px var(--td-av-bottom);
}
/* Fixed rather than absolute, so it still covers the screen once a zoomed picture has scrolled
   the overlay. Painted behind everything (z-index 0 against the stage's 1), which is what makes
   it the thing you hit when you tap where there is nothing — and nothing else. */
.td-av-backdrop {
    position: fixed; inset: 0; z-index: 0;
    background: rgba(0,0,0,.94);
    cursor: default;
}
/* margin:auto rather than align-items/justify-content:center. Centring a flex item that is
   BIGGER than its container with the alignment properties puts its top-left off the start edge,
   where no scrollbar can reach it — which is exactly the state a zoomed picture is in.
   AND THE SAME RULE APPLIES INSIDE THE STAGE, which is where it was quietly broken. max-width
   below pins the stage to the screen — it has to, or a percentage max-width on the picture has
   no definite basis to fit against — so a zoomed picture overflows the STAGE rather than the
   scroller, and a justify-content:center here re-centred it and pushed half of it off the start
   edge one level down. Measured at 500px wide with a 3000px screenshot: the left 1250px, the
   start of every line of a score, sat at negative x with scrollLeft already at 0. So the
   children carry margin:auto too (see .td-av-shot and .td-av-card) and this line carries no
   justify-content at all: auto margins centre a picture that fits and collapse to 0 against one
   that does not, which leaves it at the scroll origin where panning can reach all of it.
   NOTE the two are not interchangeable — auto margins that have collapsed to 0 still hand the
   line back to justify-content, so leaving `center` here would have undone them. */
.td-av-stage {
    position: relative; z-index: 1; margin: auto;
    max-width: 100%;
    display: flex; align-items: center;
}
/* The waiting line and the picture STACK, in one grid cell, rather than sitting beside each
   other. Side by side, a hint that was still on screen when the picture arrived pushed the
   picture off centre — so the one frame where the two overlap costs a moment of text over the
   middle of a photo instead of a photo visibly in the wrong place, and a `ready` class that
   somehow never arrived degrades to the first rather than the second.
   margin:auto is what centres it in the stage — see the note on .td-av-stage for why the
   alignment properties cannot do that job here. */
.td-av-shot { display: grid; place-items: center; margin: auto; }
.td-av-shot > * { grid-area: 1 / 1; }
.td-av-shot img {
    display: block; margin: auto;
    max-width: 100%;
    max-height: calc(100dvh - var(--td-av-top) - var(--td-av-bottom));
    width: auto; height: auto;
    border-radius: 6px;
    cursor: pointer;
}
/* Actual size. The app's viewport is user-scalable=no, so pinch-to-zoom does not exist inside
   it and this is the only way to read a screenshot of a score. Toggled as a class by
   js/attachment-viewer.js — no round trip, and nothing for a re-render to undo. */
.td-av-zoomed .td-av-shot img { max-width: none; max-height: none; cursor: zoom-out; }

/* Waiting, and not arriving. An attachment is a whole IMAP session behind an HTTP request, so
   there is a real wait to say something about; and a fetch that fails must never leave a broken
   image icon or an empty black rectangle. Switched by the load/error listeners in
   js/attachment-viewer.js — nothing in CSS can ask an image whether it loaded. */
.td-av-hint {
    display: none;
    font-size: 0.9rem; line-height: 1.5; color: rgba(255,255,255,.86);
    text-align: center; padding: 20px 22px; max-width: 34ch;
}
.td-av-hint-wait { display: block; }
.td-av-ready .td-av-hint { display: none; }
.td-av-failed .td-av-hint-wait { display: none; }
.td-av-failed .td-av-hint-failed { display: block; }
.td-av-failed .td-av-shot img { display: none; }

/* Everything that cannot be drawn. Says what it is rather than pretending — see the note in the
   markup, and AttachmentViewing.CanShowInPlace, for why a PDF is on this side of the line. */
.td-av-card {
    background: var(--ds-surface); border: 1px solid var(--ds-hairline);
    border-radius: 14px; padding: 26px 22px; max-width: min(420px, 100%);
    margin: auto;   /* the stage carries no justify-content — see the note on .td-av-stage */
    display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
}
.td-av-card-icon { font-size: 38px; line-height: 1; }
.td-av-card-name { font-weight: 700; font-size: 1rem; overflow-wrap: anywhere; }
.td-av-card-meta { font-size: 0.8rem; color: var(--ds-text-faint); overflow-wrap: anywhere; }
.td-av-card-why { margin: 8px 0 12px; font-size: 0.85rem; color: var(--ds-text-muted); }
.td-av-card-open { font-size: 0.85rem; text-decoration: none; }
/* DONE AND SHARE, ON THE SURFACE A PDF ACTUALLY LANDS ON (5a14bcb7). Josh: "there is no way to
   leave the PDF once there so you have to quit the app 😢", and what he asked for is "what a PDF
   looks like in Safari from IMSLP" — Done in one corner, Share in the other. They wrap rather than
   squeeze: three controls at 0.85rem inside a card capped at 420px do not fit one line on a narrow
   phone, and a Done pushed off the edge is the bug all over again. */
.td-av-card-actions {
    display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
    gap: 8px; width: 100%;
}
.td-av-card-actions .ds-btn { font-size: 0.85rem; }
/* A LINK OUT OF THE APP IS THE DESKTOP'S ANSWER AND ONLY THE DESKTOP'S. In the installed iPhone app
   a same-origin link is handed to a web view with no toolbar of its own, so following it replaces
   the app with the PDF and there is no back — which is the whole of 5a14bcb7. Share, which
   navigates nowhere, is what is offered there instead; the pair are mirror images, and neither
   screen is ever left with no way off it.

   EVERY SUCH LINK IN THE VIEWER, and not just the card's. .td-av-linkout is carried by the two foot
   captions as well — the task panel's own "Open in a new tab" and the Inbox's "Save", whose handler
   deliberately does not preventDefault because the browser following the link IS the download. Both
   were bare anchors with no class at all, so on a phone they were the only stranding paths left on
   screen, pinned in the bottom bar under the thumb. The 9 Sep question (Save, or Open in a new
   tab?) was never answered, so BOTH are closed off rather than one of them guessed at.

   .td-av SCOPES IT ON PURPOSE. The Inbox's Save control is one RenderFragment drawn twice — here
   and in the attachment strip inside the reader — and only the viewer's copy is a screen with
   nothing else on it.

   AND ONLY WHERE THERE IS SOMETHING TO REPLACE IT WITH: html.td-can-share is set by
   js/share-file.js when the platform really has a share sheet. A touch device without one would
   otherwise be left with no way to keep the file at all. */
@media (hover: none) {
    html.td-can-share .td-av .td-av-card-open,
    html.td-can-share .td-av .td-av-linkout { display: none; }
}
/* And the other half of that mirror: the share sheet is the phone's, so it stays out of the way
   where Download and the link out already work. Both branches are written out rather than relying
   on `(hover: none)` alone, which would miss a hover-capable coarse pointer. */
@media (hover: hover) and (pointer: fine) {
    .td-av .td-av-share { display: none; }
}
/* SHOWN ON THE CAPABILITY, NOT ON THE POINTER, and this is what the pointer query alone got wrong.
   `(hover: none)` says "a finger", not "this platform can offer a sheet". Where navigator.share is
   missing — a plain WKWebView shell is the shape MEMORY records for the Mac app — the button's own
   fallback is an <a download> click, i.e. the same same-origin navigation the rules above exist to
   remove: the one control offered to the phone either did nothing or performed the trap.
   js/share-file.js asks __tdShare.can() at load and says so on <html>; until it does, Share is not
   offered and the desktop's own controls stay exactly where they are. */
html:not(.td-can-share) .td-av .td-av-share,
html:not(.td-can-share) .td-av .td-av-card-share { display: none; }

/* THE GREY FILE BAR UNDER A TASK'S ATTACHMENTS, ONE COPY AT A TIME (145d2d9d). AttachmentUploader
   draws the bar twice: a download link (.td-file-link) and a button (.td-file-open) that opens the
   ✓ Done / ⇪ Share card above. Josh, on the iPhone: "Still doesn't give a way back". A plain link
   there hands the file to a screen with nothing on it that leads back into the app.
   Both halves are written out, so exactly one of the two is ever on screen: the link everywhere by
   default, as it was, and the button in its place where there is no hover (a phone or an iPad). A
   touch laptop that reports hover keeps the link. `.btn` beside each class so these win over
   Bootstrap's own `.btn { display: inline-block }` whatever order the files load in. */
.btn.td-file-open { display: none; }
@media (hover: none) {
    .btn.td-file-link { display: none; }
    .btn.td-file-open { display: inline-block; }
}
/* THE CARD'S OWN TEXT COLOUR. The top layer's dialog passes on the colour of whatever the strip
   sits in (`color: inherit`), and the comment card gets away with that because it only ever sits in
   the task panel. This one also sits in group chat, where your own message bubble is white text on
   green: in the light theme the card's "PDF file" came out white on a white card. */
.td-file-card .td-av-card { color: var(--ds-text); }

.td-av-top, .td-av-foot {
    position: fixed; left: 0; right: 0; z-index: 2;
    display: flex; align-items: center; gap: 10px;
    color: #fff;
}
.td-av-top {
    top: 0;
    padding: calc(env(safe-area-inset-top, 0px) + 10px) 14px 14px;
    background: linear-gradient(to bottom, rgba(0,0,0,.78), rgba(0,0,0,0));
}
.td-av-name {
    flex: 1; min-width: 0; font-weight: 600; font-size: 0.9rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.td-av .td-av-chip.ds-btn {
    flex: 0 0 auto; border: 0; border-radius: 999px;
    background: rgba(255,255,255,.94); color: #111;
    font-size: 0.85rem; font-weight: 600; padding: 9px 14px;
}
/* The task panel's Download chip is a link, not a button, so it needs the underline taken off.
   Hidden where there is no hover (phones and tablets): a plain link in the installed iPhone app is
   how a PDF once left Josh with no way back, so the phone gets its own Share button instead. */
.td-av a.td-av-chip.ds-btn { text-decoration: none; }
@media (hover: none) {
    html.td-can-share .td-av .td-av-download { display: none; }
}
/* THE BOTTOM OF AN IPHONE IS NOT FREE. The home indicator lives in the last ~34pt of the
   screen and swallows anything put under it, so the whole foot is inset by the safe area the
   same way .td-selbar and the reader's own padding already are. */
.td-av-foot {
    bottom: 0; flex-direction: column; gap: 10px;
    padding: 16px 14px calc(env(safe-area-inset-bottom, 0px) + 14px);
    background: linear-gradient(to top, rgba(0,0,0,.82), rgba(0,0,0,0));
}
.td-av-cap {
    font-size: 0.78rem; color: rgba(255,255,255,.8);
    display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap; justify-content: center;
    max-width: 100%; overflow-wrap: anywhere;
}
.td-av-cap a { color: #8fc4ff; }
/* ALL THREE SAVE ANSWERS, not just the one that flickers past. The strip's colours are picked
   for the page, and the page is white in the light theme; in here they are sitting on
   rgba(0,0,0,.94). --ds-success is #1f7a3c there, which comes out at 3.6:1 against that
   backdrop, and --ds-danger is #d12a2a at 3.7:1 — both under the 4.5:1 that 12.5px text needs,
   and the two of them are precisely the states the reader is waiting on. Light-on-dark
   variants, the same treatment the Save link itself already gets: 12.1:1 and 9.4:1. */
.td-av-cap .td-attach-saving { color: rgba(255,255,255,.75); }
.td-av-cap .td-attach-saved { color: #7ee39a; }
.td-av-cap .td-attach-savefail { color: #ff9a92; }
/* Real arrows, thumb-sized, on a bar rather than glued to the edges of the glass — the same
   shape as the reader's own ‹ 3 of 12 › counter one layer down, and reachable without
   stretching on a phone. A disabled one is visibly dead rather than silently inert. */
.td-av-nav { display: flex; align-items: center; gap: 14px; }
.td-av-nav .td-nav-btn.ds-btn-icon {
    min-width: 54px; min-height: 54px; font-size: 28px; line-height: 1;
    border-radius: 999px; border: 1px solid rgba(255,255,255,.34);
    background: rgba(255,255,255,.14); color: #fff;
}
.td-av-nav .td-nav-btn.ds-btn-icon:disabled { opacity: .28; cursor: default; }
.td-av-nav .td-nav-count {
    color: #fff; font-weight: 600; font-size: 1rem; min-width: 78px; text-align: center;
}

/* ----- link chips on a task (TaskLinkChip.razor) -------------------------------------------------
   The Links block in the task panel and the "linked PR #319" line in its History draw the same
   chip, so the rules live here rather than in either place. Tone classes are the status pills'
   own words (.td-status-go / -stop / -hold / -neutral) reused deliberately: "merged" and "ready
   to deploy" should not be two different greens on the same screen.

   WRAPS, never scrolls, for the reason the comment attachment strip gives — a horizontal scroller
   inside a scrolling panel is two gestures on a phone and the outer one always wins. */
.te-links { display: flex; flex-wrap: wrap; gap: 6px; }
.te-chip {
    display: inline-flex; align-items: center;
    max-width: 100%; min-width: 0;
    border: 1px solid var(--ds-hairline); border-radius: 999px;
    background: var(--ds-surface);
    font-size: 12.5px; line-height: 1.5;
}
.te-chip-body {
    display: inline-flex; align-items: center; gap: 6px;
    min-width: 0; padding: 3px 10px;
    color: var(--ds-text); text-decoration: none;
}
.te-chip-mark { flex: 0 0 auto; }
/* A Drive chip's mark is Google's own icon for the kind of file (wwwroot/icons/drive/), sized to
   the glyph it replaced so a row of chips with one Sheet in it lines up. The compact size is the
   one the History block uses. object-fit keeps the 96px source from smearing on a non-square
   box; display:block drops the baseline gap an inline image carries under it. */
.te-chip-icon { width: 14px; height: 14px; object-fit: contain; display: block; }
.te-chip-sm .te-chip-icon { width: 12px; height: 12px; }
.te-chip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A BUNDLE'S CHIP SHOWS ITS WHOLE NAME, never "…" (7598793f; the header tag, .te-bundle, has the
   same rule from 1dc0afd8). A bundle is named in a sentence, and the cut took the words that tell
   two bundles apart. A name wider than the Links block wraps inside the chip. Every other chip keeps
   its one line. The mark sits on the first line with the name, as it does in the header tag. The
   corner is 13px, not a full pill: half the height of a one-line chip, so on one line it looks like
   the chips beside it, and on two lines it is a rounded box and not a lozenge. */
.te-chip-bundle { border-radius: 13px; }
.te-chip-bundle .te-chip-body { align-items: baseline; }
.te-chip-bundle .te-chip-label { overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere; }
/* The state rides INSIDE the chip rather than beside it: "#319" and "merged" are one fact, and
   two pills side by side read as two links. */
.te-chip-state {
    flex: 0 0 auto; font-size: 11px; font-weight: 600; text-transform: lowercase;
    padding: 0 6px; border-radius: 999px;
    background: var(--ds-surface-2); color: var(--ds-text-muted);
}
.te-chip-go .te-chip-state      { background: var(--ds-success-bg); color: var(--ds-success); }
.te-chip-stop .te-chip-state    { background: var(--ds-danger-bg);  color: var(--ds-danger); }
.te-chip-hold .te-chip-state    { background: rgba(163,113,247,.14); color: #b48cf5; }
[data-bs-theme="light"] .te-chip-hold .te-chip-state { background: rgba(110,60,200,.10); color: #5b2fb3; }
/* A chip whose stored address today's rules would refuse. Visibly dead rather than silently inert:
   the alternative is a chip that looks like every other one and does nothing when tapped. */
.te-chip-dead { border-style: dashed; opacity: .75; }
.te-chip-dead .te-chip-body { color: var(--ds-text-muted); cursor: default; }
/* Inside a line of log text, where the chip is a detail of the sentence and not the subject of it. */
.te-chip-sm { font-size: 11.5px; vertical-align: baseline; }
.te-chip-sm .te-chip-body { padding: 1px 8px; gap: 4px; }
.te-chip-x {
    flex: 0 0 auto;
    border: none; background: transparent; color: var(--ds-text-muted);
    cursor: pointer; font-size: 14px; line-height: 1;
    /* 24px is the smallest thing a thumb can find, and these sit shoulder to shoulder. */
    width: 24px; height: 24px; border-radius: 999px;
    display: grid; place-items: center; padding: 0;
}
.te-chip:focus-within { border-color: var(--ds-accent); }
@media (hover: hover) and (pointer: fine) {
    a.te-chip-body:hover { text-decoration: underline; }
    .te-chip:has(a.te-chip-body):hover { border-color: var(--ds-accent); }
    .te-chip-x:hover { background: var(--ds-danger-bg, var(--ds-surface-2)); color: var(--ds-danger, #ef4444); }
}
/* The form under the block, and the one sentence that says a chip cannot be renamed. */
.te-link-add { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.te-link-add .te-link-url { flex: 2 1 220px; min-width: 0; }
.te-link-add .te-link-name { flex: 1 1 140px; min-width: 0; }
.te-link-note { margin-top: 6px; font-size: 12px; color: var(--ds-text-muted); }
.te-link-error { margin-top: 6px; font-size: 12px; color: var(--ds-danger, #ef4444); }

/* ----- the Related block in the task panel -------------------------------------------------------
   A relationship is drawn with the same .te-chip as everything else — it IS a TaskLink — so there is
   nothing here for the chips. What is here is the form under them: the four-way kind chooser and the
   by-name picker, which exist nowhere else.

   THE KIND IS A ROW OF BUTTONS, and which one is on is drawn as a class rather than as a value on a
   control. That is the uncontrolled-input rule the whole panel keeps: nothing a render writes can
   land inside an element somebody is typing in.

   NO SCROLLER. The picker shows at most RelatedPickMax rows, so the list is short enough to sit in
   the panel's own scroll — a second scrolling box inside a scrolling panel is two gestures on a
   phone and the outer one always wins (the link chips' own rule, three blocks up). */
.te-related-add { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }
.te-related-add .te-related-find { flex: 1 1 200px; min-width: 0; }
.te-related-kinds { display: flex; flex-wrap: wrap; gap: 4px; }
.te-related-kind {
    border: 1px solid var(--ds-hairline); border-radius: 999px;
    background: var(--ds-surface); color: var(--ds-text-muted);
    font-size: 12.5px; line-height: 1.5; padding: 3px 10px; cursor: pointer;
    /* Shoulder to shoulder, and each one has to be findable by a thumb. */
    min-height: 28px;
}
.te-related-kind-on { border-color: var(--ds-accent); background: var(--ds-surface-2); color: var(--ds-text); }
.te-related-kind:focus-visible { border-color: var(--ds-accent); }
/* The picked task's row: the title takes the room it needs and the project sits at the far end,
   because two items with the same name in two projects are not the same item. */
.te-related-hits {
    margin-top: 6px; padding: 4px;
    display: flex; flex-direction: column; gap: 2px;
    border: 1px solid var(--ds-hairline); border-radius: 10px; background: var(--ds-surface);
}
.te-related-hit {
    display: flex; align-items: baseline; gap: 8px;
    width: 100%; min-width: 0; min-height: 32px; padding: 6px 8px;
    border: none; border-radius: 8px; background: transparent;
    color: var(--ds-text); font-size: 13px; text-align: left; cursor: pointer;
}
.te-related-hit-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.te-related-hit-where { margin-left: auto; flex: 0 0 auto; font-size: 11.5px; color: var(--ds-text-muted); }
.te-related-hit:focus-visible { background: var(--ds-surface-2); }
@media (hover: hover) and (pointer: fine) {
    .te-related-kind:hover { border-color: var(--ds-accent); color: var(--ds-text); }
    .te-related-hit:hover { background: var(--ds-surface-2); }
}

/* ---------------------------------------------------------------------------------------------
   THE SELECTION BAR. One shape, two boards: the inbox picks emails with it, the project page picks
   tasks with it (1ade2d71). It floats over the list rather than sitting above it — picking two rows
   at the bottom of a 94-row board and then scrolling back to the top to act on them is the friction
   these boards exist to remove. Josh, 9 Sep: "'Select all' only living at the top of the screen is
   bad UX… I think we should have a floating modal thing at the bottom that shows the available
   options you can do when in Batch Select."

   It lived in Inbox.razor's <style> until the project board wanted the same bar. A page leaning on
   rules inside another component's render output breaks silently the day that component stops
   rendering there, so the shared shape belongs here — unchanged, geometry and all.

   Fixed to the viewport, so it is in the same place whatever you have scrolled to. Below the
   inbox's reading overlay (z-index 1000), which is opaque. */
.td-selbar {
    position: fixed;
    /* Centred over the content area, not the viewport: with left:50% the sidebar overlapped
       (and painted over) the bar's left end once the window got narrow enough. */
    left: var(--ds-sidebar-w, 0px);
    right: 0;
    width: fit-content;
    margin-inline: auto;
    /* The bottom of an iPhone is not free: the home indicator swallows anything under it. */
    bottom: calc(env(safe-area-inset-bottom, 0px) + 18px);
    transition: left .2s ease;
    z-index: 900;
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    max-width: min(760px, calc(100vw - var(--ds-sidebar-w, 0px) - 32px));
    padding: 10px 14px;
    border: 1px solid var(--ds-accent);
    border-radius: 12px;
    background: var(--ds-surface);
    box-shadow: 0 8px 28px rgba(0,0,0,.22);
}
/* So the bar can never hide the last rows of the list — including the ones you are trying to
   select. Applied only while it is on screen. */
.td-selbar-pad { padding-bottom: 92px; }

/* ================================================================================================
   THE TASK ROW — one row everywhere (49cb51ad).

   Josh, on the Dashboard's lists: they draw the project page's row now, from one piece of code
   (Components/Shared/TaskRow.razor for the cells, TaskRowPanels.razor for the notes and pictures
   under a row). These rules came here from ProjectDetail.razor's own <style> block unchanged, so
   they reach both pages: a page's <style> reaches only that page. The Height of task answers carry
   over with them (1231edb7): every row one height, no size or check chips, no Claude's turn mark.
   What only a project page has — dragging, Select, the fan-out and bundle folder heads, sections,
   the Add line — stays in that page's block.
   ================================================================================================ */
/* THE BOX SHOWS WHAT IS IN IT, up to a cap, and scrolls past that.
   It used to be a flat four rows however long the note was, so anything real showed its first
   three lines and hid the rest with no sign that there was more. Two things size it now: the
   rows attribute the markup computes (NotesBox.RowsFor — an estimate, and the only thing that
   works with no script on the page), and field-sizing, which where supported ignores rows and
   measures the actual text. min/max-height bound both, so the box never opens smaller than it
   used to and never grows past half the screen — beyond that it scrolls. */
/* GROWS AS YOU WRITE — and field-sizing alone was not doing it where it matters.
   field-sizing:content is a Chromium property. This app is used in a Mac app window and an
   installed iPhone PWA, both WebKit, where it does nothing: the box stayed at its minimum and
   a link pasted on a new line sat cut in half at the bottom. It stays, because where it IS
   supported it costs nothing and agrees with the script — but js/autogrow.js (data-autogrow on
   the elements) is what actually makes this work on the two devices this app is used on.

   resize IS GONE, the same decision the composer made: a handle you drag to a height cannot
   coexist with a height the next keystroke rewrites. The box now does by itself the one thing
   dragging it was for.

   The ceiling is these two lines, not a number in the script. */
.pd-notes-input {
    width: 100%;
    box-sizing: border-box;
    min-height: 84px;
    max-height: 46vh;
    overflow-y: auto;
    field-sizing: content;
    resize: none;
    background: var(--ds-surface);
    border: 1px solid var(--ds-hairline);
    border-radius: 8px;
    padding: 8px 10px;
    color: var(--ds-text);
    font-size: 14px;
    font-family: inherit;
    line-height: 1.5;
}
.pd-notes-input:focus { outline: none; border-color: var(--ds-accent); }
.pd-notes-input::placeholder { color: var(--ds-text-faint); }
.pd-notes-hint { font-size: 12px; color: var(--ds-text-faint); margin-top: 6px; }

.pd-rows {
    display: flex;
    flex-direction: column;
    /* A query container, so the two-line rule further down can ask how wide the ROWS are
       rather than how wide the screen is. inline-size only: the height still comes from the
       rows, which the leaving animation and the notes panels depend on. */
    container: pd-rows / inline-size;
}

/* 122px at the end is the marker cell (.pd-task-marks): three 27px markers, two 2px gaps and
   the 37px the section header's kebab takes to the right of ITS marker. It was 93px while there
   were two markers; the link marker (96f61a94) is the third, and a third marker in a cell sized
   for two does not wrap — it overflows LEFT, over the status pill, which is 569a0186 all over
   again ("Pill Label status things are crashing with Notes thing"). Fixed rather than auto, and
   widened here rather than only on rows that have a link, for the reason written on the cell:
   the pill's right edge lines up down the whole list or it lines up nowhere.

   EVERY ROW ONE HEIGHT (1231edb7, Josh, 23 Sep: "Height of task should NOT change based on the
   pills visible"). A row used to be as tall as its tallest cell, so a pill with a due date, a
   "With Joel" or a size chip under it made that row a line taller than its neighbours. Now every
   row keeps room for two lines of title whatever is in it, and nothing in it can ask for more:
     --pd-title-line  one line of a title (14px type). Two of them are 40px.
     --pd-chip-line   one line of the pill cell: the due chip, the tallest thing drawn on it, is
                      12px type at 1.5 plus 2px padding and 1px border each side, 24px.
     --pd-row-body    the pill cell's two lines and the 2px between them, 50px, which is taller
                      than two lines of title, so it is the one that sets the height.
     --pd-chip-floor  the narrowest "With Joel", a section, a bundle or a due date may be drawn:
                      "With…" in the tag mark's 12px semibold, with its padding and border, is
                      54px. Narrower than that a chip is a lozenge with half a letter in it, so it
                      is not drawn at all (.pd-due-rest). Not the ★ or the pill, which never give
                      way to those.
   The title is clamped to two lines (.pd-title-text) and the pill cell to two lines that clip
   (.pd-due-line), so no cell is ever taller than --pd-row-body and every row is 50px + 16px of
   padding + the 1px hairline. The folder and copies heads are .pd-row too and get the same.

   THE RESERVE IS A GRID TRACK, NEVER A MIN-HEIGHT ON THE ROW. A ticked row leaves by animating
   its own max-height to 0 (.ds-leaving in app.css), and min-height beats max-height: a row with
   a min-height stops half-open. A track minimum lives inside the box, so the box still closes to
   nothing around it. minmax(…, auto) and not a fixed track so the rename field, which grows a
   line at a time while a long title is being typed, can still make its row taller while it is
   open (see .pd-task-title-input). */
.pd-row {
    --pd-title-line: 20px;
    --pd-chip-line: 24px;
    --pd-row-body: 50px;
    --pd-chip-floor: 54px;
    display: grid;
    grid-template-columns: 28px 1fr 140px 130px 122px;
    grid-template-rows: minmax(var(--pd-row-body), auto);
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    border-bottom: 1px solid var(--ds-hairline);
    cursor: pointer;
    transition: background 0.1s;
}
@media (hover: hover) and (pointer: fine) {
    .pd-row:hover { background: var(--ds-surface-2); }
}

/* ★ ON A ROW: a MARK beside the pill, never a pill of its own. Josh asked the question and
   answered it himself — "Should it NOT be one of those but rather act like 'Claude's turn' as it
   is just shown" — so it is drawn the way .td-turn-mark is: small, no border, no tone of its own,
   and it never replaces what the status says. */
.pd-star-mark { color: var(--ds-warning); font-size: 13px; line-height: 1; flex: 0 0 auto; }

/* A BUNDLE CHIP on a row: items that go into one build. Not a pill (a pill is a state; a
   bundle is a membership) — a small outlined tag with the bundle mark, that filters the board
   to its members when tapped. The same chip is on the task panel's header (.te-bundle).
   How wide it may be is the pill cell's business (.pd-due-labels): on a row it only ever gets
   the room the tag mark and the due date leave, so it needs no cap of its own. The NAME is a span
   of its own because "…" is drawn only by a box's own line of text: the chip is a flex box, its
   words would sit in an anonymous box that text-overflow cannot reach, and a squeezed chip was
   cut mid-letter ("Board f") instead of ending in "…". */
.pd-bundle-chip {
    display: inline-flex; align-items: center; gap: 4px;
    min-width: 0;
    font: inherit; font-size: 11.5px; font-weight: 600; line-height: 1.4;
    padding: 2px 8px; border-radius: 999px;
    color: var(--ds-accent); background: var(--ds-accent-bg);
    border: 1px solid rgba(47,129,247,.30);
    cursor: pointer; white-space: nowrap; overflow: hidden;
}
.pd-bundle-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.pd-bundle-chip.is-active { background: var(--ds-accent); color: #fff; border-color: var(--ds-accent); }

/* THE PILL CELL IS AT MOST TWO LINES (1231edb7). It used to wrap freely, so every chip that did
   not fit beside the pill added a line to the row. Now:
     line 1  the ★ and the status pill, which never give way to anything on line 2 — they are
             what a glance is for;
     line 2  "With Joel", the section or bundle, and the due date.
   Each line is --pd-chip-line tall, so two of them and the 2px between are exactly
   --pd-row-body. An empty line is not drawn at all, so a row with only a pill shows it in the
   middle of the cell.

   Line 1 is pushed right by an auto margin rather than by justify-content: flex-end. The two
   look the same while things fit; when they do not, an end-justified line spills out of its
   LEFT side, over the assignee — the "pill drawn over JM" of 0c2fb78a — and an auto margin
   collapses to nothing and lets the line run out on the right. */
.pd-task-due {
    display: flex; flex-direction: column; justify-content: center;
    gap: 2px; min-width: 0;
}
.pd-due-line {
    display: flex; align-items: center; gap: 6px;
    height: var(--pd-chip-line); min-width: 0; white-space: nowrap;
}
.pd-due-line > * { flex: none; }
.pd-due-line > :first-child { margin-left: auto; }
/* The ★ and the pill are never wider than the cell. When the cell is narrower than they are, the
   pill ends in "…" rather than running over the initials or a marker, and its hover has the whole
   of it. That is not only an extreme row (measured, 23 Sep): the 130px column on a desk holds
   about 20 letters of pill, fewer beside a ★, so "Blocked · Apple review", "Blocked · the Shared
   Drive…" and a starred "Blocked · Dominique" end in "…" there. */
.pd-due-pill { max-width: 100%; }
.pd-due-pill > .td-status { display: block; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* LINE 2 GIVES WAY IN ONE ORDER, and what it cannot show readably it does not draw at all, never
   as a lozenge with half a letter in it or none.
     1. the section or bundle name ends in "…", then goes;
     2. "With Joel" ends in "…", down to "With…" — Josh was told "With Joel" stays on rows, and it
        is the one thing on a row that says a thread is somebody else's (14 Sep);
     3. then the due date ends in "…", down to --pd-chip-floor ("Over…");
     4. only when "With…" and that much of the date cannot share the line does "With…" go, and
        the date has the line to itself.
   On a desk that means a tagged row with a long date shows "With…" and "Overdu…": "With…" and
   "Overdue 3 days" whole need about 160px of the 130px column.

   HOW. Nothing here shrinks from its full width. Everything starts at --pd-chip-floor and GROWS
   towards its full width (max-width: max-content), and the order is which grows first: a
   flex-grow of 1000 against 1 gives one nearly all the room until it is whole. And the line
   WRAPS, in a box one line tall that clips (.pd-due-line's height, overflow: hidden): a chip
   that has not even its floor's worth of room goes down to the second line, out of sight, and so
   does everything after it. So the date is first on the line (order: -1, and row-reverse keeps
   it at the right-hand end, under the pill), then the box of labels, and in that box the tag
   mark(s) before the section or bundle. The row gap puts the hidden line well clear of the clip.
   4px between chips and not 6: "With…" and "Tomorrow" then share the 130px column whole.

   TO KEEP THE DATE WHOLE AND LET "With…" GO FIRST INSTEAD: give the due chip flex: none. It then
   takes its full width before anything else, and the labels get what is left or go. */
.pd-due-rest {
    flex-direction: row-reverse; flex-wrap: wrap; align-content: flex-start;
    gap: 8px 4px; overflow: hidden;
}
.pd-due-rest > .pd-due-chip {
    order: -1; flex: 1000 1 var(--pd-chip-floor); max-width: max-content; min-width: 0;
    overflow: hidden; text-overflow: ellipsis;
}
.pd-due-rest > .pd-due-labels {
    display: flex; flex-wrap: wrap; align-content: flex-start; align-items: center;
    gap: 8px 4px; height: var(--pd-chip-line); overflow: hidden;
    flex: 1 1 var(--pd-chip-floor); max-width: max-content; min-width: 0;
}
.pd-due-labels > * {
    flex: 1 1 var(--pd-chip-floor); max-width: max-content; min-width: 0;
    overflow: hidden; text-overflow: ellipsis;
}
.pd-due-labels > .td-tag-mark { flex-grow: 1000; }
/* "…" is drawn by a block's own line of text. The pill shape and the due chip are inline-flex,
   whose words sit in an anonymous box text-overflow cannot reach, so on a row they are blocks
   (and the bundle chip's name is a span of its own, .pd-bundle-name). */
.pd-due-labels > .td-tag-mark,
.pd-due-line > .pd-due-chip { display: block; }

.pd-row:last-child { border-bottom: none; }
/* THE DIVIDER COMES AFTER EVERYTHING THAT BELONGS TO THE TASK (97ece905). A row can have lines of
   its own under it, drawn as siblings: its open sub-task list (.pd-subs) and, on a search, the
   "IN THE NOTES" line saying why it matched (.pd-search-why, styled in ProjectDetail.razor). With
   the hairline on the row, those lines hung below it and read as the start of the next task. So
   the row hands its hairline to the last of them: the sub-task list here, the search line (which
   always comes after the list) in ProjectDetail. The notes and pictures panel keeps its own box. */
.pd-row:has(+ .pd-subs),
.pd-row:has(+ .pd-search-why) { border-bottom: none; }

.pd-circle {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: 2px solid var(--ds-hairline-strong);
    background: transparent;
    color: transparent;
    cursor: pointer;
    padding: 0;
    display: grid;
    place-items: center;
    transition: border-color 0.12s, background 0.12s, color 0.12s;
}
/* The green preview ring is for a pointer that is passing over, and only that. On iOS the
   tapped element stays hovered after the finger lifts, so when the ticked row collapses out
   and the row below takes its place, this ring was left drawn on a task that is not done —
   the "it left the green check mark on the item that took its place" report. */
@media (hover: hover) and (pointer: fine) {
    .pd-circle:hover { border-color: var(--ds-success); }
}
.pd-circle:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ds-accent-bg), 0 0 0 5px var(--ds-accent); }
.pd-circle.done { background: var(--ds-success); border-color: var(--ds-success); color: #fff; }
/* A task closed as Not planned (19461bb7): the same tick, grey instead of green, the way GitHub greys a
   not-planned issue's closed icon. Done stays green. NotPlannedTick adds .np only beside .done. */
.pd-circle.done.np { background: var(--ds-text-muted); border-color: var(--ds-text-muted); }
.pd-circle svg { width: 11px; height: 11px; stroke-width: 3; }
/* A task GitHub decides (8eeed595): the tick is GitHub's, so the circle is greyed, done or not, and
   never shows the green ring a tick would get. A tap says why (the page's toast). */
.pd-circle.locked { opacity: 0.45; cursor: not-allowed; border-style: dashed; }
.pd-circle.locked.done { background: var(--ds-text-muted); border-color: var(--ds-text-muted); border-style: solid; }
@media (hover: hover) and (pointer: fine) {
    .pd-circle.locked:hover { border-color: var(--ds-hairline-strong); }
    .pd-circle.locked.done:hover { border-color: var(--ds-text-muted); }
    .pd-circle.done.np:hover { border-color: var(--ds-text-muted); }
}

/* THE TITLE IS TWO LINES AT MOST, then "…" (1231edb7). The words are in .pd-title-text, which
   is clamped; the rename pencil, a folder's count and a copies head's ×N sit BESIDE the words in
   one flex line, never inside them, so the clamp can cut the words and never those. The whole
   title is the hover of .pd-title-text, and the task itself. flex-end lines everything beside
   the words up with their LAST line: after the "…" on a cut title, after the words on a short
   one. overflow-wrap: anywhere so a long link with no spaces still wraps and still ends in "…"
   instead of running off to the right under the clip. */
.pd-task-title {
    display: flex; align-items: flex-end; min-width: 0;
    color: var(--ds-text); font-size: 14px; line-height: var(--pd-title-line);
}
.pd-title-text {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
    overflow: hidden; overflow-wrap: anywhere; min-width: 0; flex: 0 1 auto;
}
/* A Drive link in a title is a chip drawn in the line (DriveLinkResolver). At the title's own
   line height its box is 2px taller than the line, which would make a two-line title 42px. */
.pd-title-text > a { line-height: 16px; }
.pd-task-title.done { text-decoration: line-through; color: var(--ds-text-muted); }

/* "What changed since I looked", at a glance. The dot lives INSIDE .pd-task-title and not
   beside it: the two-line layout below places every DIRECT child of .pd-row into a named grid
   cell, so a new child there would land in an implicit one and shove the assignee and the due
   chip out of the two-line layout. In here it is part of the line the title is already on and
   nothing moves on any width.

   7px, not a badge. It answers one question — is there anything — and the count is a tap away
   in Messages, which is the surface that owns unread. */
.pd-unread-dot {
    display: inline-block; vertical-align: middle;
    width: 7px; height: 7px; margin-right: 6px;
    border-radius: 50%; background: var(--ds-accent);
}
/* A ticked row is struck through, and a line drawn through a dot reads as a bullet point. The
   news on a completed task is that it was completed, and the row is usually filtered out
   anyway — so the dot stands down rather than becoming punctuation. */
.pd-task-title.done .pd-unread-dot { display: none; }

/* Rename-in-place affordance. Quiet until the row is hovered so a long list doesn't turn into
   a column of pencils; always visible where there is no hover to reveal it with. A flex item
   beside the clamped words (.pd-task-title), centred on their last 20px line: (20 - 13) / 2. */
.pd-row-rename {
    display: inline-block;
    flex: none;
    align-self: flex-end;
    margin-bottom: 3.5px;
    border: none;
    background: transparent;
    color: var(--ds-text-muted);
    cursor: pointer;
    padding: 0 2px;
    margin-left: 6px;
    line-height: 1;
    opacity: 0;
    transition: opacity 0.12s, color 0.12s;
}
.pd-row-rename svg { width: 13px; height: 13px; display: block; }
/* THE PENCIL IS NOT IN THE WORDS ANY MORE. It used to be glued to the title's last word
   (TitleTail, a nowrap span), because as inline text it could land on a line of its own — "Why
   does 'Agents needs Select All' have an extra line below it?". Under a two-line clamp the glue
   would have gone the other way: the last word of a long title is the part the clamp hides, and
   the pencil would have been hidden with it. As a flex item beside the words it is never on a
   line of its own and never clipped. */
/* The REVEAL is deliberately left unguarded. A hover query on it would take the pencil away
   from every device that is not a fine pointer, and iOS's sticky :hover — the very thing the
   guards elsewhere exist to stop — is what has always uncovered this control on a phone.
   Revealing a button is not the same failure as decorating one: a pencil that lingers on the
   row you tapped says nothing untrue about that row. Only the colour change below is
   decoration, so only it is pointer-only. */
.pd-row:hover .pd-row-rename { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
    .pd-row-rename:hover { color: var(--ds-accent); }
}
.pd-row-rename:focus-visible {
    opacity: 1;
    outline: none;
    border-radius: 4px;
    box-shadow: 0 0 0 2px var(--ds-accent);
}
@media (hover: none) {
    .pd-row-rename { opacity: 1; }
}

/* Sits in the title's own grid cell at the title's own size, so the row neither reflows nor
   changes height when the span swaps for the field.

   It is a textarea: an input element cannot wrap, so renaming a long task was a line you dragged
   sideways through, which is the whole of "expand text box down instead of to the right".
   line-height IS .pd-task-title's (--pd-title-line), and the -2px margins take back the 1px
   padding and 1px border, so each line of the field takes the room a line of the title did.

   THE ROW DOES NOT SHRINK WHEN THE FIELD OPENS (1231edb7). The field REPLACES the title span, so
   nothing reserved on the span could hold the room while it is open; the reserve is the row's
   grid track instead (--pd-row-body, and the phone's two-line title track), which the field
   sits in just as the span did. A one- or two-line rename is the row's usual height; past two
   lines the box grows down and the row grows with it, which is the point. overflow-y:hidden
   because the height is what moves, not the content — the ceiling is TitleBox.MaxRows, and
   there is nothing worth scrolling inside a title. */
.pd-task-title-input {
    font-family: inherit;
    font-size: 14px;
    line-height: var(--pd-title-line);
    color: var(--ds-text);
    background: var(--ds-surface);
    border: 1px solid var(--ds-accent);
    border-radius: 6px;
    outline: none;
    padding: 1px 6px;
    margin: -2px 0;
    width: 100%;
    min-width: 0;
    overflow-wrap: break-word;
    overflow-y: hidden;
    resize: none;
    field-sizing: content;
}

.pd-assignee-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pd-task-meta {
    font-size: 13px;
    color: var(--ds-text-muted);
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}

/* Notes marker on a task row. Unlike the section's, this one appears ONLY when the task
   has notes — a row is a list entry, not a header, and an empty outline on every line would
   read as a column of unfinished work. Writing notes stays in the task editor; this marker
   says "there is something written here" and opens it. */
/* THE PICTURE MARKER shares every measurement with the notes sheet — one rule, so the two
   cannot drift apart in size. That matters more than it sounds: the marker cell is exactly two
   27px markers wide (see .pd-task-marks), and a marker that grew would put the cell back over
   the status pill — 569a0186, "Pill Label status things are crashing with Notes thing". */
.pd-task-link,
.pd-task-pics,
.pd-task-notes {
    background: transparent;
    border: none;
    color: var(--ds-accent);
    cursor: pointer;
    padding: 4px 6px;
    border-radius: 6px;
    display: inline-grid;
    place-items: center;
    line-height: 0;
}
.pd-task-link svg,
.pd-task-pics svg,
.pd-task-notes svg { width: 15px; height: 15px; }
.pd-task-notes:hover { background: var(--ds-surface); color: var(--ds-accent-hover); }
.pd-task-notes.open { background: var(--ds-surface); }

/* What differs: a paperclip is drawn in outline and would shout beside the filled note sheet if
   it were the same accent blue, so it sits back until it is hovered or open. "Just like notes,
   if a task has a picture attached to it, it should be able to viewed without having to go into
   task" — same gesture, same place, same panel; only the ink is quieter. */
.pd-task-pics { color: var(--ds-text-muted); }
.pd-task-pics:hover { background: var(--ds-surface); color: var(--ds-accent); }
.pd-task-pics.open { background: var(--ds-surface); color: var(--ds-accent); }

/* The link marker takes the paperclip's ink and the paperclip's hover, deliberately: they are
   both quiet outline glyphs sitting beside the filled note sheet, and three markers in one cell
   that each chose their own colour would read as three different kinds of thing. The only rule
   it needs of its own is the underline — half of these are anchors, and an anchor drawn as a
   marker must not arrive with a line under its icon. */
.pd-task-link { color: var(--ds-text-muted); text-decoration: none; }
.pd-task-link:hover { background: var(--ds-surface); color: var(--ds-accent); }

/* THE MARKER CELL: the row's last grid column, holding the notes sheet, the attachment marker
   (7ea38568) and now the link marker (96f61a94) — "make sure it does not crash with attachments
   thing when that comes". Sized for all three: three 27px markers, two 2px gaps, and the 37px
   the section header's kebab takes to the right of ITS marker, which rows have no equivalent
   of; with the reservation a task's marker lands directly under its section's.

   That reservation used to be a margin on the notes BUTTON, sitting in a 27px column. A 27px
   button with 37px of margin ends 37px left of where its column does — over the right end of
   the status pill, once pills arrived in the due column: "Pill Label status things are
   crashing with Notes thing" ("To te…", "Doc revie…" under a blue sheet, on every row that had
   notes). A cell that carries the reservation as its own padding cannot leave its column,
   whatever is drawn inside it. Fixed width rather than auto so the pill's right edge lines up
   down the list whether or not a row has markers. */
.pd-task-marks {
    display: inline-flex; align-items: center; justify-content: flex-end; gap: 2px;
    box-sizing: border-box; min-width: 0;
    padding-right: 37px;
}
/* What a page puts at the far right of the cell (TaskRow's MarksEnd). It keeps its own width:
   the three markers beside it are what the cell was measured for. */
.pd-marks-end { display: inline-flex; align-items: center; flex: none; margin-left: 4px; }

/* Sits directly under the row it belongs to, for the same reason the section panel sits under
   its header: notes are read against the thing they describe. The picture panel is the same
   shape for the same reason — one selector, so a change to the inset moves both. */
.pd-task-notes-panel,
.pd-task-pics-panel {
    padding: 10px 14px 12px 52px;
    border-bottom: 1px solid var(--ds-hairline);
    background: var(--ds-surface-2);
}

/* The picture itself is as wide as the panel and no taller than the cap the component is given,
   so a tall screenshot cannot push the next four rows off the screen — the panel is a peek, and
   the task is still where you go to work on it. */
.pd-task-pics-panel img,
.pd-task-pics-panel video { border-radius: 8px; }

/* THE TWO-LINE ROW, chosen by the width of the ROWS and not of the screen.

   The desktop grid spends 28 + 140 + 130 + 93 on fixed columns, 40 on gaps and 28 on the row's
   own padding — 459px before the title gets a pixel. It used to give way to the two-line layout
   at a 640px VIEWPORT, which is a portrait phone and nothing else: "Tasks look terrible in
   iPhone landscape. Everything too far left and not using room on right side space." A phone
   on its side is 852–932px wide, the sidebar is in flow at 240px, and the rows were left with
   ~650px: ~190px of title wrapped three deep beside 300px of mostly empty assignee and due
   columns — LESS room for a title than the same phone upright gets. So the rows measure
   themselves (.pd-rows is a size container, above) and at 740px or under — the point where the
   desktop grid gives a title less than a portrait phone does — the row goes two-line: title
   full width on line 1, assignee + pill + markers on line 2. That covers a landscape phone, a
   portrait tablet and a narrow Mac window without knowing which it is, and collapsing the
   sidebar buys the desktop grid back. The assignee text label is hidden — the avatar circle
   already communicates ownership, and the due chip is what users glance for.

   ONE HEIGHT HERE TOO (1231edb7): line 1 always has room for two lines of title, line 2 is one
   --pd-chip-line, and both are grid tracks with that floor, for the reason the desktop row's
   is (the tick's collapse). The pill cell's two lines lie side by side on line 2 instead of
   stacking, and nothing on it wraps into sight.

   THE INITIALS CANNOT BE SQUEEZED (0c2fb78a, "JM" with the pill drawn over it). The columns were
   28px 1fr auto auto, and a 1fr track's floor is its cells' min-width, which was 0 — so a wide
   pill cell took the whole line and left the initials a column of nothing, drawn under the
   pill. Now the initials' cell has a min-width of its own width (min-content), which a 1fr
   track keeps as its floor, the markers keep theirs (max-content), and the pill cell has what
   is left and no floor. The floor is on the CELL and not on the track (minmax(min-content, 1fr))
   because the title spans that track too: a min-content track takes its floor from every cell
   in it, and a title's min-content is its widest unbreakable piece — a Drive link's chip is up
   to 280px — which took the whole line from the pill and the date. The title's min-width: 0
   keeps it out of a 1fr track's floor.

   In the pill cell the ★ and the pill come first and never give way. The line beside them gives
   way in the desk's order (.pd-due-rest): the section or bundle, "With Joel" down to "With…",
   then the date; and when not even --pd-chip-floor of it fits beside the pill, the whole of it
   goes down to the cell's second line, out of sight, rather than being drawn as a sliver. Only
   on a row with no room even for the ★ and the pill does the pill end in "…". Nothing in the
   cell spills left over the initials or right over the markers: it clips at its own edges. */
@container pd-rows (max-width: 740px) {
    .pd-row {
        grid-template-columns: 28px 1fr minmax(0, auto) max-content;
        grid-template-rows: minmax(calc(2 * var(--pd-title-line)), auto) minmax(var(--pd-chip-line), auto);
        grid-auto-rows: auto;
        row-gap: 4px;
        align-items: start;
        padding: 10px 14px;
    }
    .pd-row > .pd-circle,
    .pd-row > .pd-select-box { grid-column: 1; grid-row: 1; margin-top: 2px; }
    .pd-row > .pd-task-title { grid-column: 2 / span 3; grid-row: 1; }
    .pd-row > .pd-task-title-input { grid-column: 2 / span 3; grid-row: 1; }
    .pd-row > .pd-task-meta {
        grid-column: 2;
        grid-row: 2;
        align-self: center;
        min-width: min-content;
        font-size: 12px;
    }
    .pd-row > .pd-task-meta .pd-assignee-name { display: none; }
    .pd-row > .pd-task-due {
        grid-column: 3; grid-row: 2; align-self: center;
        flex-direction: row; flex-wrap: wrap; align-content: flex-start; justify-content: flex-start;
        align-items: center; gap: 8px 6px; height: var(--pd-chip-line); overflow: hidden;
    }
    .pd-row > .pd-task-due > :first-child { margin-left: auto; }
    /* The ★ and the pill never give way to the line beside them. */
    .pd-row > .pd-task-due > .pd-due-pill { flex: none; }
    .pd-row > .pd-task-due > .pd-due-rest { flex: 1 1 var(--pd-chip-floor); max-width: max-content; }
    .pd-row > .pd-task-due:empty { display: none; }
    /* No kebab column to line up with once the row wraps — the markers sit at the row's own
       right edge, beside the due chip. */
    .pd-row > .pd-task-marks { grid-column: 4; grid-row: 2; justify-self: end; align-self: center; padding-right: 0; }
    .pd-task-notes-panel { padding-left: 14px; }
    .pd-task-pics-panel { padding-left: 14px; }
}
.pd-avatar {
    width: 22px; height: 22px; border-radius: 999px;
    background: var(--ds-accent-bg); color: var(--ds-accent);
    font-size: 10px; font-weight: 700;
    display: grid; place-items: center;
    flex-shrink: 0;
}

/* Collaborator stack — small overlapping avatars that follow the assignee. The border
   matches the row's surface so adjacent avatars read as separate even when overlapping.
   Capped at 3 visible with a "+N" pill for overflow so wide tasks don't push the due chip
   off-row. */
.pd-collab-stack {
    display: inline-flex;
    align-items: center;
    margin-left: 4px;
    flex-shrink: 0;
}
.pd-collab-stack .pd-collab-avatar {
    width: 20px; height: 20px; border-radius: 999px;
    background: var(--ds-surface-2); color: var(--ds-text-muted);
    font-size: 9px; font-weight: 700;
    display: grid; place-items: center;
    border: 2px solid var(--ds-surface);
    margin-left: -7px;
    flex-shrink: 0;
}
.pd-collab-stack .pd-collab-avatar:first-child { margin-left: 2px; }
.pd-collab-stack .pd-collab-overflow {
    margin-left: -7px;
    padding: 0 6px;
    height: 20px;
    min-width: 20px;
    border-radius: 999px;
    background: var(--ds-surface-2);
    color: var(--ds-text-muted);
    font-size: 10px; font-weight: 600;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 2px solid var(--ds-surface);
    flex-shrink: 0;
}
@media (hover: hover) and (pointer: fine) {
    .pd-row:hover .pd-collab-stack .pd-collab-avatar,
    .pd-row:hover .pd-collab-stack .pd-collab-overflow { border-color: var(--ds-surface-2); }
}

.pd-due-chip {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 12px;
    white-space: nowrap;
}
.pd-due-chip.danger { background: var(--ds-danger-bg); color: var(--ds-danger); border: 1px solid var(--ds-danger); }
.pd-due-chip.warning { background: var(--ds-warning-bg); color: var(--ds-warning); border: 1px solid var(--ds-warning); }
.pd-due-chip.accent { background: var(--ds-accent-bg); color: var(--ds-accent); border: 1px solid rgba(47,129,247,0.3); }
.pd-due-chip.neutral { background: var(--ds-surface-2); color: var(--ds-text-muted); border: 1px solid var(--ds-hairline); }

.pd-due-chip.success { background: var(--ds-success-bg); color: var(--ds-success); border: 1px solid var(--ds-success); }

/* No cap of its own, like the bundle chip it stands in for: on a row it only ever gets the room
   the tag mark and the due date leave (.pd-due-labels). */
.pd-folder-section {
    min-width: 0; font-size: 11.5px; color: var(--ds-text-muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* A TICK CELL WITH NO TICK: a Dashboard timer or bonus task, whose own buttons say where it is. The
   cell is still drawn, so the title keeps the grid's second column and does not slide into the first. */
.pd-circle-blank { width: 20px; height: 20px; }

/* WHERE A TASK LIVES, AND ITS BUNDLE, AS LINKS: on the Dashboard the project's name opens the project
   and the ⧉ chip opens that project with the bundle pressed. They keep the label's and the chip's own
   look; an anchor's underline would be the only line on the row. */
a.pd-folder-section,
a.pd-bundle-chip { text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
    a.pd-folder-section:hover { color: var(--ds-accent); text-decoration: underline; }
}

/* WHAT ONLY A DASHBOARD TASK HAS, at the end of the title's line (TaskRow's Extras): a timer's time
   and its Start or Stop, the lock countdown, a bonus task's Bonus, Complete and Undo and its count
   today, the minutes and the cooldown. THEY MAY NOT MAKE THE ROW TALLER — Josh's first Height answer,
   every row one height. So they are one line that never wraps, each piece one --pd-chip-line (24px)
   tall, which fits inside a line of title twice over; the title's words give way beside them and end
   in "…" as they always do. They used to be two rows of Bootstrap buttons, 66px, under the badges.
   The inner box is inline-flex because a line through a struck title (a locked task) is not drawn
   into an inline block, and the countdown is the one thing on a locked row that must read clearly. */
.pd-row-extras {
    flex: none; align-self: center;
    margin-left: auto; padding-left: 8px;
}
.pd-row-extras-in {
    display: inline-flex; align-items: center; gap: 4px;
    white-space: nowrap; vertical-align: middle;
}
.pd-extra-chip,
.pd-extra-btn {
    display: inline-flex; align-items: center; gap: 4px;
    height: var(--pd-chip-line); box-sizing: border-box;
    padding: 0 8px; border-radius: 999px;
    font: inherit; font-size: 12px; line-height: 1;
}
.pd-extra-chip {
    color: var(--ds-text-muted); background: var(--ds-surface-2);
    border: 1px solid var(--ds-hairline);
}
.pd-extra-btn {
    font-weight: 600; cursor: pointer;
    color: var(--ds-accent); background: var(--ds-accent-bg);
    border: 1px solid rgba(47,129,247,.30);
}
.pd-extra-btn.is-stop { color: var(--ds-danger); background: var(--ds-danger-bg); border-color: rgba(248,81,73,.30); }
.pd-extra-btn.is-undo { color: var(--ds-warning); background: var(--ds-warning-bg); border-color: rgba(210,153,34,.30); }
.pd-extra-btn.is-quiet { color: var(--ds-text); background: var(--ds-surface-2); border-color: var(--ds-hairline-strong); font-variant-numeric: tabular-nums; }
.pd-extra-btn:disabled { cursor: default; color: var(--ds-text-muted); background: var(--ds-surface-2); border-color: var(--ds-hairline); }
.pd-extra-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--ds-accent); }
@media (hover: hover) and (pointer: fine) {
    .pd-extra-btn:not(:disabled):hover { border-color: currentColor; }
}
.pd-extra-chip svg,
.pd-extra-btn svg { width: 13px; height: 13px; flex: none; vertical-align: 0; }
/* Where the row goes two-line — a phone either way up, a narrow window — the buttons are icons only,
   as they always were on a phone, and every word they had is still their hover. Everything is drawn a
   little tighter there too: the fullest bonus row the Dashboard can draw (a bonus task ticked today, with
   its minutes) still leaves its title 60px and more on a 390px phone (OneTaskRowEverywhereTests measures
   it). A bonus task with a cooldown is locked by its first tick and shows its countdown instead. */
@container pd-rows (max-width: 740px) {
    .pd-extra-word { display: none; }
    .pd-row-extras { padding-left: 6px; }
    .pd-row-extras-in { gap: 3px; }
    .pd-extra-chip,
    .pd-extra-btn { padding: 0 6px; gap: 3px; }
}

/* ---- A task's subtasks under its row (SubtaskList) ----
   The arrow chip, or "+ sub-task", is at the row's FAR RIGHT: the end of the marker cell
   (TaskRow's MarksEnd), after the notes sheet. That cell is 122px (.pd-row above), exactly three
   markers and the kebab's 37px, so on a project page — whose .pd-rows carries .pd-rows-subs —
   the column is 92px wider to hold it: every row of the board, so the columns still line up
   down the page. The Dashboard's rows hand nothing in and keep the 122px. Only while the rows are wide
   enough to be one line: app.css's two-line phone row (740px and under) places the cell itself
   and sizes it to what is in it, and there "+ sub-task" is just its "+".
   The lines under the row are indented to the title's column and a little past it, and use the
   row's own circle, avatar and due chip at a smaller size. */
/* WRITTEN AT THE END OF THE SHEET, AND ITS NARROW QUERIES WITH "and (min-width: 0px)", on purpose.
   The row's own rules above are found by their text (EveryRowTheSameHeightTests, ProjectRowLayoutTests
   and ADashboardRowFitsTests read the first rule for the row and the first and the last of the
   two-line row's container query), so nothing here may be an earlier rule opening on the row's
   class or a later copy of that query's exact words — this note included. The extra condition is
   always true; the query is the same. */
@container pd-rows (min-width: 740.02px) {
    .pd-rows-subs .pd-row { grid-template-columns: 28px 1fr 140px 130px 214px; }
}
@container pd-rows (max-width: 740px) and (min-width: 0px) {
    .pd-subs-add-words { display: none; }
}
.pd-subs-add {
    height: var(--pd-chip-line, 24px); padding: 0 8px; border-radius: 999px;
    border: 1px dashed var(--ds-hairline-strong); background: transparent;
    color: var(--ds-text-faint); font: inherit; font-size: 12px; line-height: 1;
    white-space: nowrap; cursor: pointer;
}
.pd-subs-add.open { border-style: solid; border-color: var(--ds-accent); color: var(--ds-text); }
@media (hover: hover) and (pointer: fine) {
    .pd-subs-add:hover { color: var(--ds-text); border-color: var(--ds-text-muted); }
}
/* The line to type a new subtask on: the words "+ sub-task" until it is typed in. */
.pd-sub-new { border-left-style: dashed; }
.pd-sub-plus { width: 16px; height: 16px; }
.pd-sub-add {
    grid-column: 2 / -1; min-width: 0; width: 100%; max-width: 420px;
    padding: 3px 8px; border: 1px solid transparent; border-radius: 6px;
    background: transparent; color: var(--ds-text); font: inherit; font-size: 13px;
}
.pd-sub-add::placeholder { color: var(--ds-text-faint); }
.pd-sub-add:hover { border-color: var(--ds-hairline); }
.pd-sub-add:focus { outline: none; border-color: var(--ds-accent); background: var(--ds-surface); }
.pd-subs-toggle {
    border: 1px solid var(--ds-hairline); background: var(--ds-surface-2);
    color: var(--ds-text-muted); cursor: pointer;
}
.pd-subs-toggle.open { border-color: var(--ds-accent); color: var(--ds-text); }
/* A drawn chevron, not a ▸ character: at a chip's size the character is a dot. */
.pd-subs-arrow { width: 12px; height: 12px; flex: none; transition: transform .12s; }
.pd-subs-toggle.open .pd-subs-arrow { transform: rotate(90deg); }
@media (hover: hover) and (pointer: fine) {
    .pd-subs-toggle:hover { color: var(--ds-text); border-color: var(--ds-hairline-strong); }
}
.pd-subs { padding: 0 14px 8px 52px; border-bottom: 1px solid var(--ds-hairline); }
.pd-subs:has(+ .pd-search-why),
.pd-subs:last-child { border-bottom: none; }
.pd-sub {
    /* The row's own last three columns (140px 130px, and this page's 214px for the markers and
       the subtasks chip), so who and when sit under the task's who and when; the last is empty. */
    display: grid; grid-template-columns: 18px minmax(0, 1fr) 140px 130px 214px; align-items: center; gap: 10px;
    padding: 5px 0 5px 12px; border-left: 2px solid var(--ds-hairline);
    font-size: 13px;
}
.pd-sub-circle { width: 16px; height: 16px; }
.pd-sub-circle:disabled { opacity: .5; cursor: default; }
.pd-sub-title {
    min-width: 0; padding: 0; border: none; background: none; text-align: left; cursor: pointer;
    font: inherit; color: var(--ds-text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pd-sub.done .pd-sub-title { color: var(--ds-text-faint); text-decoration: line-through; }
.pd-sub-who { display: flex; align-items: center; gap: 6px; min-width: 0; color: var(--ds-text-muted); font-size: 12.5px; }
.pd-sub-who .pd-avatar { width: 18px; height: 18px; font-size: 9px; }
.pd-sub-who .pd-assignee-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-sub-due { display: flex; justify-content: flex-end; }
@container pd-rows (max-width: 740px) and (min-width: 0px) {
    /* Where the rows go two-line (a phone): the title on its own line, who and when under it. */
    .pd-subs { padding-left: 38px; }
    .pd-sub { grid-template-columns: 18px minmax(0, 1fr) auto; row-gap: 2px; }
    .pd-sub-title { grid-column: 2 / -1; white-space: normal; }
    .pd-sub-who { grid-column: 2; }
    .pd-sub-due { grid-column: 3; }
}
