/* ── Timeline item ─────────────────────────────────────────────── */
/* Whatever the timeline mounts fades in as one piece. Only OPACITY lives here: the lift
   is applied to the card below, because a transform on a milestone scene would become
   the containing block for its artwork and trap it, and the artwork has to be fixed to
   the viewport. Opacity creates a stacking context but not a containing block, so it is
   safe here. */
.timeline-item {
  opacity: 0;
  transition: opacity 0.5s ease;
}

.timeline-item.is-visible { opacity: 1; }

/* The lift, on the card rather than the item that holds it */
.card {
  transform: translate3d(0, 18px, 0);
  transition: transform 0.5s ease;
}

.timeline-item.is-visible .card,
.card.timeline-item.is-visible {
  transform: translate3d(0, 0, 0);
}

/* ── Shared card shell ─────────────────────────────────────────── */
.card {
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  /* On a light ground the card is barely lighter than the page, so the separation
     has to come from lift rather than from contrast between the two surfaces. */
  box-shadow: var(--shadow-card);
}

.card-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 16px 0;
}

.card-meta-right {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

/* Separator between date and author, only when the author is actually present */
.card-meta-right .card-author::before {
  content: "·";
  margin-right: 8px;
}

.card-age { font-weight: 600; font-size: 0.95rem; }
.card-author { color: var(--text-dim); font-size: 0.78rem; }
.card-date { color: var(--text-dim); font-size: 0.78rem; }

.card-caption {
  margin: 0;
  padding: 10px 16px 16px;
  color: var(--text);
  font-size: 0.95rem;
}

/* The caption normally supplies the card's bottom padding. Without one the meta row is
   the last thing in the card and would sit flush against the edge, so it takes over. */
.card:not(.card--quote):not(:has(.card-caption)) .card-meta {
  padding-bottom: 16px;
}

/* ── Crop box ──────────────────────────────────────────────────── */
/* Shows only a photo's crop rectangle, by oversizing the image inside an overflow box
   and offsetting it until the chosen region fills the box. object-fit cannot express an
   arbitrary rectangle, and every photo gets this treatment — an uncropped one is simply
   cropped to all of itself, so there is one code path rather than two.

   --cx/--cy/--cw/--ch are the crop rect, 0–1. Scaling by 1/--cw makes the crop's width
   fill the box; the offsets are divided by the same factor because they are percentages
   of the BOX, not of the image. */
.crop-box {
  position: relative;
  overflow: hidden;
  background: var(--surface-2);
}

/* The rotor is the crop box's only child, and stands where the media used to: it carries
   the ORIENTED aspect ratio, which is what the media's intrinsic ratio supplied before
   rotation existed. A transform never changes a layout box, so rotating the media itself
   would leave this maths measuring the shape it used to be. */
.crop-box > .rotor {
  position: absolute;
  width: calc(100% / var(--cw, 1));
  height: auto;
  max-width: none;
  left: calc(var(--cx, 0) / var(--cw, 1) * -100%);
  top: calc(var(--cy, 0) / var(--ch, 1) * -100%);
  display: block;
  background: none;
  overflow: hidden;
}

/* Upright: the media simply fills its rotor. Shared with the lightbox, which builds
   the same rotor around the same media. */
.rotor > .rotor-media {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  max-width: none;
  display: block;
  background: none;
  object-fit: fill; /* the rotor already has the media's exact shape */
  transform: translate(-50%, -50%) rotate(calc(var(--rot, 0) * 1deg));
}

/* A quarter turn: the media is laid out across the rotor's OTHER axis, then turned into
   place. --oar is the rotor's width/height, so 100%/--oar is its height expressed against
   its width, and 100%*--oar its width expressed against its height. */
.rotor.is-quarter > .rotor-media {
  width: calc(100% / var(--oar, 1));
  height: calc(100% * var(--oar, 1));
}

/* Reserve space from intrinsic dimensions to prevent reflow on load */
.card-photo {
  display: block;
  width: 100%;
  height: auto;
  background: var(--surface-2);
}

/* ── Layout: portrait (tall single photo) ──────────────────────── */
.card--portrait .photo-frame { aspect-ratio: var(--ar, 3 / 4); overflow: hidden; }

/* ── Layout: landscape (wide single photo) ─────────────────────── */
.card--landscape .photo-frame { aspect-ratio: var(--ar, 3 / 2); overflow: hidden; }

.photo-frame {
  position: relative;
  width: 100%;
}

/* The frame's aspect already matches the crop exactly, so the box just fills it. Scaled
   up a touch so the parallax has somewhere to drift without exposing an edge — `scale`
   rather than a stretched box, which would distort the crop. The separate `scale`
   property leaves `transform` free for the per-frame parallax write. */
.photo-frame .crop-box--exact {
  position: absolute;
  inset: 0;
  scale: 1.1;
  will-change: transform;
}

/* ── Layout: grid (2–4 photos) ─────────────────────────────────── */
.card--grid .grid-photos {
  display: grid;
  gap: 3px;
  grid-template-columns: repeat(2, 1fr);
}

.card--grid.count-2 .grid-photos { grid-template-columns: repeat(2, 1fr); }
.card--grid.count-3 .grid-photos { grid-template-columns: repeat(2, 1fr); }
.card--grid.count-4 .grid-photos { grid-template-columns: repeat(2, 1fr); }

/* The three-photo hero spans the full width, and is the one grid cell whose shape comes
   from a photo rather than from the grid — so cropping the lead photo reshapes it.
   See leadAspect in layout-engine.js. */
.card--grid.count-3 .grid-cell:first-child {
  grid-column: 1 / -1;
  aspect-ratio: var(--hero-ar, 1);
}

.grid-cell { position: relative; aspect-ratio: 1 / 1; overflow: hidden; }

/* A cell's shape rarely matches the crop's, so the crop still has to cover it. Sized by
   aspect-ratio against both minimums — the box grows until it covers on both axes.

   --px/--py are NOT the focal point: they are the anchor that puts the focal point as
   near the cell's centre as covering allows, computed by coverAlign() in the engine.
   Anchoring on the focal fraction directly would merely align it, so a focal point of
   0.15 would sit 15% across the cell instead of in the middle of it. */
.grid-cell > .crop-box,
.carousel-slide > .crop-box {
  position: absolute;
  top: calc(var(--py, 0.5) * 100%);
  left: calc(var(--px, 0.5) * 100%);
  translate: calc(var(--px, 0.5) * -100%) calc(var(--py, 0.5) * -100%);
  aspect-ratio: var(--crop-ar, 1);
  min-width: 100%;
  min-height: 100%;
}

/* ── Layout: carousel (5+ photos) ──────────────────────────────── */
/* Native CSS scroll-snap does the scrolling — no JS per frame, and momentum feels
   right on a phone in a way a scripted carousel never quite does. */
.card--carousel .carousel { position: relative; }

.carousel-track {
  display: flex;
  gap: 3px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain; /* swiping the strip must not drag the page sideways */
}

.carousel-track::-webkit-scrollbar { display: none; }

.carousel-slide {
  flex: 0 0 100%;
  /* Set per carousel from the photos it holds — see carouselAspect in layout-engine.js */
  aspect-ratio: var(--slide-ar, 4 / 3);
  scroll-snap-align: center;
  overflow: hidden;
}

.carousel-slide { position: relative; }

.carousel-dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 10px;
  display: flex;
  justify-content: center;
  gap: 6px;
  pointer-events: none;
}

.carousel-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(251, 252, 249, 0.55);
  box-shadow: 0 1px 2px rgba(46, 58, 48, 0.35);
  transition: transform 0.2s ease, background-color 0.2s ease;
}

.carousel-dot.is-active {
  background: #fbfcf9;
  transform: scale(1.45);
}

/* ── Edit affordance ───────────────────────────────────────────── */
/* Only rendered on posts you wrote. Top-right, opposite the milestone chip. */
.card-edit {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 3;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: none;
  border-radius: 50%;
  /* Sits over photos as often as over card background, so it carries its own contrast */
  background: rgba(251, 252, 249, 0.86);
  color: var(--text);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  box-shadow: 0 1px 4px rgba(46, 58, 48, 0.22);
  -webkit-tap-highlight-color: transparent;
}

.card-edit:active { transform: scale(0.94); }

/* Positioned so the edit button can pin to its corner, and lifted above the milestone
   artwork. EVERY card needs the z-index, not just a milestone's own: the artwork is fixed
   to the viewport, so it stays on screen while other cards scroll past it, and a card
   earlier in the DOM than the milestone would otherwise be painted over — the artwork
   showing through its lower half. */
.card { position: relative; z-index: 1; }

/* Photos open full-size on tap; say so with the cursor on a pointer device */
@media (hover: hover) {
  .card-photo[data-full] { cursor: zoom-in; }
}

/* ── Layout: quote (text-only) ─────────────────────────────────── */
.card--quote {
  background: linear-gradient(150deg, var(--surface-2), var(--surface));
  padding: 28px 24px;
  text-align: center;
}

.card--quote .quote-text {
  margin: 0;
  font-size: 1.35rem;
  line-height: 1.5;
  font-style: italic;
}

.card--quote .quote-mark {
  display: block;
  font-size: 2.4rem;
  color: var(--accent-ink);
  line-height: 0.6;
  margin-bottom: 6px;
}

/* ── Treatment: milestone highlight ────────────────────────────── */
.card--highlight {
  position: relative;
  box-shadow: 0 0 0 2px var(--accent), var(--shadow-lift);
}

/* The milestone sketch sits out on the page background beside the card, alternating
   sides down the timeline (app.js assigns side-left / side-right). It never moves —
   it only fades up as its card comes into view and fades away as it leaves. */
.milestone-scene { position: relative; }

/* FIXED to the viewport, not to the card. The card scrolls past it; the artwork never
   moves, it only fades. Anchored beside where the timeline sits — half the viewport plus
   half the card's width — and pulled back a little so the card overlaps its inner edge. */
.milestone-art-wrap {
  position: fixed;
  top: 50%;
  width: 400px;
  aspect-ratio: var(--art-ar, 1);
  transform: translateY(-50%);
  pointer-events: none;
  z-index: 0; /* the card sits above it */
  /* --art-progress is written per frame by app.js: 0 at the edges of the viewport, 1
     when the card is the thing you are looking at. No transition — the scroll position
     IS the animation, and easing it would only make it lag behind the card. */
  opacity: calc(var(--art-progress, 0) * var(--art-peak, 0.5));
}

.milestone-scene.side-right .milestone-art-wrap {
  left: calc(50% + var(--maxw) / 2 - 70px);
}

.milestone-scene.side-left .milestone-art-wrap {
  right: calc(50% + var(--maxw) / 2 - 70px);
}

/* Cards paint over the artwork — see the z-index on .card. Milestone artwork is fixed to
   the viewport, so it can end up behind ANY card on screen, not only its own. */

.milestone-art {
  width: 100%;
  height: 100%;
  /* Sage, so the milestone treatment reads green; the chip supplies the blue */
  color: var(--accent-ink);
}

/* Real artwork is applied as a mask rather than painted: the files are black line art,
   and masking makes them take --accent-ink exactly as the SVG placeholders do. Drop a
   PNG in and it joins the palette instead of fighting it. */
.milestone-art--image {
  background-color: var(--accent-ink);
  -webkit-mask-image: var(--art-src);
  mask-image: var(--art-src);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

/* Without scroll-driven fading there is no reason to hide it — settle at the peak. */
@media (prefers-reduced-motion: reduce) {
  .milestone-art-wrap { opacity: var(--art-peak, 0.5); }
}

/* A phone has no room beside the card — the artwork is a wide-screen grace note, and
   hiding it beats letting it collide with the timeline. At 400px it needs more room than
   the old 200px did: below this the outer edge would be cut off by the viewport, which
   reads as broken rather than as bleeding. */
@media (max-width: 1023px) {
  .milestone-art-wrap { display: none; }
}

/* No band above it now, so the chip on a quote card would land on the meta row again */
.card--quote.card--highlight { padding-top: 50px; }

.milestone-badge {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--blue);
  color: #12303f;
  font-size: 0.8rem;
  font-weight: 700;
  box-shadow: 0 2px 8px rgba(46, 58, 48, 0.18);
}

.milestone-badge::before { content: "★"; }

/* ── Video and motion photos ───────────────────────────────────── */
/* A card is a thumbnail, not a player: the badge says how long the clip is and the
   lightbox does the playing. Motion photos get no badge — they are already moving,
   which is the whole of what they have to say. */
.video-badge {
  position: absolute;
  right: 8px;
  bottom: 8px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px 3px 6px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: #fff;
  /* Tinted with the page's own sage rather than black, like --shadow-card. */
  background: rgba(var(--text-rgb), 0.55);
  backdrop-filter: blur(6px);
  /* A control, not decoration: it plays the clip in place. Tapping the picture around it
     still expands. Reset the button chrome the browser would otherwise supply. */
  border: 0;
  cursor: pointer;
  font-family: inherit;
  -webkit-tap-highlight-color: transparent;
}

.video-badge:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}

/* The play triangle, drawn rather than shipped as an asset. */
.video-badge::before {
  content: '';
  /* A border triangle only reads as a triangle with a ZERO content box — give it a size
     and the left border becomes a trapezoid with a transparent rectangle beside it, which
     looks like the icon has been cut off. The border-box is still 7x8, so it lays out and
     centres as if it had dimensions. */
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 4px 0 4px 7px;
  border-color: transparent transparent transparent currentColor;
}

/* Playing: the triangle becomes a pause bar pair, drawn from the same box so the badge
   does not change width and shift under the thumb mid-tap. */
.video-badge.is-playing::before {
  border: 0;
  background: currentColor;
  /* The second bar is a shadow, so it paints 5px to the right while taking no layout
     width at all. The margin makes up the whole difference — 2px laid out, 7px seen —
     or the label crowds against a bar the browser does not know is there. */
  box-shadow: 5px 0 0 currentColor;
  width: 2px;
  height: 9px;
  margin-right: 5px;
}

/* A video fills its rotor exactly like an image does. object-fit: fill is right here for
   the same reason it is on .card-photo: the rotor already has the media's exact shape,
   so there is nothing to letterbox. */
.card-video {
  background: var(--surface-2);
}

/* Someone who has asked for less movement is not handed a looping video — motion-play.js
   simply never starts it, and a paused video already shows its poster, which is the same
   picture holding still. Nothing to do here. */

/* ── The motion photo's pause button ───────────────────────────── */
/* A loop you cannot stop is tiring to sit with, but a control permanently parked on a
   photo is its own kind of noise — so it waits until the photo is pointed at. */
.video-badge--motion {
  padding: 6px;
  width: 28px;
  height: 28px;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.15s ease;
}

/* Revealed by pointing at the cell, or by tabbing to the button itself. */
.grid-cell:hover .video-badge--motion,
.carousel-slide:hover .video-badge--motion,
.photo-frame:hover .video-badge--motion,
.video-badge--motion:focus-visible {
  opacity: 1;
}

/* Held still: the control STAYS, whatever the pointer is doing. A paused loop with no
   visible way to restart it does not read as paused — it reads as broken. */
.is-held .video-badge--motion {
  opacity: 1;
}

/* No hover to speak of, so the button simply lives there — quieter than on desktop,
   since on a phone it is the only way to reach it at all. */
@media (hover: none) {
  .video-badge--motion { opacity: 0.55; }
}

/* Icon only, with no label to be spaced away from — so the play triangle drops the gap
   and centres on its own. The pause bars KEEP the base margin: it is not a gap there but
   the layout width the shadow-drawn second bar never claims, and without it the pair
   would sit left of centre. */
.video-badge--motion::before { margin-right: 0; }
