:root {
  /* default tone: grey (neutral + brighter) */
  --seam: #868685;
  --bg: #dbdbdb;
  --gap: 2px;
  --panel-hi: #f1f1f0;
  --panel-lo: #e0e0df;
  --ink: #222220;
  --ink-soft: #797977;
  --frame-a: #dbdbda;
  --frame-b: #d1d1d0;
  --frame-line: #c5c5c4;
  --badge-bg: #fcfcfc;
  --badge-line: #d2d2d1;
  --badge-chip: #ebebea;
  --panel-grad: radial-gradient(125% 115% at 50% 36%, var(--panel-hi), var(--panel-lo));
  --serif: Georgia, "Times New Roman", serif;
  --sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
html[data-tone="warm"] {
  --seam: #8d8a83; --bg: #d6d3cd;
  --panel-hi: #ece9e4; --panel-lo: #dad7d1;
  --ink-soft: #7c7970;
  --frame-a: #d4d1ca; --frame-b: #cbc8c0; --frame-line: #c0bcb4;
  --badge-bg: #fcfbf9; --badge-line: #d2cfc8; --badge-chip: #ebe9e3;
}
html[data-tone="bright"] {
  --seam: #9a9a99; --bg: #e8e8e7;
  --panel-hi: #f9f9f8; --panel-lo: #ededec;
  --ink-soft: #828280;
  --frame-a: #e7e7e6; --frame-b: #dededd; --frame-line: #d3d3d2;
  --badge-bg: #ffffff; --badge-line: #dadada; --badge-chip: #f0f0ef;
}

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
::selection { background: var(--ink); color: var(--bg); }
#root { height: 100%; }
.app { height: 100%; }
button { font: inherit; color: inherit; }

/* ---------- WALL ---------- */
.wall {
  position: relative;
  height: 100vh;
  width: 100vw;
  padding: var(--gap, 7px);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: repeat(12, 1fr);
  gap: var(--gap, 7px);
  background: var(--seam);
}

/* Persistent CV link, pinned to the wall's top-right corner over the corner tile's
   empty space. Styled like the catalog badge; its own click target (sibling of the
   tiles), so it never opens the tile beneath it. */
.wall-cv {
  position: absolute;
  top: clamp(12px, 1.6vw, 22px);
  left: clamp(12px, 1.6vw, 22px);
  z-index: 5;
  font-family: var(--sans);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .14em;
  font-size: 11px;
  color: var(--ink);
  background: var(--badge-bg);
  border: 1px solid var(--badge-line);
  border-radius: 3px;
  padding: 7px 11px;
  text-decoration: none;
  box-shadow: 0 1px 2px rgba(40, 38, 32, .08);
  transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease;
}
.wall-cv:hover { border-color: var(--ink); box-shadow: 0 4px 10px rgba(40, 38, 32, .14); transform: translateY(-1px); }
.wall-cv:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* ---------- MOBILE: stack the wall ----------
   Below this width the 12x12 pinwheel is too cramped, so the tiles collapse into
   a single, vertically-scrolling column. Desktop layout (above the breakpoint) is
   left exactly as-is. The per-tile grid placement is set inline by app.js, so it
   must be overridden with !important here. */
@media (max-width: 760px) {
  #root, .app { height: auto; }
  .wall {
    height: auto;
    min-height: 100vh;
    width: 100%;
    grid-template-columns: 1fr;
    grid-template-rows: none;
  }
  .wall .tile {
    grid-column: auto !important;
    grid-row: auto !important;
    min-height: clamp(260px, 46vh, 380px);
  }
  /* Lead with the About / identity tile (it is appended last in DOM on desktop). */
  .wall .tile--lg { order: -1; min-height: clamp(380px, 58vh, 450px); }
}

.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: clamp(10px, 1.6vh, 22px) clamp(12px, 1.4vw, 26px) clamp(12px, 1.8vh, 24px);
  background: var(--panel-grad);
  cursor: pointer;
  overflow: hidden;
  outline: none;
  transition: transform .4s cubic-bezier(.2,.7,.2,1), box-shadow .4s ease, background .4s ease;
}
.tile::after { /* hover glow overlay */
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 100% at 50% 40%, rgba(255,255,255,.55), rgba(255,255,255,0) 62%);
  opacity: 0;
  transition: opacity .4s ease;
  pointer-events: none;
}
.tile:focus-visible { box-shadow: inset 0 0 0 2px var(--ink); }

.tile-title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(13px, 1.45vw, 21px);
  letter-spacing: .2px;
  text-align: center;
  color: var(--ink);
  line-height: 1.15;
}
.tile--lg .tile-title { font-size: clamp(24px, 2.7vw, 42px); }

/* subtitle under the name on the large centre tile */
.tile-sub {
  margin: 7px 0 0;
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(15px, 1.4vw, 22px);
  letter-spacing: .2px;
  text-align: center;
  color: var(--ink-soft);
  line-height: 1.15;
}

.tile-stage {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(6px, 1.2vh, 14px) clamp(14px, 1.6vw, 26px);
  container-type: size;
}

/* ---------- SPECIMEN OBJECT (placeholder) ----------
   The specimen is a FIXED 3:2 box everywhere (wall tile, project header,
   About panel). Sized as the largest 3:2 that fits its host via container
   query units, so the shared-element morph is a pure scale — no aspect change,
   no cross-fade blend. A real image dropped in resizes contiguously. */
.obj {
  position: relative;
  width: min(100cqw, calc(100cqh * 3 / 2));
  aspect-ratio: 3 / 2;
}
/* The "me" specimen uses a square (1:1) frame instead of the 3:2 default, lightly
   cropping the 4:5 portrait top and bottom. Applies to both of its morph
   endpoints: the center wall tile (.tile--lg) and the About side image
   (.obj--side). Project specimens keep the 3:2 default. */
.obj--side {
  aspect-ratio: 1 / 1;
  width: min(100cqw, 100cqh);
}
/* On the wall, the square portrait shares vertical space with the name + tagline,
   so it sits below its full fit (leaving room under "Software Engineer") and gets
   the same hover headroom as the motion graphics. The About-page side image keeps
   its full size. */
.tile--lg .obj {
  aspect-ratio: 1 / 1;
  width: min(100cqw, calc(100cqh * 0.72));
}
/* Motion-graphic specimens get a frame matching their own artwork's aspect (set
   per-tile inline from data.js via --media-ar), instead of the 3:2 default — so
   each graphic fills its frame rather than sitting small in a mismatched box.
   The inline values from app.js override these defaults at both morph endpoints. */
.obj:has(.obj-frame--fade) {
  aspect-ratio: 2 / 1;
  width: min(100cqw, calc(100cqh * 2));
}
/* Wall-only headroom: shrink motion graphics below their full fit on the tiles, so
   the hover scale never pushes them oversized. Page headers keep the rule above
   (full size). Inline width from app.js (per-tile mediaAspect) overrides this. */
.tile .obj:has(.obj-frame--fade) {
  width: calc(min(100cqw, calc(100cqh * 2)) * 0.78);
}
.obj-frame {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background:
    repeating-linear-gradient(135deg, var(--frame-a) 0, var(--frame-a) 7px, var(--frame-b) 7px, var(--frame-b) 14px);
  border: 1px solid var(--frame-line);
  border-radius: 2px;
  box-shadow: 0 14px 30px -18px rgba(40,38,32,.5);
  transition: transform .42s cubic-bezier(.2,.7,.2,1);
  transform-origin: center 78%;
}
.obj-img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Motion-graphic specimens (a transparent-background gif/webp). No box: the frame
   is borderless and shadowless so the art floats on the site's panel background,
   with a narrow mask fading the animation out on all four edges (elements can
   emerge from any side). --fade / --fade-y set the fade band widths (small =
   fades quickly). */
.obj-frame--fade {
  --fade: 4%;
  --fade-y: 4%;
  background: transparent;
  border: none;
  box-shadow: none;
  border-radius: 0;
}
.obj-frame--fade .obj-img {
  /* Motion graphics are whole compositions — show them entire (no crop). */
  object-fit: contain;
  /* Two gradients (horizontal + vertical) intersected, so all four edges fade. */
  -webkit-mask-image:
    linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent),
    linear-gradient(to bottom, transparent, #000 var(--fade-y), #000 calc(100% - var(--fade-y)), transparent);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent),
    linear-gradient(to bottom, transparent, #000 var(--fade-y), #000 calc(100% - var(--fade-y)), transparent);
  mask-composite: intersect;
}
.obj-cap {
  position: relative;
  z-index: 2;
  font-family: var(--mono);
  font-size: clamp(9px, .82vw, 12px);
  letter-spacing: .04em;
  color: var(--ink-soft);
  background: rgba(252,251,249,.78);
  padding: 3px 7px;
  border-radius: 2px;
  white-space: nowrap;
}
/* "live" specimen — stands in for a playing GIF until a real one is dropped in */
.obj-frame--live::before {
  content: "";
  position: absolute;
  inset: -25%;
  background: radial-gradient(42% 42% at 50% 45%, rgba(255,255,255,.55), rgba(255,255,255,0) 70%);
  animation: objBreath 5.5s ease-in-out infinite alternate;
  pointer-events: none;
}
.obj-frame--live::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 35%, rgba(255,255,255,.42) 50%, transparent 65%);
  transform: translateX(-130%);
  animation: objSheen 4.8s ease-in-out infinite;
  pointer-events: none;
}
@keyframes objBreath {
  from { transform: translate(-6%, -4%) scale(1); opacity: .7; }
  to   { transform: translate(6%, 5%) scale(1.14); opacity: 1; }
}
@keyframes objSheen {
  0%      { transform: translateX(-130%); }
  55%, 100% { transform: translateX(130%); }
}
/* all specimen frames share ONE look so tile ↔ page is a clean scale morph */
.obj--header .obj-cap,
.obj--side .obj-cap { font-size: clamp(9px, .82vw, 12px); padding: 3px 7px; }

/* ---------- BADGE ---------- */
.badge-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--badge-bg);
  border: 1px solid var(--badge-line);
  border-radius: 3px;
  padding: 4px 9px 4px 4px;
  box-shadow: 0 1px 2px rgba(40,38,32,.06);
}
.badge-n {
  font-family: var(--mono);
  font-size: clamp(8px, .62vw, 10px);
  letter-spacing: .02em;
  color: var(--ink-soft);
  background: var(--badge-chip);
  border-radius: 2px;
  padding: 2px 4px;
  line-height: 1;
}
.badge-label {
  font-family: var(--sans);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .15em;
  font-size: clamp(10px, .86vw, 13px);
  color: var(--ink);
}
.badge-sub {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(11px, .95vw, 15px);
  color: var(--ink-soft);
}

/* ---------- HOVER MODES ---------- */
[data-hover="scale"] .tile:hover .obj-frame { transform: scale(1.05); }
[data-hover="lift"] .tile:hover { transform: translateY(-5px); box-shadow: 0 18px 34px -20px rgba(40,38,32,.5); z-index: 2; }
[data-hover="glow"] .tile:hover::after { opacity: 1; }
[data-hover="glow"] .tile:hover .obj-frame { box-shadow: 0 18px 30px -16px rgba(40,38,32,.6); }
html[data-transitioning] .tile:hover .obj-frame { transform: none; }
/* The active tile carries the `panel` view-transition-name so its box morphs to
   fill the screen. The seam frame is NOT drawn here (it would be baked into the
   snapshot and distort when scaled across aspect ratios) — instead it is painted
   live on ::view-transition-group(panel), below. */
html[data-transitioning] .tile.is-active { box-shadow: none; }

/* ---------- PAGES ---------- */
.page {
  min-height: 100vh;
  background: transparent;
  display: flex;
  flex-direction: column;
}
.page-bg {
  position: fixed;
  inset: 0;
  background: var(--panel-grad);
  z-index: 0;
}
.page > *:not(.page-bg) { position: relative; z-index: 1; }
.page-content { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; }
.page-head {
  position: relative;
  height: clamp(140px, 19vh, 230px);
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(8px, 1.4vh, 18px) clamp(16px, 3vw, 34px);
  container-type: size;
  overflow: visible;
}

/* ---------- ABOUT: split layout (text left, image right) ----------
   The image column is sticky and vertically centered to the viewport, so it
   tracks the scroll instead of sitting low against a tall text column.
   Vertical padding lives on the text column (not the grid) so the sticky image
   column can pin to the true top/center of the viewport. */
.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 560px) clamp(300px, 32vw, 440px);
  gap: clamp(36px, 4.5vw, 72px);
  justify-content: center;
  align-items: start;
  min-height: 100vh;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 clamp(34px, 5vw, 80px);
}
.about-text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 100vh;
  padding: clamp(48px, 9vh, 110px) 0;
}
.about-media-col {
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
.about-media {
  width: 100%;
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  container-type: size;
}
@media (max-width: 860px) {
  .about-grid { grid-template-columns: 1fr; gap: clamp(28px, 5vh, 48px); padding: clamp(36px, 7vh, 72px) clamp(24px, 6vw, 40px); align-items: stretch; }
  .about-text { min-height: 0; padding: 0; justify-content: flex-start; }
  .about-media-col { position: static; height: auto; order: -1; }
  /* Stacked on top, the square photo would otherwise be full-column-width tall and
     dominate the screen. Cap it to a modest, centered size. */
  .about-media { width: min(230px, 52vw); margin: 0 auto; }
}
.backbar {
  position: fixed;
  top: clamp(16px, 3vh, 30px);
  left: clamp(16px, 2.4vw, 36px);
  z-index: 20;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  background: var(--badge-bg);
  border: 1px solid var(--badge-line);
  border-radius: 3px;
  padding: 8px 13px;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(40,38,32,.1);
  transition: transform .2s ease, box-shadow .2s ease;
}
.backbar:hover { transform: translateX(-2px); box-shadow: 0 4px 10px rgba(40,38,32,.14); }
.backbar-arrow { font-size: 15px; }
.backbar-label {
  font-family: var(--sans);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .15em;
  font-size: 12px;
}

.page-body { flex: 1; display: flex; justify-content: center; padding: clamp(18px, 2.6vh, 38px) clamp(24px, 5vw, 60px) clamp(40px, 7vh, 90px); }
.page-col { width: 100%; max-width: 720px; }
.page-kicker {
  font-family: var(--sans);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .2em;
  font-size: 12px;
  color: var(--ink-soft);
  margin-bottom: 12px;
}
.page-title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(38px, 6vw, 76px);
  line-height: 1.02;
  margin: 0 0 8px;
  letter-spacing: -.01em;
}
/* Project / contact pages use a smaller header title than the About page. */
.page:not(.about) .page-title { font-size: clamp(27px, 3.8vw, 46px); }
.page-sub {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(15px, 1.4vw, 19px);
  color: var(--ink-soft);
  margin-bottom: 22px;
}
/* The About tagline ("Software Engineer") reads larger than a project's year. */
.about-text .page-sub { font-size: clamp(18px, 2vw, 28px); }
.prose p {
  font-family: var(--serif);
  font-size: clamp(14px, 1.05vw, 17px);
  line-height: 1.5;
  color: #34322c;
  margin: 0 0 .85em;
  text-wrap: pretty;
  max-width: 64ch;
}
.meta {
  margin: 26px 0 0;
  border-top: 1px solid var(--seam);
}
.meta-row {
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 20px;
  padding: 11px 0;
  border-bottom: 1px solid var(--seam);
}
/* No closing line — the pager's top border serves as the single divider below. */
.meta-row:last-child { border-bottom: none; }
.meta-row dt {
  font-family: var(--sans);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .14em;
  font-size: 11px;
  color: var(--ink-soft);
  padding-top: 3px;
}
.meta-row dd {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(13px, 1.05vw, 16px);
  color: var(--ink);
}
.meta-link {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--seam);
  transition: border-color .2s ease, color .2s ease;
}
.meta-link:hover { border-bottom-color: var(--ink); }

/* About-page link out to the standalone CV page. */
.cv-link {
  align-self: flex-start;
  margin: 4px 0 2px;
  font-family: var(--sans);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .14em;
  font-size: 12px;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--seam);
  padding-bottom: 3px;
  transition: border-color .2s ease;
}
.cv-link:hover { border-bottom-color: var(--ink); }
/* On phones, tighten the meta table's label column so values keep room beside it.
   Placed after the base .meta-row rule so it wins on source order. */
@media (max-width: 560px) {
  .meta-row { grid-template-columns: 100px 1fr; gap: 14px; }
}

/* On the stacked mobile wall, the square portrait was much narrower than the wide
   project graphics (185px vs 261px), so it sat with big side gaps and looked
   inconsistent down the column. Match its width to the project graphics and rely on
   the taller .tile--lg min-height (set in the stacking query) so the square has room.
   Placed here, after the base `.tile--lg .obj` rule, so it wins on source order. */
@media (max-width: 760px) {
  .tile--lg .obj { width: min(calc(100cqw * 0.78), calc(100cqh * 0.92)); }
}

/* ---------- PREV / NEXT PAGER ----------
   Text labels in a bordered box with a transparent fill — clearly buttons,
   without the filled-pill look clashing against the page. */
.pager {
  display: flex;
  justify-content: space-between;
  align-items: stretch;
  gap: 14px;
  margin-top: clamp(22px, 3.5vh, 44px);
  padding-top: clamp(20px, 3vh, 32px);
  border-top: 1px solid var(--seam);
}
.pager-btn {
  display: inline-flex;
  flex-direction: column;
  gap: 4px;
  max-width: 49%;
  background: rgba(40, 38, 32, .045);
  border: 1px solid var(--badge-line);
  border-radius: 3px;
  padding: 10px 15px;
  cursor: pointer;
  text-align: left;
  transition: border-color .2s ease, background-color .2s ease;
}
.pager-next { text-align: right; align-items: flex-end; }
.pager-btn:hover { border-color: var(--ink-soft); background: rgba(40, 38, 32, .075); }
.pager-dir {
  font-family: var(--sans);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .15em;
  font-size: 11px;
  color: var(--ink-soft);
}
.pager-name {
  font-family: var(--serif);
  font-size: clamp(15px, 1.4vw, 20px);
  line-height: 1.2;
  color: var(--ink);
}
.pager-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* ---------- VIEW TRANSITIONS ---------- */
/* The clicked tile carries name `panel`; the destination's full-viewport
   `.page-bg` shares it, so the tile box grows to fill the screen. The image
   (`hero`) glides to its new slot; the tile's title/badge fade away. */
@keyframes vtFadeOut { to { opacity: 0; } }
@keyframes vtFadeIn  { from { opacity: 0; } }
@keyframes vtRise    { from { opacity: 0; transform: translateY(16px); } }

::view-transition-group(*) {
  animation-duration: calc(.5s * var(--vt, 1));
  animation-timing-function: cubic-bezier(.5,.02,.16,1);
}
/* 1. panel: tile box grows to fill the screen FIRST.
   The seam frame is painted LIVE on the group (a real element resized every
   frame) as a box-shadow drawn OUTSIDE the box, with width matching the wall's
   --gap grid seam. Drawing it outside (not inset) means that at tile size the
   frame lands exactly ON the wall's gap — same colour, same place — instead of
   stacking a second border just inside the tile edge. That removes the brief
   double-thick edge (and the snap) at the very end of a collapse. It stays a
   crisp constant width at every size, and the snapshot is borderless so the
   old↔new crossfade between different-aspect gradient boxes is imperceptible. */
::view-transition-group(panel) {
  animation-duration: calc(.62s * var(--vt, 1));
  animation-timing-function: cubic-bezier(.5,.01,.18,1);
  animation-fill-mode: both;
  box-shadow: 0 0 0 var(--gap) var(--seam);
  z-index: 1;
}
::view-transition-image-pair(panel) { position: absolute; inset: 0; }
/* Borderless solid-gradient snapshots: stretch to FILL the group edge-to-edge
   (no letterbox gaps), and stay fully opaque (no crossfade, so there is no
   mid-transition see-through darkening). Both ends are the same gradient, so
   showing the top one throughout is seamless. */
::view-transition-old(panel),
::view-transition-new(panel) {
  animation: none;
  opacity: 1;
  object-fit: fill;
  width: 100%;
  height: 100%;
}

/* 2. hero image: begins gliding while the panel is still expanding (overlap),
   then continues to its slot after the panel settles. */
::view-transition-group(hero) {
  z-index: 3;
  animation-duration: calc(.6s * var(--vt, 1));
  animation-delay: calc(.16s * var(--vt, 1));
  animation-fill-mode: both;
  animation-timing-function: cubic-bezier(.45,.05,.15,1);
}
::view-transition-old(hero) { animation: vtFadeOut calc(.3s * var(--vt, 1)) ease both; animation-delay: calc(.16s * var(--vt, 1)); object-fit: cover; }
::view-transition-new(hero) { animation: vtFadeIn calc(.34s * var(--vt, 1)) ease both; animation-delay: calc(.16s * var(--vt, 1)); object-fit: cover; }

/* root = the WALL behaves as a constant backdrop: it never fades, it is simply
   covered by the growing panel (open) or revealed as the panel shrinks (back).
   The page's own foreground is the separate `content` layer below. */
::view-transition-group(root) { animation-duration: calc(.62s * var(--vt, 1)); }
::view-transition-old(root),
::view-transition-new(root) { animation: none; opacity: 1; }

/* content = page foreground (text, back button). Sits ABOVE the panel.
   Open: fades in AFTER the panel has grown to full size.
   Back: fades out first as the page collapses. */
::view-transition-group(content) { z-index: 2; }
::view-transition-old(content) { animation: vtFadeOut calc(.2s * var(--vt, 1)) ease both; }
::view-transition-new(content) { animation: vtRise calc(.46s * var(--vt, 1)) cubic-bezier(.3,.7,.2,1) both; animation-delay: calc(.5s * var(--vt, 1)); }

/* tile title + subtitle + badge: fade away early as the panel expands, no scaling */
::view-transition-group(tiletitle),
::view-transition-group(tilesub),
::view-transition-group(tilebadge) { z-index: 4; }
::view-transition-old(tiletitle),
::view-transition-old(tilesub),
::view-transition-old(tilebadge) { animation: vtFadeOut calc(.22s * var(--vt, 1)) ease both; }
::view-transition-new(tiletitle),
::view-transition-new(tilesub),
::view-transition-new(tilebadge) { animation: vtFadeIn calc(.26s * var(--vt, 1)) ease both; }

/* ---- direction-aware order ----
   opening: panel expands, then the image glides in.
   back: image moves FIRST and fully completes its travel, THEN the panel/border
   collapses. The panel is HELD at full size for the entire hero duration, so the
   box is never shorter than the image's path — even a far corner tile (e.g.
   bottom project) keeps the image inside the panel until it lands. */
/* back: image leads, panel begins collapsing with a SMALL overlap (the image is
   mostly home before the box starts shrinking), then both settle together. */
html[data-vtdir="back"]::view-transition-group(panel) {
  animation-delay: calc(.16s * var(--vt, 1));
  animation-duration: calc(.6s * var(--vt, 1));
  animation-fill-mode: both;
}
html[data-vtdir="back"]::view-transition-group(hero) {
  animation-delay: 0s;
  animation-duration: calc(.58s * var(--vt, 1));
  animation-timing-function: cubic-bezier(.25,.85,.3,1);
}
html[data-vtdir="back"]::view-transition-old(hero),
html[data-vtdir="back"]::view-transition-new(hero) { animation-delay: 0s; }
html[data-vtdir="back"]::view-transition-new(tiletitle),
html[data-vtdir="back"]::view-transition-new(tilesub),
html[data-vtdir="back"]::view-transition-new(tilebadge) { animation-delay: calc(.56s * var(--vt, 1)); }
/* on back, the page foreground (text/back-button) fades out immediately */
html[data-vtdir="back"]::view-transition-old(content) { animation: vtFadeOut calc(.18s * var(--vt, 1)) ease both; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation-duration: .01ms !important; animation-delay: 0 !important; }
  .tile, .obj-frame { transition: none !important; }
  .obj-frame--live::before,
  .obj-frame--live::after { animation: none !important; }
  .obj-frame--live::after { opacity: 0; }
}
