/*
Block 11 "Every marker. Every next step.". Spec: docs/blocks/11-app.md.
Every value comes from tokens.css.

A card across the whole band: an intro and the current slide's text on the left, a carousel of
app screens on the right.

--- A slide is a picture and two texts ------------------------------------------------------

The design draws the contents of each card — charts, marker lists, a dial — as vector work. NONE
of that is built. They stand in for screenshots of the product that do not exist yet, and this
block treats a slide as exactly three fields: an image, a title and a paragraph. When the
screenshots arrive they are uploaded; nothing here changes.

Until then a slide with no image shows a placeholder that is meant to look unfinished. That is
the point of it: a placeholder styled to blend in is a placeholder that ships.

--- Nothing here counts the slides ----------------------------------------------------------

Which slide is current is one attribute on the section, and each slide carries its own position
relative to it. A seventh slide added by the client gets a seventh tab, turns the counter into
07, and joins the rotation with no markup edited.
*/
.app {
  padding-block: var(--app-top) var(--app-bottom);
}

.app__inner {
  width: min(var(--page-max), 100% - var(--page-inset) * 2);
  margin-inline: auto;
}

/*
The card. A ring rather than a border for the reason the outline button states — a hairline
border on an element that animates is composited apart from its fill — and because a border
would move the contents by a pixel where an inset shadow does not.
*/
.app__card {
  display: grid;
  min-height: calc(var(--app-content-h) + var(--app-pad-y) * 2);
  padding: var(--app-pad-y) var(--app-pad-x);
  border-radius: var(--app-radius);
  background: var(--surface-tint);
  box-shadow: inset 0 0 0 var(--rule-w) var(--app-ring-color);
  column-gap: var(--app-col-gap);
  grid-template-areas:
    "intro stage"
    "aside stage";
  grid-template-columns: var(--app-text-w) minmax(0, 1fr);

  /*
  The intro's row takes the slack and the slide's text sits under it, which is what puts that
  text on the floor of the card whatever the intro above it comes to.
  */
  grid-template-rows: 1fr auto;
}

.app__intro {
  grid-area: intro;
}

.app__aside {
  grid-area: aside;
}

.app__stage {
  grid-area: stage;
}

/*
The heading is WIDER THAN ITS COLUMN, on purpose and by 48.

The column is a grid track of the design's own 526, so a max-width inside it can only ever make
the heading narrower — the track is the ceiling. An explicit width lets it overflow into the 77
of gap before the carousel, which is empty at this height: the heading occupies y 121 to 257 of
the card and the nearest card spans 288 to 692, so the two never share a line.

It overflows because DM Sans is not GT Flexa; see --app-title-w for the measurement. At the
design's own width the second line wraps into a third.

No max-width to go with it: 100% would resolve against the column's own 526 and cap the width
straight back to it, which is what happened on the first attempt — the heading measured 574 and
still broke into three lines. Below --bp-lg the block stacks and the token is 100% anyway, so
there is nothing narrow left for it to overflow.
*/
.app__title {
  width: var(--app-title-w);

  --heading-fs: var(--fs-app-title);
  --heading-ls: var(--ls-app-title);
  --heading-lh: var(--lh-app-title);
  --fw-display-accent: var(--fw-app-title-accent);
}

/*
The paragraphs in this block are --ink rather than --ink-muted, which is the opposite of every
other section here. Inside a tinted card the muted grey loses too much of its contrast; the
design sets both of them at full ink and this follows it.
*/
.app__lead {
  max-width: var(--app-lead-w);
  margin: var(--app-lead-gap) 0 0;
  color: var(--ink);
  font-size: var(--fs-app-lead);
  font-weight: var(--fw-regular);
  line-height: var(--lh-app-lead);
}

/*
All the texts in one cell, and the group is as tall as the tallest of them.

They used to be taken out of flow and put back — absolute for the ones waiting, relative for the
one showing — and that is what made the text jump when moving BACKWARDS: going from a slide with
no paragraph to one with three lines, the group's height changed the instant the two swapped, so
the pager under it jerked. A grid cell holds the height of the tallest text at all times, so
nothing below moves at all, whichever direction the carousel is going.
*/
.app__slide-texts {
  display: grid;
  align-items: start;
}

/*
Every slide's text is printed, and the current one is the one that is shown. The alternative —
one element whose words are rewritten — needs script to hold the content, which puts the copy
somewhere the client cannot see it and breaks the block completely without JavaScript.

Out of flow except for the current one, so the group is as tall as whichever slide is showing
and the card does not resize as they change.
*/
.app__slide-text {
  grid-area: 1 / 1;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--app-text-dur) var(--app-slide-ease),
    translate var(--app-text-dur) var(--app-slide-ease),
    visibility 0s linear var(--app-text-dur);
  translate: 0 var(--app-text-rise);
  visibility: hidden;
  will-change: opacity, translate;
}

/*
The words rise into place a little after the card they belong to — the delay is what makes the
two read as one movement rather than as two things happening at once. Only the arriving text is
delayed; the one leaving goes at once, or the two would overlap in the middle of the fade.
*/
.app__slide-text[data-current="true"] {
  opacity: 1;
  pointer-events: auto;
  transition-delay: var(--app-text-delay), var(--app-text-delay), 0s;
  translate: 0 0;
  visibility: visible;
}

.app__slide-title {
  margin: 0;
  color: var(--ink);
  font-size: var(--fs-app-slide-title);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-app-slide-title);
  line-height: var(--lh-app-slide-title);
}

.app__slide-body {
  max-width: var(--app-slide-body-w);
  margin: var(--app-slide-body-gap) 0 0;
  color: var(--ink);
  font-size: var(--fs-app-slide-body);
  font-weight: var(--fw-regular);
  line-height: var(--lh-app-slide-body);
}

/*
--- The pager -------------------------------------------------------------------------------
*/
.app__pager {
  display: flex;
  align-items: center;
  margin-top: var(--app-pager-gap);
  margin-bottom: var(--app-pager-bottom);
  gap: var(--app-tabs-gap);
}

/*
A fixed slot, because the figures cannot be made to behave.

The design sets this in DM Mono. The theme has no mono face, and tabular-nums does not help —
measured in this build, "1" is 6.06 wide against "0" at 10.9 with the property on or off, since
the subset of DM Sans shipped here carries no tnum feature. Left to size itself the counter went
from 56 to 60 as the slides turned and pushed the row of marks 4px sideways every time.

tabular-nums is kept anyway: it costs nothing and starts working the day the font is replaced
with one that has the feature.
*/
.app__counter {
  display: inline-block;
  width: var(--app-counter-w);
  color: var(--ink-slate);
  font-size: var(--fs-app-counter);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-app-counter);
  line-height: var(--lh-app-counter);
}

.app__tabs {
  display: flex;
  margin: 0;
  padding: 0;
  gap: calc(var(--app-tab-step) - var(--app-tab-size));
  list-style: none;
}

/*
A real button per slide. The bar inside it is smaller than the target around it, which is the
design's own arrangement — the tab is 26 square and the mark in it is 3 tall — and it is also
what makes the pager usable with a finger.
*/
.app__tab {
  display: flex;
  width: var(--app-tab-size);
  height: var(--app-tab-size);
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  align-items: center;
}

/*
The mark. One bar per slide, and it is either the current slide's or it is not — the difference
is its colour and nothing else.

ITS WIDTH IS FIXED. The design draws the current mark longer as well as darker, 26 against 18,
and built that way the row moved every time the slides changed: one bar grew while another
shrank. A pager that shifts under the thing it is pointing at is worse than one that is a little
less emphatic, so every bar is the same 18 and only the colour says which is which.
*/
.app__tab::before {
  width: var(--app-tab-bar-w);
  height: var(--app-tab-bar-h);
  margin-left: var(--app-tab-inset);
  border-radius: var(--app-tab-bar-h);
  background: var(--app-tab-track-color);
  content: "";
  transition: background-color var(--app-slide-dur) var(--app-slide-ease);
}

.app__tab[aria-selected="true"]::before {
  background: var(--ink-slate);
}

/*
--- The carousel ----------------------------------------------------------------------------

One box, three positions. Every card is the same 1:2.042 — a phone screen — and the neighbours
are that same box scaled down and pushed aside, so the radius, the shadow and any screenshot
in it all follow without a second set of numbers.

The stage is as tall as the tallest card and every card is centred in it, so the cards turn
about a fixed line rather than about their own tops.
*/
.app__stage {
  position: relative;
  display: grid;
  align-self: center;
  min-height: calc(var(--app-card-w) * var(--app-card-ratio));
  margin: 0;
  padding: 0;
  align-items: center;
  justify-items: center;
  list-style: none;
}

/*
Every slide occupies the same cell. Out of flow would need a height stated on the stage; one
cell gives the stage the tallest card's height for nothing.
*/
/*
Only transform and opacity move, and the card is given its own compositing layer to move on.

filter is deliberately NOT in this list. Animating a blur re-rasterises the card on every frame,
and the card is a bitmap being scaled at the same time — the geometry was smooth and monotonic
when measured, so what was shaking was the drawing, not the layout. The blur now changes with
the state rather than across it: over 260ms the switch is not noticeable, and the shimmer is
gone with it.

will-change promotes the layer so the scale is a GPU transform of one rasterised texture instead
of the image being resampled at every step of the way.

blur(0) on the base rather than no filter at all: adding and removing the property changes how
the element is composited, and that change costs a frame of its own. Always present, always the
same kind of layer, only the radius differs.
*/
.app__slide {
  position: relative;
  width: var(--app-card-w);
  grid-area: 1 / 1;
  filter: blur(0);
  transition: transform var(--app-slide-dur) var(--app-slide-ease),
    opacity var(--app-slide-dur) var(--app-slide-ease);
  will-change: transform, opacity;
}

/*
Order matters: translate first, in the stage's own units, then scale about the card's centre.
Written the other way round the offset would be scaled too and the neighbours would sit closer
in than the design puts them.
*/
.app__slide[data-pos="prev"] {
  filter: blur(var(--app-card-side-blur));
  opacity: var(--app-card-side-fade);
  transform: translateX(calc(-1 * var(--app-card-offset))) scale(var(--app-card-side-scale));
}

.app__slide[data-pos="next"] {
  filter: blur(var(--app-card-side-blur));
  opacity: var(--app-card-side-fade);
  transform: translateX(var(--app-card-offset)) scale(var(--app-card-side-scale));
}

/*
Raised rather than the neighbours being sunk. --z-under is -1, and a negative z-index puts an
element behind the background of the nearest ancestor that is not a stacking context — which
here is the card, so both neighbours disappeared into its fill entirely. Lifting the current
one instead keeps all three in front of it.
*/
.app__slide[data-pos="current"] {
  opacity: 1;
  transform: none;
  z-index: var(--z-raised);
}

/*
The slides that are neither current nor beside it. Hidden rather than merely transparent, so
they are out of the reading order and out of the tab order with them.
*/
.app__slide[data-pos="off"] {
  opacity: 0;
  transform: scale(var(--app-card-side-scale));
  visibility: hidden;
}

/*
A card beside the current one is a way of reaching it. The button covers its whole face, which
is a much easier target than the 26px mark in the pager and is what a reader tries first.

Only the neighbours: on the card in front there is nothing to go to, and the ones out of frame
are hidden, which takes them out of the tab order with them.
*/
.app__jump {
  position: absolute;
  inset: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.app__slide[data-pos="current"] .app__jump {
  cursor: default;
  pointer-events: none;
}

/*
height: auto is not decoration. The template prints width and height attributes on the image —
they are what stops the page reflowing while it loads — and those map to presentational hints in
CSS. width: 100% overrides the first; nothing was overriding the second, so the image kept its
intrinsic 1280 of height, aspect-ratio lost, and the screenshot came out three hundred wide by
twelve hundred tall with its middle cropped out.
*/
.app__shot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / var(--app-card-ratio);
  border-radius: var(--app-card-radius);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  object-fit: cover;
}

/*
The placeholder, and it is meant to look unfinished.

A screenshot is missing here, and the failure this guards against is the one where a stand-in
is styled well enough that it ships: the dashes and the label are there so that anyone looking
at the page can see at a glance which slides still have no picture.
*/
.app__placeholder {
  display: flex;
  width: 100%;
  aspect-ratio: 1 / var(--app-card-ratio);
  align-items: center;
  justify-content: center;
  border: var(--rule-w) dashed var(--ink-slate);
  border-radius: var(--app-card-radius);
  background: var(--surface);
  color: var(--ink-slate);
  font-size: var(--fs-app-counter);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-app-counter);
  text-align: center;
  text-transform: uppercase;
}

/*
One column, and the carousel moves BETWEEN the intro and the slide's text. --bp-lg

Order is stated on the items rather than by reordering the markup: the reading order is what
changes here, and grid's order does not move it — so the three areas are named instead.

At --bp-lg rather than --bp-sm, because that is where the two columns stop fitting: 526 of text,
77 of gap and the carousel's 675 need 1278, and the content band is only that wide from about
1440 of viewport. Left to collapse at --bp-sm the block pushed the page 55px sideways at 1168 —
measured with it hidden and shown, so it was this block's own and not the one block 05 has.
*/
@media (max-width: 1300px) {
  .app__card {
    min-height: 0;
    grid-template-areas:
      "intro"
      "stage"
      "aside";
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
    row-gap: var(--app-col-gap);
  }
}

/*
Reduced motion: the slides still change — the block would be a single screenshot otherwise —
they simply arrive. The rotation itself is not started at all; see app.js.
*/
@media (prefers-reduced-motion: reduce) {
  .app__slide,
  .app__slide-text,
  .app__tab::before {
    transition-duration: 0s;
    transition-delay: 0s;
  }

  .app__slide-text {
    translate: 0 0;
  }
}
