/* ── History Rail (HR-0) ─────────────────────────────────────────────────
   Contracts: builder/docs/history-rail-contracts.md
   Plan:      .claude/plans/history-rail-master-plan.md

   R1 — IN FLOW, NEVER OUT OF IT. This file must contain no `position:
   absolute|fixed`, no `z-index`, no `box-shadow`, no `backdrop-filter`, and no
   `border-radius` on the rail root. `history-rail-revert-gate.js` enforces
   every one of those literally, because the surface this replaces was reverted
   on 2026-08-27 for being exactly that shape.

   The reason the whole file exists rather than a block in builder.css: it loads
   LAST (index.html), so its `--pb-canvas-shift-left: 0px` rule wins the
   equal-specificity tie against builder.css's canvas-shove rule without an
   `!important` and without editing builder.css at all. One less shared file to
   carve out if this is removed. */

/* The slot is a real flex sibling of .pb-panel-center inside .pb-main, so it
   takes its width OUT of the canvas box. There is no `--pb-canvas-shift-right`
   companion here and there must never be one: the half-shift compromise
   (builder.css:317, ai-chat.css:2260) is what this initiative exists to avoid. */
.pb-panel-right {
  flex: 0 0 var(--pb-rail-width);
  max-width: var(--pb-rail-width);
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--pb-surface);
  border-left: 1px solid var(--pb-border);
  /* Square, flush, and drawn with one hairline. A dock is a wall you build
     against; an island is furniture, and furniture is something you move. */
  border-radius: 0;
}

/* `hidden` must beat `display: flex` — the UA [hidden] rule loses that
   specificity fight silently. A closed rail then costs zero flex width, which
   is what makes "closed" and "not built" the same geometry. */
.pb-panel-right[hidden] {
  display: none;
}

/* ── R3: the left panel tucks to an icon strip ──
   Not cosmetic, and not merely "nice for width". `.pb-iframe-wrap` is
   `width: 100%` of the center panel and is TRANSLATED right by
   --pb-canvas-shift-left; with the rail open and the shift left in place, its
   right edge runs UNDER the rail. Returning the shift to 0 is what keeps R1
   true, and tucking is what makes returning it correct. */
.pb-panel-left.pb-rail-tucked {
  width: var(--pb-panel-width-left-tucked);
  min-width: var(--pb-panel-width-left-tucked);
}

/* `transform: none`, NOT `--pb-canvas-shift-left: 0`.
   Both produce the same geometry, but redefining a token outside :root is a
   SCOPED TOKEN FORK, and that ledger is shrink-only — the initiative's own rule
   is that needing a ledger entry is the signal to change the code. Overriding
   the declaration the token feeds says the same thing without minting a fork.

   KNOWN GAP: this also neutralises `--pb-canvas-shift-right`, which the docked
   agent panel uses. The rail and that panel occupy the same edge and would
   collide regardless, so nothing is made worse — but if they are ever allowed
   to coexist, this rule has to compose rather than flatten. */
.pb-panel-left.pb-rail-tucked ~ .pb-panel-center .pb-iframe-wrap {
  transform: none;
}

/* Tucked contents are hidden rather than unmounted: NavSidebar keeps its DOM,
   its scroll position and its active tab, so untucking restores the panel the
   user left rather than a rebuilt one.

   HONEST NAMING: what this leaves is a blank 48px gutter, NOT an icon strip.
   NavSidebar renders text tabs ("Sections", "Design", "Project", "Ship"), and
   turning them into glyphs means editing `ui/nav-sidebar.js` — one of the five
   files the NAV revert restored, which the revert gate forbids this initiative
   from touching. The 48px is what keeps R3's arithmetic true (452 given back,
   348 taken); making it legible is a follow-up that has to be negotiated with
   the partition, not smuggled in here. */
.pb-panel-left.pb-rail-tucked > * {
  visibility: hidden;
}

/* ── Rail chrome ── */
.pb-rail-head {
  display: flex;
  align-items: flex-start;
  gap: var(--pb-space-4);
  padding: var(--pb-space-4h) var(--pb-space-5) var(--pb-space-4);
  border-bottom: 1px solid var(--pb-border-soft);
  flex: none;
}

.pb-rail-title {
  font-size: var(--pb-font-base);
  font-weight: 700;
  color: var(--pb-text-bright);
  letter-spacing: -0.01em;
}

.pb-rail-context {
  font-size: var(--pb-font-xs);
  color: var(--pb-text-dim);
  margin-top: 1px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pb-rail-head-actions {
  margin-left: auto;
  flex: none;
}

.pb-rail-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* The rail is a vh-capped scroller beside the canvas: reaching its end must
     not scroll the canvas behind it, and on iOS must not arm the back-swipe. */
  overscroll-behavior: contain;
  padding: var(--pb-space-3) var(--pb-space-5) var(--pb-space-5);
}

.pb-rail-placeholder {
  padding: var(--pb-space-6) var(--pb-space-4);
  font-size: var(--pb-font-sm);
  color: var(--pb-text-dim);
  text-align: center;
}

.pb-rail-placeholder.is-error {
  color: var(--pb-danger);
}

/* ── Now card (HR-1) ──
   The timeline's zero point is the present, so it is pinned above the list and
   states the dirty fact on open. In the modal that fact only surfaced inside
   the restore confirm — the moment it is most alarming and least actionable. */
.pb-rail-now {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--pb-space-3);
  width: 100%;
  text-align: left;
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-md);
  background: none;
  color: inherit;
  font: inherit;
  padding: var(--pb-space-4) var(--pb-space-4h);
  cursor: pointer;
  transition:
    background var(--pb-transition),
    border-color var(--pb-transition);
}

.pb-rail-now:hover {
  background: var(--pb-bg-hover);
}

.pb-rail-now.is-selected {
  border-color: var(--pb-accent);
  background: var(--pb-accent-muted);
}

.pb-rail-now-label {
  display: block;
  font-size: var(--pb-font-sm);
  font-weight: 650;
  color: var(--pb-text-bright);
}

.pb-rail-now-state {
  display: block;
  font-size: var(--pb-font-xs);
  color: var(--pb-text-dim);
  margin-top: 1px;
}

.pb-rail-now-state.is-dirty {
  color: var(--pb-warning-text);
  font-weight: 600;
}

.pb-rail-now-save {
  margin-left: auto;
}

.pb-rail-pubstate {
  flex-basis: 100%;
  font-size: var(--pb-font-xs);
  font-weight: 600;
  border-radius: var(--pb-radius-sm);
  padding: var(--pb-space-2) var(--pb-space-3);
  background: var(--pb-warning-bg);
  color: var(--pb-warning-text);
}

.pb-rail-pubstate.is-current {
  background: var(--pb-success-bg);
  color: var(--pb-success-text);
}

/* ── Filter ── */
.pb-rail-filter {
  display: flex;
  align-items: center;
  padding: var(--pb-space-3) 0 var(--pb-space-2);
}

.pb-rail-seg {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border-radius: var(--pb-radius-md);
  background: var(--pb-surface-hover);
}

.pb-rail-seg button {
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: var(--pb-font-xs);
  color: var(--pb-text-dim);
  padding: var(--pb-space-2) var(--pb-space-4);
  border-radius: var(--pb-radius-sm);
}

.pb-rail-seg button[aria-pressed='true'] {
  background: var(--pb-surface);
  color: var(--pb-text-bright);
  font-weight: 600;
}

/* ── Timeline ── */
.pb-rail-timeline-root {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}

.pb-rail-timeline {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0 calc(var(--pb-space-3) * -1);
  padding: 0 var(--pb-space-3);
}

.pb-rail-timeline:focus-visible {
  outline: 2px solid var(--pb-accent);
  outline-offset: -2px;
  border-radius: var(--pb-radius-sm);
}

/* Sticky day header. Must live INSIDE the scroller and bleed its gutters, or
   rows scroll past beside it (the .pb-tpl-v2-pin-row lesson). */
.pb-rail-day {
  position: sticky;
  top: 0;
  margin: 0 calc(var(--pb-space-3) * -1);
  padding: var(--pb-space-4) var(--pb-space-3) var(--pb-space-2);
  background: var(--pb-surface);
  font-size: var(--pb-font-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--pb-text-dim);
}

.pb-rail-day-rows {
  display: flex;
  flex-direction: column;
}

/* ── Row ── */
.pb-rail-row {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 0 var(--pb-space-3);
  width: 100%;
  text-align: left;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  padding: var(--pb-space-3) var(--pb-space-3) var(--pb-space-4) 0;
  border-radius: var(--pb-radius-md);
  cursor: pointer;
  transition: background var(--pb-transition);
}

.pb-rail-row:hover {
  background: var(--pb-bg-hover);
}

.pb-rail-row.is-selected {
  background: var(--pb-accent-muted);
}

.pb-rail-row.is-skeleton {
  cursor: default;
  pointer-events: none;
}

/* Spine + node: what makes the list read as a TIMELINE and gives selection a
   spatial anchor the eye can track while the canvas changes (HR-2). */
.pb-rail-spine {
  position: relative;
}

.pb-rail-spine::before {
  content: '';
  position: absolute;
  left: 8px;
  top: -8px;
  bottom: -8px;
  width: 1.5px;
  background: var(--pb-border);
}

.pb-rail-node {
  position: absolute;
  left: 4px;
  top: 6px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--pb-surface);
  box-shadow: inset 0 0 0 1.8px var(--pb-border);
  transition:
    box-shadow var(--pb-transition),
    transform var(--pb-transition);
}

.pb-rail-node.is-milestone {
  box-shadow: inset 0 0 0 3px var(--pb-accent-section);
}

.pb-rail-row.is-selected .pb-rail-node {
  background: var(--pb-accent);
  box-shadow:
    inset 0 0 0 1.8px var(--pb-accent),
    0 0 0 3px var(--pb-accent-muted);
  transform: scale(1.25);
}

.pb-rail-row-body {
  min-width: 0;
}

.pb-rail-row-top {
  display: flex;
  align-items: baseline;
  gap: var(--pb-space-3);
}

.pb-rail-time {
  font-size: var(--pb-font-sm);
  font-weight: 650;
  color: var(--pb-text-bright);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pb-rail-who {
  font-size: var(--pb-font-xs);
  color: var(--pb-text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The version number is support, not the axis: present when wanted, quiet when
   not. It brightens on hover and selection. */
.pb-rail-num {
  margin-left: auto;
  font-family: var(--pb-font-family-mono);
  font-size: var(--pb-font-xs);
  color: var(--pb-text-dim);
  opacity: 0.55;
  white-space: nowrap;
  transition: opacity var(--pb-transition);
}

.pb-rail-row:hover .pb-rail-num,
.pb-rail-row.is-selected .pb-rail-num {
  opacity: 1;
}

.pb-rail-name {
  display: block;
  font-size: var(--pb-font-sm);
  font-weight: 650;
  color: var(--pb-accent-section);
  margin-top: 1px;
}

.pb-rail-summary {
  display: block;
  font-size: var(--pb-font-xs);
  color: var(--pb-text-dim);
  margin-top: 2px;
}

.pb-rail-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pb-space-1h);
  margin-top: var(--pb-space-2);
}

.pb-rail-chip {
  font-size: var(--pb-font-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 700;
  border-radius: var(--pb-radius-sm);
  padding: 2px 6px;
  background: var(--pb-surface-hover);
  color: var(--pb-text-dim);
}

.pb-rail-chip.is-latest {
  background: var(--pb-accent-muted);
  color: var(--pb-accent);
}

.pb-rail-chip.is-published {
  background: var(--pb-success-bg);
  color: var(--pb-success-text);
}

.pb-rail-chip.is-milestone {
  background: var(--pb-accent-section-subtle);
  color: var(--pb-accent-section);
}

/* ── Folded run: a hairline divider, not a control in an action slot ── */
.pb-rail-fold {
  display: flex;
  align-items: center;
  gap: var(--pb-space-3);
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  padding: var(--pb-space-2) var(--pb-space-3) var(--pb-space-2) 26px;
  border-radius: var(--pb-radius-sm);
  color: var(--pb-text-dim);
}

.pb-rail-fold:hover {
  color: var(--pb-text);
  background: var(--pb-bg-hover);
}

.pb-rail-fold-line {
  flex: 1;
  height: 1px;
  background: var(--pb-border-soft);
}

.pb-rail-fold-count {
  font-size: var(--pb-font-xs);
  white-space: nowrap;
}

/* ── Footer disclosure ── */
.pb-rail-foot {
  flex: none;
  padding-top: var(--pb-space-3);
}

.pb-rail-load-older {
  width: 100%;
  justify-content: center;
}

/* ── Skeleton ── */
.pb-rail-sk {
  display: block;
  border-radius: var(--pb-radius-sm);
  background: var(--pb-surface-hover);
}

.pb-rail-sk-top {
  width: 60%;
  height: 12px;
}

.pb-rail-sk-sub {
  width: 85%;
  height: 10px;
  margin-top: var(--pb-space-2);
}

@media (prefers-reduced-motion: reduce) {
  .pb-rail-row,
  .pb-rail-node,
  .pb-rail-num,
  .pb-rail-now {
    transition: none;
  }
}

/* ── Review bar (HR-2) ──
   A ROW inside .pb-panel-center, above .pb-iframe-wrap. It takes vertical space
   and pushes the canvas down; it is not an island floating over it. Same rule
   as the rail (R1), same reason. */
.pb-rail-review {
  flex: none;
  background: var(--pb-warning-bg);
  border-bottom: 1px solid var(--pb-warning-border);
  color: var(--pb-warning-text);
}

.pb-rail-review[hidden] {
  display: none;
}

.pb-rail-review.is-error {
  background: var(--pb-danger-bg);
  color: var(--pb-danger-text);
}

.pb-rail-review-inner {
  display: flex;
  align-items: center;
  gap: var(--pb-space-4);
  flex-wrap: wrap;
  padding: var(--pb-space-3) var(--pb-space-5);
}

.pb-rail-review-label {
  font-size: var(--pb-font-sm);
  font-weight: 700;
}

.pb-rail-review-detail {
  font-size: var(--pb-font-xs);
  opacity: 0.85;
  min-width: 0;
}

.pb-rail-review-spacer {
  flex: 1;
}

.pb-rail-review-exit {
  color: inherit;
  font-size: var(--pb-font-xs);
  padding: var(--pb-space-2) var(--pb-space-3);
  border-radius: var(--pb-radius-sm);
  cursor: pointer;
}

.pb-rail-review-exit:hover {
  background: var(--pb-bg-hover);
}

/* ── Below the desktop shell the rail is not a rail ──
   HR-6 replaces this with a bottom sheet. Until then the slot takes no width
   rather than squeezing a phone canvas to nothing, and index.js refuses to
   open below this width so the entry point cannot lie about what it does. */
@media (max-width: 900px) {
  .pb-panel-right {
    display: none;
  }

  .pb-panel-left.pb-rail-tucked {
    width: min(86vw, 372px);
    min-width: min(86vw, 372px);
  }

  .pb-panel-left.pb-rail-tucked > * {
    visibility: visible;
  }
}

/* The page-fallback notice. Distinct from the read-only detail beside it: that
   line says WHAT you are looking at, this one says why it is not what you
   asked for. */
.pb-rail-review-note {
  font-size: var(--pb-font-xs);
  font-weight: 600;
  padding: var(--pb-space-1) var(--pb-space-3);
  border-radius: var(--pb-radius-sm);
  background: var(--pb-bg-hover);
}

.pb-rail-review-note[hidden] {
  display: none;
}

/* ── Compare modes (HR-4) ──
   One segmented control where the modal had three differently-named buttons. */
.pb-rail-review-seg {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border-radius: var(--pb-radius-md);
  background: var(--pb-bg-hover);
  flex: none;
}

.pb-rail-review-seg button {
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: var(--pb-font-xs);
  color: inherit;
  opacity: 0.75;
  padding: var(--pb-space-2) var(--pb-space-4);
  border-radius: var(--pb-radius-sm);
  white-space: nowrap;
}

.pb-rail-review-seg button[aria-pressed='true'] {
  background: var(--pb-surface);
  color: var(--pb-text-bright);
  font-weight: 600;
  opacity: 1;
}

.pb-rail-review-visual {
  color: inherit;
  font-size: var(--pb-font-xs);
  padding: var(--pb-space-2) var(--pb-space-3);
  border-radius: var(--pb-radius-sm);
  cursor: pointer;
  white-space: nowrap;
  flex: none;
}

.pb-rail-review-visual:hover {
  background: var(--pb-bg-hover);
}

/* The changes sentence. Its OWN element, never shared with the page-fallback
   notice: both facts can be true at once, and HR-2 already paid for putting two
   writers on one slot. */
.pb-rail-review-changes {
  font-size: var(--pb-font-xs);
  font-weight: 600;
  padding: var(--pb-space-1) var(--pb-space-3);
  border-radius: var(--pb-radius-sm);
  background: var(--pb-surface);
  color: var(--pb-text);
  min-width: 0;
}

.pb-rail-review-changes[hidden] {
  display: none;
}

/* Inline rename (HR-5). The `revision-panel.js` idiom: the label element is
   replaced in place by a real `.pb-input`, so naming a version is not a
   separate flow. */
.pb-rail-rename {
  width: 100%;
  font-size: var(--pb-font-sm);
  font-weight: 650;
  padding: var(--pb-space-1) var(--pb-space-2);
  margin-top: 1px;
}

/* Resting state on desktop: no handle, because there is nothing to drag. */
.pb-rail-grab {
  display: none;
  align-items: center;
  justify-content: center;
  width: 100%;
  border: 0;
  background: none;
  padding: var(--pb-space-3) 0 var(--pb-space-1);
  cursor: grab;
  flex: none;
}

.pb-rail-grab i {
  display: block;
  width: 2.25rem;
  height: 4px;
  border-radius: var(--pb-radius-sm);
  background: var(--pb-border);
}

/* ── The commit footer (HR-6) ──
   Restore moves out of the review bar and into a thumb-reachable footer, and it
   appears ONLY once a version is selected — so the surface never offers a
   destructive verb with no object. */
/* EXACTLY ONE restore control is on screen at any width (contract §3). There
   are two ELEMENTS — the review bar's, which is right beside what you are
   looking at on a wide screen, and the footer's, which is where a thumb already
   is on a phone — and they are mutually exclusive by width. Two live
   destructive verbs at once is what made the modal need a race guard.
   `history-rail-geometry.mjs` measures this at both widths, because a source
   count cannot see CSS. */
.pb-rail-foot-commit {
  flex: none;
  display: none;
  gap: var(--pb-space-3);
  align-items: center;
  padding: var(--pb-space-4) 0 0;
  border-top: 1px solid var(--pb-border-soft);
}

.pb-rail-foot-commit .pb-rail-commit {
  flex: 1;
  justify-content: center;
  min-height: var(--pb-tap-min);
}

.pb-rail-foot-commit .pb-rail-more {
  flex: none;
  min-width: var(--pb-tap-min);
  min-height: var(--pb-tap-min);
}

/* ── Mobile: a bottom dock, still IN FLOW (HR-6) ──────────────────────────
   The design review drew a floating bottom sheet. This is not one, and the
   difference is the whole thesis: a sheet is `position: fixed` over the canvas,
   which is the exact vocabulary R1 forbids — and it would mean exempting the
   rail from its own rule the moment the screen got small.

   Instead `.pb-main` becomes a COLUMN while the rail is open, and the rail
   takes a height the way it takes a width on desktop. The canvas genuinely
   shrinks rather than being covered, R1 needs no exemption, and the detents are
   just three flex-basis values.

   `.pb-rail-open` is set by index.js alongside the left-panel tuck rather than
   derived with `:has()`, so the behaviour does not depend on selector support. */
@media (max-width: 900px) {
  .pb-main.pb-rail-open {
    flex-direction: column;
  }

  /* Overrides the desktop `display: none` further up this file. */
  .pb-main.pb-rail-open .pb-panel-right {
    display: flex;
    flex: 0 0 auto;
    max-width: none;
    width: 100%;
    border-left: 0;
    border-top: 1px solid var(--pb-border);
    /* The command bar's row 2 is `position: fixed` at phone widths and
       publishes its height for exactly this reason. Without the reservation the
       last rows of the timeline sit under it. */
    padding-bottom: var(--pb-command-bottom-height-actual);
  }

  .pb-main.pb-rail-open .pb-panel-center {
    min-height: 0;
  }

  /* Three detents. Peek shows the head and the Now card; half is the working
     state; full is for reaching back through a long history.

     `height` per detent rather than a `--pb-rail-sheet-height` token: defining
     a --pb-* custom property outside :root is a SCOPED TOKEN FORK, and that
     ledger is shrink-only. Needing a ledger entry is the signal to change the
     code — the same call HR-3 made for the canvas shift. */
  .pb-panel-right[data-detent='peek'] {
    height: 8.25rem;
  }

  .pb-panel-right[data-detent='half'] {
    height: 52%;
  }

  .pb-panel-right[data-detent='full'] {
    height: calc(100% - 5rem);
  }

  /* The grab handle only exists at this width — on desktop the rail has a
     fixed width and nothing to resize. */
  .pb-rail-grab {
    display: flex;
    /* It is a CONTROL — tapping it cycles the detents — so it gets a real tap
       target, not just the 4px bar it draws. */
    min-height: var(--pb-tap-min);
  }

  /* Touch has no hover, so nothing may be revealed by it, and every target has
     to be reachable by a thumb. 56px is the accessible minimum; the desktop row
     is deliberately denser. */
  .pb-rail-row {
    min-height: calc(var(--pb-tap-min) + var(--pb-space-5));
    align-content: center;
  }

  .pb-rail-num {
    opacity: 1;
  }

  /* The footer owns the commit here; the review bar's copy stands down. */
  .pb-rail-foot-commit.is-armed {
    display: flex;
  }

  .pb-rail-review-restore {
    display: none;
  }
}

