@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(var(--btn-size) + 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). So it goes to the row's own end, which is
     also where fizzy keeps a step's rename and delete. 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: calc(var(--btn-size) + var(--inline-space-half));
  }

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

  /* 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 no rows at all is no
     list, so the label baselines against the placeholder. */
  .todos--subtasks .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);
  }
}
