@layer components {
  /* ——— To-dos ———
     BC's row (user, 2026-08-10): no lines, no frills — a circle, a line of
     text, the people by name. Done stays where it is and takes the strike;
     the ⋯ waits in the left gutter. The composer's grid is todo-form.css. */
  .todos {
    /* Rows retune density by the token, never by new padding rules. */
    --block-space: 0.5rem;
  }

  .todo {
    /* The circle's diameter — subtasks and the headline retune it, and the
       first-line seat below reads it, so the two can never disagree. */
    --check-size: 1.25rem;
    position: relative;
    display: flex;
    /* Start-aligned with the circle seated on the FIRST line, not centred:
       a wrapped title floated its circle between the lines on a phone
       (user, 2026-08-11). For one line the two are pixel-identical. */
    align-items: flex-start;
    gap: var(--inline-space);
    padding-block: var(--block-space-half);
    color: var(--color-ink);
    text-decoration: none;
  }

  /* The composer rows keep centring: their line is an input, whose box the
     lh arithmetic below cannot see — and a one-line control never wraps. */
  .todo--composer,
  .todo--intake {
    align-items: center;

    .todo-check {
      margin-block-start: 0;
    }
  }

  /* The tick's button_to wrapper must not be the flex item: a form is an
     inline-layout box whose line leaves descender space under the button,
     floating the circle 2–3px above true centre — measured, not guessed
     (user, 2026-08-10). display:contents makes the button itself the item. */
  .todo > form {
    display: contents;
  }

  /* The line itself is a link to the to-do's own page; done fades it. The
     strike is painted, not text-decoration, so completing draws it in left
     to right: the tick's stream morphs aria-pressed onto the element already
     standing, and the background-size transition runs
     (Todos::CompletionsController).

     The strike rides an INLINE — the row's inner span (todos/_todo), the
     headline's rename link — never the description itself: that one is a
     flex item of the row, so a block, and a block never fragments per line.
     One gradient on the block was wrong twice over on a wrapped title — 55%
     of two lines is the gap between them, and the box runs wider than the
     words, so the line struck paper past the text (user, 2026-08-30, both
     measured). An inline fragments, clone hands each fragment its own
     painting area, and every line takes the strike through its own words
     and no further. */
  .todo__description {
    color: inherit;
    text-decoration: none;
    transition: color var(--speed) var(--ease-out);
  }

  .todo__title,
  .todo__description > a {
    background: linear-gradient(currentColor, currentColor) no-repeat 0 55% / 0% 1px;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
    transition: background-size var(--speed) var(--ease-out);
  }

  .todo:has(.todo-check[aria-pressed="true"]) .todo__description {
    color: var(--color-ink-faint);
  }

  .todo:has(.todo-check[aria-pressed="true"]) :is(.todo__title, .todo__description > a) {
    background-size: 100% 1px;
  }

  /* The circle is the whole control — BC's size, kept round. A button and not
     a checkbox, though it reads as one: a checkbox would need JavaScript to
     reach the server, and aria-pressed carries the state a checkbox would
     have carried in :checked. */
  .todo-check {
    appearance: none;
    inline-size: var(--check-size);
    block-size: var(--check-size);
    /* The first-line seat: centred against one line-box of the row's own
       type, wherever the token puts the diameter. */
    margin-block-start: calc((1lh - var(--check-size)) / 2);
    padding: 0;
    flex-shrink: 0;
    background-color: transparent;
    border: 1.5px solid var(--color-ink-faint);
    border-radius: 50%;
    cursor: pointer;

    &:hover {
      border-color: var(--color-ink-subtle);
    }

    /* The tick is drawn white and blended with `difference`, so it always
       resolves to the inverse of the disc it sits on. A literal white tick
       would disappear in dark mode, where --color-ink is near-white. */
    &[aria-pressed="true"] {
      background: var(--color-ink) no-repeat center / 60%
        url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3L12.5 5"/></svg>');
      background-blend-mode: difference;
      border-color: var(--color-ink);
    }
  }

  /* `.todo__due` moved to due-dates.css when the board card wanted the same
     voice for the same datum — one rule, two selectors, rather than two rules
     drifting apart. */

  /* Everything the row says beside its title, in ONE pill — the people by
     face and name, the date, the subtask progress, the thread mark (BC's
     chain, user 2026-08-10). Faces retuned down to the row's own height. */
  .todo__meta {
    --avatar-size: 1.25rem;
    display: inline-flex;
    align-items: center;
    gap: var(--inline-space);
    padding-inline: var(--inline-space-half);
    padding-block: 2px;
    background-color: var(--color-surface);
    border-radius: var(--border-radius);
    font-size: var(--text-small);
    color: var(--color-ink-subtle);
  }

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

  /* The ⋯ in the left gutter, revealed by its row (BC). A transition only
     because something appears (HANSHI, Motion). The open popover keeps its
     anchor lit after the pointer wanders off. */
  /* Invisible but hoverable, deliberately: the box is the approach path, and
     a pointer entering the gutter where the ⋯ will appear must reveal the ⋯
     — parked at pointer-events: none, that strip was dead, so the row had to
     be crossed first and the gutter come back to (user, 2026-09-02). Hover
     anywhere on this child sets the row's :hover, box or no box, because
     :hover follows the DOM. Safe to leave live at rest because nothing else
     ever sits under it — the gutter is the shell's margin, and both
     displaced seats below stand on padding the row reserves — and a pointer
     over it lights the button through the row's :hover, so it is never
     invisible and clickable at once. (It rested at none from 2026-08-10,
     when an invisible ⋯ still over the heading rename's ✕ ate its click.) */
  .todo__menu {
    position: absolute;
    /* The shell's gutter (layout.css), so the sheet always leaves the room. */
    inset-inline-start: calc(-1 * var(--main-gutter));
    /* The box spans the whole gutter, ending flush at the row's edge. */
    inline-size: var(--main-gutter);
    opacity: 0;
    transition: opacity var(--speed-quick) var(--ease-out);
  }

  .todo:hover .todo__menu,
  .todo:focus-within .todo__menu,
  .todo__menu:has(:popover-open) {
    opacity: 1;
  }

  /* A list's header carries the same gutter ⋯ as its rows (user, 2026-08-10,
     matching BC). Descendant, not child: the header lives inside the
     section's edit frame. */
  .todolist header {
    position: relative;
  }

  .todolist header:hover .todo__menu,
  .todolist header:focus-within .todo__menu {
    opacity: 1;
  }

  /* No hover to reveal it, so it is simply there — hover-reveals.css says so
     once, with every other control that waits for a pointer. */

  /* On a tight sheet the row's meta drops to its own line under the title,
     still the same pill, tucked close (user, 2026-08-11) — side by side it
     crushed a phone's titles into wraps and folded its own date in half.
     A container query, not a viewport one: #main narrows just the same when
     the notification panel opens. */
  @container main (max-width: 70ch) {
    /* Not the composer rows: their line is a 100%-wide .input, and wrap
       would put it under its own circle. A control row never needs to. */
    .todo:not(.todo--composer, .todo--intake) {
      flex-wrap: wrap;
      row-gap: 0;
    }

    /* Wrap is for the meta pill alone. A long title's natural flex size is
       its whole unwrapped width, so once wrap is allowed it jumped to the
       NEXT flex line — a full-width title under a circle alone on its own
       (user, 2026-08-30). Basis 0 always fits beside the circle, the grow
       takes the rest of the line, and the text wraps inside its own box —
       the circle seated on the first line, as everywhere else. */
    .todo__description {
      flex: 1 1 0;
    }

    /* The line break is a zero-tall pseudo ordered between title and pill —
       basis-100% on the pill itself would stretch its wash into a bar. */
    .todo:has(> .todo__meta)::after {
      content: "";
      order: 1;
      flex-basis: 100%;
    }

    .todo__meta {
      order: 2;
      margin-inline-start: calc(var(--check-size) + var(--inline-space));
      margin-block-start: calc(var(--block-space-half) / 2);
    }
  }

  /* There is no gutter to wait in once the sheet goes full-bleed: #main drops
     to 1ch of padding, so -2.25rem puts the ⋯ at x=-26 on a 390px screen and
     body's overflow clip means nothing can scroll to it — visible under
     `hover: none`, and unreachable. Measured, not guessed.

     So on a narrow shell it moves to the row's far end instead — still
     absolute, on the first line, with the row reserving its width so a long
     title wraps short of it rather than running beneath. (It stood in the
     flow with `order` for a day, but a wrapping row then pushed the ⋯ onto a
     whole line of its own.) A viewport query and not a container one: the
     gutter is the shell's to give, and #main goes full-bleed at exactly this
     width. */
  @media (width < 35rem) {
    :is(.todo, .todolist header):has(> .todo__menu) {
      padding-inline-end: calc(2.65em + var(--inline-space-half));
    }

    .todo__menu {
      inset-inline-start: auto;
      inset-inline-end: 0;
      inline-size: auto;
    }
  }

  /* Same displacement, second cause: a row inside a form grid has no gutter to
     hang in either, because .todo-fields' label column occupies exactly the
     space the ⋯ reaches into — a checklist row's landed on top of the word
     "Subtasks" (user, 2026-08-29). It stood at the row's far end after that,
     a long way for the pointer from a short title, so now it stands in the
     row's own flow, right after what the row says (user, 2026-10-12). Hidden
     at rest, it holds its space at the end, where nothing follows it. One
     line tall with the circle centred, so the row keeps its height. Not
     folded into the query above: that one is the shell taking its gutter
     back at a width, this one is true of a checklist row at every width. */
  .todo-fields .todo:has(> .todo__menu) {
    padding-inline-end: 0;
  }

  .todo-fields .todo__menu {
    position: static;
    order: 3;
    display: flex;
    align-items: center;
    inline-size: auto;
    block-size: 1lh;
  }

  /* The show page's headline row: the same anatomy at title scale, the
     circle centred on the title's own line. */
  .todo--headline {
    --check-size: 1.5rem;
    align-items: center;

    .todo-check {
      margin-block-start: 0;
    }

    /* The heading is a rename frame now, so `.inline-rename` owns how its link
       reads — ink, no underline until hover. What stays here is the seat: the
       h1 gives up its own margins so the circle centres on the title's line. */
    h1 {
      margin-block: 0;
    }
  }

  /* Subtasks are the same rows, one size quieter — and so is the intake
     under them, or its ghost circle stood 2px wider than the rows' and its
     words 4px further in (owner, 2026-10-20, on a record's to-dos). No rows
     at all is no list, so the label baselines against the placeholder. */
  .todos--subtasks .todo,
  .todos--subtasks ~ .todo-intake .todo {
    --check-size: 1rem;
  }

  .todos--subtasks:not(:has(li)) {
    display: none;
  }

  /* An empty set draws the blank slate under the intake; the first capture
     answers with a stream that lands one row and redraws nothing else, so
     the slate has to stand down on its own when a row exists. */
  .todoset-lists:has(.todos li) .blank-slate {
    display: none;
  }

  /* ——— dragging ———
     Grab a list by its header and every list folds to its header, so the
     whole set fits under the pointer (BC, user 2026-08-10). */
  .todoset-lists:has(.todolist.is-dragging) :is(.todos, .todo-intake, .todolist__description) {
    display: none;
  }

  /* Mid-drag, links and buttons must not eat the dragover: the container
     under the pointer is the drop target, never a control inside it
     (writebook's arrangement rule). */
  .todoset-lists:has(.is-dragging) :is(a, button, input) {
    pointer-events: none;
  }

  /* No dashed edge and no wash on any target here — a column on the board
     needs its highlight, but on this page the moving row does the pointing
     (user, 2026-08-10). A variant retunes, it does not restate. */
  .todoset-lists .is-target,
  .todoset-lists.is-target {
    outline: none;
    background: none;
  }

  /* The one mark a drag carries: the travelling row wears a subtle wash
     behind its faintness (user, 2026-08-10, after BC). */
  .todoset-lists .is-dragging {
    background-color: var(--color-surface);
    border-radius: var(--border-radius);
  }
}
