/* ============================================================
   stourytravel.com — landing page
   Design system shared with the Stoury reference site: token
   names are identical so the two do not diverge.
   Self-hosted licensed fonts (SP Blunt, Alga Light) — do not
   redistribute the font files.
   ============================================================ */

/* SP Blunt ships Regular only. Declaring the single face as
   400 900 makes the browser map any requested weight onto it
   instead of synthesizing a faux bold, which is glaringly
   visible at hero size. */
:root{
  --coral:#f79e78; --butter:#f7d18a; --lime:#e3f09c;
  --sage:#87b5a6;  --olive:#5c5940;  --cream:#f6efe0; --ink:#3a3529;
  --olive-70:rgba(92,89,64,.7);
  --maxw:1120px;

  /* O-window rotation. step = 5.5s hold + 2s crossfade.
     cycle = image count x step. N=10, so 10 x 7.5s = 75s.

     Changing N or --step means recomputing --cycle AND the percentages in the
     owin-cycle / owin-pan-* keyframes, which are all derived from it:
       fade-in ends   2.0s / cycle =  2.6667%
       handover       7.5s / cycle = 10%        <- next layer starts fading in
       fade-out ends  9.5s / cycle = 12.6667%   <- also where the pan finishes
     A frame is visible for 9.5s but the next starts at 7.5s, so the 2s
     overlap is the crossfade itself.
     See docs/superpowers/plans/2026-08-27-stourytravel-landing-page.md
     (Task 4). */
  --step:7.5s;
  --cycle:75s;
}

*{margin:0;padding:0;box-sizing:border-box;}
body{
  background:var(--cream); color:var(--ink);
  font-family:'Stoury Serif',Georgia,serif;
  font-size:19px; line-height:1.55;
  -webkit-font-smoothing:antialiased;
}
/* width + max-width:100%, rather than max-width alone (fixed 2026-09-03).
   .hero is display:flex, so its .wrap is a FLEX ITEM, and a flex item carries
   min-width:auto — an automatic floor at its own min-content width. Here that
   floor is the wordmark (292.6px) plus the padding, so .wrap would not shrink
   below ~337px and stuck out of any viewport narrower than that. Because body
   is cream, the overflow strip painted cream down the right of the olive hero,
   which is how CJ spotted it.

   The automatic minimum is clamped by a definite max main size, so declaring
   max-width:100% is what actually lets the item shrink; the width then carries
   the 1120px cap that max-width used to. For the non-flex .wrap elements (the
   beats band, the footer) the two forms are equivalent. */
.wrap{width:var(--maxw);max-width:100%;margin:0 auto;padding:0 32px;}

/* Visually hidden but present for assistive tech and crawlers. */
.vh{
  position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);
  white-space:nowrap;border:0;
}

/* ---- ticket container: rounded rect with side notches ---- */
.ticket{
  position:relative;--notch:13px;border-radius:12px;
  -webkit-mask:
    radial-gradient(circle at left center,transparent 0 var(--notch),#000 var(--notch)) left/50.5% 100% no-repeat,
    radial-gradient(circle at right center,transparent 0 var(--notch),#000 var(--notch)) right/50.5% 100% no-repeat;
          mask:
    radial-gradient(circle at left center,transparent 0 var(--notch),#000 var(--notch)) left/50.5% 100% no-repeat,
    radial-gradient(circle at right center,transparent 0 var(--notch),#000 var(--notch)) right/50.5% 100% no-repeat;
}

/* ---- film grain + vignette, applied to photographic bands ---- */
.grain,.filmic{position:relative;}
.grain::after{
  content:"";position:absolute;inset:0;pointer-events:none;z-index:2;
  opacity:.4;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
}
.filmic::before{
  content:"";position:absolute;inset:0;pointer-events:none;z-index:1;
  background:radial-gradient(120% 120% at 50% 30%,rgba(0,0,0,0) 52%,rgba(40,30,15,.42) 100%);
}

/* Smooth scrolling exists only for the hero's scroll cue. Guarded by
   no-preference rather than switched off under reduce, so a motion-sensitive
   visitor gets an instant jump instead of a long glide. */
@media (prefers-reduced-motion:no-preference){
  html{scroll-behavior:smooth;}
}

/* ---- hero ---- */
.hero{
  position:relative;              /* anchor for .scroll-cue */
  /* Full viewport. Briefly tried clamp(360px,66svh,700px) on 2026-09-01 so the
     statement section below would break the fold and act as its own
     affordance; reverted 2026-09-02, and the question went away entirely later
     that day when the statement was folded INTO the hero. The hero now carries
     the whole opening — tagline, wordmark, statement — and the scroll cue is
     the only affordance, which is why its contrast floor matters. */
  min-height:100vh;
  min-height:100svh;
  background:var(--olive);
  display:flex;align-items:center;justify-content:center;
  text-align:center;
  /* Asymmetric on purpose: .scroll-cue is absolutely positioned against this
     element's bottom, so the extra bottom padding is what keeps the lead
     paragraph from running into it on a short viewport. */
  padding:64px 0 clamp(96px,14vh,132px);
}

/* Scroll cue. A plain in-page anchor, so tapping it scrolls without a line of
   JavaScript. The padding is there to make a ~56x48 touch target out of a
   small mark; the chevrons themselves stay light. */
.scroll-cue{
  position:absolute;left:50%;bottom:clamp(18px,4vh,40px);
  transform:translateX(-50%);
  display:flex;flex-direction:column;align-items:center;
  padding:12px 16px;
  color:var(--cream);
  text-decoration:none;
  border-radius:8px;
}
.scroll-cue:hover .chev{opacity:1;}
.scroll-cue:focus-visible{outline:2px solid var(--butter);outline-offset:2px;}
.chev{
  display:block;width:26px;height:15px;
  fill:none;stroke:currentColor;stroke-width:2.2;
  stroke-linecap:round;stroke-linejoin:round;
  opacity:.7;
  animation:cue 2.8s ease-in-out infinite;
}
/* The second chevron sits under the first and trails it slightly, which is
   what makes the pair read as a downward beckon rather than a blink. */
.chev + .chev{margin-top:-6px;animation-delay:.22s;}
/* The opacity floor is .58, not lower, because this is a link and WCAG 1.4.11
   wants 3:1 for a non-text control — measured against olive, cream composites
   to 3.12:1 at .55 and only 2.45:1 at .42. The dimmest frame of the animation
   is the one that has to pass, so the bob is shallow by necessity. */
@keyframes cue{
  0%,100% {transform:translateY(-2px);opacity:.58}
  50%     {transform:translateY(3px); opacity:.92}
}
/* The tagline sits between the wordmark and the statement paragraph, so its
   margin runs UP to the wordmark; the gap below it is owned by .hero .lead's
   own margin-top. (It led the wordmark briefly on 2026-09-02 and was moved
   back the same day.) */
.hero .tagline{
  font-family:'Stoury Serif',serif;
  font-size:clamp(26px,3.6vw,38px);
  color:#f4ead2;
  margin-top:clamp(8px,1.5vw,20px);
}

/* The statement paragraph, folded into the hero on 2026-09-02 (it had its own
   cream section before). Two changes came with the move:

   1. It is CREAM ON OLIVE now, not ink on cream — 5.93:1, AA for body text
      (spec 6.2). Do not reach for --sage or a dimmed cream here; the ratio has
      no room in it.
   2. It is WIDER and SMALLER, because a full-viewport hero now has to hold a
      tagline, a 280px wordmark and this without overflowing. At 1120px the old
      700px/29px measure ran ~24 characters a line; 940px/22px runs ~43, which
      is close to half the lines for the same words. That is wide for a measure
      — deliberately so, and the reason it stays centred and short. */
.hero .lead{
  max-width:940px;margin:clamp(20px,3vw,34px) auto 0;
  font-size:clamp(18px,1.9vw,28px);line-height:1.5;
  color:var(--cream);
}

/* ---- wordmark ---- */
.wm{
  font-family:'Stoury Display',system-ui,sans-serif;
  font-weight:800;                 /* maps onto the single Regular face */
  display:inline-flex;align-items:baseline;justify-content:center;
  line-height:1;letter-spacing:.01em;
  color:var(--butter);
  font-size:clamp(72px,18vw,280px);
}
.wm-sm{font-size:26px;}

/* The O of STOURY, as a photographic aperture. */
/* Size and position are taken from SP Blunt's OWN O, measured out of the font
   rather than eyeballed (2026-09-02, after CJ spotted the aperture reading
   small against the other caps):

     cap height of S/T/U/R/Y   0.710em
     the real O, ink height    0.720em  — 0.710 above the baseline, 0.010 BELOW
     the real O, ink width     0.678em
     the real O, advance       0.752em

   The O is TALLER than cap height because round letters are drawn with
   overshoot: a circle that stops exactly at the cap line reads as undersized
   next to flat-topped letters. The aperture was .7em, which was 1.4% under cap
   height and therefore about 4% under where a round letter needs to sit — small
   enough to look like a mistake and not enough to look deliberate. It is now
   .72em, dropped .01em below the baseline, which is exactly the vertical extent
   the type designer drew.

   It stays a TRUE CIRCLE rather than matching the real O's narrower .678em
   width: the circle is the brand device — it is the favicon and the profile
   mark — so it should be round, not an oval that happens to match one glyph.

   The margin absorbs the extra .02em so the ADVANCE is unchanged at .63em.
   That is not cosmetic: spec 5.1 records the wordmark measuring 292.6px against
   293px of available width at 390px, so a wider word overflows the phone. */
.owin{
  position:relative;display:inline-block;overflow:hidden;
  width:.72em;height:.72em;margin:0 -.045em;top:.01em;
  border-radius:50%;
  /* Coral ground only. There is deliberately NO base photograph here any more.
     It used to carry o-01 so the aperture was never blank at t=0, and that was
     the source of a real artifact: during every crossfade the outgoing layer at
     50% over the incoming at 50% composites to 75% opaque, not 100%, so a
     quarter of whatever sat beneath showed through — the first image ghosting
     faintly through every single transition. The keyframe below now holds the
     outgoing layer opaque until the incoming has fully arrived, so the stack is
     never partly transparent and the aperture is filled from t=0 without a base
     to bleed. Do not reintroduce one. */
  background:var(--coral) center/cover no-repeat;
}
/* The footer's O is a SOLID coral disc, not a photograph, and it needs more air
   than the hero's for a reason that is optical rather than geometric: the two
   are identical in em terms — 0.7em circle, -0.035em overlap, 0.01em tracking,
   measured — but a solid saturated disc reads heavier and larger than a
   textured one at the same diameter (irradiation), so the same geometry looks
   crowded. Swinging the margin from -0.035em to +0.015em gives it back.
   Judged against 0 and 0.03em: 0 is still snug, 0.03em floats the O loose. */
.owin-static{
  background-image:none;background-color:var(--coral);
  margin:0 .015em;
}
/* The opening beat: the O arrives CORAL and dissolves into the photograph.
   This began as a bug — .owin has a coral ground, the first layer snaps to
   opaque at t=0, and until o-01.jpg decodes you see coral through an opaque
   but empty layer. The duration of that flash was whatever the network
   decided, which is exactly why it read as a glitch. Making it deliberate
   turns a nondeterministic artifact into a fixed beat, and it happens to be
   the brand's own move: the coral disc IS the favicon and the O-window mark,
   so the page opens on the mark and the mark then opens its eye.

   The fade is on the STACK, not on a layer, so it composites as a group over
   the coral ground and does not disturb the per-layer crossfade underneath.
   One iteration with `both`, so it holds opaque forever after and the coral
   can never reappear — the layers' own composite is already always 1.0.
   Timing: 1.5s HOLD, then a 1.2s dissolve. It was briefly .1s/1s, which read
   as a flicker rather than a beat — too close to the loading artifact it was
   meant to replace. The hold is the point: the coral O has to be seen as a
   STATE, the brand's own mark, before it becomes a window. ease-in-out rather
   than ease-out because this is now a transition between two held states, not
   something getting out of the way.

   The rotation underneath runs from t=0 regardless, so the first frame is
   already ~28% into its pan when it is revealed. That is deliberate — it
   arrives moving rather than sitting still and then starting. */
.owin-stack{
  position:absolute;inset:0;
  animation:owin-intro 1.2s ease-in-out 1.5s 1 both;
}
@keyframes owin-intro{
  from{opacity:0}
  to  {opacity:1}
}

/* Each frame is cropped for its OWN motion rather than to one shared ratio,
   and the layer is sized to match that crop so the pan travels through real
   picture instead of zooming into a small one:

     .ol.h  wide image  -> layer is --span WIDE, aperture tall,  pans on X
     .ol.v  tall image  -> layer is aperture wide, --span TALL,  pans on Y
     .ol.z  square      -> layer fills the aperture, slow zoom, no travel

   --span is the image's long edge as a percentage of its short edge, taken
   from the file's actual dimensions. --from and --to are the pan endpoints as
   a percentage of the LAYER's own size, so the safe limit is
   -((span - 100) / span) x 100%; going past it walks an edge into the circle.
   docs/build-imagery.sh prints the dimensions these are derived from. */
.ol{
  position:absolute;
  background-position:center;background-size:cover;background-repeat:no-repeat;
  opacity:0;
  /* --o0 / --oEnd parameterise the ONE shared keyframe so the first and last
     layers can behave differently without needing their own keyframe names —
     which matters because .ol.h/.v/.z already override animation-name, and a
     per-position keyframe would need every combination of the two. See the
     keyframe for what they do. */
  --o0:0;
  --oEnd:1;
  animation:owin-cycle var(--cycle) linear infinite both,
            owin-pan-x var(--cycle) linear infinite both;
}
/* The stack paints in DOM order, so each layer arrives ON TOP of the one it
   replaces — except the wrap from last back to first, where the incoming layer
   is at the BOTTOM. Those two ends are therefore the exceptions:
     first: snaps straight to opacity 1 instead of fading in. It is underneath
            the outgoing last layer at that moment, so the snap cannot be seen,
            and it means the last layer has something fully opaque to dissolve
            away over.
     last:  fades out over the handover instead of snapping off, because it is
            the one layer that reveals rather than being covered.
   Between them, every instant has at least one layer at opacity 1. */
.ol:first-child{--o0:1;}
.ol:last-child {--oEnd:0;}
.ol.h{top:0;bottom:0;left:0;height:100%;width:var(--span);}
.ol.v{left:0;right:0;top:0;width:100%;height:var(--span);
      animation-name:owin-cycle,owin-pan-y;}
.ol.z{inset:0;animation-name:owin-cycle,owin-zoom;}

/* A single animation-delay value applies to both animations in the list, so
   the pan stays locked to the frame's own visible window.

   Each endpoint is chosen by what is in the picture. CJ fixed the relative
   order of six frames — Hotel Empire, Chicago, Raoul's, Katz's, sandwich,
   Rocky — and that order is preserved exactly; the other three are interleaved
   between them to break up runs of similar frames. The rotation ENDS on Rocky:
   the traverse is its payoff, so it is held back rather than opening the set.
     o-01  Lower Manhattan, crosses and lands with One World Trade centred
     o-02  pans DOWN the blade sign, so the picture rides upward
     o-03  descends from the brickwork through the awning to the neon and car
     o-04  crosses to the CROWN & ANCHOR lettering and hanging sign
     o-05  runs right-to-left to finish on SEND A SALAMI
     o-06  a close-up with nowhere to travel, so it zooms instead
     o-07  Times Square, crosses left->right over the marquees and traffic
     o-08  descends to settle with the neon band centred
     o-09  crosses the storefront to the Union Jack window
     o-10  traverses the full width and lands with the statue centred */
.ol:nth-child(1) {background-image:url('img/o-01.jpg');animation-delay:calc(var(--step) * 0);--span:150%;   --from:0%;  --to:-22%;}
.ol:nth-child(2) {background-image:url('img/o-02.jpg');animation-delay:calc(var(--step) * 1);--span:125%;   --from:0%;  --to:-19%;}
.ol:nth-child(3) {background-image:url('img/o-03.jpg');animation-delay:calc(var(--step) * 2);--span:150.75%;--from:0%;  --to:-31%;}
.ol:nth-child(4) {background-image:url('img/o-04.jpg');animation-delay:calc(var(--step) * 3);--span:133.375%;--from:0%; --to:-20%;}
.ol:nth-child(5) {background-image:url('img/o-05.jpg');animation-delay:calc(var(--step) * 4);--span:150%;   --from:-28%;--to:-4%;}
.ol:nth-child(6) {background-image:url('img/o-06.jpg');animation-delay:calc(var(--step) * 5);--z0:1;--z1:1.12;}
.ol:nth-child(7) {background-image:url('img/o-07.jpg');animation-delay:calc(var(--step) * 6);--span:150%;   --from:0%;  --to:-31%;}
.ol:nth-child(8) {background-image:url('img/o-08.jpg');animation-delay:calc(var(--step) * 7);--span:150%;   --from:0%;  --to:-18%;}
.ol:nth-child(9) {background-image:url('img/o-09.jpg');animation-delay:calc(var(--step) * 8);--span:150%;   --from:0%;  --to:-19%;}
.ol:nth-child(10){background-image:url('img/o-10.jpg');animation-delay:calc(var(--step) * 9);--span:177.75%;--from:0%;  --to:-36.9%;}

/* Authored for N=10 / step 7.5s / cycle 75s. Recompute these percentages from
   --cycle if either changes; see the note beside --cycle above.

   THIS IS NOT A SYMMETRIC CROSSFADE, and that is the point. The obvious version
   fades the outgoing layer out while the incoming fades in — but two layers at
   50% composite to 75% opaque, not 100%, so for two seconds a quarter of
   whatever sits beneath the stack shows through. With a base image on .owin
   that meant the first frame ghosted through every transition; with no base it
   would mean the coral ground does.

   So the outgoing layer HOLDS at opacity 1 through the whole handover and only
   drops once the incoming layer has fully arrived on top of it — at which point
   it is completely covered and the drop cannot be seen. At every instant of the
   cycle at least one layer is fully opaque.

     0        -> 2.6667%   incoming fades in (over the outgoing, still at 1)
     2.6667%  -> 10%       sole visible layer
     10%      -> 12.6667%  handover: successor fades in on top; this one holds 1
     12.6667%              drops to 0, invisibly, beneath the opaque successor
     12.68%   -> 100%      off

   --o0 and --oEnd bend this for the two ends of the stack (see .ol above):
   the first layer starts AT 1 rather than fading up, and the last layer
   animates down to 0 across the handover rather than holding and snapping. */
@keyframes owin-cycle{
  0%       {opacity:var(--o0)}
  2.6667%  {opacity:1}
  10%      {opacity:1}
  12.6667% {opacity:var(--oEnd)}
  12.68%   {opacity:0}
  100%     {opacity:0}
}

/* The pan is LINEAR across the frame's WHOLE VISIBLE LIFE, 0 -> 12.6667% —
   the same window over which the layer is on screen at all. Constant velocity
   from the instant it appears to the instant it is covered.

   That is the third answer to this, and the first two are worth recording
   because each fixed one end and broke the other (all 2026-09-02):

     ease-in-out to 12.6667%  Stationary WHILE FADING IN. The fade-in is the
                              first fifth of the pan and that is the slow ramp,
                              so about a tenth of the travel happened while the
                              frame was appearing.
     linear to 10%            Fixed the entrance, broke the exit: the pan
                              reached its endpoint at 7.5s and then sat DEAD
                              STILL for the 2s handover. Worse than the original
                              — a full stop is more noticeable than a slow start.
     ease-out to 12.6667%     Better on paper (32% of travel by fade-in, 94% by
                              handover) but the deceleration is asymptotic, so
                              the last two seconds covered 6% of the travel —
                              about 1.5px/sec on o-02. Still reads as stopped.

   The lesson is that this is a VELOCITY problem, not a distance one. Motion is
   perceived as speed, so any curve that is slow at an end looks stopped at that
   end no matter how much ground it covers overall. Only a constant velocity
   looks continuous at both ends, which is what linear across the full window
   gives: the pan moves at exactly the same rate while fading in, while held,
   and while being covered.

   The cost, accepted deliberately: endpoints here are chosen by what is in the
   picture (the statue centred, One World Trade centred), and under constant
   velocity the frame is only ~79% of the way there when the handover starts and
   ~89% when the incoming frame is half opaque. The intended composition is
   therefore approached rather than parked on. That is the trade for never
   appearing to stop, and it reads better in motion than the alternative.

   Do NOT try to buy the landing back by extending the endpoints: several frames
   are already near the safe limit of -((span - 100) / span) x 100%, and o-02
   sits at -19% against a -20% limit, so there is no headroom to travel further.

   The reset to the start value happens at the 100%/0% boundary. For every
   layer but the first that is while opacity is 0; for the first layer, whose
   opacity snaps to 1 at 0%, it happens underneath the outgoing last layer,
   which is still fully opaque. Either way it is never seen. The endpoints come
   from the element's own custom properties, so one keyframe pair per axis
   serves every frame while each frame still moves its own way. */
@keyframes owin-pan-x{
  0%       {transform:translateX(var(--from))}
  12.6667% {transform:translateX(var(--to))}
  100%     {transform:translateX(var(--to))}
}
@keyframes owin-pan-y{
  0%       {transform:translateY(var(--from))}
  12.6667% {transform:translateY(var(--to))}
  100%     {transform:translateY(var(--to))}
}
@keyframes owin-zoom{
  0%       {transform:scale(var(--z0))}
  12.6667% {transform:scale(var(--z1))}
  100%     {transform:scale(var(--z1))}
}

/* `animation:none` cancels the pan as well as the crossfade, which matters
   more now than it did: an opacity fade is generally safe for vestibular
   sensitivity, but a sustained pan is real motion and is exactly what this
   query exists to suppress.
   Without an animation a layer would rest at its untransformed origin, which
   for o-01 is the far LEFT of the Lower Manhattan frame. So
   pin each variant to the endpoint its pan was travelling towards: the still
   becomes the frame the motion was going to land on. */
@media (prefers-reduced-motion: reduce){
  /* The cancel has to repeat the .h/.v/.z selectors, and that is not
     redundancy. `.ol.v` and `.ol.z` set animation-name above at specificity
     (0,2,0), which OUTRANKS a bare `.ol{animation:none}` at (0,1,0) — so five
     of the nine layers keep their animation-name here. They currently sit
     still only because the `animation` shorthand also resets
     animation-duration to its initial 0s, and a 0s animation with fill-mode
     none has no effect. That is a side-effect, not a guarantee: rewrite this
     as `animation-name:none` (which reads like a tidier equivalent) and the
     duration survives, and the pan restarts for exactly the visitors this
     query protects. Matching the specificity makes the cancel mean what it
     says. */
  .ol,.ol.h,.ol.v,.ol.z{animation:none;}
  /* No opening dissolve either — the still should simply be present. */
  .owin-stack{animation:none;opacity:1;}
  .ol{opacity:0;}
  .ol:nth-child(1){opacity:1;}
  .ol.h{transform:translateX(var(--to));}
  .ol.v{transform:translateY(var(--to));}
  .ol.z{transform:scale(var(--z1));}
  /* The cue stays visible and still works — only its bob is suppressed. */
  .chev{animation:none;opacity:.7;}
}

/* ---- four-beat band ---- */
.beats{background:var(--coral);padding:clamp(56px,7vw,88px) 0;}
.beat-row{
  list-style:none;display:grid;grid-template-columns:repeat(4,1fr);gap:16px;
}
.beat{
  background:var(--butter);color:var(--olive);
  font-family:'Stoury Display',sans-serif;font-weight:700;
  /* The cells sit on butter, not directly on coral: olive on coral is
     3.42:1 and large-text-only, while olive on butter is 4.88:1 and
     passes for body text at any size. Display size here is a design
     choice — the beats are meant to read as display type — not a
     contrast requirement. */
  font-size:clamp(24px,2.9vw,38px);
  text-align:center;padding:26px 12px;
}

/* ---- the tour clip ---- */
/* Cream ground between the coral beats and the dark photo bands. The clip
   plays inside a MacBook Pro shell — a PNG with a transparent screen
   cutout — so it reads as the app running on a laptop, as on
   secretweapon.tech/productionhero. The shell paints ABOVE the screen box
   (z 2 over z 1): the clip shows through the cutout and the bezel hides its
   edges, so nothing has to be registered to the pixel. The screen box is
   placed by percentages measured off the image, so it stays put at every
   width. 940px is the hero paragraph's measure, reused so the two widest
   things on the page agree. */
.tour{background:var(--cream);padding:clamp(48px,6vw,80px) 0 clamp(40px,5vw,64px);}
.tour-fig{margin:0 auto;max-width:940px;}
.tour-laptop{
  /* 600px, centred — CJ's call (2026-09-18). max-width:100% is what lets it
     give way on a phone, where 600px would overflow a 390px screen. */
  width:600px;margin:0 auto;
  position:relative;max-width:100%;
  /* The shadow goes on the container, not the shell: drop-shadow follows
     the alpha, and on the shell alone it would fall through the cutout and
     lie across the picture. On the container the silhouette is shell plus
     screen, and the shadow falls only on the cream. */
  filter:drop-shadow(0 26px 30px rgba(58,53,41,.28));
}
.tour-shell{display:block;width:100%;height:auto;position:relative;z-index:2;pointer-events:none;}
.tour-screen{
  position:absolute;z-index:1;overflow:hidden;background:#fff;
  left:7.2993%;top:1.2162%;width:87.3827%;height:76.0811%;
}
.tour-clip,.tour-still{display:block;width:100%;height:100%;object-fit:cover;}
/* The still is the reduced-motion and print fallback; the video is the
   default. Both are in the DOM so the swap is pure CSS. */
.tour-still{display:none;}
.tour-cap{
  font-family:'Stoury Serif',serif;color:var(--ink);
  font-size:clamp(15px,1.35vw,18px);line-height:1.5;text-align:center;
  margin:clamp(10px,1.6vw,18px) auto 0;max-width:620px;
}
/* This cancel lives next to the thing it cancels (compare the reel, 4.5.1):
   nothing else in the file targets .tour-clip, so specificity is not in
   play, but the rule is easier to find here than in the page's main
   reduced-motion block. A looping screen recording is sustained motion,
   which is exactly what the query exists to suppress. */
@media (prefers-reduced-motion: reduce){
  .tour-clip{display:none;}
  .tour-still{display:block;}
}

/* ---- system bands ---- */
/* Three overlays are needed here — scrim, vignette, grain — but an
   element has only two pseudo-elements. The scrim therefore rides as
   an extra background layer on .band itself, leaving ::before for the
   vignette (.filmic) and ::after for the grain (.grain). */
.band{
  position:relative;min-height:clamp(320px,44vw,520px);
  /* The plate sits near the TOP of the band, not centred: centred, it landed
     across the skyline on films/shows and across the storefront on food. High
     puts it on quieter sky and keeps the subject of each photograph clear. */
  display:flex;align-items:flex-start;justify-content:center;

  /* Two opacities, not one. --edge-a is the film base along the top and
     bottom, near solid so the perforations read; --mid-a is the frame between
     them, kept light so the photograph shows through as the frame's content.
     --mid-a is what the type sits on, so it is the one that has to clear the
     contrast floor, and it is per band. */
  --edge-a:.92;
  --mid-a:.45;

  /* The photographs are shown RAW — no scrim, no vignette, no grain. That is
     an explicit design decision (2026-09-01): every overlay tried here, flat
     or localised, read as haze over the picture, and the picture is the point.
     The `grain` and `filmic` classes were removed from the band markup at the
     same time; their rules are kept above because they remain part of the
     design system, but nothing uses them now.

     Separation for the type is bought locally instead, by the film strip
     below, which covers only its own footprint and leaves the rest of the
     photograph alone. An earlier build leaned on a crisp text-shadow for this;
     a shadow contributes nothing to a WCAG ratio and measured 1.03-1.07:1.
     docs/verify-band-contrast.py measures the strip so the number stays
     visible rather than forgotten. */
  background-image:var(--img);
  background-size:cover;
  background-position:center;
  background-repeat:no-repeat;
}

/* Band imagery is declared here, not inline in the HTML: a url() inside a
   custom property resolves against the stylesheet that consumes the var(),
   so keeping the declaration and the consumption in the same file means the
   path reads correctly and cannot drift. */
/* band-films is Lower Manhattan at sunset (2026-09-02, replacing Monument
   Valley). Its top strip is the deepest blue in the frame, which is where the
   film strip lands, so it needs no more scrim than band-shows. */
.band-films{--img:url('img/band-films.jpg');}
.band-shows{--img:url('img/band-shows.jpg');}
/* band-food is the one band that reverses the problem. The other two are dark
   where the strip sits, so white type wins easily at .45. This one was chosen
   for a LIGHT top (so the punched sprocket holes have something bright to show
   through and the strip reads as film), and a light backing is the worst case
   for white type: at .45 it measures 3.79:1, which clears the 3:1 floor but
   sits well under its neighbours' 5.84 and 6.73. .55 brings it to 5.29:1, in
   line with the other two, and the frame is still translucent enough to read
   the board through. Measured, not chosen — re-run the verifier if the image
   changes. */
.band-food {--img:url('img/band-food.jpg');--mid-a:.55;}
/* The line sits on a FILM STRIP that runs the full width of the band: dark and
   translucent, with sprocket holes punched through the top and bottom edges so
   the photograph shows through them. It replaced a ticket-shaped plate, which
   was the wrong object at this size — a ticket 1440px wide is not a ticket.

   The holes are punched with a MASK rather than drawn, because they have to be
   genuinely transparent: the photograph shows through each one, which is what
   makes it read as film rather than as a dark bar with dots on it. See the
   mask construction on ::before below. */
.band .band-line{
  position:relative;z-index:3;
  /* Perforations are rectangles, TALLER than wide, with a clear gap between
     each -- 35mm stock, not a dotted border. */
  --pitch:20px;               /* hole + gap */
  --hw:8px;                   /* hole width */
  --e1:5px;                   /* strip edge -> near edge of hole */
  --e2:17px;                  /* strip edge -> far edge (so 12px tall) */
  --band:22px;                /* depth of the opaque edge band */
  width:100%;
  font-family:'Stoury Display',system-ui,sans-serif;
  font-weight:700;                  /* maps onto the single Regular face */
  color:#fff;
  text-align:center;
  /* The 19px floor is not arbitrary: WCAG treats >=18.66px bold as "large
     scale", where AA is 3:1 rather than 4.5:1. At 17px the 390px breakpoint
     fell under the stricter floor and band-shows missed it by 0.07. Raising
     the type is the honest way to clear it -- larger text really is easier to
     read -- and it reads better on a phone regardless. */
  font-size:clamp(19px,2.6vw,30px);
  line-height:1.3;
  /* Vertical padding clears the sprocket rows so the type never collides with
     a perforation. */
  padding:30px 24px;
  text-shadow:none;
}
.band .band-line::before{
  content:"";position:absolute;inset:0;z-index:-1;
  /* Film has a near-opaque base along its edges, carrying the perforations,
     and a frame between them that IS the picture. So the strip is not one flat
     tint: the edge bands are almost solid, and the middle is light enough that
     the photograph reads through it as the frame content.
     Hard stops, not a fade -- on real stock the edge and the frame are
     distinct, not blended. */
  background:linear-gradient(to bottom,
    rgba(0,0,0,var(--edge-a)) 0 var(--band),
    rgba(0,0,0,var(--mid-a))  var(--band) calc(100% - var(--band)),
    rgba(0,0,0,var(--edge-a)) calc(100% - var(--band)) 100%);
  /* Square holes out of two gradient layers, using the DEFAULT `add`
     compositing -- no mask-composite needed. The mask is the union of the two
     alphas, so a pixel is only punched where BOTH layers are transparent:
     layer one is transparent in vertical stripes at each hole's x range, layer
     two in horizontal bands at the top and bottom hole rows. Their
     intersection is a grid of rectangles. */
  -webkit-mask:
    repeating-linear-gradient(to right,transparent 0 var(--hw),#000 var(--hw) var(--pitch)),
    linear-gradient(to bottom,
      #000 0 var(--e1),
      transparent var(--e1) var(--e2),
      #000 var(--e2) calc(100% - var(--e2)),
      transparent calc(100% - var(--e2)) calc(100% - var(--e1)),
      #000 calc(100% - var(--e1)));
  mask:
    repeating-linear-gradient(to right,transparent 0 var(--hw),#000 var(--hw) var(--pitch)),
    linear-gradient(to bottom,
      #000 0 var(--e1),
      transparent var(--e1) var(--e2),
      #000 var(--e2) calc(100% - var(--e2)),
      transparent calc(100% - var(--e2)) calc(100% - var(--e1)),
      #000 calc(100% - var(--e1)));
}




/* ---- tail leader: the rotating tagline strip ---- */
/* A fourth film strip, sitting between the last band and the footer, carrying
   NO photograph. That is not a compromise — blank stock at the end of a reel is
   leader, so an imageless strip is the right object for the end of the page.
   The perforations punch through to the olive ground rather than to a picture.

   White on the strip over olive measures 13.11:1, far above any floor, because
   there is no photograph to fight — which is why this one is not in
   docs/verify-band-contrast.py: with a known flat ground behind it the ratio is
   arithmetic, not a measurement. */
.reel{background:var(--olive);}

/* Same strip object as .band .band-line: identical perforation geometry, so the
   four strips on the page read as one piece of film rather than as variations.
   The values are repeated rather than shared because .band .band-line is scoped
   to .band; if these ever need to move in step, hoist them to :root. */
.reel .reel-strip{
  /* z-index is REQUIRED, not cosmetic. ::before below is z-index:-1 so it
     paints behind the type; without a z-index here this element creates no
     stacking context, that -1 escapes to the root, and the strip is painted
     behind .reel's olive background — i.e. invisible, leaving white text on
     bare olive. .band .band-line gets away with the same ::before because it
     sets z-index:3. Any value but `auto` fixes it on a positioned element. */
  position:relative;z-index:0;
  --pitch:20px;  --hw:8px;  --e1:5px;  --e2:17px;  --band:22px;
  --mid-a:.45;   --edge-a:.92;
  width:100%;
  font-family:'Stoury Display',system-ui,sans-serif;
  font-weight:700;                  /* maps onto the single Regular face */
  color:#fff;
  text-align:center;
  font-size:clamp(19px,2.6vw,30px);
  line-height:1.3;
  padding:30px 24px;
  margin:0;
}
.reel .reel-strip::before{
  content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(to bottom,
    rgba(0,0,0,var(--edge-a)) 0 var(--band),
    rgba(0,0,0,var(--mid-a))  var(--band) calc(100% - var(--band)),
    rgba(0,0,0,var(--edge-a)) calc(100% - var(--band)) 100%);
  -webkit-mask:
    repeating-linear-gradient(to right,transparent 0 var(--hw),#000 var(--hw) var(--pitch)),
    linear-gradient(to bottom,
      #000 0 var(--e1),
      transparent var(--e1) var(--e2),
      #000 var(--e2) calc(100% - var(--e2)),
      transparent calc(100% - var(--e2)) calc(100% - var(--e1)),
      #000 calc(100% - var(--e1)));
  mask:
    repeating-linear-gradient(to right,transparent 0 var(--hw),#000 var(--hw) var(--pitch)),
    linear-gradient(to bottom,
      #000 0 var(--e1),
      transparent var(--e1) var(--e2),
      #000 var(--e2) calc(100% - var(--e2)),
      transparent calc(100% - var(--e2)) calc(100% - var(--e1)),
      #000 calc(100% - var(--e1)));
}

.reel .reel-stack{position:relative;display:block;}
/* Hidden but laid out — visibility, not display, so it still occupies space. */
.reel .reel-sizer{display:block;visibility:hidden;}
/* overflow:hidden is the whole mechanism. Each panel simply slides through this
   box; anything above or below is clipped, so no opacity fading is needed to
   hide a panel and there is no half-drawn state to go wrong. The clip line sits
   inside the strip's 30px padding, so a line disappears well before it reaches
   the perforation rows. */
.reel .reel-turns{position:absolute;inset:0;overflow:hidden;}

/* .rlw exists so the two sentences lay out as ORDINARY INLINE TEXT. The panel
   above it is a flex container (that is what centres a shorter line vertically
   against the sizer's height), and flex would break the copy two ways: it does
   not wrap by default, so the halves would overflow the viewport instead of
   breaking; and it discards the whitespace-only text node between them, so on
   one row the two sentences would run together with no space. One block child
   sidesteps both — the flex box only ever has a single item, and everything
   inside .rlw is plain inline flow. It also guarantees the sizer and the panels
   wrap identically, since both are now the same layout. */
.reel .rlw{display:block;width:100%;text-align:center;}

/* Each sentence is its own nowrap unit, so the only break the line can take is
   the space BETWEEN the two sentences: one row on a wide screen, and exactly
   two rows — one sentence each — when it cannot fit. An orphaned word is not
   reachable, because neither half is allowed to break inside itself. */
.reel .rl{white-space:nowrap;}

/* The panels ROLL: each slides up into place from below, holds, then slides up
   and out of the top as the next rises behind it — a departure board.

   This is NOT a crossfade, and the difference is load-bearing. Every line
   repeats "Your favorite", so two layers dissolving into each other would show
   two 50%-opacity copies of the same glyphs — which composite to 75%, not
   100% — and the shared words would visibly dip grey on every transition. A
   roll has no overlap in opacity at all: the two moving panels are simply at
   different heights, and the clip does the rest.

   (A 3D prism turn was built first and rejected on the look. It solved the same
   problem, but the foreshortening read as a gimmick where a roll reads as
   mechanism. The roll is also the simpler object: no perspective, no backface,
   no opacity ramps to hide the edge-on hairline.) */
.reel .reel-line{
  position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
  animation:reel-roll var(--reel-cycle) linear infinite both;
}

/* N = 7 panels. step = 0.5s slide + 3.1s hold = 3.6s, so the round is 25.2s.
   Offsets are derived from N, not chosen — change the count and every
   percentage below moves:
     slide-in ends   0.5s / 25.2s =  1.9841%
     hold ends       3.6s / 25.2s = 14.2857%
     slide-out ends  4.1s / 25.2s = 16.2698%

   The 0.5s overlap is the roll itself: the outgoing panel is travelling from 0
   to -100% over exactly the window in which the incoming one travels from 100%
   to 0, so they move together as one belt rather than trading places. Unlike a
   crossfade or a turn, an overlap here is not a hazard to be minimised — it IS
   the effect, and the two panels are never at the same height. */
.reel{--reel-step:3.6s; --reel-cycle:25.2s;}
/* Delays are positive multiples of the step, matching the O-window: every panel
   starts from a clean 0% keyframe and reaches its offset naturally, rather than
   starting mid-cycle on a negative delay. */
.reel .reel-line:nth-child(1){animation-delay:calc(var(--reel-step) * 0);}
.reel .reel-line:nth-child(2){animation-delay:calc(var(--reel-step) * 1);}
.reel .reel-line:nth-child(3){animation-delay:calc(var(--reel-step) * 2);}
.reel .reel-line:nth-child(4){animation-delay:calc(var(--reel-step) * 3);}
.reel .reel-line:nth-child(5){animation-delay:calc(var(--reel-step) * 4);}
.reel .reel-line:nth-child(6){animation-delay:calc(var(--reel-step) * 5);}
.reel .reel-line:nth-child(7){animation-delay:calc(var(--reel-step) * 6);}

/* The opacity steps are not decoration: a plane at exactly +/-90deg projects to
   a hairline rather than to nothing, and that hairline is visible as a bright
   seam on a dark strip. Fading over the first and last fraction of the turn
   hides it, while the panel is still nearly edge-on and unreadable anyway.
   The 90deg -> -90deg jump at the cycle boundary happens while opacity is 0. */
/* No opacity anywhere: a panel is hidden because it is outside the clip, not
   because it has been faded. The -100% -> 100% jump at the cycle boundary
   happens while the panel is above the box and unseen. The eased segments are
   declared per-keyframe, so only the two slides ease and the hold stays still. */
@keyframes reel-roll{
  0%       {transform:translateY(100%);  animation-timing-function:cubic-bezier(.4,0,.2,1);}
  1.9841%  {transform:translateY(0);}
  14.2857% {transform:translateY(0);     animation-timing-function:cubic-bezier(.4,0,.2,1);}
  16.2698% {transform:translateY(-100%);}
  100%     {transform:translateY(-100%);}
}

/* This cancel lives HERE, immediately after the rule it cancels, and not up in
   the page's main prefers-reduced-motion block — which is the obvious place for
   it and is the wrong one. `.reel .reel-line` and the cancel would carry the
   SAME specificity (0,2,0), so source order decides, and the reel section sits
   later in this file than that block: the animation simply won and the panels
   kept turning for every visitor who asked them not to. It failed silently —
   nothing looks broken, the motion just does not stop. Keep this adjacent to
   the animation, or match specificity deliberately, but do not move it up. */
@media (prefers-reduced-motion: reduce){
  /* Hiding the other six is not optional here. With the animation cancelled
     every panel rests at its untransformed origin — translateY(0) — so all
     seven would sit stacked in the same box, overprinted into an unreadable
     smear. The O-window has the same trap for the same reason (5.1). */
  .reel .reel-line{animation:none;opacity:0;transform:translateY(0);}
  .reel .reel-line:nth-child(1){opacity:1;}
}

/* The lone footer ticket. It reuses .beat + .ticket verbatim — same butter
   ground, same olive type, same notch mask — so it is recognisably the same
   object as the four in the beats band rather than a lookalike. Only the size
   and the width are dialled back: at full beat size a single centred ticket
   would outweigh the wordmark under it, and the footer's job is to sign off,
   not to announce. Olive on butter is 4.88:1 (6.2), unchanged by the resize. */
.foot .soon{
  display:block;width:max-content;max-width:100%;
  /* Bottom margin pulled in from clamp(22px,3.5vw,34px) on 2026-09-03: it used
     to open onto the credit line and now opens onto the contact link, which
     carries 10px of its own top padding as a touch target. The ticket and the
     address are one beat — the status and the way to answer it — so this gap
     is deliberately the smaller of the two in the footer. */
  margin:0 auto clamp(14px,2.2vw,22px);
  font-size:clamp(19px,2.2vw,26px);
  padding:16px 34px;
}

/* ---- footer ---- */
/* Asymmetric padding as of 2026-09-02: the top is HALF the bottom. The footer
   opens on the "Coming soon" ticket now, and a ticket is a compact, high-
   contrast object — at the old symmetric 72px it read as marooned in olive,
   detached from the film strip above it rather than following on from it. The
   bottom keeps the full measure so the credit line still has room to sit in. */
.foot{
  background:var(--olive);
  /* The bottom is pulled in from clamp(48px,6vw,72px) to pay for .fine's
     margin-top below: the credit line drops away from the ticket without the
     footer growing. The two numbers move together — change one, change both. */
  padding:clamp(24px,3vw,36px) 0 clamp(38px,4.3vw,62px);
  text-align:center;
}
.foot .wm{color:var(--butter);margin-bottom:clamp(14px,1.8vw,20px);}
.foot .fine{
  /* 15px, up from 13px. Size and colour are the ONLY levers here: 'Stoury
     Serif' is a single Alga Light file declared font-weight:300 700, so every
     weight in that range maps onto the one Light face and asking for a heavier
     one changes nothing — while asking for 800 would fall outside the range and
     let the browser synthesize a faux bold, which is exactly what the SP Blunt
     rule exists to prevent. */
  font-size:15px;letter-spacing:.02em;
  /* Full-strength cream, up from rgba(...,.9). The old alpha composited to
     #E5DCC3 = 5.17:1; this is 5.93:1, and it is the same cream the hero tagline
     uses. The earlier comment argued .9 over .72 because .72 was 3.95:1 and
     failed AA at 13px — that reasoning stands, this just goes further in the
     same direction. It stays fine print by being small and set apart, not by
     being faint. */
  color:#f4ead2;
  /* Drops the credit line clear of the ticket. Paid for out of the footer's
     bottom padding above, so the footer's overall height is unchanged. */
  margin-top:clamp(20px,2.6vw,32px);
}
/* Inherits the colour above rather than setting its own, so the 5.19:1 the
   comment argues for still holds. The underline is what marks it as a link —
   a faint one at rest so it stays fine print, full strength on interaction. */
.foot .fine-link{
  color:inherit;
  text-decoration:underline;
  text-decoration-color:rgba(244,234,210,.32);
  text-decoration-thickness:1px;
  text-underline-offset:3px;
}
/* The credit line is two halves, and each one is nowrap, so the only place it
   can break is the space between them. Left to itself the line wrapped after
   "Inc." and left "venture." alone on the second row (CJ, 2026-09-03). Measured
   at 15px: the whole line is 327.4px and holds one row down to a 372px
   viewport; below that it becomes exactly two rows, 92.3px and 231.7px, and
   the wider half still fits down to 276px — narrower than the wordmark's own
   floor, so this never becomes the thing that overflows. */
.foot .fine-link span{white-space:nowrap;}
.foot .fine-link:hover,
.foot .fine-link:focus-visible{text-decoration-color:currentColor;}
.foot .fine-link:focus-visible{
  outline:2px solid var(--butter);outline-offset:3px;border-radius:3px;
}

/* ---- footer contact link (added 2026-09-03) ---- */
/* The page's one contact link. It follows the "Coming soon" ticket directly
   because the two are one beat — the ticket says the product is not live yet,
   and this is how to ask about it. It collects nothing: no form, no field, no
   backend, just an address.

   Set in Stoury Serif, not the display face. This is body copy rather than a
   headline, and the display face is caps-only, so the address would render
   INQUIRIES@STOURYTRAVEL.COM — shouting, and harder to read as an email.

   Cream on olive is 5.93:1, the same measure as the credit line. What separates
   the two is SIZE, not colour, and that is forced rather than chosen: sage is
   unavailable at 2.00:1, and butter would hand a contact link more voice than
   the wordmark above it. Raised to 18-22px on 2026-09-03 (from 16-19), CJ's
   call — it widens the gap to the 15px credit line, which is the whole reason
   size is carrying this. */
.foot .contact{
  /* Collapses against .soon's larger bottom margin, so this value only bites
     if that one is ever reduced below it. */
  margin-top:clamp(10px,1.4vw,14px);
  font-size:clamp(18px,1.9vw,22px);
  letter-spacing:.02em;
  color:#f4ead2;
}
/* The vertical padding is a touch target, not spacing. The line is ~31px tall
   at its 22px ceiling and WCAG 2.5.8 asks for 44px, so 10px a side clears it
   comfortably. The margins around it were reduced to pay for it, so the padding does
   not read as a gap on top of the gap. */
.foot .contact-link{
  display:inline-block;
  padding:10px 6px;
  color:inherit;
  text-decoration:underline;
  /* Heavier than the credit line's .32 alpha. That link is an attribution most
     visitors never need; this is the one thing on the page to act on, so it
     reads as a link at rest rather than only on hover. */
  text-decoration-color:rgba(244,234,210,.55);
  text-decoration-thickness:1px;
  text-underline-offset:3px;
}
.foot .contact-link:hover,
.foot .contact-link:focus-visible{text-decoration-color:currentColor;}
.foot .contact-link:focus-visible{
  outline:2px solid var(--butter);outline-offset:3px;border-radius:3px;
}

/* ---- partner login: a CSS-only dialog -----------------------------------
   No script. Two URL fragments carry the whole thing:

     #partner-login          opens the dialog
     #pl-error               opens it AND reveals the failure line

   The second is why there is an empty <span id="pl-error"> immediately BEFORE
   the overlay in the markup: an adjacent-sibling selector can then reach the
   overlay from it. That is the reason the anchor is a separate element rather
   than the error paragraph itself — a paragraph inside the panel could not be
   selected upward to open its own container without :has(), and a plain "+"
   works in every browser that will ever load this page.

   Both the anchor and the overlay are position:fixed on purpose. The browser
   scrolls a :target into view; a fixed element is already in view, so neither
   link scrolls the page sitting behind the dialog. */

/* Zero-size, fixed, purely a scroll-free landing spot for the fragment. */
.pl-anchor{position:fixed;top:0;left:0;width:0;height:0;}

/* Top-right utility link. Cream on olive is 5.93:1. The padding is a touch
   target (WCAG 2.5.8 wants 44px) and the negative margin pulls the visible
   text back to the corner so the padding does not read as a wide inset. */
.partner-login{
  position:absolute;
  top:clamp(10px,1.6vw,20px);
  right:clamp(8px,1.8vw,22px);
  z-index:4;
  font-family:'Stoury Serif',Georgia,serif;
  font-size:clamp(14px,1.3vw,16px);
  letter-spacing:.04em;
  color:#f4ead2;
  text-decoration:underline;
  text-decoration-color:rgba(244,234,210,.38);
  text-decoration-thickness:1px;
  text-underline-offset:4px;
  padding:12px 14px;
}
.partner-login:hover,
.partner-login:focus-visible{text-decoration-color:currentColor;}
.partner-login:focus-visible{
  outline:2px solid var(--butter);outline-offset:2px;border-radius:3px;
}

/* ---- the overlay ---- */
/* visibility, not display: it still transitions, and unlike opacity alone it
   genuinely removes the panel from the tab order and the accessibility tree
   while the dialog is shut. */
.pl-overlay{
  position:fixed;inset:0;z-index:50;
  display:flex;align-items:center;justify-content:center;
  padding:20px;
  visibility:hidden;opacity:0;
  transition:opacity .22s ease,visibility 0s linear .22s;
}
#partner-login:target,
#pl-error:target + .pl-overlay{
  visibility:visible;opacity:1;
  transition:opacity .22s ease,visibility 0s linear 0s;
}
/* The failure line only exists once "Sign in" has been followed. */
#pl-error:target + .pl-overlay .pl-error{display:block;}

/* Sits next to what it cancels rather than in the page's main reduced-motion
   block: same specificity there would let source order decide the winner. */
@media (prefers-reduced-motion:reduce){
  .pl-overlay,
  #partner-login:target,
  #pl-error:target + .pl-overlay{transition:none;}
}

/* Ink at .78 over the olive hero. Ink and olive are close in luminance, so a
   light wash would barely register — this needs to be most of the way to solid
   for the cream panel to sit forward of it. */
.pl-scrim{
  position:absolute;inset:0;
  background:rgba(58,53,41,.78);
  cursor:default;
}

/* ---- the panel ---- */
/* Cream ground, ink type: 10.66:1, the strongest pairing in the palette, and
   the right one for the only part of this site that asks the visitor to read
   small text and type into it. */
.pl-panel{
  position:relative;
  width:min(380px,100%);
  background:var(--cream);
  border-radius:14px;
  padding:clamp(26px,3vw,34px) clamp(22px,2.6vw,30px) clamp(22px,2.6vw,28px);
  box-shadow:0 24px 60px rgba(26,23,16,.45);
  text-align:left;
}
.pl-title{
  font-family:'Stoury Display',Georgia,serif;
  font-weight:400;
  font-size:clamp(24px,2.6vw,30px);
  line-height:1.1;
  color:var(--ink);
  margin:0 0 clamp(18px,2.2vw,24px);
  letter-spacing:.01em;
}
/* Olive on cream is 6.20:1 — a step down from the ink it labels, which is what
   makes it read as a label rather than as content. */
.pl-label{
  display:block;
  font-family:'Stoury Serif',Georgia,serif;
  font-size:14px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--olive);
  margin:0 0 6px;
}
/* 16px is not a style choice: iOS zooms the viewport when a focused input is
   under 16px, which would throw the whole page off centre on a phone. */
.pl-input{
  display:block;width:100%;
  font-family:'Stoury Serif',Georgia,serif;
  font-size:16px;
  color:var(--ink);
  background:#fffdf7;
  border:1px solid rgba(58,53,41,.30);
  border-radius:7px;
  padding:11px 13px;
  margin:0 0 16px;
}
.pl-input:focus-visible{
  outline:2px solid var(--olive);outline-offset:1px;
  border-color:transparent;
}
/* Ink on coral is 5.88:1. Coral is the brand's accent — it is the O — so the
   one action in this panel wears it. */
.pl-submit{
  display:block;width:100%;
  box-sizing:border-box;
  margin:4px 0 0;
  padding:13px 16px;
  font-family:'Stoury Display',Georgia,serif;
  font-size:18px;letter-spacing:.02em;
  text-align:center;
  color:var(--ink);
  background:var(--coral);
  border-radius:8px;
  text-decoration:none;
}
.pl-submit:hover{filter:brightness(1.04);}
.pl-submit:focus-visible{
  outline:2px solid var(--ink);outline-offset:2px;
}
/* Hidden until the fragment says otherwise. Coral at .30 over cream composites
   to #f6d7c1, and ink on that is 8.95:1 — the palette has no red, so the band
   is what marks this as a failure rather than a colour change on the text. */
.pl-error{
  display:none;
  margin:16px 0 0;
  padding:10px 13px;
  border-radius:7px;
  background:rgba(247,158,120,.30);
  font-family:'Stoury Serif',Georgia,serif;
  font-size:15px;
  color:var(--ink);
}
/* The whole sentence is the link, which is how CJ asked for it. Same
   construction as the footer address: mailto with a %20-encoded subject. */
.pl-ask{
  margin:clamp(18px,2.2vw,22px) 0 0;
  padding-top:clamp(14px,1.8vw,18px);
  border-top:1px solid rgba(58,53,41,.16);
  font-family:'Stoury Serif',Georgia,serif;
  font-size:15px;
  text-align:center;
}
.pl-mail{
  color:var(--ink);
  text-decoration:underline;
  text-decoration-color:rgba(58,53,41,.45);
  text-decoration-thickness:1px;
  text-underline-offset:3px;
}
.pl-mail:hover,
.pl-mail:focus-visible{text-decoration-color:currentColor;}
.pl-mail:focus-visible{
  outline:2px solid var(--olive);outline-offset:3px;border-radius:3px;
}
/* 44px square, sitting over the panel's top-right corner. */
.pl-close{
  position:absolute;top:6px;right:6px;
  width:44px;height:44px;
  display:flex;align-items:center;justify-content:center;
  color:var(--ink);
  border-radius:8px;
}
.pl-close svg{width:15px;height:15px;}
.pl-close svg path{
  stroke:currentColor;stroke-width:1.9;stroke-linecap:round;fill:none;
}
.pl-close:hover{background:rgba(58,53,41,.08);}
.pl-close:focus-visible{
  outline:2px solid var(--olive);outline-offset:1px;
}

/* ---- responsive ---- */
@media (max-width:860px){
  .wrap{padding:0 22px;}
  .beat-row{grid-template-columns:repeat(2,1fr);}
}
/* The wordmark is the last thing on the page that will not shrink.
   .wm is display:inline-flex, so it sizes to its own content and simply
   ignores the width of the box around it. Held at the clamp's 72px floor it
   is 292.6px wide, which with the 22px side padding needs 336.6px of LAYOUT
   viewport — and a 320px phone whose browser draws a classic 15px scrollbar
   only has ~305px. The wordmark then pushed past the olive and body's cream
   showed through down the right edge.

   Dropping the floor below 400px lets it scale instead. 18vw is not a new
   number: it is the SAME middle term the clamp already uses, and 18vw is
   exactly 72px at a 400px viewport, so the two meet at the boundary and there
   is no jump in size crossing it. This only removes a floor that was holding
   the mark too large underneath, and the O aperture rides along because it is
   sized in em.

   Both sides scale linearly, so the ratio is fixed: the mark needs
   0.7315 x viewport and has viewport minus 44, which balances at ~164px, or
   ~220px once a 15px scrollbar is taken out. Below any real device.

   Scoped to .hero deliberately: .wm-sm (the footer's 26px mark) carries only
   (0,1,0) and this block is later in the file, so a bare .wm here would win
   and blow the footer wordmark up to 18vw. */
@media (max-width:400px){
  .hero .wm{font-size:18vw;}
}

@media (max-width:520px){
  .beat-row{grid-template-columns:1fr;}
  .beat{font-size:26px;padding:20px 12px;}
}

/* ---- print: backgrounds are suppressed by default, so re-ground the
   light-on-dark sections in ink-on-white ---- */
@media print{
  .hero,.foot,.band{background:none;min-height:0;}
  .hero .wm,.foot .wm,.hero .tagline,.hero .lead,.band .band-line,.foot .fine,
  .foot .contact,
  .reel .reel-strip{
    color:var(--ink);text-shadow:none;}
  .reel{background:none;}
  .reel .reel-strip::before{display:none;}
  /* Only the first panel prints; the rest are stacked on top of it. */
  .reel .reel-line{animation:none;position:static;opacity:0;transform:none;}
  .reel .reel-line:nth-child(1){opacity:1;}
  .reel .reel-sizer,.reel .reel-turns{position:static;visibility:hidden;height:0;}
  .reel .reel-turns{visibility:visible;height:auto;}
  .beats{background:none;}
  .beat{border:1px solid var(--ink);}
  .ol{display:none;}
  .owin{background:none;box-shadow:inset 0 0 0 2px var(--ink);}
  .band{min-height:0;padding:24px 0;}
  /* Screen-only affordances. The dialog would otherwise print as an empty
     cream block, or not at all, depending on the fragment in the URL. */
  .partner-login,.pl-overlay{display:none!important;}
  .tour{background:none;}
  .tour-clip{display:none;}
  .tour-still{display:block;}
  .tour-laptop{filter:none;}
}
