@layer components {
  /* No dividing lines. A list of short sentences separated by rules reads as a
     table of data; separated by space it reads as work. The rail of task
     references does the alignment a rule would otherwise be doing. */
  .task-list { list-style: none; margin: 0; padding: 0; }

  .task {
    position: relative;
    display: grid;
    /* The title takes the slack, and everything after it is a rail sized to
       what it holds — so the dates and the faces still line up down the page,
       and the space between a short title and its date is the only thing that
       varies. The trailing column used to be 1fr, which meant the row's spare
       width pooled to the right of everything and the title was capped at
       27rem while 120px sat empty past the faces. */
    grid-template-columns:
      var(--rail-status) var(--rail) minmax(0, 1fr)
      var(--rail-when) var(--rail-who) var(--rail-pull);
    align-items: center;
    column-gap: var(--space-2);
    row-gap: var(--space);
    min-height: var(--row-h);
    padding: var(--space) var(--space-2);
    margin-inline: calc(var(--space-2) * -1);
    border-radius: var(--radius);
    transition: background var(--transition);
  }

  /* Inbox and Anytime show no date on any row, so they do not keep a
     rail for one — the title takes it. See TasksHelper#task_list_class.

     Bounded to the wide layout on purpose. A media query adds no specificity,
     so an unbounded `.task-list--dateless .task` outranks the phone rule below
     it and puts a five-rail desktop grid on a 390px screen — which is exactly
     what it did, with the pull landing on top of the reference. */
  @media (width > 48rem) {
    .task-list--dateless .task {
      grid-template-columns:
        var(--rail-status) var(--rail) minmax(0, 1fr)
        var(--rail-who) var(--rail-pull);
    }
  }

  .task-list--dateless .task__when { display: none; }

  .task:hover { background: var(--surface-hover); }
  .task:has(.task__link:focus-visible) { background: var(--surface-hover); }

  /* One real link, stretched over the whole row, so the click target is the row
     and the tab stop is still a single sensible thing. */
  .task__link {
    display: block;
    min-width: 0;
    color: var(--ink);
    text-decoration: none;
    font-size: var(--text-md);
    letter-spacing: var(--tracking-tight);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .task__link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
  .task__link:hover { text-decoration: underline; text-decoration-thickness: 1.5px; }

  .task__ref {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    letter-spacing: var(--tracking-label);
    color: var(--ink-faint);
    white-space: nowrap;
  }

  /* Status and date share a cell and are set flush right, so the dates form a
     column whatever is or is not next to them. */
  .task__when {
    display: flex;
    align-items: baseline;
    justify-content: flex-end;
    gap: var(--space);
    min-width: 0;
  }

  /* Who has it, and who is behind them. A button, because it is also how the
     work is handed on — the row's own link covers everything underneath, so
     anything pressable on the row has to sit above it. */
  .task__who {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    min-width: 0;
    padding: var(--space) 0 var(--space) var(--space);
    margin-right: calc(var(--space) * -1);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--ink-muted);
    cursor: pointer;
    position: relative;
    z-index: 1;
  }

  .task__who:hover .faces__empty { color: var(--ink-muted); }

  .task__date {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    color: var(--ink-muted);
    white-space: nowrap;
  }

  /* Overdue is the only thing in a list allowed to shout, and it whispers. */
  .task__date--overdue { color: var(--warn); font-weight: var(--weight-medium); }

  .task--closed .task__link { color: var(--ink-muted); text-decoration: line-through; text-decoration-color: var(--line-strong); }

  /* The progress mark, where the drag handle used to be. A circle that fills
     up — one drawing, see TasksHelper::MARK, with the parts it shows and how
     full its ring is decided below — and a button, because the state of a task
     is the thing most often changed about it and opening a page to change it
     is three clicks for one decision.

     Above the row's stretched link, like everything else pressable here. */
  .task__status {
    display: grid;
    place-items: center;
    width: var(--rail-status);
    height: var(--control-h-sm);
    padding: 0;
    border: 0;
    background: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    position: relative;
    z-index: 1;
    color: var(--ink-faint);
    transition: color var(--transition), var(--responds);
  }

  /* Which parts of the mark this status shows, and how full its ring is.
     TasksHelper::MARK draws all of them every time; these decide.

     20.42 is the fill circle's circumference and the only raw number in this
     file. It is geometry rather than design — it comes off the r=3.25 in the
     helper and changes only if that does — so it is not a token, and it is
     written out rather than hidden behind one that would suggest it could be
     tuned. Half of it is the halfway ring In progress has always drawn. */
  .mark__fill {
    stroke-dasharray: 20.42;
    stroke-dashoffset: 20.42;
    transition: stroke-dashoffset var(--transition-slow);
  }

  .mark__dot,
  .mark__tick,
  .mark__cross {
    opacity: 0;
    transition: opacity var(--transition-enter);
  }

  .task__status--captured  .mark__dot   { opacity: 1; }
  .task__status--done      .mark__tick  { opacity: 1; }
  .task__status--cancelled .mark__cross { opacity: 1; }

  /* Nobody has taken this on, drawn the way an empty seat is (see
     .faces__empty): dashed means not yet. */
  .task__status--waiting_on .mark__track { stroke-dasharray: 2.7 2.7; }

  .task__status--in_progress .mark__fill { stroke-dashoffset: 10.21; }
  .task__status--done        .mark__fill { stroke-dashoffset: 0; }

  /* The tick is drawn in the page's own colour on top of the disc, so it does
     not need holding back until the ring has filled: until there is something
     under it, it is canvas on canvas. The fill arriving is what reveals it. */

  /* New is a touch more present than Todo — it is the one status that is
     asking for something. Not a signal colour: those are spoken for, and an
     inbox that shouts is an inbox people stop opening. */
  .task__status--captured    { color: var(--ink-muted); }
  .task__status--in_progress { color: var(--accent); }
  .task__status--waiting_on  { color: var(--warn); }
  .task__status--done        { color: var(--go); }
  .task__status--cancelled   { color: var(--ink-faint); }

  .task__status:hover { color: var(--ink); }
  .task__status--in_progress:hover { color: var(--accent); }
  .task__status--waiting_on:hover  { color: var(--warn); }
  .task__status--done:hover        { color: var(--go); }

  .task--dragging { opacity: 0.4; }

  /* Coming and going (arrivals_controller)

     A row arriving is news — somebody captured something, or a broadcast
     brought somebody else's — so it takes the arrival clock and drops in from
     eight pixels above. A row leaving is not news, so it takes the exit clock,
     which is shorter: nobody needs to watch something they have finished with.

     `arriving` and `leaving` are set by the controller and are deliberately
     not BEM. They are one state, applied to whatever a list happens to hold —
     a task here, a comment on the timeline, a notification on the inbox — and
     each component says what the state means to it.

     The height is inline, set by the controller, because it is a measurement
     and only the controller can take it. Everything else about closing up is
     here, including the two the row would otherwise refuse to give: its
     min-height, and the padding that would leave sixteen pixels of nothing
     where the row was. */
  .task.leaving {
    overflow: hidden;
    min-height: 0;
    padding-block: 0;
    opacity: 0;
    transition: height var(--transition-exit),
                min-height var(--transition-exit),
                padding-block var(--transition-exit),
                opacity var(--transition-exit);
  }

  @media (prefers-reduced-motion: no-preference) {
    .task.arriving { animation: task-in var(--transition-enter); }
  }

  @media (prefers-reduced-motion: reduce) {
    .task.arriving { animation: fade-in var(--transition-enter); }
  }

  @keyframes task-in {
    from { opacity: 0; translate: 0 calc(var(--space) * -1); }
  }

  /* The pull (design scope §7). It sits in the row's trailing column, which was
     a spacer, so nothing else on the row moves to make room for it.

     Quiet until the row is reached for, like the comment actions: on Today every
     row would otherwise carry a permanent "NOT TODAY", which is a lot of shouting
     about a list whose whole premise is that everything on it is already yours.
     On touch there is no hover to reach with, so it stays. */
  .task__focus {
    /* The last column, counted from the end: a dateless list has five of them
       rather than six, and the pull is at the end of both. */
    grid-column: -2 / -1;
    grid-row: 1;
    justify-self: end;
    position: relative;
    z-index: 1;
  }

  .task__pull {
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    /* "Not today" wraps to NOT above TODAY; the rail is cut to the wider of
       the two words (--rail-pull) and the break falls out of that. Ranged
       right so both lines finish on the row's own right edge rather than
       ragging away from it, and set tight, because body leading on a
       two-line label of 14px caps is a gap you read as a gap. */
    text-align: right;
    line-height: var(--leading-tight);
    color: var(--ink-faint);
    background: none;
    border: 0;
    padding: var(--space) 0 var(--space) var(--space-2);
    cursor: pointer;
    opacity: 0;
    /* Eight pixels of somewhere to come from. A reveal that only fades reads
       as a light being switched on; one that travels reads as something
       arriving, and above 80rem this control genuinely does arrive — it lives
       out in the page's own margin, so moving away from the row is a literal
       description of where it goes.

       `translate` rather than a transform, because --responds already spends
       `transform` on the press and one property cannot hold two durations.
       The opacity leads and the travel trails, which is what stops it reading
       as a slide. */
    translate: calc(var(--space) * -1) 0;
    transition: opacity var(--transition), color var(--transition),
                translate var(--transition-enter), var(--responds);
  }

  .task:hover .task__pull,
  .task:focus-within .task__pull { opacity: 1; translate: none; }

  .task__pull:hover { color: var(--accent); }
  .task__pull--on:hover { color: var(--warn); }

  /* The pull, moved out of the row

     Inside the row the pull was a rail the title had to pay for. Out here it
     stands in the page's own margin, beside the row rather than in it, and the
     list gets the whole 92px back — the track and the gap that went with it.
     The faces become the last thing in the row, which is where a row of rails
     should end: on the person, not on a control.

     Only the box moves. The control stays exactly where it was in the DOM, so
     it is still the Turbo Stream target, still the next tab stop after the row,
     and still inside .task — which is what keeps `.task:hover .task__pull`
     firing while you reach for it, even though what you are reaching for is no
     longer over the row.

     And only where there is an outside to move it to. The column is --column
     wide and centred, so the space beside it is 50vw - 528px: 48px at 72rem,
     112px here. Below this the control is wider than the margin it would stand
     in and the row would push a horizontal scrollbar onto the page, so there
     it stays a rail.

     The breakpoint is set by the touch pill, not by the pointer label — 96px
     against 76px — because it is the wider of the two and both are laid out
     by this one rule. At 78rem the pointer branch cleared it by 21px and the
     touch branch by 1px. */
  @media (width >= 80rem) {
    .task {
      grid-template-columns:
        var(--rail-status) var(--rail) minmax(0, 1fr)
        var(--rail-when) var(--rail-who);
    }

    .task-list--dateless .task {
      grid-template-columns:
        var(--rail-status) var(--rail) minmax(0, 1fr) var(--rail-who);
    }

    .task__focus {
      position: absolute;
      /* Both placements back to auto, and this is the whole trick. An
         absolutely positioned grid child still gets its containing block from
         its grid placement, so with `-2 / -1` left over from the in-row rule
         `left: 100%` measures the *faces* column and lands the control 16px
         short, back inside the row. Only `auto` on both makes the containing
         block the row itself. */
      grid-column: auto;
      grid-row: auto;
      left: 100%;
      top: 0;
      bottom: 0;
      /* Width said rather than inferred. An absolutely positioned box with a
         left and no right shrinks to min-content, which for this label happens
         to be about right and would silently stop being right the day the
         wording changes. The rail is gone from the grid but it is still the
         measurement the control was cut to, so it is still the width. */
      width: var(--rail-pull);
      display: flex;
      align-items: center;
    }

    /* Ranged left now: out here the label reads away from the row it belongs
       to, so it should start against it rather than end against nothing. */
    .task__pull { text-align: left; }

    /* The overhang has to go with the rail. The -8px was the button reaching
       into the gap before the pull — free hit area, invisible with 96px of
       control sitting after it. With the pull gone the faces are the last
       thing in the row, and that same -8px was eating half the row's own
       right padding: they finished 8px from the edge where everything on the
       left starts 16px in. Squared up, the row is padded the same on both
       sides. */
    .task__who { margin-right: 0; }
  }

  /* On touch there is no hover to reveal it with, so it is always there — and
     once it is always there it has to read as a control rather than as a second
     date. A quiet outline is enough; it is still the least important thing on
     the row. */
  @media (hover: none) {
    .task__pull {
      opacity: 1;
      /* Never revealed, so never travelled: the hover rule that would have put
         it back is the one rule this branch exists because there is no. */
      translate: none;
      padding: var(--space) var(--space-2);
      border: 1px solid var(--line-strong);
      border-radius: var(--radius-pill);
      min-height: var(--control-h-sm);
    }

    .task__pull--on { color: var(--accent); border-color: var(--accent); }
  }

  /* The row on a phone (M7, D-13). The desktop row is five fixed rails, which
     add up to more than a phone has: the title column is the one that gives, and
     at 390px it gives entirely — the most important thing on the row disappears
     while the assignee hangs off the right-hand edge.

     So the rails become three lines, in the order they are needed: what it is
     called, then who and when, with the reference and the pull on the line
     above. The title is allowed two lines here rather than one, because a
     45-character cap on a 390px screen is not a cap, it is a truncation of
     everything. */
  @media (max-width: 48rem) {
    .task {
      grid-template-columns: var(--rail-status) minmax(0, 1fr) auto;
      column-gap: var(--space);
      row-gap: var(--space);
      padding-block: var(--space-2);
    }

    .task__ref   { grid-column: 2; grid-row: 1; }
    .task__focus { grid-column: 3; grid-row: 1; }

    .task__link {
      grid-column: 2 / -1;
      grid-row: 2;
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      white-space: normal;
    }

    .task__when {
      grid-column: 2;
      grid-row: 3;
      justify-content: flex-start;
    }

    .waiting-note { grid-column: 2 / -1; grid-row: 4; }

    /* The status mark keeps its rail on a phone. Reordering does not: a drag
       here is the HTML5 kind, which a touch screen does not fire at all, and
       there is no longer a handle to hide — the whole row was the handle and
       on touch it simply does not answer. Known gap, written up in the README.

       It leads each row rather than sitting beside the reference, because on
       three lines the thing that says what state the work is in belongs at the
       top left where reading starts. */
    .task__status { grid-column: 1; grid-row: 1 / 3; align-self: start; }
    .task__who { grid-column: 3; grid-row: 3; }
  }
}
