/*
 * Slide rendering.
 *
 * One stylesheet, three consumers: editor preview, filmstrip thumbnails, and
 * the offscreen export node (spec §6.1). Preview and export share this file and
 * the same component, so they cannot drift.
 *
 * ---------------------------------------------------------------------------
 * EVERY LENGTH IS PROPORTIONAL, NOT FIXED
 * ---------------------------------------------------------------------------
 * The previous version hardcoded 110px/100px padding and 88px type regardless
 * of canvas. That is roughly right at 1080×1350 and wrong everywhere else — on
 * a 1920×1080 LinkedIn canvas the same padding is a third of the proportion,
 * and on 1080×1920 stories it is cramped. Both are now derived from the canvas
 * short edge and set as custom properties by the component, so a template holds
 * its proportions across 4:5, 1:1, 16:9 and 9:16.
 *
 *   --u    the unit: 1% of the short edge
 *   --pad-x / --pad-y   frame margins, where the furniture sits
 *   --pad-top / -right / -bottom / -left   the padding of `.pad` itself
 *   --h1 / --h1-lh / --h1-track   headline size, leading and tracking
 *   --body-size   body size for this composition
 *
 * ---------------------------------------------------------------------------
 * THE TYPE SIZES ARE NOT DECIDED HERE
 * ---------------------------------------------------------------------------
 * The composition multipliers used to live in `calc()` on the rules below —
 * 1.42 on full bleed, 1.55 on scale jump. That made the rendered size a thing
 * only the browser knew, so the component could not check whether the headline
 * fitted and shipped a 203px headline into a box that could hold four lines of
 * it. The multipliers now live in `src/slide/fit.ts`, which resolves a size
 * against the space this file leaves, and the result arrives as `--h1`.
 *
 * The padding moved for a second reason. `.comp-full-bleed .pad` set the
 * `padding` shorthand at the same specificity as `.has-footer .pad` and later
 * in the file, so on a full-bleed cover with a brand footer the footer reserve
 * was silently thrown away and the headline printed through the brand mark.
 * One declaration, four custom properties, no cascade to lose.
 */

.slide {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  user-select: none;
  -webkit-user-select: none;
  font-kerning: normal;
  text-rendering: geometricPrecision;
  /* Long words must break rather than push past the frame. The old build
     clipped headlines on two of four compositions. */
  overflow-wrap: break-word;
  word-break: normal;
  hyphens: auto;
}

.slide img {
  -webkit-user-drag: none;
}

/* --- background ----------------------------------------------------------- */

/* The seamless background is ONE PICTURE ACROSS THE WHOLE DECK: `SlideView`
   sizes it to `total * dims.w` and slides it by `-index * dims.w`. So it is the
   one image in the product that is deliberately many times wider than the box
   containing it, and Tailwind's preflight — `img, video { max-width: 100% }` —
   clamped it back to a single frame. `max-width` wins over an inline `width`
   whatever the specificity, so the picture came out exactly one frame wide:
   frame 0 looked correct and every frame after it was pushed fully outside a
   `.slide` that is `overflow: hidden`, and drew nothing. The gradient twin was
   never affected because it paints into a `<div>`. */
.slide .deckbg  { position: absolute; top: 0; z-index: 0; max-width: none; }
.slide .bgimg   { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
.slide .bgscrim { position: absolute; inset: 0; z-index: 1;
                  background: linear-gradient(180deg, rgba(10,12,14,.28), rgba(10,12,14,.62)); }
/* A brand's own pattern, repeated. Everything that varies — the image, the tile
   size and the opacity — is set inline by `tilePattern`, because the tile size
   is in canvas units and only the component knows what a unit is here. */
.slide .bgtile { position: absolute; inset: 0; z-index: 0; }
.slide .texture { position: absolute; inset: 0; z-index: 1; pointer-events: none; }

/* --- frame ---------------------------------------------------------------- */

.slide .pad {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  height: 100%;
  /* Composition insets, the footer reserve and the photo-half offset are all
     folded into these four by the component. */
  padding: var(--pad-top) var(--pad-right) var(--pad-bottom) var(--pad-left);
  /* Content is top-aligned by default. Vertical placement is a composition
     decision, not a global one — the old build centred everything, which is
     what made all ten layouts read the same. */
  justify-content: flex-start;
}

.slide.align-center .pad { align-items: center; text-align: center; }

/* --- composition ---------------------------------------------------------- */

/*
 * Structural, not cosmetic. Previously these differed only by padding and a
 * font size, so a deck set to "split" looked like a deck set to "anchored".
 */

/* Stack — the institutional default. Top-aligned, ruled, generous. */
.slide.comp-stack .pad { justify-content: flex-start; }
.slide.comp-stack .s-rule {
  display: block;
  width: calc(var(--u) * 14);
  position: relative;
  --rule-stroke-height: calc(var(--u) * 0.55);
  /* Preserve the old stroke + gap total while expanding the hit target. */
  margin-bottom: calc(var(--u) * 5 - 28px + var(--rule-stroke-height));
}
.slide.comp-stack .s-rule-stroke {
  display: block;
  width: 100%;
  height: var(--rule-stroke-height);
  background: var(--sl-accent);
  pointer-events: none;
}

/* Anchored — content pinned low, large silence above. */
.slide.comp-anchored .pad { justify-content: flex-end; }

/* Full bleed — type to the frame edge, tight leading, no sanctuary. */
.slide.comp-full-bleed .pad { justify-content: center; }

/*
 * Split — a real division. The old version tinted 42% of the frame one step
 * off the background, which was invisible at any size. This is a hard colour
 * block carrying its own ink.
 */
.slide.comp-split { flex-direction: row; }
.slide.comp-split .pad {
  width: 56%;
  justify-content: center;
}
.slide.comp-split .split-fill {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 44%;
  z-index: 1;
  /* The panel carries its own ink so a dark block against a pale ground still
     reads, rather than inheriting text colour meant for the other half. */
  color: inherit;
  display: flex;
  align-items: flex-end;
  padding: var(--pad-y) calc(var(--pad-x) * 0.7);
}
.slide.comp-split .split-fill img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}

/* Scale jump — one element dominant, everything else deliberately small. */
.slide.comp-scale-jump .pad { justify-content: flex-start; }
.slide.comp-scale-jump .s-body {
  margin-top: calc(var(--u) * 5);
  max-width: var(--scale-body-width, 58%);
}

/* One authored composition, three deterministic structural recipes. These
   selectors never reach `.responsive-broken`: manual geometry is an explicit
   exception and stays where the author put it. */
.slide.responsive-wide.comp-split .pad,
.slide.responsive-square.comp-split .pad { width: var(--split-content); }
.slide.responsive-wide.comp-split .split-fill,
.slide.responsive-square.comp-split .split-fill { width: var(--split-media); }
.slide.responsive-tall.comp-split .pad {
  width: 100%;
  height: var(--split-content);
  padding-bottom: calc(var(--pad-y) * .7);
}
.slide.responsive-tall.comp-split .split-fill {
  top: auto;
  left: 0;
  width: 100%;
  height: var(--split-media);
}

/*
 * Band — headline sits in a full-width accent band. Editorial, and the one
 * composition that reads instantly at thumbnail size, which matters because
 * that is how a carousel is first seen in a feed.
 */
.slide.comp-band .pad { justify-content: center; }
.slide.comp-band .s-headline {
  /* Contrast-checked band rather than the raw accent: on palettes where the
     accent equals the ground, a raw-accent band is invisible. */
  background: var(--sl-band);
  color: var(--sl-on-band);
  padding: calc(var(--u) * 3) var(--pad-x) calc(var(--u) * 3.6);
  width: 100%;
}
.slide.comp-band .s-body,
.slide.comp-band .s-caption,
.slide.comp-band .s-signoff,
.slide.comp-band .s-cta,
.slide.comp-band .s-list { margin-left: var(--pad-x); margin-right: var(--pad-x); }

/* --- type ----------------------------------------------------------------- */

.slide .s-eyebrow {
  margin: 0 0 18px;
  color: var(--sl-ink-soft);
  font-size: calc(var(--u) * 2.1);
  font-weight: 600;
  letter-spacing: 0.12em;
  line-height: 1.2;
  text-transform: uppercase;
}

.slide .s-headline {
  color: var(--sl-ink);
  font-size: var(--h1);
  line-height: var(--h1-lh);
  letter-spacing: var(--h1-track);
  text-wrap: balance;
  max-width: 100%;

  /*
   * The author's own breaks, honoured.
   *
   * This was `normal`, so a newline in the headline reached the DOM and was
   * rendered as a space — the one field where a designer needs to place the
   * break was the one field that collapsed it, while the body two rules down
   * had honoured breaks all along.
   *
   * `pre-line` rather than `pre-wrap`: a headline needs control of the break
   * and nothing else, and a trailing space left over from typing visibly
   * shifts a centred line under `pre-wrap`. Neither value changes the text
   * nodes, so compliance offsets are unaffected either way.
   */
  white-space: pre-line;

  /*
   * Display type does not get hyphenated or guillotined mid-word. The frame
   * rule above sets `hyphens: auto` and `overflow-wrap: break-word` for the
   * slide, which is right for body copy in a narrow column and is what turned
   * "everything" into "everythin" and an orphaned "g" at 200px. The fitter
   * guarantees the longest word fits the measure, so nothing here needs to
   * break; `anywhere` stays off and only a soft hyphen the author typed will
   * split a word.
   */
  hyphens: manual;
  overflow-wrap: normal;
}

/*
 * Emphasis on part of a headline.
 *
 * Ranges held beside the text rather than markers inside it — see
 * `src/slide/emphasis.ts` for why. Everything here is plain inherited CSS on a
 * span inside the shared renderer, so the exporter rasterises exactly what the
 * preview shows.
 */
.slide .s-headline .em-strong { font-weight: 900; }
.slide .s-headline .em-em { font-style: italic; }
.slide .s-headline .em-accent { color: var(--sl-emph); }

/* Where no colour in the palette separates from both the ground and the ink,
   the accent mark is drawn rather than coloured. An emphasis that cannot be
   seen is worse than none: it loses the word carrying the hook silently. */
.slide.emph-ruled .s-headline .em-accent {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 0.06em;
  text-underline-offset: 0.12em;
}

.slide .s-body {
  font-size: var(--body-size);
  line-height: 1.45;
  color: var(--sl-ink-soft);
  margin-top: calc(var(--u) * 3);
  max-width: calc(var(--u) * 74);
  white-space: pre-wrap;
}

/*
 * A TEXT BOX SOMEBODY ADDED — see `TextBox` in `types.ts`.
 *
 * It reads as body copy because that is what it is: words on the artwork, in
 * the deck's own body face, taking the deck's own ink. It is deliberately NOT
 * given a border, a placeholder colour or a dashed outline — the selection ring
 * the canvas draws is how you can see it, and furniture that shows only in the
 * editor is furniture that eventually ships in an export.
 *
 * `min-height` is the one editor-facing concession, and it is load-bearing: an
 * emptied box has no words and therefore no height, and a zero-height element
 * cannot be clicked to get back into. It paints nothing, so it exports as
 * nothing.
 */
.slide .s-textbox {
  font-size: var(--body-size);
  line-height: 1.45;
  color: var(--sl-ink-soft);
  white-space: pre-wrap;
  min-height: calc(var(--u) * 4);
}

/*
 * A box set as a heading takes the deck's INK rather than its soft ink. That is
 * what the named slots already do — `.s-headline` is `--sl-ink` and `.s-body` is
 * `--sl-ink-soft` — and it is most of what makes a heading read as one at a
 * glance. Size and face are set inline from the pairing (`Slide.tsx`), because
 * they are resolved from the deck's type rather than stated here.
 */
.slide .s-textbox.tb-heading,
.slide .s-textbox.tb-subheading {
  color: var(--sl-ink);
}

/*
 * A BULLETED OR NUMBERED BOX.
 *
 * `white-space: normal` on the item is load-bearing, not tidying. Each item's
 * text node still ENDS WITH THE NEWLINE that separated it from the next one —
 * see `textBoxLines` for why that character may not be dropped — and `normal`
 * is what collapses it to trailing whitespace the browser does not paint. Under
 * `pre-wrap` every item would render an extra blank line.
 *
 * The marker is a `::before`, never a text node, for the same reason: a bullet
 * or a number rendered as text would be characters inside the element that are
 * not in `box.text`, and every compliance offset after it would shift.
 */
.slide .s-textbox.tb-list {
  white-space: normal;
  counter-reset: tb-item;
}

.slide .s-textbox .tb-li {
  display: block;
  position: relative;
  white-space: normal;
  padding-inline-start: calc(var(--u) * 3.2);
}

.slide .s-textbox.tb-bullet .tb-li::before {
  content: '•';
  position: absolute;
  inset-inline-start: 0;
  color: var(--sl-accent);
}

.slide .s-textbox.tb-number .tb-li {
  counter-increment: tb-item;
}

.slide .s-textbox.tb-number .tb-li::before {
  content: counter(tb-item) '.';
  position: absolute;
  inset-inline-start: 0;
  color: var(--sl-accent);
  font-variant-numeric: tabular-nums;
}

/*
 * A line with nothing on it gets no marker and no counter step.
 *
 * A blank line between two items is somebody spacing their list out, not a
 * third item — and an empty bullet, or a number that jumps from 2 to 4 around
 * one, is the kind of small wrongness that gets noticed in an exported PNG and
 * cannot be explained. The line itself is kept, because its newline is part of
 * the model string.
 */
.slide .s-textbox .tb-li[data-blank]::before {
  content: none;
}

.slide .s-textbox.tb-number .tb-li[data-blank] {
  counter-increment: none;
}

/*
 * A PICTURE SOMEBODY PLACED — see `SlideImage` in `types.ts`.
 *
 * `cover` rather than `contain`, matching every other picture in this
 * stylesheet: the box is the crop, and the handles are how it is framed. A
 * `contain` fit would letterbox the picture inside a box the maker sized, which
 * silently disagrees with the rectangle the selection ring is drawn around and
 * with the one `checkLayout` measures for margins and overlap.
 *
 * The wrapper carries the geometry and the `<img>` fills it, so `freeBox` can
 * write width and height in one place for all three added kinds. `overflow`
 * clips the crop; without it a tall picture paints outside its own selection
 * ring and outside the box the export believes it occupies.
 *
 * Fallback size, for the one case that should not happen: a stored deck with
 * `images` and `free: false`. `freeBox` returns `{}` there and the element would
 * otherwise collapse to nothing.
 */
.slide .s-image {
  width: calc(var(--u) * 40);
  height: calc(var(--u) * 30);
  overflow: hidden;
}

.slide .s-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/*
 * THE THREE SHAPES — see `SHAPE_KINDS` in `types.ts` for what a shape is here
 * and why there are exactly three of them.
 *
 * The paint is set inline from a CSS VARIABLE (`shapeStyle` in `Slide.tsx`),
 * never from a stored colour, so a shape repaints with the colourway and can
 * never carry an off-palette value onto the artwork.
 *
 * One box model for all three: `rule` and `block` differ only in their corner —
 * a stroke has round ends and a field has square corners — and `ring` is the
 * same rectangle with its paint moved to the border. That is what makes resize
 * behave identically for all three, with no branch anywhere in the canvas.
 *
 * `box-sizing: border-box` is load-bearing on the ring: without it the border
 * would be drawn OUTSIDE the box `els` records, so the selection ring, the
 * measured rectangle and the painted pixels would each say a different size.
 */
.slide .s-shape {
  box-sizing: border-box;
  pointer-events: auto;
}

.slide .s-shape-rule {
  border-radius: 999px;
}

.slide .s-shape-ring {
  border-radius: 50%;
}

/*
 * Caption — a TRAILING qualifier, not a leading eyebrow. The prototype's
 * `.s-kicker` (uppercase, .18em tracking, above the headline, in eight of ten
 * layouts) is deliberately absent (spec §8.1).
 */
.slide .s-caption {
  font-size: calc(var(--u) * 2.1);
  line-height: 1.4;
  color: var(--sl-ink-soft);
  margin-top: calc(var(--u) * 2.6);
  max-width: calc(var(--u) * 62);
}

.slide .s-signoff {
  font-style: italic;
  font-size: calc(var(--u) * 4.2);
  color: var(--sl-ink);
  margin-top: calc(var(--u) * 3.6);
}

.slide .s-cta {
  display: inline-block;
  align-self: flex-start;
  margin-top: calc(var(--u) * 3.6);
  background: var(--sl-accent);
  color: var(--sl-on-accent);
  font-weight: 600;
  font-size: calc(var(--u) * 2.5);
  padding: calc(var(--u) * 1.8) calc(var(--u) * 3.6);
  border-radius: 999px;
}
.slide.align-center .s-cta { align-self: center; }
.slide.cta-square .s-cta { border-radius: 0; }

.slide .s-stat {
  font-size: calc(var(--u) * 22);
  line-height: 0.86;
  color: var(--sl-accent);
  font-weight: 700;
  letter-spacing: -0.045em;
}

.slide .s-list {
  margin-top: calc(var(--u) * 3);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 1.9);
  z-index: 2;
}
.slide .s-list .li { display: flex; gap: calc(var(--u) * 1.9); align-items: baseline; }
.slide .s-list .num {
  flex: none;
  min-width: calc(var(--u) * 4.6);
  color: var(--sl-accent);
  font-weight: 700;
  font-size: calc(var(--u) * 2.8);
  font-variant-numeric: tabular-nums;
}
.slide .s-list .lt { font-size: calc(var(--u) * 2.9); line-height: 1.35; color: var(--sl-ink); }

/* Institutional list treatment: hairline rules instead of coloured chips. */
.slide.list-ruled .s-list .li {
  border-top: 1px solid color-mix(in srgb, var(--sl-ink) 22%, transparent);
  padding-top: calc(var(--u) * 1.6);
}

/* --- furniture ------------------------------------------------------------ */

.slide .s-brand {
  position: absolute;
  bottom: var(--pad-y);
  left: var(--pad-x);
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 1.3);
  font-size: calc(var(--u) * 2.1);
  font-weight: 600;
  color: var(--sl-ink);
  z-index: 3;
}
.slide .s-brand .s-mark {
  width: calc(var(--u) * 3.5);
  height: calc(var(--u) * 3.5);
  border-radius: calc(var(--u) * 1);
  background: var(--sl-accent);
  color: var(--sl-on-accent);
  display: flex; align-items: center; justify-content: center;
  font-size: calc(var(--u) * 2);
  font-weight: 700;
  overflow: hidden;
}
.slide .s-brand .s-mark img { width: 100%; height: 100%; object-fit: cover; }

/*
 * `.s-mark`, NOT `.mark`.
 *
 * `index.css:1126` owns `.mark` product-wide: an inline text action with a 2px
 * `--beam` rule under it. The chip carried the bare word, so anywhere the
 * renderer is mounted inside the app — the Bench, the strip, the review page —
 * the chip was asking for that underline. Measured on the Bench at
 * `/app/b/…001/work/preview?preview`: with the old class the chip computed
 * `text-decoration: underline` in `rgb(11, 117, 111)`, and with this one it
 * computes `none`.
 *
 * NOTHING WAS VISIBLE, AND THAT IS THE ARGUMENT RATHER THAN AGAINST IT. The
 * `overflow: hidden` above clips the rule, so the before and after shots of the
 * chip are the same picture. What was wrong is that a renderer the exporter
 * also runs was drawing whatever the app's utility layer happened to say about
 * a three-letter class name, and the thing keeping it off the slide was a
 * property declared for a cropped image. The prefix is the one this file
 * already uses for everything it owns — `.s-brand`, `.s-logo`, `.s-swipe`.
 */

/*
 * A mark the brand stated is a wordmark.
 *
 * `cover` on a square is right for a monogram and for the primary lockup, and
 * it is what every deck already in the database is drawn with — so it stays the
 * default. A wordmark cropped to its middle third is the failure the `shape`
 * axis exists to prevent, and this is the half of that prevention the renderer
 * pays: the chip grows sideways and the image is contained rather than cropped.
 * Capped at 14u so a long lockup cannot walk into the swipe cue on the right.
 */
.slide .s-brand .s-mark.wide {
  width: auto;
  min-width: calc(var(--u) * 3.5);
  max-width: calc(var(--u) * 14);
  padding: 0 calc(var(--u) * 0.7);
}
.slide .s-brand .s-mark.wide img { width: auto; max-width: 100%; object-fit: contain; }

.slide .s-swipe {
  position: absolute;
  bottom: var(--pad-y);
  right: var(--pad-x);
  font-size: calc(var(--u) * 2);
  font-weight: 600;
  color: var(--sl-accent);
  z-index: 3;
}

/*
 * Frame counter, top-right. Sequence position is real information in a
 * carousel — it is the swipe mechanic — and the bottom edge already carries
 * the brand mark and the swipe cue, so putting it there collided with one or
 * the other depending on which was enabled.
 */
.slide .s-frame {
  position: absolute;
  top: var(--pad-y);
  right: var(--pad-x);
  /*
   * ONE MONO IN THE INSTRUMENT.
   *
   * Measured: `01/05` rendered Martian Mono at 17.28px while the plate directly
   * above it rendered SF Mono, so two monospace typefaces were on screen at
   * once, inches apart. The stack is spelled out rather than taken from
   * `var(--mono)` for the reason the hover rule below already gives: this file
   * is the tenant renderer and must not inherit chrome theme variables, or a
   * brand kit could move the instrument's type.
   *
   * NOTE FOR THE OWNER: `.s-frame` is the only one of these that is CUSTOMER
   * ARTWORK — the frame counter is drawn into the exported PNG. This changes
   * the typeface of `01/05` in every deck already exported and every deck
   * exported from now on. It is the right call under the type law and it is not
   * a chrome-only change; flagged so it is a decision rather than a side effect.
   */
  font-family: 'SF Mono', ui-monospace, 'Menlo', 'Cascadia Mono', monospace;
  font-size: calc(var(--u) * 1.6);
  letter-spacing: 0.04em;
  color: var(--sl-ink-soft);
  z-index: 3;
  font-variant-numeric: tabular-nums;
}

/* --- layout specifics ----------------------------------------------------- */

.slide.photo-half .half-img {
  position: absolute; top: 0; left: 0; width: 100%; height: 46%;
  object-fit: cover; z-index: 1;
}
/* The top inset that clears the photo is folded into `--pad-top`. */
.slide.photo-half .pad { justify-content: flex-start; }
.slide.responsive-wide.photo-half .half-img {
  width: var(--photo-media);
  height: 100%;
}
.slide.responsive-wide.photo-half .pad {
  width: var(--photo-content);
  margin-left: var(--photo-media);
}
.slide.responsive-square.photo-half .half-img,
.slide.responsive-tall.photo-half .half-img { height: var(--photo-media); }

.slide.photo-bg .bg-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
.slide.photo-bg .scrim  { position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, rgba(10,12,14,.06) 28%, rgba(10,12,14,.8) 100%); }
.slide.photo-bg .pad    { justify-content: flex-end; }
.slide.photo-bg .s-headline,
.slide.photo-bg .s-body,
.slide.photo-bg .s-caption,
.slide.photo-bg .s-brand { color: #fff; }

.slide.screenshot .pad { flex-direction: row; align-items: center; gap: calc(var(--u) * 5); }
.slide.screenshot .sc-text { flex: 1; min-width: 0; }
.slide.screenshot .phone {
  flex: none;
  width: var(--phone-width, calc(var(--u) * 30));
  height: calc(var(--u) * 62);
  border-radius: calc(var(--u) * 4);
  background: #fff;
  border: calc(var(--u) * 1) solid var(--sl-ink);
  overflow: hidden;
}
.slide.responsive-tall.screenshot .pad {
  flex-direction: column;
  align-items: flex-start;
}
.slide.responsive-tall.screenshot .sc-text { flex: 0 1 auto; }
.slide.responsive-tall.screenshot .phone {
  align-self: center;
  height: calc(var(--u) * 58);
}
.slide.screenshot .phone img { width: 100%; height: 100%; object-fit: cover; }
.slide.screenshot .ph-empty {
  display: flex; align-items: center; justify-content: center; height: 100%;
  color: #9aa7ac; font-size: calc(var(--u) * 1.9); text-align: center; padding: calc(var(--u) * 2);
}

.slide .s-logo {
  max-width: calc(var(--u) * 32);
  max-height: calc(var(--u) * 24);
  margin-bottom: calc(var(--u) * 3.4);
  object-fit: contain;
}

/* --- editing affordances (preview only; absent from export) --------------- */

/*
 * MOVE-VERSUS-RESIZE HAS TO BE VISIBLE BEFORE THE PRESS, not after it.
 *
 * The owner: "there are no visual affordances when i'm actually able to drag it
 * rather than resize it."
 *
 * Every element is grabbable, and the eight handles on the ring carry the
 * resize cursors, so the two gestures now read as two cursors: open hand on the
 * body, directional arrows on the handles.
 *
 * The text elements used to be exempted to `cursor: text` on the grounds that a
 * click puts a caret in. It does not — `pressOutcome` selects on the first click
 * and only opens typing on a click on the ALREADY selected element — so on
 * approach the caret cursor described a gesture the first press does not
 * perform, on exactly the elements a maker most wants to drag. Measured on the
 * real Bench: the headline reported `cursor: text` while a real mouse drag from
 * its centre moved it 40x25px.
 *
 * While typing in place the inline editor draws its own field over the element
 * and brings its own caret, so nothing here has to describe that state.
 */
.slide[data-editable='true'] [data-el] { cursor: grab; }
.slide[data-editable='true'] [data-el]:active { cursor: grabbing; }
.slide[data-editable='true'] [data-el]:hover {
  /* Chrome colour, deliberately hardcoded rather than pulled from the app
     token: slide.css is the tenant renderer and must not inherit chrome theme
     variables, or a brand kit could move an editing affordance. It is stripped
     from the export node with the rest of this block. */
  outline: 2px dashed rgba(34, 224, 213, 0.8);
  outline-offset: 3px;
}

/*
 * Review markers on the canvas.
 *
 * Two of them, matching the two things a check can know.
 *
 * A finding that matched a pattern knows the characters, and gets painted by
 * the CSS Custom Highlight API — no wrapper elements, so nothing the exporter
 * could serialise. That is the rule below.
 */
/*
 * THE SQUIGGLE MARKS THE WORDS. IT USED TO SWAMP THEM.
 *
 * Measured on frame 3 of the Northwind deck: a blocking finding spanning "the
 * most powerful oral option in dermatology" — three lines of white serif on the
 * brand's crimson band — drew a translucent pink box across every line AND a
 * wavy underline through it. The finding was legible; the sentence was not.
 *
 * The fill is gone. The mark is the squiggle, which is the signature, and one
 * mark is enough to say "these words". A fill is a second mark saying the same
 * thing over the top of the thing it is pointing at.
 */
::highlight(review-flag) {
  text-decoration: underline wavy var(--flag-on-ground, var(--color-flag-canvas));
  text-decoration-skip-ink: none;
}

/*
 * THE SQUIGGLE HAS TO SURVIVE THE BRAND'S OWN COLOUR.
 *
 * `--color-flag-canvas` is #ff2e8e, which is 3.29:1 against the paper and
 * 1.88:1 against the crimson band the flagged headline actually sits on. A mark
 * nobody can pick out is the same defect as no mark, and the fill that used to
 * hide it is gone.
 *
 * Mixed toward the ground's OWN ink rather than toward a second hardcoded pink:
 * `--sl-on-band` is the colour the palette already contrast-checked for this
 * ground, so on a dark band the flag lightens (to #FF97C7, 3.27:1 on crimson)
 * and on a light band it darkens. One expression, no per-brand table, and it
 * cannot be wrong in a direction the ground is not.
 *
 * Nothing here can reach an export. `useCanvasHighlights` paints ranges through
 * the CSS Custom Highlight API, which has no DOM to serialise, so the node
 * `html-to-image` rasterises is not merely unstyled — it is never referenced.
 * The whole-field `[data-flagged]` mark below is keyed off an attribute the
 * EDITOR puts on its own wrapper, never on the slide.
 */
.slide.comp-band .s-headline,
.slide .s-cta {
  --flag-on-ground: color-mix(in srgb, var(--color-flag-canvas) 50%, var(--sl-on-band, #fff));
}

/* The public hero's legacy-browser mark. It is opt-in, never exported. */
.s-review-mark {
  background-color: color-mix(in srgb, var(--color-flag-canvas) 28%, transparent);
  text-decoration: underline wavy color-mix(in srgb, var(--color-flag-canvas) 95%, transparent);
  text-decoration-skip-ink: none;
}

/*
 * A finding that came from a model names a field and nothing finer, so the
 * whole element is marked. Keyed off an attribute the editor puts on its own
 * wrapper, never on the slide, so there is no path by which this reaches the
 * detached node the exporter renders.
 *
 * Underline rather than a box: a box around a headline reads as a selection,
 * and the reader would have to work out which of the two states they are
 * looking at.
 */
[data-flagged~='headline'] [data-el='headline'],
[data-flagged~='body'] [data-el='body'],
[data-flagged~='caption'] [data-el='caption'],
[data-flagged~='signoff'] [data-el='signoff'],
[data-flagged~='cta'] [data-el='cta'],
[data-flagged~='stat'] [data-el='stat'],
[data-flagged~='items'] [data-el='list'] {
  text-decoration: underline wavy color-mix(in srgb, var(--color-flag-canvas) 95%, transparent);
  text-decoration-skip-ink: none;
  text-underline-offset: 0.14em;
}
