/* ============================================================================
   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 */
}

/* ==========================================================================
   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__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 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; }
.opkg__shot--viz { 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%);
}

/* ==========================================================================
   OCCASION FOOTER

   The homepage footer is `.footer__*` and fully styled. These pages were built
   with `.foot__*` markup, and that CSS was never written — so the occasion
   footers have been rendering with no styling at all, which is why the links
   ran together as one unbroken string.
   ========================================================================== */
.foot {
  border-top: 1px solid var(--line-soft);
  background: #040302;
  padding: 34px 0 30px;
  margin-top: clamp(40px, 8vh, 90px);
}
.foot__in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 18px 32px; flex-wrap: wrap;
}
.foot__copy {
  margin: 0;
  font-family: var(--font-m); font-size: 0.66rem; letter-spacing: 0.14em;
  color: #6d6455;
}
.foot__nav {
  display: flex; align-items: center;
  gap: 14px 26px; flex-wrap: wrap;
  font-size: 0.85rem;
}
.foot__nav a {
  color: var(--muted); text-decoration: none;
  transition: color 0.25s;
}
.foot__nav a:hover,
.foot__nav a:focus-visible { color: var(--oc-1); }
@media (max-width: 620px) {
  .foot__in { flex-direction: column; align-items: flex-start; }
  .foot__nav { gap: 10px 18px; }
}

/* ==========================================================================
   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); }
}
