/* terrain-v1.css: presentation for the `terrain-to-schema` signature move.
   Chapter III of the coreshift-desert scroll-craft build. Peak of the page.

   WHAT THIS FILE IS RESPONSIBLE FOR
   Everything that moves. terrain-v1.js measures the box once per layout, writes
   a handful of unitless numbers onto the elements, and then never touches them
   again. Every frame of the morph is CSS reading `--sc-p`.

   That is deliberate, not a flourish. The move has ~100 travelling grains; if
   JS repositioned each one per frame it would be a hundred style writes a tick
   on a 3.7 GB box. Here the browser interpolates and JS is idle after layout.

   🔴 THE ONE RULE THIS FILE MUST NOT BREAK
   The ridge in the photograph and the chart line are ONE path with ONE `d`,
   placed by ONE mapping (see terrain-v1.js `placement()`). Nothing in here may
   translate, scale or reposition `.tts-ridge` relative to `.tts-photo`, because
   the whole point of the move is that the two are never two shapes. Stroke,
   colour, dash and opacity only. If you find yourself writing a transform on
   the ridge, the move is broken.

   Animated properties are transform, opacity, stroke-dashoffset and clip-path
   only. No `transition: all` anywhere.

   Tokens come from desert-v1.css. Every var() carries the literal fallback so
   the move also renders correctly in an isolated harness page. */

/* Registered so the calc() chains below are typed arithmetic rather than token
   substitution. An unregistered custom property that fails to parse takes the
   whole declaration with it and the failure is silent. Registration turns that
   into a clamped number instead. */
@property --tts-p    { syntax: "<number>"; inherits: true;  initial-value: 0; }
@property --t-trace  { syntax: "<number>"; inherits: true;  initial-value: 0; }
@property --t-wash   { syntax: "<number>"; inherits: true;  initial-value: 0; }
@property --t-grain  { syntax: "<number>"; inherits: true;  initial-value: 0; }
@property --t-chart  { syntax: "<number>"; inherits: true;  initial-value: 0; }
@property --t-node   { syntax: "<number>"; inherits: true;  initial-value: 0; }
@property --t-table  { syntax: "<number>"; inherits: true;  initial-value: 0; }
@property --t-dim    { syntax: "<number>"; inherits: true;  initial-value: 0; }
@property --nd  { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --rd  { syntax: "<number>"; inherits: false; initial-value: 0; }

/* --- 1. The scroll span and the pinned stage ------------------------------ */
/* `.tts-span` is the scroll length the move is read over; `.tts-stage` is the
   one viewport that stays put while it is read. When the scroll-craft engine is
   driving (the section is a `data-sc-act`), the engine owns both and JS mounts
   straight into its stage, and these two rules then do not apply. */
.tts-span {
  position: relative;
  min-height: var(--tts-span, 340svh);
}
.tts-stage {
  position: sticky;
  top: 0;
  height: 100svh;
  min-height: 34rem;
  display: flex;
  flex-direction: column;
  gap: clamp(.55rem, 1vw, .95rem);
  padding-block: clamp(1rem, 2.2vh, 2.2rem);
  justify-content: center;
  overflow: clip;
}
/* Only when measured to be full-bleed; see hostEngine() in terrain-v1.js. */
.tts-hold--gutter {
  box-sizing: border-box;
  width: 100%;
  max-width: var(--sc-maxw, 1320px);
  margin-inline: auto;
  padding-inline: var(--sc-gutter, clamp(1.25rem, 4vw, 3.5rem));
}

.tts-hold {                       /* mounted inside the engine's own stage */
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: clamp(.55rem, 1vw, .95rem);
  padding-block: clamp(1rem, 2.2vh, 2.2rem);
  justify-content: center;
}

/* One ramp source. Every phase below is a window cut out of this.
   🔴 It is declared on the SPAN and the HOLD, not on `.tts`. In engine mode the
   act publishing `--sc-p` is an ANCESTOR of `.tts-hold`, so the hold inherits
   it; in own mode the driver writes `--sc-p` onto `.tts-span` itself. Declaring
   it on `.tts` broke the second case silently: `.tts` is the PARENT of the span
   and so never saw the value it was being handed. The move mounted, measured and
   rendered perfectly, and sat frozen at p=0 forever. */
.tts-span, .tts-hold { --tts-p: var(--sc-p, 0); }

/* --- 2. The phases -------------------------------------------------------- */
/* Read this table and you have read the choreography:

     .08 → .30  the trace draws itself along the photographed crest
     .28 → .56  the photograph washes out to the paper ground
     .34 → .52  the sand grain separates into discrete marks
     .42 → .80  the marks travel to the grid (staggered per mark)
     .44 → .66  the axis and the area under the line arrive
     .54 → .80  the sampled points are plotted ON the line
     .76 → .98  the table resolves into the settled rows
     .82 → 1.0  the marks dim back to being row rules

   Overlaps are the point. Nothing in this move cuts; the photograph is still
   leaving while the chart is already arriving, which is what stops it reading
   as a crossfade between two finished pictures. */
.tts-span,
.tts-hold,
.tts-scene {
  --t-trace: clamp(0, calc((var(--tts-p) - .08) / .22), 1);
  --t-wash:  clamp(0, calc((var(--tts-p) - .28) / .28), 1);
  --t-grain: clamp(0, calc((var(--tts-p) - .34) / .18), 1);
  --t-chart: clamp(0, calc((var(--tts-p) - .44) / .22), 1);
  --t-node:  clamp(0, calc((var(--tts-p) - .54) / .26), 1);
  --t-table: clamp(0, calc((var(--tts-p) - .76) / .22), 1);
  --t-dim:   clamp(0, calc((var(--tts-p) - .82) / .18), 1);

  /* The per-mark settle window. Read by Scene.paint() in terrain-v1.js, which
     paints the marks on a canvas and therefore cannot use the ramps above.
     Declared HERE anyway so the choreography is written down in exactly one
     file: start of the first mark, how far the last one is staggered behind
     it, and how long each takes. Last mark lands at .38 + .20 + .18 = .76,
     which is where the table begins to resolve. */
  --g-start:  .38;
  --g-spread: .20;
  --g-dur:    .18;
}

/* `.tts-scene` repeats the block deliberately. A reduced-motion still sets its
   own `--tts-p` inline, and the phases must be re-derived from the NEARER value
   (an inherited computed number would not recompute). Same rule, two scopes. */

/* --- 3. Furniture --------------------------------------------------------- */
.tts-legend {
  /* core-v2.css caps every <p> at 70ch. These two are furniture, not copy, and
     the cap silently pulled the legend's right-hand readout into the middle of
     the frame. Measured, not guessed: the flex row simply had no room to be a
     row in. */
  max-width: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex: 0 0 auto;
  font-family: var(--sc-font-mono, 'IBM Plex Mono', ui-monospace, monospace);
  font-size: .72rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--sc-accent, #A96A22);
  border-top: 2px solid var(--sc-accent, #A96A22);
  padding-top: .5rem;
}
.tts-legend b { font-weight: 400; color: var(--sc-ink, #1A1714); }
/* The state readout is the instrument label, and it is the one place the move
   admits in words what the reader is watching. It swaps at the wash. */
.tts-state { position: relative; white-space: nowrap; }
.tts-state span { display: block; }
.tts-state span + span { position: absolute; inset: 0; }
/* A hard cut at the halfway point, not a cross-fade. Two overlaid words at 50%
   each rendered as one unreadable smear on the frame where the reader is most
   likely to stop, and this grammar bans cross-fades anyway. The multiplier
   turns the ramp into a switch without a transition or a script. */
.tts-state .is-terrain { opacity: clamp(0, calc((.5 - var(--t-wash)) * 24), 1); }
.tts-state .is-schema  { opacity: clamp(0, calc((var(--t-wash) - .5) * 24), 1); }

.tts-caption {
  flex: 0 0 auto;
  margin-block: 0;
  font-family: var(--sc-font-mono, 'IBM Plex Mono', ui-monospace, monospace);
  font-size: .74rem;
  line-height: 1.5;
  color: #766B5E;
  padding-left: .75rem;
  border-left: 2px solid var(--sc-accent, #A96A22);
  max-width: 58ch;
  margin: 0;
}

/* --- 4. The frame --------------------------------------------------------- */
/* Paper treatment lifted from desert-v1.css §5 so the move is not a foreign
   object on the page: 2px radius, hairline, warm shadow, plate ground. */
.tts-frame {
  position: relative;
  /* A letterbox, not "whatever height is left". Filling the stage put the crest
     at 40% of the frame and left a third of the finished chart as empty paper
     above the line. Measured on the rendered frames, invisible in the source.
     Capping the aspect crops sky instead of inventing headroom, and the spare
     stage height becomes margin, which is what this grammar wants anyway.
     max-height keeps it honest on a short window: the box gets wider than 2:1
     rather than overflowing, and placement() handles any aspect above 1.45. */
  flex: 0 1 auto;
  width: 100%;
  aspect-ratio: 2 / 1;
  max-height: 100%;
  min-height: 0;
  border-radius: 2px;
  border: 1px solid rgba(26, 23, 20, .18);
  box-shadow: 0 18px 38px rgba(42, 35, 28, .13);
  /* Paper, because that is what the move ends on: the photograph leaves by
     opacity and this is what it leaves behind. There is no full-frame wash rect
     any more; it was one more full-size thing to repaint every frame. */
  background: var(--ch-paper, #EFE8DB);
  overflow: clip;
}
.tts-svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* The photograph. It drains of colour rather than fading out, and the paper
   wash above it does the rest, so the picture leaves as light.
   The flat copy's filter is STATIC on purpose. See the note in terrain-v1.js:
   an animated filter over a full-width image was the most expensive thing left
   in the scrub. Only the opacity moves here, and a static filter is rasterised
   once. Both copies are placed by the same mapping, so they cannot separate. */
.tts-plate      { opacity: calc(1 - var(--t-wash)); will-change: opacity; }
.tts-plate--flat {
  filter: saturate(.08) contrast(.66) brightness(1.10);
  /* Rises fast, leaves slowly, and never quite reaches zero: a ghost of the
     dune stays under the schema so it sits ON the terrain rather than over a
     hole where the terrain used to be. */
  opacity: calc(min(var(--t-wash) * 2.4, 1) * (1 - var(--t-wash) * 0.90));
  will-change: opacity;
}

/* --- 5. The ridge. One path. ---------------------------------------------- */
/* `--tts-len` is the measured path length, written once per layout by JS. */
.tts-ridge {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: var(--tts-len, 4000);
  stroke-dashoffset: calc(var(--tts-len, 4000) * (1 - var(--t-trace)));
  stroke-width: calc(2px + 1.4px * var(--t-wash));
  /* On the photograph it is brass, so it reads as an instrument laid over the
     land. On paper it is ink, so it reads as the chart. Same stroke throughout,
     never two elements cross-fading. */
  stroke: color-mix(in oklab,
            var(--sc-accent, #A96A22) calc((1 - var(--t-wash)) * 100%),
            var(--sc-ink, #1A1714));
}
/* A soft halo only while the line is still sitting on a photograph, so it stays
   legible against the sand. It leaves with the photograph. */
.tts-ridge-halo {
  fill: none;
  stroke: rgba(255, 249, 240, .72);
  stroke-width: 6px;
  stroke-linecap: round;
  stroke-dasharray: var(--tts-len, 4000);
  stroke-dashoffset: calc(var(--tts-len, 4000) * (1 - var(--t-trace)));
  opacity: calc((1 - var(--t-wash)) * .85);
}
/* The area under the line is the SAME `d`, closed to the baseline. */
.tts-area {
  fill: var(--sc-accent, #A96A22);
  opacity: calc(var(--t-chart) * .22);
  stroke: none;
}

/* --- 6. Axis ------------------------------------------------------------- */
/* ⚠️ These groups animate opacity too, and promoting them with will-change
   was TRIED and REVERTED: it made the scrub worse (21 long frames in 200 back
   up to 42), measured interleaved so box load could not explain it. Chrome is
   software-rastering here, so every extra layer is another surface to composite
   on the CPU. Only the two photograph plates are promoted, because those are
   the two big ones, and that is where the whole win came from. More layers is
   not automatically faster; measure before adding one. */
.tts-axis { stroke: rgba(26, 23, 20, .38); stroke-width: 1px; opacity: var(--t-chart); }
.tts-axis-soft { stroke: rgba(26, 23, 20, .16); stroke-width: 1px; opacity: calc(var(--t-chart) * .9); }
.tts-axis-label {
  font-family: var(--sc-font-mono, 'IBM Plex Mono', ui-monospace, monospace);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  fill: #8A7F71;
  opacity: var(--t-chart);
}

/* --- 7. The grains -------------------------------------------------------- */
/* Painted, not styled. See Scene.paint() in terrain-v1.js and the note there
   about why: 144 SVG rects driven by calc() over --sc-p rendered this move at
   15fps, and the same marks on a canvas hold 60. The phases they use are the
   --t-grain / --t-dim / --g-* tokens above, read back out of this file. */
.tts-grains {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* --- 8. The plotted points ------------------------------------------------ */
/* These are placed by terrain-v1.js at points read back off the live path with
   getPointAtLength(). They cannot drift off the line, because they are not
   authored coordinates. They are the line, asked where it is. */
.tts-node {
  transform-box: view-box;
  transform-origin: center;
  fill: var(--ch-paper, #EFE8DB);
  stroke: var(--sc-accent, #A96A22);
  stroke-width: 2px;
  --nt: clamp(0, calc((var(--tts-p) - (.54 + var(--nd) * .16)) / .18), 1);
  opacity: var(--nt);
  transform: scale(calc(.4 + .6 * var(--nt)));
}
.tts-node-drop { stroke: rgba(169, 106, 34, .42); stroke-width: 1px; stroke-dasharray: 2 3; opacity: calc(var(--t-node) * .8); }

/* Hover ties the two halves together: the row and the point are one record.
   Not motion, so it stays available under prefers-reduced-motion. */
@media (hover: hover) and (pointer: fine) {
  .tts-frame.is-lit .tts-node { opacity: calc(var(--nt) * .35); }
  .tts-frame .tts-node.is-lit { opacity: 1; fill: var(--sc-accent, #A96A22); }
}

/* --- 9. The table --------------------------------------------------------- */
/* Real markup, registered on the settled grid. Row height is written by JS from
   the same grid the grains land on, so the rules ARE the marks. */
.tts-table-layer {
  position: absolute;
  left: var(--tts-grid-x, 0px);
  top: var(--tts-grid-y, 0px);
  width: var(--tts-grid-w, 100%);
  pointer-events: none;
}
.tts-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  font-family: var(--sc-font-mono, 'IBM Plex Mono', ui-monospace, monospace);
  font-variant-numeric: tabular-nums;
  color: var(--sc-ink, #1A1714);
}
.tts-table caption { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.tts-table th,
.tts-table td {
  height: var(--tts-row, 40px);
  padding: 0 .5rem;
  border: 0;
  text-align: left;
  font-size: clamp(11px, var(--tts-fs, 13px), 15px);
  line-height: 1;
  font-weight: 400;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tts-table th {
  font-size: clamp(9px, calc(var(--tts-fs, 13px) - 3px), 12px);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--sc-accent, #A96A22);
}
.tts-table td:first-child, .tts-table th:first-child { color: #8A7F71; }
.tts-table tbody tr {
  --rt: clamp(0, calc((var(--tts-p) - (.76 + var(--rd) * .13)) / .16), 1);
  opacity: var(--rt);
  transform: translateY(calc((1 - var(--rt)) * 6px));
}
.tts-table thead tr { opacity: var(--t-table); }
.tts-table tbody tr.is-peak td { color: var(--sc-accent, #A96A22); }
.tts-table tbody tr.is-peak td:first-child::after {
  content: "\00a0\25CF";
  font-size: .7em;
  vertical-align: middle;
}
@media (hover: hover) and (pointer: fine) {
  .tts-table-layer { pointer-events: auto; }
  .tts-table tbody tr { cursor: default; }
  .tts-table tbody tr.is-lit td { background: rgba(169, 106, 34, .10); }
}

/* --- 10. Mobile ----------------------------------------------------------- */
/* A separate composition, not a squeeze. The photograph stops being the whole
   frame and becomes a landscape plate at the top of it, which is the editorial
   grammar this build already uses everywhere else, and the grid gets the room
   below it. terrain-v1.js switches the placement to match; this only handles
   the furniture. */
@media (max-width: 860px) {
  .tts-stage { min-height: 30rem; }
  /* Narrow: the frame fills, and placement() switches to the plate composition
     because the measured aspect drops below 1.45. */
  .tts-frame { flex: 1 1 auto; aspect-ratio: auto; height: auto; }
  .tts-legend { font-size: .64rem; letter-spacing: .1em; }
  .tts-caption { font-size: .68rem; }
  .tts-state .is-terrain, .tts-state .is-schema { text-align: right; }
}
@media (max-width: 560px) {
  .tts-legend { flex-wrap: wrap; gap: .25rem .75rem; }
  .tts-state { width: 100%; }
  .tts-state .is-terrain, .tts-state .is-schema { text-align: left; }
}

/* --- 11. Reduced motion --------------------------------------------------- */
/* Decided in BRIEF.md, not retrofitted: two stills side by side with the shared
   ridge aligned. Nothing pins, nothing scrubs, nothing transitions. The reader
   is shown the two ends of the morph and a registration rule that proves they
   are the same curve, which is the idea, minus the motion.

   The scroll span collapses too. Leaving 340svh of empty scroll behind a static
   picture is the "filler pinning" the skill bans, and it is worse for the
   reader this branch exists for. */
@media (prefers-reduced-motion: reduce) {
  .tts-span { min-height: 0; }
  .tts-state { display: none; }   /* the two stills carry their own labels */
  .tts-stage { position: static; height: auto; min-height: 0; overflow: visible; }
  .tts-hold { height: auto; }

  .tts-stills {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: clamp(1rem, 3vw, 2.5rem);
    position: relative;
  }
  .tts-still { position: relative; }
  /* The stills carry the SAME 2:1 letterbox as the moving composition. At 3:2
     the cover crop barely bit, the crest dropped to 45% of the frame and the
     schema still came back with 40% of its height as empty paper above the
     line: the two stills stopped reading as the same picture twice. */
  .tts-still .tts-frame { height: auto; aspect-ratio: var(--tts-still-ar, 2 / 1); max-height: none; }
  .tts-still-label {
    font-family: var(--sc-font-mono, 'IBM Plex Mono', ui-monospace, monospace);
    font-size: .7rem;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--sc-accent, #A96A22);
    margin: 0 0 .5rem;
  }
  /* The registration rule. It runs across both frames at the height of the
     crest's highest point, so "aligned" is something the reader can check
     rather than something the caption asserts. */
  .tts-reg {
    position: absolute;
    left: 0; right: 0;
    top: var(--tts-reg-y, 50%);
    border-top: 1px dashed var(--sc-accent, #A96A22);
    opacity: .75;
    pointer-events: none;
  }
  .tts-reg b {
    position: absolute;
    right: 0;
    top: -1.15rem;
    font-family: var(--sc-font-mono, 'IBM Plex Mono', ui-monospace, monospace);
    font-size: .62rem;
    font-weight: 400;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--sc-accent, #A96A22);
    background: var(--ch-paper, #EFE8DB);
    padding: 0 .35rem;
  }
  @media (max-width: 860px) {
    .tts-stills { grid-template-columns: 1fr; }
    /* Stacked, so the shared coordinate is x, not y: the rule stands vertically
       through the crest of both frames instead. */
    .tts-reg {
      left: var(--tts-reg-x, 50%);
      right: auto;
      top: 0; bottom: var(--tts-reg-bottom, 0px);
      border-top: 0;
      border-left: 1px dashed var(--sc-accent, #A96A22);
    }
    /* Stacked, the rule spans the whole pair, so its label goes to the foot of
       it rather than the head. At the head it landed on top of the first
       still's own label. */
    /* No label at all when stacked. Head of the rule sat on the first still's
       own label, foot of it sat on the last table row, and the caption two
       lines below already says exactly what the rule is. */
    .tts-reg b { display: none; }
    /* Cover cropping needs a box wider than 1.45:1 or it starts eating the ends
       of the trace, which is the one thing this branch exists to show. At phone
       width 2:1 is only 174px tall and the table is squeezed against the chart,
       so the stacked stills take 3:2, the widest shape that still breathes. */
    .tts-still .tts-frame { max-height: none; aspect-ratio: 3 / 2; }
  }
}
