/* =============================================================================
   project.css — the project column. The shell (sticky aside, 40 / 60 grid,
   .column padding, the progress line) lives in base.css §6; this file fills
   the column that js/project.js renders.

   1  Column rhythm
   2  Hero
   3  Shared atoms     labels, prose, media boxes, placeholders, captions,
                       the rules that draw themselves, pictures that fade in
   4  Question, text, highlight
   5  Figures          image, phone frame, pair, grid
   6  Challenges, quote, metrics, trend
   7  Prototype
   8  More work
   9  Flyer            owned by P3a

   House rules, restated where they bite: rem only (hairlines excepted), svh
   never vh, three type sizes (--fs-display / --fs-body / --fs-small — the two
   larger figures below are derived from --fs-display, the way the aside
   number is, never a fourth token), no shadows, radius ≤ 0.25rem except the
   phone frame and the round play control, every :hover inside the
   (hover: hover) and (pointer: fine) query.
============================================================================= */


/* -- 1  Column rhythm -------------------------------------------------------- */
/*
   One gap between any two things in the column — paragraph to picture,
   caption to paragraph — wherever they sit, and a larger one only above a
   highlight's hairline: a highlight is a chapter of the argument, and the
   reader feels the turn where its rule starts. Not below it as well: a
   highlight ends on a picture like any other block, and 6rem there made one
   caption-to-paragraph gap half again another's. A highlight's own picture
   is the same gap away (.hl-media). The gaps are margins on the .blk
   wrapper, never on the [data-seq] element inside it, so a block's spacing
   does not travel with its entrance.
*/

.hero + .blk,
.blk + .blk { margin-top: 4rem; }

.blk + .blk--highlight { margin-top: 6rem; }

/*
   The question is its own beat. It is the one row with no hairline above
   it, so 4rem under the hero read as a caption to the picture; 5rem puts
   it on its own. After it, 3rem: the next row's hairline is already a
   pause, and at 4rem the question floated free of the argument it opens.
   After the two rules above, so it wins at equal specificity when the next
   block is a highlight too.
*/
.hero + .blk--question { margin-top: 5rem; }
.blk--question + .blk { margin-top: 3rem; }

/* The column's own width is the image width; prose sets its own measure. */
.blk-in { min-width: 0; }


/* -- 2  Hero ----------------------------------------------------------------- */
/*
   The flyer (flyer.js) lands a board thumb on exactly this box, so it is a
   plain rectangle whose only child fills it — 4:3 unless the project sets
   heroRatio, which project.js writes inline as --hero-ratio (--hero-default
   is the unauthored shape; mobile.css makes it square). Its entrance is
   opacity only: the shared [data-seq] rise would put the box half a rem low for the
   first 500ms, and a flight that measured it then would land short.
*/

.hero {
  position: relative;
  aspect-ratio: var(--hero-ratio, var(--hero-default, 4 / 3));
  overflow: hidden;
  background: var(--paper-2);
}

.hero[data-seq] { translate: none; }

/* An exported picture brings its own paper, so the hero never draws a frame
   or a slab of its own around one. The gradient (.hero--ph) keeps the look
   above. */
.hero--image {
  border: 0;
  outline: 0;
}

.hero > img,
.hero > .ph,
.hero > video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A loop's first frame is the still under it (project.js, loopVideo), so
   the fade only hides the moment a decoder hands over. */
.loop { opacity: 0; transition: opacity 240ms linear; }
.loop.is-on { opacity: 1; }

/* The cue on a loop that is wanted but not moving (project.js, loopCue):
   the prototype's navy disc at a corner's size, a play glyph, and an arc
   turning round a faint track. Hidden until a wait is long enough to
   notice; it leaves on the same 240ms as the loop arrives, so the
   hand-over is one moment. Clickable only when autoplay was refused
   (.is-blocked), and then the arc goes: nothing is loading, it is waiting
   for a tap. */
.loop-cue {
  position: absolute;
  right: 1rem;
  bottom: 1rem;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  color: var(--paper-2);
  opacity: 0;
  pointer-events: none;
  transition: opacity 240ms linear, scale var(--t-hover) var(--ease-enter);
}
.loop-cue.is-waiting { opacity: 1; }
.loop-cue.is-blocked { pointer-events: auto; cursor: pointer; }
/* Blocked, the cue is a real button: it answers the hand as the
   prototype's disc does, the fade keeping its 240ms under a press. */
@media (hover: hover) and (pointer: fine) {
  .loop-cue.is-blocked:hover { scale: 1.06; }
}
button.loop-cue.is-blocked:active {
  scale: 0.92;
  transition-duration: 240ms, calc(var(--t-hover) * 0.3);
}
.loop-cue svg { display: block; width: 100%; height: 100%; }
.loop-cue-track,
.loop-cue-arc { fill: none; stroke: currentColor; stroke-width: 1.5; }
.loop-cue-track { opacity: 0.24; }
.loop-cue-arc {
  stroke-linecap: round;
  transform-origin: 20px 20px;
  animation: loop-cue-turn 1s linear infinite;
}
.loop-cue:not(.is-waiting) .loop-cue-arc { animation-play-state: paused; }
.is-blocked .loop-cue-track,
.is-blocked .loop-cue-arc { display: none; }
@keyframes loop-cue-turn { to { rotate: 1turn; } }


/* -- 3  Shared atoms --------------------------------------------------------- */

/* Small and quiet: a label names the row, it does not compete with it. */
.blk-label {
  font-size: var(--fs-small);
  line-height: 1.5;
  font-weight: 400;
  color: var(--ink-3);
}

.prose {
  max-width: 36rem;
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--ink);
  text-wrap: pretty;
}
.prose > p + p { margin-top: 1em; }

/* A box that holds its ratio before the image arrives (aspect-ratio is set
   inline from the block's `ratio`), so nothing below it jumps. */
.media {
  position: relative;
  overflow: hidden;
  background: var(--paper-2);
}

.media > img,
.media > .ph,
.media > video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A lazy picture fades in as it lands (project.js, fadeIn) on the loop's
   own 240ms picture fade (§2), so every picture arrives the same way. */
img.fade { opacity: 0; transition: opacity 240ms linear; }
.fade.is-in { opacity: 1; }

/*
   The rules that open something (a decision, a challenge, a metric, a
   More-work row) draw left to right as their block arrives, with the
   block's own --d from the SEQ table, so the line is drawn as the words
   rise under it; More-work rows, each its own [data-seq], print 70ms apart.
   Each box keeps its border-top, transparent, and the line is a border of
   the same width standing on the padding box: a hair of padding instead
   moved the rows at 2×, where Chrome rounds a 0.5px border up to 1px and
   padding not at all. Only a block that has never arrived holds it at 0.
*/
:is(.rowed, .ch, .metric, .more-row) { position: relative; }
:is(.rowed, .ch, .metric, .more-row)::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  border-top: var(--hair) solid var(--line);
  transform-origin: 0 50%;
  transition: scale var(--t-flip) var(--ease-flip) var(--d, 0ms);
  pointer-events: none;
}
[data-seq]:not([data-in]) :is(.rowed, .ch, .metric, .more-row)::before { scale: 0 1; }

/* Placeholder (.ph): the project's own grainy duotone, drawn in base.css
   §2 from the custom properties project.js sets on the column (and per
   thumb in the more-work list). Not a grey box, so an unfinished page still
   looks like that project — and the same picture as its board tile. */

/* A gradient set straight in a figure — a full-width image, a cell of a
   landscape set, a landscape prototype's poster — gets the stage's
   hairline, so it reads as an object laid on the page like everything
   framed around it, not a slab of colour. On a stage the stage is the
   frame; a photo brings its own edge; the hero is never framed (§2). The
   one rule for all of them: a figure's own .media holding a .ph. An
   outline on the box, so no box changes size, and on the paper side of
   its edge as a stage's border is: the object's edge, where it matters
   most at a pale highlight running off the picture, rather than a pale
   keyline printed onto a dark gradient. */
figure > .media:has(> .ph) { outline: var(--hair) solid var(--line); }

.cap {
  margin-top: 0.75rem;
  max-width: 36rem;
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-3);
  text-wrap: pretty;
}


/* -- 4  Question, text, highlight -------------------------------------------- */

/* The page's <h1>, and the only heading at the display size. */
.q {
  max-width: 18em;
  font-size: var(--fs-display);
  line-height: 1.12;
  letter-spacing: -0.02em;
  font-weight: 400;
  text-wrap: balance;
}

/*
   A highlight's head: a hairline, the title, the argument under it — all on
   the column's one left edge, the same edge the question, the prose and
   the pictures start on. Title over prose, never beside it: beside, an
   untitled text block had to leave the title track empty, and the page had
   two left edges. The hairline is a highlight's alone, so a rule always
   means a decision starts here.
*/
.rowed {
  display: grid;
  grid-template-columns: minmax(0, 36rem);
  row-gap: 0.75rem;
  padding-top: 0.75rem;
  border-top: var(--hair) solid transparent;   /* drawn by ::before (§3) */
}

.hl-title {
  font-size: var(--fs-body);
  line-height: 1.4;
  font-weight: 500;
  color: var(--ink);
  text-wrap: balance;
}

.highlight .prose { color: var(--ink-2); }

/* The column's one gap (§1): a decision's picture is as far from its prose
   as any picture is from the paragraph above it. */
.hl-media { margin-top: 4rem; }


/* -- 5  Figures -------------------------------------------------------------- */

.fig { min-width: 0; }

/*
   The stage: where portrait pictures stand (project.js, the portrait rule).
   A fixed shape — 4/3 for one or two, 16/10 for a row of a grid — so its
   height comes from the column width, never from a screen's ratio. Items
   are the full height of the padded stage and as wide as their ratio;
   object-fit: cover inside each. If a row would overflow (a very wide set),
   items give up width, not height, and the picture crops.
*/
.stage {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 2.5rem;
  padding: 2rem;
  background: var(--paper-2);
  /* A hairline frame, as a board tile has, so the stage reads as an object
     laid on the page and not as a paler patch of the same paper. */
  border: var(--hair) solid var(--line);
}

/* Every item a real export (project.js): the screens are matted on paper
   already, so the stage gives them less of its own and they stand taller.
   Gradient placeholders keep 2rem. Same specificity as .stage, so the
   phone's tighter stage (mobile.css, later) still wins there. */
.stage--shots { padding: 1.5rem; }

.stage-item {
  position: relative;
  flex: 0 1 auto;
  min-width: 0;
  height: 100%;
}

/* The picture box inside a phone frame, or inside a captioned item's
   wrapper (project.js), fills its height; the wrapper is as wide as the
   screen, plus a frame's padding. */
.stage .phone > .media,
.stage .stage-item > .media { height: 100%; }

/* A stage whose items are captioned keeps a caption's height free under
   them, so the caption stands in that room and the stage's own padding
   still runs below it: hung in the padding alone, it sat on the frame. */
.stage--capped { align-items: flex-start; }
.stage--capped .stage-item { height: calc(100% - 0.5rem - 1.2 * var(--fs-small)); }

/* An item's caption hangs under it, in the room above. */
.stage-cap {
  position: absolute;
  top: 100%;
  left: 50%;
  translate: -50% 0;
  padding-top: 0.5rem;
  font-size: var(--fs-small);
  line-height: 1.2;
  color: var(--ink-3);
  white-space: nowrap;
}

/* Stacked stages share one hairline between them rather than drawing two. */
.set--stage .stage + .stage { margin-top: calc(var(--hair) * -1); }

/*
   The phone frame: a hairline rounded rectangle around the screen, on the
   stage. Radii are concentric — outer = inner + padding — so the gap
   between screen and frame is even all the way round. No notch: the
   proportions already say "phone".
*/
.phone {
  padding: 0.5rem;
  border: var(--hair) solid var(--ink-3);
  border-radius: 2.25rem;
}

.phone > .media { border-radius: 1.75rem; }

/* Pair and grid: the pictures sit a hairline apart, so they read as one
   plate cut into frames rather than a gallery of separate cards. Start-
   aligned so two different ratios never stretch each other. */
.set-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
  gap: max(var(--hair), 1px);
  align-items: start;
}
/* A one-column grid stacks strips of the same screen; a hairline would join
   them into one tall picture, so each row stands apart (project.js). Across
   the whole column a 375pt-wide strip was three times the scale of the
   staged phones around it, so the strips stand on a stage at half the
   column; on a phone the column is already about a screen wide
   (mobile.css's one-up grid wins there). */
.set--rows .set-grid {
  grid-template-columns: minmax(0, 50%);
  justify-content: center;
  row-gap: 1rem;
  padding: 1.5rem;
  background: var(--paper-2);
  border: var(--hair) solid var(--line);
}



/* -- 6  Challenges, quote, metrics, trend ------------------------------------ */

.challenges > .blk-label,
.metrics > .blk-label,
.finals > .blk-label { margin-bottom: 1.25rem; }

/* Three across, each under its own rule: equal weight, read left to right. */
.ch-list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: 2rem;
  row-gap: 2.5rem;
}

.ch {
  padding-top: 0.75rem;
  border-top: var(--hair) solid transparent;   /* drawn by ::before (§3) */
}

.ch-no {
  display: block;
  font-size: var(--fs-small);
  color: var(--ink-3);
  font-variant-numeric: lining-nums tabular-nums;
}

/* Title and body are one thought: 0.5rem apart, and both set a little
   tighter so their leading does not add a second gap on top of it. */
.ch-title {
  margin-top: 2rem;
  font-size: var(--fs-body);
  line-height: 1.3;
  font-weight: 500;
  text-wrap: balance;
}

.ch-body {
  margin-top: 0.5rem;
  font-size: var(--fs-body);
  line-height: 1.5;
  color: var(--ink-2);
  text-wrap: pretty;
}

/* Large but a step under the question, so the <h1> stays the loudest line
   in the column. The opening mark hangs into the gutter so the text edge
   stays aligned with everything above it. */
.quote { max-width: 40rem; }

.quote-text {
  position: relative;
  font-size: calc(var(--fs-display) * 0.75);
  line-height: 1.25;
  letter-spacing: -0.015em;
  font-weight: 400;
  text-wrap: balance;
}
.quote-text::before {
  content: '\201C';
  position: absolute;
  right: 100%;
  padding-right: 0.08em;
  color: var(--ink-3);
}
.quote-text::after { content: '\201D'; color: var(--ink-3); }

.quote-cite { margin-top: 1.5rem; }

/* Values oversized — the number is the finding; the label only says what it
   counts. Derived from the display token, as the aside number is. A value
   is one line, always ("66% → / 73%" reads as two numbers): it does not
   wrap, and project.js §6 writes --mscale on the list — the largest scale
   at which every value in it fits, floor 0.6 — so a list stays one size. */
.metric-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  column-gap: 2rem;
  row-gap: 2.5rem;
}

.metric {
  min-width: 0;
  display: flex;
  flex-direction: column-reverse;   /* dd (value) above dt (label) */
  justify-content: flex-end;
  padding-top: 0.75rem;
  border-top: var(--hair) solid transparent;   /* drawn by ::before (§3) */
}

.metric-value {
  font-size: calc(var(--fs-display) * 1.25 * var(--mscale, 1));
  white-space: nowrap;
  line-height: 1;
  letter-spacing: -0.035em;
  font-weight: 400;
  font-variant-numeric: lining-nums tabular-nums;
  padding-top: 1.5rem;
}

.metric-label {
  margin-top: 0.75rem;
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-3);
}

/* The trend under the values: thin bars on one baseline (the hairline under
   them), each value sitting on its own bar. --trend-h is the tallest bar, so
   the list is the chart's height whatever the data; --v (0…1) is written
   per bar by project.js. Accent, the mark's navy — one series, one colour.
   A running month is hatched in the same ink so it reads as unfinished,
   not as a fall. */
.trend { margin-top: 3rem; --trend-h: 11rem; }

.trend-label {
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-3);
  margin-bottom: 1.25rem;
}

.trend-bars {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  column-gap: 2.5rem;
  align-items: end;
}

.trend-bar {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}

.trend-value {
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-2);
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
  padding-bottom: 0.375rem;
}

.trend-fill {
  display: block;
  height: calc(var(--trend-h) * var(--v, 0));
  min-height: var(--hair);
  background: var(--accent);
  border-radius: 0.125rem 0.125rem 0 0;
  /* Rises from the baseline (project.js, mountTrends); a scale moves no text. */
  transform-origin: 50% 100%;
  transition: scale var(--t-flip) var(--ease-flip);
}
.trend.is-pending .trend-fill { scale: 1 0; }
.is-partial .trend-fill {
  background:
    repeating-linear-gradient(135deg, var(--accent) 0 var(--hair), transparent var(--hair) 0.375rem);
  outline: var(--hair) solid var(--accent);
  outline-offset: calc(-1 * var(--hair));
}

.trend-period {
  padding-top: 0.5rem;
  border-top: var(--hair) solid var(--line-strong);
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-3);
}

.metric-note {
  margin-top: 1.5rem;
  max-width: 36rem;
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-3);
}


/* -- 7  Prototype ------------------------------------------------------------ */
/*
   Poster plus one round control in the middle. The control carries a word
   on a paper chip beside the disc, so it reads on any poster, light or dark.
   The iframe replaces the poster in the same box: opening a prototype
   changes what is in the frame, not the frame.
*/

.proto-stage { display: grid; place-items: center; }

/* A portrait prototype stands on a stage in a phone frame (project.js). Its
   screen is ~14rem wide, too narrow for disc and word side by side, so the
   word drops under the disc and the pair stays centred on the poster. */
.proto--stage .proto-cta {
  flex-direction: column;
  gap: 0.625rem;
}

.proto-cta {
  position: relative;          /* above the absolutely-placed poster */
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  font-size: var(--fs-small);
  font-weight: 500;
  line-height: 1;
  color: var(--ink);
}

.proto-disc {
  display: grid;
  place-items: center;
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
  background: var(--accent);
  color: var(--paper-2);
  transition: scale var(--t-hover) var(--ease-enter);
}
.proto-disc svg { width: 1.25rem; height: 1.25rem; }

.proto-word {
  padding: 0.5rem 0.625rem;
  border-radius: 0.125rem;
  background: var(--paper-2);
}

.proto-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  background: var(--paper-2);
}

.proto-cap {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  max-width: none;
}

.proto-close {
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--ink);
  padding-bottom: 0.125rem;
  border-bottom: var(--hair) solid var(--ink-3);
  transition: border-color var(--t-hover) ease-out;
}
.proto-close[hidden] { display: none; }

@media (hover: hover) and (pointer: fine) {
  .proto-cta:hover .proto-disc { scale: 1.06; }
  .proto-close:hover { border-color: var(--ink); }
}

/* A press gives, quicker than a hover lifts; after it, so a press wins. */
.proto-cta:is(button, [href]):active .proto-disc {
  scale: 0.92;
  transition-duration: calc(var(--t-hover) * 0.3);
}


/* -- 8  More work ------------------------------------------------------------ */
/*
   The other projects as a dated listing, like an exhibition programme:
   number over year, a square thumb, a vertical rule, the title with its
   roles as one quiet line at the foot, the blurb on the right. The rule
   runs the full row height because the cells carry the padding, not the
   row. A row answers as a board tile does, without a fill: pointed at,
   focused or pressed, its thumb's frame goes to ink; pressed, the picture
   sinks inside the frame and the title steps to --ink-2.
*/

.more {
  margin-top: 8rem;
  /* A project → project crossfade can land with the list at the top edge;
     this keeps it clear of it. (mobile.css sets its own, under the minibar.) */
  scroll-margin-top: 4rem;
}

.more-label {
  padding-bottom: 1rem;
  font-size: var(--fs-small);
  color: var(--ink-3);
}

.more-list { border-bottom: var(--hair) solid var(--line); }

.more-row {
  --more-pad: 1.25rem;   /* the cells' padding; the thumb's frame reads it */
  display: grid;
  grid-template-columns: 6rem 5rem minmax(0, 1fr) minmax(0, 1fr);
  column-gap: 1.5rem;
  border-top: var(--hair) solid transparent;   /* drawn by ::before (§3) */
}

.more-row > * { padding-block: var(--more-pad); }

.more-when {
  display: flex;
  flex-direction: column;
  font-size: var(--fs-body);
  line-height: 1.25;
  font-variant-numeric: lining-nums tabular-nums;
}
.more-no   { color: var(--ink); }
.more-year { color: var(--ink-3); }

.more-thumb {
  position: relative;
  display: block;
  align-self: start;
  box-sizing: content-box;
  width: 5rem;
  height: 5rem;
  background: var(--paper-2);
  background-clip: content-box;
}
.more-thumb > img,
.more-thumb > .ph {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Every thumb is framed by the same hairline, so a photo on its own paper
   (remittance) and a gradient read as one row of objects, the way the
   board's tiles do. The thumb carries the row's padding (the rule beside it
   runs full height that way), so the frame is placed off --more-pad, one
   hair outside the picture: on the paper, not printed on a dark gradient,
   and the square does not grow. Nothing overflows the thumb (its picture is
   exactly the content box), so it does not clip. The hero is never framed
   (§2). */
.more-thumb::after {
  content: '';
  position: absolute;
  inset: calc(var(--more-pad) - var(--hair)) calc(var(--hair) * -1);
  border: var(--hair) solid var(--line);
  pointer-events: none;
  transition: border-color var(--t-hover) ease-out;
}

/* The press, and the lazy fade (§3) that this transition would replace. */
.more-thumb > img,
.more-thumb > .ph { transition: scale var(--t-hover) var(--ease-enter), opacity 240ms linear; }

.more-main {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  padding-left: 1.5rem;
  border-left: var(--hair) solid var(--line);
}

.more-title {
  font-size: var(--fs-body);
  line-height: 1.3;
  font-weight: 500;
  color: var(--ink);
  text-wrap: balance;
  transition: color var(--t-hover) ease-out;
}

/* Roles: plain words at the foot of the cell, as in the reference listing. */
.more-tags {
  margin-top: auto;
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-3);
}

.more-blurb {
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-2);
  text-wrap: pretty;
}

@media (hover: hover) and (pointer: fine) {
  .more-row:hover .more-title { color: var(--ink-2); }
  .more-row:hover .more-thumb::after { border-color: var(--ink); }
}
.more-row:focus-visible .more-thumb::after,
.more-row[href]:active .more-thumb::after { border-color: var(--ink); }
.more-row[href]:active .more-title { color: var(--ink-2); }
.more-row[href]:active .more-thumb > * {
  scale: 0.97;
  transition-duration: calc(var(--t-hover) * 0.3), 240ms;   /* the press only; the fade keeps its own */
}


/* -- 9  Flyer — owned by P3a ------------------------------------------------- */
/*
   The one thing that travels between the landing and a project: a fixed copy
   of the picture that flyer.js places at the rect it leaves and sends to the
   rect it lands on. It moves by translate, never top / left, and on the same
   duration and curve as the board, so it reads as the same material.
   Everything else here only lets a page leave without moving.
*/

.flyer {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  display: block;
  margin: 0;
  max-width: none;
  width: var(--w);
  height: var(--h);
  translate: var(--x) var(--y);
  object-fit: cover;
  pointer-events: none;
  transition:
    translate var(--t-flip) var(--ease-flip),
    width     var(--t-flip) var(--ease-flip),
    height    var(--t-flip) var(--ease-flip);
}

/* Tiles that were not clicked settle back a little as they fade. The scale
   rides the exit's own duration and the stagger flyer.js writes as --d. */
.tile-in.is-leaving[data-in="0"] {
  scale: 0.96;
  transition:
    opacity   var(--t-exit) ease-in var(--d, 0ms),
    translate var(--t-exit) ease-in var(--d, 0ms),
    scale     var(--t-exit) ease-in var(--d, 0ms);
}

/* The column rule (base.css §6) belongs to no entrance group, so it would
   blink in and out with the page. flyer.js holds it with these two states.
   The aside's progress line is the last member of the aside group, so the
   stamp times it — on a deep link as well as after a flight — and it only
   needs holding here while the page is pinned on its way out. It does not
   rise: a hairline on the aside's bottom edge would slide under it. */
.aside-progress[data-seq] { translate: none; }
.project::before { transition: opacity var(--t-enter) var(--ease-enter); }
.project.is-arriving .aside-progress,
.project.is-arriving::before { opacity: 0; }
.project.is-leaving .aside-progress,
.project.is-leaving::before {
  opacity: 0;
  transition-duration: var(--t-exit);
  transition-timing-function: ease-in;
}

/* Project → project (flyer.js crossfade): the aside is the same box on both
   pages, so it changes where it stands — a fade, no rise. */
.project.is-crossing .aside [data-seq] { translate: none; }

/* Back, in the same spot on both pages, is chrome and must not blink: the
   leaving page keeps it through its exit (.is-to-project, flyer.js) and the
   arriving one shows it at once (unmount, mount and stamp are one task, so
   no frame is drawn without it). Desktop only: a phone's aside scrolls. */
@media (min-width: 901px) {
  .project.is-leaving.is-to-project .aside-back[data-in="0"] { opacity: 1; }
  .project.is-crossing:not(.is-leaving) .aside-back[data-seq] { transition: none; }
}

/* A leaving page, held at the rect it had (flyer.js writes it inline) and
   behind everything, so the arriving page lays out from the top while this
   one finishes fading. */
.is-pinned {
  position: fixed;
  z-index: -1;
  pointer-events: none;
}
