/* The Lotus Den landing page. Extracted from lotus-den-poster.html —
   see lotus-den-handoff.md before changing the curve, the layer order,
   or any cqw value.

   This file WAS generated from that poster, but is now maintained here —
   the site has diverged from the print piece (real medallion artwork,
   smaller medallion type, no phone number). Do not regenerate over it. */
/* ============================================================
   THE LOTUS DEN — technique reference
   Pure CSS + SVG. No libraries, no raster assets.

   Four techniques carry the whole design:
     1. clip-path  ....... the sweeping curve that splits the page
     2. mask-image ....... soft / faded edges (badge, shadow, motifs)
     3. feTurbulence ..... paper grain + irregular torn edges
     4. mix-blend-mode ... making grain and shadow sit *in* the page

   Every length is written in cqw — 1cqw = 1% of the poster's own
   width — so the whole composition scales as one unit and there is
   not a single media query in the file.
   ============================================================ */

.stage {
  --linen: #e8d6c2;
  --linen-deep: #dcc7ae;
  --rust: #8d421d;
  --rust-deep: #7a3715;
  --ink-serif: #77391a;
  --ink-quote: #86401a;
  --ink-body: #3a2c22;
  --ink-olive: #3b3a24;
  --cream-text: #f0dcc2;
  --badge: #e7d2b4;
  --script: #3a3722;
  --phone: #7a3812;
  --gold: #c2a079;

  /* ---- DROP ANY PHOTOGRAPH IN HERE ----------------------------------
     Set --photo and the painted fallback scene disappears automatically.
     The photo needs no mask of its own: the cream and rust shapes paint
     on top of it, so the visible region is always the exact complement
     of the S curve. Change the curve and the photo window follows.

       --photo:      url("room.jpg");
       --photo-x:    56%;   horizontal framing (0% = left edge of photo)
       --photo-y:    50%;   vertical framing
       --photo-zoom: 1;     1 = fill the poster; >1 pushes in

     Zoom happens *about* the framing point, so pushing in keeps whatever
     you framed in place instead of drifting off.

     Framed for jennieMassageTestImage-leg-removed.png (896x1200): puts the
     wall mandala in the upper wedge and the head of the table below it.
     --photo-x is negative, which is legal and fine here: the photo stops
     short of the left edge, but the cream and rust shapes are complementary
     along the shared arc, so they always cover whatever the photo doesn't
     reach. Keep the image file next to this HTML.                       */
  /* The photograph is embedded as a data URI so this file is fully
     self-contained and works with no adjacent assets. To swap in a
     different image, replace the whole value with url("your.jpg"). */
  /* Framing only. The photograph itself is the <img class="photo-img">
     in the markup below — swap its src to change the image. */
  /* Framing for lotus-den-room.jpg (1448x1086).

     NO ZOOM, and that is the ceiling rather than a preference. The poster is
     680x1020 and this photograph is 1086 tall, so object-fit:cover scales it by
     height at 0.939 — only 6% of vertical slack. Any zoom below 1 shrinks the
     image off the poster's edges and exposes the painted fallback scene
     underneath. The generous margin either side of the mandala cannot help:
     it is horizontal, and the shortfall is vertical.

     Which is why the mandala reads about twice the size it did before. That is
     the photograph, not the framing. The previous one was shot from the far
     corner and the mandala was ~15% of its frame height; this is shot close to
     the wall, where the mandala is ~34%. Rendered, 50cqw against the old 27.
     Nothing in CSS recovers the difference — only a photograph taken from
     further back would.

     --photo-x is what remains steerable. object-fit leaves 680px of horizontal
     overflow. --photo-y does nothing here: after cover the image is exactly the
     poster's height, so there is no vertical overflow to position within.

     ZOOMING OUT IS POSSIBLE AFTER ALL, and the note above used to say it was
     not. The reasoning was wrong, not just the number. It assumed a gap would
     SHOW. It does not: the image is anchored bottom-right, so shrinking opens
     the gap at the top and the left, and the poster's own cream shape covers
     both. Measured against a magenta test layer, the top edge may travel 153px
     before it appears at the top-right corner, where the curve meets the
     poster's edge; at 162px it leaks. That budget is shared — see --photo-shift.

     The floor is 0.845. It is set by the photograph, not by CSS: the file is
     1086px tall and the cutout it shows through is about 862px, so 862/1086 is
     the smallest it can be drawn and still cover. Only a taller photograph
     beats that. Padding the canvas upward was built and tried (1448x1856,
     twice the artwork's height in fabricated wall) and it does lift the ceiling
     entirely — but it was not needed at this zoom, so the real photograph is
     still what ships.

     0.85 is that floor, rounded to the last value measured clean, and it is
     what Jennie chose: it shows more of the wall artwork than anything else
     reachable. It spends the whole budget on zoom — 1020 x 0.15 = 153px — so
     --photo-shift has nothing left and is 0. Do not raise the shift without
     lowering the zoom to pay for it.

     --photo-x is deliberately LEFT ALONE at 22.8%, and that is a composition
     decision rather than an oversight. Anchoring the right edge drags the
     mandala rightwards as the image shrinks, so holding its centre still would
     need 26.51%; that was built and rejected. Pushing the framing right to
     rescue the sunburst's outermost rays costs the carved disc its place in the
     composition, and the disc is the thing people read as the artwork. So the
     rays run off the poster's right edge on purpose and the whole circle stays.

     The consequence is that the mandala sits at 82.5cqw rather than 79, and
     .badge-shadow / .badge-text are set to 69.5cqw to stay centred under it.
     These two numbers move together — change --photo-x or --photo-zoom and the
     medallion must be re-measured against the render. */
  --photo-x: 22.8%;
  --photo-y: 50%;
  --photo-zoom: 0.85;

  /* Travel DOWN, applied after the zoom. Zoom already moves the picture down,
     because it shrinks against the bottom edge; this buys more of the same. The
     two draw on ONE budget: 1020(1 - zoom) + shift must stay under ~153px. At
     zoom 0.85 that budget is exhausted, hence 0 here. */
  --photo-shift: 0px;

  /* Anchor the bottom-right corner. This is the whole reason a gap never
     shows: the photograph is visible at the poster's right and bottom edges,
     and hidden behind cream at its top and left, so the two edges that must
     not move are pinned and the slack is pushed where it cannot be seen. */
  --photo-origin-x: 100%;
  --photo-origin-y: 100%;
}

.stage * {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* body rule dropped — .stage handles placement; see build_poster.py */

/* The frame holds the poster and the medallion side by side, so the medallion
   can hang past the poster's bottom edge without the poster's overflow:hidden
   cutting it off. It carries the width and the container-type that used to live
   on .poster, so every cqw in the composition still measures against the same
   680px. */
.poster-frame {
  position: relative;
  width: min(680px, 100%);
  margin-inline: auto;
  container-type: inline-size;
}

.poster {
  width: 100%;
  position: relative;
  aspect-ratio: 1024/1536;
  overflow: hidden;
  /* No drop shadow and no rounded corner. Both said "a printed sheet lying on a
     surface", which is what this stopped being: the page ground is now the same
     cream as the composition, so the cream simply continues past the edge and
     only the photograph and the rust read as having a boundary. */
  isolation: isolate; /* keeps blend modes contained */
  background: var(--linen);
}
.layer {
  position: absolute;
  inset: 0;
}

/* ---------- 1 · PHOTO (full bleed, underneath everything) ---------- */
.photo {
  overflow: hidden;
}
.photo-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover; /* same maths as background-size:cover */
  object-position: var(--photo-x) var(--photo-y);
  /* Order matters and is not the reading order: scale runs FIRST, then the
     translate. So --photo-shift is a distance on the poster, not a distance in
     the photograph — 55px means 55 poster pixels down whatever the zoom. */
  transform: translateY(var(--photo-shift, 0)) scale(var(--photo-zoom));
  /* Zoom pivots about --photo-origin-*, which is NOT the same thing as
     --photo-x/y and defaults to them only for backwards compatibility.

     They are different coordinate systems, which is the trap. object-position
     aligns a point in the IMAGE with the same point in the box; transform-origin
     names a point in the BOX. Tying zoom's pivot to object-position therefore
     pivots about wherever that fraction happens to fall in the box, not about
     the thing you framed — so zooming out walks the subject off the edge.

     Set --photo-origin-* to where the subject actually RENDERS, in box terms,
     and zoom then scales about it and leaves it still. */
  transform-origin: var(--photo-origin-x, var(--photo-x))
    var(--photo-origin-y, var(--photo-y));
  display: block;
}
/* Painted stand-in. It sits BELOW .photo, so as soon as --photo is set
   the real image covers it. No JS, no toggle. */
.photo-scene {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Framing guide — shows where the photo will actually be visible.
   Delete this rule and the .guide element for production. */
.guide {
  z-index: 60;
  pointer-events: none;
  opacity: 0;
}
.poster.show-guide .guide {
  opacity: 1;
}
.guide path {
  fill: none;
  stroke: #22d3ee;
  stroke-width: 3;
  vector-effect: non-scaling-stroke;
}
.guide .halo {
  stroke: #0b1120;
  stroke-width: 7;
  opacity: 0.55;
}

/* ---------- 2 · CREAM SHAPE, cut by the curve ----------
   clipPathUnits="objectBoundingBox" writes the path in 0–1 space,
   so it stretches with the element and never needs recalculating. */
.cream {
  clip-path: url(#clipCream);
  /* The page paints this same gradient, in the poster's coordinates, so the two
     are one surface — see the body rule under "Landing-page chrome". Change the
     stops or the centre here and change them there. */
  background: radial-gradient(
    120% 90% at 14% 20%,
    #e3d3bc 0%,
    #dcc8b0 50%,
    #cbb699 100%
  );
}

/* ---------- 3 · RUST SHAPE ---------- */
.rust {
  clip-path: url(#clipRust);
  /* Soften only the RIGHT edge, where the band meets the photograph, so the
     room bleeds through instead of the block ending on a hard line.
     The angle (88deg) follows the edge, which leans right as it descends.
     The top edge stays crisp — that boundary is cut by the cream layer
     above, so the S curve keeps its definition. */
  -webkit-mask-image: linear-gradient(
    88deg,
    rgb(0, 0, 0) 0%,
    rgb(0, 0, 0) 42%,
    rgba(0, 0, 0, 0.92) 47%,
    rgba(0, 0, 0, 0.45) 52%,
    rgba(0, 0, 0, 0) 58%
  );
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-image: linear-gradient(
    88deg,
    rgb(0, 0, 0) 0%,
    rgb(0, 0, 0) 42%,
    rgba(0, 0, 0, 0.92) 47%,
    rgba(0, 0, 0, 0.45) 52%,
    rgba(0, 0, 0, 0) 58%
  );
  mask-repeat: no-repeat;
  mask-size: 100% 100%;
  background: radial-gradient(
    110% 80% at 24% 24%,
    #91441d 0%,
    var(--rust) 46%,
    #7a3715 100%
  );
}

/* ---------- 4 · PAPER GRAIN ----------
   feTurbulence generates the noise; overlay blend lets the colour
   underneath through, so it reads as fibre rather than a grey film. */
.grain {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 34cqw 34cqw;
  mix-blend-mode: overlay;
  opacity: 0.34;
  pointer-events: none;
}
/* second, directional pass gives the linen its slubby weave */
.weave {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='w'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='2'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23w)'/%3E%3C/svg%3E");
  background-size: 56cqw 56cqw;
  mix-blend-mode: soft-light;
  opacity: 0.55;
  pointer-events: none;
}

/* ---------- 5 · FOLIAGE SHADOW ----------
   Blurred blobs, multiplied onto the page, then faded out with a
   radial mask so the overlay itself has no visible edge.           */
.leafshadow {
  mix-blend-mode: multiply;
  opacity: 0.68;
  pointer-events: none;
  -webkit-mask-image: radial-gradient(
    76% 56% at 80% 4%,
    #000 40%,
    transparent 92%
  );
  mask-image: radial-gradient(76% 56% at 80% 4%, #000 40%, transparent 92%);
}

/* ---------- 6 · FLOWER OF LIFE ---------- */
.flower {
  position: absolute;
  left: 47.9cqw;
  top: 2.9cqw;
  width: 30.3cqw;
  height: 30.3cqw;
  opacity: 0.9;
  -webkit-mask-image: radial-gradient(
    circle at 52% 46%,
    #000 42%,
    transparent 82%
  );
  mask-image: radial-gradient(circle at 52% 46%, #000 42%, transparent 82%);
}
.flower circle {
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.5;
}

/* ---------- 7 · MASTHEAD & BODY TYPE ---------- */
.lotus-top {
  position: absolute;
  left: 24.2cqw;
  top: 4.25cqw;
  width: 14.1cqw;
  color: var(--ink-quote);
}

.wordmark {
  position: absolute;
  left: 5.9cqw;
  width: 52cqw;
  text-align: center;
  font-family: 'Cormorant Garamond', serif;
  font-weight: 300;
  color: var(--ink-serif);
  line-height: 1;
}
.the {
  top: 16.9cqw;
  font-size: 5.4cqw;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
}
.big {
  top: 21.1cqw;
  font-size: 13cqw;
  letter-spacing: 0.132em;
  text-indent: 0.132em;
}
.big.l2 {
  top: 31.9cqw;
}

.tagline {
  position: absolute;
  top: 52.26cqw;
  /* Shifted two characters left of where it started (5.5cqw). The line is
     centred inside this box, so moving the box moves the text; at font-size
     2.9cqw one Lato character is about 1.4cqw. The first move stopped the
     final "e" of "Massage" crossing onto the rust curve; the second is
     compositional. */
  left: 2.7cqw;
  width: 56cqw;
  text-align: center;
  font-size: 2.9cqw;
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--ink-olive);
  white-space: nowrap;
}
.tagline b {
  font-weight: 400;
  color: var(--rust);
  padding: 0 0.7em;
}

.quote {
  position: absolute;
  top: 63.26cqw;
  left: 3cqw;
  width: 54.5cqw;
  text-align: center;
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-weight: 400;
  font-size: 4.3cqw;
  line-height: 4.6cqw;
  color: var(--ink-quote);
}

.body {
  position: absolute;
  left: 7.2cqw;
  width: 46cqw;
  white-space: nowrap;
  font-size: 2.62cqw;
  line-height: 3.35cqw;
  font-weight: 400;
  color: var(--ink-body);
}
/* Centred on the same axis as the rest of this column: the box spans
   7.2→53.2cqw, so its centre is 30.2 — the quote centres on 30.25, the tagline
   on 30.7. The block does not move; only the lines centre within it.

   p2 deliberately stays left-aligned. The cream is a wedge that closes to the
   right as it descends (its edge runs 53% of the poster width at 85cqw down to
   35% at 100cqw), and p2's lines get shorter as they go. Left-aligned, each
   short line ends further left and stays on the cream; centred, half the slack
   is pushed right and every line after the first lands on the photograph, dark
   ink on a dark chair. p1 sits higher where the wedge is still wide enough to
   centre in. */
.body.p1 {
  top: 80.46cqw;
  text-align: center;
  /* Centred in the cream rather than on the column axis the quote uses. The
     cream runs edge to edge on the left and is cut on the right by the arch, so
     its middle drifts as it descends — 27.21cqw at this paragraph's top row,
     25.88 at its middle, 24.26 at its foot, mean 25.78. The box is 46 wide, so
     it starts 23 left of that. Measure this against the rows the paragraph
     actually occupies: the same block sits 7cqw higher on a phone, where the
     wedge is wider and the answer is 26 (see the media query).

     Then right again, to sit half way between "centred" and the arc: from 3.11,
     where 1.54cqw of cream followed the final "e" of "presence.", to 3.88, which
     splits that gap and leaves 0.77. One Lato character here is 1.27cqw (line
     three sets 33 characters in 41.77cqw), so this is the finest of the moves —
     about six tenths of a character. Past roughly 4.6 the "e" touches the arc,
     which is the constraint this is balanced against. */
  left: 3.88cqw;
}

/* ---------- 8 · DIVIDER RULES ---------- */
.rule {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 1.3cqw;
}
.rule::before,
.rule::after {
  content: '';
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.4;
}
.rule svg {
  width: 2.9cqw;
  height: 2.9cqw;
  flex: none;
}
.rule.r-head {
  top: 48.05cqw;
  left: 17.1cqw;
  width: 28.3cqw;
  color: var(--ink-quote);
}

.lotus-div {
  position: absolute;
  left: 27.2cqw;
  top: 73.16cqw;
  width: 8.1cqw;
  color: var(--ink-quote);
}

/* ---------- 9 · RUST-BAND CONTENT ---------- */
.band {
  position: absolute;
  /* Centred on the rust (which runs 0-52cqw at these rows, middle 26.5cqw),
     then moved 3cqw down and 3cqw right — a 45-degree shift of about 20px on
     each axis. Three quarters of an inch was asked for, which needs 51px of
     downward travel at 45 degrees; only 27px exists before the closing heart
     leaves the poster. This is the most that fits without retiming the spacing
     between the three blocks. */
  /* Widened from 4.25/44.5 to hold the session line on one line. Note that
     this does NOT move any text: every line here is centred, and the centre is
     unchanged at 26.5cqw, which is the rust's own midpoint. The column only
     decides where a line is forced to wrap, and the session line was the one
     hitting it. Every other line in the band is far shorter and sits exactly
     where it did. */
  left: 1.5cqw;
  width: 50cqw;
  text-align: center;
  color: var(--cream-text);
}
.band .caps {
  text-transform: uppercase;
  font-size: 2.32cqw;
  letter-spacing: 0.19em;
  text-indent: 0.19em;
  line-height: 3.9cqw;
}
.band .ital {
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-size: 3.4cqw;
  line-height: 4.6cqw;
  /* Cormorant defaults to oldstyle figures, where the 9 descends and the 0 sits
     at x-height — "90" reads as "9o" at this size. The caps lines beside it use
     lining figures, so this also keeps the numerals consistent down the column. */
  font-variant-numeric: lining-nums;
}

/* The session line only. It is the widest thing in the band and the only line
   that has ever threatened to wrap.

   "check-in · 90 min bodywork · integration" is 48.98cqw at the shared 3.4,
   which is why the word was briefly shortened to "integrate" and the type was
   briefly dropped to 3cqw. Both were the wrong trade. The right one is the
   widened column above plus a small reduction in the SPACES: word-spacing
   touches only the six gaps, where letter-spacing touches all forty characters
   and a font-size change is visible next to "Sliding scale" beside it.

   -0.03em is about 0.7px per gap at this size. It brings the line to 48.37cqw
   inside a 50cqw column — 1.63cqw of slack, comfortably more than the 1.03cqw
   the original, shorter line ran with. That margin is the point: Cormorant is a
   webfont, and the fallback serif's wider metrics have to fit too.

   The right-hand end lands at 50.68cqw against rust that runs to 52, so about
   9px of clearance before the photograph. That is the real limit here — not the
   poster's edge, which is 15px away on the left. */
.band .ital.tight {
  word-spacing: -0.03em;
}

.rule.r-b1 {
  top: 110.4cqw;
  left: 17cqw;
  width: 19cqw;
  color: var(--cream-text);
}
/* b1, r-b2 and b2 all carry a +0.7cqw nudge that r-b1 and r-b3 do not.

   The three blocks between the outer two sparkle rules were sitting high in
   that space: measured off the render, 1.79cqw of air above the first line of
   type and 3.19cqw below the last. Half the 1.4cqw difference moves the whole
   group down and splits it evenly at 2.49cqw a side.

   Measured on the type's INK, not its line boxes. line-height here is much
   larger than the font-size, so the boxes carry leading the eye does not see —
   centring the boxes would leave it looking high all over again. The first and
   last lines of the group are both .caps at the same size, so the leading
   cancels between them.

   The nudge is on all three so the group translates as one and its internal
   spacing is untouched. r-b1 and r-b3 are the fixed walls and must not move. */
.band.b1 {
  top: 115cqw;
}
.rule.r-b2 {
  top: 123.5cqw;
  left: 17cqw;
  width: 19cqw;
  color: var(--cream-text);
}
.band.b2 {
  top: 125.4cqw;
}
.rule.r-b3 {
  top: 135.6cqw;
  left: 17cqw;
  width: 19cqw;
  color: var(--cream-text);
}
/* One line here now, where there were two, so this band and its rule move down
   together as a pair. Centring the line in the whole gap instead left the rule
   orphaned 3.35cqw above it — the other two sit 1.0cqw off their rules, and a
   divider that far from what it divides reads as belonging to nothing. The rule
   ends at 138.5, this clears it by that same 1.0, and the slack falls where it
   is wanted: between the closing line and the heart. */
.band.b3 {
  top: 139.5cqw;
}
/* The lotus that closed the cream column, repeated in cream on the rust.

   Sized to the heart it replaces rather than to the lotus above, which is much
   larger: matched on drawn height, since that is what sets the gap between the
   location line and the bottom of the poster. The heart drew 2.48cqw tall. This
   viewBox is 120x48 but the top petal reaches y=-5, outside it, so the drawn
   height is 49/48 of the box — 0.408 of the width once the 120:48 ratio is in.
   2.48 / 0.408 gives 6.08, rounded to 6. Being a wider mark than a heart, it
   comes out broader; that is the shape, not a size mismatch.

   left and top place the DRAWN mark, not the box. Centred on 26.5cqw, the axis
   the three rust lines centre on, and dropped so the ink sits where the heart's
   ink sat: the box starts 0.25cqw above its own drawing. */
.lotus-foot {
  position: absolute;
  top: 146.5cqw;
  left: 23.5cqw;
  width: 6cqw;
  color: var(--cream-text);
}

/* ---------- 10 · THE BADGE ----------
   The signature move. The mask composites a soft radial falloff with
   a turbulence-displaced edge, so the disc dissolves into the photo
   instead of stamping a hard circle on top of it.                   */
/* The shadow lives on a wrapper, not on .badge itself, and it has to.
   Filters are applied BEFORE masking, so a drop-shadow on the masked element
   is computed from the unmasked artwork and is then cut away by the mask —
   mask-clip is border-box, and everything the shadow throws past the disc lies
   outside it. Verified in Chrome with a loud red shadow: on .badge it appeared
   only as a sliver trapped inside the disc's own edge; on this wrapper it
   blooms below the disc as intended. mask-clip:no-clip does not rescue it,
   because the radial-gradient is itself transparent beyond the circle.

   Filtering the parent means the shadow is taken from the child's ALREADY
   masked alpha, so it keeps the feathered edge rather than the square box a
   box-shadow would have drawn. */
.badge-shadow {
  position: absolute;
  /* HORIZONTAL — centred on the carved mandala on the wall, so the eye drops
     from the wall piece straight down to the medallion and the two lotuses
     stack.

     The old value here was 70.24cqw, derived from a measurement of the sunburst
     centred on 83.2cqw. That measurement was taken off the PREVIOUS room
     photograph. The photo was replaced, and the new one was shot much closer to
     the wall, which moved the mandala and left the old number aligned to
     nothing — the disc simply sat right of the carving.

     Re-measured off the current render: the carved lotus centres on 79cqw, so
     a 26cqw disc starts at 66. Confirmed by rendering 70.24 / 68 / 66 / 64 and
     comparing them in a crop holding the mandala and the medallion in one
     column; 66 is the one where the two centres stack. If the room photograph
     is EVER replaced again, this number is stale — re-measure, do not trust it.

     VERTICAL — dropped 9cqw below the alignment, on purpose. That carries the
     lotus at its foot past the poster's bottom edge: the mark lands at
     150.7–152.7cqw against an edge at 150, so it laps onto the page below and
     the disc reads as an object ON the poster rather than part of the picture.
     This only works because the medallion lives in .poster-frame rather than in
     .poster, which clips.

     (Before that, the alignment itself was nudged down a third of an inch by
     eye: 32px — CSS treats 1in as 96px — against the poster's 680px rendered
     width is 4.71cqw, so 116.77 became 121.48. Expressed in cqw rather than px
     so the offset scales with the poster instead of drifting as it resizes.) */
  left: 69.5cqw;
  top: 130.48cqw;
  width: 26cqw;
  height: 26cqw;
  filter: drop-shadow(0 0.7cqw 1.1cqw rgba(40, 22, 10, 0.34));
}

/* Fills the wrapper, which carries the placement. Every cqw below still
   measures against .poster-frame, so the mask geometry is unchanged. */
.badge {
  position: absolute;
  inset: 0;
}
.badge-text {
  position: absolute;
  /* Exactly coincident with .badge, so its flex centring centres the contents
     in the disc rather than in a box offset from it. */
  left: 69.5cqw;
  top: 130.48cqw;
  width: 26cqw;
  height: 26cqw;
}
/* Real deckled-paper artwork, replacing the painted gradient disc. The file
   already carries its own ragged alpha edge, so the roughEdge turbulence
   filter and the border-radius are gone — either would fight it. Size is
   unchanged at 38.1cqw; only the surface differs.

   The mask feathers just the outermost few percent so the disc settles into
   the photograph instead of cutting against it. Every stop has exactly one
   position and uses rgba(0,0,0,0) rather than `transparent` — per the
   handoff, two-position stops and the `transparent` keyword make stricter
   parsers drop the whole declaration. */
.badge {
  background-image: url('./medallion.png');
  background-size: 100% 100%;
  background-repeat: no-repeat;
  -webkit-mask-image: radial-gradient(
    circle closest-side at 50% 50%,
    rgba(0, 0, 0, 1) 88%,
    rgba(0, 0, 0, 0.75) 95%,
    rgba(0, 0, 0, 0) 100%
  );
  mask-image: radial-gradient(
    circle closest-side at 50% 50%,
    rgba(0, 0, 0, 1) 88%,
    rgba(0, 0, 0, 0.75) 95%,
    rgba(0, 0, 0, 0) 100%
  );
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
.badge-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}
.badge-text .sun {
  width: 3.66cqw;
  color: var(--phone);
  opacity: 0.75;
}
/* Two lines instead of four leaves room, so the type is 1.55x what it was
   while the sun and the mark below grew only 1.25x — the ornaments stay subordinate to
   the words. Measured: the contents reach 77% of the disc radius, so the disc
   reads as full but not crammed. */
.badge-text .script {
  font-family: 'Caveat', cursive;
  font-weight: 600;
  font-size: 4.39cqw;
  line-height: 5.6cqw;
  color: var(--script);
  margin-top: 1.05cqw;
}
/* Matched to the heart it replaces on DRAWN height, not box width — the heart
   drew 2.047cqw tall inside a 2.73cqw box. This viewBox is 120x48 with the top
   petal at y=-5, outside it, so the drawing is 49/48 of the box and 0.408 of the
   width. 2.047 / 0.408 gives 5. The margin is larger than the heart's 2cqw
   because the heart's box padded its drawing from above while this one's
   overhangs it: 0.205cqw down there, 0.208cqw up here, so the margin absorbs
   both to leave the ink where it was. */
.badge-text .lotus {
  width: 5cqw;
  color: var(--phone);
  opacity: 0.85;
  margin-top: 2.41cqw;
}

/* ---------- 11 · UNIFYING PASSES ---------- */
.vignette {
  background: radial-gradient(
    120% 100% at 50% 45%,
    transparent 54%,
    rgba(70, 40, 18, 0.18) 100%
  );
  mix-blend-mode: multiply;
  pointer-events: none;
}

svg.linework {
  overflow: visible;
}
svg.linework path,
svg.linework circle,
svg.linework line {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
}

/* ── Landing-page chrome. Not part of the poster composition. ─────────── */

/* The page ground is the composition's own cream, so the poster stops reading
   as an object on a page and the page reads as the thing itself.

   It is the SAME radial gradient .cream paints, rewritten in page coordinates
   so it lands in the same place. A flat colour cannot do this: the cream's
   gradient changes with height, so any single colour matches at one row and
   drifts everywhere else — measured 9 levels out a fifth of the way down and 21
   by half, which is the seam getting stronger as it descends.

   The rewrite. The poster is min(680px, 100vw - 2rem) wide, 1.5 times that
   tall, centred by .stage, and 1rem from the top. Its gradient is
   "120% 90% at 14% 20%" of its own box, so against the page that is:

     radius x   120% of the poster's width          1.2  x its width
     radius y    90% of its height (1.5 x width)    1.35 x its width
     centre x   its left edge + 14% of its width    50% - 0.36 x its width
     centre y   its top edge + 20% of its height    1rem + 0.3  x its width

   Written as it is below for two reasons, both of which cost an afternoon.
   Chromium rejects multiplying a min() — calc(1.2 * min(680px, 100%)) is
   invalid and drops the whole declaration, leaving the page white — so each
   factor is distributed into its own min() instead. And percentages in a radial
   gradient resolve against the box's WIDTH for the x radius but its HEIGHT for
   the y radius, so a percentage cannot express "1.35 times the poster's width"
   vertically; vw is a length and has no such axis. Hence 120vw, not 120%.

   Because it is written against the poster's own width, it follows the poster
   at every size instead of being tuned for one.

   The stops are lifted from the ones .cream declares, because the grain
   (overlay, .34) and weave (soft-light, .55) sit over the poster and not over
   the page: the same declared colour renders four to ten levels lighter inside
   the poster than out. These are those values measured back. Change the grain or
   weave opacity and these need re-measuring — sample just inside the poster's
   edge and just outside, at the top of the cream and at the bottom of it.

   Set here rather than on the shared token because poster.css loads on the
   landing page alone — the About page keeps the site's own bone ground. Colour
   is a literal rather than var(--ink): that flips with prefers-color-scheme,
   and light text would be unreadable on a ground that is cream either way. */
body {
  /* The gradient is sized to the body's box, so all three of these matter. The
     box has to reach the bottom of the viewport or it ends where the content
     does and the canvas shows through; without no-repeat the gradient tiles
     past that box and the second tile's top edge draws a hard horizontal line
     across the page; and the colour catches anything either misses. */
  min-height: 100svh;
  background-color: #d2c0a7;
  background-repeat: no-repeat;
  background-image: radial-gradient(
    min(816px, calc(120vw - 2.4rem)) min(918px, calc(135vw - 2.7rem)) at
      calc(50% - min(244.8px, calc(36vw - 0.72rem)))
      calc(1rem + min(204px, calc(30vw - 0.6rem))),
    #e8dbc8 0%,
    #e1d1bd 50%,
    #d2c0a7 100%
  );
  color: #3a2c22;
}

.stage {
  --stage-reserve: 4rem;
  display: grid;
  place-items: center;
  padding: 1rem;
}

/* The wordmark is an h1 now; its three lines keep the poster's absolute
   placement, so the heading itself must not introduce a box of its own. */
.wordmark-group {
  margin: 0;
  font-weight: inherit;
}
.wordmark-group .wordmark {
  display: block;
}

/* The medallion is the page's only call to action. */
.badge-link {
  text-decoration: none;
  cursor: pointer;
  transition: transform 180ms ease-out;
}
.badge-link .script {
  /* Matches the wordmark rather than the olive --script, so the medallion reads
     as part of the same piece. Measured against the real paper pixels behind
     the text: worst case 3.92:1, 5th percentile 4.50:1. At the desktop size
     (28px) this is WCAG "large text", which needs 3.0 — comfortable. On a
     375px-wide poster the type falls to ~15.6px, where AA wants 4.5; #663015
     would clear that everywhere if it ever matters. */
  color: var(--ink-serif);
}
.badge-link:hover {
  transform: scale(1.02);
}
.badge-link:active {
  transform: scale(0.99);
}
.badge-link:focus-visible {
  outline: 0.5cqw solid var(--phone);
  outline-offset: 0.8cqw;
  border-radius: 50%;
}

@media (prefers-reduced-motion: reduce) {
  .badge-link {
    transition: none;
  }
}

/* ── The continuation band ───────────────────────────────────────────────
   A round photograph on the left with the second paragraph beside it.

   The motivation was a phone: a 2:3 poster on a tall screen leaves about a
   third of the view empty while the poster's own copy renders around 9px. This
   takes one paragraph out of the composition and sets it at a readable size in
   the space that was going to waste. It is not phone-only — the same move suits
   the desktop composition, so the band shows at every width.

   This was chosen over two alternatives (the poster carrying every paragraph,
   and the paragraph set over a full-width image) after comparing all three on a
   real device. The other two are gone; see git history if they are ever wanted.

   It lines up with the poster: .stage pads 1rem a side and the poster is
   min(680px, 100%) of what remains, so this repeats that sum rather than
   guessing a value that would drift if either changes. */
.continuation {
  width: min(680px, calc(100% - 2rem));
  margin: 0 auto;
  display: grid;
  grid-template-columns: 9rem 1fr;
  align-items: center;
  gap: 1.25rem;
  /* The indent moves the circle and the text right together. It is padding
     inside a box whose width still matches the poster's, so the right edge
     stays flush with the poster above and only the left edge comes in — the
     band keeps sitting squarely under the composition rather than drifting out
     from under it. */
  padding: 1.5rem 0 2rem 2rem;
}

.continuation__text {
  margin: 0;
  font-family: 'Lato', system-ui, sans-serif;
  line-height: 1.55;
}

.continuation .continuation__figure {
  margin: 0;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 50%;
}

.continuation .continuation__figure img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Framed by rendering the candidates rather than by eye. The image is 800x532
     and the frame is square, so 532px of its width shows. Both hands and the
     whole head span about 450px of that, so unlike the photograph this replaced
     there is slack: the choice is compositional rather than forced. 70% puts the
     hand resting on the head fully inside the circle, keeps the face complete,
     and leaves the lower hand its fingers — further left and those fingers meet
     the circle's edge, further right and the face slides off centre. There is no
     vertical overflow to position: cover scales this image by its height, so the
     second value does nothing here. */
  object-position: 70% 50%;
}

.continuation .continuation__text {
  font-size: 1rem;
  color: #3a2c22;
}

@media (max-width: 48rem) {
  /* The half-inch drop is a desktop move, and on a phone it has to come back.
     Enlarged (below), the first paragraph is a far taller block — four wrapped
     lines instead of three set ones — and at the dropped position its foot
     reaches 97cqw, where the cream wedge has closed to 42% of the poster width.
     Its last lines crossed onto the photograph. Putting the column back where
     it was drawn buys that height in the part of the wedge that is still wide. */
  .stage .tagline {
    top: 45.2cqw;
  }
  .stage .rule.r-head {
    top: 50.4cqw;
  }
  .stage .quote {
    top: 56.2cqw;
  }
  .stage .lotus-div {
    top: 66.1cqw;
  }

  /* Enlarged because 9px is too small to read, and shifted left because the
     wedge narrows as it descends. 46cqw from 3 centres the block on 26, which
     is where the cream's own mid-line falls across the rows it occupies (28.6
     at the top row, 24 at the bottom). Four lines, every one clear of the arc
     by at least 5.8cqw. Narrower boxes fit too but cost a fifth line and strand
     "presence." on its own. */
  .stage .body.p1 {
    white-space: normal;
    top: 73.4cqw;
    left: 3cqw;
    width: 46cqw;
    font-size: 3.4cqw;
    line-height: 4.2cqw;
  }

  /* Its <br>s were placed for the poster's own line lengths; once it wraps at a
     different width they fire as well as the wrapping and strand single words. */
  .stage .body.p1 br {
    display: none;
  }

  .continuation {
    grid-template-columns: 7.8rem 1fr;
    gap: 1rem;
    /* Same 20% growth and the same indent as the desktop rule, scaled down:
       a phone has far less width to give away, so the indent is half. */
    padding: 1.25rem 0 2rem 1rem;
  }

  .continuation .continuation__text {
    font-size: 0.95rem;
  }
}
