/* Recap — the post-trip state (issue #892 / PRD Phase 3 #933 item 1).
   js/recap.js fills #recap-content. Uses the same "departure fever" card
   idiom as the Expenses / People panels (--df-panel on a 2px --df-ink
   keyline, hard offset --df-press shadow, --df-mono section heads) so Recap
   reads as one more tab, not a bolt-on. #panel-recap is a plain capped
   .panel — never .panel-wide.

   Section order is not fixed top-to-bottom: js/recap.js's _recapSettleInfo()
   only leads with Settle Up while a transfer is genuinely outstanding — once
   the books are closed it renders again, quietly, near the end (see the
   settle.outstanding ternaries in renderRecap()). The record — what the
   trip cost, the photos, the numbers, the Hall of Fame — is what a finished
   trip opens on when there's nothing left to chase. */

#recap-content {
  display: flex;
  flex-direction: column;
  gap: var(--space-4xl);
  padding-bottom: var(--space-5xl);
}

/* ── Header: the trip's name, and the entry stamp struck beside it ── */
.recap-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xl) var(--space-5xl);
}

.recap-header-text {
  flex: 1 1 280px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* ★ THE ONE SHOUT — a passport entry stamp for the trip just finished.
   A die with a double rule (a heavy border, then a hairline outline set off
   it), ruled top and bottom like the frame of a real entry stamp, struck by
   hand at -2.6deg. Coral because it is stamped (DESIGN.md: coral means
   stamped, nothing else); the ghost behind the place name is the second ink
   pass landing out of register, the same trick as .lp-stampword/.dtl-stamp.
   The resting transform lives HERE, not in the keyframes, so an interrupted
   or reduced-motion render still leaves the stamp correctly struck. */
.recap-stamp {
  --recap-ghost: color-mix(in srgb, var(--vn-teal) 30%, transparent);
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-xs);
  max-width: min(100%, 320px);
  /* Room for the outline and the swing of the rotation — without it the
     struck corner kisses whatever wraps next to it. */
  margin: var(--space-md) var(--space-lg) var(--space-md) var(--space-sm);
  padding: var(--space-md) var(--space-4xl) var(--space-lg);
  border: 3px solid currentColor;
  border-radius: var(--df-radius-stub, 12px);
  outline: 1.5px solid currentColor;
  outline-offset: 3px;
  color: var(--vn-coral);
  text-align: center;
  transform: rotate(-2.6deg);
  transform-origin: 30% 60%;
}

.recap-stamp-top,
.recap-stamp-date {
  font-family: var(--df-mono, monospace);
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1.3;
}

.recap-stamp-top {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding-bottom: var(--space-sm);
  border-bottom: 1.5px solid currentColor;
  font-size: var(--fs-xs);
  letter-spacing: 0.14em;
}
.recap-stamp-top .ti { font-size: 1.25em; }

/* The one shout on this screen (see the recap-title comment above) — the
   only display-tier element that stays at --fs-3xl at every width. Nothing
   else on this tab (the title, the total, the four ticket fields) is
   allowed to reach this size, mobile included — a downstep here used to let
   the stub's own counters outrank the stamp beneath 680px. */
.recap-stamp-place {
  padding-block: var(--space-2xs);
  font-family: var(--df-display, sans-serif);
  font-size: var(--fs-3xl);
  font-weight: 800;
  line-height: 0.98;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  text-wrap: balance;
  overflow-wrap: anywhere;
  text-shadow: 0.028em 0.036em 0 var(--recap-ghost);
}

.recap-stamp-date {
  padding-top: var(--space-sm);
  border-top: 1.5px solid currentColor;
  font-size: var(--fs-sm);
  letter-spacing: 0.1em;
}

/* The stamp coming down. js/recap.js adds .is-struck once per trip per page
   load, only while the panel is on screen, and removes it again. The delay
   lets the panel's own 280ms entrance (.panel-enter) settle first so the
   strike lands on a still page. Guarded behind no-preference so a reader who
   opted out never has a hidden start state at all. */
@media (prefers-reduced-motion: no-preference) {
  .recap-stamp.is-struck {
    /* Same curve as the landing page's own stamp-strike (--lp-ease-slam,
       css/landing.css) — an overshoot-then-settle for anything meant to
       land rather than arrive. Not var()-referenced: --lp-ease-slam is
       scoped to .landing-screen, not :root, so it isn't in scope on this
       tab's DOM. Hardcoded here on purpose rather than promoting the token
       to :root for one non-landing consumer. */
    animation: recap-strike 420ms cubic-bezier(0.34, 1.56, 0.64, 1) 180ms both;
  }
}
@keyframes recap-strike {
  0%   { opacity: 0; transform: rotate(-11deg) scale(1.85); }
  55%  { opacity: 1; transform: rotate(-2.6deg) scale(0.95); }
  78%  { transform: rotate(-2.6deg) scale(1.025); }
  100% { transform: rotate(-2.6deg) scale(1); }
}

/* --fs-2xl, not --fs-4xl: the stamp beside it is DESIGN.md's one shout on
   this screen (--fs-3xl, see .recap-stamp-place below), and a page title
   set as loud or louder than the stamp was a second shout competing with
   it — "one shout, everything else disciplined" halves the energy of both,
   it doesn't add them. */
.recap-title {
  font-family: var(--df-display, sans-serif);
  font-size: var(--fs-2xl);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--df-ink);
}

.recap-sub {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}

/* ── Cards ── */
.recap-card {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card, 14px);
  box-shadow: var(--df-press, 4px 4px 0 var(--df-ink));
  padding: var(--space-4xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
}

.recap-card-h {
  font-family: var(--df-mono, monospace);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.recap-subhead {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: var(--space-md);
}

.recap-empty {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}

.recap-cta {
  /* .btn-primary/.btn-secondary both declare width:100% for their usual
     home in a narrow modal or form. align-self:flex-start alone can't
     override that — align-self only governs cross-axis sizing when the
     item's own width is auto, and 100% is not auto — so without this
     override every CTA on the tab rendered as a full-card-width slab
     despite declaring shrink-to-fit intent right above it. */
  width: auto;
  max-width: 100%;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: var(--tap-min);
}
.recap-cta-quiet { font-weight: 500; }

/* ── Settle up ── */
/* No .recap-settle-urgent card treatment: DESIGN.md is explicit twice over
   that coral means stamped, nothing else, and must never read as danger.
   An outstanding transfer's card kept the plain --df-ink keyline and
   shadow every other card gets — urgency lives in the lead copy's own
   wording, and in the ABSENCE of the struck coral die on .recap-settle-badge
   below, the same way a real ledger reads "not yet paid" from a blank
   line rather than a red one. */

.recap-settle-lead {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}
.recap-settle-lead-done {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--df-ink);
  font-weight: 600;
}
.recap-settle-lead-done .ti { color: var(--vn-coral); }

.recap-settle-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.recap-settle-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
  padding: var(--space-lg) 0;
  border-top: 2px dashed var(--df-hair);
  font-size: var(--fs-sm);
}
.recap-settle-row:first-child { border-top: none; }

.recap-settle-who { flex: 1 1 auto; }
.recap-settle-amt {
  font-family: var(--df-mono, monospace);
  font-weight: 700;
  color: var(--df-ink);
}

/* A transfer's state, told as whether it has been stamped yet. A settled
   transfer carries a small struck die in coral — the entry stamp's own
   material at row scale — and only that one tilts, because only it was
   struck. One waiting on confirmation is the same die drawn dashed (the
   stamp is lined up, not yet down), in Amber Ink since Route Amber is too
   faint to be text. An open one is a plain hairline die. Dies, not pills:
   pills are for things you can press, and these can't be. */
.recap-settle-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-2xs) var(--space-md);
  border: 2px solid currentColor;
  border-radius: 4px;
  font-family: var(--df-mono, monospace);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}
.recap-settle-badge-done {
  color: var(--vn-coral);
  box-shadow: inset 0 0 0 1px currentColor;
  transform: rotate(-2.6deg);
}
.recap-settle-badge-mid  { color: var(--vn-amber-text); border-style: dashed; }
.recap-settle-badge-open { color: var(--df-ink-soft); border-color: var(--df-hair); }

/* ── Spend ── */
.recap-total {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}
.recap-total-label {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.recap-total-line {
  font-family: var(--df-display, sans-serif);
  font-size: var(--fs-2xl); /* below .recap-stamp-place's --fs-3xl — see its comment */
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--df-ink);
}

.recap-cat-group + .recap-cat-group { margin-top: var(--space-xl); }
.recap-cat-group-h {
  font-family: var(--df-mono, monospace);
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-bottom: var(--space-sm);
}

.recap-people-list {
  list-style: none;
  display: flex;
  flex-direction: column;
}
.recap-person-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: var(--space-md) 0;
  border-top: 2px dashed var(--df-hair);
  font-size: var(--fs-sm);
}
.recap-person-row:first-child { border-top: none; }
.recap-person-me { font-weight: 600; }
.recap-person-you { color: var(--df-ink-soft); font-weight: 400; }
.recap-person-amt { font-family: var(--df-mono, monospace); font-weight: 600; }

/* ── What actually happened (#933 item 2) — same dashed-row rhythm as
   .recap-people-list above, plus a leading category icon since a stop has
   no natural numeric column to right-align against. */
.recap-actual-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  margin-bottom: var(--space-lg);
}
.recap-actual-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) 0;
  border-top: 2px dashed var(--df-hair);
  font-size: var(--fs-sm);
}
.recap-actual-row:first-child { border-top: none; }
.recap-actual-icon { color: var(--df-ink-soft); font-size: var(--fs-lg); flex: none; }
.recap-actual-body { display: flex; flex-direction: column; }
.recap-actual-dest { font-weight: 600; }
.recap-actual-when { color: var(--df-ink-soft); font-size: var(--fs-xs); }

/* ── Planned vs. actual diff (#933 item 3) — groups reuse .recap-actual-list's
   row shape, .recap-subhead's label style; only the group spacing and the
   count badge are new here. */
.recap-diff-group + .recap-diff-group { margin-top: var(--space-xl); }
.recap-diff-count {
  display: inline-block;
  background: var(--df-hair);
  color: var(--df-ink-soft);
  border-radius: 999px;
  padding: 0 var(--space-sm);
  font-size: var(--fs-xs);
  font-weight: 700;
  margin-left: var(--space-2xs);
}

/* ── Photo strip ──
   DESIGN.md's Instant Print, in front of the record itself — the same
   idiom css/discover.css's .dtl-prints already puts in front of a public
   trip (real photos, tape, the Lift shadow). Recap previously carried none
   at all despite being the one surface whose whole job is handing the trip
   back. Local --recap-stock/--recap-tape alias the same values .dtl/.lp
   already declare in their own scopes — the brand belongs to the product,
   every surface aliases it locally rather than sharing one global token
   (DESIGN.md: "Do alias the app's tokens for every control ink"). */
#recap-content {
  --recap-stock: #fdf6ea;   /* Instant Stock — pinned in both themes: a print is white at midnight too */
  --recap-tape: light-dark(rgba(186, 117, 23, 0.42), rgba(232, 163, 61, 0.28));
}

.recap-prints {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0;
  max-width: 232px;
  margin-left: auto;
}
.recap-print {
  position: relative;
  flex: 0 1 34%;
  min-width: 68px;
  max-width: 92px;
  margin: 0;
  padding: 6px 6px 20px;
  background: var(--recap-stock);
  border-radius: var(--df-radius-print, 3px);
  box-shadow: var(--df-press-lift, 0 1px 0 rgba(9, 20, 13, 0.1), 0 22px 40px -26px rgba(9, 20, 13, 0.75));
  transition: transform var(--dur-state) var(--df-ease, var(--ease-out));
}
.recap-print:nth-child(1) { transform: rotate(-3.4deg); }
.recap-print:nth-child(2) { transform: rotate(2.2deg); }
.recap-print:nth-child(3) { transform: rotate(-1.4deg); }
.recap-print + .recap-print { margin-left: -26px; }

.recap-print::before,
.recap-print::after {
  content: '';
  position: absolute;
  top: -6px;
  width: 38px;
  height: 13px;
  background: var(--recap-tape);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
}
.recap-print::before { left: 8%; transform: rotate(-4deg); }
.recap-print::after { right: 8%; transform: rotate(3.4deg); }

.recap-print img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: 1px;
  background: color-mix(in srgb, var(--df-ink) 12%, var(--recap-stock));
}
.recap-print:nth-child(2) img { aspect-ratio: 1 / 1; }
.recap-print:nth-child(3) img { aspect-ratio: 5 / 4; }

/* What you actually do when you look at a crooked photo on a wall. */
.recap-print:hover {
  transform: rotate(0deg) scale(1.05);
  z-index: 2;
}
@media (prefers-reduced-motion: reduce) {
  .recap-print { transition: none; }
}

/* ── The trip in numbers: the stub you keep ──
   A boarding-pass stub (12px stub radius, Press offset) rather than one more
   card: the route printed across the top, a torn perforation with a notch
   bitten out of each edge, and the numbers set below as ticket fields —
   Martian Mono label over a Bricolage value, the way a pass prints them. */
.recap-stub {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub, 12px);
  box-shadow: var(--df-press, 4px 4px 0 var(--df-ink));
}

/* DESIGN.md's Dashed Route, compressed to one wrapping row: amber pins on a
   3px amber dash. The dash is shortened from the poster's 9-on/9-off so a
   connector between two city names still reads as dashes, not a single
   stroke. Each connector belongs to the stop after it, so a wrap never
   strands a dash at the end of a line with nothing to lead to. */
.recap-route {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  row-gap: var(--space-lg);
  margin: 0;
  padding: var(--space-2xl) var(--space-4xl);
}
.recap-route-stop {
  display: flex;
  align-items: center;
  min-width: 0;
}
.recap-route-stop + .recap-route-stop::before {
  content: '';
  flex: 0 0 auto;
  width: 34px;
  height: 3px;
  margin-inline: var(--space-sm);
  background: repeating-linear-gradient(90deg, var(--df-amber) 0 6px, transparent 6px 11px);
}
.recap-route-name {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
  font-family: var(--df-mono, monospace);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--df-ink);
  overflow-wrap: anywhere;
}
.recap-route-name::before {
  content: '';
  flex: 0 0 auto;
  box-sizing: border-box;
  width: 14px;
  height: 14px;
  border: 2px solid var(--df-ink);
  border-radius: 50%;
  background: var(--df-amber);
}

.recap-stub-fields {
  position: relative;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: var(--space-2xl) var(--space-4xl);
  margin: 0;
  padding: var(--space-2xl) var(--space-4xl) var(--space-4xl);
}

/* The perforation, and the two notches where it was torn. Each notch is a
   ground-coloured disc centred on the stub's outer edge and on the dashed
   line, clipped to the half that sits inside the stub, so it bites out of
   the keyline instead of sitting on top of it. Absolute offsets are measured
   from the padding box: the dashed border's 2px sits above it, the stub's
   own 2px keyline sits outside it. Only drawn when there is a route above
   to tear from. */
.recap-route + .recap-stub-fields {
  border-top: 2px dashed var(--df-ink-soft);
}
.recap-route + .recap-stub-fields::before,
.recap-route + .recap-stub-fields::after {
  content: '';
  position: absolute;
  top: -9px;
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  border: 2px solid var(--df-ink);
  border-radius: 50%;
  background: var(--df-ground);
}
.recap-route + .recap-stub-fields::before { left: -10px; clip-path: inset(0 0 0 50%); }
.recap-route + .recap-stub-fields::after  { right: -10px; clip-path: inset(0 50% 0 0); }

.recap-num {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-width: 0;
}
.recap-num-l {
  font-family: var(--df-mono, monospace);
  font-size: var(--fs-xs);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
}
.recap-num-n {
  margin: 0;
  font-family: var(--df-display, sans-serif);
  font-size: var(--fs-2xl); /* below .recap-stamp-place's --fs-3xl — see its comment */
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--df-ink);
}

/* ── The scroll replay ──
   Scrolling down the Recap replays the trip. When the stub comes into view,
   the route draws itself city to city (dash, then the pin lands, then the name
   is written along it), and then the ticket fields tally up to their values.
   Further up, the spend bars fill in category order as the breakdown comes into view.

   Everything is driven by scroll position (view timelines), not time, so:
   - It is complete whenever it is fully on screen. Every range ends at
     `entry 100%`, the moment the element has fully entered. So a render that is
     already in view, or scrolled past, never shows a half-drawn state, and
     the page can always reach it: content always follows the stub and the
     bars, further than the inset below.
   - It needs none of the stamp strike's arm-once bookkeeping. renderRecap()
     rebuilding the markup on an edit changes nothing, since the animation's
     state is a function of where the element sits, not of when it was
     rendered.
   - Before an element's range starts, the fill mode is `backwards` (the
     hidden start state); after it ends there is no fill at all, so the
     resting value is the plain base style, which is the truth.

   The inset lifts the timeline's bottom edge above what covers it. On a
   phone that is the fixed .tabs bar, so the replay plays where the reader
   can see it instead of underneath the tab bar. Firefox (no scroll-driven animations) and
   prefers-reduced-motion both get the static, finished stub, and neither
   ever has a hidden state to undo. */
@property --recap-n {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

/* The scrubbed ease is a cubic out, not the app's expo --ease-out. Mapped onto
   scroll distance, the expo curve does most of its travel in the first sliver
   of the range, so a bar or a count seemed to jump ahead of your finger. */
#recap-content {
  --recap-replay-inset: 6%;
  --recap-scrub-ease: cubic-bezier(0.33, 1, 0.68, 1);
}
@media (max-width: 1023px) {
  #recap-content { --recap-replay-inset: calc(92px + env(safe-area-inset-bottom, 0px) + 3%); }
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* One name for every replay subject. A descendant resolves the nearest
       ancestor carrying it, so a multi-currency trip's bars key off their
       own currency group, not the whole breakdown. */
    .recap-stub,
    .recap-cats,
    .recap-cat-group {
      view-timeline: --recap-replay block;
      view-timeline-inset: auto var(--recap-replay-inset);
    }

    /* Route: the first 55% of the stub's entry, split evenly per stop. Each
       stop's slice opens with its dash drawing in from the previous pin and
       closes with its own pin landing and its name being written. */
    .recap-route-stop {
      --seg-a: calc(var(--i) * 55% / var(--n));
      --seg-m: calc((var(--i) + 0.45) * 55% / var(--n));
      --seg-b: calc((var(--i) + 1) * 55% / var(--n));
    }
    .recap-route-stop + .recap-route-stop::before {
      transform-origin: 0 50%;
      animation: recap-dash linear backwards;
      animation-timeline: --recap-replay;
      animation-range: entry var(--seg-a) entry var(--seg-m);
    }
    .recap-route-name {
      animation: recap-write linear backwards;
      animation-timeline: --recap-replay;
      animation-range: entry var(--seg-m) entry var(--seg-b);
    }
    .recap-route-name::before {
      animation: recap-pin linear backwards;
      animation-timeline: --recap-replay;
      animation-range: entry var(--seg-m) entry var(--seg-b);
    }

    /* Fields: the tally. The real number stays in the DOM as text, set in
       transparent ink so it still holds the field's width, still copies,
       and is still what a screen reader reads. The counter printed over it
       is marked decorative with the `/ ""` alt text. Staggered so the last
       field lands exactly at entry 100%. */
    .recap-num-n {
      position: relative;
      color: transparent;
      counter-reset: recap-n var(--recap-n);
      animation: recap-tally var(--recap-scrub-ease) backwards;
      animation-timeline: --recap-replay;
      animation-range: entry calc(45% + var(--i) * 9%) entry calc(73% + var(--i) * 9%);
    }
    .recap-num-n::after {
      content: counter(recap-n) / "";
      position: absolute;
      inset: 0;
      color: var(--df-ink);
    }

    /* Spend bars: each fills from the left in category order. At most six
       rows (CATEGORY_META), the last starting at 45%, so the whole
       breakdown is full by the time it has fully entered. The n+7 step is
       only a ceiling in case a category is ever added. */
    .recap-cats .cat-bar-fill {
      transform-origin: 0 50%;
      animation: recap-fill var(--recap-scrub-ease) backwards;
      animation-timeline: --recap-replay;
      animation-range: entry calc(var(--k, 0) * 9%) entry calc(46% + var(--k, 0) * 9%);
    }
    .recap-cats .cat-bar-row:nth-child(2 of .cat-bar-row) { --k: 1; }
    .recap-cats .cat-bar-row:nth-child(3 of .cat-bar-row) { --k: 2; }
    .recap-cats .cat-bar-row:nth-child(4 of .cat-bar-row) { --k: 3; }
    .recap-cats .cat-bar-row:nth-child(5 of .cat-bar-row) { --k: 4; }
    .recap-cats .cat-bar-row:nth-child(6 of .cat-bar-row) { --k: 5; }
    .recap-cats .cat-bar-row:nth-child(n + 7 of .cat-bar-row) { --k: 6; }
  }
}

@keyframes recap-dash {
  from { scale: 0 1; }
  to   { scale: 1 1; }
}
/* Written left to right. The negative insets keep the pin's keyline and
   the Bricolage overshoot unclipped at the end of the wipe. */
@keyframes recap-write {
  from { clip-path: inset(-6px 100% -6px -6px); }
  to   { clip-path: inset(-6px -6px -6px -6px); }
}
@keyframes recap-pin {
  0%   { scale: 0; }
  45%  { scale: 1.35; }
  72%  { scale: 0.9; }
  100% { scale: 1; }
}
/* A field reads as "not counted yet" rather than as a real zero until its
   tally is under way. */
@keyframes recap-tally {
  from { --recap-n: 0; opacity: 0.3; }
  30%  { opacity: 1; }
}
@keyframes recap-fill {
  from { scale: 0 1; }
}

/* The scroll replay's hidden-until-scrolled-to start states are guarded by
   prefers-reduced-motion and @supports (animation-timeline: view()) above,
   but neither guard fires for a browser print/PDF render: the page never
   scrolls, so the view-timeline that would normally resolve every range to
   "already past, therefore finished" never advances past its own starting
   values either — printing (or a full-page screenshot) caught .recap-num-n
   still at its transparent, opacity:0.3 start and every unentered
   .recap-route-name still fully clipped. Reset both back to their plain
   resting values for print specifically, same as the reduced-motion/no
   -@supports fallback already does for its own two cases. */
@media print {
  .recap-num-n {
    color: var(--df-ink);
    opacity: 1;
  }
  .recap-num-n::after { content: none; }
  .recap-route-name,
  .recap-route-name::before,
  .recap-route-stop + .recap-route-stop::before {
    clip-path: none;
    scale: 1 1;
  }
  .recap-cats .cat-bar-fill { scale: 1 1; }
}

/* ── Ratings ── */
.recap-rate-prompt { border-style: dashed; }
.recap-rate-lead { font-size: var(--fs-sm); color: var(--df-ink-soft); }

.recap-trophy-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--space-lg);
}
.recap-trophy {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-2xs);
  padding: var(--space-lg);
  /* 1.5px, not the card's 2px keyline weight — --df-hair is a hairline
     colour (16% alpha); drawn at keyline thickness it read as neither. */
  border: 1.5px solid var(--df-hair);
  border-radius: var(--radius-xs);
  /* Now carries tabindex="0" (js/recap.js) so its title= criteria are
     keyboard/AT-reachable too — the app-wide
     .app [tabindex]:not([tabindex="-1"]):focus-visible rule (css/style.css)
     already rings it; nothing extra needed here. */
}
.recap-trophy-emoji { font-size: var(--fs-2xl); }
.recap-trophy-label { font-size: var(--fs-xs); font-weight: 700; color: var(--df-ink-soft); }
.recap-trophy-winner { font-size: var(--fs-sm); font-weight: 600; color: var(--df-ink); }

/* ── Keepsake ── */
.recap-keepsake {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xl);
}
.recap-keepsake-lead {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  margin-top: var(--space-2xs);
  max-width: 40ch;
}

@media (max-width: 680px) {
  /* .recap-title and .recap-stamp-place no longer need a mobile step — both
     already hold their final size (--fs-2xl and --fs-3xl respectively) at
     every width; the old downstep here was what let the stamp shrink below
     the stub's own counters on a phone. */
  .recap-keepsake { flex-direction: column; align-items: flex-start; }
}

/* ================================================================
   #app.trip-past — planning call-to-actions recede on a finished trip.
   The record and the settle-up lead (the Recap tab); planning stays
   reachable (a late log is still valid) but stops shouting. Issue #892.
   ================================================================ */

/* The itinerary's loud filled-green "Add stop" reverts to the quiet
   link-weight treatment its Ideas-shelf twin already uses. */
#app.trip-past .day-box-actions-row .day-box-add-btn {
  background: var(--df-panel);
  color: var(--vn-teal);
  font-weight: 500;
  border-top-color: var(--df-hair);
}
#app.trip-past .day-box-actions-row .day-box-add-btn:hover {
  background: var(--vn-teal-light);
}

/* The Ideas shelf "Add idea" button steps back a little further. */
#app.trip-past #ideas-add-btn { opacity: 0.72; }
#app.trip-past #ideas-add-btn:hover { opacity: 1; }

/* ================================================================
   Issue #2199 (#1199 Part 0) — the wrap-up banner above every tab:
   "We're home" for the host, "waiting for the host" for members, and the
   cancelled notice. Written by renderTripWrapUp() (js/trip-wrap-up.js).
   Static — no entrance motion, so nothing to answer in the
   prefers-reduced-motion block.
   ================================================================ */
.trip-wrapup {
  margin: var(--space-lg) var(--space-lg) 0;
  padding: var(--space-lg) var(--space-xl);
  border: 2px solid var(--df-hair);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
}
.trip-wrapup-title {
  margin: 0 0 var(--space-xs);
  font-size: var(--fs-lg);
}
.trip-wrapup-text {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}
.trip-wrapup-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}
.trip-wrapup-actions button { min-height: var(--tap-min); }

/* A cancelled trip is read-only except money (the server refuses the rest
   with TRIP_CANCELLED); canEditItinerary() already hides most editing, and
   the planning entry points that don't consult it step out here. */
#app.trip-cancelled #itin-create-actions,
#app.trip-cancelled .day-box-add-btn,
#app.trip-cancelled #ideas-add-btn {
  display: none;
}
