/* ScreenQube SECTIONS — per-section layout; components live in components.css. Breakpoints are the System-Spec scale only: 620 · 720 · 960 · 1040 · 1280. */

/* ── Hero ────────────────────────────────────────────────────────────── */
.hero{
  position:relative; overflow:hidden;
  padding-block:clamp(var(--s7),6vw,var(--s9)) var(--s6);
}
/* Ambient red radial — warmth without a gradient background wash. */
.hero::before{
  content:""; position:absolute; top:-30%; left:50%;
  transform:translateX(-50%);
  width:min(1100px,130vw); height:640px; border-radius:50%;
  background:radial-gradient(closest-side,rgba(217,39,63,.13),rgba(217,39,63,0) 72%);
  pointer-events:none;
}
/* Copy left, the real world on the right, and left-aligned from here down — the page previously ran three alignment systems: centred hero, flush-left body, centred closing panel. */
.hero__grid{ display:grid; gap:var(--s7); align-items:center }

/* The hero owns the whole first screen, or the console below pushes its tilted top edge into the fold. svh, not vh: on mobile vh is the tallest possible viewport, so the section overflows the screen it is sizing to. */
@media (min-width:960px){
  .hero__grid{
    grid-template-columns:minmax(30ch,37ch) 1fr;
    gap:var(--s8);
    min-height:calc(100vh - 176px);
    min-height:calc(100svh - 176px);
  }
}

.hero__in{ position:relative; max-width:54ch }
/* Warm tint on the hero badge only — it balances the red at "Display" and stops the top of the column reading as plain white. */
.hero__in .badge{
  background:var(--red-wash);
  border-color:var(--red-soft);
  color:var(--red-deep);
  box-shadow:none;
}
.hero__in .badge::before{ box-shadow:0 0 0 3px rgba(196,30,58,.14) }
/* --t-d1 tops out at 5.4rem, sized for a full-width centred hero. In a ~40ch column "Every screen." overruns at 86px and the art-directed break collapses, so this sizes to the column. */
/* Three stacked verbs, one word per line — the line breaks are the design. The red rule down the left turns three loose words into one block that reads as a sequence, and is decorative, so a pseudo-element rather than markup. */
.hero__in .hero__h1{
  position:relative;
  margin:var(--s5) 0 var(--s4);
  padding-left:var(--s5);
  font-size:clamp(2.9rem, 6.4vw, 5rem);
  line-height:.96;
  letter-spacing:-.05em;
}
/* The rule warms as it descends — pale at Create, full red by Display, where the last verb picks the colour up. */
.hero__in .hero__h1::before{
  content:""; position:absolute; left:0; top:.09em; bottom:.09em;
  width:4px; border-radius:2px;
  background:linear-gradient(to bottom,var(--red-soft),var(--red-hot) 58%,var(--red));
}
.hero__verb{ display:block }
/* The payoff verb carries the brand colour, and is the only red word in the copy — "Anywhere." gives its red back to keep one focal point. */
.hero__verb--out{ color:var(--red) }

/* The deck and the body both hang off the rule, so the whole column has one left edge instead of two. */
.hero__deck{
  margin:0 0 var(--s4);
  padding-left:var(--s5);
  font-size:clamp(1.12rem, 1.9vw, 1.45rem);
  font-weight:700; letter-spacing:-.02em; line-height:1.3;
  color:var(--ink-2); text-wrap:balance;
}
.hero__deck .accent{ color:var(--ink) }
.hero__body{
  padding-left:var(--s5);
  max-width:46ch;
  text-wrap:balance;
}

/* Two facts under the buttons, where the eye lands after the CTA. No trial length — the period is partner-defined and stating one would be wrong. */
.hero__trust{
  display:flex; flex-wrap:wrap; gap:var(--s3) var(--s5);
  margin:var(--s5) 0 0; padding-left:var(--s5); list-style:none;
  font-size:var(--t-sm); font-weight:600; color:var(--ink-2);
}
.hero__trust li{ display:flex; align-items:center; gap:.5em }
.hero__trust svg{
  width:16px; height:16px; flex:none;
  fill:none; stroke:var(--red); stroke-width:2.6;
  stroke-linecap:round; stroke-linejoin:round;
}

@media (max-width:520px){
  .hero__in .hero__h1{
    font-size:clamp(2.6rem, 12.5vw, 3.4rem);
    padding-left:var(--s4);
  }
  .hero__deck{ padding-left:var(--s4); font-size:1.08rem }
  .hero__body,
  .hero__trust{ padding-left:var(--s4) }
}

/* The display — the hero video is presented as a screen, not a video: real bezel thickness, a panel behind glass, a specular top edge, a mount, and a red ambient wash on the wall. Two screens, wide and portrait, make "every screen, one place" literal. */
.hero__media{ margin:0; position:relative }

@media (min-width:960px){
  /* Push toward the right viewport edge: the container keeps the copy aligned with every other section while the media escapes it, which is what stops the hero reading as two boxes side by side. */
  .hero__media{
    /* Escape the container to 24px off the viewport edge at any width — the wrap's content edge sits at half the leftover gutter plus its own padding, and the fixed negative margin this replaced only worked at one width. */
    margin-right:calc(-1 * (max(0px, (100vw - var(--max)) / 2) + var(--pad) - 24px));
    margin-left:var(--s6);
    padding-bottom:var(--s9);
  }
}

/* The wall — a positioning context for the pair. It briefly carried a 3.4° rotateY, removed after measuring: any 3D rotation puts this subtree on Chrome's 3D raster path, which rasterises below DPR and turned the arrivals-board text to mush at 1024/2x. */
.screens{ position:relative }

/* The hardware — a 6px bezel with concentric radii (outer 18, inner 12): a frame whose curves disagree is the tell that separates a drawn screen from a real one. It hangs on the wall, so a tight contact shadow then two long soft ones. */
/* The bezel, defined once. Three mocks used to draw their own — .screen milled, .laptop__screen and .phone a flat border. The chamfer is four separate inset edges, not one even ring, because a milled edge catches light hardest on top and goes dark underneath. */
:root{
  --bezel-face:
    radial-gradient(130% 90% at 50% 0%, rgba(255,255,255,.07), transparent 58%),
    linear-gradient(178deg, var(--bezel-hi) 0%, var(--bezel-mid) 40%, var(--bezel-lo) 100%);
  --bezel-chamfer:
    0 1px 0 rgba(255,255,255,.22) inset,
    1px 0 0 rgba(255,255,255,.05) inset,
    -1px 0 0 rgba(255,255,255,.05) inset,
    0 -1px 0 rgba(0,0,0,.65) inset;
  --bezel-lift:
    0 6px 14px -8px rgba(25,20,22,.45),
    0 34px 54px -28px rgba(25,20,22,.50),
    0 90px 100px -56px rgba(25,20,22,.38);
}

.screen{
  position:relative;
  /* Paired with the media by hand and must stay that way: a url() inside a custom property resolves against the stylesheet that CONSUMES it, so setting this inline in index.html silently 404'd to assets/css/assets/media/…. */
  --ambient:url('../media/venue/glow-wide.png');
  border-radius:18px;
  padding:6px;
  /* The face is not one flat black — room light falls from above, so the top is warm graphite and the bottom nearly black. Flat #000 is what makes a drawn bezel read as plastic. */
  background: var(--bezel-face);
  box-shadow: var(--bezel-chamfer), var(--bezel-lift);
}
/* The wall catches the light the screen throws, in two layers; underneath is the warm red core that keeps a pale wall from reading as a grey studio backdrop. */
/* Sized in percentages of the glow box, not closest-side, which on a 1400px screen puts the whole gradient behind the panel — the reason the old bloom was invisible at desktop widths. Pushed below centre, because light gathers under a hung screen. */
.screen::before{
  content:""; position:absolute; z-index:-1;
  inset:-24% -18% -34%;
  background:
    radial-gradient(58% 52% at 50% 64%, rgba(217,39,63,.20), transparent 72%),
    radial-gradient(78% 68% at 50% 56%, rgba(255,168,128,.17), transparent 74%);
  filter:blur(26px);
  pointer-events:none;
}
/* The picture thrown far out of focus, so the spill answers to what is playing. The blur is BAKED: --ambient is a 24x14 png and upscaling it is a free blur, with the saturation and lift baked in too. Computed, blur(56px) plus a blend mode cost 26 dropped frames against 5 over a 60-notch scroll. */
.screen::after{
  content:""; position:absolute; z-index:-1;
  /* Kept off the bottom on purpose: the caption sits directly under the screen, and soft-light from footage this dark eats its contrast. */
  inset:-7% -8% -10%;
  border-radius:50%;
  background-image:var(--ambient);
  background-size:cover; background-position:center;
  /* brightness above 1 lifts the frame's mid-greys past the soft-light midpoint so the layer tints the wall upward instead of bruising it. Saturation is held at 1.7 — any harder and the arrivals board throws a violet bloom over Ember's warm ground. */
  filter:blur(14px);
  opacity:.42;
  pointer-events:none;
}
/* This layer is deliberately STATIC. Breathing .26 → .38 over 9s cost the hero video half its frame rate — an opacity animation on a blurred, blended element cannot go to the compositor, so Chrome re-rasterised every frame: 33.3ms median against 16.7ms. Do not re-add it. */

/* The outset shadow is the panel's own light bleeding onto the bezel, which is what tells the eye the screen is ON; the inset hairline is the air gap where the glass drops behind the frame. */
/* --reel lives on the ancestor the picture and the progress bar share, because the bar is only honest while the two run on one clock. */
.screen__panel{
  position:relative;
  --reel:16s;
  border-radius:12px; overflow:hidden;
  background:var(--ink);
  box-shadow:
    0 0 0 1px rgba(0,0,0,.7) inset,
    0 0 16px 1px rgba(255,235,225,.09);
}
/* 16:9, the ratio the footage was shot at and a display is sold in — 16:10 cropped a band off the top and bottom of every frame. */
.screen__panel video,.screen__panel img{
  display:block; width:100%;
  aspect-ratio:16 / 9; object-fit:cover;
}

/* The playlist. The video is the base layer and never moves; the still dissolves over it, so no frame exposes the dark panel the way cross-fading layers would. */
.screen__reel{ position:relative }
.screen__item + .screen__item{
  position:absolute; inset:0; height:100%;
  opacity:0;
  animation:reel-advance var(--reel) var(--e) infinite;
}
@keyframes reel-advance{
  0%,46%{ opacity:0 }
  50%,96%{ opacity:1 }
  100%{ opacity:0 }
}

/* Glass, and the last thing painted — a reflection sits on the OUTSIDE of the screen. Two parts: a wide shallow window across the top-left, and the corner falloff every real panel has. */
.screen__panel::after{
  content:""; position:absolute; inset:0; z-index:3; pointer-events:none;
  background:
    linear-gradient(104deg,
      rgba(255,255,255,0)   6%,
      rgba(255,255,255,.09) 20%,
      rgba(255,255,255,.03) 33%,
      rgba(255,255,255,0)   45%),
    radial-gradient(125% 105% at 50% 42%, transparent 56%, rgba(0,0,0,.20));
}

/* On-screen playback: three segments filling in turn under a short scrim — a playlist running, rather than a caption claiming it. No counts, no durations, nothing asserting a number. */
.screen__osd{
  position:absolute; left:0; right:0; bottom:0; z-index:2;
  display:flex; gap:11px;
  padding:38px 14px 12px;
  background:linear-gradient(to top, rgba(10,7,8,.62), rgba(10,7,8,.18) 55%, rgba(10,7,8,0));
  pointer-events:none;
}
.screen__seg{
  position:relative; flex:1; height:3px; overflow:hidden;
  border-radius:var(--r-pill);
  background:rgba(255,255,255,.30);
}
/* A finished item goes quiet and only the one on air stays red — two adjacent full-red segments merge into one bar and read as a single clip half-played. */
.screen__seg::after{
  content:""; position:absolute; inset:0;
  border-radius:inherit;
  background-color:var(--red-hot);
  transform:scaleX(0); transform-origin:left;
  animation:osd-fill var(--reel) linear infinite;
}
.screen__seg:nth-child(2)::after{ animation-delay:calc(var(--reel) / 2) }
@keyframes osd-fill{
  0%       { transform:scaleX(0); background-color:var(--red-hot) }
  50%      { transform:scaleX(1); background-color:var(--red-hot) }
  57%,100% { transform:scaleX(1); background-color:rgba(255,255,255,.66) }
}

/* The second screen, portrait, overlapping the first. A still, not video: three clips already autoplay on this page. Lit and shadowed harder so it reads as nearer — without that occlusion the two look like flat cut-outs stacked in a layers panel. */
.screen--sub{
  display:none;
  --ambient:url('../media/venue/glow-portrait.png');
  position:absolute; left:-7%; bottom:-13%; z-index:2;
  width:25%;
  border-radius:15px; padding:5px;
  box-shadow:
    0 1px 0 rgba(255,255,255,.22) inset,
    1px 0 0 rgba(255,255,255,.05) inset,
    -1px 0 0 rgba(255,255,255,.05) inset,
    0 -1px 0 rgba(0,0,0,.65) inset,
    0 4px 10px -5px rgba(25,20,22,.5),
    /* Cast to the right, onto the panel behind: the light comes from above and left, so a purely downward drop would leave the two looking pasted rather than stacked in depth. */
    26px 30px 52px -22px rgba(20,14,16,.62),
    0 26px 44px -18px rgba(25,20,22,.45),
    0 50px 70px -40px rgba(25,20,22,.40);
}
.screen--sub .screen__panel{ border-radius:10px }
.screen--sub .screen__panel img{ aspect-ratio:9 / 16 }
.screen--sub::before{ inset:-20% -24% -22%; filter:blur(18px); opacity:.75 }
/* No breathing on this one: it is showing a still, and a still whose spill pulses is a tell rather than a detail. */
.screen--sub::after{
  inset:-8% -12% -8%; filter:blur(10px);
  opacity:.4;
}
@media (min-width:960px){
  .screen--sub{
    display:block;
    /* --tx/--ty are written by initScreenTilt() in ui.js in degrees and default to 0, so this is inert with no JS and under reduced motion. Kept to ±5°: past about six the panel resamples visibly. */
    transform:
      perspective(900px)
      rotateY(var(--tx, 0deg))
      rotateX(var(--ty, 0deg));
    transform-style:preserve-3d;
    transition:transform 400ms var(--e);
  }
  /* While the pointer is moving, drop the easing so the panel tracks the cursor instead of lagging behind it — the transition only exists to glide back to rest on leave. */
  .screens.is-tilting .screen--sub{ transition:none }
}

/* The state pill. Only the 404 slate uses it now — the playing screens carry no badge. */
.screen__live{
  position:absolute; left:14px; top:14px; z-index:3;
  display:inline-flex; align-items:center; gap:7px;
  padding:5px 12px 5px 9px;
  border-radius:var(--r-pill);
  background:rgba(18,14,16,.62);
  box-shadow:
    0 0 0 1px rgba(255,255,255,.14) inset,
    0 2px 10px -2px rgba(0,0,0,.4);
  font-size:var(--t-lab); font-weight:700; letter-spacing:.09em;
  text-transform:uppercase; color:var(--white);
}
.screen__live::before,
.screen__dot::before{
  content:""; width:7px; height:7px; border-radius:50%; flex:none;
  background:var(--red-hot);
  animation:live-pulse 2s var(--e) infinite;
}
/* The portrait screen's pill reduced to the dot alone, pulsing off the beat of the wide screen's — two devices on their own clocks, not one animation driving both. */
.screen__dot{
  position:absolute; left:11px; top:11px; z-index:3;
  display:inline-flex; align-items:center; justify-content:center;
  width:21px; height:21px;
  border-radius:var(--r-pill);
  background:rgba(18,14,16,.62);
  box-shadow:
    0 0 0 1px rgba(255,255,255,.14) inset,
    0 2px 10px -2px rgba(0,0,0,.4);
}
.screen__dot::before{ animation-delay:-.9s }
@keyframes live-pulse{
  0%,100%{ opacity:1; box-shadow:0 0 0 0 rgba(217,39,63,.6) }
  60%{ opacity:.55; box-shadow:0 0 0 6px rgba(217,39,63,0) }
}
/* Everything that moves here is decoration. Frozen it still reads: the glow holds, the reel rests on its first item, and the playlist sits part-run — a coherent single moment. */
@media (prefers-reduced-motion: reduce){
  .screen__live::before,
  .screen__dot::before,
  .screen__item + .screen__item,
  .screen__seg::after{ animation:none }
  .screen__item + .screen__item{ opacity:0 }
  .screen__seg:nth-child(1)::after{ transform:scaleX(.62) }
}

.screen__caption{
  margin-top:var(--s5);
  font-size:var(--t-xs); color:var(--ink-2);
}
@media (min-width:960px){
  .screen__caption{ margin-left:22%; max-width:44ch }  /* clears the portrait screen */
}

/* Big video — the competitor's equivalent is a YouTube embed: hundreds of KB, third-party cookies, and visitor data sent to Google before anyone presses play. Ours is one self-hosted clip, poster-first, and keeps the strict CSP intact. */
.reelbig{
  position:relative;
  border-radius:var(--r-2xl); overflow:hidden;
  border:1px solid var(--line);
  box-shadow:var(--sh-4);
  background:var(--ink);
}
.reelbig video,.reelbig img{
  display:block; width:100%;
  aspect-ratio:16 / 9; object-fit:cover;
}
.reelbig figcaption{
  position:absolute; inset:auto 0 0 0;
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--s3) var(--s5);
  padding:var(--s8) var(--s6) var(--s5);
  font-size:var(--t-sm); font-weight:600; color:var(--white);
  background:linear-gradient(180deg,rgba(25,20,22,0),rgba(25,20,22,.78));
}
.reelbig figcaption .xs{ color:rgba(255,255,255,.72); max-width:46ch }

/* The stage — the product leans back then stands up as you scroll, on the shared rAF engine rather than framer-motion. --p defaults to 1 on the elements, so with JS absent everything rests in its FINAL state rather than frozen mid-animation. */
.hero__in{ --p:1 }

/* data-scroll sits on .hero__in, NOT .hero: writing a custom property invalidates the element and its whole subtree, and on .hero that re-styled the video, both mockups, the reel and the pills every frame to move one text column — 156ms of recalc against 78ms stubbed. */

/* A gentle counter-drift against the console rising below it. Kept small: beside a static video column, anything larger reads as a glitch. */
.hero__in{ transform:translateY(calc((1 - var(--p,1)) * -32px)) }

/* The console and venue strip are ONE composition in ONE pinned screen. The rotation is a one-shot transition, NOT scroll-scrubbed — scrubbing ran backwards on scroll-up, froze mid-rotation, and needed a long runway. .console 125vh against .console__pin 100vh: that 25vh is the whole scroll cost. */
.console{
  /* Carries the sand band so grounds keep alternating: hero → console → why. NOT via .plate, whose padding-block adds on top of the runway, and NOT overflow:hidden, which would make this a scroll container and pin the sticky child against the section instead of the viewport. */
  background:var(--sand);
  position:relative;
}
.console__pin{ padding-inline:var(--pad) }

@media (min-width:960px){
  .console{ height:125vh }
  .console__pin{
    position:sticky; top:0;
    height:100vh;
    /* The floating nav sits over the top of this box; without the inset the frame centres behind it. Measured, not guessed: the bar's bottom edge is at 79px. */
    padding-top:104px;
    display:flex; flex-direction:column;
    align-items:center; justify-content:center;
    gap:clamp(var(--s4),2.2vh,var(--s6));
    perspective:1800px;
  }
}

/* Legible from the first frame — it is the section's heading, not an effect. Fading it from zero left the top half of the pinned screen empty at the moment the reader arrives. */
.console__title{
  /* --t-h1, not --t-d2: every pixel this heading takes is a pixel the console below it does not get, and the console is the point of the section. */
  font-size:var(--t-h1); font-weight:800; letter-spacing:-.035em;
  text-align:center; text-wrap:balance;
  transition:translate 700ms var(--e);
}
.js .console__title{ translate:0 14px }
.console.is-in .console__title{ translate:0 0 }

/* The strip shares a screen with the console, so shorter cards and a tighter caption. As a flex item it needs align-self:stretch and its own bleed maths — the .wrap escape formula assumed a 1280 container and overshoots here. */
.console .reel{
  align-self:stretch;
  width:auto;
  margin-inline:calc(-1 * var(--pad));
}
.console .reel img{ height:clamp(112px, 13vh, 150px) }
.console .reel figcaption{
  padding:var(--s2) var(--s3);
  font-size:var(--t-lab); letter-spacing:0; text-transform:none;
}

/* Cropped to 1904×700 of the full 1904×1064. Nothing is sliced — Settings ends around y=635 — and what comes off is the dead white of an empty test account, which is what lets the venue strip share the screen. */
.console .frame__shot img{
  aspect-ratio:1904 / 700;
  height:auto;
  object-fit:cover; object-position:top center;
}

/* Capped on BOTH axes against what the pin can spare. These are UNTRANSFORMED layout boxes: flex lays the frame out flat and only then rotates the pixels, so sizing by eye overflowed the pinned box by ~58px and shoved the heading behind the nav. */
.stage__frame{
  width:min(94vw, calc(272vh - 1230px));
  /* Rotate about the centre, not the base: pivoting at the bottom edge swings the face downward out of the pinned box, and flex centring cannot see that the layout box never moved. */
  transform-origin:50% 50%;
  /* Both states written out in full rather than computed from a custom property — a transform reading an unregistered custom property does not interpolate, so it swaps abruptly at the end of the duration. */
  transition:transform 900ms var(--e);
  /* Layered, warm-tinted, and deep — the depth is what sells the lift. */
  box-shadow:
    0 9px 20px rgba(25,20,22,.10),
    0 37px 37px rgba(25,20,22,.09),
    0 84px 50px rgba(25,20,22,.06),
    0 149px 60px rgba(25,20,22,.03),
    0 233px 65px rgba(25,20,22,.01);
  border-radius:var(--r-xl);
}
/* The leaning start state is guarded by .js, like .rv in base.css: no script, no lean. Nested in the desktop query because .js raises specificity to (0,2,0) and unnested it outranked the (0,1,0) mobile override, leaving every phone permanently tilted. */
@media (min-width:960px){
  .js .stage__frame{
    transform:
      perspective(1800px)
      rotateX(34deg)
      scale(.86);
  }
}
/* Lands on `none`, not rotateX(0deg), which keeps the element on the 3D compositing path forever — a promoted layer re-composited every scroll frame for nothing. Measured: 25% dropped frames against 15% with the transform gone. */
.console.is-in .stage__frame{ transform:none }
.stage__glow{
  position:absolute; left:8%; right:8%; bottom:6vh;
  height:120px; border-radius:50%;
  background:radial-gradient(closest-side,rgba(196,30,58,.26),transparent 70%);
  transition:background 900ms var(--e);
  z-index:-1; pointer-events:none;
}
.js .stage__glow{ background:radial-gradient(closest-side,rgba(196,30,58,.12),transparent 70%) }
.console.is-in .stage__glow{
  background:radial-gradient(closest-side,rgba(196,30,58,.26),transparent 70%);
}

/* Below 960 there is no pin and no runway: a 34° rotation at phone width turns the screenshot to mush, and 220vh of sticky on a touch device is a long stretch of nothing. */
@media (max-width:959.98px){
  .console{ height:auto; padding-block:var(--s7) }
  .console__pin{ position:static; height:auto; perspective:none; display:block }
  .console__title{ opacity:1; translate:none; margin-bottom:var(--s5) }
  .stage__frame{ width:100%; transform:none }
  .stage__glow{ display:none }
}
/* Same reasoning by preference rather than width: keep the pin, which is layout, but let nothing move within it. */
@media (prefers-reduced-motion: reduce){
  .hero__in,.stage__frame,.js .stage__frame{ transform:none }
  .screen--sub{ transform:none !important }
  .console{ height:auto }
  .console__pin{ position:static; height:auto }
  .console__title{ opacity:1; translate:none }
}

/* ── Marquee — an ambient loop. One per page, and never a carousel. ──── */
.marquee{
  overflow:hidden;
  padding-block:var(--s5);
  -webkit-mask-image:linear-gradient(90deg,transparent,black 12%,black 88%,transparent);
          mask-image:linear-gradient(90deg,transparent,black 12%,black 88%,transparent);
}
.marquee__track{
  display:flex; gap:var(--s7); width:max-content;
  animation:marquee-march 34s linear infinite;
}
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track{ animation-play-state:paused }
.marquee__item{
  display:inline-flex; align-items:center; gap:var(--s3);
  font-size:var(--t-sm); font-weight:600; color:var(--ink-2);
  white-space:nowrap;
}
.marquee__item::before{
  content:""; width:6px; height:6px; border-radius:50%;
  background:var(--red); flex:none;
}
@keyframes marquee-march{ to{ transform:translateX(-50%) } }

/* Under reduced motion the track stops and the container becomes a manual scroller. It carries tabindex="0" unconditionally, or the accommodation hides content rather than revealing it. */
@media (prefers-reduced-motion: reduce){
  .marquee{ overflow-x:auto; -webkit-mask-image:none; mask-image:none }
  .marquee__track{ animation:none }
}

/* Venue reel — a fixed-height strip so portrait and landscape frames sit on one baseline and the varying widths read as a film strip rather than a broken grid. These are customers' venues, never ours. */
/* Full-bleed to the viewport edge. Held inside the wrap it was only as wide as the text column and the two end cards sat permanently half-dissolved under the edge mask. Same escape maths as .hero__media. */
.reel{
  overflow:hidden;
  margin-inline:calc(-1 * (max(0px, (100vw - var(--max)) / 2) + var(--pad)));
  padding-block:var(--s2);
}
/* No padding on this track, ever: the loop is translateX(-50%) across a list duplicated once, so any inline padding shifts the halfway point off the seam and the strip jumps each cycle. */
.reel__track{
  display:flex; gap:var(--s4); width:max-content;
  animation:marquee-march 46s linear infinite;
}
/* Runs without stopping, including under the pointer — asked for directly. WCAG 2.2.2 cover is now keyboard focus (:focus-within via tabindex="0") plus prefers-reduced-motion; a pointer user has no way to halt it, and a discoverable control is the only thing that properly closes it. Noted in the README. */
.reel:focus-within .reel__track{ animation-play-state:paused }
/* Uniform tile width: sizing each figure to its own image left portrait frames narrower than their captions, so the figure stretched and the image sat against dead white. */
.reel figure{
  flex:none; margin:0;
  width:clamp(230px,26vw,320px);
  background:var(--white);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  box-shadow:var(--sh-2);
  overflow:hidden;
}
.reel img{
  display:block; width:100%;
  height:clamp(170px,20vw,230px);
  object-fit:cover; object-position:center 35%;
}
.reel figcaption{
  font-size:var(--t-xs); font-weight:600; color:var(--ink-2);
  padding:var(--s3) var(--s4);
  border-top:1px solid var(--line);
}
@media (prefers-reduced-motion: reduce){
  .reel{ overflow-x:auto }
  .reel__track{ animation:none }
}

/* ── Tier cards — the three consoles ─────────────────────────────────── */
.tiers{ display:grid; gap:var(--s4); grid-template-columns:1fr }
@media (min-width:720px){ .tiers{ grid-template-columns:repeat(3,1fr) } }
.tier{ display:flex; flex-direction:column; padding:0; overflow:hidden }
.tier__shot{
  border-bottom:1px solid var(--line);
  background:var(--sand);
  overflow:hidden;
}
.tier__shot img{
  width:100%; display:block;
  aspect-ratio:16 / 7; object-fit:cover; object-position:top left;
}
.tier__body{ padding:var(--s5); display:flex; flex-direction:column; flex:1 }
.tier__role{
  font-size:var(--t-lab); font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--red);
}
.tier h3{ margin:var(--s2) 0 }
.tier__figure{
  margin-top:auto; padding-top:var(--s4);
  border-top:1px solid var(--line);
  display:flex; align-items:baseline; gap:var(--s3);
}
.tier__figure .figure{ font-size:clamp(1.8rem,3vw,2.4rem) }
/* Its own class, not `.tier__figure span` — that was (0,1,1), out-ranked `.figure` (0,1,0) and painted the gradient number opaque. Never qualify a descendant selector by element when a utility class has to win inside it. */
.tier__unit{ font-size:var(--t-xs); color:var(--ink-2) }

/* ── Numbers band ────────────────────────────────────────────────────── */
.stats{ display:grid; gap:var(--s4); grid-template-columns:1fr }
@media (min-width:620px){ .stats{ grid-template-columns:repeat(3,1fr) } }

/* ── Scroll rotation — makes a flat screenshot feel like an object ───── */
/* Sits on a white ground: surfaces lift by being whiter than the page behind them, and blush-on-blush gave the stage no edge at all. */
.rotator{
  perspective:1400px;
  display:grid; place-items:center;
  padding:clamp(var(--s5),3vw,var(--s6));
  background:var(--white);
  border-radius:var(--r-2xl);
  border:1px solid var(--line);
  box-shadow:var(--sh-2);
}
.rotator__card{
  width:min(100%,880px);
  /* Swing stays under 40°: past that the screenshot stops being readable. */
  transform:
    perspective(1400px)
    rotateY(calc((.5 - var(--p,.5)) * 30deg))
    rotateX(calc(var(--p,.5) * 8deg - 4deg))
    scale(calc(.94 + var(--p,.5) * .06));
}
@media (prefers-reduced-motion: reduce){
  .rotator__card{ transform:none }
}

/* Annotated screenshot — the product flat and sharp with numbered markers, and the same numbers spelled out beneath. The markers are DECORATIVE and the list carries the words: hover-revealed pill labels put the content behind hover, needed a separate list on touch anyway, and sat on the values they pointed at. */
.showcase{ position:relative }
/* Caps the mock at a normal browser-window width instead of the full content column — a screenshot that wide reads as a banner, not a window. */
.showcase--compact{ max-width:840px; margin-inline:auto }
.showcase--compact .hotspot__list{ max-width:840px; margin-inline:auto }
/* This is a video, not an image with hotspot dots, so the dot-alignment insets and centred layout below do not apply — and the billing-cycle caption wraps raggedly once centred into four narrow columns. */
@media (min-width:1040px){
  .showcase--compact .hotspot__list{
    grid-template-columns:repeat(2,1fr);
    column-gap:var(--s7);
    row-gap:var(--s4);
    padding-inline:0;
  }
  .showcase--compact .hotspot__list li{
    flex-direction:row;
    align-items:flex-start;
    text-align:left;
    gap:var(--s3);
  }
}
.hotspot{ position:relative; display:block }
.hotspot img{ width:100%; display:block }
.hotspot__dot{
  position:absolute;
  left:var(--x); top:var(--y);
  translate:-50% -50%;
  width:30px; height:30px;
  display:grid; place-items:center;
  border-radius:50%;
  background:var(--grad); color:var(--white);
  border:2px solid var(--white); box-shadow:var(--sh-red);
  font-size:var(--t-lab); font-weight:800; line-height:1;
}
.hotspot__dot::after{
  content:""; position:absolute; inset:-5px; border-radius:50%;
  border:2px solid var(--red); opacity:.5;
  /* Three pulses, then it rests: an endless ring reads as restless rather than as a marker calling attention once. */
  animation:hotspot-ping 2.4s var(--e) 3;
  animation-fill-mode:forwards;
}
@keyframes hotspot-ping{
  0%{ transform:scale(.85); opacity:.55 }
  70%,100%{ transform:scale(1.65); opacity:0 }
}
@media (prefers-reduced-motion: reduce){
  .hotspot__dot::after{ animation:none; opacity:.3 }
}
/* Markers are hidden below 720px, where they would sit on top of each other. The list carries everything there. */
@media (max-width:719.98px){
  .hotspot__dot{ display:none }
}

/* The caption row is inset to match the tile grid inside the screenshot, so each number lands under its own marker. The insets come from the tiles' position in the 1904px capture: first at 287px (15%), last ending at 1878px (98.6%). */
.hotspot__list{
  display:grid; gap:var(--s4);
  margin-top:var(--s5); padding:0; list-style:none;
}
@media (min-width:720px){
  .hotspot__list{ grid-template-columns:repeat(2,1fr); column-gap:var(--s7) }
}
/* Solved from the tiles' own geometry in the 1904px capture — first at 287px, last ending 1878px, each 367px wide with a 41px gutter — so with a 16px column gap each column's CENTRE lands on its tile's centre. */
@media (min-width:1040px){
  .hotspot__list{
    grid-template-columns:repeat(4,1fr);
    column-gap:var(--s4);
    padding-inline:14.6% 1.1%;
  }
}
.hotspot__list li{
  display:flex; gap:var(--s3); align-items:flex-start;
  font-size:var(--t-sm); color:var(--ink-2);
}
.hotspot__list b{
  flex:none; display:grid; place-items:center;
  width:26px; height:26px; border-radius:50%;
  background:var(--grad); color:var(--white);
  font-size:var(--t-lab); font-weight:800;
}

/* Stacked and centred so the number sits on its column's centre line. Must come AFTER the base `li` rule: same specificity, so source order decides. */
@media (min-width:1040px){
  .hotspot__list li{
    flex-direction:column; align-items:center;
    text-align:center; gap:var(--s2);
  }
}

/* Device frames — the console on a laptop, venue footage on a phone, CSS-drawn with no image assets. Used once on the site; repeated device frames read as a template. */
.devices{
  display:flex; align-items:flex-end; justify-content:center;
  gap:clamp(var(--s4),4vw,var(--s7));
  flex-wrap:wrap;
  padding:clamp(var(--s6),5vw,var(--s8)) clamp(var(--s5),4vw,var(--s7));
  background:linear-gradient(180deg,var(--sand),var(--page));
  border:1px solid var(--line);
  border-radius:var(--r-2xl);
}
.laptop{ width:min(100%,560px); flex:1 1 340px }
/* padding, not border: a border paints one flat colour and the bezel is a gradient. Same trick .screen uses. */
.laptop__screen{
  border-radius:18px 18px 0 0; overflow:hidden;
  padding:6px 6px 0;
  background:var(--bezel-face);
  box-shadow:var(--bezel-chamfer), var(--bezel-lift);
}
/* Inner radius = outer 18 minus the 6 of bezel, as on .screen — a square picture inside a rounded frame is the tell that it was drawn. */
.laptop__screen img{
  width:100%; display:block; border-radius:12px 12px 0 0;
  aspect-ratio:1904 / 660; object-fit:cover; object-position:top center;
}
.laptop__base{
  height:12px; background:var(--ink);
  border-radius:0 0 var(--r-sm) var(--r-sm);
  position:relative; box-shadow:var(--sh-2);
}
.laptop__base::after{
  content:""; position:absolute; left:50%; top:0;
  transform:translateX(-50%);
  width:72px; height:4px; background:rgba(255,255,255,.22);
  border-radius:0 0 var(--r-pill) var(--r-pill);
}
.phone{
  width:min(42%,170px); flex:0 0 auto;
  border-radius:26px; overflow:hidden;
  padding:7px;
  background:var(--bezel-face);
  box-shadow:var(--bezel-chamfer), var(--bezel-lift);
  position:relative;
}
.phone::before{
  content:""; position:absolute; left:50%; top:7px;
  transform:translateX(-50%); z-index:2;
  width:40px; height:5px; border-radius:var(--r-pill);
  background:rgba(255,255,255,.32);
}
/* Outer 26 minus the 7 of bezel. */
.phone video,.phone img{
  width:100%; display:block; border-radius:19px;
  aspect-ratio:608 / 1080; object-fit:cover;
}

/* A play control appears only when autoplay has been suppressed — under reduced motion, or when the browser refuses it. */
.video-note{ margin-top:var(--s4); text-align:center }

/* ── Three steps ─────────────────────────────────────────────────────── */
.steps{ display:grid; gap:var(--s4); grid-template-columns:1fr; padding:0; list-style:none }
@media (min-width:720px){ .steps{ grid-template-columns:repeat(3,1fr) } }
.step{ position:relative }
.step__n{
  display:inline-grid; place-items:center;
  width:34px; height:34px; border-radius:var(--r-sm);
  background:var(--grad); color:var(--white);
  font-size:var(--t-xs); font-weight:800;
  box-shadow:var(--sh-red);
  margin-bottom:var(--s4);
}

/* ── Mechanism cards — how pricing actually works ────────────────────── */
.mechanism{ display:grid; gap:var(--s4); grid-template-columns:1fr }
@media (min-width:720px){ .mechanism{ grid-template-columns:repeat(2,1fr) } }
.mechanism .tile{ display:flex; flex-direction:column }
.mechanism__cat{
  font-size:var(--t-lab); font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--ink-2);
  margin-bottom:var(--s2);
}
.tile--dark .mechanism__cat{ color:var(--red-soft) }
.mechanism__lead{ font-size:var(--t-h2); font-weight:700; letter-spacing:-.025em; margin-bottom:var(--s4) }

/* Caption sitting under a full-bleed band. */
.band-note{ margin-top:var(--s5) }

.spec-list{ display:grid; gap:var(--s3); margin-top:auto; padding-top:var(--s5) }
.spec-list li{
  position:relative; padding-left:var(--s5);
  font-size:var(--t-sm); color:var(--ink-2);
}
.spec-list li::before{
  content:""; position:absolute; left:0; top:.5em;
  width:10px; height:10px; border-radius:50%;
  background:var(--red-wash); box-shadow:inset 0 0 0 2px var(--red);
}
.tile--dark .spec-list li{ color:rgba(255,255,255,.78) }
.tile--dark .spec-list li::before{ background:rgba(255,255,255,.14); box-shadow:inset 0 0 0 2px var(--red-soft) }

/* ── Split row — copy one side, product the other ────────────────────── */
.split{ display:grid; gap:var(--s6); grid-template-columns:1fr; align-items:center }
/* a 1fr track will not shrink below its content's min-content width; without this the media column widened the row past the viewport */
.split > *{ min-width:0 }
@media (min-width:960px){
  .split{ grid-template-columns:1fr 1.15fr; gap:var(--s8) }
  .split--flip .split__copy{ order:2 }
}
.split + .split{ margin-top:clamp(var(--s8),7vw,var(--s10)) }
.split__media img{ width:100% }

/* Sticky feature section — the pinned column is an enhancement over a plain stacked list. Every step carries its own inline image, which is what shows on mobile and without JS; the pinned column is aria-hidden because it duplicates them. */
.sticky{ display:grid; gap:var(--s6); grid-template-columns:1fr }
.sticky__media{ display:none }
.sticky__steps > * + *{ margin-top:var(--s7) }
.sticky__inline{ margin-top:var(--s5) }

@media (min-width:960px){
  .js .sticky{ grid-template-columns:1fr 1fr; gap:var(--s8) }
  .js .sticky__media{
    display:block; position:sticky; align-self:start;
    top:calc(var(--s9) + var(--s3));
  }
  .js .sticky__inline{ display:none }
  .js .sticky__steps > * + *{ margin-top:clamp(var(--s9),18vh,var(--s10)) }
  .js .sticky__steps > *:first-child{ padding-top:var(--s7) }
  .js .sticky__steps > *:last-child{ padding-bottom:var(--s8) }
}

.sticky__stack{ position:relative }
.sticky__stack [data-shot]{
  transition:opacity 450ms var(--e);
}
.sticky__stack [data-shot].is-off{
  opacity:0; position:absolute; inset:0; pointer-events:none;
}

/* ── Comparison table ────────────────────────────────────────────────── */
.table-wrap{ border-radius:var(--r-lg); border:1px solid var(--line); background:var(--white) }
/* Only a scroll container where the table needs one: an `overflow` ancestor makes position:sticky resolve against THAT box rather than the viewport, so applying it unconditionally kills the sticky compare header on every desktop. */
@media (max-width:719px){ .table-wrap{ overflow-x:auto } }
.table{ min-width:34rem; font-size:var(--t-sm) }
.table th,.table td{ padding:var(--s4) var(--s5); text-align:left; border-bottom:1px solid var(--line) }
.table thead th{
  font-size:var(--t-lab); font-weight:700; text-transform:uppercase;
  letter-spacing:.08em; color:var(--ink-2); background:var(--sand);
}
.table tbody th{ font-weight:600; color:var(--ink) }
.table td{ color:var(--ink-2); font-variant-numeric:tabular-nums }
.table tr:last-child th,.table tr:last-child td{ border-bottom:0 }

/* ── Form ────────────────────────────────────────────────────────────── */
.form{ display:grid; gap:var(--s4) }
@media (min-width:620px){ .form__pair{ display:grid; grid-template-columns:1fr 1fr; gap:var(--s4) } }
.field{ display:grid; gap:var(--s2) }
.field label{ font-size:var(--t-sm); font-weight:600; color:var(--ink) }
/* --line-2 measures 1.3:1 on white and cannot carry a control boundary (WCAG 1.4.11 needs 3:1); --line-3 exists for exactly this. */
.field input,.field textarea,.field select{
  width:100%; min-height:48px;
  padding:var(--s3) var(--s4);
  background:var(--white);
  border:1px solid var(--line-3);
  border-radius:var(--r-sm);
  font-size:var(--t-sm);
  transition:border-color var(--dur) var(--e), box-shadow var(--dur) var(--e);
}
.field textarea{ min-height:120px; resize:vertical }
.field input:hover,.field textarea:hover,.field select:hover{ border-color:var(--ink-2) }
.field input[aria-invalid="true"],.field textarea[aria-invalid="true"]{ border-color:var(--red); border-width:2px }
.field__hint{ font-size:var(--t-xs); color:var(--ink-2) }

/* Required marker, aria-hidden because the input already carries `required` — reading "star" after each label would be noise, not information. */
.field__req{ color:var(--red); margin-left:2px; font-weight:700 }

/* Deployment comparison — two cards, deliberately NOT identical: one option ships and one is unverified, and matching treatment would flatten the difference the block exists to show. The unestablished one stays flat on a dashed border. */
.deploy{ display:grid; gap:var(--s5); grid-template-columns:1fr }
@media (min-width:860px){
  .deploy{ grid-template-columns:repeat(2,1fr); align-items:start }
}
.deploy__card{
  display:flex; flex-direction:column;
  padding:clamp(var(--s5),4vw,var(--s6));
  border-radius:var(--r-lg);
}
.deploy__card--live{
  background:var(--white); border:1px solid var(--line-2); box-shadow:var(--sh-2);
}
.deploy__card--unknown{
  background:transparent; border:1px dashed var(--line-3);
}
.deploy__card h3{ margin:var(--s3) 0 var(--s2) }
.deploy__card .cta-pair{ margin-top:auto; padding-top:var(--s5) }

.deploy__badge{
  display:inline-flex; align-items:center; gap:var(--s2); align-self:flex-start;
  margin:0; padding:6px 12px; border-radius:var(--r-pill);
  font-size:var(--t-xs); font-weight:700; letter-spacing:.01em;
}
.deploy__badge svg{ width:14px; height:14px; fill:none; stroke:currentColor; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round }
/* State is carried by the icon and the words as well as the fill — a reader who cannot separate the two greens still gets "Available today". */
.deploy__badge--live{ background:var(--red-wash); color:var(--red) }
.deploy__badge--unknown{ background:var(--sand); color:var(--ink-3); border:1px solid var(--line-2) }

.deploy__list{ display:grid; gap:var(--s3); margin:var(--s5) 0 0; padding:0; list-style:none }
.deploy__list li{
  display:grid; grid-template-columns:minmax(0,10rem) 1fr; gap:var(--s2) var(--s4);
  padding-bottom:var(--s3); border-bottom:1px solid var(--line);
  font-size:var(--t-sm);
}
.deploy__list li:last-child{ border-bottom:0; padding-bottom:0 }
.deploy__list b{ font-weight:700; color:var(--ink) }
.deploy__list span{ color:var(--ink-2) }
@media (max-width:520px){
  .deploy__list li{ grid-template-columns:1fr; gap:2px }
}

/* ── Article list + pagination ─────────────────────────────────────────── */
.posts{ display:grid; gap:var(--s5); grid-template-columns:1fr }
.posts:focus{ outline:none }
@media (min-width:720px){ .posts{ grid-template-columns:repeat(2,1fr) } }
@media (min-width:1060px){ .posts{ grid-template-columns:repeat(3,1fr) } }
.post{
  display:flex; flex-direction:column; gap:var(--s2);
  padding:var(--s5);
  background:var(--white); border:1px solid var(--line-2); border-radius:var(--r-lg);
  box-shadow:var(--sh-1);
  transition:box-shadow var(--dur) var(--e), transform var(--dur) var(--e);
}
.post:hover{ box-shadow:var(--sh-3); transform:translateY(-2px) }
.post__meta{ display:flex; flex-wrap:wrap; gap:var(--s2) var(--s3); font-size:var(--t-xs); color:var(--ink-3) }
.post__title{ margin:0; font-size:1.15rem; font-weight:800; letter-spacing:-.02em; color:var(--ink) }
.post__title a{ color:inherit; text-decoration:none }
/* The whole card is the target, but the LINK is still the only focusable thing in it — a card-wide click handler would be unreachable by keyboard. */
.post__title a::after{ content:""; position:absolute; inset:0 }
.post{ position:relative }
.post__desc{ margin:0; font-size:var(--t-sm); color:var(--ink-2); line-height:1.55 }

.posts__empty{
  display:grid; justify-items:center; text-align:center; gap:var(--s3);
  padding:var(--s8) var(--s5);
  background:var(--sand); border:1px dashed var(--line-3); border-radius:var(--r-lg);
}
.posts__empty[hidden]{ display:none }
.posts__empty-ico{
  display:grid; place-items:center; width:52px; height:52px;
  border-radius:var(--r-sm); background:var(--white); border:1px solid var(--line-2); color:var(--ink-4);
}
.posts__empty-ico svg{ width:24px; height:24px; fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round }
.posts__empty .sm{ max-width:52ch; color:var(--ink-2) }

.pager{
  display:flex; align-items:center; justify-content:center; gap:var(--s5);
  margin-top:var(--s6);
}
.pager[hidden]{ display:none }
.pager__btn{
  display:inline-flex; align-items:center; gap:var(--s2);
  min-height:44px; padding:10px 18px;
  background:var(--white); border:1px solid var(--line-2); border-radius:var(--r-pill);
  font-family:var(--f); font-size:var(--t-xs); font-weight:600; color:var(--ink-2);
  cursor:pointer;
  transition:color .18s ease, border-color .18s ease, background .18s ease;
}
.pager__btn svg{ width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round }
.pager__btn:hover:not(:disabled){ border-color:var(--red-soft); background:var(--red-wash) }
/* Disabled ends of the range stay visible rather than vanishing — a control that disappears at page 1 makes the row jump. */
.pager__btn:disabled{ opacity:.45; cursor:not-allowed }
.pager__label{ margin:0; font-size:var(--t-xs); font-weight:600; color:var(--ink-2) }

/* Team — empty today; the grid is here so a roster drops in without a layout pass. */
.team{ display:grid; gap:var(--s5); grid-template-columns:1fr }
.team:empty{ display:none }
@media (min-width:720px){ .team{ grid-template-columns:repeat(2,1fr) } }
@media (min-width:1000px){ .team{ grid-template-columns:repeat(3,1fr) } }
.member{
  display:flex; flex-direction:column; gap:var(--s2);
  padding:var(--s5);
  background:var(--white); border:1px solid var(--line-2); border-radius:var(--r-lg);
  box-shadow:var(--sh-1);
}
.member__photo{ width:64px; height:64px; border-radius:var(--r-pill); object-fit:cover; margin-bottom:var(--s3) }
.member__name{ margin:0; font-size:1.05rem; font-weight:800; letter-spacing:-.02em; color:var(--ink) }
.member__role{ margin:0; font-size:var(--t-xs); font-weight:600; color:var(--red) }
.member__bio{ margin:var(--s2) 0 0; font-size:var(--t-sm); color:var(--ink-2); line-height:1.55 }

/* Reference tables — each scrolls inside its own container. A table that widens the page body is a horizontal scrollbar on the whole document, and on a phone the header then moves when you swipe a spec row. */
.rtable{ overflow-x:auto; -webkit-overflow-scrolling:touch; border:1px solid var(--line-2); border-radius:var(--r-lg); background:var(--white) }
.rtable table{ width:100%; min-width:34rem; border-collapse:collapse; font-size:var(--t-sm) }
.rtable caption{
  padding:var(--s4) var(--s5) 0; text-align:left;
  font-size:var(--t-xs); color:var(--ink-2);
}
.rtable th, .rtable td{ padding:var(--s3) var(--s5); text-align:left; vertical-align:top }
.rtable thead th{
  position:sticky; top:0; z-index:1;
  background:var(--sand); color:var(--ink);
  font-size:var(--t-xs); font-weight:700; letter-spacing:.02em;
  border-bottom:1px solid var(--line-2);
}
.rtable tbody tr + tr th, .rtable tbody tr + tr td{ border-top:1px solid var(--line) }
.rtable tbody th{ font-weight:700; color:var(--ink) }
.rtable td{ color:var(--ink-2) }
.rtable .yes{ color:var(--red); font-weight:700 }
.rtable .no{ color:var(--ink-3) }

/* ── Knowledge base search ─────────────────────────────────────────────── */
.kb{ max-width:var(--measure) }
.kb__label{ display:block; margin-bottom:var(--s2); font-size:var(--t-sm); font-weight:700; color:var(--ink) }
/* The icon is decorative and inside the field, so the input keeps padding-left clear of it rather than the icon overlapping typed text. */
.kb__row{ position:relative; display:grid; grid-template-columns:1fr auto; gap:var(--s2) }
.kb__ico{
  position:absolute; left:var(--s4); top:50%; translate:0 -50%;
  display:grid; place-items:center; pointer-events:none; color:var(--ink-3);
}
.kb__ico svg{ width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round }
.kb input[type="search"]{
  width:100%; min-height:52px;
  padding:var(--s3) var(--s4) var(--s3) calc(var(--s4) * 2 + 18px);
  background:var(--white); border:1px solid var(--line-3); border-radius:var(--r-sm);
  font-size:var(--t-sm);
  transition:border-color var(--dur) var(--e);
}
.kb input[type="search"]:hover{ border-color:var(--ink-2) }
.kb__status{ margin-top:var(--s3) }
.kb__alt{
  margin-top:var(--s4); padding:var(--s4);
  background:var(--sand); border:1px solid var(--line-2); border-radius:var(--r);
}
.kb__alt[hidden]{ display:none }
.kb__alt-lead{ margin:0 0 var(--s3); font-size:var(--t-xs); color:var(--ink-2); line-height:1.5 }
.kb__links{ display:flex; flex-wrap:wrap; gap:var(--s2) var(--s5); margin:0; padding:0; list-style:none }
.kb__links a{ font-size:var(--t-sm); font-weight:600; color:var(--red); text-decoration:none }
.kb__links a:hover{ text-decoration:underline; text-underline-offset:3px }

@media (max-width:520px){
  .kb__row{ grid-template-columns:1fr }
}

/* Dial code + number. The code is a fixed 118px rather than a fraction, so the number field does not shrink on the narrow viewports where a phone number is most likely typed. */
.field__combo{ display:grid; grid-template-columns:118px 1fr; gap:var(--s2) }
.field__combo select{ padding-inline:var(--s3) }
/* If the code list fails to load the picker is removed rather than left sitting empty — the number field alone still works. */
.field__combo.is-codeless{ grid-template-columns:1fr }
.field__combo.is-codeless select{ display:none }
/* CAPTCHA mount point, sized like a real challenge widget so the form does not reflow when a provider is dropped in. */
.captcha{
  display:flex; align-items:center; gap:var(--s4);
  padding:var(--s4);
  background:var(--sand);
  border:1px dashed var(--line-3);
  border-radius:var(--r);
  margin-bottom:var(--s4);
}
.captcha__slot{
  display:grid; place-items:center; flex:none;
  width:56px; height:56px; border-radius:var(--r-sm);
  background:var(--white); border:1px solid var(--line-2);
  color:var(--ink-4);
}
.captcha__slot svg{
  width:24px; height:24px; fill:none; stroke:currentColor;
  stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round;
}
.captcha__note{ margin:0; font-size:var(--t-xs); color:var(--ink-2); line-height:1.5 }
.captcha__note b{ display:block; color:var(--ink); font-weight:700 }

@media (max-width:520px){
  .captcha{ flex-direction:column; align-items:flex-start; gap:var(--s3) }
}
.field__error{ font-size:var(--t-xs); color:var(--red); font-weight:600; min-height:1.2em }
.field__error:not(:empty)::before{ content:"! "; font-weight:800 }
.field--hp{ position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden }
/* Neutral by default: the colour is carried by data-tone further down, so a red base made an untoned message read as an error it was not. */
.form__status{ font-size:var(--t-sm); color:var(--ink); font-weight:600 }

/* Contact routes, beside the form. Nobody should fill a form to find a number. */
.routes{ display:grid; gap:var(--s3); align-content:start }
.routes a{
  display:flex; align-items:center; gap:var(--s4);
  background:var(--white);
  border:1px solid var(--line-2);
  border-radius:var(--r);
  box-shadow:var(--sh-1);
  padding:var(--s4) var(--s5);
  text-decoration:none; color:var(--ink);
  transition:box-shadow var(--dur) var(--e);
}
.routes a:hover{ box-shadow:var(--sh-2); color:var(--ink) }
.routes .chip{ width:38px; height:38px }
.routes b{ display:block; font-size:var(--t-sm); font-weight:700 }
.routes span{ display:block; font-size:var(--t-xs); color:var(--ink-2) }

/* ── Page hero — a short masthead for interior pages ─────────────────── */
.page-hero{
  position:relative;
  padding-block:clamp(var(--s8),7vw,var(--s9)) clamp(var(--s7),5vw,var(--s8));
  text-align:center;
}
.page-hero::before{
  content:""; position:absolute; top:-40%; left:50%;
  transform:translateX(-50%);
  /* 100% not 120vw — 120vw pushed the glow 38px past a 375px viewport and every interior page scrolled sideways */
  width:min(900px,100%); height:460px; border-radius:50%;
  background:radial-gradient(closest-side,rgba(217,39,63,.1),rgba(217,39,63,0) 72%);
  pointer-events:none;
}
/* Pricing leads with its cards, so the masthead gives back the vertical room a text-only interior page can afford. */
/* Sand, not page-ground: white cards on #FDFBFA differ by ~1.2% luminance and simply do not read as cards. */
.page-hero--tight{
  background:var(--sand);
  padding-block:clamp(var(--s6),4vw,var(--s7)) clamp(var(--s8),6vw,var(--s9));
}
.page-hero--tight .d1{ margin-bottom:var(--s4) }
.page-hero--tight .body{ max-width:56ch }
.page-hero > *{ position:relative }
.page-hero .body{ margin-inline:auto }

/* ── Not found — the home hero's composition, with the one screen on the site that is not playing anything ── */
.lost{
  position:relative; overflow:hidden;
  padding-block:clamp(var(--s8),6vw,var(--s9)) clamp(var(--s7),5vw,var(--s8));
}
.lost::before{
  content:""; position:absolute; top:-36%; left:50%;
  transform:translateX(-50%);
  width:min(1000px,125vw); height:560px; border-radius:50%;
  background:radial-gradient(closest-side,rgba(217,39,63,.12),rgba(217,39,63,0) 72%);
  pointer-events:none;
}
.lost__grid{ position:relative; display:grid; gap:var(--s7); align-items:center }
@media (min-width:960px){
  .lost__grid{ grid-template-columns:minmax(30ch,38ch) 1fr; gap:var(--s8) }
}
.lost__in{ max-width:54ch }
.lost__in .d1{ margin:var(--s4) 0; font-size:clamp(2.3rem,4.2vw,3.5rem); text-wrap:balance }

/* The path that missed, written back in the console's own mono face. */
.lost__path{
  display:inline-flex; align-items:baseline; gap:var(--s3); flex-wrap:wrap;
  max-width:100%; margin-top:var(--s5); padding:7px 16px;
  background:var(--mist); border:1px solid var(--line-2);
  border-radius:var(--r-pill);
}
.lost__path[hidden]{ display:none }
.lost__path b{
  font-size:var(--t-lab); font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--ink-3);
}
.lost__path span{
  font-family:var(--f-mono); font-size:var(--t-xs); color:var(--ink);
  overflow-wrap:anywhere;
}

.lost__media{ margin:0 }
@media (min-width:960px){ .lost__media{ margin-left:var(--s5) } }

/* No footage behind this one, so no picture-derived spill — only the red core from .screen::before. */
.lost__screen::after{ display:none }

/* Aspect ratio is set here: .screen__panel takes its 16:9 from the img or video inside, and this holds neither. */
.lost__slate{
  position:relative; aspect-ratio:16 / 9;
  display:grid; place-content:center; justify-items:center; gap:var(--s2);
  padding:var(--s5); text-align:center;
  background:
    radial-gradient(72% 62% at 50% 44%, rgba(217,39,63,.22), transparent 70%),
    repeating-linear-gradient(180deg, rgba(255,255,255,.04) 0 1px, transparent 1px 4px);
}
/* inline-block + fit-content for the same reason .figure carries them — a block samples one slice of the ramp. */
.lost__code{
  display:inline-block; width:fit-content;
  font-size:clamp(3.2rem,9vw,6.4rem); font-weight:800;
  letter-spacing:-.05em; line-height:1;
  font-variant-numeric:tabular-nums;
  background:linear-gradient(135deg,var(--red-soft) 0%,var(--red-hot) 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}
.lost__note{
  font-size:var(--t-lab); font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:rgba(255,255,255,.66);
}
/* The one pill on the site not reporting playback — the dot goes quiet and stops pulsing. */
.screen__live--idle::before{ background:var(--ink-4); animation:none }

.lost__caption{ margin-top:var(--s5); font-size:var(--t-xs); color:var(--ink-2); max-width:46ch }
.lost__banner{ margin-top:var(--s4) }

/* An arrow so a link tile reads as one target, not a paragraph that happens to be clickable. */
.lost__go{
  display:inline-flex; align-items:center; gap:var(--s2);
  margin-top:var(--s4);
  font-size:var(--t-sm); font-weight:700; color:var(--red);
}
.lost__go svg{
  width:16px; height:16px; flex:none;
  stroke:currentColor; stroke-width:2; fill:none;
  stroke-linecap:round; stroke-linejoin:round;
  transition:translate var(--dur) var(--e);
}
.tile--link:hover .lost__go{ color:var(--red-deep) }
.tile--link:hover .lost__go svg{ translate:3px 0 }

/* Auth and trial pages — they reuse .form / .field / .btn wholesale; only what is genuinely new to these pages lives here. */

.back-link{
  display:inline-block; margin-bottom:var(--s5);
  font-size:var(--t-xs); font-weight:600; color:var(--ink-2); text-decoration:none;
}
.back-link:hover{ color:var(--red) }

/* Marks a field as skippable at the point of asking — without it people fill optional fields defensively and the form feels twice as long. */
.field__opt{ font-weight:400; color:var(--ink-3); font-size:var(--t-xs) }

/* The honeypot, off-screen rather than display:none: some bots skip hidden inputs, and a positioned one still looks fillable. tabindex="-1" and aria-hidden keep it away from real people. */
.hp{ position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden }

/* Status line under a submit. The tone attribute is set by submit.js so success and failure are not both red, and colour is never the only signal — the wording says which it is. */
.form__status[data-tone="ok"]{ color:var(--ink) }
.form__status[data-tone="bad"]{ color:var(--red) }
.form__status:empty{ display:none }        /* no reserved gap before it speaks */

/* Replaces the form once the request is in: a confirmation above a still-editable form invites a second submission. */
.done{
  background:var(--white);
  border:1px solid var(--line-2);
  border-radius:var(--r-lg);
  box-shadow:var(--sh-2);
  padding:clamp(var(--s5),4vw,var(--s7));
}
.done:focus{ outline:none }               /* focused programmatically, not by tab */
.done h2{ margin-bottom:var(--s4) }
.done .body + .body{ margin-top:var(--s4) }
.done .cta-pair{ margin-top:var(--s6) }

.signin{
  background:var(--white);
  border:1px solid var(--line-2);
  border-radius:var(--r-lg);
  box-shadow:var(--sh-2);
  padding:clamp(var(--s5),4vw,var(--s6));
  text-align:center;
}
.signin .btn-note{ margin-top:var(--s4) }
.signin__aside{ margin-top:var(--s7) }
.signin__aside h2{ margin-bottom:var(--s4) }

/* Auth pages — two columns on desktop, a brand panel carrying the atmosphere and a card carrying the work. Stacked, four fields plus two SSO buttons plus a terms row is taller than a laptop. Every animation here is transform/opacity only; no filter, no backdrop-filter. */
.auth{
  min-height:100svh;
  /* --cc-h is published by consent.js while the notice is up. Without it the fixed banner sat on top of the submit button. */
  padding-bottom:var(--cc-h, 0px);
  transition:padding-bottom var(--dur) var(--e);
  background:var(--page);
}
.auth__split{ min-height:100svh; display:grid }
@media (min-width:900px){
  .auth__split{ grid-template-columns:minmax(0,1fr) minmax(0,1.05fr) }
}

/* Brand panel — a real venue photograph under a heavy brand gradient: texture, not a picture. The gradient does the darkening so no filter is needed. */
.auth__aside{
  display:none;
  position:relative; overflow:hidden;
  background:var(--ink);
  padding:var(--s7) clamp(var(--s6),4vw,var(--s8));
  align-content:center;
  color:var(--white);
}
@media (min-width:900px){ .auth__aside{ display:grid } }
/* A real venue loop at 288x512, 20fps, CRF 34 — 127KB from a 1044KB source, because it sits behind a gradient at half opacity. Resolution and frame rate were the levers; CRF alone bottomed out at 256KB. preload="none" keeps it off the critical path. */
.auth__video{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  opacity:.5;
  pointer-events:none;
}
.auth__aside::after{
  content:""; position:absolute; inset:0;
  background:
    radial-gradient(120% 80% at 18% 10%, rgba(217,39,63,.5), transparent 60%),
    linear-gradient(200deg, rgba(25,20,22,.42) 0%, rgba(25,20,22,.88) 82%);
}
.auth__aside-in{ position:relative; z-index:1; max-width:34ch }
.auth__aside-title{
  margin-top:var(--s5);
  font-size:var(--t-h1); font-weight:800; letter-spacing:-.035em; color:var(--white);
}
.auth__points{ margin-top:var(--s5); display:grid; gap:var(--s3) }
.auth__points li{
  position:relative; padding-left:var(--s5);
  font-size:var(--t-sm); color:rgba(255,255,255,.78); line-height:1.5;
}
.auth__points li::before{
  content:""; position:absolute; left:0; top:.55em;
  width:8px; height:8px; border-radius:50%;
  background:var(--red-hot); box-shadow:0 0 0 4px rgba(217,39,63,.22);
}
.auth__help{
  display:inline-flex; align-items:center; gap:var(--s2);
  margin-top:var(--s7);
  font-size:var(--t-xs); font-weight:600;
  color:rgba(255,255,255,.72); text-decoration:none;
  border:1px solid rgba(255,255,255,.22); border-radius:var(--r-pill);
  padding:9px 16px;
  transition:background var(--dur-fast) var(--e), color var(--dur-fast) var(--e), border-color var(--dur-fast) var(--e);
}
.auth__help svg{ width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:1.8;
                 stroke-linecap:round; stroke-linejoin:round }
.auth__help:hover{ background:rgba(255,255,255,.10); color:var(--white); border-color:rgba(255,255,255,.4) }

/* The Qube — the brand mark as a character: it watches the field you are in and covers its eyes while you type a password, which is a legible signal that the field is masked. Everything is a transform on a group, so no re-layout. */
.qube{ width:118px; filter:none }
.qube svg{ width:100%; height:auto; overflow:visible }
.qube__shadow{ fill:rgba(0,0,0,.45) }
.qube__body{ transform-origin:65px 110px; animation:qube-float 5s var(--e) infinite }
.qube__neck,.qube__base{ fill:rgba(255,255,255,.30) }
.qube__frame{ fill:var(--red) }
.qube__face{ fill:#2A1F23 }
.qube__eye{ fill:var(--white) }
.qube__mouth{ fill:none; stroke:var(--white); stroke-width:3.4; stroke-linecap:round; opacity:.9 }
/* Eyes and mouth ride together so a single translate aims the whole face. */
.qube__look{ transition:translate 380ms var(--e) }
.qube__eyes{ transform-origin:65px 50px; animation:qube-blink 6.5s steps(1,end) infinite }

/* Eyes shut, not hands over them: this character is a screen on a stand, with no body for a hand to come FROM — bars read as sunglasses and mitts as broccoli. Open eyes and shut lids are separate cross-fading groups, which is what makes the one-eye peek possible. */
.qube__lids{ opacity:0; transition:opacity 180ms var(--e) }
/* Arched UP while the mouth arches down — with both curving the same way the face showed three near-identical smiles stacked and stopped reading as a face. Sat on y=50, the open eye's centre line, so the closed eye lands where the open one was. */
.qube__lid{
  fill:none; stroke:var(--white); stroke-width:4.5;
  stroke-linecap:round; stroke-linejoin:round;
}
.qube__blush{ opacity:0; fill:#FF6B7E; transition:opacity 240ms var(--e) }
.qube__eyes{ transition:opacity 140ms var(--e) }
.qube__eye{ transition:opacity 140ms var(--e) }

/* Password focused: eyes shut, cheeks up. */
.qube.is-hiding .qube__eyes{ animation:none; opacity:0 }
.qube.is-hiding .qube__lids{ opacity:1 }
.qube.is-hiding .qube__blush{ opacity:.5 }

/* Show-password pressed: one eye opens, the other stays shut — a peek. */
.qube.is-peeking .qube__eyes{ animation:none; opacity:1 }
.qube.is-peeking .qube__eye:first-child{ opacity:0 }
.qube.is-peeking .qube__lids{ opacity:1 }
.qube.is-peeking .qube__lid:last-child{ opacity:0 }
.qube.is-peeking .qube__blush{ opacity:.5 }

.qube.is-happy .qube__body{ animation:qube-hop 620ms var(--e-spring) }
.qube.is-sad   .qube__body{ animation:qube-shake 420ms var(--e) }

@keyframes qube-float{ 0%,100%{ translate:0 0 } 50%{ translate:0 -7px } }
@keyframes qube-blink{ 0%,94%{ scale:1 1 } 96%,98%{ scale:1 .12 } 100%{ scale:1 1 } }
@keyframes qube-hop{ 0%{ translate:0 0 } 35%{ translate:0 -18px } 60%{ translate:0 2px } 100%{ translate:0 0 } }
@keyframes qube-shake{ 0%,100%{ translate:0 0 } 20%{ translate:-6px 0 } 50%{ translate:6px 0 } 80%{ translate:-3px 0 } }

/* ── Card column ── */
/* One fixed column rather than centred items, so the back control lines up with the card's left edge instead of drifting into the gutter. */
.auth__main{
  display:grid; align-content:center;
  grid-template-columns:min(400px, 100%);
  justify-content:center;
  /* s2, not s3: the back control became a 40px pill rather than a line of text, and those 20px pushed signup back over the fold. */
  gap:var(--s2);
  padding:var(--s5) var(--s4);
  background:
    radial-gradient(80% 45% at 50% 0%, var(--blush), transparent 68%),
    var(--page);
}
/* A real control, not a stray line of underlined text: it reads as something you can press, sits at a proper 40px target, and is anchored to the card's left edge. */
.auth__back{
  justify-self:start;      /* left edge of the card's column */
  display:inline-flex; align-items:center; gap:8px;
  min-height:40px; padding:8px 16px 8px 12px;
  border:1px solid var(--line-2); border-radius:var(--r-pill);
  background:var(--white);
  box-shadow:var(--sh-1);
  font-size:var(--t-xs); font-weight:600; color:var(--ink-2); text-decoration:none;
  transition:color var(--dur-fast) var(--e), border-color var(--dur-fast) var(--e),
             box-shadow var(--dur-fast) var(--e);
}
.auth__back svg{ width:15px; height:15px; flex:none;
                 fill:none; stroke:currentColor; stroke-width:2;
                 stroke-linecap:round; stroke-linejoin:round;
                 transition:translate var(--dur-fast) var(--e) }
.auth__back:hover{ color:var(--red); border-color:var(--red-soft); box-shadow:var(--sh-2) }
.auth__back:hover svg{ translate:-3px 0 }
.auth__back:focus-visible{ outline:2px solid var(--red); outline-offset:2px }

/* Disabled only in the gap before submit.js binds. Reads as "not yet", not as "refused". */
.auth__submit[disabled]{ opacity:.55; cursor:progress }

.auth__card{
  width:100%;
  background:var(--white);
  border:1px solid var(--line-2);
  border-radius:var(--r-lg);
  box-shadow:var(--sh-3);
  padding:clamp(var(--s5), 3vw, var(--s6));
  animation:auth-in 420ms var(--e) both;
}
@keyframes auth-in{ from{ opacity:0; translate:0 14px } to{ opacity:1; translate:0 0 } }

.auth__title{ font-size:1.375rem; font-weight:800; letter-spacing:-.03em; text-align:center; line-height:1.2 }
.auth__sub{
  margin-top:5px; margin-bottom:var(--s4);
  font-size:var(--t-xs); color:var(--ink-2); text-align:center; max-width:none;
}

/* ── SSO ── */
.auth__sso{ display:grid; gap:var(--s2) }
.sso{
  display:flex; align-items:center; justify-content:center; gap:var(--s3);
  width:100%; min-height:44px; padding:10px var(--s4);
  background:var(--white); color:var(--ink);
  border:1px solid var(--line-2); border-radius:var(--r-pill);
  font-size:var(--t-sm); font-weight:600; cursor:pointer;
  transition:border-color var(--dur-fast) var(--e), background var(--dur-fast) var(--e),
             translate var(--dur-fast) var(--e);
}
.sso:hover{ border-color:var(--line-3); background:var(--sand); translate:0 -1px }
.sso:active{ translate:0 0 }
.sso:disabled{ opacity:.6; cursor:progress }
.sso:focus-visible{ outline:2px solid var(--red); outline-offset:2px }

.auth__or{
  display:flex; align-items:center; gap:var(--s3);
  margin:var(--s4) 0;
  font-size:var(--t-xs); color:var(--ink-3);
}
.auth__or::before,.auth__or::after{ content:""; flex:1; height:1px; background:var(--line-2) }

/* Tighter than the marketing forms: those breathe down a long page, this one competes for a single screen. */
.auth__card .form{ gap:var(--s3) }
.auth__card .field{ gap:5px }
.auth__card .field label{ font-size:var(--t-xs) }
.auth__card .field input{ min-height:44px; padding:10px var(--s4) }
/* Errors take no space until they say something — six reserved empty slots is 100px of nothing on a card fighting for the fold. */
.auth__card .field__error:empty{ display:none }
.auth__card .field__error{ animation:err-in 220ms var(--e) both }
@keyframes err-in{ from{ opacity:0; translate:0 -4px } to{ opacity:1; translate:0 0 } }

.auth__submit{ min-height:48px; margin-top:2px; transition:translate var(--dur-fast) var(--e), box-shadow var(--dur) var(--e) }
.auth__submit:hover:not(:disabled){ translate:0 -1px }
.auth__submit:active:not(:disabled){ translate:0 0 }

/* ── Password field ── */
.pw{ position:relative; display:grid }
.pw input{ padding-right:44px }
.pw__eye{
  position:absolute; right:5px; top:50%; translate:0 -50%;
  display:grid; place-items:center;
  width:34px; height:34px; border-radius:var(--r-pill);
  color:var(--ink-3); cursor:pointer;
  transition:color var(--dur-fast) var(--e), background var(--dur-fast) var(--e);
}
.pw__eye:hover{ color:var(--ink); background:var(--mist) }
.pw__eye[aria-pressed="true"]{ color:var(--red) }
.pw__eye svg{ grid-area:1/1; width:18px; height:18px; fill:none; stroke:currentColor;
              stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round;
              transition:opacity 160ms var(--e), scale 160ms var(--e-spring) }
.pw__shut{ opacity:0; scale:.7 }
.pw__eye[aria-pressed="true"] .pw__open{ opacity:0; scale:.7 }
.pw__eye[aria-pressed="true"] .pw__shut{ opacity:1; scale:1 }
.pw__eye:focus-visible{ outline:2px solid var(--red); outline-offset:2px }

/* Strength meter — appears only once there is something to measure, so it never sits empty. */
.meter{ display:grid; gap:4px; margin-top:2px }
.meter__track{ height:4px; border-radius:var(--r-pill); background:var(--mist); overflow:hidden }
.meter__fill{
  display:block; height:100%; width:100%;
  border-radius:inherit; background:var(--red);
  transform-origin:left; scale:0 1;
  transition:scale 320ms var(--e), background var(--dur) var(--e);
}
.meter__label{ font-size:var(--t-lab); color:var(--ink-3); letter-spacing:0; text-transform:none }
.meter[data-level="1"] .meter__fill{ scale:.25 1; background:#C9403F }
.meter[data-level="2"] .meter__fill{ scale:.5 1;  background:#D98324 }
.meter[data-level="3"] .meter__fill{ scale:.75 1; background:#B0942A }
.meter[data-level="4"] .meter__fill{ scale:1 1;   background:#2F8A5B }

.field__row{ display:flex; align-items:baseline; justify-content:space-between; gap:var(--s3) }
/* Padding buys the 24px WCAG 2.5.8 target; the negative margin gives the space back so the label and the link still sit on one baseline. */
.field__aside{
  font-size:var(--t-xs); font-weight:600;
  display:inline-flex; align-items:center; min-height:24px;
  padding-inline:6px; margin-inline:-6px;
}

/* The whole label toggles the box, so the label is the target — one line of --t-xs text leaves it 21px, three short of WCAG 2.5.8. */
.check{
  display:flex; gap:var(--s3); align-items:flex-start; cursor:pointer;
  margin-top:2px; min-block-size:24px;
}
.check input{ margin-top:2px; width:16px; height:16px; accent-color:var(--red); flex:none; cursor:pointer }
.check span{ font-size:var(--t-xs); color:var(--ink-2); line-height:1.5 }

.auth__alt{
  margin-top:var(--s4); padding-top:var(--s4);
  border-top:1px solid var(--line);
  font-size:var(--t-xs); color:var(--ink-2); text-align:center;
}
.auth__foot{ font-size:var(--t-lab); color:var(--ink-3); text-align:center; letter-spacing:0; text-transform:none }
.auth__foot a{ color:var(--ink-2) }
.auth__card .done{ border:0; box-shadow:none; padding:0; text-align:center }

/* Short windows: trims decorative room first and NEVER the controls, which stay at their 44px floor. Triggered at 860px, not 780 — a 1440x800 laptop is one of the commonest sizes and was missing the rule entirely. */
@media (max-height:860px){
  .auth__main{ padding-block:var(--s3); gap:var(--s2) }
  .auth__card{ padding:var(--s5) var(--s5) var(--s4) }
  .auth__sub{ margin-bottom:var(--s3) }
  .auth__or{ margin:var(--s3) 0 }
  .auth__card .form{ gap:var(--s2) }
  .auth__alt{ margin-top:var(--s3); padding-top:var(--s3) }
}
/* Shorter still: the subtitle is the only thing here carrying no function, so it is the only thing that goes. */
@media (max-height:760px){
  .auth__sub{ display:none }
  .auth__title{ margin-bottom:var(--s3) }
}

/* Motion is the first thing to go. The Qube keeps its EXPRESSIONS — covering its eyes for a password is information, not decoration — but stops moving on its own. */
@media (prefers-reduced-motion: reduce){
  .qube__body,.qube__eyes{ animation:none }
  .qube__lids,.qube__blush,.qube__look,.auth__card,.field__error,
  .sso,.auth__submit,.pw__eye svg,.meter__fill,.auth__back svg{ transition:none; animation:none }
}

/* Plan cards. Subgrid keeps the figure, the button and the feature list on one line across all three — flex bottom-alignment drifts the moment one card's copy wraps differently. The figure carries no placeholder underline: a dotted rule under a 3rem number reads as a broken link. */
.plans{ display:grid; gap:var(--s5); grid-template-columns:1fr }
@media (min-width:960px){
  .plans{ grid-template-columns:repeat(3,1fr); grid-template-rows:auto auto auto auto auto }
  .plan{ grid-row:span 5; grid-template-rows:subgrid }
}
/* Card-count shapes. The grid above is hardcoded to three columns, which is right
   for 3 and acceptable for 5+, but /pricing/ now renders whatever the partner has
   published plus the contact card, so 1, 2 and 4 are all reachable -- 1 being the
   API-down state. ADDITIVE on purpose: if a drop re-import loses this block the
   page falls back to three columns, still correct for the common case. Below
   960px the grid is 1fr and any count stacks fine, so this is all inside the
   query. */
@media (min-width:960px){
  /* One or two cards stretched across a 1280px wrap read as a layout bug. 400px
     is the width a card already has in the three-column case, so the cards do not
     change size with the count -- only the grid does. */
  .plans[data-count="1"]{ grid-template-columns:minmax(0,400px); justify-content:center }
  .plans[data-count="2"]{ grid-template-columns:repeat(2,minmax(0,400px)); justify-content:center }
  /* Four goes to two rows of two, NOT to four columns: .plan__amount is
     clamp(2rem,3.2vw,2.5rem) with white-space:nowrap inside a flex-wrap:nowrap
     .plan__fig, and a converted figure overflows a quarter-width card. Ten rows
     because each row-block of two needs its own five subgrid rows to align on. */
  .plans[data-count="4"]{ grid-template-columns:repeat(2,1fr); grid-template-rows:repeat(10,auto) }
}
.plan{ display:grid; align-content:start; padding:var(--s6) var(--s6) var(--s7); row-gap:0; position:relative }

/* The tag is lifted OUT OF FLOW and pinned to the card's own top-left corner, so
   its top and left edges are the card's border rather than a second line drawn
   inside it. Out of flow is the point: the slot below then reserves the same gap
   on every card whether or not it carries a tag, so a category-less card can no
   longer sit a few px shy of its neighbours in the subgrid. */
.plan__flagslot{ min-height:var(--s1) }
.plan__flag{
  position:absolute;
  /* 0/0, not a negative inset: an absolutely positioned child resolves against
     the PADDING box, whose origin is already the inner edge of the card's 1px
     border. That is exactly where the tag wants to sit — a -s6 offset here hangs
     it 31px off the outside of the card. */
  top:0; left:0;
  padding:9px var(--s5) 9px var(--s6);
  /* Only the bottom-right is the tag's own corner. The top-left follows the
     card's, inset by the 1px border it is sitting against; the other two are
     square because they die into the card's edges. */
  border-radius:calc(var(--r-lg) - 1px) 0 var(--r-lg) 0;
  background:var(--blush); color:var(--red);
  font-size:var(--t-lab); font-weight:1000; letter-spacing:.16em; text-transform:uppercase;
}
.tile--dark .plan__flag{ background:rgba(255,255,255,.12); color:var(--red-soft) }
/* Tier flag. Standard is the common case — 38 of the 45 plans in the console are
   standard — so it is the QUIET one: a blush pill on every card turns the accent
   into wallpaper and leaves Enterprise nothing to say. Enterprise keeps the base
   treatment above, which is why only this modifier carries colour. */
.plan__flag--standard{
  background:var(--sand); color:var(--ink-2);
  /* Only the two edges that are not the card's own border get a line — a full
     ring would double up the card's border along the top and left. */
  box-shadow:inset -1px -1px 0 0 var(--line-2);
}
.tile--dark .plan__flag--standard{
  background:rgba(255,255,255,.08); color:rgba(255,255,255,.74); box-shadow:none;
}

/* Was-price and saving. Their own row UNDER .plan__fig rather than a third item
   inside it: that figure is flex-wrap:nowrap so the amount and its unit stay on
   one line, and a third item overflows the card the moment a price is converted
   (₹1,580 is far wider than $19). Sits in .plan__price, which is a plain block —
   the four-children subgrid contract is on .plan itself, not on this. */
.plan__save{ display:flex; align-items:center; gap:var(--s3); flex-wrap:wrap; margin-top:var(--s3) }
/* Required: the display:flex above out-specifies the UA's [hidden]{display:none},
   so a card with no discount would render an empty row and shift the figure. */
.plan__save[hidden]{ display:none }
.plan__was{
  font-size:var(--t-sm); font-weight:700; color:var(--ink-3);
  text-decoration:line-through; text-decoration-thickness:1.5px;
}
/* Deliberately the same pill as .billing__save — the page already says "saving"
   in this exact shape above the grid, and a second visual for one idea reads as
   two different things. */
.plan__pct{
  padding:4px var(--s3); border-radius:var(--r-pill);
  background:var(--blush); color:var(--red);
  font-size:var(--t-xs); font-weight:700; white-space:nowrap;
}
.tile--dark .plan__was{ color:rgba(255,255,255,.62) }
.tile--dark .plan__pct{ background:rgba(255,255,255,.12); color:var(--red-soft) }

/* Hard caps on the two partner-authored strings. Without them one long name or
   blurb drags its card's price row down and every other card in the band follows
   it, because .plan is a subgrid and the row track takes the tallest card. The
   full string stays in the DOM — only the visual box is cut, so screen readers
   and the page's own search still see all of it. */
.plan__name{
  font-size:clamp(1.55rem,2.4vw,1.95rem); font-weight:800; letter-spacing:-.03em;
  margin-bottom:var(--s3);
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; line-clamp:2;
  overflow:hidden;
  /* The h1..h4 reset sets 1.1, which cuts the descenders off the last visible
     line once overflow is hidden — "Signage" loses the tail of its g. */
  line-height:1.18;
  /* A name with no spaces in it is one unbreakable token: without this it takes a
     single line, gets sliced off at the edge with no ellipsis, and leaves the
     second allowed line empty. There is a real plan in the console shaped exactly
     like that. `anywhere` rather than break-all — it only breaks a word that would
     otherwise overflow, so ordinary names still wrap on their spaces. */
  overflow-wrap:anywhere;
}
.plan__lead{
  font-size:var(--t-sm); color:var(--ink-2); line-height:1.6;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:3; line-clamp:3;
  overflow:hidden;
  overflow-wrap:anywhere;
}

/* stretch + column so the block fills its subgrid row track: the CTA below is
   pushed to the BOTTOM of that track, which is a fixed distance above the
   feature-list rule, instead of floating wherever the copy above it happens to
   end. Without this the contact card's "Contact us" sat ~70px higher than the
   Purchase buttons beside it, because it carries no save row and no note. */
.plan__price{ margin-top:var(--s6); align-self:stretch; display:flex; flex-direction:column }
/* nowrap: the figure and its unit must stay on one line in every currency — ₹1,580 is wider than $19 and was wrapping the unit under the amount on one card but not the next. */
.plan__fig{ display:flex; align-items:baseline; gap:8px; flex-wrap:nowrap }
.plan__amount{ font-size:clamp(2rem,3.2vw,2.5rem); font-weight:800; letter-spacing:-.04em; line-height:1; white-space:nowrap }
.plan__unit{ font-size:var(--t-xs); color:var(--ink-2); font-weight:600; white-space:nowrap }
.plan__note{ display:block; margin-top:var(--s3); font-size:var(--t-xs); color:var(--ink-3); line-height:1.5 }
.tile--dark .plan__lead{ color:rgba(255,255,255,.74) }
.tile--dark .plan__note{ color:rgba(255,255,255,.62) }

/* margin-top stays as the FLOOR — an auto margin cannot also carry a minimum, so
   the slack is absorbed by the element directly above the button instead (below). */
.plan .btn{ margin-top:var(--s5); width:100%; justify-content:center }
/* Whatever element happens to sit directly above the CTA absorbs the slack —
   .plan__note on a generated card, the last <p> on the hand-written contact card.
   Addressed by POSITION rather than by class on purpose: naming the classes meant
   that adding a line to the contact card's markup silently moved the auto margin
   to the wrong element and blew a 70px hole in the middle of it. Exactly one auto
   margin per card; two would split the free space and strand the price mid-track.
   Where :has() is unsupported this simply degrades to the old behaviour — the CTA
   sits var(--s5) under the copy instead of bottoming out. */
.plan__price > :has(+ .btn){ margin-bottom:auto }
.plan .spec-list{
  margin-top:var(--s6); padding-top:var(--s6);
  border-top:1px solid var(--line);
}
.tile--dark .plan .spec-list{ border-top-color:rgba(255,255,255,.16) }

/* The middle card is the common route: raised and ringed, NOT colour-inverted — a black card beside two white ones reads as a different design system rather than as emphasis. */
.plan--lead{
  box-shadow:0 1px 2px rgba(38,26,30,.05), 0 18px 44px -14px rgba(38,26,30,.22);
  outline:2px solid var(--red); outline-offset:-2px;
}

/* Ticks, not rings. A ring is a bullet; a tick says "included", which is the only thing this list is for. Drawn in CSS so it scales with the type. */
/* Two lines per feature, same treatment the name and the blurb already get. A
   feature title is partner-authored free text with no length limit on it
   anywhere, so one essay-length entry otherwise ran to five or six lines and —
   because .plan is a subgrid whose row track takes the tallest card — dragged
   every other card in the band down with it. Clamping the li is safe: both the
   disc and the tick are position:absolute inside it, so overflow:hidden does not
   touch them. `anywhere` catches a title with no spaces in it, which would
   otherwise sit on one line and get sliced at the card edge with no ellipsis. */
.plan .spec-list li{
  padding-left:30px; color:var(--ink);
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; line-clamp:2;
  overflow:hidden; overflow-wrap:anywhere;
}
.plan .spec-list li::before{
  width:20px; height:20px; top:.15em; border-radius:50%;
  background:var(--blush); box-shadow:none;
}
.plan .spec-list li::after{
  content:""; position:absolute; left:7px; top:.45em;
  width:5px; height:9px;
  border:solid var(--red); border-width:0 2px 2px 0;
  transform:rotate(45deg);
}

/* Full-bleed rule above the feature list, the way this category draws it — an inset rule reads as a stray underline. */
.plan .spec-list{
  gap:var(--s4);
  margin-inline:calc(-1 * var(--s6));
  padding-inline:var(--s6);
}

/* Currency and period controls share one row above the cards. */
.pricing-controls{
  display:flex; flex-wrap:wrap; gap:var(--s4) var(--s5);
  align-items:center; justify-content:center;
  margin-top:var(--s6);
}
/* Read-out, not a control — currency is detected, so there is nothing to pick. */
.cur{ display:flex; align-items:center; gap:6px; margin:0; font-size:var(--t-xs); color:var(--ink-3) }
.cur[hidden]{ display:none }
.cur__note[hidden]{ display:none }
.cur b{ color:var(--ink-2); font-weight:700 }
.plan__tax{ display:block; margin-top:6px; font-size:var(--t-xs); font-weight:700; color:var(--ink-2) }
.plan__tax:empty{ display:none }

/* Loading skeletons for /pricing/. Two ship in the HTML so the grid has roughly
   its final height before the fetch lands and the real cards drop in without a
   jump. They deliberately carry NO .rv: .rv starts at opacity:0 and only clears
   when the observer fires, and a placeholder that is invisible is not a
   placeholder. They are also aria-hidden -- the live region below the grid is
   what announces progress. */
.plan--skeleton .sk{
  display:block; border-radius:var(--r-xs);
  background:var(--line);
  animation:sk-pulse 1.4s ease-in-out infinite;
}
.plan--skeleton .sk--name{ height:1.9rem; width:54%; margin-bottom:var(--s3) }
.plan--skeleton .sk--lead{ height:.75rem; width:92% }
.plan--skeleton .sk--lead + .sk--lead{ width:66%; margin-top:var(--s3) }
.plan--skeleton .sk--fig{ height:2.4rem; width:46% }
/* The dot marker belongs to a real feature, not to a grey bar. */
.plan--skeleton .spec-list li{ padding-left:0; display:block }
.plan--skeleton .spec-list li::before{ display:none }
.plan--skeleton .sk--row{ height:.75rem; width:92% }
.plan--skeleton .spec-list li:nth-child(2) .sk--row{ width:78% }
.plan--skeleton .spec-list li:nth-child(3) .sk--row{ width:88% }
.plan--skeleton .spec-list li:nth-child(4) .sk--row{ width:70% }
@keyframes sk-pulse{ 0%,100%{ opacity:1 } 50%{ opacity:.42 } }
@media (prefers-reduced-motion: reduce){
  .plan--skeleton .sk{ animation:none }
}

/* Status line under the grid. Sits OUTSIDE .plans -- a sixth grid child would
   take a column and break the subgrid row count for every card beside it. */
.plans-status{ margin-top:var(--s5) }

/* Comparison table marks — never colour alone (WCAG 1.4.1): each carries a glyph and an sr-only word, so "included" and "capped" are distinguishable without it. */
.table--compare td{ text-align:center; width:17%; color:var(--ink) }
.table--compare th[scope="row"]{ text-align:left; min-width:17rem; font-weight:500 }

/* The header carries each plan's name and its call to action, so a reader who has scrolled past the cards can still act without scrolling back. */
/* Sticky header: on a 35-row table the column you are reading scrolls out of sight within two screens. 79px is the floating nav's own bottom edge (12px top + 67px tall) plus a hair. */
.table--compare thead th{
  position:sticky; top:84px; z-index:2;
  background:var(--sand); vertical-align:top;
  padding-top:var(--s4); padding-bottom:var(--s4);
  text-transform:none; letter-spacing:normal;
  box-shadow:inset 0 -1px 0 var(--line);
}
@media (max-width:719px){ .table--compare thead th{ position:static } }
.table--compare thead th:not(:first-child){ text-align:center }
.cmp__title{ font-size:var(--t-h2); font-weight:800; letter-spacing:-.025em; color:var(--ink) }
.cmp__plan{ display:block; font-size:var(--t-sm); font-weight:800; color:var(--ink); margin-bottom:var(--s2) }
.cmp__note{ display:block; margin-top:6px; font-size:var(--t-lab); color:var(--ink-3); text-transform:none; letter-spacing:0; font-weight:500 }
.table--compare thead .btn{ width:100%; max-width:11rem; justify-content:center }

/* The featured column reads as one continuous band down the table. */
.table--compare .is-lead{ background:var(--blush) }
.table--compare thead th.is-lead{ background:var(--blush) }
.table--compare .cell{ font-size:var(--t-sm); color:var(--ink-2); font-weight:600 }
.mark--no{ background:transparent; color:var(--line-3) }
.mark{ display:inline-flex; width:22px; height:22px; align-items:center; justify-content:center; border-radius:50% }
.mark svg{ width:14px; height:14px; stroke-width:2.6; fill:none; stroke:currentColor }
.mark--yes{ background:var(--blush); color:var(--red) }

/* Group headers inside a long comparison table, so it stays scannable. */
.table__group th,.table__group td{
  background:var(--sand); color:var(--ink);
  font-size:var(--t-lab); font-weight:800; letter-spacing:.08em; text-transform:uppercase;
}
.table__why{ font-size:var(--t-xs); color:var(--ink-3); display:block; margin-top:2px; text-transform:none; letter-spacing:0; font-weight:500 }

/* Billing period switch — a real control: billing type is a field on every plan, so it changes what the cards and table say about the period. It never changes a price, because we publish none. Radios, so arrow keys work and the state is announced. */
.billing{
  display:inline-flex; align-items:center; gap:2px;
  /* No auto margins: this is a flex child of .pricing-controls, and `auto` there eats the free space and pushes the currency control to the far edge. */
  margin:0;
  padding:4px; border-radius:999px;
  background:var(--sand); border:1px solid var(--line);
}
.billing__opt{ position:relative; display:block }
.billing__opt input{ position:absolute; inset:0; opacity:0; width:100%; height:100%; margin:0; cursor:pointer }
.billing__opt span{
  display:block; padding:10px var(--s5); border-radius:999px;
  font-size:var(--t-sm); font-weight:700; color:var(--ink-2);
  transition:background var(--dur) var(--e), color var(--dur) var(--e);
}
.billing__opt input:checked + span{ background:var(--red); color:var(--white) }
.billing__opt input:focus-visible + span{ outline:2px solid var(--ink); outline-offset:2px }
.billing__opt input:not(:checked):hover + span{ color:var(--ink) }
.billing__save{
  margin-left:var(--s3); padding:6px var(--s4); border-radius:999px;
  background:var(--blush); color:var(--red);
  font-size:var(--t-xs); font-weight:700;
}
@media (max-width:520px){ .billing__save{ display:none } }

/* Step-through flow — three steps as tabs rather than three cards side by side, because the sequence is the point. Built on the WAI-ARIA tabs pattern with roving tabindex, and degrades to three stacked sections with no JS. */
.flow__tabs{
  display:flex; gap:var(--s2); flex-wrap:wrap;
  border-bottom:1px solid var(--line);
  margin-bottom:var(--s7);
}
.flow__tab{
  display:flex; align-items:baseline; gap:var(--s3);
  padding:var(--s4) var(--s4) var(--s4) 0;
  margin-bottom:-1px;
  border:0; border-bottom:2px solid transparent;
  background:none; cursor:pointer;
  font-size:var(--t-sm); font-weight:700; color:var(--ink-3);
  transition:color var(--dur) var(--e), border-color var(--dur) var(--e);
}
.flow__tab:hover{ color:var(--ink) }
.flow__tab[aria-selected="true"]{ color:var(--ink); border-bottom-color:var(--red) }
.flow__tab:focus-visible{ outline:2px solid var(--ink); outline-offset:3px; border-radius:var(--r-sm) }
.flow__tab i{ font-style:normal; font-variant-numeric:tabular-nums; color:var(--red); font-weight:800 }
.flow__tab[aria-selected="false"] i{ color:var(--ink-3) }
@media (max-width:640px){
  .flow__tabs{ gap:0 }
  .flow__tab{ flex:1 1 auto; justify-content:center; padding-inline:var(--s2) }
  .flow__tab span{ display:none }
}

.flow__grid{ display:grid; gap:var(--s6); align-items:center }
@media (min-width:900px){ .flow__grid{ grid-template-columns:1.15fr .85fr; gap:var(--s8) } }
.flow__num{
  display:block; font-size:clamp(3rem,6vw,4.5rem); font-weight:800;
  line-height:1; letter-spacing:-.04em; color:var(--red-soft);
}
.flow__panel h3{ margin:var(--s3) 0 var(--s4) }
.flow__media{ border-radius:var(--r-lg); overflow:hidden; background:var(--sand) }
.flow__media img,.flow__media video{ width:100%; display:block }

/* Rotation-in, echoing .stage__frame rather than inventing a second motion language. transform is untouched by .rv (which moves on translate), so the two compose. Y-axis, not X: this sits in a column, so it swings open like a page. .js-gated and reduced-motion-safe. */
@media (min-width:900px){
  .js .flow__media{
    transform:perspective(1400px) rotateY(-9deg) scale(.96);
    transition:transform 700ms var(--e);
  }
  .js .flow__media.is-in{ transform:none }
}
@media (prefers-reduced-motion: reduce){
  .js .flow__media{ transform:none; transition:none }
}

/* Hiding is the ENHANCEMENT, so it is gated on .js. Panels used to carry [hidden], which base.css pins with display:none !important — so the no-JS fallback showed one step out of three and no author rule could override it. */
html:not(.js) .flow__tabs{ display:none }
html:not(.js) .flow__panel + .flow__panel{ margin-top:var(--s8) }
html.js .flow__panel{ display:none }
html.js .flow__panel.is-active{ display:block }

/* The console tour, capped well below the content column: at full width the mock dominated the page and the 1920px source was upscaled on a 2x display. 940px means it is downsampled even on Retina. */
.showcase--tour{ max-width:940px; margin-inline:auto }
/* 1920x780 — the console page cropped to its content, so the frame carries no dead space and the section stays short. */
.tour{ width:100%; display:block; aspect-ratio:1920 / 780; object-fit:cover; background:var(--sand) }
.showcase--tour .hotspot__list{ max-width:940px; margin-inline:auto }
/* A video, not a screenshot with markers, so the dot-alignment insets and centred layout above do not apply — and forced into four narrow centred columns the App Store caption wraps to three ragged lines. */
@media (min-width:1040px){
  .showcase--tour .hotspot__list{
    grid-template-columns:repeat(2,1fr);
    column-gap:var(--s7);
    row-gap:var(--s4);
    padding-inline:0;
  }
  .showcase--tour .hotspot__list li{
    flex-direction:row;
    align-items:flex-start;
    text-align:left;
    gap:var(--s3);
  }
}

/* Device mocks tilt on a SHARED perspective, so they rotate around one vanishing point rather than each in its own 3D space. Each turns away from the centre so the pair opens toward the reader; only transform and box-shadow animate. */
.devices{ perspective:1500px; perspective-origin:50% 45% }
.laptop,.phone{
  transition:transform .55s cubic-bezier(.22,1,.36,1);
  transform-style:preserve-3d;
  will-change:transform;
}
.laptop:hover{ transform:perspective(1500px) rotateY(5deg) rotateX(2.5deg) translateZ(18px) }
.phone:hover{  transform:perspective(1500px) rotateY(-9deg) rotateX(2.5deg) translateZ(24px) }
/* Keyboard parity — the mocks are not focusable themselves, so lift the pair when anything inside them takes focus. */
.laptop:focus-within{ transform:perspective(1500px) rotateY(5deg) rotateX(2.5deg) translateZ(18px) }
.phone:focus-within{  transform:perspective(1500px) rotateY(-9deg) rotateX(2.5deg) translateZ(24px) }

@media (prefers-reduced-motion:reduce){
  .laptop,.phone{ transition:none }
  .laptop:hover,.phone:hover,
  .laptop:focus-within,.phone:focus-within{ transform:none }
}
