/**
 * History Timeline (TL-5/TL-6/TL-7).
 *
 * Everything here is `.pb-tl-*`. The frame keeps `.pb-history-overlay` /
 * `.pb-history-modal`, which are SHARED with the project switcher and asserted
 * by `modal-convergence-tests.js` — so this file forks nothing the kit owns and
 * `builder.css`'s restored `.pb-history-*` fallback block is scoped away from
 * `.pb-tl-modal`.
 *
 * No new `--pb-*` token is minted here and no new attention keyframe: the
 * design-tokens ledger for both only shrinks.
 */

/* ── the frame ───────────────────────────────────────────────────────────── */

/**
 * KEEPING A SHARED CLASS INHERITS THAT CLASS'S LAYOUT, and only the geometry
 * harness could have said so.
 *
 * §0.4 requires `.pb-history-modal` on this frame — it is shared with the
 * project switcher and `modal-convergence-tests.js` asserts it, deliberately,
 * so that a future rename is an explicit edit. What comes with it is two rules
 * written for the dialog this replaces:
 *
 *   · `auth-and-projects.css` `.pb-history-modal { max-width: 560px }` — a
 *     single-class rule that BEATS `--pb-modal-max-w`, so `maxWidth: 940`
 *     silently did nothing and the dialog rendered at 560.
 *   · the restored fallback rule, now scoped as
 *     `.pb-history-modal:not(.pb-tl-modal) .pb-modal-body`, would otherwise
 *     stack this split into a column and force its overflow/max-height. The
 *     `flex-direction: row` below is kept anyway: it is one line, it is what
 *     this surface actually wants, and re-deriving it from source order is how
 *     the next reader reintroduces the bug.
 *
 * jsdom passed all 33 behavioural tests through both. The switcher's own rule
 * is untouched; it is overridden here, at equal specificity, on source order —
 * this file is the last stylesheet in `index.html` for exactly that reason.
 */
/* The fallback MUST be `1000px` — the same one `builder.css` declares. CK-5
   fails a token that resolves to two different values depending on which
   stylesheet the reader happens to be in, and it is right to: `createModal`
   always writes the property here, so the fallback is never the rendered
   value and a second number would be pure misinformation. */
.pb-tl-modal {
  max-width: var(--pb-modal-max-w, 1000px);
  /**
   * A FIXED HEIGHT, and it is the whole point of the frame.
   *
   * `.pb-modal` declares `max-height: 85dvh` and no height, so the card is
   * sized by its content: the dialog grew and shrank as you scrubbed — a
   * three-row filter result and a twenty-row one are different dialogs, the
   * poster's own height moved it again, and the commit bar walked up and down
   * the screen under the pointer that was about to click it. A surface you
   * navigate with ↑/↓ must not move while you navigate it.
   *
   * 620px is the prototype's number. `max-height` still wins on a short
   * viewport, which is what keeps the footer reachable.
   */
  height: 620px;
  /* The grid below clips; without this the rail's background paints past the
     card's rounded corners. */
  overflow: hidden;
}

.pb-tl-storage-near .pb-modal-context {
  color: var(--pb-warning-text);
}

/**
 * THE SPLIT IS A GRID, and the footer is INSIDE it.
 *
 * The prototype's shape is `360px 1fr` × `1fr auto` with the list spanning both
 * rows, so the rail runs to the bottom of the dialog and the commit bar sits
 * under the PREVIEW only. A footer left where the kit puts it — a sibling of
 * the split — draws its top border straight across the list as well, which
 * reads as the list ending early. `index.js` moves the footer into this
 * container; the two halves of that decision are the DOM move and this grid.
 *
 * The 360px literal is repeated from `rail.width` because `--pb-modal-rail-w`
 * is written on the RAIL, and a custom property set on a child cannot size the
 * track that lays that child out.
 */
/**
 * THE DIVIDER MOVES BELOW THE COMMAND ROW, because that is what the command row
 * is: head matter. The kit draws its rule on `.pb-modal-header`, and this
 * surface inserts the chips + filter BETWEEN the header and the split — so the
 * kit's line cut the dialog's head in half, separating the scope chips from the
 * title they scope and leaving the list to start at no line at all. The
 * prototype has exactly one rule and it is on the split.
 */
.pb-tl-modal .pb-modal-header {
  border-bottom: 0;
}

.pb-tl-modal .pb-modal-body-split {
  display: grid;
  grid-template-columns: 360px minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 0;
  max-height: none;
  min-height: 0;
  border-top: 1px solid var(--pb-border-soft);
}

.pb-tl-modal .pb-modal-rail {
  grid-column: 1;
  grid-row: 1 / 3;
}

.pb-tl-modal .pb-modal-body-main {
  grid-column: 2;
  grid-row: 1;
}

.pb-tl-modal .pb-modal-footer {
  grid-column: 2;
  grid-row: 2;
  /* `-soft`, like the two rules above it — three dividers inside one card, and
     the commit bar's is the least load-bearing of them. */
  border-top: 1px solid var(--pb-border-soft);
}

/**
 * THE SAME TRAP THE KIT ALREADY DOCUMENTS, ONE LEVEL DEEPER — and the geometry
 * harness is what caught it, exactly as `builder.css`'s own comment predicts.
 *
 * `.pb-modal-body-split` is `flex: 1 1 auto` with NO height, so its height comes
 * from content. Making the rail a COLUMN flex container and giving its scroller
 * `flex: 1 1 0` meant the scroller contributed nothing to that content height:
 * the rail measured the command row and the list collapsed to zero, with the
 * filter input and the rail itself intercepting every pointer event aimed at a
 * row. jsdom passed all 33 tests through it.
 *
 * So the scroller is `flex: 1 1 auto` (it contributes its content height and
 * still shrinks under the modal's max-height), the rail drops the kit's padding
 * and overflow because its children own both, and `min-height: 0` sits on the
 * child that scrolls — where the kit's comment says it belongs.
 */
.pb-tl-modal .pb-modal-rail {
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 0;
  overflow: hidden;
  /* `-soft`, matching the prototype: a full-strength divider between two panes
     of one dialog reads as two dialogs docked together. */
  border-right: 1px solid var(--pb-border-soft);
  background: var(--pb-surface-2);
}

/* The preview pane. It does NOT scroll: the poster clips and the change list
   owns its own overflow, so the head and the note under the poster stay put. */
.pb-tl-modal .pb-modal-body-main {
  display: flex;
  flex-direction: column;
  gap: var(--pb-space-5);
  min-height: 0;
  overflow: hidden;
  padding: var(--pb-space-6);
}

/* ── command row: chips + filter ─────────────────────────────────────────── */

/**
 * ONE ROW, spanning the whole dialog under the header — chips left, filter
 * right. It used to be a COLUMN inside the 360px rail, which wrapped four
 * scope chips onto three lines and put a full-width text input under them,
 * eating the top of the list to say what the list is filtered by.
 */
.pb-tl-command {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--pb-space-4);
  padding: 0 var(--pb-space-6) var(--pb-space-5);
}

/* Chips scroll rather than wrap on a phone (P22); the strip itself is the
   kit's segmented control, never a hand-rolled one. What is overridden is only
   its SKIN: individual outlined pills, the active one filled — a grey track
   with a white thumb reads as one control with a setting, and these are four
   scopes with four counts. */
.pb-tl-chips.pb-seg {
  overflow-x: auto;
  scrollbar-width: none;
  gap: var(--pb-space-3);
  padding: 0;
  border: 0;
  background: none;
  min-width: 0;
}

.pb-tl-chips::-webkit-scrollbar {
  display: none;
}

.pb-tl-chips .pb-seg-option {
  flex: 0 0 auto;
  height: 26px;
  padding: 0 var(--pb-space-5);
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-pill);
  background: var(--pb-surface);
  color: var(--pb-text-dim);
  font-size: var(--pb-font-base);
  font-weight: 550;
}

.pb-tl-chips .pb-seg-option:hover {
  background: var(--pb-surface-hover);
  color: var(--pb-text);
}

.pb-tl-chips .pb-seg-option.pb-active {
  background: var(--pb-accent);
  border-color: var(--pb-accent);
  color: var(--pb-on-accent);
  box-shadow: none;
}

.pb-tl-chip-n {
  margin-left: var(--pb-space-2);
  opacity: 0.65;
  font-variant-numeric: tabular-nums;
}

.pb-tl-filter {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--pb-space-3);
  height: 28px;
  min-width: 168px;
  padding: 0 var(--pb-space-4h);
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-pill);
  background: var(--pb-field-bg);
  color: var(--pb-text-faint);
  transition:
    border-color 140ms ease-out,
    background 140ms ease-out;
}

.pb-tl-filter:focus-within {
  border-color: var(--pb-accent);
  background: var(--pb-surface);
  color: var(--pb-accent);
}

.pb-tl-filter-icon {
  display: inline-flex;
  flex: 0 0 auto;
}

.pb-tl-filter-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  outline: 0;
  background: none;
  color: var(--pb-text);
  font: inherit;
  font-size: var(--pb-font-base);
}

/* ── the list ────────────────────────────────────────────────────────────── */

.pb-tl-scroller {
  position: relative;
  /* `1 1 auto`, NOT `1 1 0` — see the rail note above. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* The prototype's numbers. The extra tail is what stops the last row sitting
     flush against the bottom of a 620px card. */
  padding: var(--pb-space-4) 0 var(--pb-space-6);
}

.pb-tl-groups {
  position: relative;
  z-index: 1;
}

.pb-tl-day {
  position: sticky;
  top: 0;
  z-index: 3;
  margin: 0;
  padding: var(--pb-space-5) var(--pb-space-6) var(--pb-space-2);
  font-size: var(--pb-font-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--pb-text-faint);
  background: linear-gradient(180deg, var(--pb-surface-2) 88%, transparent);
}

.pb-tl-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/**
 * A TWO-ROW GRID, so the run expander can sit under the label without being a
 * button inside a button. Column 1 holds the row button (and, on row 2, the
 * expander); column 2 holds the trailing cluster — delta, badge, pin.
 */
.pb-tl-row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  transition: background 90ms ease-out;
}

.pb-tl-row:hover {
  background: var(--pb-surface-hover);
}

/* Rewound rows are DOWNSTREAM of the selection, not deleted — dimmed, never
   struck through. */
.pb-tl-row-rewound .pb-tl-row-label,
.pb-tl-row-rewound .pb-tl-row-meta {
  opacity: 0.5;
}

/* A step inside a run indents to 46px — the button already carries 16px of its
   own, so the extra is 30 — and sits tighter vertically than the run it belongs
   to, which is what makes an expanded run read as one block. */
.pb-tl-row-step {
  padding-left: calc(var(--pb-space-6h) + var(--pb-space-4h));
}

.pb-tl-row-step .pb-tl-row-btn {
  padding-top: var(--pb-space-3);
  padding-bottom: var(--pb-space-3);
}

.pb-tl-row-btn {
  grid-column: 1;
  grid-row: 1;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  column-gap: var(--pb-space-4);
  align-items: center;
  padding: var(--pb-space-4h) var(--pb-space-3) var(--pb-space-4h) var(--pb-space-6);
  border: 0;
  background: transparent;
  color: var(--pb-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* `aria-current` and focus are two DIFFERENT things now, so the focus ring must
   be visibly distinct from the selected state. */
.pb-tl-row-btn:focus-visible {
  outline: 2px solid var(--pb-accent);
  outline-offset: -2px;
}

/* The SELECTED tint paints the whole row, not just the button, so the pin and
   the delta are inside the selection rather than beside it. */
.pb-tl-row:has(.pb-tl-row-btn[aria-current='true']) {
  background: var(--pb-accent-tint);
}

.pb-tl-row-btn[aria-current='true'] .pb-tl-row-label {
  font-weight: 650;
}

/* The dot carries the row's kind. It spans both text rows so it reads as a
   marker on the timeline rather than a bullet on the first line. */
.pb-tl-row-dot {
  grid-column: 1;
  grid-row: 1 / 3;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--pb-border);
  box-shadow: 0 0 0 3px var(--pb-surface-2);
}

.pb-tl-row[data-kind='now'] .pb-tl-row-dot {
  background: var(--pb-success);
}

.pb-tl-row[data-source='cloud'] .pb-tl-row-dot {
  background: var(--pb-accent);
  opacity: 0.6;
}

.pb-tl-row[data-pinned='true'] .pb-tl-row-dot {
  background: var(--pb-warning);
}

.pb-tl-row-label {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  font-size: var(--pb-font-md);
  font-weight: 550;
  color: var(--pb-text-bright);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: opacity 200ms ease-out;
}

.pb-tl-row[data-pinned='true'] .pb-tl-row-label {
  font-weight: 650;
}

.pb-tl-row-meta {
  grid-column: 2;
  grid-row: 2;
  margin-top: 1px;
  font-size: var(--pb-font-base);
  color: var(--pb-text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: opacity 200ms ease-out;
}

/* A renaming row must be allowed to be as tall as its field plus its cost
   hint; the ellipsis clamp above would otherwise hide the hint. */
.pb-tl-row-renaming .pb-tl-row-label {
  overflow: visible;
  white-space: normal;
}

.pb-tl-row-tags {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  align-items: center;
  gap: var(--pb-space-3);
  padding-right: var(--pb-space-5);
}

/* What this point did to the section count, as a number rather than as a
   clause buried in the meta line. */
.pb-tl-delta {
  padding: 1px var(--pb-space-3);
  border-radius: var(--pb-radius-pill);
  font-size: var(--pb-font-sm);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.pb-tl-delta.is-add {
  background: var(--pb-success-bg);
  color: var(--pb-success-text);
}

.pb-tl-delta.is-remove {
  background: var(--pb-danger-bg);
  color: var(--pb-danger-text);
}

.pb-tl-badge {
  padding: 1px var(--pb-space-3);
  border-radius: var(--pb-radius-sm);
  background: var(--pb-surface-3);
  font-size: var(--pb-font-sm);
  font-weight: 650;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--pb-text-dim);
  white-space: nowrap;
}

/* The run expander: a link under the meta line, in the row's second grid row.
   It is a SIBLING of the row button, never a child — nesting it would make an
   interactive control inside an interactive control. */
.pb-tl-run-toggle {
  grid-column: 1;
  grid-row: 2;
  justify-self: start;
  /* 32px = the button's 16px padding + the 8px dot + the 8px column gap, so the
     expander starts exactly under the label rather than 14px past it. */
  margin: 0 0 var(--pb-space-3) 32px;
  padding: 0;
  color: var(--pb-accent);
  font-size: var(--pb-font-base);
}

.pb-tl-run-toggle::before {
  content: '›';
  display: inline-block;
  margin-right: var(--pb-space-2);
  transition: transform 140ms ease-out;
}

.pb-tl-run-toggle[aria-expanded='true']::before {
  transform: rotate(90deg);
}

.pb-tl-handoff {
  display: block;
  width: 100%;
  padding: var(--pb-space-3) var(--pb-space-6);
  border-top: 1px solid var(--pb-border);
  color: var(--pb-text-dim);
  text-align: left;
}

.pb-tl-none {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--pb-space-3);
  padding: var(--pb-space-10) var(--pb-space-6);
  text-align: center;
}

.pb-tl-none-title {
  margin: 0;
  font-size: var(--pb-font-lg);
  font-weight: 650;
  color: var(--pb-text-bright);
}

.pb-tl-none-note {
  margin: 0;
  max-width: 32ch;
  color: var(--pb-text-dim);
}

/* ── the marker (P9) and the rewind band (P8) ────────────────────────────── */

.pb-tl-marker,
.pb-tl-rewind {
  position: absolute;
  left: 0;
  pointer-events: none;
}

/**
 * A 3px BAR ABOVE THE ROWS, not a border underneath them.
 *
 * As a `border-left` on a full-width box at `z-index: 0` it sat below
 * `.pb-tl-groups`, so the selected row's `--pb-accent-tint` painted over the one
 * element whose whole job is to say which row is selected. It is its own 3px
 * element at `z-index: 2` now, with the prototype's rounded outer edge.
 */
.pb-tl-marker {
  z-index: 2;
  width: 3px;
  border-radius: 0 var(--pb-radius-pill) var(--pb-radius-pill) 0;
  background: var(--pb-accent);
  transition:
    transform 180ms ease-out,
    height 180ms ease-out;
}

/* A wash, not a hatch. `--pb-surface-3` is an opaque grey and rendered as hard
   stripes over the rows it is only meant to tint; the prototype's value is a
   ~3% darkening of the rail, which is why it reads as a band rather than as
   damage. */
.pb-tl-rewind {
  right: 0;
  z-index: 0;
  background: repeating-linear-gradient(
    -45deg,
    color-mix(in srgb, var(--pb-border-soft) 28%, transparent) 0 6px,
    transparent 6px 12px
  );
  opacity: 0;
  transition:
    transform 200ms ease-out,
    height 200ms ease-out,
    opacity 200ms ease-out;
}

/* A striped BACKGROUND is invisible in forced colors, so the band gets a border
   there instead — the only way it survives as a signal at all. */
@media (forced-colors: active) {
  .pb-tl-rewind {
    background: none;
    border-top: 1px dashed CanvasText;
    border-bottom: 1px dashed CanvasText;
  }

  .pb-tl-marker {
    background: Highlight;
    forced-color-adjust: none;
  }

  .pb-tl-badge {
    border-color: CanvasText;
  }
}

/* ── the pin (P12) ───────────────────────────────────────────────────────── */

.pb-tl-pin {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: var(--pb-radius-md);
  background: transparent;
  color: var(--pb-text-faint);
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition:
    opacity 160ms ease-out,
    background 90ms ease-out,
    color 90ms ease-out;
}

.pb-tl-row:hover .pb-tl-pin,
.pb-tl-row:focus-within .pb-tl-pin,
.pb-tl-pin:focus-visible {
  opacity: 1;
}

.pb-tl-pin:hover {
  background: var(--pb-bg-hover);
  color: var(--pb-text);
}

/* A named point's pin is PERMANENT — it is the badge that says the point is
   kept, so hiding it until hover would hide the state it reports. */
.pb-tl-row[data-pinned='true'] .pb-tl-pin {
  opacity: 1;
  color: var(--pb-warning-icon);
}

.pb-tl-rename {
  width: 100%;
  padding: 2px var(--pb-space-1);
  border: 1px solid var(--pb-accent);
  border-radius: var(--pb-radius-sm);
  background: var(--pb-surface);
  color: var(--pb-text);
  font: inherit;
}

.pb-tl-rename-hint {
  display: block;
  margin-top: 2px;
  font-size: var(--pb-font-xs);
  font-weight: 400;
  color: var(--pb-text-dim);
}

.pb-tl-row-pending {
  opacity: 0.6;
}

/* ── the poster and the change list ──────────────────────────────────────── */

/**
 * The engine WRITES `--pb-tl-poster-h` from the scaled 1280-logical page, so it
 * is routinely taller than the pane — 905px inside a 667px body on the first
 * capture, which pushed the change list and the commit bar below the fold and
 * cost P16 its "under the preview" placement. The host therefore CLIPS: a
 * poster shows the top of the page, which is what a poster is.
 */
/* ── the preview head, the split pane, the note ──────────────────────────── */

.pb-tl-pv-head {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: var(--pb-space-4);
  min-height: 22px;
  min-width: 0;
}

.pb-tl-pv-title {
  font-size: var(--pb-font-lg);
  font-weight: 650;
  color: var(--pb-text-bright);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pb-tl-pv-sub {
  flex: 0 0 auto;
  font-size: var(--pb-font-base);
  color: var(--pb-text-dim);
  white-space: nowrap;
}

/* Poster and change list SIDE BY SIDE. Stacked, the list was below the fold on
   every viewport the poster filled — which is the one place the decision is
   actually made. */
.pb-tl-pv-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  gap: var(--pb-space-6);
}

/* ONE line naming what the poster is. */
.pb-tl-pv-note {
  flex: 0 0 auto;
  margin: 0;
  font-size: var(--pb-font-sm);
  color: var(--pb-text-faint);
}

/**
 * The engine WRITES `--pb-tl-poster-h` from the scaled 1280-logical page, so it
 * is routinely taller than the pane. The host therefore CLIPS: a poster shows
 * the top of the page, which is what a poster is. It no longer carries a
 * max-height of its own — the dialog has a fixed height now, so the pane is
 * definite and the poster simply fills the space beside the change list.
 */
.pb-tl-poster {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  margin: 0;
  border: 1px solid var(--pb-border-soft);
  border-radius: var(--pb-radius-md);
  overflow: hidden;
  background: var(--pb-surface-2);
  box-shadow: var(--pb-shadow-thumb);
}

/**
 * NO `height` here. The frame's width, height and transform are set INLINE by
 * `state-poster.js` — `height: var(--pb-tl-poster-h)` could never have won
 * against them, and reading the published height back into the element that
 * produces it says the size is decided here when it is not. What this rule is
 * actually for is `display: block`: an iframe is a replaced INLINE element, so
 * without it the box takes baseline leading under the poster.
 */
.pb-tl-poster-frame {
  display: block;
  transition: opacity 180ms ease-out;
}

/* `100%`, not the published height: the strip is a fallback drawn while the
   real page composes, and `showPlaceholder` does not rewrite the var, so
   reading it here sized the fallback from whatever the LAST poster measured. */
.pb-tl-poster-strip {
  display: flex;
  gap: 2px;
  height: 100%;
  overflow: hidden;
}

.pb-tl-poster-strip img {
  width: 100%;
  object-fit: cover;
}

.pb-tl-poster-caption,
.pb-tl-poster-error {
  margin: 0;
  padding: var(--pb-space-2) var(--pb-space-3);
  font-size: var(--pb-font-xs);
  color: var(--pb-text-dim);
}

.pb-tl-changes {
  flex: 0 0 208px;
  min-width: 0;
  overflow-y: auto;
  padding: 0;
}

.pb-tl-changes:focus-visible {
  outline: 2px solid var(--pb-accent);
  outline-offset: 2px;
}

.pb-tl-changes-title {
  margin: 0 0 var(--pb-space-3);
  font-size: var(--pb-font-sm);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--pb-text-faint);
}

.pb-tl-changes-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--pb-space-3);
}

/* A 14px glyph column plus a 6px gap, which is the prototype's `.change` laid
   out with a pseudo-element instead of a span — 2px of leading between items and
   a size below the rest of the pane made this read as a caption. */
.pb-tl-change {
  position: relative;
  padding-left: 20px;
  font-size: var(--pb-font-base);
  line-height: 1.45;
}

.pb-tl-change::before {
  position: absolute;
  left: 0;
  width: 14px;
  text-align: center;
  font-weight: 800;
  content: '·';
}

.pb-tl-change-add::before {
  content: '+';
  color: var(--pb-success);
}

.pb-tl-change-remove::before {
  content: '−';
  color: var(--pb-danger);
}

.pb-tl-changes-none {
  margin: 0;
  color: var(--pb-text-faint);
  font-size: var(--pb-font-base);
}

/* ── the commit bar (P16) ────────────────────────────────────────────────── */

.pb-tl-commit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--pb-space-5);
  width: 100%;
}

.pb-tl-commit-copy {
  flex: 1 1 auto;
  min-width: 0;
}

.pb-tl-commit-line {
  margin: 0;
  font-size: var(--pb-font-md);
  font-weight: 600;
  color: var(--pb-text-bright);
}

/* The safety net is a FACT, not a clause in a warning. */
.pb-tl-commit-label {
  margin: 1px 0 0;
  font-size: var(--pb-font-base);
  color: var(--pb-text-dim);
}

.pb-tl-commit-refused {
  color: var(--pb-warning-text);
  font-weight: 550;
}

.pb-tl-commit-actions {
  display: flex;
  align-items: center;
  gap: var(--pb-space-4);
  margin-left: auto;
}

/**
 * P18: restore reads as ONE movement — the primary fills for 420ms, then the
 * dialog dismisses over 150ms while the canvas cross-fades behind it.
 *
 * The fill is a TRANSFORM on a pseudo-element, deliberately not a keyframe: the
 * attention-keyframe ledger in `design-tokens-tests.js` only shrinks, and a
 * progress fill is not an attention pulse anyway. A transition needs no name.
 */
.pb-tl-commit-actions .pb-btn-primary {
  position: relative;
  overflow: hidden;
}

.pb-tl-commit-actions .pb-btn-primary::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: scaleX(0);
  transform-origin: left center;
  background: currentColor;
  opacity: 0.22;
  pointer-events: none;
  transition: transform 420ms ease-out;
}

.pb-tl-restoring .pb-tl-commit-actions .pb-btn-primary::after {
  transform: scaleX(1);
}

/* the dismiss overlaps the canvas cross-fade so it reads as one movement. */
.pb-tl-committing {
  opacity: 0;
  transition: opacity 150ms ease-out;
}

/* ── the empty state (P21) ───────────────────────────────────────────────── */

/* A dialog that says "nothing has happened yet" must not be 620px tall to say
   it — this is the one state the fixed height gives back. */
.pb-tl-empty-modal {
  height: auto;
}

.pb-tl-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--pb-space-3);
  padding: var(--pb-space-10) var(--pb-space-8);
  text-align: center;
}

.pb-tl-empty-title {
  margin: 0;
  font-weight: 600;
}

.pb-tl-empty-note {
  margin: 0;
  color: var(--pb-text-dim);
  font-size: var(--pb-font-sm);
  max-width: 42ch;
}

/* ── phone (P22) ─────────────────────────────────────────────────────────── */

@media (max-width: 640px) {
  /**
   * THE ORDER OVERRIDE, and the reason it has to exist.
   *
   * `rail.position: 'start'` puts the list first in DOM ORDER, which is right
   * on desktop. The kit's phone rule is
   * `.pb-modal-overlay .pb-modal-body-split { flex-direction: column }` — DOM
   * order again — so the phone would stack list-above-poster, the opposite of
   * P22. One `order` declaration per pane, scoped to `.pb-tl-*` so it forks
   * nothing the kit owns.
   *
   * jsdom cannot see this. The `history-timeline-phone` geometry surface
   * captures the COMPUTED order of both panes, because this is exactly the
   * class of claim a DOM test certifies as true while it is false.
   */
  /**
   * THE SHEET NEEDS A HEIGHT TOO, and `auto` is not it.
   *
   * The kit's phone card is `max-height: min(92dvh, …)` with no height, so it
   * is content-sized — and a grid whose list track is `minmax(0, 1fr)`
   * contributes 0 to that content height, where the old flex scroller
   * contributed all eleven rows and pushed the card to its cap. `auto` here
   * rendered a 160px list with 150px of dead white under it. 92dvh matches the
   * cap the kit already applies, so the card fills the sheet and the cap still
   * decides on a short viewport.
   */
  .pb-tl-modal {
    height: 92dvh;
  }

  /**
   * ONE COLUMN, THREE ROWS — preview, list, commit. The grid's own DOM order
   * is rail → preview → footer, which is right on desktop and the opposite of
   * P22 on a phone (thumb reaches the list, eye reaches the poster). Explicit
   * placement rather than `order`, because the tracks are named here anyway.
   *
   * jsdom cannot see this. The `history-timeline-phone` geometry surface
   * captures the computed placement of both panes, because this is exactly the
   * class of claim a DOM test certifies as true while it is false.
   */
  .pb-tl-modal .pb-modal-body-split {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto;
  }

  .pb-tl-modal .pb-modal-body-main {
    grid-column: 1;
    grid-row: 1;
    padding: var(--pb-space-5) var(--pb-space-5) var(--pb-space-4);
    gap: var(--pb-space-4);
  }

  /**
   * The `> :first-child` half of the selector is NOT decoration — it is the
   * specificity. The kit's phone block caps a rail at 45% of the split with
   * TWO selectors, and the second one
   * (`.pb-modal-overlay .pb-modal-body-split > .pb-modal-rail:first-child`) is
   * (0,4,0), so a two-class rule here loses to it however late it appears. The
   * cap is right for a trailing sidebar and wrong for the pane that IS this
   * surface: it left the list 160px tall with 150px of dead white under it, and
   * matching the shape of the rule is the only way to say so.
   */
  .pb-tl-modal .pb-modal-rail,
  .pb-tl-modal .pb-modal-body-split > .pb-modal-rail:first-child {
    grid-column: 1;
    grid-row: 2;
    min-height: 160px;
    max-height: none;
    border-right: 0;
    border-top: 1px solid var(--pb-border);
  }

  .pb-tl-modal .pb-modal-footer {
    grid-column: 1;
    grid-row: 3;
  }

  .pb-tl-command {
    padding: 0 var(--pb-space-5) var(--pb-space-4);
  }

  /* The chips already scroll; a 168px filter pill beside them on a 390px
     screen leaves room for one chip. Type-to-filter still redirects into it
     on a keyboard, and there is no keyboard here. */
  .pb-tl-filter {
    display: none;
  }

  /**
   * The change list STACKS under the poster rather than collapsing out. It is
   * the half that is load-bearing for the decision — it is read from state and
   * stays exact when the preview fails — so a phone may shrink it and must not
   * delete it.
   */
  .pb-tl-pv-body {
    flex-direction: column;
    gap: var(--pb-space-4);
  }

  /* A FIXED 168px, not a viewport fraction. The preview pane is content-sized
     here (`grid-row: 1` is an `auto` track), so every pixel the poster takes is
     a pixel the LIST does not get — and the list is what the thumb is for.
     32dvh measured 270px on a 844pt screen and left the list 130px. */
  .pb-tl-poster {
    flex: 0 0 auto;
    height: 168px;
  }

  /* Bounded, not deleted: the change list is the half that stays exact when the
     preview fails, so a phone may shrink it and must not drop it. */
  .pb-tl-changes {
    flex: 0 1 auto;
    max-height: 96px;
  }

  /* An affordance that exists only on hover does not exist on a phone. */
  .pb-tl-pin {
    opacity: 1;
  }

  .pb-tl-commit-actions {
    width: 100%;
  }

  .pb-tl-commit-actions .pb-btn-primary {
    flex: 1 1 auto;
    min-height: 44px;
  }
}

/* ── reduced motion: NONE, never a short duration ────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .pb-tl-marker,
  .pb-tl-rewind,
  .pb-tl-pin,
  .pb-tl-row,
  .pb-tl-row-label,
  .pb-tl-row-meta,
  .pb-tl-run-toggle::before,
  .pb-tl-filter,
  .pb-tl-poster-frame,
  .pb-tl-committing,
  .pb-tl-commit-actions .pb-btn-primary::after {
    /* `none`, never a shortened duration: a 60ms slide is still a slide. */
    transition: none;
  }
}
