/* ===========================================================================
   Layout + components. Every selector is nested under .pv2 (set on <body>)
   so these rules cannot affect the original portfolio at /index.html.
   =========================================================================== */

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

html.pv2-html, .pv2 {
  height: 100%;
  overflow: hidden;            /* the timeline owns movement, not the document */
  overscroll-behavior: none;
}

.pv2 {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-base);
  font-weight: var(--fw-regular);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.pv2 :focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: 2px;
}
.pv2 :focus:not(:focus-visible) { outline: none; }

.skip-link {
  position: absolute; left: var(--sp-4); top: -4rem; z-index: 300;
  background: var(--text); color: var(--bg);
  padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-control);
  text-decoration: none; font-size: var(--fs-sm);
  transition: top var(--motion-ui) var(--ease-fast-out);
}
.skip-link:focus { top: var(--sp-4); }

.sr-only {
  position: absolute !important; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}


/* ===== Header — identity top-left, links top-right ======================= */
.hd {
  position: fixed; z-index: 60;
  top: var(--page-top); left: var(--page-inline); right: var(--page-inline);
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-6);
  pointer-events: none;        /* the timeline behind stays draggable */
}
.hd > * { pointer-events: auto; }

.hd__name {
  font-size: var(--fs-name); font-weight: var(--fw-medium);
  line-height: 1.05; letter-spacing: -0.01em; margin: 0;
}
/* Year odometer. The "20" prefix never moves, fades or shifts; only the
   two-digit suffix rolls inside a fixed-height, overflow-hidden slot. */
.hd__year {
  display: inline-flex; align-items: flex-start;
  font-size: var(--fs-year); font-weight: var(--fw-regular);
  color: var(--text-muted);
  margin: 2px 0 0;
  font-variant-numeric: tabular-nums;   /* no width shift between 25 and 26 */
  line-height: 1.05;
}
.yr-prefix, .yr-slot, .yr-reel > span {
  display: block; height: 1.05em; line-height: 1.05;
}
.yr-slot { overflow: hidden; }
.yr-reel {
  display: block;
  transition: transform var(--motion-ui) var(--ease-fast-out);
  will-change: transform;
}

.hd__links { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.hd__links a, .hd__links button {
  font-family: inherit; font-size: var(--fs-nav); line-height: 1.5;
  color: var(--text-muted); text-decoration: none;
  background: none; border: 0; padding: 0; cursor: pointer;
  transition: color var(--motion-fast) var(--ease-fast-out);
}
.hd__links a:hover, .hd__links a:focus-visible,
.hd__links button:hover, .hd__links button:focus-visible { color: var(--text); }

/* ===== Main timeline ===================================================== */
.stage {
  position: fixed; inset: 0;
  overflow: hidden;
  touch-action: pinch-zoom;    /* single-finger drag reaches our handlers */
  cursor: grab;
  transition: opacity var(--duration-project-close) var(--ease-fluid);
}
.stage.is-dragging { cursor: grabbing; }
/* No dimming: the viewer is an opaque surface that already covers the timeline,
   and a translucent timeline under a fading viewer is what produced the grey
   wash on close. The timeline stays plain white underneath at all times. */
.pv2.is-viewing .stage, .pv2.is-abouting .stage { pointer-events: none; }

.track {
  position: relative;
  display: flex; align-items: center;
  height: 100%;
  padding-inline: var(--page-inline);
  padding-top: calc(var(--page-top) + var(--header-h));   /* clear the header */
  gap: var(--project-gap);
  width: max-content;
  /* Compositor-friendly: the track only ever moves via translate3d. */
  will-change: transform;
  backface-visibility: hidden;
}

.panel { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-start; }

/* --- Project --- */
.project__open {
  display: block; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; text-align: left;
  cursor: pointer;
  transition: transform var(--motion-ui) var(--ease-fast-out);
}
.project__open:hover { transform: translateY(-3px); }
.project__open:hover .frame { border-color: var(--border-strong); }
.project__open.is-flip-target .frame > * { visibility: hidden; }



/* ===== About — a separate editorial view, not a timeline panel =========== */
.aboutview {
  position: fixed; inset: 0; z-index: 110;
  background: var(--bg);
  overflow-y: auto;
  will-change: transform;
}
.aboutview[hidden] { display: none; }

/* About is an in-page state change, not a navigation. Its content slides in
   from the right and settles; the surface itself never fades, so there is no
   page-wide wash. On the way out the surface steps aside so the timeline —
   mounted underneath the whole time — is what the departing content leaves. */
.aboutview__grid,
.aboutview .viewer__back {
  opacity: 0;
  transform: translateX(32px);
}
.aboutview.is-open .aboutview__grid,
.aboutview.is-open .viewer__back {
  opacity: 1;
  transform: none;
  transition: opacity var(--about-duration) var(--about-ease),
              transform var(--about-duration) var(--about-ease);
}
/* After the open rules so source order wins on equal specificity. */
.aboutview.is-closing { background: transparent; pointer-events: none; }
.aboutview.is-closing .aboutview__grid,
.aboutview.is-closing .viewer__back {
  opacity: 0;
  transform: translateX(32px);
  transition: opacity var(--about-exit) var(--about-ease),
              transform var(--about-exit) var(--about-ease);
}

/* One centred composition: a bounded inner wrapper with equal breathing room on
   both sides, rather than two columns pushed out to the viewport edges. */
.aboutview__grid {
  min-height: 100%;
  display: grid;
  grid-template-columns: minmax(0, 42fr) minmax(0, 53fr);
  align-items: center;
  gap: clamp(2rem, 4.5vw, 5rem);
  width: min(86vw, 1320px);
  margin-inline: auto;
  padding: calc(var(--page-top) + 3rem) 0 calc(var(--page-top) + 3rem);
}
.aboutview__bio {
  margin: 0;
  font-size: clamp(0.95rem, 1.2vw, 1.1rem);
  line-height: 1.7;
  max-width: 60ch;          /* keeps the measure at roughly 45–65 characters */
}
.aboutview__links {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 6px;
  list-style: none; margin: var(--sp-7) 0 0; padding: 0;
}
.aboutview__links a {
  font-size: var(--fs-sm); color: var(--text-muted); text-decoration: none;
  transition: color var(--motion-fast) var(--ease-fast-out);
}
.aboutview__links a:hover, .aboutview__links a:focus-visible { color: var(--text); }
.aboutview__links .is-todo { font-size: var(--fs-sm); color: var(--text-faint); }

.aboutview__figure { margin: 0; }
.aboutview__figure img {
  display: block; width: 100%; height: auto;   /* natural aspect, not cropped */
  border-radius: 18px;                          /* subtle, not a media frame */
}

/* ===== Media frames ====================================================== */
.frame {
  position: relative; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-media);
  transition: border-color var(--motion-ui) var(--ease-fast-out);
}
.frame--landscape { aspect-ratio: 16 / 9;  width: var(--frame-w-landscape); }
.frame--phone     { aspect-ratio: 9 / 19.5; height: var(--frame-h-phone); }
.frame--square    { aspect-ratio: 1 / 1;    height: var(--frame-h-square); }
/* --ar is set inline from the media item's real dimensions, so the rounded
   frame matches the asset instead of boxing it inside dead space. */
.frame--auto {
  aspect-ratio: var(--ar, 1.3333);
  width: min(var(--frame-w-landscape), calc(var(--frame-h-auto) * var(--ar, 1.3333)));
  transition: width var(--motion-ui) var(--ease-fast-out), aspect-ratio var(--motion-ui) var(--ease-fast-out);
}

/* V6 §7 — media fills its rounded frame. Frame aspects are chosen to match the
   assets, so nothing is cropped; `fit: 'contain'` in the data is the escape
   hatch for an asset that must not be. */

.frame > img.is-contain, .frame > video.is-contain { object-fit: contain; }
.frame > img, .frame > video {
  display: block; width: 100%; height: 100%;
  object-fit: cover;
  background: var(--surface);
  -webkit-user-drag: none; user-select: none;
}

/* Media fades in once it actually paints. Until then the frame is simply its
   neutral surface — no copy, no filename, no broken-image icon, and no layout
   shift, because the frame's size comes from its aspect ratio, not the media. */
.frame > img,
.frame > video {
  opacity: 0;
  transition: opacity 180ms var(--ease-fast-out);
}
.frame.is-loaded > img,
.frame.is-loaded > video { opacity: 1; }

/* ===== Expanded project — a second horizontal timeline =================== */
.viewer {
  position: fixed; inset: 0; z-index: 100;
  background: var(--bg);
  will-change: transform;
}
.viewer[hidden] { display: none; }
/* Staged reveal of internal UI only — never the surface itself. Driven by
   transitions rather than keyframes: an animation cannot be interpolated away
   by a later transition, so the collapse would pop these off instead of fading
   them. Delays stage them behind the frame expansion. */
.viewer .vstage--info,
.viewer .vstage__label,
.viewer .dots,
.viewer .viewer__back {
  opacity: 0;
  transform: translateY(6px);
}
.viewer.is-ready .vstage--info,
.viewer.is-ready .vstage__label {
  opacity: 1; transform: none;
  transition: opacity var(--duration-ui-reveal) var(--ease-fast-out) var(--delay-meta),
              transform var(--duration-ui-reveal) var(--ease-fast-out) var(--delay-meta);
}
.viewer.is-ready .dots,
.viewer.is-ready .viewer__back {
  opacity: 1; transform: none;
  transition: opacity var(--duration-ui-reveal) var(--ease-fast-out) var(--delay-controls),
              transform var(--duration-ui-reveal) var(--ease-fast-out) var(--delay-controls);
}

/* Collapsing back into the source frame. The backdrop steps aside so the live
   timeline is what the shrinking frame lands on, and every piece of internal UI
   clears out first. The media surface itself never changes opacity.
   These come after the reveal rules so they win on source order. */
.viewer.is-collapsing {
  background: transparent;
  pointer-events: none;
}
.viewer.is-collapsing .vstage--info,
.viewer.is-collapsing .vstage__label,
.viewer.is-collapsing .dots,
.viewer.is-collapsing .viewer__back,
.viewer.is-collapsing .vplayer,
.viewer.is-collapsing .vplay {
  opacity: 0;
  transform: none;
  transition: opacity var(--duration-ui-exit) var(--ease-fast-out);
}
/* Non-anchor stages carry no transition of their own; hide them outright so
   stray media never floats over the timeline during the collapse. */
.viewer.is-collapsing .vstage { opacity: 0; transition: opacity var(--duration-ui-exit) var(--ease-fast-out); }
.viewer.is-collapsing .vstage.is-anchor { opacity: 1; }

/* State machine: idle -> dragging -> settling | exiting -> closed.
   No transition while dragging so the surface tracks the pointer exactly.
   Both exits are purely spatial — opacity is never animated here. */
.viewer.is-settling {
  transition: transform var(--duration-drag-cancel) var(--ease-fluid);
}
.viewer.is-exiting {
  transition: transform var(--duration-drag-complete) var(--ease-fluid);
}
.viewer.is-closing {
  transition: transform var(--duration-project-close) var(--ease-fluid);
}
/* The collapsing frame animates its own geometry, radius included. */
.viewer.is-collapsing .frame.is-anchor-frame {
  transition: transform var(--duration-project-close) var(--ease-fluid),
              border-radius var(--duration-project-close) var(--ease-fluid);
}
/* The timeline stays put underneath — no flicker as the project moves. */
/* Slightly smaller than the timeline frames so the info panel and the first
   media stage can both be read at once (lateral context, not edge-to-edge). */
.viewer {
  --frame-w-landscape: clamp(260px, 52vw, 940px);
  --frame-h-phone:     clamp(320px, 62vh, 640px);
  --frame-h-square:    clamp(260px, 56vh, 620px);
  --frame-h-auto:      clamp(260px, 58vh, 620px);
}

.viewer__back {
  position: fixed; z-index: 120;
  top: var(--page-top); left: var(--page-inline);
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3) var(--sp-2) 0;
  border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--fs-sm); color: var(--text-muted);
  transition: color var(--motion-fast) var(--ease-fast-out);
}
.viewer__back:hover { color: var(--text); }
.viewer__back svg { width: 15px; height: 15px; }

.vscroll {
  position: absolute; inset: 0;
  overflow: hidden;
  touch-action: pinch-zoom;
  cursor: grab;
}
.vscroll.is-dragging { cursor: grabbing; }

.vtrack {
  position: relative;
  display: flex; align-items: center;
  height: 100%;
  padding-inline: var(--page-inline);
  padding-top: calc(var(--page-top) + var(--header-h));
  padding-bottom: 3.5rem;                /* room for the dots */
  gap: var(--project-gap);
  width: max-content;
  will-change: transform;
  backface-visibility: hidden;
}

.vstage { flex: 0 0 auto; display: flex; flex-direction: column; justify-content: center; }

/* --- Stage 0: project information --- */
.vstage--info { width: clamp(240px, 26vw, 380px); gap: var(--sp-2); }
/* One compact factual block: name, role, achievement and year read as project
   identity, so none of them are greyed out. Only the description softens. */
.vinfo__title { font-size: var(--fs-xl); font-weight: 600; margin: 0; letter-spacing: -0.01em; line-height: 1.15; }
.vinfo__role  { font-size: var(--fs-base); font-weight: var(--fw-regular); color: var(--text); margin: 5px 0 0; line-height: 1.4; }
.vinfo__org   { font-size: var(--fs-sm); color: var(--text-muted); margin: 1px 0 0; line-height: 1.4; }
.vinfo__achievement { font-size: var(--fs-base); font-weight: var(--fw-regular); color: var(--text); margin: 3px 0 0; line-height: 1.4; }
.vinfo__year  { font-size: var(--fs-base); font-weight: var(--fw-regular); color: var(--text); margin: 6px 0 0; line-height: 1.4; font-variant-numeric: tabular-nums; }
.vinfo__summary { font-size: var(--fs-sm); color: var(--text-muted); margin: var(--sp-5) 0 0; line-height: 1.6; }
.vinfo__note  { font-size: var(--fs-sm); color: var(--text-faint); margin: var(--sp-2) 0 0; }
.vinfo__live {
  margin-top: var(--sp-5);
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border: 1px solid var(--border-strong); border-radius: 50%;
  color: var(--text-muted); text-decoration: none;
  transition: color var(--motion-fast) var(--ease-fast-out),
              border-color var(--motion-fast) var(--ease-fast-out);
}
.vinfo__live:hover, .vinfo__live:focus-visible { color: var(--text); border-color: var(--text); }
.vinfo__live svg { width: 16px; height: 16px; }

.vinfo__collab { margin: var(--sp-6) 0 0; }
.vinfo__collab-h { font-size: var(--fs-xs); color: var(--text-faint); letter-spacing: 0.08em; text-transform: uppercase; margin: 0 0 var(--sp-1); }
.vinfo__collab ul { list-style: none; margin: 0; padding: 0; }
.vinfo__collab li { font-size: var(--fs-sm); color: var(--text-muted); }


/* --- Media stages --- */
.vstage--media { gap: var(--sp-4); }
.vstage__label {
  display: flex; align-items: baseline; gap: var(--sp-3);
  margin-top: var(--sp-4);
}
.vstage__title { font-size: var(--fs-sm); color: var(--text); margin: 0; }
.vstage__year  { font-size: var(--fs-xs); color: var(--text-faint); margin: 0; font-variant-numeric: tabular-nums; }

/* ===== Video player ======================================================
   Native controls are not used: their bar spans the full media width and the
   ~56px corner radius clips its ends, and Chrome keeps its own centre play
   overlay up. The transport lives in an overlay inset from the corners; the
   centre play button is centred on the media frame independently of it. */

/* Centre play — a bare white icon, no circle, disc, border or backdrop.
   Positioned against .frame (the visible media area) rather than any wrapper
   that also holds the transport, so appearing controls cannot shift it. */
.vplay {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  display: grid; place-items: center;
  width: auto; height: auto;
  padding: 0; line-height: 0;
  border: 0; background: transparent; box-shadow: none;
  color: #fff; cursor: pointer;
  z-index: 2;
  transition: opacity var(--motion-fast) var(--ease-fast-out);
}
/* Scoped to .vplay so the transport bar's own small play glyph, which shares
   the .play-icon class, keeps its size and colour. */
.vplay .play-icon {
  display: block;
  width: clamp(34px, 3.2vw, 48px);
  height: clamp(34px, 3.2vw, 48px);
  fill: #fff;
  /* Keeps a white icon legible over light media. */
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));
  transform: translateX(1px);   /* optical only — the triangle leans left */
  opacity: 0.95;
  transition: opacity var(--motion-fast) var(--ease-fast-out);
}
.vplay:hover .play-icon,
.vplay:focus-visible .play-icon { opacity: 1; }
/* Quiet state change: no scale, no bounce. */
.frame.is-playing .vplay {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
}

/* Transport overlay — a separate layer, inset clear of the rounded corners. */
.vplayer {
  position: absolute;
  inset: var(--video-safe-y) var(--video-safe-x);
  pointer-events: none;
  z-index: 1;
}
.vplayer > * { pointer-events: auto; }

.vplayer__bar {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; gap: 10px;
  padding: 7px 12px;
  border-radius: 999px;
  background: rgba(255,255,255,0.93);
  border: 1px solid var(--border);
  opacity: 0;
  transition: opacity var(--motion-ui) var(--ease-fast-out);
}
.frame.is-controls-visible .vplayer__bar,
.frame:not(.is-playing) .vplayer__bar,
.vplayer:focus-within .vplayer__bar { opacity: 1; }

.vctl {
  flex: 0 0 auto;
  width: 28px; height: 28px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; background: none;
  color: var(--text-muted); cursor: pointer;
  transition: color var(--motion-fast) var(--ease-fast-out);
}
.vctl:hover, .vctl:focus-visible { color: var(--text); }
.vctl svg { width: 15px; height: 15px; display: block; }

.vseek {
  flex: 1 1 auto; min-width: 40px; height: 18px; margin: 0;
  -webkit-appearance: none; appearance: none; background: none; cursor: pointer;
}
.vseek::-webkit-slider-runnable-track { height: 3px; border-radius: 2px; background: var(--border-strong); }
.vseek::-moz-range-track { height: 3px; border-radius: 2px; background: var(--border-strong); }
.vseek::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--text); border: 0; margin-top: -4px;
}
.vseek::-moz-range-thumb { width: 11px; height: 11px; border-radius: 50%; background: var(--text); border: 0; }

.vtime {
  flex: 0 0 auto;
  font-size: var(--fs-xs); color: var(--text-muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* --- Progress dots — anchored, stable --- */
.dots {
  position: fixed; z-index: 120;
  left: 0; right: 0; bottom: clamp(16px, 3.5vh, 34px);
  display: flex; justify-content: center; align-items: center; gap: 10px;
}
/* The dot IS the circle. It was previously a 26px flex box with a pseudo-element
   inside, which could shrink on the cross axis and render as an oval. */
.dot {
  position: relative;
  flex: 0 0 var(--dot-size);
  width: var(--dot-size); height: var(--dot-size);
  aspect-ratio: 1 / 1;
  padding: 0; border: 0; border-radius: 50%;
  background: var(--dot);
  cursor: pointer;
  transition: background var(--motion-ui) var(--ease-fast-out);
}
/* Invisible larger hit area — the visible circle stays small. */
.dot::before { content: ''; position: absolute; inset: -8px; border-radius: 50%; }
.dot[aria-current="true"] { background: var(--dot-active); }
.dot:hover { background: var(--text-muted); }
.dot:focus-visible { outline: var(--focus-ring); outline-offset: 4px; }

/* ===== Mobile swipe affordance ========================================== */
/* Informational text only — no pill, no card, and pointer-events: none so it
   can never intercept the very gesture it is advertising. */
.swipehint {
  position: fixed;
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 22px);
  transform: translateX(-50%);
  z-index: 90;                      /* under the viewer and About */
  margin: 0;
  font-size: 13px;
  font-weight: var(--fw-regular);
  color: rgba(0, 0, 0, 0.45);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
.swipehint.is-shown { opacity: 1; }

/* ===== Touch feedback =================================================== */
/* The browser's native tap highlight draws a dark box over text links and
   controls on mobile. Removing it must not remove keyboard focus, which is
   handled separately by :focus-visible and is untouched. */
.pv2,
.pv2 a,
.pv2 button,
.pv2 [role="button"] {
  -webkit-tap-highlight-color: transparent;
}
/* A quiet opacity response replaces it, so a tap still registers visibly. */
.hd__links a:active,
.hd__links button:active,
.viewer__back:active,
.vinfo__live:active,
.aboutview__links a:active,
.vctl:active,
.vplay:active,
.dot:active { opacity: 0.55; }

/* ===== Responsive — timeline stays horizontal at every width ============= */
@media (max-width: 1023px) {
  .pv2 { --frame-w-landscape: clamp(260px, 76vw, 720px); --frame-h-phone: clamp(320px, 58vh, 560px); }
  .vstage--info { width: min(76vw, 420px); }
}

@media (max-width: 860px) {
  .aboutview__grid { grid-template-columns: minmax(0, 1fr); align-content: start; width: min(90vw, 640px); }
  .aboutview__bio { max-width: none; }
  .aboutview__figure { order: -1; }
}

@media (max-width: 600px) {
  /* Frames are sized so the next project always peeks past the right edge —
     the primary signal that the timeline continues horizontally. The widest
     frame is the worst case, so it sets the peek. The square frame was also
     wider than the viewport at 390px and is now capped. */
  .pv2 {
    --frame-w-landscape: 74vw;
    --frame-h-phone: clamp(300px, 50vh, 460px);
    --frame-h-square: min(66vw, 40vh);
    --project-gap: 12vw;
    --header-h: 132px;
  }
  .hd { flex-direction: column; gap: var(--sp-4); }
  .hd__links { flex-direction: row; flex-wrap: wrap; align-items: flex-start; gap: var(--sp-2) var(--sp-4); }
  .vstage--info { width: 82vw; }
}

@media (prefers-reduced-motion: reduce) {
  .pv2 *, .pv2 *::before, .pv2 *::after {
    animation-duration: 1ms !important;
    transition-duration: 1ms !important;
  }
  .project__open:hover { transform: none; }
}
