/* ============================================================================
   OCCASION PAGES — /weddings and /dj-shows

   One skeleton, two skins. Everything structural lives here; each page sets
   data-occasion on <html> and only the accent tokens change. That keeps the
   pages unmistakably Avira — same type, same chamfers, same hairlines — while
   letting a wedding feel nothing like a nightclub.

   Loads AFTER css/style.css and reuses its tokens, so the near-black ground,
   Archivo/Plex Mono pairing and form styling all come for free.
   ========================================================================== */

/* ---------- per-occasion accents ----------
   The palettes are pulled from the owner's own footage rather than invented:
   the wedding clips are blush uplighting on champagne drape, the DJ clips are
   magenta and cyan beams. Using the real colours means the page and the video
   agree instead of fighting. */
:root[data-occasion="wedding"] {
  --oc-1:   #f3c9d4;   /* blush */
  --oc-2:   #f4e0bd;   /* champagne */
  --oc-3:   #c98fa6;   /* deeper rose, for depth */
  --oc-glow: rgba(243, 201, 212, 0.42);
  --oc-line: rgba(243, 201, 212, 0.20);
  --oc-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --oc-beat: 1;            /* motion speed multiplier — wedding is unhurried */
}
:root[data-occasion="dj"] {
  --oc-1:   #ff3d9a;   /* magenta beam */
  --oc-2:   #3de0ff;   /* cyan beam */
  --oc-3:   #8b5cf6;   /* the violet where they cross */
  --oc-glow: rgba(255, 61, 154, 0.45);
  --oc-line: rgba(61, 224, 255, 0.22);
  --oc-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --oc-beat: 0.68;         /* snappier */
}

/* The shared header, mobile menu and footer (css/style.css) are drawn from
   the --nav-* set, which defaults to gold. Re-pointed here at each page's
   own accent so the chrome agrees with the hero and the intro mark under
   it: blush on weddings — the --oc-1 the eyebrows and the <em> already use,
   champagne for the catch-lights, the deeper rose as the dark end of the
   CTA — and magenta on DJ shows, with the violet "where the beams cross"
   as the dark end. The CTA text is the same near-black .obtn uses. Only
   these tokens move: the gold tick in the consent box, the scrollbar and
   ::selection are style.css's own and stay gold. */
:root[data-occasion="wedding"] {
  --nav-accent: var(--oc-1);
  --nav-accent-hi: var(--oc-1);
  --nav-accent-deep: var(--oc-3);
  --nav-accent-rgb: 243, 201, 212;
  --nav-accent-hi-rgb: 244, 224, 189;
  --nav-on-accent: #14100c;
  --nav-glow: var(--oc-glow);
}
:root[data-occasion="dj"] {
  --nav-accent: var(--oc-1);
  --nav-accent-hi: var(--oc-1);
  --nav-accent-deep: var(--oc-3);
  --nav-accent-rgb: 255, 61, 154;
  --nav-accent-hi-rgb: 255, 61, 154;
  --nav-on-accent: #14100c;
  --nav-glow: var(--oc-glow);
}

/* ==========================================================================
   INTRO — rotating rings behind the mark, then it clears

   Same beat as the homepage (hold, reveal, dissolve) so moving between pages
   feels like one site, but the gesture is different: rings winding up rather
   than a lamp striking.
   ========================================================================== */
.oint {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  background: var(--bg0);
  transition: opacity 0.8s var(--oc-ease), visibility 0.8s;
}
.oint.is-done { opacity: 0; visibility: hidden; pointer-events: none; }

.oint__stage { position: relative; display: grid; place-items: center; width: min(78vw, 460px); aspect-ratio: 1; }

/* the rings: three concentric, counter-rotating, drawn as dashed circles so
   they read as machined rather than as a loading spinner */
.oint__ring {
  position: absolute; inset: 0; margin: auto;
  border-radius: 50%;
  border: 1px solid var(--oc-line);
  opacity: 0;
}
.oint__ring:nth-child(1) { width: 100%; height: 100%; }
.oint__ring:nth-child(2) { width: 74%;  height: 74%;  border-style: dashed; border-color: var(--oc-1); opacity: 0; }
.oint__ring:nth-child(3) { width: 50%;  height: 50%;  border-color: var(--oc-2); }

.oint.is-run .oint__ring { animation: oRingIn 0.9s var(--oc-ease) forwards; }
.oint.is-run .oint__ring:nth-child(1) { animation-delay: 0s;    }
.oint.is-run .oint__ring:nth-child(2) { animation-delay: 0.12s; }
.oint.is-run .oint__ring:nth-child(3) { animation-delay: 0.24s; }

/* spin continues under the fade-out, so the rings never look frozen */
.oint.is-run .oint__ring:nth-child(1) { animation: oRingIn .9s var(--oc-ease) forwards, oSpin calc(14s * var(--oc-beat)) linear infinite; }
.oint.is-run .oint__ring:nth-child(2) { animation: oRingIn .9s var(--oc-ease) .12s forwards, oSpinR calc(9s * var(--oc-beat)) linear infinite; }
.oint.is-run .oint__ring:nth-child(3) { animation: oRingIn .9s var(--oc-ease) .24s forwards, oSpin calc(6s * var(--oc-beat)) linear infinite; }

@keyframes oRingIn {
  from { opacity: 0; transform: scale(0.72); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes oSpin  { to { transform: rotate(360deg); } }
@keyframes oSpinR { to { transform: rotate(-360deg); } }

.oint__mark {
  position: relative; z-index: 2;
  width: clamp(64px, 16vw, 104px); height: auto;
  opacity: 0; transform: scale(0.9);
  filter: drop-shadow(0 0 26px var(--oc-glow));
}
.oint.is-run .oint__mark { animation: oMarkIn 0.8s var(--oc-ease) 0.3s forwards; }
@keyframes oMarkIn { to { opacity: 1; transform: none; } }

.oint__label {
  position: absolute; bottom: -46px; left: 50%; transform: translateX(-50%);
  font-family: var(--font-m); font-size: 0.62rem; letter-spacing: 0.42em;
  color: var(--muted); white-space: nowrap; opacity: 0;
}
.oint.is-run .oint__label { animation: oFade 0.7s ease 0.7s forwards; }
@keyframes oFade { to { opacity: 1; } }

.oint__skip {
  position: absolute; bottom: 34px; left: 50%; transform: translateX(-50%);
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-m); font-size: 0.62rem; letter-spacing: 0.2em;
  color: var(--muted); padding: 10px 16px;
}
.oint__skip:hover { color: var(--oc-1); }

/* the page underneath holds still until the intro clears */
body.oint-busy { overflow: hidden; }

/* ==========================================================================
   PAGE TRANSITION — a wipe out of the current page before navigating.
   Cheap: one fixed element, one transform. No library, no route framework.
   ========================================================================== */
.owipe {
  position: fixed; inset: 0; z-index: 300; pointer-events: none;
  background: linear-gradient(120deg, var(--bg0), #0d0a08 60%, var(--bg0));
  transform: scaleY(0); transform-origin: bottom;
  transition: transform 0.5s var(--oc-ease);
}
.owipe.is-in  { transform: scaleY(1); transform-origin: bottom; }
.owipe.is-out { transform: scaleY(0); transform-origin: top; }

/* ==========================================================================
   HERO
   ========================================================================== */
.ohero {
  position: relative; min-height: 100svh;
  display: grid; align-items: end;
  overflow: hidden;
}
/* No rule under the hero - the video should run out into the page ground
   instead of stopping at a line. A tall gradient in the page's own
   background colour does the blend. */
.ohero::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 34vh; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(5,4,3,0) 0%, rgba(5,4,3,0.55) 46%, var(--bg0) 100%);
}
.ohero__inner { position: relative; z-index: 2; }
.ohero__bg { position: absolute; inset: 0; z-index: 0; }
.ohero__bg video, .ohero__bg img {
  width: 100%; height: 100%; object-fit: cover;
  /* The source is portrait phone footage on a landscape hero, so `cover` crops
     it to a middle strip and upscales ~1.5x. Rather than pretend that is sharp,
     lean into it: a deliberate defocus plus a brightness knock-back reads as
     atmosphere behind the type instead of a low-quality video. The gallery
     below shows the same clips at their native shape and full sharpness. */
  filter: saturate(1.08) contrast(1.04) brightness(0.58) blur(3px);
  transform: scale(1.04);          /* hides the blur bleeding in at the edges */
}
.ohero__bg::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(to top, var(--bg0) 2%, rgba(5,4,3,0.55) 38%, rgba(5,4,3,0.15) 70%),
    radial-gradient(80% 60% at 50% 100%, var(--oc-glow), transparent 70%);
  opacity: 0.92;
}
.ohero__inner { position: relative; z-index: 1; padding: 0 0 clamp(48px, 9vh, 110px); }

.ohero__eyebrow {
  font-family: var(--font-m); font-size: 0.66rem; letter-spacing: 0.34em;
  color: var(--oc-1); text-transform: uppercase; margin-bottom: 18px;
  display: flex; align-items: center; gap: 12px;
}
.ohero__eyebrow::before {
  content: ""; width: 34px; height: 1px; background: var(--oc-1);
  box-shadow: 0 0 12px var(--oc-glow);
}
.ohero__title {
  font-weight: 900; font-stretch: 118%;
  font-size: clamp(2.4rem, 7.4vw, 5.6rem); line-height: 0.98;
  letter-spacing: -0.015em; margin: 0 0 20px;
}
.ohero__title em {
  font-style: normal; color: var(--oc-1);
  text-shadow: 0 0 40px var(--oc-glow);
}
.ohero__lede { color: var(--ink); opacity: 0.82; max-width: 58ch; font-size: clamp(1rem, 1.5vw, 1.16rem); line-height: 1.65; }
.ohero__cta { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 30px; }

.obtn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 14px 26px; font-size: 0.9rem; font-weight: 700;
  letter-spacing: 0.02em; cursor: pointer; border: 0;
  background: var(--oc-1); color: #14100c;
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
  transition: filter 0.25s, box-shadow 0.25s, transform 0.25s;
}
.obtn:hover { filter: brightness(1.12); box-shadow: 0 0 32px -4px var(--oc-glow); transform: translateY(-1px); }
.obtn--ghost {
  background: transparent; color: var(--ink);
  outline: 1px solid var(--oc-line); outline-offset: -1px;
}
.obtn--ghost:hover { color: var(--oc-1); outline-color: var(--oc-1); }

/* ==========================================================================
   SECTIONS
   ========================================================================== */
.osec { padding: clamp(64px, 11vh, 130px) 0; }
.osec--tight { padding-top: 0; }
.oeyebrow {
  font-family: var(--font-m); font-size: 0.62rem; letter-spacing: 0.32em;
  color: var(--oc-1); text-transform: uppercase; margin-bottom: 14px;
}
.oh2 {
  font-weight: 800; font-stretch: 114%;
  font-size: clamp(1.7rem, 3.6vw, 2.9rem); line-height: 1.08;
  margin: 0 0 18px; letter-spacing: -0.01em;
}
.oh2 em { font-style: normal; color: var(--oc-1); }
.olede { color: var(--muted); max-width: 62ch; line-height: 1.7; }

/* Alternating section grounds. Every band being the same near-black made the
   page read flat between the hero and the gallery — the content was fine, the
   rhythm was missing.

   The atmosphere plate behind this band is generated (nano-banana), not
   photographed: soft beams and haze in each occasion's own colours. It is
   deliberately abstract — no room, no gear, nothing that could be mistaken for
   a job Avira delivered. The real work is the video below it. A gradient sits
   over the top so body text keeps its contrast wherever the plate is bright. */
.osec--lift {
  position: relative;
  border-top: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
  background: #080706;
  isolation: isolate;
}
.osec--lift::before {
  content: ""; position: absolute; inset: 0; z-index: -2;
  background-size: cover; background-position: center;
  opacity: 0.55;
}
.osec--lift::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgba(8,7,6,0.55) 0%, rgba(8,7,6,0.86) 55%, #080706 100%);
}
:root[data-occasion="wedding"] .osec--lift::before { background-image: url("../assets/occasion/wedding/bg-atmosphere.jpg"); }
:root[data-occasion="dj"]      .osec--lift::before { background-image: url("../assets/occasion/dj/bg-atmosphere.jpg"); }

/* what's in it — cards */
.ocards { display: grid; gap: 18px; margin-top: 44px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
.ocard {
  position: relative; padding: 34px 28px 32px;
  background: linear-gradient(165deg, rgba(255,255,255,0.045), transparent 58%), var(--bg1);
  outline: 1px solid var(--line-soft); outline-offset: -1px;
  clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px));
  transition: outline-color 0.35s, transform 0.35s var(--oc-ease), background 0.35s;
  overflow: hidden;
}
/* a hairline of accent along the top edge — the one bit of colour on the card,
   so the eye gets a rhythm down the row without four coloured boxes */
.ocard::before {
  content: ""; position: absolute; left: 0; top: 0; height: 2px; width: 46px;
  background: var(--oc-1); box-shadow: 0 0 16px var(--oc-glow);
  transition: width 0.45s var(--oc-ease);
}
.ocard:hover::before { width: 100%; }
.ocard:hover {
  outline-color: var(--oc-line); transform: translateY(-4px);
  background: linear-gradient(165deg, rgba(255,255,255,0.07), transparent 58%), var(--bg1);
}
.ocard__n {
  font-family: var(--font-m); font-size: 0.6rem; letter-spacing: 0.26em;
  color: var(--oc-1); opacity: 0.85; text-transform: uppercase;
}
.ocard h3 {
  margin: 14px 0 11px; font-size: 1.18rem; font-weight: 700; font-stretch: 110%;
  line-height: 1.25;
}
.ocard p { color: var(--muted); font-size: 0.95rem; line-height: 1.72; margin: 0; }

/* ==========================================================================
   GALLERY — portrait clips, the shape the footage was actually shot in
   ========================================================================== */
/* Explicit column counts rather than auto-fit. These galleries are curated, not
   feeds — auto-fit gave the six wedding clips a 5+1 wrap with one orphan on its
   own row. Counts are chosen so each set divides evenly and the cards stay big. */
.ogal {
  display: grid; gap: 14px; margin-top: 40px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
@media (min-width: 1000px) {
  :root[data-occasion="wedding"] .ogal { grid-template-columns: repeat(3, 1fr); }  /* 6 clips → 3 + 3 */
  :root[data-occasion="dj"]      .ogal { grid-template-columns: repeat(4, 1fr); }  /* 4 clips → one row */
}
@media (min-width: 700px) and (max-width: 999px) {
  .ogal { grid-template-columns: repeat(2, 1fr); }
}
.ogal figure {
  position: relative; margin: 0; aspect-ratio: 9 / 16; overflow: hidden;
  background: var(--bg1);
  outline: 1px solid var(--line-soft); outline-offset: -1px;
  clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 14px 100%, 0 calc(100% - 14px));
  transition: outline-color 0.4s, transform 0.5s var(--oc-ease);
}
.ogal figure:hover { outline-color: var(--oc-1); transform: translateY(-4px); }
.ogal video, .ogal img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.ogal figcaption {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  padding: 34px 16px 15px;
  background: linear-gradient(to top, rgba(5,4,3,0.94), transparent);
  font-family: var(--font-m); font-size: 0.6rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--ink);
}

/* ==========================================================================
   THE FORM

   The markup was borrowed from /tradeshow, but its stylesheet was not loaded
   here — so every ts-* class resolved to nothing and the form rendered as raw
   browser defaults. Worse than ugly: `.ts-hp` is what HIDES the honeypot, so
   "Leave this empty" was on screen, and a real customer typing in it would have
   had their enquiry silently binned (send-mail.php answers a filled honeypot
   with a fake success). These pages now own their form styling outright rather
   than depending on another page's CSS.
   ========================================================================== */
.oform-wrap {
  margin-top: 38px; padding: clamp(26px, 4vw, 48px);
  background:
    radial-gradient(120% 90% at 0% 0%, rgba(255,255,255,0.045), transparent 55%),
    var(--bg1);
  outline: 1px solid var(--oc-line); outline-offset: -1px;
  clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 18px 100%, 0 calc(100% - 18px));
}

.ts-form { display: grid; gap: 18px; }
.ts-form .consent-check__policy { margin-top: -10px; }   /* 18px grid gap: 8px under the box */
.ts-form__row { display: grid; gap: 18px; grid-template-columns: 1fr 1fr; }
@media (max-width: 620px) { .ts-form__row { grid-template-columns: 1fr; } }

.ts-field { display: grid; gap: 8px; }
.ts-field > span {
  font-family: var(--font-m); font-size: 0.62rem; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--muted);
}
.ts-field > span em {
  font-style: normal; text-transform: none; letter-spacing: 0.04em;
  color: #6b6459; margin-left: 6px; font-size: 0.92em;
}
.ts-field input, .ts-field textarea {
  width: 100%; font: inherit; font-size: 0.98rem; color: var(--ink);
  background: #0c0b09;
  border: 0; border-bottom: 1px solid rgba(255, 255, 255, 0.14);
  padding: 12px 14px;
  transition: border-color 0.25s, background 0.25s, box-shadow 0.25s;
}
.ts-field textarea { resize: vertical; min-height: 116px; line-height: 1.6; }
.ts-field input::placeholder, .ts-field textarea::placeholder { color: #55504a; }
.ts-field input:hover, .ts-field textarea:hover { border-bottom-color: rgba(255, 255, 255, 0.28); }
.ts-field input:focus, .ts-field textarea:focus {
  outline: none;
  background: #100e0b;
  border-bottom-color: var(--oc-1);
  box-shadow: 0 6px 22px -12px var(--oc-glow);
}

/* Validation. These class names are not arbitrary — js/form-validate.js puts
   `is-invalid` on the INPUT and injects a `.field__err` paragraph into the
   wrapper. Guessing different names here is exactly how the rest of this form
   ended up unstyled. */
.ts-field input.is-invalid,
.ts-field textarea.is-invalid { border-bottom-color: #e0685c; background: #140d0c; }
.ts-field .field__err { margin: 0; color: #f0a49a; font-size: 0.8rem; line-height: 1.5; }

/* THE HONEYPOT. Off-screen rather than display:none — some bots skip fields
   that are display:none, and this one only works if they fill it in. Never
   focusable or announced, so nobody real can reach it by accident. */
.ts-hp {
  position: absolute !important;
  left: -9999px !important; top: auto !important;
  width: 1px !important; height: 1px !important;
  overflow: hidden !important;
}

.ts-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  padding: 15px 30px; margin-top: 4px;
  font: inherit; font-size: 0.92rem; font-weight: 700; letter-spacing: 0.02em;
  cursor: pointer; border: 0;
  background: var(--oc-1); color: #14100c;
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
  transition: filter 0.25s, box-shadow 0.25s, transform 0.25s;
}
.ts-btn--full { width: 100%; }
.ts-btn:hover:not(:disabled) { filter: brightness(1.12); box-shadow: 0 0 34px -6px var(--oc-glow); transform: translateY(-1px); }
.ts-btn:disabled { opacity: 0.55; cursor: default; }

.ts-form__status { margin: 4px 0 0; font-size: 0.88rem; min-height: 1.2em; }
.ts-form__status.ok  { color: #9fd8a4; }
.ts-form__status.err { color: #f0a49a; }

/* ==========================================================================
   SCROLL REVEALS
   ========================================================================== */
.orev { opacity: 0; transform: translateY(22px); transition: opacity 0.8s var(--oc-ease), transform 0.8s var(--oc-ease); }
.orev.is-in { opacity: 1; transform: none; }

/* ==========================================================================
   REDUCED MOTION — the whole point of the intro is motion, so it is skipped
   outright rather than played slowly.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .oint { display: none; }
  .oint__ring, .oint__mark, .oint__label { animation: none !important; }
  .owipe { display: none; }
  .orev { opacity: 1; transform: none; transition: none; }
  .ohero__bg video { display: none; }
}

@media (max-width: 700px) {
  .ohero { min-height: 92svh; }
  .ogal { grid-template-columns: repeat(2, 1fr); }
}

/* ==========================================================================
   SECTION SEAM — replaces the flat 1px rules between bands

   A hairline border between sections reads like a spreadsheet rule. Avira
   sells light, so the divider is a beam instead: a filament that ignites in
   the middle, fades out before it reaches either edge, and sits inside its own
   soft bloom with a single lit node at the centre. Same idea as a fixture on a
   truss span, which is the language the rest of the site already speaks.
   ========================================================================== */
.oseam {
  position: relative;
  height: 1px; border: 0; margin: 0;
  background: linear-gradient(90deg,
      transparent 0%, var(--oc-line) 20%, var(--oc-1) 50%,
      var(--oc-line) 80%, transparent 100%);
  opacity: 0.85;
  overflow: visible;
}
/* the bloom the filament sits in — blurred so it never reads as a shape */
.oseam::before {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: min(820px, 82vw); height: 150px;
  transform: translate(-50%, -50%);
  background: radial-gradient(ellipse at center, var(--oc-glow) 0%, transparent 70%);
  opacity: 0.30; filter: blur(16px); pointer-events: none;
  animation: oseam-breathe 7s ease-in-out infinite;
}
/* the lit node, rotated to a diamond so it matches the chamfer language */
.oseam::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 5px; height: 5px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--oc-1);
  box-shadow: 0 0 10px var(--oc-glow), 0 0 26px var(--oc-glow);
  animation: oseam-breathe 7s ease-in-out infinite;
}
@keyframes oseam-breathe {
  0%, 100% { opacity: 0.30; }
  50%      { opacity: 0.62; }
}
@media (prefers-reduced-motion: reduce) {
  .oseam::before, .oseam::after { animation: none; }
}

/* ---------- the DJ seam is a length of truss ----------
   The wedding seam is a hairline with a lit diamond on it, and tinting that
   same shape magenta is one design in two colours rather than two designs.
   On a page about renting truss the answer is obvious: divide the sections
   with a length of truss, laid on its side and running off both edges.

   Drawn, not photographed. The obvious route is a stock product shot of a
   chrome box truss, but that is somebody else's copyrighted photograph on a
   commercial site. This is an SVG: chords are rounded rects carrying a
   six-stop chrome gradient across their thickness, which is exactly how a
   lit cylinder falls; the diagonals are a dark stroke with a thinner bright
   one offset above it, because a gradient along a diagonal runs the wrong
   way round the tube and reads as a wash instead of a highlight. It tiles
   seamlessly (both ends of the zigzag sit at the same height), weighs about
   a kilobyte, and stays sharp at any width. */
:root[data-occasion="dj"] .oseam {
  position: relative;
  /* Tall enough to hold the truss, the fixtures AND the full length of their
     throw. It has to be: mask-clip defaults to the border box, so a beam
     running past the bottom is not merely unstyled, it is sliced off flat —
     which reads as a clipped rectangle rather than light. The padding under
     the divider is cut to match, so the run between sections stays the same. */
  height: 176px;
  /* Longhands, not the `background` shorthand. The shorthand form
     `url(...) repeat-x top center / 128px 46px` puts a position and a size
     either side of a slash, which is the fussiest corner of that grammar
     and the first thing to differ between engines. Spelled out there is
     nothing to disagree about. */
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='128'%20height='46'%3E%3Cdefs%3E%3ClinearGradient%20id='c'%20x1='0'%20y1='0'%20x2='0'%20y2='1'%3E%3Cstop%20offset='0'%20stop-color='%233c4045'/%3E%3Cstop%20offset='.16'%20stop-color='%238d949c'/%3E%3Cstop%20offset='.34'%20stop-color='%23eff2f5'/%3E%3Cstop%20offset='.52'%20stop-color='%23b4bbc3'/%3E%3Cstop%20offset='.78'%20stop-color='%23585d63'/%3E%3Cstop%20offset='1'%20stop-color='%232e3237'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath%20d='M0%2038.5L64%208.5L128%2038.5'%20fill='none'%20stroke='%2340454b'%20stroke-width='7.5'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3Cpath%20d='M0%2036.8L64%206.8L128%2036.8'%20fill='none'%20stroke='%23c4cbd3'%20stroke-width='2.6'%20stroke-linecap='round'%20stroke-linejoin='round'%20opacity='.85'/%3E%3Crect%20x='0'%20y='2'%20width='128'%20height='9'%20rx='4.5'%20fill='url(%23c)'/%3E%3Crect%20x='0'%20y='35'%20width='128'%20height='9'%20rx='4.5'%20fill='url(%23c)'/%3E%3Cg%20fill='%23aab1b9'%3E%3Ccircle%20cx='0'%20cy='39.5'%20r='2.6'/%3E%3Ccircle%20cx='64'%20cy='7.5'%20r='2.6'/%3E%3Ccircle%20cx='128'%20cy='39.5'%20r='2.6'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-position: center top;
  background-size: 128px 46px;
  opacity: 1;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 11%, #000 89%, transparent 100%);
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-image: linear-gradient(90deg, transparent 0, #000 11%, #000 89%, transparent 100%);
  mask-repeat: no-repeat;
  mask-size: 100% 100%;
}
:root[data-occasion="dj"] .oseam::after { display: none; }
/* a wash behind the span so it reads as lit rather than drawn, and a still
   one — the breathing belongs to the other skin */
:root[data-occasion="dj"] .oseam::before {
  top: 26px; width: min(820px, 84vw); height: 130px;
  opacity: 0.26; animation: none;
}
:root[data-occasion="dj"] .oseam { margin: clamp(18px, 3vh, 40px) 0; }
/* the divider is now tall and the beams already occupy the space under it,
   so the section below does not need the full seam gap on top as well */
:root[data-occasion="dj"] .oseam + .osec,
:root[data-occasion="dj"] .oseam + * > .osec { padding-top: clamp(36px, 5vh, 76px); }

/* ---------- and four fixtures hanging off it ----------
   Built from boxes rather than an image, because they have to MOVE.

   No yoke. Two attempts went the other way — a painted plate, then two
   real arms turning in 3D — and neither read at this size: at forty pixels
   a yoke is three or four pixels of metal either side of the lamp, too
   little to carry depth, and rotating it only squashed the silhouette. A
   lamp on a clamp is the shape that survives being small.

   Flat rotation, deliberately: nothing here foreshortens, so nothing can
   squash. Two nested swings on different periods — thirteen seconds on the
   hanger, nine on the lamp — so the drift never settles into one loop, and
   each fixture carries its own offset so the run never moves as a block.
   The lamp takes its delay from the head with `animation-delay: inherit`. */
.oseam__head { position: absolute; top: 33px; width: 40px; height: 72px; margin-left: -20px; }
.oseam__head:nth-child(1) { left: 19%; animation-delay: -1.4s; }
.oseam__head:nth-child(2) { left: 39%; animation-delay: -5.2s; }
.oseam__head:nth-child(3) { left: 61%; animation-delay: -8.6s; }
.oseam__head:nth-child(4) { left: 81%; animation-delay: -3.1s; }

/* the clamp on the bottom chord — this part does not move */
.oseam__head::before {
  content: ""; position: absolute; left: 50%; top: 0; z-index: 3;
  width: 32px; height: 13px; margin-left: -16px;
  border-radius: 2px 2px 3px 3px;
  background: linear-gradient(#7e868f, #3d434a 24%, #262b30 62%, #14171a);
}
/* the hanger the lamp swings on */
.oseam__head::after {
  content: ""; position: absolute; left: 50%; top: 12px; z-index: 3;
  width: 15px; height: 6px; margin-left: -7.5px;
  border-radius: 0 0 3px 3px;
  background: linear-gradient(#4a5158, #23272c);
}

/* outer swing */
.oseam__head > span {
  position: absolute; left: 50%; top: 16px;
  width: 36px; height: 54px; margin-left: -18px;
  transform-origin: 50% 1px;
  animation: oheadPan 13s ease-in-out infinite;
  animation-delay: inherit;
}
/* the lamp */
.oseam__head b {
  position: absolute; left: 50%; top: 0;
  width: 22px; height: 46px; margin-left: -11px;
  border-radius: 4px 4px 6px 6px;
  background: linear-gradient(90deg, #15181c, #454c54 24%, #7a828b 43%, #2f343a 74%, #0f1215);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
  transform-origin: 50% 3px;
  animation: oheadTilt 9s ease-in-out infinite;
  animation-delay: inherit;
}
/* a band across the body, so it is not one flat slab of gradient */
.oseam__head b::after {
  content: ""; position: absolute; left: 0; right: 0; top: 11px; height: 4px;
  background: linear-gradient(90deg, rgba(0,0,0,.5), rgba(255,255,255,.16) 44%, rgba(0,0,0,.55));
}
/* the lens, alternating the two beam colours down the run */
.oseam__head b::before {
  content: ""; position: absolute; left: 50%; bottom: -1px; z-index: 2;
  width: 20px; height: 8px; margin-left: -10px;
  border-radius: 2px 2px 7px 7px;
  background: var(--oc-1);
  box-shadow: 0 0 11px var(--oc-glow), 0 6px 22px var(--oc-glow);
}
.oseam__head:nth-child(even) b::before {
  background: var(--oc-2);
  box-shadow: 0 0 11px rgba(61, 224, 255, 0.55), 0 6px 22px rgba(61, 224, 255, 0.4);
}
/* the throw. A child of the LAMP, so it swings with it for free. */
.oseam__head b em {
  position: absolute; left: 50%; top: 100%;
  width: 54px; height: 80px; margin-left: -27px;
  background: linear-gradient(to bottom, var(--oc-1), transparent 94%);
  clip-path: polygon(42% 0, 58% 0, 100% 100%, 0 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 38%, #000 62%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 38%, #000 62%, transparent);
  opacity: 0.34; filter: blur(4px); pointer-events: none;
}
.oseam__head:nth-child(even) b em {
  background: linear-gradient(to bottom, var(--oc-2), transparent 94%);
  opacity: 0.28;
}
@keyframes oheadPan  { 0%, 100% { transform: rotate(-11deg); } 50% { transform: rotate(11deg); } }
@keyframes oheadTilt { 0%, 100% { transform: rotate(-7deg); }  50% { transform: rotate(9deg); } }

@media (max-width: 720px) {
  /* two is a run; four at this width is a crowd */
  .oseam__head:nth-child(2), .oseam__head:nth-child(3) { display: none; }
  .oseam__head:nth-child(1) { left: 27%; }
  .oseam__head:nth-child(4) { left: 73%; }
}
@media (prefers-reduced-motion: reduce) {
  .oseam__head > span, .oseam__head b { animation: none; }
  .oseam__head > span { transform: rotate(-7deg); }
  .oseam__head b { transform: rotate(5deg); }
}

/* the lifted band no longer needs hard edges — the seams do that job now */
.osec--lift { border-top: 0; border-bottom: 0; }

/* ==========================================================================
   BLOOM — the rotating glass flowers, used as a background element

   Generated glass, not a photograph of anyone's flowers, so it decorates
   without pretending to be a job we delivered. Pure black plate, so `screen`
   drops the background out completely and only the glass survives; that also
   means it composites correctly over whatever band it sits in.
   ========================================================================== */
.obloom {
  position: absolute; z-index: 0;
  width: min(760px, 92vw); aspect-ratio: 1;
  pointer-events: none; user-select: none;
  mix-blend-mode: screen;
  opacity: 0.17;
  filter: blur(2px) saturate(1.1);
  /* fades to nothing at the edges so it never reads as a pasted square */
  -webkit-mask-image: radial-gradient(circle at center, #000 38%, transparent 72%);
          mask-image: radial-gradient(circle at center, #000 38%, transparent 72%);
}
.obloom video, .obloom img { width: 100%; height: 100%; object-fit: contain; display: block; }
.obloom--left  { left: -18%;  top: 4%;  }
.obloom--right { right: -20%; bottom: -6%; }
/* content has to sit above it */
.osec > .wrap, .osec.wrap > *:not(.obloom) { position: relative; z-index: 1; }
.osec { position: relative; overflow: clip; }

@media (max-width: 760px) {
  .obloom { width: 118vw; opacity: 0.11; }
  .obloom--left  { left: -32%; }
  .obloom--right { right: -34%; }
}

/* ==========================================================================
   PACKAGES — three real builds, small to large
   ========================================================================== */
.opkgs { display: grid; gap: 22px; margin-top: 46px; }
.opkg {
  position: relative;
  display: grid; gap: 0;
  background: var(--bg1);
  outline: 1px solid var(--line-soft); outline-offset: -1px;
  clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px));
  transition: outline-color 0.4s, transform 0.4s var(--oc-ease);
}
@media (min-width: 900px) {
  .opkg { grid-template-columns: 1.35fr 1fr; align-items: stretch; }
  /* Alternate which side the picture sits on so the column has a rhythm.
     The track widths have to swap with it — ordering alone put the rig in
     the 1fr column on every second card, so it rendered visibly smaller. */
  .opkg:nth-child(even) { grid-template-columns: 1fr 1.35fr; }
  .opkg:nth-child(even) .opkg__shot { order: 2; }
}
.opkg:hover { outline-color: var(--oc-line); transform: translateY(-3px); }

.opkg__shot { position: relative; overflow: hidden; background: #000; min-height: 240px; }
.opkg__shot img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 0.9s var(--oc-ease);
}
.opkg:hover .opkg__shot img { transform: scale(1.035); }
/* keeps the badge legible over a bright ballroom shot */
.opkg__shot::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(5,4,3,0.55) 0%, transparent 34%, transparent 66%, rgba(5,4,3,0.5) 100%);
}
.opkg__tag {
  position: absolute; z-index: 2; top: 16px; left: 16px;
  font-family: var(--font-m); font-size: 0.58rem; letter-spacing: 0.28em;
  text-transform: uppercase; color: var(--oc-1);
  padding: 7px 12px; background: rgba(5,4,3,0.72);
  outline: 1px solid var(--oc-line); outline-offset: -1px;
  backdrop-filter: blur(4px);
}
/* an honest label: two of the three shots are visualisations built on top of
   our own photograph, and saying so is cheaper than being caught not saying it */
.opkg__real {
  position: absolute; z-index: 2; bottom: 14px; right: 14px;
  font-family: var(--font-m); font-size: 0.52rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: rgba(255,255,255,0.62);
}

.opkg__body { padding: 34px 30px 32px; display: flex; flex-direction: column; }
.opkg__n {
  font-family: var(--font-m); font-size: 0.58rem; letter-spacing: 0.3em;
  color: var(--muted); text-transform: uppercase;
}
.opkg__body h3 {
  font-weight: 800; font-stretch: 112%; margin: 10px 0 12px;
  font-size: clamp(1.15rem, 2vw, 1.5rem); line-height: 1.12;
}
.opkg__body p { color: var(--muted); line-height: 1.65; margin: 0 0 18px; }
.opkg__spec { list-style: none; padding: 0; margin: 0 0 20px; display: grid; gap: 8px; }
.opkg__spec li {
  position: relative; padding-left: 20px; font-size: 0.9rem; color: var(--fg);
}
.opkg__spec li::before {
  content: ""; position: absolute; left: 0; top: 0.55em;
  width: 5px; height: 5px; transform: rotate(45deg);
  background: var(--oc-1); box-shadow: 0 0 8px var(--oc-glow);
}
.opkg__cta { margin-top: auto; }

/* ==========================================================================
   INTRO — the real glass object

   The three dashed circles were a stand-in. The wedding intro now turns an
   actual pair of interlocked glass bands, generated in the page's own palette
   and rendered on a pure black plate — `screen` drops that plate out entirely,
   so the glass floats in the intro's void with nothing boxing it in. The mark
   sits in front of it, which is the whole gesture: the rings turn behind the A.
   ========================================================================== */
.oint__glass {
  /* deliberately much larger than the 460px stage — the object should feel like
     it is turning around the mark rather than sitting politely behind it */
  position: absolute; left: 50%; top: 50%; z-index: 1;
  width: min(104vw, 900px); aspect-ratio: 1;
  display: none; pointer-events: none;
  mix-blend-mode: screen;
  opacity: 0; transform: translate(-50%, -50%) scale(0.86);
}
.oint__glass video { width: 100%; height: 100%; object-fit: contain; display: block; }
.oint.is-run .oint__glass { animation: oGlassIn 1.5s var(--oc-ease) 0.05s forwards; }
@keyframes oGlassIn { to { opacity: 1; transform: translate(-50%, -50%) scale(1); } }

/* only the wedding skin has a glass object; the DJ skin keeps its rings */
:root[data-occasion="wedding"] .oint__glass { display: block; }
:root[data-occasion="wedding"] .oint__ring  { display: none; }

/* the mark has to read against a moving, bright object rather than empty black,
   so it gets a darker pad behind it and a heavier glow */
:root[data-occasion="wedding"] .oint__mark {
  z-index: 3;
  width: clamp(76px, 18vw, 126px);
  filter: drop-shadow(0 0 12px rgba(5,4,3,0.95)) drop-shadow(0 0 34px rgba(5,4,3,0.8))
          drop-shadow(0 0 30px var(--oc-glow));
}
:root[data-occasion="wedding"] .oint__label {
  z-index: 3; bottom: -152px;
  /* it now sits over a lit, moving object rather than flat black */
  text-shadow: 0 0 10px #050403, 0 0 24px #050403, 0 0 40px #050403;
}

@media (prefers-reduced-motion: reduce) {
  .oint__glass { animation: none !important; opacity: 1; transform: translate(-50%, -50%); }
  .oint__glass video { display: none; }
}

/* ---------- package card: live rig ----------
   The card hosts a real visualizer instance rather than a picture. The
   generated still stays underneath as the fallback layer and is faded out
   only once the canvas has actually been sized by the renderer, so a device
   without WebGL — or a slow module load — never shows a black hole. */
.opkg__shot--viz { background: #050403; }
.opkg__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%; display: block;
  opacity: 0; transition: opacity 0.7s var(--oc-ease);
  /* the rig turns on its own; dragging rotates it and must not scroll the page */
  touch-action: none;
}
.opkg__shot--viz.is-live .opkg__canvas { opacity: 1; }
.opkg__fallback { transition: opacity 0.7s var(--oc-ease); }
.opkg__shot--viz.is-live .opkg__fallback { opacity: 0; }
/* the gradient scrim is for photographs — over a black 3D scene it just
   muddies the beams, so it only stays while the still is showing */
.opkg__shot--viz.is-live::after { opacity: 0; transition: opacity 0.7s; }
/* The fallback still must NOT decide how tall the card is. Without a definite
   height here, `height:100%` on the image has nothing to resolve against, so
   the image falls back to its own intrinsic ratio and drags the card with it —
   the DJ stills are 720x1280 phone video frames, which stretched a 689px card
   to 1226px tall. Fixing the ratio on the box means any still can be dropped in
   and object-fit crops it instead. Stretch still wins when the text column is
   taller, so this is a floor on the row, not a cap. */
/* `width:100%` is load-bearing, not tidiness. align-items:stretch gives this
   item a definite HEIGHT, and aspect-ratio then transfers that back into a
   width — 459 x 1.6 = 735px in a 689px column, overflowing onto the text
   beside it. Pinning the width to the track makes the ratio one-way. */
.opkg__shot--viz { aspect-ratio: 16 / 10; width: 100%; min-height: 300px; }
@media (min-width: 900px) { .opkg__shot--viz { min-height: 360px; } }

/* ---------- seam spacing ----------
   A seam sitting one line above a heading reads as crowding rather than as a
   division, so a band that opens straight after one gets extra air. */
.oseam + .osec,
.oseam + * > .osec { padding-top: clamp(84px, 13vh, 160px); }
.oseam { margin: clamp(10px, 2vh, 26px) 0; }

/* ---------- blooms: anchored to the page edge, sized to the window ----------
   They were pinned by percentage of the section, so on a tall band they drifted
   into the middle of the copy and on a short one they vanished. Anchoring to
   the viewport width and to the section's own top edge keeps them where they
   were designed to sit at every size. */
.obloom {
  width: clamp(260px, 42vw, 720px);
  top: clamp(24px, 6vh, 90px);
  bottom: auto;
}
.obloom--left  { left: auto; right: auto; inset-inline-start: min(-14vw, -90px); }
.obloom--right {
  left: auto; right: auto; inset-inline-end: min(-13vw, -80px);
  top: auto; bottom: clamp(24px, 6vh, 90px);
}
@media (max-width: 760px) {
  .obloom { width: clamp(240px, 74vw, 420px); opacity: 0.10; }
  .obloom--left  { inset-inline-start: -22vw; }
  .obloom--right { inset-inline-end: -22vw; }
}

/* ==========================================================================
   HERO — centred

   Bottom-left anchoring left the whole lockup hanging off one corner with a
   large empty diagonal opposite it. Centred matches the homepage hero and
   gives the type the frame it was drawn for.
   ========================================================================== */
.ohero { align-items: center; }
.ohero__inner {
  text-align: center;
  padding-block: clamp(90px, 14vh, 160px) clamp(48px, 9vh, 110px);
}
.ohero__eyebrow { justify-content: center; }
.ohero__lede { margin-inline: auto; }
.ohero__cta { justify-content: center; }
.ohero__title { text-wrap: balance; }

/* ==========================================================================
   BLOOMS — vertically centred in their band

   Anchoring to the top or bottom edge meant the section's own `overflow: clip`
   sliced the flower in half along a hard horizontal line, and on the very tall
   packages band the bottom-anchored one ended up so far down it read as
   missing. Centring inside the band keeps both of them whole and on screen at
   every section height; only the intended edge crop remains.
   ========================================================================== */
.obloom {
  width: clamp(300px, 36vw, 620px);
  top: 50%; bottom: auto;
  transform: translateY(-50%);
}
.obloom--left  { inset-inline-start: min(-15vw, -110px); inset-inline-end: auto; }
.obloom--right { inset-inline-end: min(-15vw, -110px); inset-inline-start: auto;
                 top: 50%; bottom: auto; }
@media (max-width: 760px) {
  .obloom { width: clamp(260px, 66vw, 400px); opacity: 0.10; }
  .obloom--left  { inset-inline-start: -24vw; }
  .obloom--right { inset-inline-end: -24vw; }
}

/* ==========================================================================
   BLOOMS — run to the window edge, not the text column

   The bug was the clip, not the coordinates. `.osec` is also `.wrap`, so the
   band's box IS the max-width content column — and `overflow: clip` on it was
   slicing the flower off at the column edge, which lands well inside the page.
   No offset could ever push past it.

   So the band stops clipping and the page clips instead. `calc(50% - 50vw)`
   resolves to the viewport's left edge from inside a centred column, so the
   flower can be positioned against the window regardless of how wide the
   column is, bleed past it, and be cut by the window itself.

   `overflow-x: clip` rather than `hidden`: hidden on one axis forces the other
   to `auto`, which would give the page a scroll container it does not want and
   break position: sticky.
   ========================================================================== */
html, body { overflow-x: clip; }
.osec { overflow: visible; }

.obloom {
  /* was far too small and too faint to register as anything */
  width: clamp(460px, 56vw, 1000px);
  top: 50%; bottom: auto;
  transform: translateY(-50%);
  opacity: 0.32;
  filter: blur(1px) saturate(1.14);
  inset-inline: auto;
}

/* Solid toward the window, dissolving toward the copy: the outer side is meant
   to be cut off by the edge, the inner side has to melt into the page so it
   never reads as a pasted rectangle behind the text. */
.obloom--left {
  left: calc(50% - 50vw - 13vw); right: auto;
  -webkit-mask-image: radial-gradient(ellipse 145% 80% at 24% 50%, #000 44%, transparent 88%);
          mask-image: radial-gradient(ellipse 145% 80% at 24% 50%, #000 44%, transparent 88%);
}
.obloom--right {
  right: calc(50% - 50vw - 13vw); left: auto;
  -webkit-mask-image: radial-gradient(ellipse 145% 80% at 76% 50%, #000 44%, transparent 88%);
          mask-image: radial-gradient(ellipse 145% 80% at 76% 50%, #000 44%, transparent 88%);
}

@media (max-width: 760px) {
  .obloom { width: clamp(340px, 92vw, 560px); opacity: 0.20; }
  .obloom--left  { left: calc(50% - 50vw - 22vw); }
  .obloom--right { right: calc(50% - 50vw - 22vw); }
}

/* ==========================================================================
   LIFTED BAND — stop the atmosphere plate reading as a pasted rectangle

   Two separate hard edges were showing:

   1. The plate itself (`::before`) was `inset: 0` at a flat 0.55 opacity, so
      the bokeh simply started and stopped on a straight horizontal line. The
      existing scrim only darkened downward - it reached solid at the bottom
      but sat at 55% at the top, which is exactly where the seam was.

   2. The band's own ground, #080706, is a shade off the page's #050403. Even
      with the plate gone that leaves a faint rectangle the width of the page.

   So the plate now fades out at top and bottom with a mask, and the band's
   ground is a gradient that resolves to the page void at both ends instead of
   a flat fill. Nothing changes in the middle of the band, where it should read
   as lifted; only the two boundaries dissolve.
   ========================================================================== */
.osec--lift {
  background: linear-gradient(180deg,
      var(--bg0) 0%,
      #080706 14%,
      #080706 86%,
      var(--bg0) 100%);
}
.osec--lift::before {
  /* the plate has to die out well before the band's edge, or the mask just
     moves the hard line rather than removing it */
  -webkit-mask-image: linear-gradient(180deg,
      transparent 0%, rgba(0,0,0,0.55) 12%, #000 30%, #000 68%, rgba(0,0,0,0.5) 88%, transparent 100%);
          mask-image: linear-gradient(180deg,
      transparent 0%, rgba(0,0,0,0.55) 12%, #000 30%, #000 68%, rgba(0,0,0,0.5) 88%, transparent 100%);
  opacity: 0.5;
}
/* the scrim is now symmetric - it was only ever darkening toward the bottom */
.osec--lift::after {
  background: linear-gradient(180deg,
      rgba(8,7,6,0.86) 0%,
      rgba(8,7,6,0.62) 26%,
      rgba(8,7,6,0.70) 68%,
      rgba(8,7,6,0.92) 100%);
}

/* ==========================================================================
   LIFTED BAND — the actual edge, measured

   Sampling a column of empty background down the page showed a clean step of
   4 -> 7 luminance at the band's top and 7 -> 4 at its bottom. The ground
   gradient added earlier was never the thing being seen: `::after` is
   `inset: 0` and opens at 0.86 alpha, so the scrim paints straight over that
   gradient and re-creates a hard step at the exact pixel the band begins.

   Two changes. The scrim now fades to zero at both ends, so nothing paints at
   the boundary itself. And the band drops its own ground entirely -- #080706
   against the page's #050403 is a three-unit difference, which 8-bit cannot
   ramp smoothly over a short distance anyway, so it bands instead of blending.
   The masked plate and the scrim already do all the lifting; the flat fill was
   only ever contributing an edge.
   ========================================================================== */
.osec--lift { background: none; }
.osec--lift::after {
  background: linear-gradient(180deg,
      rgba(8, 7, 6, 0) 0%,
      rgba(8, 7, 6, 0.58) 16%,
      rgba(8, 7, 6, 0.68) 50%,
      rgba(8, 7, 6, 0.58) 84%,
      rgba(8, 7, 6, 0) 100%);
}

/* The 1px blur was there to stop an upscaled 760px plate from looking like a
   soft mess. The source is native 1440 now, so the blur is only throwing away
   the detail that was just paid for. */
.obloom { filter: saturate(1.14); }

/* ==========================================================================
   INTRO EXIT — scale through the rings into the page

   A straight cross-fade is the cheapest possible ending. Instead the glass
   object rushes past the camera as the overlay clears, so the page arrives
   from behind it: the rings blow past the viewport edges rather than
   dissolving on the spot.

   The overlay's own fade is held back slightly and eased late, so the scale is
   still visibly happening when the page underneath starts to show through —
   the two overlap rather than running one after the other.
   ========================================================================== */
.oint {
  transition: opacity 0.9s cubic-bezier(0.55, 0, 0.9, 0.45) 0.22s,
              visibility 1.15s;
}
.oint__glass {
  /* the exit needs its own, much longer curve than the entrance */
  transition: transform 1.15s cubic-bezier(0.5, 0, 0.75, 0),
              opacity 1.05s ease-in 0.12s;
}
.oint.is-done .oint__glass {
  transform: translate(-50%, -50%) scale(4.2);
  opacity: 0;
  animation: none;            /* the entrance keyframes must not fight this */
}
/* the mark and label lead the object out, so nothing is left floating */
.oint__mark, .oint__label {
  transition: transform 0.85s cubic-bezier(0.5, 0, 0.75, 0), opacity 0.6s ease-in;
}
.oint.is-done .oint__mark {
  transform: scale(2.3);
  opacity: 0;
  animation: none;
}
.oint.is-done .oint__label { opacity: 0; transform: translate(-50%, 14px); animation: none; }
/* the dashed rings on the DJ skin get the same push */
.oint.is-done .oint__ring { transform: scale(2.6); opacity: 0; animation: none; transition: transform 1.05s cubic-bezier(0.5,0,0.75,0), opacity 0.8s ease-in; }

@media (prefers-reduced-motion: reduce) {
  .oint, .oint__glass, .oint__mark, .oint__label, .oint__ring { transition: opacity 0.3s linear; }
  .oint.is-done .oint__glass,
  .oint.is-done .oint__mark,
  .oint.is-done .oint__ring { transform: none; }
}

/* ==========================================================================
   BLOOM SIZE — matched to the source

   The background object is the rotating glass mark, and the best master we
   have of it is 720 square. Sizing the bloom past that just upscales it, which
   is the exact fault we already fixed once on the flowers, so the cap comes
   down to meet the source rather than the other way round.
   ========================================================================== */
.obloom { width: clamp(300px, 40vw, 700px); }
@media (max-width: 760px) { .obloom { width: clamp(260px, 70vw, 420px); } }

/* ==========================================================================
   HERO WATERMARK (restored)

   The static mark behind the hero type. Removed by mistake when the animated
   overlay experiment was rolled back — that rollback should only have taken
   out the video layer, not this.
   ========================================================================== */
.ohero__mark {
  position: absolute; left: 50%; top: 50%;
  width: auto; height: min(88vh, 900px);
  transform: translate(-50%, -50%);
  z-index: 1;                  /* over the footage, under .ohero__inner */
  opacity: 0.10;
  pointer-events: none;
  filter: drop-shadow(0 0 60px var(--oc-glow));
}
@media (max-width: 760px) { .ohero__mark { height: 62vh; opacity: 0.13; } }

/* ==========================================================================
   BLOOM — ultrawide fix, and stop the two reading as one object

   The bleed was `13vw` while the width is capped at 700px, so past about
   3440px the offset simply outgrew the element and pushed it off screen
   entirely — at 5120px only 34px of it was still on the page. Offsetting by a
   share of the element's OWN width instead keeps the same proportion of it
   visible at every viewport, ultrawide included.

   The two blooms also play one file, so without this they are the same object
   twice. A different resting angle on each — plus a different start point in
   the loop, set in js/occasion.js — makes them read as two.
   ========================================================================== */
.obloom--left,
.obloom--right {
  left: auto; right: auto;
  inset-inline-start: auto; inset-inline-end: auto;
}
.obloom--left  { left:  calc(50% - 50vw); }
.obloom--right { right: calc(50% - 50vw); }

/* translateY(-50%) is already doing the vertical centring, so the horizontal
   bleed has to ride along in the same transform */
.obloom--left  { transform: translate(-34%, -50%); }
.obloom--right { transform: translate( 34%, -50%); }

/* the resting angle, so the pair are not identical */
.obloom--left  video, .obloom--left  img { transform: rotate(-14deg) scale(1.06); }
.obloom--right video, .obloom--right img { transform: rotate(21deg) scale(1.06); }

/* ==========================================================================
   BLOOM ANCHORING — to the copy, not to the window

   Anchoring at `calc(50% - 50vw)` pinned it to the viewport edge. That reads
   fine at 1600px, where the edge is close to the text column anyway, but the
   column is capped at 1200px — so on a 3440px display the bloom ended up
   roughly 1100px away from the nearest word, marooned in black.

   Anchoring to the column edge with a fixed bleed instead gives the right
   result at both ends. On a narrow window the column edge IS effectively the
   window edge, so it still runs off the page and is cut there. On ultrawide it
   stays beside the copy where it belongs, and simply is not cut.
   ========================================================================== */
.obloom--left,
.obloom--right { inset-inline-start: auto; inset-inline-end: auto; }

.obloom--left  { left:  -230px; right: auto; transform: translateY(-50%); }
.obloom--right { right: -230px; left:  auto; transform: translateY(-50%); }

/* the resting angles have to be reapplied — the transform above replaced the
   one that used to carry them */
.obloom--left  video, .obloom--left  img { transform: rotate(-14deg) scale(1.06); }
.obloom--right video, .obloom--right img { transform: rotate(21deg)  scale(1.06); }

@media (max-width: 760px) {
  .obloom--left  { left:  -34vw; }
  .obloom--right { right: -34vw; }
}

/* ==========================================================================
   BLOOM — centred in the gutter

   Previous positions were both wrong for the same reason: they anchored to an
   edge. At the window edge it was stranded a thousand pixels from the copy; at
   the column edge it sat behind the cards where you cannot see it.

   The empty channel between the text column and the window is where it should
   live, so put it in the middle of that. `(50% - 50vw)` is the window edge in
   the column's own coordinates, so half of it is the midpoint of the gutter —
   and it stays the midpoint at every viewport width without a media query.
   ========================================================================== */
.obloom--left {
  left: calc((50% - 50vw) / 2); right: auto;
  transform: translate(-50%, -50%);
}
.obloom--right {
  right: calc((50% - 50vw) / 2); left: auto;
  transform: translate(50%, -50%);
}

/* ==========================================================================
   BLOOM — stop the mask cutting the mark

   `mask-image` clips to the border box by default. The resting angle was being
   applied to the <video>, so a square rotated 14 deg and scaled 1.06 needed
   about 1.28x the element's width — everything past the box was sliced off
   with straight edges along the bottom and right.

   Rotating the CONTAINER instead of its contents fixes it outright rather than
   by shrinking anything: the mask lives in the element's own coordinate space,
   so it turns with the mark and the video still exactly fills its box. No
   overflow, nothing to clip, and the fade still runs the intended direction.
   ========================================================================== */
.obloom--left  video, .obloom--left  img,
.obloom--right video, .obloom--right img { transform: none; }

.obloom--left  { transform: translate(-50%, -50%) rotate(-14deg); }
.obloom--right { transform: translate( 50%, -50%) rotate(21deg); }

/* ==========================================================================
   BLOOM — the falloff is baked into the video now

   The frame's own glow used to run right to its border, and the CSS mask was
   far too large to hide it: `ellipse 145%` is still fully opaque where the box
   actually ends, so the rotated frame edge showed as a straight diagonal.

   The encode now fades every frame radially to true black before the border
   (measured: brightest border pixel is 1, against a peak of 250 in frame).
   Under `screen` that is literally nothing, so the edge cannot be seen — and
   the mask that was clipping the mark can come off entirely.
   ========================================================================== */
.obloom {
  -webkit-mask-image: none;
          mask-image: none;
}

/* ==========================================================================
   INTRO — the entrance must not rescale a playing video

   The glass object came in from scale(0.86) to scale(1) over 1.5s. For the
   whole of that the browser is resampling every decoded frame at a scale
   factor that changes each tick, and on a large object that reads as a wobble
   — which is exactly why it only ever happened at the start and settled once
   the animation finished. The source file was never the problem.

   The entrance is now opacity only. Nothing is being resized while the video
   plays, so the motion on screen is the motion in the file.
   ========================================================================== */
.oint__glass { transform: translate(-50%, -50%); }
.oint.is-run .oint__glass { animation: oGlassFade 1.2s ease-out 0.05s forwards; }
@keyframes oGlassFade { from { opacity: 0; } to { opacity: 1; } }
/* promoted to its own layer so compositing never resamples it mid-playback */
.oint__glass video { will-change: opacity; backface-visibility: hidden; }

/* ==========================================================================
   INTRO LOCKUP — AVIRA / PRODUCTIONS under the mark

   Same construction as the homepage intro: the wordmark rises letter by
   letter, then the sub-line follows. Only the palette changes.
   ========================================================================== */
.oint__lockup {
  position: absolute; left: 50%; top: 50%; z-index: 3;
  transform: translateX(-50%);
  margin-top: clamp(54px, 12vw, 92px);   /* clears the mark above it */
  display: grid; justify-items: center; gap: 9px;
  pointer-events: none;
}
.oint__word {
  display: flex; gap: 0.04em;
  font-weight: 900; font-stretch: 122%;
  font-size: clamp(1.5rem, 4.4vw, 2.5rem);
  letter-spacing: 0.06em; line-height: 1;
  color: var(--ink);
  text-shadow: 0 0 12px #050403, 0 0 30px #050403;
}
.oint__word span { opacity: 0; transform: translateY(0.5em); }
.oint.is-run .oint__word span { animation: oRise 0.6s var(--oc-ease) forwards; }
.oint.is-run .oint__word span:nth-child(1) { animation-delay: 0.42s; }
.oint.is-run .oint__word span:nth-child(2) { animation-delay: 0.48s; }
.oint.is-run .oint__word span:nth-child(3) { animation-delay: 0.54s; }
.oint.is-run .oint__word span:nth-child(4) { animation-delay: 0.60s; }
.oint.is-run .oint__word span:nth-child(5) { animation-delay: 0.66s; }
@keyframes oRise { to { opacity: 1; transform: none; } }

.oint__sub {
  font-family: var(--font-m); font-size: 0.64rem;
  letter-spacing: 0.58em; text-indent: 0.58em;
  color: var(--oc-1); opacity: 0;
  text-shadow: 0 0 10px #050403, 0 0 24px #050403;
}
.oint.is-run .oint__sub { animation: oRise 0.7s var(--oc-ease) 0.88s forwards; }

/* the occasion name drops below the lockup rather than colliding with it */
:root[data-occasion="wedding"] .oint__label,
:root[data-occasion="dj"] .oint__label {
  bottom: auto; top: 50%;
  /* clear of PRODUCTIONS rather than crowding it */
  margin-top: clamp(152px, 29vw, 226px);
}
.oint.is-run .oint__label { animation: oFade 0.7s ease 1.25s forwards; }

/* the lockup leaves with everything else */
.oint__lockup { transition: transform 0.85s cubic-bezier(0.5, 0, 0.75, 0), opacity 0.6s ease-in; }
.oint.is-done .oint__lockup { transform: translateX(-50%) scale(1.6); opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .oint__word span, .oint__sub { animation: none !important; opacity: 1; transform: none; }
  .oint.is-done .oint__lockup { transform: translateX(-50%); }
}

/* ==========================================================================
   BLOOM PLACEMENT MODIFIERS

   The packages band is tall enough that a single mark on one side left the
   opposite gutter empty for most of its height. It now carries a pair, offset
   against each other rather than sitting level — one high on the right, one
   low and smaller on the left — so the eye reads two objects at different
   depths instead of a symmetrical pair of bookends.
   ========================================================================== */
.obloom--high { margin-top: clamp(-260px, -16vh, -110px); }
.obloom--low  { margin-top: clamp(110px, 16vh, 260px); }
.obloom--sm   { width: clamp(230px, 28vw, 520px); }

/* A third angle, and it has to lean the OTHER way. At +8deg it tilted the
   same direction as the +21deg mark opposite it, so the pair read as one
   repeated object rather than two. Negative sends it left instead. */
.obloom--low.obloom--left { transform: translate(-50%, -50%) rotate(-26deg); }

@media (max-width: 760px) {
  .obloom--high { margin-top: -12vh; }
  .obloom--low  { margin-top: 12vh; }
  .obloom--sm   { width: clamp(200px, 52vw, 330px); }
}

/* ==========================================================================
   SPEC TABLE — the rentable stock, piece by piece

   A table and not a card grid on purpose. Three columns of short facts is what
   a table is for, and it is also the block an answer engine can lift cleanly;
   the same content as bullets in boxes reads as marketing and parses as soup.

   It scrolls inside its own box rather than widening the page. Three columns of
   technical text will not fit a phone at any sensible size, and a page that
   scrolls sideways feels broken.
   ========================================================================== */
.ospecs {
  margin-top: 46px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  outline: 1px solid var(--line-soft); outline-offset: -1px;
  background: var(--bg1);
  clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px));
}
.ospecs__t {
  width: 100%; min-width: 640px;
  border-collapse: collapse;
  font-size: 0.9rem;
}
.ospecs__t thead th {
  text-align: left; vertical-align: bottom;
  padding: 18px 20px 12px;
  font-family: var(--font-m); font-weight: 500;
  font-size: 0.56rem; letter-spacing: 0.28em; text-transform: uppercase;
  color: var(--oc-1); white-space: nowrap;
  border-bottom: 1px solid var(--oc-line);
}
.ospecs__t tbody th,
.ospecs__t tbody td {
  padding: 15px 20px; vertical-align: top; text-align: left;
  border-top: 1px solid var(--line-soft);
}
/* the item name is the row's heading, so it is the one thing that reads as
   type rather than as data */
.ospecs__t tbody th {
  font-weight: 700; font-stretch: 108%; color: var(--ink);
  white-space: nowrap;
}
.ospecs__t tbody td { color: var(--muted); line-height: 1.6; }
/* the numbers column is the reason the table exists — mono so figures line up
   down the column and can be scanned rather than read */
.ospecs__t tbody td:last-child {
  font-family: var(--font-m); font-size: 0.78rem; letter-spacing: 0.01em;
  color: var(--ink); opacity: 0.9;
}
.ospecs__t tbody tr:first-child th,
.ospecs__t tbody tr:first-child td { border-top: 0; }
.ospecs__t tbody tr { transition: background 0.25s; }
.ospecs__t tbody tr:hover { background: rgba(255, 255, 255, 0.028); }

@media (max-width: 720px) {
  .ospecs__t thead th { padding: 15px 15px 10px; }
  .ospecs__t tbody th, .ospecs__t tbody td { padding: 13px 15px; }
}

/* ==========================================================================
   FAQ — native <details>, no JavaScript

   Disclosure widgets are a solved problem in the platform: keyboard access,
   screen-reader semantics and find-in-page all work without a line of script,
   and find-in-page in particular matters because a closed <details> is still
   searchable. A hand-rolled accordion would have had to re-earn all of that.
   ========================================================================== */
.ofaq { margin-top: 44px; display: grid; gap: 1px; background: var(--line-soft); }
.ofaq__i { background: var(--bg1); }
.ofaq__i summary {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 20px 22px; cursor: pointer;
  list-style: none;
  font-weight: 700; font-stretch: 106%;
  font-size: clamp(0.95rem, 1.5vw, 1.05rem); line-height: 1.35;
  color: var(--ink);
  transition: color 0.25s, background 0.25s;
}
.ofaq__i summary::-webkit-details-marker { display: none; }
.ofaq__i summary:hover { color: var(--oc-1); background: rgba(255, 255, 255, 0.025); }
/* the marker: a small rotated square, matching the bullet on .opkg__spec, so
   the page has one shape vocabulary rather than a new icon per section */
.ofaq__i summary::before {
  content: ""; flex: 0 0 auto; margin-top: 0.5em;
  width: 6px; height: 6px; transform: rotate(45deg);
  background: var(--oc-1); box-shadow: 0 0 9px var(--oc-glow);
  transition: transform 0.35s var(--oc-ease), background 0.25s;
}
.ofaq__i[open] summary { color: var(--oc-1); }
.ofaq__i[open] summary::before { transform: rotate(225deg); background: var(--oc-2); }
.ofaq__a { padding: 0 22px 22px 42px; }
.ofaq__a p { color: var(--muted); line-height: 1.72; margin: 0; max-width: 68ch; }
.ofaq__i summary:focus-visible {
  outline: 1px solid var(--oc-1); outline-offset: -2px;
}

@media (max-width: 720px) {
  .ofaq__i summary { padding: 17px 16px; gap: 11px; }
  .ofaq__a { padding: 0 16px 18px 33px; }
}

/* ==========================================================================
   THE BUILD REEL — /dj-shows

   A rigging drawing that is alive. The right-hand side is the elevation: the
   rig itself, in 3D, bleeding off the edge of the screen. Tucked into its
   floor, bottom right, is the plan: a top-down stage plot drawn from the same
   metres the 3D is built from, with the footprint dimensioned and a title
   block of the build's real numbers. Left is the copy. Scroll, and the sheet
   redraws for the next build.

   The plot is the one thing on this page spent on being memorable, and it is
   there because it answers the question a promoter actually has — will it fit
   the room — not because it looks technical.

   PROGRESSIVE ENHANCEMENT, deliberately. Everything that hides a slide or pins
   the stage lives under `.reel.is-ready`, which js/dj-reel.js adds when it
   runs. Before that — and forever, if it never runs, as on a file:// open
   where module scripts are refused — the four builds simply stack, visible.
   The first version of this section hid the slides unconditionally and relied
   on the script to reveal one; opened from disk, every build vanished.
   ========================================================================== */
.reel { --reel-n: 4; --reel-step: 86vh; --reel-p: 0; position: relative; }

.reel__track { position: relative; }
/* clip, not hidden. "overflow: hidden" makes this a scroll container, which
   kills position:sticky for anything inside it — and in the no-script state
   the rig column sticks alongside the stacked builds. "clip" does the same
   clipping without becoming a scroller. */
.reel__stage { position: relative; overflow: clip; }

/* ---- one field, not two boxes ---- */
/* Two washes that travel in opposite directions as you scroll the builds, so
   the room behind the rig keeps moving rather than sitting under one fixed
   pool of light for three and a half screens. They cross near the middle
   build, which is where the picture is widest.

   Split across two pseudo-elements and moved with TRANSFORM rather than by
   animating the gradient positions. A blurred radial gradient the size of
   the stage is expensive to repaint, and repainting it on every scroll
   frame next to a live WebGL canvas is the kind of thing that costs frames
   on a phone. A transform is composited instead of repainted. */
.reel__glow { position: absolute; inset: -20% -10%; z-index: 0; pointer-events: none; }
.reel__glow::before,
.reel__glow::after {
  content: ""; position: absolute; inset: 0;
  filter: blur(24px);
}
.reel__glow::before {
  background: radial-gradient(38% 46% at 40% 50%, var(--oc-glow), transparent 70%);
  opacity: 0.85;
  transform: translate3d(calc(var(--reel-p, 0) * 30%), calc(var(--reel-p, 0) * -7%), 0);
}
.reel__glow::after {
  background: radial-gradient(30% 40% at 66% 62%, rgba(61, 224, 255, 0.15), transparent 72%);
  opacity: 0.9;
  transform: translate3d(calc(var(--reel-p, 0) * -34%), calc(var(--reel-p, 0) * 5%), 0);
}

/* Deliberately not a .wrap. The rig is meant to run off the right edge, so
   the grid is full-bleed and the COPY carries the page gutter, which keeps its
   left edge on the same line as every other section. */
.reel__grid {
  position: relative; z-index: 2;
  width: 100%;
  display: grid; align-items: center;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  gap: 0;
  padding-left: max(24px, calc((100% - var(--wrap)) / 2));
  padding-right: 24px;
}

/* ---- left: the words ---- */
.reel__copy { position: relative; z-index: 3; }

.rslide { position: relative; padding: 34px 0; }
.rslide + .rslide { border-top: 1px solid var(--line-soft); }

.rslide__n {
  display: block;
  font-family: var(--font-m); font-size: 0.58rem; letter-spacing: 0.3em;
  color: var(--oc-1); text-transform: uppercase;
}
.rslide h3 { margin: 10px 0 14px; }
/* The build's NAME, set the way a flight case is stencilled: condensed, heavy,
   all caps. Every other heading on the site is set wide; this is set narrow on
   purpose, because it is a label on a thing and not a headline about it. The
   only place the condensed width is used. */
.rslide__name {
  display: block;
  font-stretch: 72%; font-weight: 800;
  font-size: clamp(2.6rem, 5.6vw, 4.8rem); line-height: 0.88;
  letter-spacing: 0.005em; text-transform: uppercase;
  color: var(--ink);
}
.rslide__sub {
  display: block; margin-top: 12px;
  font-weight: 600; font-stretch: 106%;
  font-size: clamp(1rem, 1.5vw, 1.22rem); line-height: 1.25;
  max-width: 24ch; color: var(--ink);
}
.rslide > p {
  color: var(--muted); line-height: 1.68; margin: 0 0 20px; max-width: 46ch;
  font-size: clamp(0.92rem, 1.1vw, 1rem);
}
.rslide__spec { list-style: none; padding: 0; margin: 0 0 24px; display: grid; gap: 9px; }
.rslide__spec li {
  position: relative; padding-left: 20px;
  font-size: 0.9rem; color: var(--ink); line-height: 1.5;
}
.rslide__spec li::before {
  content: ""; position: absolute; left: 0; top: 0.52em;
  width: 5px; height: 5px; transform: rotate(45deg);
  background: var(--oc-1); box-shadow: 0 0 8px var(--oc-glow);
}

/* ---- right: the rig, with the sheet under it ----
   A flex column with a definite height, so the rig takes whatever the sheet
   does not. The sheet used to float in a corner of the stage and it kept
   landing on the build: `aspect-ratio: 1/1` against a `max-height` makes the
   canvas box SQUARE, so on a short screen it shrinks to 543px wide inside a
   689px column and no fixed corner offset clears it at every size. Stacked,
   overlap is not something that has to be avoided — it cannot happen. */
.reel__right {
  position: relative; z-index: 1; min-width: 0;
  display: flex; flex-direction: column; gap: 10px;
  height: min(84vh, 760px);
}
.reel__view {
  position: relative; z-index: 1;
  flex: 1 1 auto; min-height: 0;
}
.reel__canvas,
.reel__stills {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
}
.reel__canvas { opacity: 0; transition: opacity 0.8s var(--oc-ease); }
.reel.is-live .reel__canvas { opacity: 1; }

.reel__still {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0;
  transition: opacity 0.7s var(--oc-ease), transform 1.4s var(--oc-ease);
  transform: scale(1.06);
}
.reel__still.is-active { opacity: 0.5; transform: scale(1); }
.reel.is-live .reel__stills { opacity: 0; transition: opacity 0.8s var(--oc-ease); }


/* ---- the sheet: plan view + title block ----
   Sits in the rig's floor, where the 3D has already faded to nothing, so the
   sheet reads as part of the same drawing — elevation above, plan below — and
   not as a card dropped on top of it. */
.reel__sheet {
  position: relative; z-index: 4;
  flex: 0 0 auto; align-self: flex-end;
  display: grid; grid-template-columns: auto auto; gap: 0 18px; align-items: end;
  padding: 11px 13px 9px;
  background: rgba(5, 4, 3, 0.78);
  outline: 1px solid var(--line-soft); outline-offset: -1px;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 14px 100%, 0 calc(100% - 14px));
}
.reel__plot { margin: 0; }
.reel__plotsvg { display: block; width: 204px; height: auto; overflow: visible; }
/* a drawing's line weights: structure thin and grey, the things that matter
   coloured — screen in the beam magenta, sound in the beam cyan */
.reel__plotsvg .p-deck   { fill: rgba(255,255,255,0.035); stroke: rgba(242,237,226,0.34); stroke-width: 1; }
.reel__plotsvg .p-truss  { fill: none; stroke: rgba(242,237,226,0.5); stroke-width: 1; stroke-dasharray: 3 2; }
.reel__plotsvg .p-lift,
.reel__plotsvg .p-totem  { fill: rgba(242,237,226,0.06); stroke: rgba(242,237,226,0.55); stroke-width: 1; }
.reel__plotsvg .p-led    { fill: var(--oc-1); stroke: none; }
.reel__plotsvg .p-booth  { fill: rgba(242,237,226,0.1); stroke: rgba(242,237,226,0.6); stroke-width: 1; }
.reel__plotsvg .p-sub    { fill: none; stroke: var(--oc-2); stroke-width: 1; }
.reel__plotsvg .p-top    { fill: var(--oc-2); stroke: none; }
.reel__plotsvg .p-up     { fill: none; stroke: rgba(255,61,154,0.7); stroke-width: 1; }
.reel__plotsvg .p-head   { fill: none; stroke: rgba(242,237,226,0.7); stroke-width: 1; }
.reel__plotsvg .p-dim    { stroke: rgba(242,237,226,0.42); stroke-width: 1; }
.reel__plotsvg .p-txt    { font-family: var(--font-m); font-size: 8.5px; fill: rgba(242,237,226,0.7); letter-spacing: 0.06em; }
.reel__plotsvg .p-aud    { font-family: var(--font-m); font-size: 7px; fill: rgba(242,237,226,0.38); letter-spacing: 0.22em; }
.reel__dims {
  margin: 8px 0 0;
  font-family: var(--font-m); font-size: 0.6rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--ink);
}
.reel__dims i { font-style: normal; color: var(--muted); margin-left: 8px; }

.reel__plate {
  margin: 0; display: grid; grid-template-columns: auto auto; gap: 4px 12px;
  font-family: var(--font-m); font-size: 0.55rem; letter-spacing: 0.12em;
  text-transform: uppercase; white-space: nowrap;
}
.reel__plate dt { color: var(--muted); }
.reel__plate dd { margin: 0; color: var(--ink); display: flex; align-items: center; gap: 9px; }
/* the same 0-8 meter the homepage draws for these numbers, so a build reads
   the same on both pages */
.reel__plate i {
  display: inline-grid; grid-template-columns: repeat(8, 4px); gap: 2px;
}
.reel__plate i::before { content: none; }
.reel__plate b {
  display: block; width: 4px; height: 7px;
  background: rgba(255, 255, 255, 0.1);
}
.reel__plate b.on { background: var(--oc-1); box-shadow: 0 0 5px var(--oc-glow); }

/* ---- the rail ---- */
.reel__rail {
  position: absolute; z-index: 5; left: 0; right: 0; bottom: 18px;
  display: flex; justify-content: center; gap: 24px;
}
.reel__go {
  position: relative;
  display: flex; align-items: center; gap: 12px;
  background: none; border: 0; padding: 9px 0; cursor: pointer;
  color: var(--muted);
}
.reel__go i {
  width: 11px; height: 11px; flex: 0 0 auto; display: block;
  transform: rotate(45deg) scale(0.5);
  background: var(--muted);
  transition: transform 0.45s var(--oc-ease), background 0.35s, box-shadow 0.35s;
}
/* label for screen readers; on screen the copy already names the build */
.reel__go span {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.reel__go.is-active i {
  transform: rotate(45deg) scale(1);
  background: var(--oc-1); box-shadow: 0 0 12px var(--oc-glow);
}
.reel__go:focus-visible { outline: 1px solid var(--oc-1); outline-offset: 3px; }

/* Before the script runs there is nothing to page through, nothing to plot
   and no live canvas. */
.reel:not(.is-ready) .reel__rail,
.reel:not(.is-ready) .reel__sheet { display: none; }
/* No script: the rig column sticks alongside the stacked builds. It is the
   COLUMN that sticks now, not the picture inside it — and the picture needs
   its ratio back, because everything in it is absolutely positioned, so
   without one it has no content to take a height from and collapses to
   nothing. With the script running, .reel__right carries a definite height
   and the picture flexes into it instead. */
.reel:not(.is-ready) .reel__right {
  align-self: start; position: sticky; top: 4vh; height: auto;
}
.reel:not(.is-ready) .reel__view { aspect-ratio: 4 / 3; }

/* ==========================================================================
   ENHANCED — the script is running, so the stage may pin and the builds may
   take turns.
   ========================================================================== */
.reel.is-ready .reel__track {
  height: calc(100vh + (var(--reel-n) - 1) * var(--reel-step));
}
.reel.is-ready .reel__stage {
  position: sticky; top: 0;
  height: 100vh; height: 100dvh;
  display: flex; align-items: center;
}
.reel.is-ready .reel__copy { min-height: 430px; }
.reel.is-ready .rslide {
  position: absolute; left: 0; top: 50%; width: 100%;
  padding: 0; border: 0;
  transform: translateY(-50%);
  opacity: 0; visibility: hidden;
  transition: opacity 0.62s var(--oc-ease), visibility 0s linear 0.62s;
}
.reel.is-ready .rslide.is-active { opacity: 1; visibility: visible; transition-delay: 0s, 0s; }

/* Each line arrives on its own beat. --dir is set per slide by the script from
   its position relative to the active one, so builds behind you drift up and
   out while builds ahead rise from below — right in both directions with no
   need to remember which way you were scrolling. */
.reel.is-ready .rslide > * {
  opacity: 0;
  transform: translateY(var(--dir, 38px));
  transition: opacity 0.6s var(--oc-ease), transform 0.7s var(--oc-ease);
}
.reel.is-ready .rslide.is-active > * { opacity: 1; transform: none; }
.reel.is-ready .rslide.is-active > *:nth-child(1) { transition-delay: 0.04s; }
.reel.is-ready .rslide.is-active > *:nth-child(2) { transition-delay: 0.10s; }
.reel.is-ready .rslide.is-active > *:nth-child(3) { transition-delay: 0.18s; }
.reel.is-ready .rslide.is-active > *:nth-child(4) { transition-delay: 0.26s; }
.reel.is-ready .rslide.is-active > *:nth-child(5) { transition-delay: 0.34s; }

/* the rig reaches back under the copy and its left edge is masked away, so
   there is no line where one side stops and the other starts */
.reel.is-ready .reel__view { margin-left: -16%; }
/* The fade belongs to the PICTURE, not to the column. Masking .reel__view
   masked its descendants too, and the sheet is one of them — the bottom
   fade was rubbing out the last rows of the title block and the whole
   caption underneath the plan. */
.reel.is-ready .reel__canvas,
.reel.is-ready .reel__stills {
  -webkit-mask-image:
    linear-gradient(96deg, transparent 3%, #000 26%, #000 86%, transparent 99%),
    linear-gradient(transparent 0%, #000 9%, #000 72%, transparent 97%);
  mask-image:
    linear-gradient(96deg, transparent 3%, #000 26%, #000 86%, transparent 99%),
    linear-gradient(transparent 0%, #000 9%, #000 72%, transparent 97%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* ---- leaving the reel ----
   Exactly what one build does to the next, applied to the section itself.
   --reel-away is how far outside the reel you are at either end, and
   --reel-edge is the matching drift: positive on the way in, so the stage
   rises from below like an arriving build, negative on the way out, so it
   lifts away like a build you have passed. Both set by js/dj-reel.js.

   Scroll-linked rather than a transition, so it tracks the wheel exactly
   and reverses the moment you scroll back. The rail and the sheet only
   fade: each carries its own positioning transform, and overwriting that
   here would fling them across the page. */
.reel.is-ready .reel__grid {
  opacity: calc(1 - var(--reel-away, 0));
  transform: translate3d(0, var(--reel-edge, 0px), 0);
}
.reel.is-ready .reel__rail,
.reel.is-ready .reel__glow { opacity: calc(1 - var(--reel-away, 0)); }

/* the sheet redraws: a short settle rather than a fade, like a plotter */
.reel__sheet.is-redrawn { animation: reelSheet 0.55s var(--oc-ease) 1; }
@keyframes reelSheet {
  0%   { opacity: 0.35; transform: translateY(6px); }
  100% { opacity: 1;    transform: none; }
}

/* Only once there is a gutter to stand in does the rail go vertical. Between
   roughly 860 and 1100px the page has no side margin worth the name. */
@media (min-width: 1100px) {
  .reel.is-ready .reel__grid { padding-left: max(72px, calc((100% - var(--wrap)) / 2)); }
  .reel.is-ready .reel__rail {
    left: max(22px, calc((100% - var(--wrap)) / 2 - 34px)); right: auto;
    top: 50%; bottom: auto; transform: translateY(-50%);
    display: grid; gap: 2px; justify-content: start;
  }
  .reel.is-ready .reel__rail::before,
  .reel.is-ready .reel__rail::after {
    content: ""; position: absolute; left: 5px; top: 6px; bottom: 6px; width: 1px;
  }
  .reel.is-ready .reel__rail::after { background: rgba(255, 255, 255, 0.1); z-index: -1; }
  .reel.is-ready .reel__rail::before {
    background: linear-gradient(var(--oc-1), var(--oc-2));
    transform: scaleY(var(--reel-p)); transform-origin: top; opacity: 0.8;
  }
}

/* ---- narrow: the same reel, stacked ---- */
@media (max-width: 860px) {
  .reel { --reel-step: 76vh; }
  .reel__grid {
    grid-template-columns: 1fr;
    align-content: center; gap: 2px;
    padding: 24px 24px 54px;
  }
  .reel__right { order: -1; height: auto; gap: 8px; }
  .reel__view { aspect-ratio: 4 / 3; max-height: 34dvh; flex: 0 0 auto; }
  .reel.is-ready .reel__view { margin-left: 0; }
  .reel.is-ready .reel__canvas,
  .reel.is-ready .reel__stills {
    -webkit-mask-image: linear-gradient(#000 70%, transparent 99%);
    mask-image: linear-gradient(#000 70%, transparent 99%);
    -webkit-mask-composite: source-over; mask-composite: add;
  }
  .reel.is-ready .reel__copy { min-height: 322px; }
  .rslide__name { font-size: clamp(2.2rem, 11vw, 3rem); }
  .rslide__sub { max-width: none; font-size: 1rem; margin-top: 8px; }
  .rslide h3 { margin: 8px 0 10px; }
  .rslide > p {
    margin-bottom: 15px; max-width: none;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .rslide__spec { gap: 6px; margin-bottom: 16px; }
  .rslide__spec li { font-size: 0.83rem; padding-left: 17px; }
  .reel__sheet { display: none; }
  .reel__go { padding: 6px; }
  .reel__go i { transform: rotate(45deg) scale(0.68); }
  .reel:not(.is-ready) .reel__right { position: static; }
}

/* Short viewports. Stay pinned — un-pinning collapses the scroll span to zero
   and strands three of the four builds — and make the content fit instead. */
@media (max-height: 620px) {
  .reel { --reel-step: 72vh; }
  .reel.is-ready .reel__copy { min-height: 0; }
  .rslide__name { font-size: clamp(1.9rem, 4vw, 2.6rem); }
  .rslide__sub { font-size: 0.95rem; margin-top: 6px; }
  .rslide h3 { margin: 6px 0 9px; }
  .rslide > p {
    margin-bottom: 12px;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .rslide__spec { gap: 5px; margin-bottom: 14px; }
  .rslide__spec li { font-size: 0.8rem; }
  .reel__sheet { display: none; }
}
@media (max-height: 620px) and (min-width: 861px) {
  .reel__right { height: 86vh; }
}
/* Short AND narrow — a phone on its side. The 3D goes: squeezed into ~90px it
   was an unreadable smudge that pushed the button off screen. */
@media (max-height: 620px) and (max-width: 860px) {
  .reel__right { display: none; }
  .reel__grid { padding-top: 10px; padding-bottom: 36px; }
  .rslide__spec { margin-bottom: 14px; }
  .reel__rail { bottom: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  .reel.is-ready .rslide,
  .reel.is-ready .rslide > *,
  .reel__still,
  .reel__canvas { transition-duration: 0.001s !important; transition-delay: 0s !important; }
  .reel.is-ready .rslide > * { transform: none; }
  .reel__still { transform: none; }
  .reel__sheet.is-redrawn { animation: none; }
  /* the fades stay — they are not vestibular, and they are what stops the
     stage jumping. The drift and the travelling wash go. */
  .reel.is-ready .reel__grid { transform: none; }
  .reel__glow::before, .reel__glow::after { transform: none; }
}
