/* ===================================================================
   OPS INTEL — UI THEME LAYER
   Loaded site-wide from website_base.html, on top of website.css
   (compiled Tailwind) and any per-page stylesheet.

   This file owns the *look* that runs through every page: instrument
   panels, hairline rules, corner brackets, the measured grid, the
   pointer-tracked glow, and the motion primitives that go with them.

   The motif is the crosshair in the logo: things are measured, framed
   and registered rather than floated on a soft background. Corners are
   0px everywhere — that is the brand rule, not a style choice here, so
   the depth has to come from layering and light instead of radius.

   Colours are the locked brand set only (navy #0a1628, amber #f59e0b,
   white). Everything below is an alpha layer of one of those three —
   no new hues are introduced.

   Class prefix `oi-` so nothing here can collide with a Tailwind
   utility or with landing.css / geo.css / automations.css, all of
   which use bare semantic names.
   =================================================================== */

:root {
  /* Surface ladder — each step is a real layer, not a tint of the same flat navy */
  --oi-navy-900: #060d1a;
  --oi-navy-800: #0a1628;
  --oi-navy-700: #0e1d33;
  --oi-navy-600: #14263f;

  --oi-amber:       #f59e0b;
  --oi-amber-light: #fbbf24;

  /* Hairlines. Three weights: structural, active, hot. */
  --oi-line:       rgba(255, 255, 255, 0.08);
  --oi-line-mid:   rgba(255, 255, 255, 0.16);
  --oi-line-amber: rgba(245, 158, 11, 0.38);

  --oi-text:      #ffffff;
  --oi-text-soft: #94a3b8;
  --oi-text-dim:  #64748b;

  --oi-ease: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ── Measured grid ────────────────────────────────────────────────
   A blueprint rule rather than a dot field: two hairline sets at 96px
   with a finer 24px set inside, masked so it dissolves before it
   reaches the section edge. Reads as a drawing, not as a texture.   */
.oi-grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right,  var(--oi-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--oi-line) 1px, transparent 1px),
    linear-gradient(to right,  rgba(255,255,255,0.028) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255,255,255,0.028) 1px, transparent 1px);
  background-size: 96px 96px, 96px 96px, 24px 24px, 24px 24px;
  -webkit-mask-image: radial-gradient(ellipse 78% 65% at 50% 40%, #000 25%, transparent 78%);
          mask-image: radial-gradient(ellipse 78% 65% at 50% 40%, #000 25%, transparent 78%);
}

/* ── Grain ────────────────────────────────────────────────────────
   Stops large flat navy fields reading as a default dark theme. The
   noise is an inline SVG turbulence — no extra request, no asset to
   force-add past .gitignore.                                        */
.oi-grain::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.16;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

/* ── Pointer-tracked glow ─────────────────────────────────────────
   --oi-mx / --oi-my are written by motion.js on pointer move. With no
   JS the fallback centres it, so the section still has depth.       */
.oi-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    620px circle at var(--oi-mx, 50%) var(--oi-my, 35%),
    rgba(245, 158, 11, 0.10),
    rgba(245, 158, 11, 0.035) 38%,
    transparent 68%
  );
  transition: background 220ms linear;
}

/* ── Panel ────────────────────────────────────────────────────────
   The workhorse surface. A lifted plane with a lit top edge — the
   sheen is what separates it from the page without a border radius
   or a drop shadow doing the work.                                  */
/* The lit top edge is a BACKGROUND LAYER, not a pseudo-element, and that
   is deliberate: an element has only ::before and ::after, and a panel
   that is also `.oi-bracket` needs both of them for its corner marks.
   Using ::before here made the two classes fight over one pseudo-element
   and the result inherited half its properties from each — a 14px square
   filled with the sheen gradient, sitting where a bracket should be. */
.oi-panel {
  position: relative;
  background:
    linear-gradient(90deg, transparent, var(--oi-line-mid) 22%, var(--oi-line-mid) 78%, transparent) top left / 100% 1px no-repeat,
    linear-gradient(180deg, rgba(255,255,255,0.045) 0%, rgba(255,255,255,0) 42%),
    var(--oi-navy-700);
  border: 1px solid var(--oi-line);
  transition: border-color 320ms var(--oi-ease), transform 320ms var(--oi-ease), background 320ms var(--oi-ease);
}
.oi-panel-hover:hover {
  border-color: var(--oi-line-amber);
  transform: translateY(-3px);
  background:
    linear-gradient(90deg, transparent, var(--oi-amber) 30%, var(--oi-amber) 70%, transparent) top left / 100% 1px no-repeat,
    linear-gradient(180deg, rgba(245,158,11,0.06) 0%, rgba(255,255,255,0) 42%),
    var(--oi-navy-700);
}

/* ── Corner brackets ──────────────────────────────────────────────
   The crosshair motif, applied to a surface. Registration marks at
   two opposite corners: cheap, unmistakable, and impossible with a
   rounded corner — which is the point.                              */
.oi-bracket::before,
.oi-bracket::after {
  content: '';
  position: absolute;
  width: 14px;
  height: 14px;
  border-color: var(--oi-amber);
  transition: width 320ms var(--oi-ease), height 320ms var(--oi-ease);
}
/* The colour has to be repeated in each longhand. `border-top: 2px solid`
   is a shorthand, and a shorthand resets every value it omits — so the
   colour set above silently became `currentColor` and the registration
   marks rendered as grey squares in the corner of every panel. */
/* Offsets are 0, not -1px: `.oi-scan` sets overflow:hidden, and anything
   placed outside the box is clipped away there. */
.oi-bracket::before {
  top: 0; left: 0;
  border-top: 2px solid var(--oi-amber);
  border-left: 2px solid var(--oi-amber);
}
.oi-bracket::after {
  bottom: 0; right: 0;
  border-bottom: 2px solid var(--oi-amber);
  border-right: 2px solid var(--oi-amber);
}
.oi-panel-hover.oi-bracket:hover::before,
.oi-panel-hover.oi-bracket:hover::after {
  width: 26px;
  height: 26px;
}

/* ── Eyebrow / micro label ────────────────────────────────────────
   The wide-tracked amber label already used across the site, given a
   tick mark so it registers against the grid instead of floating.   */
.oi-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--oi-amber);
}
.oi-eyebrow::before {
  content: '';
  width: 18px;
  height: 2px;
  background: var(--oi-amber);
  flex-shrink: 0;
}

/* ── Hairline rule with a registration tick ───────────────────────── */
.oi-rule {
  position: relative;
  height: 1px;
  background: linear-gradient(90deg, var(--oi-line-mid), transparent);
  border: 0;
}
.oi-rule::before {
  content: '';
  position: absolute;
  left: 0; top: -3px;
  width: 2px; height: 7px;
  background: var(--oi-amber);
}

/* ── Data type ────────────────────────────────────────────────────
   Inter only — the brand allows no second family — so numerals get
   their weight from tabular figures and tracking, not from a mono.  */
.oi-num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1, 'ss01' 1;
  letter-spacing: -0.02em;
}

/* ── Buttons ──────────────────────────────────────────────────────
   Sharp, weighted, with a light sweep on hover. The sweep is a
   transform on a pseudo-element, so it composites on the GPU and
   costs nothing on scroll.                                          */
.oi-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 15px 28px;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-decoration: none;
  border: 1px solid transparent;
  overflow: hidden;
  transition: transform 240ms var(--oi-ease), background 240ms var(--oi-ease),
              color 240ms var(--oi-ease), border-color 240ms var(--oi-ease);
}
.oi-btn > * { position: relative; z-index: 1; }
.oi-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-101%) skewX(-16deg);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.28), transparent);
  transition: transform 620ms var(--oi-ease);
}
.oi-btn:hover::after { transform: translateX(101%) skewX(-16deg); }
.oi-btn:hover { transform: translateY(-2px); }

.oi-btn-primary {
  background: var(--oi-amber);
  color: var(--oi-navy-800);
  box-shadow: 0 10px 30px -12px rgba(245, 158, 11, 0.75);
}
.oi-btn-primary:hover { background: var(--oi-amber-light); }

.oi-btn-ghost {
  background: rgba(255, 255, 255, 0.04);
  color: #ffffff;
  border-color: var(--oi-line-mid);
}
.oi-btn-ghost:hover { border-color: var(--oi-line-amber); background: rgba(245, 158, 11, 0.08); }

/* Arrow that travels on hover — one detail, used everywhere, so the
   site feels like it was built by one hand. */
.oi-arrow { display: inline-block; transition: transform 240ms var(--oi-ease); }
.oi-btn:hover .oi-arrow,
a:hover > .oi-arrow { transform: translateX(5px); }

/* ── Sequence (the "what happens next" ladder) ────────────────────── */
.oi-seq { position: relative; margin: 0; padding: 0; list-style: none; }
.oi-seq::before {
  content: '';
  position: absolute;
  left: 23px; top: 12px; bottom: 12px;
  width: 1px;
  background: linear-gradient(180deg, var(--oi-amber), var(--oi-line) 22%, var(--oi-line));
}
.oi-seq-item {
  position: relative;
  display: grid;
  grid-template-columns: 48px 1fr;
  gap: 24px;
  padding-bottom: 40px;
}
.oi-seq-item:last-child { padding-bottom: 0; }

.oi-seq-mark {
  position: relative;
  z-index: 1;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--oi-navy-800);
  border: 1px solid var(--oi-line-mid);
  font-size: 13px;
  font-weight: 700;
  color: var(--oi-text-dim);
}
/* Done: filled amber. Now: hollow amber with a live pulse. Waiting: flat. */
.oi-seq-item[data-state="done"] .oi-seq-mark {
  background: var(--oi-amber);
  border-color: var(--oi-amber);
  color: var(--oi-navy-800);
}
.oi-seq-item[data-state="now"] .oi-seq-mark {
  border-color: var(--oi-amber);
  color: var(--oi-amber);
  box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12);
}
.oi-seq-item[data-state="now"] .oi-seq-mark::after {
  content: '';
  position: absolute;
  inset: -1px;
  border: 1px solid var(--oi-amber);
  animation: oi-pulse 2.4s var(--oi-ease) infinite;
}
@keyframes oi-pulse {
  0%   { transform: scale(1);    opacity: 0.9; }
  70%  { transform: scale(1.45); opacity: 0; }
  100% { transform: scale(1.45); opacity: 0; }
}

.oi-seq-title {
  margin: 0 0 6px;
  font-size: 16px;
  font-weight: 700;
  color: var(--oi-text);
  letter-spacing: -0.01em;
}
.oi-seq-body {
  margin: 0;
  font-size: 15px;
  line-height: 1.7;
  color: var(--oi-text-soft);
  max-width: 58ch;
}
.oi-seq-item[data-state="waiting"] .oi-seq-title { color: var(--oi-text-soft); }
.oi-seq-item[data-state="waiting"] .oi-seq-body  { color: var(--oi-text-dim); }

/* ── Scan line ────────────────────────────────────────────────────
   One slow sweep across a panel on entry. Deliberately once, not on a
   loop — a looping scanner is a gimmick, a single pass reads as the
   page registering the panel.

   The beam is an explicit child element, NOT a pseudo-element. Both of
   this element's pseudo-elements are routinely already spoken for by
   `.oi-bracket`, and a library whose classes silently cancel each other
   out when combined is a trap for whoever composes them next.
   Markup: <div class="oi-panel oi-bracket oi-scan" data-oi-scan>
             <i class="oi-scan-beam" aria-hidden="true"></i> … </div>   */
.oi-scan { position: relative; overflow: hidden; }
.oi-scan-beam {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 140px;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(245,158,11,0.14), transparent);
  transform: translateX(-140px);
}
.oi-scan-run .oi-scan-beam {
  animation: oi-sweep 1.5s var(--oi-ease) 1 forwards;
}
@keyframes oi-sweep {
  to { transform: translateX(calc(100% + 140px)); }
}

/* ── Reveal ───────────────────────────────────────────────────────
   The static, no-JS state is fully visible. GSAP opts elements OUT of
   visibility only after it has loaded, so a blocked or failed script
   leaves a readable page rather than a blank one — same contract
   motion.js already follows.                                         */
.oi-reveal { will-change: transform, opacity; }

@media (prefers-reduced-motion: reduce) {
  .oi-btn::after,
  .oi-scan-beam,
  .oi-seq-item[data-state="now"] .oi-seq-mark::after { animation: none; display: none; }
  .oi-btn:hover, .oi-panel-hover:hover { transform: none; }
  .oi-glow { transition: none; }
}

/* ── Aurora ───────────────────────────────────────────────────────
   Slow-drifting blurred light sources behind a dark section. This is
   what stops a large navy field reading as flat: the grid gives it
   structure, the grain gives it texture, and this gives it depth and
   movement that never repeats on a visible cycle.

   Technique borrowed from the 21st.dev "Aurora Bento Grid" — three
   offset blobs on long, differently-phased loops — with two changes.
   Theirs are purple/yellow/blue on `mix-blend-mode: hard-light`, which
   is a palette we do not own; these are amber and two navies at low
   alpha, so the result is atmosphere rather than colour.

   And they are blurred RECTANGLES, not circles. At blur(110px) the
   shape is indistinguishable, and a 50% border-radius on anything —
   even an invisible light source — is a rule this brand does not bend.

   Markup: <div class="oi-aurora" aria-hidden="true"><i></i><i></i><i></i></div>
   Must sit inside a `position: relative` host, behind the content.  */
.oi-aurora {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.oi-aurora i {
  position: absolute;
  display: block;
  filter: blur(110px);
  will-change: transform;
}
.oi-aurora i:nth-child(1) {
  top: -22%; left: -12%;
  width: 46rem; height: 34rem;
  background: rgba(245, 158, 11, 0.20);
  animation: oi-drift-a 34s ease-in-out infinite alternate;
}
.oi-aurora i:nth-child(2) {
  top: 8%; right: -18%;
  width: 38rem; height: 30rem;
  background: rgba(37, 99, 235, 0.20);
  animation: oi-drift-b 41s ease-in-out infinite alternate;
}
.oi-aurora i:nth-child(3) {
  bottom: -28%; left: 24%;
  width: 42rem; height: 28rem;
  background: rgba(20, 38, 63, 0.85);
  animation: oi-drift-c 47s ease-in-out infinite alternate;
}

/* Three different periods and paths so the loops never visibly line up.
   Transform only — no layout, no paint, so this costs nothing on scroll. */
@keyframes oi-drift-a {
  0%   { transform: translate3d(0, 0, 0)         scale(1); }
  50%  { transform: translate3d(9rem, -5rem, 0)  scale(1.12); }
  100% { transform: translate3d(-4rem, 7rem, 0)  scale(0.96); }
}
@keyframes oi-drift-b {
  0%   { transform: translate3d(0, 0, 0)         scale(1); }
  50%  { transform: translate3d(-8rem, 6rem, 0)  scale(0.94); }
  100% { transform: translate3d(5rem, -6rem, 0)  scale(1.1); }
}
@keyframes oi-drift-c {
  0%   { transform: translate3d(0, 0, 0)         scale(1); }
  50%  { transform: translate3d(6rem, -4rem, 0)  scale(1.08); }
  100% { transform: translate3d(-7rem, 3rem, 0)  scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .oi-aurora i { animation: none; }
}

/* ── Shader canvas ────────────────────────────────────────────────
   Inserted by shader-bg.js as the FIRST child of [data-oi-shader],
   so it sits under .oi-aurora, .oi-grid and the content. The aurora
   is the fallback and stays in the markup: if WebGL is missing or the
   context is lost, the canvas removes itself and the aurora is simply
   what is there.                                                    */
.oi-shader-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}
.oi-spot { position: relative; overflow: hidden; }
.oi-spot::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 300ms var(--oi-ease);
  background: radial-gradient(
    340px circle at var(--oi-mx, 50%) var(--oi-my, 50%),
    rgba(245, 158, 11, 0.13), transparent 62%
  );
}
.oi-spot:hover::after,
.oi-spot:focus-visible::after { opacity: 1; }
/* The amber panel needs a WHITE spotlight — an amber radial on an amber
   fill is invisible. Renamed from .service-card-featured on 2026-08-14,
   when the three-tile block became the framework schedule. */
.oi-spot.framework-primary::after {
  background: radial-gradient(
    340px circle at var(--oi-mx, 50%) var(--oi-my, 50%),
    rgba(255, 255, 255, 0.22), transparent 62%
  );
}

.oi-litborder { position: relative; }
.oi-litborder::before {
  content: '';
  position: absolute;
  inset: 0;
  padding: 1px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 320ms var(--oi-ease);
  background: conic-gradient(
    from var(--oi-angle, 0deg),
    transparent 0deg, var(--oi-amber) 60deg,
    var(--oi-amber-light) 90deg, transparent 160deg, transparent 360deg
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
.oi-litborder:hover::before { opacity: 1; animation: oi-spin 2.4s linear infinite; }

@property --oi-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
@keyframes oi-spin { to { --oi-angle: 360deg; } }

.oi-word {
  display: inline-block;
  will-change: transform, opacity, filter;
}

.oi-marquee {
  position: relative;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
}
.oi-marquee-track {
  display: flex;
  gap: 20px;
  width: max-content;
  animation: oi-marquee-left 64s linear infinite;
}
.oi-marquee-track.is-reverse { animation-name: oi-marquee-right; }
.oi-marquee:hover .oi-marquee-track { animation-play-state: paused; }
@keyframes oi-marquee-left  { to { transform: translateX(-50%); } }
@keyframes oi-marquee-right { from { transform: translateX(-50%); } to { transform: translateX(0); } }

.oi-avatar {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: var(--oi-navy-800);
  background: var(--oi-amber);
}
.oi-avatar-quiet { background: rgba(255, 255, 255, 0.12); color: #fff; }

.oi-acc-panel {
  overflow: hidden;
  height: 0;
  transition: height 380ms var(--oi-ease), opacity 300ms var(--oi-ease);
  opacity: 0;
}
.oi-acc-item.is-open .oi-acc-panel { opacity: 1; }
.oi-acc-chevron { transition: transform 320ms var(--oi-ease); }
.oi-acc-item.is-open .oi-acc-chevron { transform: rotate(180deg); }

.oi-sticky-visual { position: sticky; top: 96px; }
.oi-sticky-card {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 420ms var(--oi-ease), transform 420ms var(--oi-ease);
  pointer-events: none;
}
.oi-sticky-card.is-active { opacity: 1; transform: none; pointer-events: auto; }

.oi-nav-indicator {
  position: absolute;
  bottom: -2px;
  left: 0;
  height: 2px;
  width: 0;
  background: var(--oi-amber);
  box-shadow: 0 0 12px 1px rgba(245, 158, 11, 0.85);
  opacity: 0;
  transition: transform 420ms var(--oi-ease), width 420ms var(--oi-ease), opacity 260ms ease;
  pointer-events: none;
}
.oi-nav-indicator.is-on { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .oi-marquee-track { animation: none; }
  .oi-litborder:hover::before { animation: none; }
  .oi-acc-panel { transition: none; }
  .oi-sticky-card { transition: none; }
  .oi-nav-indicator { transition: none; }
}

/* ── Pricing cards ────────────────────────────────────────────────
   The plan cards sit on WHITE, so they get a light spotlight rather
   than `.oi-spot` — an amber radial tuned for a navy panel reads as a
   dirty smudge on paper. Same pointer hook (data-oi-spot), same
   custom properties, different paint.

   The lift and the top edge are the same language as the dark cards,
   so the site holds together across its light and dark sections. */
.pricing-card {
  position: relative;
  overflow: hidden;
  transition: transform 320ms var(--oi-ease), box-shadow 320ms var(--oi-ease),
              border-color 320ms var(--oi-ease);
}
.pricing-card::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 300ms var(--oi-ease);
  background: radial-gradient(
    320px circle at var(--oi-mx, 50%) var(--oi-my, 50%),
    rgba(245, 158, 11, 0.09), transparent 62%
  );
}
.pricing-card:hover::after { opacity: 1; }
.pricing-card:hover {
  transform: translateY(-4px);
  border-color: rgba(245, 158, 11, 0.55);
  box-shadow: 0 22px 50px -30px rgba(10, 22, 40, 0.55);
}
/* The recommended tier is already amber-bordered, so it lifts further
   rather than changing colour — scale carries the emphasis, as it does
   on the bento. */
.pricing-card-featured:hover {
  transform: translateY(-7px);
  box-shadow: 0 30px 60px -28px rgba(245, 158, 11, 0.45);
}

@media (prefers-reduced-motion: reduce) {
  .pricing-card:hover, .pricing-card-featured:hover { transform: none; }
}

/* ── Step wizard ──────────────────────────────────────────────────
   A long form broken into steps, with a rail showing where you are.

   The rail lives on a WHITE surface — the intake form's fields sit on
   paper, not on navy — so it cannot reuse `.oi-seq`'s marks, which are
   drawn to read against `--oi-navy-800`. Same three states and the same
   meaning (done = filled amber, now = the live one, waiting = flat),
   repainted for light. `.pricing-card` sets the precedent: one language,
   two surfaces.

   Everything here is inert without JS. The template ships every panel
   visible and the rail reads as a plain list of what is going to be
   asked, so a blocked script leaves the long form that worked before —
   which is the contract every other primitive in this file follows.   */
.oi-steprail {
  display: flex;
  align-items: flex-start;
  margin: 0 0 36px;
  padding: 0;
  list-style: none;
}
.oi-steprail-item {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
}
/* The connector belongs to the mark on its LEFT and runs to the middle
   of the next one, which is why it is 100% wide from the 50% mark. Only
   ::before is spoken for — ::after is left free deliberately, so a rail
   item can still take a bracket later without the two fighting over one
   pseudo-element (see the note on .oi-panel). */
.oi-steprail-item::before {
  content: '';
  position: absolute;
  top: 17px;
  left: 50%;
  width: 100%;
  height: 1px;
  background: #e2e8f0;
  transition: background 320ms var(--oi-ease);
}
.oi-steprail-item:last-child::before { display: none; }
.oi-steprail-item[data-state="done"]::before { background: var(--oi-amber); }

.oi-steprail-mark {
  position: relative;
  z-index: 1;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #ffffff;
  border: 1px solid #cbd5e1;
  color: #64748b;
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  transition: background 320ms var(--oi-ease), border-color 320ms var(--oi-ease),
              color 320ms var(--oi-ease), box-shadow 320ms var(--oi-ease);
}
.oi-steprail-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #94a3b8;
  transition: color 320ms var(--oi-ease);
}
.oi-steprail-item[data-state="done"] .oi-steprail-mark {
  background: var(--oi-amber);
  border-color: var(--oi-amber);
  color: var(--oi-navy-800);
}
.oi-steprail-item[data-state="done"] .oi-steprail-label { color: #475569; }
.oi-steprail-item[data-state="now"] .oi-steprail-mark {
  background: var(--oi-navy-800);
  border-color: var(--oi-navy-800);
  color: #ffffff;
  box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.18);
}
.oi-steprail-item[data-state="now"] .oi-steprail-label { color: var(--oi-navy-800); }

/* The labels are the first thing to go on a narrow screen: four words
   across 320px wrap into a wall. The numbered marks still carry the
   position, and the panel's own heading names the step. */
@media (max-width: 520px) {
  .oi-steprail-label { display: none; }
}

/* `hidden` is stated explicitly rather than left to the UA sheet: this
   form is rendered inside compiled Tailwind, and a display utility on
   the same element would otherwise win over the UA default and leave a
   "hidden" step on the page. */
.oi-step-panel[hidden] { display: none; }
.oi-step-panel { animation: oi-step-in 400ms var(--oi-ease) both; }
@keyframes oi-step-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.oi-step-count {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #64748b;
  font-variant-numeric: tabular-nums;
}

/* Actions sit in one row, and the primary control is always on the
   right whether it says Continue or Generate. Back is quiet: it is a
   correction, not a choice being offered. */
.oi-wiz-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}
.oi-wiz-actions > [type="submit"] { flex: 1; }
/* Stated for the same reason as .oi-step-panel[hidden]: these buttons are
   shown and hidden by step, and a display utility from the compiled
   Tailwind sheet would otherwise beat the UA default for [hidden]. */
.oi-wiz-actions > [hidden] { display: none; }
.oi-wiz-btn {
  padding: 16px 24px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.02em;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 240ms var(--oi-ease), color 240ms var(--oi-ease),
              border-color 240ms var(--oi-ease);
}
.oi-wiz-next {
  flex: 1;
  background: var(--oi-amber);
  color: var(--oi-navy-800);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 800;
  font-size: 16px;
}
.oi-wiz-next:hover { background: var(--oi-amber-light); }
.oi-wiz-back {
  background: #ffffff;
  color: #475569;
  border-color: #cbd5e1;
}
.oi-wiz-back:hover { border-color: var(--oi-navy-800); color: var(--oi-navy-800); }

@media (prefers-reduced-motion: reduce) {
  .oi-step-panel { animation: none; }
  .oi-steprail-mark, .oi-steprail-label, .oi-steprail-item::before { transition: none; }
}

/* ── Dialog ───────────────────────────────────────────────────────
   A confirmation that arrives where the visitor is looking, instead
   of a line of text appearing under a form they have already stopped
   reading.

   Built on native <dialog>, so focus, Escape, inertness and the
   backdrop are the platform's job rather than ours. `showModal` also
   promotes it to the top layer, which is above everything including
   the chat launcher — no z-index to lose (bug-240 was exactly that
   argument, settled by DOM order).

   The element itself carries the transition, not an inner wrapper:
   `.oi-dialog` fades, `.oi-dialog-panel` is what moves. Both are
   driven by one `.is-open` class that motion.js adds a forced reflow
   after `showModal()` — a transition has nothing to start from while
   the dialog is still `display: none`.                               */
.oi-dialog {
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  /* `margin: auto` is not a tidy-up, it is what CENTRES the dialog, and
     it has to be written here because something in the stack ahead of
     this file zeroes it. A modal <dialog> is positioned by the browser's
     own `inset: 0` and centred by `margin: auto` splitting the leftover
     space between the two sides; with the margins at 0 the box obeys
     `left: 0; top: 0` instead and sits hard in the corner of the screen.
     It shipped that way on first render here, and nothing about this
     rule's own source said why. */
  margin: auto;
  /* One definite width rather than `width: 100%` with `max-width` capping
     it — the used width is then the same number the centring is solved
     against, with no clamp applied afterwards. */
  width: min(520px, calc(100vw - 32px));
  max-height: calc(100vh - 48px);
  overflow: visible;
  opacity: 0;
  transition: opacity 260ms var(--oi-ease);
}
.oi-dialog.is-open { opacity: 1; }
.oi-dialog::backdrop {
  background: rgba(3, 8, 18, 0.72);
  backdrop-filter: blur(3px);
}

.oi-dialog-panel {
  position: relative;
  padding: 36px 32px 32px;
  transform: translateY(12px) scale(0.985);
  transition: transform 320ms var(--oi-ease);
}
.oi-dialog.is-open .oi-dialog-panel { transform: none; }

.oi-dialog-title {
  font-family: 'Outfit', sans-serif;
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: #fff;
  margin: 14px 0 12px;
}
.oi-dialog-body {
  font-size: 15px;
  line-height: 1.7;
  color: var(--oi-text-soft);
  margin: 0;
}
.oi-dialog-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 26px;
}
/* The close control sits inside the panel's padding rather than over
   its corner bracket — the bracket is the ::before/::after pair that
   `.oi-bracket` owns, and a control on top of it is a control the
   visitor has to aim past the decoration to hit. */
.oi-dialog-close {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  font: inherit;
  font-size: 18px;
  line-height: 1;
  color: var(--oi-text-soft);
  background: none;
  border: 0;
  cursor: pointer;
  transition: color 180ms ease;
}
.oi-dialog-close:hover { color: var(--oi-amber); }

@media (prefers-reduced-motion: reduce) {
  .oi-dialog, .oi-dialog-panel { transition: none; }
}

/* The Services menu is twelve entries tall — 597px, measured — because GEO,
   the Intelligence Report and Your AI Team went back into it on 2026-08-14
   after coming off the front of the site that morning. That cleared a 720px
   window by 69px and overflowed a 560px one, putting the last two entries
   off the bottom of the screen with no way to reach them. The cap comes from
   the viewport rather than a fixed number, so it holds if the menu grows.

   It lives here and not in a Tailwind class because `max-h-[calc(...)]` and
   `overflow-y-auto` were both absent from the compiled stylesheet: the markup
   carried them and the browser applied nothing at all. Measured in the page,
   not assumed (rules-frontend.md, and the guard CompiledTailwindHasEveryClassTest). */
.oi-menu-panel {
  max-height: calc(100vh - 5rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* The mobile menu lives inside a position:fixed header, so it does not scroll
   with the page — whatever hangs below the fold is unreachable at every scroll
   position, not merely out of sight. Open on a 812px phone it measured 1048px
   tall, which left Contact, the phone number and the booking button permanently
   off the screen. Adding the three Beyond Compliance entries made that worse,
   which is how it was found; it was already true of the four entries below them.

   4rem is the fixed nav row above it and 3.25rem clears the 50px bottom call
   bar, so the scrollable region is exactly the gap between the two fixed
   elements. dvh second because it wins where supported and is the honest
   measure once a phone browser's own chrome is counted. */
.oi-mobile-menu {
  max-height: calc(100vh - 4rem - 3.25rem);
  max-height: calc(100dvh - 4rem - 3.25rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}
