/* todo.css — todo/kanban board mode styling.
   Extracted from main.css to keep that file focused on the draw surface and
   below the project's per-file size budget. Loaded alongside main.css (see
   index.html) and precached in sw.js. All selectors here are scoped to the
   kanban surface (#todo-board, .todo-*, #mode-switch) and the [data-mode]
   visibility toggles, so nothing in main.css depends on these rules. */

/* ---- Mode switch button (top bar) ---- */
/* One button shown only while the board is switchable: a ghost-blue CTA to
   create a todo board on an empty draw board, or a muted "back to drawing"
   escape hatch on a task-free todo board. */
#mode-switch {
    display: inline-flex;
    flex-shrink: 0;
}

#mode-switch[hidden] {
    display: none;
}

#mode-switch-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: transparent;
    color: #007aff;
    border: 1px solid rgba(0, 122, 255, 0.45);
    border-radius: 999px;
    padding: 5px 13px;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    line-height: 1;
    white-space: nowrap;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}

#mode-switch-btn:hover {
    background: #007aff;
    color: #fff;
    border-color: #007aff;
}

/* "Back to drawing" — muted/neutral, not a primary CTA. */
#mode-switch-btn.mode-switch-back {
    color: var(--ui-text-color);
    border-color: var(--border-color);
    font-weight: 500;
}

#mode-switch-btn.mode-switch-back:hover {
    background: var(--glass-bg);
    color: var(--ui-text-color);
    border-color: var(--ui-text-color);
}

/* ---- Mode-driven visibility ---- */
/* Default (draw): the kanban surface is hidden. */
#todo-board {
    display: none;
    /* Height of a single one-line task card (title only: 10px padding ×2 + 1px
       border ×2 + ~25px first-line) — drives the column's minimum height, the
       "+ Add task" button, and the drag placeholder so an empty column, the add
       button, and a drop slot are all exactly one task tall. */
    --todo-task-h: 47px;
}

[data-mode="todo"] #todo-board {
    display: block;
    position: fixed;
    inset: 0;
    top: 0;
    z-index: 500;
    overflow: hidden;
    background: var(--bg-color);
    /* The kanban isn't a drawing surface — drop the canvas crosshair. Editable
       text fields opt back into the text I-beam; buttons get pointer. */
    cursor: default;
}

/* Space-held pan cursor (outside columns only — columns keep their own cursors). */
[data-pan-ready="true"][data-mode="todo"] #todo-board {
    cursor: grab;
}
[data-panning="true"][data-mode="todo"] #todo-board {
    cursor: grabbing !important;
}

/* In todo mode the freeform drawing surface and its drawing tools are
   inert/hidden. The colour palette (#color-picker) is kept VISIBLE: in todo
   mode a colour click recolours the title of whatever is being edited (a task
   card or a column header) rather than the draw ink. */
[data-mode="todo"] #canvas,
[data-mode="todo"] #undo-btn,
[data-mode="todo"] #eraser-toggle,
[data-mode="todo"] #empty-hint {
    display: none;
}

/* The Recent-boards rail is shared by both modes — it's per-user navigation,
   not drawing content. Its default tier (z-index:50) sits BELOW the todo
   overlay (#todo-board, z-index:500), so without this lift the kanban would
   paint over it. Raise it (and its context menu) into the chrome tier so it
   floats above the kanban exactly like every other toolbar control does. */
[data-mode="todo"] #recent-boards {
    z-index: 1000;
}
/* …but the top bar (with the Switch-to-drawing button) must stay above the
   Recent rail. Both default to z-index:1000, and #recent-boards comes later in
   the DOM, so it would otherwise paint over the button. Lift the bar one tier
   above it, and keep Recent's own context menu above the bar. */
[data-mode="todo"] #top-bar {
    z-index: 1001;
}
[data-mode="todo"] .recent-ctx-menu {
    z-index: 1002;
}

/* ---- Kanban layout ---- */
/* The scroller is a vertical STACK of lanes (rows). Each lane is a horizontal
   row of columns. A single-lane board (all columns lane 0) is one lane row →
   visually identical to the legacy single-row layout. */
.todo-scroller {
    display: flex;
    flex-direction: column;
    gap: 16px;
    align-items: flex-start;
    width: max-content;
    padding: 96px 24px 24px;
    /* Navigation is canvas-style pan/zoom (see applyTransform), not native
       scroll — origin 0 0 matches the zoomAt math. */
    overflow: visible;
    transform-origin: 0 0;
    will-change: transform;
}

/* A lane GROUP: the optional row-name header stacked on top of the lane row.
   It's a column flex whose width is dictated by its widest child — the lane row
   — so the header (and its full-width <hr>) stretches to exactly the row width. */
.todo-lane-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
    align-items: stretch;
}

/* Per-row header: an editable title above a horizontal rule that spans the row. */
.todo-lane-header {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.todo-lane-title {
    align-self: flex-start;
    max-width: 100%;
    box-sizing: border-box;
    padding: 4px 6px;
    font-size: 16px;
    font-weight: 600;
    line-height: 1.4;
    /* Lane (row) names read as section headers: always uppercase with a touch
       of tracking and a hair larger than column titles. */
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--ui-text-color);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border-radius: 6px;
    cursor: text;
    outline: none;
}

/* Hover affordance only for NAMED rows — an unnamed row's placeholder must not
   grow a box on hover (it would draw attention to a row the user hasn't named). */
.todo-lane-title:not(.is-placeholder):hover {
    background: var(--glass-bg);
}

.todo-lane-title.is-editing {
    background: var(--glass-bg);
    box-shadow: 0 0 0 1px var(--brand-color);
}

/* Faint hint shown only on writable boards when the row is unnamed. Very low
   opacity by default so it stays out of the way; grayer (more visible) on hover
   so it reads as "click to name this row" when the user reaches for it. */
.todo-lane-title.is-placeholder::before {
    content: 'Name this row';
    opacity: 0.18;
    font-weight: 500;
}

.todo-lane-title.is-placeholder:hover::before {
    opacity: 0.45;
}

/* A lane: one horizontal row of columns plus its trailing "+ Add column". The
   lane's box already spans the full row — its width includes the reserved
   add-column slot (the button keeps its flex slot while hidden via opacity) and
   its height is the tallest column — so hovering ANYWHERE in it (a column, the
   gaps between them, the empty area below shorter columns, or the add-column slot
   on the right) counts as `.todo-lane:hover` and keeps "+ Add column" revealed.
   Columns keep their natural heights (flex-start). */
.todo-lane {
    display: flex;
    flex-direction: row;
    gap: 16px;
    align-items: flex-start;
}

/* Bottom button that starts a brand-new lane (new row). Sized identically to a
   per-lane "+ Add column" (.todo-add-column) — same width/height/style — it's
   just positioned below all lanes. See the shared rules near .todo-add-column. */
.todo-add-lane {
    /* The scroller is a column flex, so set the cross-axis (width) explicitly —
       flex-basis here would size HEIGHT. Match a column: 280px wide × 44px. */
    flex: 0 0 auto;
    align-self: flex-start;
    width: var(--todo-col-width, 280px);
    height: 44px;
    box-sizing: border-box;
    margin-top: 0;
    background: transparent;
    border: 1px dashed var(--border-color);
    /* Match the dimmed "+" add-task/add-column affordances. */
    color: color-mix(in srgb, var(--ui-text-color) 62%, transparent);
    border-radius: 10px;
    padding: 8px;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    text-align: center;
}

.todo-add-lane:hover {
    border-color: var(--brand-color);
    color: var(--brand-color);
}

.todo-column {
    flex: 0 0 var(--todo-col-width, 280px);
    display: flex;
    flex-direction: column;
    position: relative;
    background: var(--glass-bg);
    backdrop-filter: blur(8px);
    border: 1px solid var(--border-color);
    border-radius: 14px;
    box-shadow: var(--shadow);
    padding: 12px;
}

.todo-column-header {
    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
}

/* Drag-resize handle in the column's bottom-right corner — mirrors the canvas
   text-node handle (.text-resize-handle): a 16×16 corner hit area with a small
   L-shaped indicator, hidden until the column is hovered. Dragging it changes
   the ONE board column width (horizontal only), so all columns resize together. */
.todo-col-resize {
    position: absolute;
    right: -2px;
    bottom: -2px;
    width: 16px;
    height: 16px;
    opacity: 0;
    transition: opacity 0.15s ease;
    cursor: ew-resize;
    z-index: 2;
    touch-action: none;
}

.todo-col-resize::before {
    content: "";
    position: absolute;
    right: 3px;
    bottom: 3px;
    width: 8px;
    height: 8px;
    border-right: 2px solid rgba(128, 128, 128, 0.55);
    border-bottom: 2px solid rgba(128, 128, 128, 0.55);
    border-radius: 0 0 3px 0;
    transition: border-color 0.12s ease;
}

.todo-column:hover > .todo-col-resize,
.todo-col-resize.is-resizing {
    opacity: 1;
}

.todo-col-resize:hover::before,
.todo-col-resize.is-resizing::before {
    border-right-color: var(--brand-color);
    border-bottom-color: var(--brand-color);
}

/* While resizing: force the resize cursor everywhere and kill text selection so
   the drag reads cleanly across the whole board. */
body.todo-col-resizing,
body.todo-col-resizing * {
    cursor: ew-resize !important;
    user-select: none !important;
    -webkit-user-select: none !important;
}

.todo-column-title {
    flex: 1 1 auto;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-color);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Centred in the column: the title spans the full header width (the delete ×
       is taken out of flow, below) and reserves symmetric room on both sides so a
       long title ellipsizes clear of the corner × and the centring stays true. */
    text-align: center;
    padding: 0 22px;
    border-radius: 6px;
    /* Editable in place — show the text I-beam, not the canvas crosshair. */
    cursor: text;
}

/* In-place title edit: subtle focus affordance, no format change, no layout
   shift (outline drawn outside the box; allow text to show fully while typing). */
.todo-column-title.is-editing,
.todo-column-title:focus {
    outline: 2px solid var(--brand-color);
    outline-offset: 2px;
    overflow: visible;
    text-overflow: clip;
    white-space: pre-wrap;
    cursor: text;
}

.todo-column-rename,
.todo-column-delete {
    background: transparent;
    border: none;
    color: var(--ui-text-color);
    cursor: pointer;
    font-size: 15px;
    line-height: 1;
    padding: 2px 4px;
    border-radius: 6px;
}

/* Pulled out of the header flow into the top-right corner so the title can span
   the full width and centre truly (the title's symmetric padding keeps text off
   it). Positioned against the header (position:relative above). */
.todo-column-delete {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    z-index: 1;
}

.todo-column-rename:hover,
.todo-column-delete:hover {
    background: var(--bg-color);
}

.todo-column-delete:hover {
    color: #ff3b30;
}

.todo-task-list {
    flex: 1 1 auto;
    /* No internal scroll — the whole board pans (canvas-style), so columns grow
       to fit all tasks and you pan to reach them. */
    overflow: visible;
    display: flex;
    flex-direction: column;
    gap: 8px;
    /* At least one task tall, even when empty. */
    min-height: var(--todo-task-h);
    padding: 2px;
    border-radius: 8px;
}

/* Drop slot shown while dragging — same size as a one-line task. */
.todo-drop-placeholder {
    flex: 0 0 auto;
    height: var(--todo-task-h);
    box-sizing: border-box;
    border-radius: 10px;
    border: 1px dashed var(--brand-color);
    background: color-mix(in srgb, var(--brand-color) 10%, transparent);
    /* Never a dragenter/leave target itself — keeps the placeholder from
       generating the very events that drive the strobe loop. */
    pointer-events: none;
}

/* Native drag image: an empty content-less box the size of the dragged task /
   column, styled like the drop slot. Built in JS (makeDragGhost), rendered off-
   screen, used via setDragImage, then removed. Inline width/height set per drag. */
.todo-drag-ghost {
    position: fixed;
    top: -9999px;
    left: -9999px;
    box-sizing: border-box;
    border: 2px dashed var(--brand-color);
    background: color-mix(in srgb, var(--brand-color) 16%, transparent);
    pointer-events: none;
}
.todo-drag-ghost-task { border-radius: 10px; }
.todo-drag-ghost-column { border-radius: 14px; }

/* Touch reorder drag image: an EMPTY content-less frame (the touch counterpart
   of the native makeDragGhost image) that tracks the finger. Floats above the
   board (#todo-board is z-index:500) and is transparent to elementFromPoint so
   the placeholder can still hit-test the list/lane under the finger. Mirrors the
   .todo-drag-ghost dashed-box look so touch and desktop drags read the same. */
.todo-touch-ghost {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1500;
    box-sizing: border-box;
    margin: 0;
    pointer-events: none;
    border: 2px dashed var(--brand-color);
    background: color-mix(in srgb, var(--brand-color) 16%, transparent);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
    will-change: left, top;
}
.todo-touch-ghost-task { border-radius: 10px; }
.todo-touch-ghost-column { border-radius: 14px; }

/* Empty column: drop the dashed placeholder look, but KEEP the base one-task
   min-height (var(--todo-task-h)) so an empty column is just as tall as a column
   with one task — a blank task slot — with the always-on "+ Add task" button
   below it. The whole column is still the drop target (enableColumnDrop binds the
   column), so dropping into an empty column works. */
.todo-task-list.is-empty {
    border: none;
    cursor: default;
    /* Blank reserved slot that sits BELOW the button (see the order:1 rule) and
       keeps the column exactly one-task tall. One task (var) + the list's 4px
       top+bottom padding + 4px to offset the button's smaller top margin (2px vs
       a task row's 6px), so the total still equals a one-task column. */
    min-height: calc(var(--todo-task-h) + 8px);
}

.todo-task {
    position: relative;
    background: var(--bg-color);
    border: 1px solid var(--border-color);
    border-radius: 10px;
    padding: 10px;
    cursor: pointer;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

.todo-task:hover {
    border-color: var(--brand-color);
}

/* A task being edited — or a new one being typed — keeps a constant blue frame:
   like the hover ring, but a touch thicker (an extra 2px brand ring outside the
   border) so the open editor is unmistakable. Box-shadow ring instead of a wider
   border → no layout shift. Keyed off the text block's .is-editing (set for the
   whole edit) and the add-task placeholder card. */
.todo-task:has(.todo-task-text.is-editing),
.todo-task.todo-task-placeholder {
    border-color: var(--brand-color);
    box-shadow: 0 0 0 2px var(--brand-color), 0 1px 2px rgba(0, 0, 0, 0.06);
}

/* Hover-× in the card's top-right corner (delete). Hidden until the card is
   hovered; never shown on locked cards (those have pointer-events:none anyway).
   Clicking swaps it for a ✓ / ✕ confirm (.todo-task-delete-confirm). */
.todo-task-delete {
    position: absolute;
    top: 4px;
    right: 4px;
    z-index: 1;
    background: transparent;
    border: none;
    color: #ff3b30;
    cursor: pointer;
    font-size: 15px;
    line-height: 1;
    padding: 2px 5px;
    border-radius: 6px;
    opacity: 0;
    transition: opacity 0.12s ease;
}

.todo-task:hover .todo-task-delete {
    opacity: 1;
}

.todo-task-delete:hover {
    background: color-mix(in srgb, #ff3b30 15%, transparent);
}

.todo-task.is-locked .todo-task-delete {
    display: none;
}

/* Two-step confirm shown in place of the corner × — reuses the column-confirm
   ✓ / ✕ buttons (.todo-confirm-yes / .todo-confirm-no). */
.todo-task-delete-confirm {
    position: absolute;
    top: 4px;
    right: 4px;
    z-index: 1;
    display: inline-flex;
    gap: 2px;
    align-items: center;
    background: var(--bg-color);
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}

/* Pulled out of flow (not just dimmed) while dragging so the drop placeholder
   occupies exactly the freed slot → near-zero reflow → no dragover/dragleave
   strobe. Applied one tick after dragstart so the drag image is captured first. */
.todo-task.todo-dragging {
    display: none;
}

/* Single WYSIWYG text block: first line is the title (bold via ::first-line),
   remaining lines are the description. Identical in display and edit mode.
   `padding-left` leaves a gutter for the round done-checkbox that sits before
   the name on the first line; every line (wrapped title + description) lands at
   the same indent, so the checkbox only occupies the first-line gutter. */
.todo-task-text {
    font-size: 12px;
    color: var(--ui-text-color);
    white-space: pre-wrap;
    word-break: break-word;
    border-radius: 6px;
    padding-left: 22px;
    /* Editable in place — show the text I-beam, not the canvas crosshair. */
    cursor: text;
}

/* Round done-toggle in the first line's gutter, before the name. Same circle +
   checkmark look as the draw board's "[ ]" task-list checkboxes (.task-checkbox
   in text.css), kept in sync deliberately. Absolutely positioned so it aligns
   with the first line without entering the contentEditable text block. */
.todo-task-check {
    position: absolute;
    left: 10px;
    top: 15px;
    width: 14px;
    height: 14px;
    border: 1.5px solid var(--task-title-color, var(--text-color));
    border-radius: 50%;
    box-sizing: border-box;
    cursor: pointer;
    opacity: 0.7;
    background: transparent;
    transition: opacity 0.12s ease, background 0.12s ease;
    z-index: 1;
    user-select: none;
    -webkit-user-select: none;
}

.todo-task-check:hover {
    opacity: 1;
}

/* Enlarge the touch target far beyond the 14px circle (an invisible ~34px hit
   area, centred on the circle) so a finger tap lands on the toggle instead of
   falling through to the card — which would open the editor and feel like the
   first tap "only selected the task". The circle keeps its 14px look. */
.todo-task-check::before {
    content: "";
    position: absolute;
    inset: -10px;
    border-radius: 50%;
}

.todo-task-check.todo-task-check-checked {
    background: var(--task-title-color, var(--text-color));
    border-color: var(--task-title-color, var(--text-color));
    opacity: 1;
}

.todo-task-check.todo-task-check-checked::after {
    content: "";
    position: absolute;
    left: 2.5px;
    top: 0px;
    width: 4px;
    height: 7.5px;
    border: solid;
    border-color: transparent;
    border-right-color: var(--bg-color);
    border-bottom-color: var(--bg-color);
    border-width: 0 1.75px 1.75px 0;
    transform: rotate(45deg);
}

/* A completed task reads as "checked off": title + description dimmed and
   struck through. The checkbox itself stays full-opacity (set above). */
.todo-task-done .todo-task-text {
    text-decoration: line-through;
    opacity: 0.55;
}

/* A task locked by another editor isn't editable here — no text cursor. */
.todo-task.is-locked .todo-task-text {
    cursor: default;
}

.todo-task-text::first-line {
    font-weight: 600;
    font-size: 13px;
    /* Only the title (first line) is tintable. --task-title-color is set on the
       card element when the task carries a named colour; otherwise it falls back
       to the default text colour. The description (later lines) stays default. */
    color: var(--task-title-color, var(--text-color));
    /* Taller first line = a small gap between the title and the description. */
    line-height: 1.9;
}

/* Seamless WYSIWYG edit: no box/outline around the task text — you edit the
   exact text you see (the card is the only container). */
.todo-task-text.is-editing,
.todo-task-text:focus {
    outline: none;
}

/* URLs inside a task description. They read as plain text until the card is
   hovered, then surface as an underlined, clickable link (brand colour). Inert
   while editing (the text block is contentEditable — clicks place the caret). */
.todo-task-link {
    color: inherit;
    text-decoration: none;
    cursor: text;
}

.todo-task:hover .todo-task-text:not(.is-editing) .todo-task-link {
    color: var(--brand-color);
    text-decoration: underline;
    cursor: pointer;
}

.todo-task-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 8px;
}

.todo-pill {
    font-size: 11px;
    font-weight: 600;
    padding: 2px 7px;
    border-radius: 999px;
    background: var(--glass-bg);
    border: 1px solid var(--border-color);
    color: var(--ui-text-color);
    white-space: nowrap;
}

/* Assignee pill always hugs the task's right edge — `margin-left:auto` eats the
   free space in the flex meta row so priority/due stay left and the assignee is
   pushed flush right (and right-aligned on its own line if the row wraps). */
.todo-assignee {
    margin-left: auto;
}

/* Overdue due date (past + task not done) → red pill so it stands out. */
.todo-due.todo-due-overdue {
    background: #ff3b30;
    border-color: #ff3b30;
    color: #fff;
}

.todo-pri-H { background: #ff3b30; color: white; border-color: #ff3b30; }
.todo-pri-M { background: #ff9500; color: white; border-color: #ff9500; }
.todo-pri-L { background: #34c759; color: white; border-color: #34c759; }

/* Priority as a 3-bar "volume" indicator (bottom green → top red), lit
   cumulatively. Used read-only on cards and interactive in the editor. */
.todo-prio-bars {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    vertical-align: middle;
    line-height: 0;
}

.todo-prio-bar {
    width: 14px;
    height: 3px;
    border-radius: 2px;
    background: var(--border-color);
}

.todo-prio-bar.on[data-bar="0"] { background: #34c759; }
.todo-prio-bar.on[data-bar="1"] { background: #ff9500; }
.todo-prio-bar.on[data-bar="2"] { background: #ff3b30; }

/* Interactive (editor): bigger hit area + pointer. */
.todo-prio-bars.is-interactive {
    gap: 3px;
    cursor: pointer;
}

.todo-prio-bars.is-interactive .todo-prio-bar {
    width: 18px;
    height: 4px;
    cursor: pointer;
}

.todo-prio-bars.is-interactive .todo-prio-bar:hover {
    opacity: 0.7;
}

.todo-add-task,
.todo-add-column {
    margin-top: 10px;
    background: transparent;
    border: 1px dashed var(--border-color);
    /* Slightly dimmer than regular UI text — these are low-emphasis "+" affordances. */
    color: color-mix(in srgb, var(--ui-text-color) 62%, transparent);
    border-radius: 10px;
    padding: 8px;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    text-align: center;
}

/* "+ Add task" matches a real one-line task: same height + box (--todo-task-h)
   so it reads as a ghost task slot, not a generic button. The label is centred
   within that height. margin-top 6px + the list's 2px bottom padding = the same
   8px rhythm as the gap between task cards, so it doesn't sit lower than a task. */
.todo-add-task {
    box-sizing: border-box;
    min-height: var(--todo-task-h);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 8px 10px;
    margin-top: 6px;
}

/* The per-row "+ Add column" fills the whole slot to the right of the last
   column — full row height, not a 44px stub — so there's a large, easy click
   target there (it's hidden until you hover the row; see the reveal rules
   below). Label stays vertically centred in the tall button. */
.todo-add-column {
    flex: 0 0 var(--todo-col-width, 280px);
    margin-top: 0;
    align-self: stretch;
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.todo-add-task:hover,
.todo-add-column:hover {
    border-color: var(--brand-color);
    color: var(--brand-color);
}

/* Keep the "+ Add task" / "+ Add column" affordances out of sight until you
   hover the column / row they belong to, so an idle board reads clean. They
   only FADE (opacity) — the element keeps its box in the layout, so revealing
   one never changes the column height or reflows the row (no hover flicker). */
.todo-add-task,
.todo-add-column {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease;
}

.todo-column:hover .todo-add-task,
.todo-lane:hover .todo-add-column {
    opacity: 1;
    pointer-events: auto;
}

/* A lane with no real columns yet (e.g. a brand-new board) always shows its
   add-column, so the very first column is reachable without hunting for a
   hover target on an otherwise-empty row. */
.todo-lane:not(:has(.todo-column)) .todo-add-column {
    opacity: 1;
    pointer-events: auto;
}

/* An empty column always shows its "+ Add task" (it replaces the removed
   placeholder slot), so there's a clear target without needing to hover first.
   margin-top 2px lines the button up with where the first task sits (the list's
   own top padding), rather than a task row's 6px. */
.todo-column:has(.todo-task-list.is-empty) .todo-add-task {
    opacity: 1;
    pointer-events: auto;
    margin-top: 2px;
}

/* ...and keep that button directly under the header: push the reserved one-task
   slot (the empty list) BELOW it via flex order, so the column stays one-task
   tall but the button sits at the top, not at the bottom. */
.todo-column:has(.todo-task-list.is-empty) .todo-task-list.is-empty {
    order: 1;
}

/* Touch / no-hover devices (tablet, phone) can't reveal these on hover, so the
   buttons would stay invisible forever. Always show them there. Placed after the
   reveal rules so source order wins (the :hover rules never match on touch). */
@media (hover: none), (pointer: coarse) {
    .todo-add-task,
    .todo-add-column {
        opacity: 1;
        pointer-events: auto;
    }
}

/* ---- Inline task editor: compact single-row controls strip ---- */
.todo-editor {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--border-color);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    cursor: default;
}

/* When the editor IS the card's own meta row (UX #2: in-place editing of an
   existing task), it already carries .todo-task-meta's top margin/gap — so drop
   the extra divider + double margin to keep the controls reading as the same
   pills the user clicked, edited in place, not a separate strip below. */
.todo-task-meta.todo-editor {
    margin-top: 8px;
    padding-top: 0;
    border-top: none;
}

/* Date + assignee editors styled like the read-only display pills, so editing
   reads as inline/WYSIWYG rather than heavy form inputs. */
.todo-editor-due,
.todo-editor-assignee {
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--ui-text-color);
    background: var(--glass-bg);
    border: 1px solid var(--border-color);
    border-radius: 999px;
    padding: 2px 8px;
    /* Match heights: a date input renders taller than a text input by default
       (embedded picker), so pin both to the same explicit height + box model
       so the two pills line up exactly. Resetting the native appearance is what
       makes the date input actually honour `height` on mobile Safari (otherwise
       it keeps its taller intrinsic UA height and outgrows the assignee pill). */
    -webkit-appearance: none;
    appearance: none;
    box-sizing: border-box;
    height: 22px;
}

.todo-editor-due:focus,
.todo-editor-assignee:focus {
    outline: none;
    border-color: var(--brand-color);
}

.todo-editor-due {
    flex: 0 0 auto;
    min-width: 0;
}

/* Priority bars control in the editor — borderless, just the bars. */
.todo-editor-priority {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    padding: 2px;
}

.todo-editor-assignee {
    flex: 1 1 70px;
    min-width: 0;
    width: 70px;
}

/* ---- Inline structural editing (add/rename column, add task) ---- */
.todo-inline-input {
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-color);
    background: transparent;
    /* Seamless WYSIWYG — no boxed input / blue ring when naming a new task. */
    border: none;
    outline: none;
    border-radius: 8px;
    padding: 6px 8px;
    width: 100%;
    box-sizing: border-box;
}

/* Add-task WYSIWYG entry (contenteditable): seamless, multi-line, placeholder
   hint while empty. Inherits .todo-task-text (::first-line title styling). */
.todo-task-input {
    outline: none;
    min-height: 1.2em;
    width: 100%;
    box-sizing: border-box;
}

.todo-task-input:empty::before {
    content: attr(data-placeholder);
    color: var(--ui-text-color);
    opacity: 0.55;
    pointer-events: none;
}

.todo-column-placeholder {
    justify-content: flex-start;
}

.todo-column-placeholder .todo-inline-input {
    font-weight: 600;
}

/* Column-sized drop slot shown while dragging a whole column (feature 3) —
   mirrors the task .todo-drop-placeholder but column-width and full-height. */
.todo-column-placeholder.todo-column-drop {
    flex: 0 0 var(--todo-col-width, 280px);
    align-self: stretch;
    min-height: var(--todo-task-h);
    padding: 0;
    background: color-mix(in srgb, var(--brand-color) 10%, transparent);
    border: 1px dashed var(--brand-color);
    border-radius: 14px;
    box-shadow: none;
    backdrop-filter: none;
    /* Never a dragenter/leave target itself (see .todo-drop-placeholder). */
    pointer-events: none;
}

/* The column being dragged — pulled out of flow (not just dimmed) so the
   column-sized placeholder fills the freed slot without reflowing the row into a
   strobe. Applied one tick after dragstart so the drag image is captured first. */
.todo-column.todo-col-dragging {
    display: none;
}

/* The header is the column drag handle: hint with a move cursor (the title
   itself keeps the text I-beam for click-to-rename). */
.todo-column-header {
    cursor: grab;
}

.todo-column-header:active {
    cursor: grabbing;
}

.todo-task-placeholder {
    cursor: default;
    border-color: var(--brand-color);
    /* Same padding as a real .todo-task card so the WYSIWYG name input sits at
       the exact same inset/margins as the in-place editor (the card box-sizing +
       min-height keep the outer height equal to a one-line task regardless). */
    padding: 10px;
    /* Start at the height of a real one-line task (== the "+ Add task" button),
       not the bare 1.2em input, so opening the editor doesn't show a shorter
       card that then grows as you type. */
    box-sizing: border-box;
    min-height: var(--todo-task-h);
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* Inline two-step delete-column confirm in the column header. */
.todo-column-header.todo-column-confirm {
    gap: 6px;
}

.todo-confirm-msg {
    flex: 1 1 auto;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-color);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.todo-confirm-yes,
.todo-confirm-no {
    background: transparent;
    border: none;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    padding: 2px 6px;
    border-radius: 6px;
}

.todo-confirm-yes { color: #ff3b30; }
.todo-confirm-no { color: var(--ui-text-color); }
.todo-confirm-yes:hover,
.todo-confirm-no:hover { background: var(--bg-color); }

/* ---- Locked card (someone else is editing) ---- */
.todo-task.is-locked {
    cursor: default;
    opacity: 0.65;
    /* Read-only: can't be opened or dragged. */
    pointer-events: none;
}

.todo-task-editing {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 8px;
}

/* Column title being renamed by someone else: read-only + a dots indicator
   right after the title. */
.todo-column-title.is-locked {
    cursor: default;
    opacity: 0.7;
}

.todo-column-editing {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
}

/* "Someone is adding a task here" presence, shown above the + Add task button. */
.todo-add-presence {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 4px 0 2px;
    opacity: 0.8;
}

.todo-editing-label {
    font-size: 11px;
    font-weight: 500;
    color: var(--ui-text-color);
}

/* 3-dot typing animation. */
.todo-typing-dots {
    display: inline-flex;
    align-items: center;
    gap: 3px;
}

.todo-typing-dots i {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--brand-color);
    display: inline-block;
    animation: todo-typing-bounce 1.2s infinite ease-in-out both;
}

.todo-typing-dots i:nth-child(1) { animation-delay: -0.32s; }
.todo-typing-dots i:nth-child(2) { animation-delay: -0.16s; }
.todo-typing-dots i:nth-child(3) { animation-delay: 0s; }

@keyframes todo-typing-bounce {
    0%, 80%, 100% { transform: scale(0.5); opacity: 0.4; }
    40% { transform: scale(1); opacity: 1; }
}

/* ---- Auto-save countdown bar (idle-timeout grace) ---- */
.todo-autosave-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    /* Span the full editor width — within the flex-wrap meta row it drops onto
       its own line below the controls. */
    flex: 1 1 100%;
    width: 100%;
    box-sizing: border-box;
    margin-top: 8px;
    padding: 8px;
    border-radius: 8px;
    background: color-mix(in srgb, var(--brand-color) 12%, transparent);
    border: 1px solid var(--brand-color);
}

.todo-autosave-note {
    flex: 1 1 auto;
    font-size: 12px;
    font-weight: 500;
    color: var(--text-color);
}

.todo-autosave-bar button {
    font: inherit;
    font-size: 12px;
    font-weight: 500;
    border: none;
    border-radius: 8px;
    padding: 5px 9px;
    cursor: pointer;
}

.todo-autosave-save {
    background: var(--brand-color);
    color: white;
}

.todo-autosave-discard {
    background: #5a5a5c;
    color: white;
}

/* ---------------------------------------------------------------------------
   Offline / read-only affordances on the kanban. Mirrors the draw board's
   html.offline grey-out (main.css): the board stays readable + pannable, but
   every WRITE affordance is dimmed and click-dead. The JS already no-ops these
   actions offline (canWrite() now consults isReadOnly()); this is the visual
   half so a writable-looking control never lies about being live.
   --------------------------------------------------------------------------- */
html.offline #todo-board .todo-add-column,
html.offline #todo-board .todo-add-lane,
html.offline #todo-board .todo-add-task,
html.offline #todo-board .todo-column-delete,
html.offline #todo-board .todo-task-delete,
html.offline #todo-board .todo-task-check {
    pointer-events: none;
    opacity: 0.35;
    cursor: not-allowed;
}

/* Drag handles + the resize grip carry no meaning while writes are blocked —
   hide them (matching the draw board hiding its resize/drag handles offline).
   The column header doubles as a drag handle, so reset its grab cursor too. */
html.offline #todo-board .todo-col-resize {
    display: none !important;
}
html.offline #todo-board .todo-column-header,
html.offline #todo-board .todo-task,
html.offline #todo-board .todo-column-title,
html.offline #todo-board .todo-lane-title {
    cursor: default;
}

/* ===========================================================================
   Stitched (mobile) display mode
   ---------------------------------------------------------------------------
   An alternative to the free pan/zoom canvas for phones: ONE full-screen active
   lane (its columns become full-width pages you swipe between with scroll-snap,
   each column's task list scrolling vertically), with the OTHER lanes collapsed
   into thin tappable bars stacked above/below it. Activated per-user via the
   #display-mode-btn toggle (auto-on for touch/narrow). Every rule is scoped to
   [data-mode="todo"][data-display-mode="stitched"], so free mode, draw mode and
   the desktop kanban are completely untouched.
   =========================================================================== */

/* ---- The toggle button in the top bar ---- */
/* Mirrors the look of #copy-btn / #new-canvas-btn / #print-btn. Hidden unless
   we're in todo mode on a small / touch viewport (where it's actually useful). */
#display-mode-btn {
    display: none;
    background: #5a5a5c;
    color: white;
    border: none;
    padding: 6px 12px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s, transform 0.1s;
    align-items: center;
    gap: 6px;
    line-height: 1;
    white-space: nowrap;
    flex-shrink: 0;
}
#display-mode-btn svg { flex-shrink: 0; }
#display-mode-btn:hover { background: #7a7a7c; }
#display-mode-btn.is-stitched { background: var(--brand-color); }

/* Touch devices only — a narrow desktop window (fine pointer) must never surface
   the Mobile toggle, so stitched can't be entered (and then get stuck) there.
   Device emulation reports a coarse pointer, so it still shows for testing. */
@media (hover: none) and (pointer: coarse) {
    [data-mode="todo"] #display-mode-btn { display: inline-flex; }
}
/* Always expose the toggle while the stitched layout is actually active in todo
   mode — even on a wide desktop. A persisted "Mobile" choice (cnvs:display_mode)
   sticks across viewports by design, so without this the button would be hidden
   on desktop and the user couldn't switch back to the free canvas (a trap). */
html[data-mode="todo"][data-display-mode="stitched"] #display-mode-btn {
    display: inline-flex;
}
/* On touch the top bar is icon-only (matches copy/new/print) — drop the label. */
@media (hover: none) {
    #display-mode-btn .display-mode-label { display: none; }
}

/* ---- Scroller: native scroll layout replaces pan/zoom ---- */
[data-mode="todo"][data-display-mode="stitched"] .todo-scroller {
    /* transform is also cleared in applyTransform(); !important guards against a
       stale inline transform from a prior free-mode frame. */
    transform: none !important;
    width: 100vw;
    max-width: 100vw;
    height: 100dvh;
    box-sizing: border-box;
    padding: 64px 0 50px;       /* clear the top bar; reserve bottom for dots/icons */
    gap: 0;
    overflow: hidden;
    will-change: auto;
}

/* Header chrome lines up with the row frames: the Recents pill flush-left with
   the rows' left edge, the top-bar flush-right with their right edge (the rows
   sit at a 12px side inset — see the collapsed bars / add-row / column padding). */
html[data-mode="todo"][data-display-mode="stitched"] #recent-boards {
    left: 12px;
}
html[data-mode="todo"][data-display-mode="stitched"] #top-bar {
    right: 12px;
}

/* "+ Add row" stays available on mobile as a thin full-width bar at the bottom
   of the stacked rows (matching the collapsed-row bars' footprint). */
[data-mode="todo"][data-display-mode="stitched"] .todo-add-lane {
    display: block;
    flex: 0 0 auto;
    align-self: stretch;
    width: auto;
    height: 36px;
    margin: 3px 12px 0;
}

/* "+ Add row" spawns a brand-new lane whose first column is being named. That
   lane is a bare .todo-lane appended straight to the scroller (no lane-group
   wrapper, so the active/collapsed rules don't reach it) — style its name input
   as a full-width bar matching the collapsed row bars (.todo-stitch-bar): 12px
   side inset, ~40px tall, centred text — instead of a desktop-width glass column
   that's too tall with too little margin. */
[data-mode="todo"][data-display-mode="stitched"] .todo-scroller > .todo-lane {
    width: 100vw;
    box-sizing: border-box;
    padding: 0 12px;
    gap: 0;
}
[data-mode="todo"][data-display-mode="stitched"] .todo-scroller > .todo-lane > .todo-column-placeholder {
    flex: 1 1 auto;
    width: auto;
    max-width: none;
    box-sizing: border-box;
    height: 40px;
    min-height: 40px;
    margin: 3px 0;
    padding: 0 14px;
    border-radius: 10px;
    justify-content: center;
}
[data-mode="todo"][data-display-mode="stitched"] .todo-scroller > .todo-lane .todo-inline-input {
    padding: 0;
    font-size: 14px;
    font-weight: 600;
    /* The input inherits a collapsed line-height (via `font: inherit`), which
       shrinks the caret to a sliver even though glyphs type at 14px. A unitless
       line-height is ignored by the text-input control here, so pin the caret
       height with an EXPLICIT px box: 24px content height + matching line-height.
       The 40px bar centres it via justify-content. */
    height: 24px;
    line-height: 24px;
}

/* ---- Active lane: fills remaining height ---- */
[data-mode="todo"][data-display-mode="stitched"] .todo-lane-group.is-active-lane {
    flex: 1 1 auto;
    min-height: 0;
    width: 100vw;
    gap: 4px;
}

/* Row (lane) name lines up with the column titles below it (both sit at the
   12px column inset) and reads on the bare board background — no glass pill /
   hover box, so it looks like a plain heading over the row, not a control. */
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane > .todo-lane-header {
    padding: 0 12px;
}
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-lane-title {
    padding-left: 0;          /* the header's 12px inset already aligns it */
}
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-lane-title,
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-lane-title:hover,
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-lane-title.is-editing {
    background: transparent;
}

/* The active lane's columns become a horizontal scroll-snap pager. */
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane > .todo-lane {
    flex: 1 1 auto;
    min-height: 0;
    width: 100vw;
    gap: 0;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    align-items: stretch;
}

/* Each column is exactly one screen wide and full height, and is itself the
   vertical scroller: the task list grows to its content so "+ Add task" sits
   right under the last task (not pinned to the column floor). The big bottom
   padding reserves room for the floating bottom toolbar + page dots, so when the
   tasks overflow you can scroll "+ Add task" up clear of the toolbar. */
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-column {
    flex: 0 0 100vw;
    width: 100vw;
    max-width: 100vw;
    box-sizing: border-box;
    height: 100%;
    max-height: 100%;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 28px;
    scroll-snap-align: start;
    border-radius: 0;
    border-left: none;
    border-right: none;
}

/* Keep the column title visible while its tasks scroll under it. */
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-column-header {
    position: sticky;
    top: 0;
    z-index: 2;
    margin: 0 0 10px;
    padding-bottom: 6px;
    /* Use the column's own translucent glass (not the solid board bg) and blur
       whatever task scrolls beneath the sticky header so it reads as frosted. */
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
}

/* Soft fade just below the sticky header: a task scrolling up under it melts out
   through a glass→transparent gradient (+ light blur) instead of a hard cut. It
   sits in the header's bottom margin and scrolls with the (sticky) header. */
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-column-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 12px;
    background: linear-gradient(to bottom, var(--glass-bg), transparent);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    pointer-events: none;
}

/* Mirror it above the header: the column's top padding strip would otherwise
   show a hard edge where a task peeks in before the overflow clip. Extend the
   frosted glass upward (transparent→glass) so the top transition is as soft as
   the bottom. -12px reaches up over the column's 12px top padding. */
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-column-header::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 12px;
    background: linear-gradient(to top, var(--glass-bg), transparent);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    pointer-events: none;
}

/* "+ Add column" is the final full-width page (kept visible even on a narrow
   non-touch window, where the hover-reveal would otherwise leave a blank page). */
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-add-column {
    flex: 0 0 100vw;
    width: 100vw;
    scroll-snap-align: start;
    opacity: 1;
    pointer-events: auto;
}
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-add-task {
    opacity: 1;
    pointer-events: auto;
}

/* Roomier stitched viewports (≥760px wide — tablets, or the mobile layout pinned
   on a wide window): show TWO columns per screen instead of one. Halving the page
   width keeps the x-scroll-snap pager stepping a column at a time while two stay
   visible; a hairline between adjacent columns gives them back their separation
   (box-sizing:border-box keeps 2×50vw == 100vw exactly). */
@media (min-width: 760px) {
    [data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-column {
        flex-basis: 50vw;
        width: 50vw;
        max-width: 50vw;
    }
    [data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-column + .todo-column {
        border-left: 1px solid var(--border-color);
    }
    [data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-add-column {
        flex-basis: 50vw;
        width: 50vw;
    }
}

/* Wider still (≥1180px): step up to THREE columns per screen (the hairline
   separator from the ≥760px block above still applies). 3×33.333vw ≈ 100vw. */
@media (min-width: 1180px) {
    [data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-column {
        flex-basis: 33.3333vw;
        width: 33.3333vw;
        max-width: 33.3333vw;
    }
    [data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-add-column {
        flex-basis: 33.3333vw;
        width: 33.3333vw;
    }
}

/* Task list grows to its content (the column scrolls, not the list) so the
   "+ Add task" sibling lands directly under the last task. */
[data-mode="todo"][data-display-mode="stitched"] .is-active-lane .todo-task-list {
    flex: 0 0 auto;
    min-height: 0;
    overflow: visible;
}

/* The resize grip is meaningless in the fixed-width mobile pager. */
[data-mode="todo"][data-display-mode="stitched"] .todo-col-resize {
    display: none;
}

/* ---- Collapsed-row stacks: scroll within, capped at ~3 rows ----
   Wrappers built in applyStitchedLayout around the rows above / below the active
   one, so a board with many rows can't consume the screen vertically. */
[data-mode="todo"][data-display-mode="stitched"] .todo-stitch-above,
[data-mode="todo"][data-display-mode="stitched"] .todo-stitch-below {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    max-height: 140px;          /* ~3 collapsed row bars; scrolls beyond */
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}

/* On shorter viewports (≤900px tall) the active lane needs the height back, so
   cap the collapsed-row stacks ~1/4 lower (~2 bars instead of 3); the rest still
   scroll within the stack. */
@media (max-height: 900px) {
    [data-mode="todo"][data-display-mode="stitched"] .todo-stitch-above,
    [data-mode="todo"][data-display-mode="stitched"] .todo-stitch-below {
        max-height: 105px;
    }
}

/* Shorter still (≤740px tall): drop one more bar — another ~1/3 lower (~1 bar);
   the active lane gets the reclaimed height, the rest still scroll in-stack. */
@media (max-height: 740px) {
    [data-mode="todo"][data-display-mode="stitched"] .todo-stitch-above,
    [data-mode="todo"][data-display-mode="stitched"] .todo-stitch-below {
        max-height: 70px;
    }
}

/* ---- Collapsed lanes: just their summary bar ---- */
[data-mode="todo"][data-display-mode="stitched"] .todo-lane-group.is-collapsed-lane {
    flex: 0 0 auto;
    width: 100vw;
    gap: 0;
    padding: 0 12px;
    box-sizing: border-box;
}
[data-mode="todo"][data-display-mode="stitched"] .is-collapsed-lane > .todo-lane,
[data-mode="todo"][data-display-mode="stitched"] .is-collapsed-lane > .todo-lane-header {
    display: none;
}
[data-mode="todo"][data-display-mode="stitched"] .todo-stitch-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    box-sizing: border-box;
    width: 100%;
    height: 40px;
    margin: 3px 0;
    padding: 0 14px;
    background: var(--glass-bg);
    border: 1px solid var(--border-color);
    border-radius: 10px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
[data-mode="todo"][data-display-mode="stitched"] .todo-stitch-bar:hover {
    border-color: var(--brand-color);
}
.todo-stitch-bar-label {
    font-weight: 600;
    font-size: 14px;
    color: var(--ui-text-color);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.todo-stitch-bar-meta {
    flex: 0 0 auto;
    font-size: 12px;
    color: color-mix(in srgb, var(--ui-text-color) 60%, transparent);
}

/* ---- Bottom chrome: collapsible tools panel ----
   The global toolbar (#bottom-bar: colours / lock / delete / theme) is fixed at
   the bottom centre and would cover the stacked rows + page dots. In stitched
   mode it becomes a slide-up panel hidden by default, revealed by the floating
   "Tools" button (#tools-toggle) — same idea as the collapsible Recents rail. */
html[data-mode="todo"][data-display-mode="stitched"] #bottom-bar {
    left: 12px;
    right: 96px;            /* leave the bottom-right corner free for the icons */
    bottom: 8px;            /* same line as the Tools / "?" icons */
    max-width: none;
    /* Centre the tools (colours / lock / theme …) in the panel. `safe` falls back
       to start-alignment if they ever overflow the row, so nothing gets clipped
       out of reach of the horizontal scroll. */
    justify-content: safe center;
    overflow-x: auto;       /* swatches scroll if the row is too narrow */
    transition: transform 0.25s ease, opacity 0.2s ease;
    transform: translateY(calc(100% + 20px));   /* hidden below the edge */
    opacity: 0;
    pointer-events: none;
}
html[data-mode="todo"][data-display-mode="stitched"][data-tools-open] #bottom-bar {
    transform: translateY(0);
    opacity: 1;
    pointer-events: auto;
}

/* Slim the toolbar down to the Tools / "?" icons' height (~34px) in this view. */
html[data-mode="todo"][data-display-mode="stitched"] #bottom-bar {
    padding: 3px 10px;
    gap: 8px;
}
html[data-mode="todo"][data-display-mode="stitched"] #bottom-bar > #color-picker {
    gap: 8px;
}
html[data-mode="todo"][data-display-mode="stitched"] #theme-toggle,
html[data-mode="todo"][data-display-mode="stitched"] #eraser-toggle,
html[data-mode="todo"][data-display-mode="stitched"] #lock-btn,
html[data-mode="todo"][data-display-mode="stitched"] #undo-btn,
html[data-mode="todo"][data-display-mode="stitched"] #delete-board-btn {
    width: 28px;
    height: 28px;
}
html[data-mode="todo"][data-display-mode="stitched"] #theme-toggle .theme-icon,
html[data-mode="todo"][data-display-mode="stitched"] #eraser-toggle svg,
html[data-mode="todo"][data-display-mode="stitched"] #lock-btn svg,
html[data-mode="todo"][data-display-mode="stitched"] #undo-btn svg,
html[data-mode="todo"][data-display-mode="stitched"] #delete-board-btn svg {
    width: 16px;
    height: 16px;
}
html[data-mode="todo"][data-display-mode="stitched"] .color-btn {
    width: 20px;
    height: 20px;
}

/* The Tools + "?" buttons live together in the bottom-right corner, on the same
   line as the page dots and below the "+ Add row" bar. Tools sits just left of
   the help "?". Both hidden outside the stitched mobile kanban (tools entirely,
   help keeps its normal placement in other contexts). */
#tools-toggle { display: none; }
html[data-mode="todo"][data-display-mode="stitched"] #tools-toggle,
html[data-mode="todo"][data-display-mode="stitched"] #touch-help-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    bottom: 8px;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 1px solid var(--border-color);
    background: var(--glass-bg);
    backdrop-filter: blur(8px);
    color: var(--ui-text-color);
    box-shadow: var(--shadow);
    z-index: 1001;
    padding: 0;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
html[data-mode="todo"][data-display-mode="stitched"] #touch-help-btn { right: 14px; }
html[data-mode="todo"][data-display-mode="stitched"] #tools-toggle { right: 56px; }
html[data-mode="todo"][data-display-mode="stitched"] #tools-toggle:active,
html[data-mode="todo"][data-display-mode="stitched"] #touch-help-btn:active {
    transform: scale(0.95);
}
html[data-mode="todo"][data-display-mode="stitched"][data-tools-open] #tools-toggle {
    background: var(--brand-color);
    color: #fff;
}

/* ---- Page dots: at the bottom of the active column, above the lower rows ---- */
[data-mode="todo"][data-display-mode="stitched"] .todo-stitch-dots {
    flex: 0 0 auto;
    display: flex;
    justify-content: center;
    gap: 6px;
    margin: 0;
    padding: 6px 0 2px;
}
.todo-stitch-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--ui-text-color) 30%, transparent);
    transition: background 0.15s ease, transform 0.15s ease;
}
.todo-stitch-dot.is-active {
    background: var(--brand-color);
    transform: scale(1.3);
}
