/* ============================================================
   RANK EDGE, inner pages  —  scoped stylesheet

   This is the site's design system (site.css + inner.css) with
   every selector rewritten to sit under .rankedge-seo, plus a
   short WordPress adjustments block at the very bottom.

   Why it had to be scoped. The original sets, globally:
       body { font-family: Urbanist; font-size: 17px; ... }
       a    { color: inherit }
       p    { margin: 0 }
       img  { max-width: 100%; display: block }
       *    { box-sizing: border-box }
   and uses class names as generic as .btn, .head, .wrap, .band,
   .dark, .light, .stat, .form and .review. Dropped raw onto a
   WordPress page all of that reaches the Elementor header and
   footer: the header would take the Urbanist font, its links
   would lose their colour to a{color:inherit}, and the footer
   would lose its paragraph spacing to p{margin:0}. Scoped, none
   of it can reach past the wrapper div. Verified by rendering
   the page with a mock header and footer outside the wrapper and
   confirming both kept every one of their own styles.

   Every selector was rewritten. Three global rules could not be
   and were dropped rather than left to leak:
       html { scroll-behavior: smooth }   (twice, one of them in
              the reduced-motion block)
       html { overflow-x: clip }          (kept, on the wrapper)
   In-page anchor links therefore jump rather than glide.

   DO NOT hand-edit this file. It is generated from site.css and
   inner.css in the static build. Edit those and re-run the
   scoping pass, or WordPress and the static site drift apart.

   Paths inside url() resolve against THIS file, so assets/ must
   stay a sibling folder.
   ============================================================ */

/* ============================================================
   THEME NEUTRALISATION
   Prepended by the build, BEFORE the scoped design system.

   The standalone page owned the whole document, so headings and
   paragraphs simply inherited their colour from the band they
   sat in: .dark set #fff on the section and everything below it
   followed. Under a WordPress theme that stops working, and not
   because of specificity. A theme that writes

       h1, h2, h3 { color: #2b2b4a }
       p          { color: #666 }

   sets the colour ON the element. Inheritance only ever fills in
   a value nothing else supplied, so a direct rule beats it no
   matter how specific the ancestor's rule is: .dark{color:#fff}
   at (0,1,0) still loses to h1{color:#2b2b4a} at (0,0,1),
   because they are not competing for the same thing. That is
   what left the dark-band headings navy on navy.

   The cure is to hand those elements back to inheritance. Every
   rule below is wrapped in :where(), which contributes ZERO
   specificity, so each one weighs exactly (0,1,0) from the
   wrapper class alone: enough to beat any bare element selector
   the theme uses, and light enough that every real rule in the
   design system still wins.

   Order matters as much as specificity here. This block comes
   FIRST so that where the weights tie, the design system's own
   colours win on source order. Moving it after would silently
   flatten .phero-sub, .svc p and every other deliberate colour
   on the page to inherit.
   ============================================================ */

.rankedge-seo :where(h1, h2, h3, h4, h5, h6,
                    p, li, dt, dd, figure, blockquote,
                    time, small, b, strong, em, i, cite, address, label){
  color: inherit;
}

/* Themes set margins on headings and paragraphs too. The design system spaces
   everything with its own rules, so anything it does not explicitly set should
   start from nothing rather than from the theme's rhythm. */
.rankedge-seo :where(h1, h2, h3, h4, h5, h6, p, figure, blockquote, ul, ol){
  margin: 0;
}

/* Blockquote, which this theme decorates hard. Its rule is

       .wp-block-quote, blockquote {
           background:#F1F2F6; padding:35px 50px; border-radius:4px;
           margin-bottom:35px;
           border-left:5px solid var(--tp-theme-primary) !important;
       }

   and that !important on border-left is why the review cards kept a blue bar
   down their left edge even after the rest was overridden. An !important
   declaration can only be answered by another one, so these carry it.

   The reviews no longer use <blockquote> at all — they are plain paragraphs
   now, for exactly this reason — but the reset stays so that a stray one
   pasted into the page later does not arrive pre-dressed. */
.rankedge-seo :where(blockquote){
  padding: 0 !important;
  border: 0 !important;
  background: none !important;
  border-radius: 0 !important;
  font-style: normal;
  quotes: none;
}
.rankedge-seo :where(blockquote)::before,
.rankedge-seo :where(blockquote)::after{ content: none; }

/* ============================================================
   RANK EDGE, shared design system
   A verbatim mirror of the <style> block in index.html, so the
   home page and every inner page draw on one source of truth.
   Tokens, section furniture, buttons, mascot loops, forms and
   footer all live here.

   KEEPING IT IN SYNC  <-- read this before editing either file
   index.html still carries this CSS inline, so right now the two
   are duplicates and this file is the copy. Re-export it after
   any change to that <style> block:
       sed -n '43,815p' index.html  (re-check these numbers)
   Better, retire the duplication altogether by replacing that
   whole <style>...</style> block with:
       <link rel="stylesheet" href="site.css">
   Nothing else about the home page changes.

   Do not do that swap against a stale copy of this file. It has
   already drifted once: the home page rebuilt its services
   section around image cards and dropped the .svc component,
   fourteen rules that the SEO page was still standing on. Those
   now live in inner.css. Swapping a stale mirror back over
   index.html would silently delete whatever the home page has
   gained since the mirror was taken.

   This file sits at the site root, not in assets/, because the
   one url() it contains (the contact galaxy) is written relative
   to the page and a stylesheet resolves url() against itself.

   Inner-page-only rules (page hero, breadcrumb, standing mascot,
   pillar strip, prose brief, and now the service card) live in
   inner.css, so the home page never downloads what it does not use.
   ============================================================ */
.rankedge-seo{
  /* Rank Edge tokens, taken from the prototype */
  --ink:#0A0C18;
  --ink-2:#12142B;
  --paper:#F7F7FB;
  --paper-2:#FFFFFF;
  --purple:#5B38A8;
  --purple-deep:#2E1B5C;
  --blue:#1365A9;
  --cyan:#5BD8FF;
  --muted:#6B6A7B;
  --muted-d:#B7B4D9;
  --line:#E6E4F0;
  --line-d:rgba(255,255,255,.12);
  --field-line:#8F8CA6;

  --font:'Urbanist',system-ui,-apple-system,sans-serif;
  --ease:cubic-bezier(.22,.8,.3,1);
  --ease-soft:cubic-bezier(.4,0,.2,1);
  --hero-t:0;

  --s3:24px; --s4:32px; --s5:48px; --s6:72px; --s7:104px;
}
.rankedge-seo *,
.rankedge-seo *::before,
.rankedge-seo *::after{box-sizing:border-box}
.rankedge-seo{overflow-x:hidden;overflow-x:clip}
.rankedge-seo{
  margin:0;background:var(--paper);color:var(--ink);
  font-family:var(--font);font-size:17px;line-height:1.62;font-weight:400;
  -webkit-font-smoothing:antialiased;
}
.rankedge-seo h1,
.rankedge-seo h2,
.rankedge-seo h3,
.rankedge-seo h4{font-family:var(--font);font-weight:800;line-height:1.06;letter-spacing:-.028em;margin:0}
.rankedge-seo p{margin:0}
.rankedge-seo a{color:inherit}
.rankedge-seo img{max-width:100%;display:block}
.rankedge-seo button{font:inherit}
.rankedge-seo .grad{background:linear-gradient(100deg,var(--cyan),#A78BFA);-webkit-background-clip:text;background-clip:text;color:transparent}
/* on the light half of the hero the cyan gradient has no contrast, so the
   opening beat uses the brand purple-to-blue ramp instead (7.6:1 on paper) */
.rankedge-seo .grad-d{background:linear-gradient(100deg,var(--purple),var(--blue));-webkit-background-clip:text;background-clip:text;color:transparent}
.rankedge-seo .wrap{max-width:1240px;margin:0 auto;padding:0 22px}
@media (min-width:900px){.rankedge-seo .wrap{padding:0 40px}}
.rankedge-seo .band{padding:var(--s7) 0;position:relative;z-index:1}
@media (max-width:760px){.rankedge-seo .band{padding:var(--s6) 0}}
.rankedge-seo .skip{position:absolute;left:0;top:-60px;z-index:400;background:var(--cyan);color:#04202B;padding:14px 20px;font-weight:700;text-decoration:none}
.rankedge-seo .skip:focus{top:0}
.rankedge-seo :focus-visible{outline:3px solid var(--cyan);outline-offset:3px}
.rankedge-seo .sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
/* ---------- loader ---------- */
.rankedge-seo #loader{position:fixed;inset:0;z-index:500;background:#FFF;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:22px;transition:opacity .6s var(--ease-soft),visibility .6s}
.rankedge-seo #loader.gone{opacity:0;visibility:hidden}
.rankedge-seo #loader img{height:38px;width:auto}
.rankedge-seo .ring{width:46px;height:46px;transform:rotate(-90deg)}
.rankedge-seo .ring circle{fill:none;stroke-width:3;stroke-linecap:round}
.rankedge-seo .ring .bg{stroke:rgba(10,12,24,.12)}
.rankedge-seo .ring .fg{stroke:var(--purple);stroke-dasharray:126;stroke-dashoffset:calc(126 - 126*var(--p,0));transition:stroke-dashoffset .2s linear}
/* ---------- nav ---------- */
.rankedge-seo .nav{position:fixed;top:0;left:0;right:0;z-index:200;transition:background .4s var(--ease-soft),border-color .4s var(--ease-soft),backdrop-filter .4s}
.rankedge-seo .nav-in{display:flex;align-items:center;justify-content:space-between;gap:16px;height:74px}
.rankedge-seo .nav-logo img{height:60px;width:auto;transition:filter .4s var(--ease-soft)}
.rankedge-seo .nav-links{display:none;gap:30px;list-style:none;margin:0;padding:0}
.rankedge-seo .nav-links a{text-decoration:none;font-size:15px;font-weight:600;opacity:.82;transition:opacity .25s}
.rankedge-seo .nav-links a:hover{opacity:1}
@media (min-width:960px){.rankedge-seo .nav-links{display:flex}}
/* The nav matches whatever section is currently behind it. Driving this off
   --hero-t alone was wrong: that value stays at 1 once the hero is scrolled
   past, which left the nav dark over every light section below. */
.rankedge-seo .nav{color:var(--ink);background:rgba(255,255,255,.88);border-bottom:1px solid var(--line);backdrop-filter:blur(14px)}
.rankedge-seo.dark-nav .nav{color:#fff;background:rgba(10,12,24,.78);border-bottom-color:var(--line-d)}
.rankedge-seo.dark-nav .nav-logo img{filter:brightness(0) invert(1)}
.rankedge-seo.at-top .nav{background:transparent;border-bottom-color:transparent;backdrop-filter:none}
.rankedge-seo .btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;padding:15px 28px;border-radius:0;font-weight:700;font-size:15.5px;text-decoration:none;border:2px solid transparent;cursor:pointer;transition:transform .3s var(--ease),background .3s var(--ease-soft),color .3s var(--ease-soft),border-color .3s var(--ease-soft),box-shadow .3s var(--ease)}
/* ---------- hover states name their own colour, on purpose ---------------
   None of the three :hover rules below used to mention `color`, on the
   reasonable assumption that not changing a property leaves it alone. On the
   WordPress build it does not, and the reason is source order rather than
   specificity.

   The theme ships Bootstrap, and Bootstrap says

       .btn:hover{color:#212529}

   which is (0,2,0). Our resting colours arrive as `.rankedge-seo a{color:
   inherit}` and `.rankedge-seo .btn-p{color:#fff}`, also (0,2,0) once the
   build has scoped them. A tie is broken by document order and
   rankedge-inner-page.css is enqueued BEFORE bootstrap.css, so Bootstrap
   takes every one of these buttons to #212529 the moment it is hovered.

   On the ghost button that is white-on-hero going to near-black on a
   transparent ground: the dull black that was reported. On the primary it is
   white going near-black on the purple gradient. On the cyan button #212529
   against its own #04202B is close enough that nobody noticed, but it is the
   same defect and it is corrected here too.

   Stating the colour in our own :hover rules settles it at (0,3,0) once
   scoped, which beats Bootstrap's (0,2,0) on specificity and so does not
   depend on which stylesheet is enqueued first. No !important needed.

   Prototype pages load this file unscoped, where `.btn-g:hover` is (0,1,1)
   and there is no Bootstrap to fight; the declarations simply restate what
   those pages already render, so nothing there changes. */
.rankedge-seo .btn-p{background:linear-gradient(100deg,var(--purple),var(--blue));color:#fff;box-shadow:0 10px 30px -12px rgba(91,56,168,.7)}
.rankedge-seo .btn-p:hover{transform:translateY(-2px);box-shadow:0 16px 38px -12px rgba(91,56,168,.85);color:#fff}
.rankedge-seo .btn-g{background:transparent;border-color:currentColor;opacity:.9}
/* inherit, not a literal: the ghost button borrows the colour of whatever
   band it is sitting on, and its border is currentColor, so pinning the text
   would also pin the border. */
.rankedge-seo .btn-g:hover{transform:translateY(-2px);opacity:1;color:inherit}
.rankedge-seo .btn-c{background:var(--cyan);color:#04202B}
/* Hero CTA: a light band sweeps across and then holds before repeating,
   the same shape of effect the prototype uses. Driven by transform rather
   than the prototype's left, so it stays on the compositor. */
.btn.sheen{position:relative;overflow:hidden}
.btn.sheen::after{
  content:"";position:absolute;top:0;left:0;width:40%;height:100%;pointer-events:none;
  background:linear-gradient(115deg,transparent,rgba(255,255,255,.55),transparent);
  animation:btnSheen 2.6s ease-in-out infinite;
}
@keyframes btnSheen{
  0%{transform:translateX(-160%) skewX(-20deg)}
  55%,100%{transform:translateX(325%) skewX(-20deg)}
}
.rankedge-seo .btn-c:hover{transform:translateY(-2px);box-shadow:0 14px 34px -12px rgba(91,216,255,.7);color:#04202B}
@media (pointer:coarse){.rankedge-seo .btn{min-height:48px}}
.rankedge-seo .nav-cta{display:none}
@media (min-width:960px){.rankedge-seo .nav-cta{display:inline-flex}}
.rankedge-seo .menu-btn{width:46px;height:46px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:1.5px solid currentColor;border-radius:0;color:inherit;cursor:pointer;opacity:.8}
@media (min-width:960px){.rankedge-seo .menu-btn{display:none}}
.rankedge-seo .sheet{position:fixed;inset:0 0 0 auto;width:min(330px,88vw);z-index:320;background:var(--ink);color:#fff;padding:24px;display:flex;flex-direction:column;gap:26px;transform:translateX(101%);transition:transform .42s var(--ease)}
.rankedge-seo .sheet.open{transform:translateX(0)}
.rankedge-seo .sheet ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:18px}
.rankedge-seo .sheet a{text-decoration:none;font-size:20px;font-weight:700}
.rankedge-seo .veil{position:fixed;inset:0;z-index:310;background:rgba(6,7,16,.6);opacity:0;pointer-events:none;transition:opacity .35s var(--ease-soft)}
.rankedge-seo .veil.open{opacity:1;pointer-events:auto}
/* ---------- hero scrub ---------- */
/* Sets how far you scroll to clear the hero: 260vh is ~1440px at a 900px
   viewport, down from ~2520px at 380vh. Shortening it makes the scrub finer,
   not coarser (~12px of scroll per frame against the old ~21px), so there is
   no stepping cost; what it does cost is dwell time on the middle beats. */
.rankedge-seo .hero{height:260vh;position:relative;z-index:1}
.rankedge-seo .scrub-stage{position:sticky;top:0;height:100vh;height:100svh;overflow:hidden}
/* Nova keeps flying while the page is still at rest. The scrub only advances
   with scroll, so before the first scroll the sequence sat frozen on frame 1.
   Drifting through the frames instead is not an option: 121 frames cover the
   source at close to its own rate (adjacent frames are ~22dB apart), so playing
   them at hover speed would run about four times slower than the footage and
   strobe. Floating the rendered frame stays smooth at 60fps and costs no
   redraws at all, because the compositor owns the transform.
   No scale headroom is needed any more: both the desktop and mobile fits now
   letterbox the frame by far more than the 13px the float travels, and that gap
   is already filled and feathered, so a bare translate stays seamless. */
.rankedge-seo .hero canvas{
  width:100%;height:100%;display:block;
  transform-origin:50% 50%;
  animation:heroIdle 5s ease-in-out infinite;
  will-change:transform;
}
@keyframes heroIdle{
  0%,100%{transform:translate3d(0,0,0) scale(var(--idle-s,1))}
  /* --idle is driven from the scrub: 1 at rest, 0 once scrolling has started,
     so the drift never fights the frames once they are carrying the motion */
  50%{transform:translate3d(0,calc(var(--idle,1) * -13px),0) scale(var(--idle-s,1))}
}
/* no-scrub is the reduced-motion presentation, which never runs the scrub loop
   and so would never damp --idle back down */
.rankedge-seo .hero.no-scrub canvas{animation:none;transform:none}
.rankedge-seo .hero-ui{position:absolute;inset:0;display:flex;align-items:center;pointer-events:none}
.rankedge-seo .hero-ui .wrap{width:100%}
.rankedge-seo .hero-beat{position:absolute;left:0;right:0;padding:0 22px;max-width:1240px;margin:0 auto;opacity:0}
@media (min-width:900px){.rankedge-seo .hero-beat{padding:0 40px}}
.rankedge-seo .hero-beat.b1{max-width:none}
.rankedge-seo .hero-beat .in{max-width:1240px;margin:0 auto}
.rankedge-seo .kick{display:inline-flex;align-items:center;gap:10px;font-size:12.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;margin-bottom:20px}
.rankedge-seo .kick::before{content:"";width:26px;height:2px;background:currentColor;border-radius:2px}
.rankedge-seo .hero h1{font-size:clamp(2.6rem,6.4vw,5.1rem);font-weight:900;max-width:15ch}
.rankedge-seo .hero-sub{margin-top:20px;font-size:clamp(1rem,1.5vw,1.18rem);max-width:46ch;opacity:.86}
.rankedge-seo .hero-btns{display:flex;flex-wrap:wrap;gap:14px;margin-top:34px;pointer-events:auto}
.rankedge-seo .beat-mid{text-align:center}
.rankedge-seo .beat-mid h2{font-size:clamp(1.9rem,4.6vw,3.4rem);font-weight:900;max-width:16ch;margin:0 auto}
.rankedge-seo .scroll-hint{position:absolute;left:50%;bottom:26px;transform:translateX(-50%);font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;font-weight:700;opacity:.55;display:flex;flex-direction:column;align-items:center;gap:8px}
.rankedge-seo .scroll-hint i{width:1px;height:34px;background:currentColor;display:block;animation:drop 1.9s var(--ease) infinite}
@keyframes drop{0%{transform:scaleY(0);transform-origin:top}45%{transform:scaleY(1);transform-origin:top}55%{transform:scaleY(1);transform-origin:bottom}100%{transform:scaleY(0);transform-origin:bottom}}
/* reduced motion: one screen, no pinned scroll, opening frame + opening beat */
.rankedge-seo .hero.no-scrub{height:100vh;height:100svh}
.rankedge-seo .hero.no-scrub .scroll-hint{display:none}
/* Phones cover-fill the frame, so the opening copy needs a scrim. It is keyed
   to --hero-t: solid while the scene is white and the copy is dark ink, gone by
   the time the scene is in space and the copy has switched to white. */
@media (max-width:759px){/* 380vh is ~2340px of travel before the next section, which is three to five
     flicks on a phone and reads as the page not responding. 220vh brings that
     to ~1040px. The sequence stays smooth because phones also run half the
     frames, leaving ~17px of scroll per frame against the desktop's ~21px. */
.rankedge-seo .hero{height:220vh}
.rankedge-seo .hero-ui{align-items:flex-start}
.rankedge-seo .hero-beat.b1{top:96px}
.rankedge-seo .hero-beat.b1::before{
    content:"";position:absolute;inset:-40px -22px;z-index:-1;pointer-events:none;
    background:linear-gradient(180deg,
      rgba(247,247,251,calc(.95*(1 - var(--hero-t)))) 0%,
      rgba(247,247,251,calc(.88*(1 - var(--hero-t)))) 62%,
      rgba(247,247,251,0) 100%);
  }
/* column, or the heading and the stat row become side-by-side flex siblings */
.rankedge-seo .beat-mid{top:0;display:flex;flex-direction:column;justify-content:center;min-height:100%}
.rankedge-seo .beat-mid h2{margin:0 auto}
.rankedge-seo .hero h1{font-size:clamp(2.2rem,9vw,3rem)}
.rankedge-seo .hero-sub{font-size:1rem}}
/* ---------- generic section furniture ---------- */
.rankedge-seo .dark{background:var(--ink);color:#fff}
.rankedge-seo .dark .eyebrow{color:var(--cyan)}
.rankedge-seo .light{background:var(--paper)}
.rankedge-seo .white{background:var(--paper-2)}
.rankedge-seo .eyebrow{font-size:12.5px;font-weight:800;letter-spacing:.15em;text-transform:uppercase;color:var(--purple);display:flex;align-items:center;gap:10px;margin-bottom:18px}
.rankedge-seo .eyebrow::before{content:"";width:24px;height:2px;background:currentColor;border-radius:2px}
.rankedge-seo .head{max-width:62ch;margin-bottom:var(--s6)}
/* centred variant: used above the services timeline, which is symmetric about
   its rail, so a left-aligned head would sit off-balance over it */
.rankedge-seo .head-c{max-width:70ch;margin-left:auto;margin-right:auto;text-align:center}
.rankedge-seo .head-c h2{max-width:20ch;margin-left:auto;margin-right:auto}
.rankedge-seo .head-c p{margin-left:auto;margin-right:auto}
.rankedge-seo .head-c .eyebrow{justify-content:center}
.rankedge-seo .head h2{font-size:clamp(1.9rem,4.2vw,3.1rem);max-width:17ch}
.rankedge-seo .head p{margin-top:20px;font-size:1.06rem;max-width:56ch;color:var(--muted)}
.rankedge-seo .dark .head p{color:var(--muted-d)}
.rankedge-seo .rv{opacity:0;transform:translateY(22px);transition:opacity .75s var(--ease),transform .75s var(--ease)}
.rankedge-seo .rv.in{opacity:1;transform:none}
.rankedge-seo .stg .rv:nth-child(2){transition-delay:.07s}
.rankedge-seo .stg .rv:nth-child(3){transition-delay:.14s}
.rankedge-seo .stg .rv:nth-child(4){transition-delay:.21s}
.rankedge-seo .stg .rv:nth-child(5){transition-delay:.28s}
.rankedge-seo .stg .rv:nth-child(6){transition-delay:.35s}
/* Retire the stagger once a card has arrived. Without this the delay stays on
   the element forever, so every later hover is held back by up to 0.35s and
   feels sluggish. !important because the nth-child rules above outrank it. */
.rankedge-seo .rv.settled{transition-delay:0s!important}
/* ---------- per-section entrances ----------------------------------------
   Declared after the .stg block on purpose: the process stagger below has the
   same specificity as .stg .rv:nth-child(), so source order is what decides.
   Each group gets a motion that echoes what its content is doing.
   ------------------------------------------------------------------------ */
/* PROCESS: the four steps sit on a descending diagonal, so they arrive along
   it, dropping in from up-and-left and settling into the stair one by one. */
.rankedge-seo .step.rv{transform:translate(-34px,-30px) rotate(-2.6deg) scale(.95);
  transition:opacity .72s var(--ease),transform .86s var(--ease)}
.rankedge-seo .step.rv.in{transform:none}
.rankedge-seo .proc .rv:nth-child(1){transition-delay:0s}
.rankedge-seo .proc .rv:nth-child(2){transition-delay:.14s}
.rankedge-seo .proc .rv:nth-child(3){transition-delay:.28s}
.rankedge-seo .proc .rv:nth-child(4){transition-delay:.42s}
/* SERVICES: "one story, six chapters" — each card tips open from its top edge
   like a page turning, in reading order. */
.rankedge-seo .tl-a .tl-copy.rv{transform:translateX(-38px);
  transition:opacity .7s var(--ease),transform .8s var(--ease)}
.rankedge-seo .tl-b .tl-copy.rv{transform:translateX(38px);
  transition:opacity .7s var(--ease),transform .8s var(--ease)}
.rankedge-seo .tl-a .tl-copy.rv.in,
.rankedge-seo .tl-b .tl-copy.rv.in{transform:none}
/* FUTURE-READY: three parallel claims rather than a sequence, so they expand
   into place from their own centres instead of arriving in order. */
.rankedge-seo .wwd-i.rv{transform:scale(.91);
  transition:opacity .62s var(--ease),transform .74s var(--ease)}
.rankedge-seo .wwd-i.rv.in{transform:none}
/* ...and Nova drives in from the right of the same heading, overshooting a
   touch before she settles so it reads as braking rather than sliding. */
.rankedge-seo .drive-art.rv{transform:translateX(64px);
  transition:opacity .5s var(--ease),transform .92s cubic-bezier(.16,.84,.3,1.04)}
.rankedge-seo .drive-art.rv.in{transform:none}
/* ---------- stats ---------- */
.rankedge-seo .stats{display:grid;gap:28px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));margin-top:var(--s5)}
.rankedge-seo .stat b{display:block;font-size:clamp(2.1rem,4.6vw,3.2rem);font-weight:900;letter-spacing:-.035em;line-height:1;background:linear-gradient(100deg,var(--purple),var(--blue));-webkit-background-clip:text;background-clip:text;color:transparent}
.rankedge-seo .dark .stat b{background:linear-gradient(100deg,var(--cyan),#A78BFA);-webkit-background-clip:text;background-clip:text;color:transparent}
.rankedge-seo .stat span{display:block;margin-top:8px;font-size:14px;font-weight:600;color:var(--muted)}
.rankedge-seo .dark .stat span{color:var(--muted-d)}
/* ---------- section head with mascot alongside ---------- */
.rankedge-seo .head-with-art{margin-bottom:var(--s6)}
.rankedge-seo .head-art{display:none}
@media (min-width:1000px){.rankedge-seo .head-with-art{display:grid;grid-template-columns:1fr auto;gap:var(--s5);align-items:end}
.rankedge-seo .head-art{display:block;justify-self:end;width:clamp(190px,19vw,266px);position:relative}
.rankedge-seo .head-art img{
    width:100%;height:auto;position:relative;
    /* a different period from Nova in the roof section so the two are never
       in lockstep, which would read as one looping asset used twice */
    animation:novaBob 5.2s ease-in-out infinite;
    animation-delay:-1.1s;
    will-change:transform;
  }
.rankedge-seo .head-art::before{
    content:"";position:absolute;left:50%;bottom:-2%;width:58%;height:22%;
    transform:translateX(-50%);border-radius:50%;pointer-events:none;
    background:radial-gradient(ellipse at center,rgba(91,216,255,.42) 0%,rgba(91,56,168,.2) 45%,transparent 72%);
    filter:blur(15px);
    animation:novaThruster 5.2s ease-in-out infinite;
    animation-delay:-1.1s;
  }}
@keyframes novaBob{
  0%,100%{transform:translateY(0) rotate(0deg)}
  50%{transform:translateY(-14px) rotate(1.4deg)}
}
.rankedge-seo.paused .head-art img,
.rankedge-seo.paused .head-art::before{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .head-art img{animation:none;transform:none}
.rankedge-seo .head-art::before{animation:none;opacity:.7}}
/* ---------- services: the six chapters as a timeline ----------
   A centre rail with numbered nodes, copy alternating either side. The column
   opposite the copy is deliberately left empty on every row: that is the lane
   the single mascot flies down, so she never has to sit over text. */
.rankedge-seo .tl{position:relative;max-width:1120px;margin:0 auto}
/* the rail itself, faded at both ends so it does not butt into the section */
.rankedge-seo .tl::before{
  content:"";position:absolute;top:0;bottom:0;left:50%;width:2px;transform:translateX(-50%);
  background:linear-gradient(180deg,transparent,var(--line) 4%,var(--line) 96%,transparent);
}
/* fills from the top as the section is read, so the rail doubles as a progress bar */
.rankedge-seo .tl-fill{
  position:absolute;top:0;left:50%;width:2px;height:0;transform:translateX(-50%);
  background:linear-gradient(180deg,var(--purple),var(--blue));
  z-index:1;will-change:height;
}
.rankedge-seo .tl-row{position:relative;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;padding:56px 0}
.rankedge-seo .tl-copy{padding:0 48px;grid-row:1}
.rankedge-seo .tl-node{grid-column:2;grid-row:1;display:flex;align-items:center;justify-content:center;position:relative;z-index:3}
/* copy left on odd rows, right on even; the node stays on the rail throughout */
.rankedge-seo .tl-a .tl-copy{grid-column:1;text-align:right}
.rankedge-seo .tl-b .tl-copy{grid-column:3;text-align:left}
.rankedge-seo .tl-a .tl-tag{margin-left:auto}
.rankedge-seo .tl-a .tl-copy li{padding-left:0;padding-right:22px;justify-content:flex-end}
.rankedge-seo .tl-a .tl-copy li::before{order:2}
.rankedge-seo .tl-node span{
  width:64px;height:64px;border-radius:50%;background:var(--paper-2);
  border:2px solid var(--line);display:flex;align-items:center;justify-content:center;
  font-weight:800;font-size:17px;color:var(--purple);
  box-shadow:0 8px 24px rgba(18,17,28,.06);
  transition:background .5s var(--ease-soft),border-color .5s var(--ease-soft),color .5s var(--ease-soft),transform .5s var(--ease),box-shadow .5s var(--ease);
}
.rankedge-seo .tl-node.on span{
  background:linear-gradient(135deg,var(--purple),var(--blue));
  border-color:transparent;color:#fff;transform:scale(1.08);
  box-shadow:0 10px 28px rgba(91,56,168,.35);
}
/* scoped past .tl-copy p, which is more specific than a bare .tl-tag and was
   silently overriding this to the body-copy size */
.rankedge-seo .tl-copy .tl-tag{
  display:inline-block;font-size:11.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  color:var(--blue);background:rgba(19,101,169,.08);padding:6px 14px;margin-bottom:16px;
}
.rankedge-seo .tl-copy h3{font-size:1.35rem;margin-bottom:12px}
/* grad-d gives these the purple-to-blue fill; this adds a lighter band that
   travels across it. background-position runs 100% -> 0% because with a
   background wider than the box that is what moves the band left to right. */
.rankedge-seo .tl-copy h3.grad-d{
  background:linear-gradient(100deg,
    var(--purple) 0%,var(--purple) 32%,#C4B5FD 46%,#93C5FD 54%,var(--blue) 68%,var(--blue) 100%);
  background-size:260% 100%;
  -webkit-background-clip:text;background-clip:text;color:transparent;
  animation:tlShine 4.6s linear infinite;
}
/* Offset per row so the six never sweep in lockstep. Counted with sibling
   chains rather than nth-child: the fill and the flyer are also children of
   .tl, so nth-child would be off by two and drift again if either moved. */
.rankedge-seo .tl-row + .tl-row h3.grad-d{animation-delay:-.7s}
.rankedge-seo .tl-row + .tl-row + .tl-row h3.grad-d{animation-delay:-1.4s}
.rankedge-seo .tl-row + .tl-row + .tl-row + .tl-row h3.grad-d{animation-delay:-2.1s}
.rankedge-seo .tl-row + .tl-row + .tl-row + .tl-row + .tl-row h3.grad-d{animation-delay:-2.8s}
.rankedge-seo .tl-row + .tl-row + .tl-row + .tl-row + .tl-row + .tl-row h3.grad-d{animation-delay:-3.5s}
@keyframes tlShine{from{background-position:100% 0}to{background-position:0% 0}}
.rankedge-seo .tl-copy p{color:var(--muted);font-size:16px}
.rankedge-seo .tl-copy ul{list-style:none;margin:18px 0 0;padding:0;display:flex;flex-direction:column;gap:9px}
.rankedge-seo .tl-copy li{font-size:16px;color:var(--muted);display:flex;gap:10px;align-items:flex-start;padding-left:22px}
.rankedge-seo .tl-copy li::before{content:"";flex:none;width:6px;height:6px;background:var(--purple);margin-top:8px}
.rankedge-seo .tl-more{
  display:inline-flex;align-items:center;gap:7px;margin-top:20px;
  font-size:14px;font-weight:700;color:var(--purple);text-decoration:none;
  border-bottom:1px solid rgba(91,56,168,.3);padding-bottom:3px;transition:gap .2s,border-color .2s
}
.rankedge-seo .tl-more:hover{gap:11px;border-color:var(--blue)}
.rankedge-seo .tl-a .tl-more{flex-direction:row-reverse}
.rankedge-seo .tl-a .tl-more svg{transform:scaleX(-1)}
.rankedge-seo .tl-more svg{width:15px;height:15px}
/* The mascot, flown down the empty lane and swapped for the one belonging to
   each chapter as she arrives. Position is written as a transform from the
   scrub loop; the artwork cross-fades on the same crossing parameter, so she
   changes mid-flight and always lands as the right one.
   The six illustrations come at aspect ratios from 0.65 to 0.90, so the box is
   sized by height and each is centred inside it. Sizing by width instead would
   make the character noticeably bigger in the narrow frames than the wide. */
.rankedge-seo .tl-flyer{
  position:absolute;top:0;left:0;z-index:2;
  width:clamp(190px,21vw,290px);height:clamp(190px,21vw,290px);
  pointer-events:none;
  /* opacity is written per frame: in one column she fades out while moving
     between bands, so a CSS transition here would smear the dip */
  opacity:0;
  will-change:transform,opacity;
}
.rankedge-seo .tl-flyer img{
  position:absolute;inset:0;margin:auto;
  height:100%;width:auto;max-width:100%;object-fit:contain;
  opacity:0;
  /* identical duration and delay on all six, so they stay in phase through a
     cross-fade and the swap never reads as a jump */
  animation:novaHover 6.1s ease-in-out infinite;animation-delay:-1.7s;
}
/* One column under 900px: the rail moves to the left edge and every row reads
   left-to-right. There is no lane beside the copy any more, so instead each row
   gets an empty band above it and she moves straight down through those, which
   is the one path in this layout that never crosses text. */
@media (max-width:899px){.rankedge-seo .tl::before,
.rankedge-seo .tl-fill{left:22px}
/* the top padding IS the band she rides; the JS reads this value back rather
     than hard-coding it, so changing it here moves her with it. It has to stay
     taller than .tl-flyer or she starts overlapping the copy below her. */
.rankedge-seo .tl-row{grid-template-columns:auto 1fr;padding:230px 0 30px}
.rankedge-seo .tl-a .tl-copy,
.rankedge-seo .tl-b .tl-copy{grid-column:2;grid-row:1;text-align:left;padding:0 0 0 22px}
.rankedge-seo .tl-node{grid-column:1;grid-row:1}
.rankedge-seo .tl-node span{width:44px;height:44px;font-size:14px}
.rankedge-seo .tl-a .tl-tag{margin-left:0}
.rankedge-seo .tl-a .tl-copy li{padding-left:22px;padding-right:0;justify-content:flex-start}
.rankedge-seo .tl-a .tl-copy li::before{order:0}
.rankedge-seo .tl-a .tl-more{flex-direction:row}
.rankedge-seo .tl-a .tl-more svg{transform:none}
/* Sized by height, because the art is height:100%/width:auto: at 200 the
     widest frame renders about 180px across and the narrowest about 130px.
     Pushing height further to make the narrow ones 200px wide would need a
     band deeper than a phone screen is tall. */
.rankedge-seo .tl-flyer{width:200px;height:200px}
/* the copy slides in from its own side of the rail on desktop, which is what
     makes the alternation read. In one column every row is left-aligned, so a
     per-side slide is just inconsistent, and the leftward one carries the copy
     under the node circle on its way in. Back to the plain lift. */
.rankedge-seo .tl-a .tl-copy.rv,
.rankedge-seo .tl-b .tl-copy.rv{transform:translateY(22px)}
.rankedge-seo .tl-a .tl-copy.rv.in,
.rankedge-seo .tl-b .tl-copy.rv.in{transform:none}}
/* ---------- process ---------- */
.rankedge-seo .proc{display:grid;gap:18px;grid-template-columns:1fr}
@media (min-width:640px){.rankedge-seo .proc{grid-template-columns:1fr 1fr}}
/* Diagonal staircase: each step sits lower than the one before, so the four
   cards read as a descending sequence rather than four equal boxes.
   margin-top (not transform) so the grid row grows to contain the last card. */
@media (min-width:1060px){.rankedge-seo .proc{grid-template-columns:repeat(4,1fr);align-items:start;gap:20px}
.rankedge-seo .proc .step:nth-child(1){margin-top:0}
.rankedge-seo .proc .step:nth-child(2){margin-top:58px}
.rankedge-seo .proc .step:nth-child(3){margin-top:116px}
.rankedge-seo .proc .step:nth-child(4){margin-top:174px}}
/* No reduced-motion override here on purpose: the cascade is a static layout,
   not an animation. Nothing moves, so there is nothing to reduce, and the
   scroll reveal that does animate is already handled globally. */
/* Cards on dark bands are a translucent wash of the brand purple rather than
   white at 3%, which was almost invisible. The tint reads as a surface and
   ties the cards to the palette; the blur lets the band behind show through. */
.rankedge-seo .step{
  border:1px solid rgba(139,110,220,.22);border-radius:20px;padding:28px;position:relative;overflow:hidden;
  background:linear-gradient(160deg,rgba(91,56,168,.26) 0%,rgba(19,101,169,.14) 100%);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  transition:transform .4s var(--ease),border-color .4s var(--ease-soft),background .4s var(--ease-soft);
}
.rankedge-seo .step:hover{transform:translateY(-5px);border-color:rgba(91,216,255,.45)}
/* the step marker sits in a pill, like the reference's category badge */
.rankedge-seo .step-n{
  display:inline-block;font-size:11px;font-weight:800;letter-spacing:.14em;
  color:var(--cyan);background:rgba(91,216,255,.13);
  border:1px solid rgba(91,216,255,.3);border-radius:8px;
  padding:6px 12px;margin-bottom:18px;
}
.rankedge-seo .step h3{font-size:1.2rem;margin-bottom:10px}
.rankedge-seo .step p{color:var(--muted-d);font-size:15px}
.rankedge-seo .step-bar{position:absolute;left:0;bottom:0;height:3px;background:linear-gradient(90deg,var(--cyan),#A78BFA);width:100%;transform:scaleX(0);transform-origin:left;transition:transform .9s var(--ease)}
.rankedge-seo .step.in .step-bar{transform:scaleX(1)}
/* ---------- results, showreel background ---------- */
.rankedge-seo .reel{position:relative;overflow:hidden;isolation:isolate}
.rankedge-seo .reel-media{position:absolute;inset:0;z-index:-2;background:var(--ink) center/cover no-repeat}
.rankedge-seo .reel-media video{width:100%;height:100%;object-fit:cover;display:block;opacity:0;transition:opacity 1.1s var(--ease-soft)}
.rankedge-seo .reel-media video.on{opacity:1}
.rankedge-seo .reel-scrim{
  position:absolute;inset:0;z-index:-1;pointer-events:none;
  /* the showreel is busy and carries its own on-screen type, so the band is
     dimmed hard and darkest through the middle where the figures sit */
  background:
    linear-gradient(180deg,var(--ink) 0%,rgba(10,12,24,.72) 12%,rgba(10,12,24,.72) 88%,var(--ink) 100%),
    radial-gradient(ellipse 96% 74% at 50% 50%,rgba(10,12,24,.34) 0%,rgba(10,12,24,.2) 62%,rgba(10,12,24,.1) 100%);
}
/* the figures sit on their own glass panel, so the reel can stay legible in the
   gaps without the numbers ever competing with it */
.rankedge-seo .reel .res{
  background:rgba(9,11,21,.72);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
  border-color:rgba(255,255,255,.16);
}
.rankedge-seo .reel .head h2,
.rankedge-seo .reel .head p,
.rankedge-seo .reel .reel-tag{text-shadow:0 2px 14px rgba(6,7,16,.85)}
.rankedge-seo .reel-tag{
  display:inline-flex;align-items:center;gap:9px;margin-top:22px;
  font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-d)
}
.rankedge-seo .reel-tag i{width:7px;height:7px;border-radius:50%;background:var(--cyan);flex:none;box-shadow:0 0 10px var(--cyan)}
.rankedge-seo .res-grid{display:grid;gap:20px;grid-template-columns:1fr}
@media (min-width:700px){.rankedge-seo .res-grid{grid-template-columns:1fr 1fr}}
.rankedge-seo .res{border:1px solid var(--line-d);border-radius:20px;padding:28px;background:rgba(255,255,255,.03)}
.rankedge-seo .res h3{font-size:1.32rem;font-weight:800;letter-spacing:.03em;text-transform:uppercase;color:var(--cyan);margin-bottom:20px}
.rankedge-seo .res-row{display:flex;justify-content:space-between;align-items:baseline;gap:16px;padding:12px 0;border-top:1px solid var(--line-d)}
.rankedge-seo .res-row b{font-size:1.7rem;font-weight:900;letter-spacing:-.03em}
.rankedge-seo .res-row span{font-size:14px;color:var(--muted-d);text-align:right}
/* ---------- roof ---------- */
.rankedge-seo .roof{display:grid;gap:var(--s5);align-items:center}
@media (min-width:920px){.rankedge-seo .roof{grid-template-columns:1fr .9fr;gap:var(--s6)}}
.rankedge-seo .roof-list{display:flex;flex-wrap:wrap;gap:12px;margin-top:var(--s4)}
.rankedge-seo .chip{border:1px solid var(--line);background:var(--paper-2);border-radius:0;padding:11px 20px;font-size:14.5px;font-weight:600}
/* Nova drifts as if hovering on her thruster. The float lives on the <img> while
   the .rv entrance stays on .roof-art, so the entrance transform and the loop
   never write to the same element. */
.rankedge-seo .roof-art{justify-self:center;max-width:330px;position:relative}
.rankedge-seo .roof-art::before{
  content:"";position:absolute;left:50%;bottom:-4%;width:62%;height:26%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.5) 0%,rgba(19,101,169,.22) 42%,transparent 72%);
  filter:blur(16px);
  animation:novaThruster 6.5s ease-in-out infinite;
  animation-delay:-2.2s;
}
.rankedge-seo .roof-art img{
  position:relative;
  animation:novaFloat 6.5s ease-in-out infinite;
  animation-delay:-2.2s;
  will-change:transform;
}
@keyframes novaFloat{
  0%,100%{transform:translateY(0) rotate(0deg)}
  50%{transform:translateY(-18px) rotate(-1.8deg)}
}
@keyframes novaThruster{
  0%,100%{opacity:.55;transform:translateX(-50%) scale(1)}
  50%{opacity:.95;transform:translateX(-50%) scale(1.16)}
}
/* the loop stops with everything else on a hidden tab */
.rankedge-seo.paused .roof-art img,
.rankedge-seo.paused .roof-art::before{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .roof-art img{animation:none;transform:none}
.rankedge-seo .roof-art::before{animation:none;opacity:.7}}
/* ---------- wwd / trust ---------- */
.rankedge-seo .wwd{display:grid;gap:20px;grid-template-columns:1fr}
@media (min-width:860px){.rankedge-seo .wwd{grid-template-columns:repeat(3,1fr)}}
.rankedge-seo .wwd-i{
  border:1px solid rgba(139,110,220,.22);border-radius:20px;padding:28px;
  background:linear-gradient(160deg,rgba(91,56,168,.26) 0%,rgba(19,101,169,.14) 100%);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  transition:transform .4s var(--ease),border-color .4s var(--ease-soft);
}
.rankedge-seo .wwd-i:hover{transform:translateY(-5px);border-color:rgba(91,216,255,.45)}
.rankedge-seo .wwd-i b{display:block;font-size:12px;font-weight:800;letter-spacing:.14em;color:var(--cyan);margin-bottom:14px}
.rankedge-seo .wwd-i h4{font-size:1.1rem;margin-bottom:10px;font-weight:700}
.rankedge-seo .wwd-i p{color:var(--muted-d);font-size:15px}
/* ---------- future ready: Nova takes the wheel ----------
   Same split the other two mascots use: the .rv entrance sits on the wrapper
   and the idle loop on the <img>, so the entrance transform and the looping
   transform never write to the same element and fight each other.
   What differs here is the entrance itself. The other mascots fade upward; a
   car that faded upward would read as floating, so this one drives in from the
   right and settles, which is what makes the asset legible as a vehicle. */
.rankedge-seo .drive-art{position:relative;isolation:isolate;margin:var(--s5) auto 0;width:min(100%,340px)}
@media (min-width:1000px){/* alongside the heading once there is room; the grid is .head-with-art,
     which bottom-aligns the two so the car sits on the baseline of the copy */
.rankedge-seo .drive-art{margin:0;justify-self:end;width:clamp(300px,32vw,460px)}}
.rankedge-seo .drive-art img{
  display:block;width:100%;height:auto;position:relative;
  /* a third period again, distinct from the roof (6.5s) and services (5.2s)
     mascots so the three are never caught in lockstep */
  animation:novaHover 7.4s ease-in-out infinite;
  animation-delay:-3s;
  will-change:transform;
}
/* she is drawn hovering on four lit turbines, so the glow sits wide and low
   under the whole chassis rather than in a single column under a thruster */
.rankedge-seo .drive-art::before{
  content:"";position:absolute;left:50%;bottom:2%;width:80%;height:20%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.46) 0%,rgba(19,101,169,.2) 46%,transparent 72%);
  filter:blur(18px);
  animation:novaThruster 7.4s ease-in-out infinite;
  animation-delay:-3s;
}
/* Motion trails, one shot on arrival only. A perpetual streak loop beside a
   heading pulls the eye off the copy for as long as the section is on screen,
   so these run once as she arrives and are never seen again. */
.rankedge-seo .drive-art::after{
  content:"";position:absolute;z-index:-1;pointer-events:none;opacity:0;
  left:-14%;right:-14%;top:40%;height:30%;
  background:
    linear-gradient(90deg,transparent,rgba(91,216,255,.55) 60%,transparent) left 18%/42% 2px no-repeat,
    linear-gradient(90deg,transparent,rgba(91,216,255,.34) 60%,transparent) left 54%/28% 2px no-repeat,
    linear-gradient(90deg,transparent,rgba(167,139,250,.34) 60%,transparent) left 84%/34% 2px no-repeat;
}
.rankedge-seo .drive-art.rv.in::after{animation:novaStreak .85s var(--ease) .12s 1 both}
@keyframes novaHover{
  0%,100%{transform:translateY(0) rotate(0deg)}
  50%{transform:translateY(-12px) rotate(.9deg)}
}
@keyframes novaStreak{
  0%{opacity:0;transform:translateX(64px)}
  22%{opacity:1}
  100%{opacity:0;transform:translateX(-88px)}
}
.rankedge-seo.paused .drive-art img,
.rankedge-seo.paused .drive-art::before{animation-play-state:paused!important}
/* ---------- trusted by clients ----------
   Two logo rails running in opposite directions. Each row holds its list twice
   and travels exactly -50%, so the second copy lands where the first began and
   the loop is seamless with no jump. */
/* Several of these logos came as JPEGs with a white background baked in.
   On the off-white band that showed as a visible tile behind them, so the
   section is pure white and they blend out. */
.rankedge-seo #clients{background:var(--paper-2)}
.rankedge-seo .trust-stats{display:flex;align-items:center;justify-content:center;gap:40px;flex-wrap:wrap;margin:30px 0 var(--s6)}
.rankedge-seo .trust-stat{display:flex;align-items:center;gap:10px}
.rankedge-seo .trust-stat i{flex:none;width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--blue))}
.rankedge-seo .trust-stat b{font-size:20px;font-weight:800;letter-spacing:-.01em}
.rankedge-seo .trust-stat span{font-size:14px;font-weight:500;color:var(--muted)}
/* full-bleed: the rails sit outside .wrap so they run edge to edge */
.rankedge-seo .trust-rail{
  position:relative;width:100%;overflow:hidden;margin-bottom:8px;
  /* fade both ends rather than cutting the logos off hard */
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
}
.rankedge-seo .trust-row{position:relative;display:flex;width:max-content}
.rankedge-seo .trust-row.a{animation:trustLeft 34s linear infinite}
.rankedge-seo .trust-row.b{animation:trustRight 34s linear infinite}
.rankedge-seo .trust-logo{
  flex:none;width:220px;margin:0 14px;padding:16px 22px;
  display:flex;align-items:center;justify-content:center;
  transition:transform .3s var(--ease);
}
.rankedge-seo .trust-logo img{
  max-width:100%;width:auto;height:auto;object-fit:contain;
  /* held back so the logos read as a texture, and brought forward on hover */
  filter:grayscale(100%);opacity:.6;transition:filter .3s,opacity .3s;
}
.rankedge-seo .trust-logo:hover{transform:translateY(-3px)}
.rankedge-seo .trust-logo:hover img{filter:grayscale(0);opacity:1}
/* let a visitor stop a rail to actually look at a logo */
.rankedge-seo .trust-rail:hover .trust-row{animation-play-state:paused}
@keyframes trustLeft{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@keyframes trustRight{from{transform:translateX(-50%)}to{transform:translateX(0)}}
@media (max-width:640px){.rankedge-seo .trust-stats{gap:24px;margin:24px 0 var(--s5)}
.rankedge-seo .trust-logo{width:150px;margin:0 8px;padding:10px 16px}}
/* ---------- faq ---------- */
.rankedge-seo .faq{max-width:860px;border-top:1px solid var(--line)}
.rankedge-seo .faq-i{border-bottom:1px solid var(--line)}
.rankedge-seo .faq-q{width:100%;display:flex;justify-content:space-between;align-items:center;gap:20px;text-align:left;background:none;border:0;padding:26px 0;cursor:pointer;font-size:1.12rem;font-weight:700;letter-spacing:-.02em;color:var(--ink)}
.rankedge-seo .faq-q:hover{color:var(--purple)}
.rankedge-seo .faq-ic{flex:none;width:20px;height:20px;position:relative}
.rankedge-seo .faq-ic::before,
.rankedge-seo .faq-ic::after{content:"";position:absolute;left:0;top:9px;width:20px;height:2px;background:var(--purple);border-radius:2px;transition:transform .35s var(--ease)}
.rankedge-seo .faq-ic::after{transform:rotate(90deg)}
.rankedge-seo .faq-i[data-open="true"] .faq-ic::after{transform:rotate(0)}
.rankedge-seo .faq-a{max-height:0;overflow:hidden;transition:max-height .42s var(--ease-soft)}
.rankedge-seo .faq-a p{padding:0 0 26px;color:var(--muted);max-width:66ch}
/* ---------- blog ---------- */
.rankedge-seo .blog{display:grid;gap:20px;grid-template-columns:1fr}
@media (min-width:760px){.rankedge-seo .blog{grid-template-columns:repeat(3,1fr)}}
.rankedge-seo .post{background:var(--paper-2);border:1px solid var(--line);border-radius:20px;overflow:hidden;text-decoration:none;display:flex;flex-direction:column;transition:transform .4s var(--ease),box-shadow .4s var(--ease)}
.rankedge-seo .post:hover{transform:translateY(-5px);box-shadow:0 22px 44px -28px rgba(46,27,92,.45)}
/* the three featured images arrive at different aspect ratios, so the frame
   fixes the ratio and the image is cropped into it rather than the cards
   ending up different heights */
.rankedge-seo .post-thumb{position:relative;aspect-ratio:16/10;background:var(--paper);overflow:hidden}
.rankedge-seo .post-thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .5s var(--ease)}
.rankedge-seo .post:hover .post-thumb img{transform:scale(1.05)}
/* the badge sits over the artwork, so it carries its own light chip to stay
   readable whatever the image behind it happens to be */
.rankedge-seo .post .cat{position:absolute;top:14px;left:14px;z-index:1;padding:6px 12px;border-radius:999px;background:rgba(255,255,255,.94);box-shadow:0 4px 14px -6px rgba(46,27,92,.5);font-size:11.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--purple)}
.rankedge-seo .post-body{flex:1;display:flex;flex-direction:column;gap:14px;padding:24px 26px 26px}
.rankedge-seo .post h3{font-size:1.12rem;font-weight:700;line-height:1.28}
.rankedge-seo .post .by{margin-top:auto;padding-top:16px;border-top:1px solid var(--line);font-size:13.5px;color:var(--muted);display:flex;justify-content:flex-end;align-items:center;gap:12px}
.rankedge-seo .post .by span{display:flex;align-items:center;gap:10px}
.rankedge-seo .post .by i{flex:none;width:26px;height:26px;border-radius:50%;background:var(--purple);color:#fff;font-style:normal;font-size:12px;font-weight:800;display:flex;align-items:center;justify-content:center}
.rankedge-seo .post .by em{font-style:normal;font-weight:700;color:var(--purple)}
/* ---------- contact: a galaxy behind the closing band ----------
   Same layering as the showreel band (.reel/.reel-media/.reel-scrim), but the
   media layer is a still rather than a video. The scrim is weighted to the
   left, where the white heading and contact details sit; the form is an opaque
   card so it needs no protection of its own, which is why the right side is
   left clear enough for the bright arm of the galaxy to read. */
.rankedge-seo .space-media{background-image:url("assets/contact-space.webp")}
.rankedge-seo .space-scrim{
  /* Both layers are the same ink, so they compose to 1-(1-a1)(1-a2) and the
     two together are what the text sits on. Tuned against the actual image:
     the bright arm of the galaxy crosses the copy column, so the left stays
     protected (worst case ~6.4:1 on white, against the 4.5 AA needs) while the
     right is left open enough for the arm to still read as light rather than
     being flattened to a uniform near-black. */
  background:
    /* only enough to blend into the light blog band above and the ink footer
       below; a heavier hold here was what washed the whole image out */
    linear-gradient(180deg,var(--ink) 0%,rgba(10,12,24,.18) 11%,rgba(10,12,24,.18) 89%,var(--ink) 100%),
    /* holds across the copy column, then drops away past the midline */
    linear-gradient(90deg,rgba(10,12,24,.78) 0%,rgba(10,12,24,.68) 32%,rgba(10,12,24,.3) 48%,rgba(10,12,24,.08) 70%,rgba(10,12,24,.05) 100%);
}
.rankedge-seo .space .ct-direct a,
.rankedge-seo .space h2,
.rankedge-seo .space .eyebrow,
.rankedge-seo .space .night-body{text-shadow:0 2px 16px rgba(6,7,16,.9)}
/* one column on small screens: the form stacks under the copy, so the scrim
   has to be even top-to-bottom instead of left-weighted */
@media (max-width:959px){.rankedge-seo .space-scrim{
    background:
      linear-gradient(180deg,var(--ink) 0%,rgba(10,12,24,.62) 10%,rgba(10,12,24,.62) 90%,var(--ink) 100%);
  }}
/* ---------- contact ---------- */
.rankedge-seo .ct{display:grid;gap:var(--s5)}
@media (min-width:960px){.rankedge-seo .ct{grid-template-columns:.95fr 1.05fr;gap:var(--s6)}}
.rankedge-seo .ct-direct{margin-top:var(--s4);display:flex;flex-direction:column;gap:14px}
.rankedge-seo .ct-direct a{text-decoration:none;font-size:1.06rem;font-weight:700;display:flex;align-items:center;gap:12px}
.rankedge-seo .ct-direct a:hover{color:var(--cyan)}
.rankedge-seo .ct-direct small{display:block;font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-d);font-weight:700;margin-bottom:3px}
.rankedge-seo .form{background:var(--paper-2);color:var(--ink);border-radius:24px;padding:30px}
@media (min-width:600px){.rankedge-seo .form{padding:38px}}
.rankedge-seo .fld{margin-bottom:20px}
.rankedge-seo .fld label{display:block;font-size:14px;font-weight:700;margin-bottom:8px;color:var(--ink)}
.rankedge-seo .fld .hint{font-weight:500;color:var(--muted)}
.rankedge-seo .fld input,
.rankedge-seo .fld textarea{width:100%;padding:14px 16px;border:1.5px solid var(--field-line);border-radius:12px;background:var(--paper);color:var(--ink);font-family:var(--font);font-size:16px;transition:border-color .2s,box-shadow .2s}
.rankedge-seo .fld input:focus,
.rankedge-seo .fld textarea:focus{border-color:var(--purple);box-shadow:0 0 0 3px rgba(91,56,168,.16);outline:none;background:#fff}
.rankedge-seo .fld textarea{resize:vertical;min-height:120px}
.rankedge-seo .fld input[aria-invalid="true"],
.rankedge-seo .fld textarea[aria-invalid="true"]{border-color:#B3261E;background:#FDF3F2}
.rankedge-seo .err{display:none;margin-top:7px;font-size:13.5px;color:#B3261E;font-weight:600}
.rankedge-seo .err.show{display:block}
.rankedge-seo .summary{display:none;border:1.5px solid #B3261E;background:#FDF3F2;border-radius:14px;padding:16px 18px;margin-bottom:20px}
.rankedge-seo .summary.show{display:block}
.rankedge-seo .summary h3{font-size:1rem;color:#B3261E;margin-bottom:8px}
.rankedge-seo .summary ul{margin:0;padding-left:18px}
.rankedge-seo .summary a{color:#B3261E;font-size:14.5px}
.rankedge-seo .sent{display:none;border:1.5px solid #0F7A5A;background:#F0FAF6;border-radius:14px;padding:24px}
.rankedge-seo .sent.show{display:block}
.rankedge-seo .sent strong{display:block;font-size:1.2rem;font-weight:800;color:#0F7A5A;margin-bottom:6px}
.rankedge-seo .sent p{color:#0B5C44;font-size:15.5px}
/* ---------- footer ---------- */
.rankedge-seo .ft{background:var(--ink);color:var(--muted-d);padding:var(--s6) 0 var(--s4);border-top:1px solid var(--line-d);position:relative;z-index:1}
.rankedge-seo .ft-grid{display:grid;gap:var(--s5);grid-template-columns:1fr}
@media (min-width:820px){.rankedge-seo .ft-grid{grid-template-columns:1.5fr 1fr 1fr 1fr}}
.rankedge-seo .ft h4{font-size:13px;font-weight:800;letter-spacing:.13em;text-transform:uppercase;color:#fff;margin-bottom:18px}
.rankedge-seo .ft ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:11px;font-size:14.5px}
.rankedge-seo .ft a{color:var(--muted-d);text-decoration:none;font-size:14.5px}
.rankedge-seo .ft a:hover{color:var(--cyan)}
.rankedge-seo .ft-logo{height:56px;width:auto;filter:brightness(0) invert(1);margin-bottom:18px}
.rankedge-seo .ft-base{margin-top:var(--s5);padding-top:var(--s4);border-top:1px solid var(--line-d);display:flex;flex-wrap:wrap;gap:10px 26px;justify-content:space-between;font-size:13px}
@media (prefers-reduced-motion: reduce){.rankedge-seo .hero canvas{animation:none;transform:none}
/* the per-section entrance rules are .step.rv / .tl-copy.rv / .wwd-i.rv,
     which outrank a bare .rv — they must be named here or their offsets and
     rotations would stay applied permanently with motion turned off */
.rankedge-seo .rv,
.rankedge-seo .step.rv,
.rankedge-seo .wwd-i.rv,
.rankedge-seo .drive-art.rv,
.rankedge-seo .tl-a .tl-copy.rv,
.rankedge-seo .tl-b .tl-copy.rv,
.rankedge-seo .step.rv.in,
.rankedge-seo .wwd-i.rv.in,
.rankedge-seo .drive-art.rv.in,
.rankedge-seo .tl-a .tl-copy.rv.in,
.rankedge-seo .tl-b .tl-copy.rv.in{opacity:1;transform:none;transition:none}
/* reduced motion never starts the scrub loop, so nothing would fly her and
     she would sit stuck at the origin; the rail and its nodes read fine alone */
.rankedge-seo .tl-flyer{display:none}
.rankedge-seo .drive-art img{animation:none;transform:none}
.rankedge-seo .drive-art::before{animation:none;opacity:.7}
.rankedge-seo .drive-art::after,
.rankedge-seo .drive-art.rv.in::after{animation:none;opacity:0}
.rankedge-seo .step-bar{transform:scaleX(1);transition:none}
/* the blanket rule below collapses every animation to .001ms, which for an
     infinite marquee means it restarts thousands of times a second instead of
     stopping. Park the rails outright and let the logos sit still. */
.rankedge-seo .trust-row.a,
.rankedge-seo .trust-row.b{animation:none;transform:none}
.btn.sheen::after{display:none}
.rankedge-seo .tl-copy h3.grad-d{animation:none;background-position:50% 0}
.rankedge-seo *,
.rankedge-seo *::before,
.rankedge-seo *::after{animation-duration:.001ms!important;transition-duration:.001ms!important}}
/* ============================================================
   RANK EDGE, inner page layer
   Loads on top of site.css. Everything here exists because an
   inner page needs something the home page does not: a compact
   hero that has to clear the fixed nav rather than fill the
   viewport, a breadcrumb, a standing mascot, and a long-form
   prose block.

   It also keeps the service card, which the home page used to own
   and has since dropped. See the block below.

   No token is redefined here. If a rule in this file starts
   fighting site.css rather than layering on it, the rule belongs
   in site.css instead.
   ============================================================ */
/* ---------- current page marker in the nav ----------
   Inner pages link back to the home page, so the nav has to say
   which part of the site you are standing in. The rule is split
   by band because a cyan underline disappears on the white nav
   and a purple one disappears on the dark one. */
.rankedge-seo .nav-links a[aria-current="page"]{opacity:1;position:relative}
.rankedge-seo .nav-links a[aria-current="page"]::after{
  content:"";position:absolute;left:0;right:0;bottom:-7px;height:2px;
  border-radius:2px;background:var(--purple);
}
.rankedge-seo.dark-nav .nav-links a[aria-current="page"]::after{background:var(--cyan)}
.rankedge-seo .sheet a[aria-current="page"]{color:var(--cyan)}
/* ==========================================================================
   THE SERVICE CARD  (adopted from the home page)
   This component used to live in site.css, because the home page's services
   section was built from it. That section has since been rebuilt around
   image cards and dropped .svc entirely, which pulled all fourteen of these
   rules out from under this page and left its six cards as unstyled text.

   Adopted here rather than pinned in site.css, because site.css is a verbatim
   mirror of the home page's stylesheet and has to stay free to follow it. A
   component only this layer still uses belongs to this layer.

   Copied byte for byte from the last home page revision that carried it, so
   the cards look exactly as they were signed off. The enhancements further
   down this file (deeper entrance, cursor spotlight, accent-bar sweep) are
   declared after it and override it on purpose.
   ========================================================================== */
.rankedge-seo .svc-grid{display:grid;gap:20px;grid-template-columns:1fr}
@media (min-width:720px){.rankedge-seo .svc-grid{grid-template-columns:1fr 1fr}}
@media (min-width:1080px){.rankedge-seo .svc-grid{grid-template-columns:repeat(3,1fr)}}
/* On the light band there is nothing behind to blur, so these get a barely-there
   brand tint instead of flat white, and a purple wash that deepens on hover. */
.rankedge-seo .svc{
  background:linear-gradient(160deg,#FFFFFF 0%,#FAF8FE 100%);
  border:1px solid var(--line);border-radius:22px;padding:30px;position:relative;overflow:hidden;
  transition:transform .4s var(--ease),box-shadow .4s var(--ease),border-color .4s var(--ease-soft),background .4s var(--ease-soft);
}
.rankedge-seo .svc::after{
  content:"";position:absolute;left:0;right:0;top:0;height:3px;
  background:linear-gradient(90deg,var(--purple),var(--blue));
  transform:scaleX(0);transform-origin:left;transition:transform .45s var(--ease);
}
.rankedge-seo .svc:hover{
  transform:translateY(-6px);
  box-shadow:0 24px 50px -30px rgba(46,27,92,.5);
  border-color:rgba(91,56,168,.34);
  background:linear-gradient(160deg,#FFFFFF 0%,#F4F0FD 100%);
}
.rankedge-seo .svc:hover::after{transform:scaleX(1)}
.rankedge-seo .svc-n{font-size:12px;font-weight:800;letter-spacing:.14em;color:var(--purple);margin-bottom:6px}
.rankedge-seo .svc-tag{font-size:11.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:16px}
.rankedge-seo .svc h3{font-size:1.35rem;margin-bottom:12px}
.rankedge-seo .svc p{color:var(--muted);font-size:15.5px}
.rankedge-seo .svc ul{list-style:none;margin:18px 0 0;padding:0;display:flex;flex-direction:column;gap:9px}
.rankedge-seo .svc li{font-size:14.5px;color:var(--muted);display:flex;gap:10px;align-items:flex-start}
.rankedge-seo .svc li::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--purple);margin-top:8px}
/* ---------- page hero ----------------------------------------------------
   The home page hero is 380vh of pinned canvas. That is the front door and
   it should stay unique to it. An inner hero has the opposite job: say what
   this page is in one screenful and get out of the way, so this one is
   sized by its content and never taller than it needs to be.

   The nav is fixed at 74px and transparent at the top of the page, so the
   top padding here is what stops the breadcrumb sliding under it.
   ------------------------------------------------------------------------ */
.rankedge-seo .phero{
  position:relative;z-index:1;isolation:isolate;overflow:hidden;
  background:var(--ink);color:#fff;
  padding:132px 0 var(--s6);
}
@media (min-width:900px){.rankedge-seo .phero{padding:178px 0 var(--s7)}}
/* Where the home page hero ends up is space, so an inner hero opens there
   rather than on flat ink. Painted rather than photographed: the contact
   band further down this same page already carries the galaxy still, and
   running one image twice on a page reads as a repeat. */
.rankedge-seo .phero::before{
  content:"";position:absolute;left:-10%;right:-10%;top:-34%;height:132%;
  z-index:-2;pointer-events:none;
  background:
    radial-gradient(ellipse 62% 54% at 20% 6%,rgba(91,56,168,.58) 0%,transparent 64%),
    radial-gradient(ellipse 56% 50% at 86% 26%,rgba(19,101,169,.46) 0%,transparent 66%),
    radial-gradient(ellipse 44% 42% at 62% 92%,rgba(91,216,255,.16) 0%,transparent 70%);
}
/* A faint measured grid under the aurora. It is the one cue that says this
   is the engineering side of the brand without adding an asset, and it is
   masked out downward so it never collides with the pillar strip. */
.rankedge-seo .phero::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;opacity:.55;
  background-image:
    linear-gradient(rgba(255,255,255,.05) 1px,transparent 1px),
    linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px);
  background-size:64px 64px;
  -webkit-mask-image:linear-gradient(180deg,#000 0%,rgba(0,0,0,.34) 56%,transparent 90%);
  mask-image:linear-gradient(180deg,#000 0%,rgba(0,0,0,.34) 56%,transparent 90%);
}
.rankedge-seo .phero-in{display:grid;gap:var(--s5);align-items:center}
@media (min-width:1000px){.rankedge-seo .phero-in{grid-template-columns:1.12fr .88fr;gap:var(--s6)}}
/* ---------- the hero heading ---------------------------------------------
   Uppercase at every width. It is the heading's voice rather than a
   response to space, so it should not change character between a phone
   and a desktop.

   The size step-down is desktop only, and 1000px is the right seam
   because it is the one this hero already turns on: .phero-in becomes
   two columns there and the heading starts sharing the row with the
   mascot. Below it the clamp floors at 2.25rem, which is 36px,
   so a flat 2.5rem there would make the mobile heading BIGGER, not
   smaller. The clamp is left in place to carry every width under 1000px
   exactly as before.

   max-width stays 14ch and is deliberately not retuned. ch tracks
   font-size, so the measure holds at roughly fourteen characters a line:
   the heading keeps its line count and simply gets smaller, rather than
   collapsing into a narrow column. */
/* 16ch, not the 14ch this shipped with. Changed on the live site first,
   where the Product Videos H1 "Thirty Seconds to Make Somebody Want It" broke
   across three lines with WANT IT stranded alone on the last one, and ported
   back here afterwards because rankedge-inner-page.css is GENERATED and a
   hand edit to it is erased by the next build.
   IT IS SHARED BY ALL TWELVE INNER PAGES. Checked against every one before
   changing it: the H1s run 25 to 62 characters and this is a widening, so no
   heading can overflow, they only reflow onto fewer lines. */
.rankedge-seo .phero h1{font-size:clamp(2.25rem,5.2vw,3.95rem);font-weight:900;max-width:16ch;text-transform:uppercase}
@media (min-width:1000px){.rankedge-seo .phero h1{font-size:2.5rem}}
.rankedge-seo .phero-sub{margin-top:20px;font-size:clamp(1rem,1.4vw,1.14rem);max-width:50ch;color:var(--muted-d)}
.rankedge-seo .phero-btns{display:flex;flex-wrap:wrap;gap:14px;margin-top:34px}
/* ---------- keeping the hero CTA above the fold --------------------------
   The three clamps above each carry a viewport-HEIGHT term as well as a
   width one, and that second term is the point.

   Sized on width alone, the two 1024x1536 mascots stand 570px tall at their
   380px ceiling. Add the header clearance and the section padding and the
   hero row runs past a laptop screen, which put the "Get a Free Audit"
   button below the fold on the live page: the first thing the design is
   there to make people click, and you had to scroll for it.

   min(Nvw, Nsvh) caps the artwork at roughly half the viewport height, so on
   a short screen the mascot shrinks instead of the button disappearing. The
   svh figure is derived per mascot from its own aspect ratio, because the
   chart artwork is near square and the other two are 2:3.

   The floor in each clamp still wins on very small screens; below 1000px the
   layout is single column anyway and the mascot sits under the copy, where
   its height costs the button nothing.

   Padding follows the same idea: 180px of top clearance is a reasonable
   share of a tall screen and an unreasonable one of a short laptop. */
@media (min-width:1000px) and (max-height:900px){.rankedge-seo .phero{
    /* The fallback is the standalone nav's own height, not zero. --re-header-h
       is defined by the WordPress layer, which knows how tall the Elementor
       header is; on the static build nothing defines it, and a 0px fallback
       left 72px of clearance under a 74px fixed nav. The breadcrumb ended up
       tucked behind it. */
    padding-top:calc(var(--re-header-h,74px) + var(--s6));
    padding-bottom:var(--s6);
  }
.rankedge-seo .phero-pillars{margin-top:var(--s5);padding-top:var(--s4)}}
/* ---------- breadcrumb ---------- */
.rankedge-seo .crumb{
  display:flex;flex-wrap:wrap;align-items:center;gap:9px;
  list-style:none;margin:0 0 22px;padding:0;
  font-size:13px;font-weight:600;color:var(--muted-d);
}
.rankedge-seo .crumb li{display:flex;align-items:center;gap:9px}
.rankedge-seo .crumb li+li::before{content:"";flex:none;width:4px;height:4px;border-radius:50%;background:currentColor;opacity:.45}
.rankedge-seo .crumb a{text-decoration:none;color:inherit;transition:color .25s}
.rankedge-seo .crumb a:hover{color:var(--cyan)}
.rankedge-seo .crumb [aria-current="page"]{color:#fff}
/* ---------- the standing mascot ------------------------------------------
   The three mascots on the home page are all drawn mid-flight with a lit
   thruster, which is why each of them floats over a plume. This one is
   drawn standing on her feet holding a magnifier. Floating her would read
   as a mistake, so she keeps both feet down: a contact shadow instead of a
   plume, and the only thing that loops is the lens, which is the part of
   the drawing that means "search" on a page about being found.
   ------------------------------------------------------------------------ */
.rankedge-seo .nova-stand{position:relative;width:min(100%,278px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){.rankedge-seo .nova-stand{margin:0;justify-self:end;width:clamp(248px,min(25vw,33svh),344px)}}
.rankedge-seo .nova-stand img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance. The home page mascots fade upward, which suits three
   characters who are already airborne. This one has to arrive on her feet,
   so she comes up from below and settles with a touch of overshoot, scaled
   about her soles so the landing reads as weight rather than a fade. */
.rankedge-seo .nova-stand.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-stand.rv.in{transform:none}
/* The idle loop, and the reason it is a scaleY about her soles rather than
   the translate the other three use: translating a standing figure lifts her
   off her own shadow and reads as a hop. Growing her a fraction from the
   feet up is what a breath looks like on a character with ground contact.
   Fifth period on the site, so she never falls into step with the others. */
.rankedge-seo .nova-stand img{
  /* her soles, measured at 94.3% down the frame. The breath scales about
     this point, so putting it at the box's bottom edge instead would pivot
     her around empty space and lift her heels off the shadow. */
  transform-origin:50% 94%;
  animation:novaBreathe 5.8s ease-in-out infinite;
  will-change:transform;
}
@keyframes novaBreathe{
  0%,100%{transform:translateY(0) scaleY(1) rotate(0deg)}
  50%{transform:translateY(-3px) scaleY(1.008) rotate(.5deg)}
}
/* The shadow answers the breath instead of ignoring it. On the way up it
   tightens and lightens, which is the whole trick: a contact shadow that
   holds still while the figure moves is what makes cheap mascot animation
   look pasted on. Same period, deliberately locked to hers. */
/* Measured off the 1024x1536 artwork rather than guessed: her lowest solid
   pixel is 94.3% down, so 5.7% of the frame below her is empty and a shadow
   at the box's bottom edge would float clear of her soles. Her shoes also
   span 11.7% to 78.9%, which centres them at 45%, not 50% — she stands with
   her weight off to one side, and a shadow centred on the box would sit
   visibly to the right of her. */
.rankedge-seo .nova-stand::before{
  content:"";position:absolute;left:45%;bottom:4.4%;width:64%;height:5%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.66) 0%,rgba(3,5,14,.3) 52%,transparent 78%);
  filter:blur(9px);
  animation:novaStandShadow 5.8s ease-in-out infinite;
}
@keyframes novaStandShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.8;transform:translateX(-50%) scale(.93)}
}
/* ---------- the lens ----------
   Her own element rather than a pseudo, because the scan needs three layers:
   the glow in the glass and two rings leaving it. An <img> cannot carry
   pseudo-elements, so the lens gets a span of its own.
   Positioned over the magnifier in the 1024x1536 artwork. Measured, not
   eyeballed: the pale glass sits between 78.5% and 93.4% across, and the
   ring around it runs 74.6% to 97.3%, putting the lens centre at 86% across
   and 27.6% down, about 22.7% of the frame wide. The box below is a shade
   wider than the glass so the blur covers the rim rather than stopping at
   it. Re-measure these four numbers if the artwork is ever re-cropped. */
.rankedge-seo .nova-lens{
  position:absolute;z-index:0;pointer-events:none;
  top:19.6%;right:2%;width:24%;aspect-ratio:1;border-radius:50%;
  background:radial-gradient(circle at center,rgba(91,216,255,.55) 0%,rgba(91,216,255,.2) 46%,transparent 70%);
  filter:blur(11px);
  animation:novaScan 4.4s var(--ease-soft) infinite;
}
@keyframes novaScan{
  0%,100%{opacity:.3;transform:scale(.88)}
  50%{opacity:1;transform:scale(1.14)}
}
/* Two rings leaving the glass, a sonar ping. On a page about being found
   this is the one loop on the site that means something literal, which is
   why the mascot gets it and the cards do not.
   These share the lens period on purpose. The no-lockstep rule elsewhere
   stops separate characters looking like one recycled asset; here the pulse
   and the ping are one event, and staggering them would break that. */
.rankedge-seo .nova-lens::before,
.rankedge-seo .nova-lens::after{
  content:"";position:absolute;inset:0;border-radius:50%;
  border:1.5px solid rgba(91,216,255,.5);
  opacity:0;
  animation:novaPing 4.4s var(--ease-soft) infinite;
}
.rankedge-seo .nova-lens::after{animation-delay:2.2s;border-color:rgba(167,139,250,.42)}
@keyframes novaPing{
  0%{transform:scale(.62);opacity:0}
  14%{opacity:.7}
  100%{transform:scale(2.15);opacity:0}
}
/* Nothing scans until she has landed, so the loops hold until .in arrives. */
.rankedge-seo .nova-lens,
.rankedge-seo .nova-lens::before,
.rankedge-seo .nova-lens::after,
.rankedge-seo .nova-stand img,
.rankedge-seo .nova-stand::before{animation-play-state:paused}
.rankedge-seo .nova-stand.in .nova-lens,
.rankedge-seo .nova-stand.in .nova-lens::before,
.rankedge-seo .nova-stand.in .nova-lens::after,
.rankedge-seo .nova-stand.in img,
.rankedge-seo .nova-stand.in::before{animation-play-state:running}
/* hidden tab, same hook the home page mascots use */
.rankedge-seo.paused .nova-stand img,
.rankedge-seo.paused .nova-stand::before,
.rankedge-seo.paused .nova-lens,
.rankedge-seo.paused .nova-lens::before,
.rankedge-seo.paused .nova-lens::after{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-stand.rv,
.rankedge-seo .nova-stand.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-stand img,
.rankedge-seo .nova-stand::before{animation:none;transform:none}
.rankedge-seo .nova-stand::before{transform:translateX(-50%)}
.rankedge-seo .nova-lens{animation:none;opacity:.6}
.rankedge-seo .nova-lens::before,
.rankedge-seo .nova-lens::after{animation:none;opacity:0}}
/* ---------- the rising mascot ---------------------------------------------
   The paid-ads hero. She is drawn hovering on a lit thruster beside a bar
   chart with a climbing arrow, which is the opposite brief to .nova-stand:
   that one has both feet down and gets a contact shadow, this one is airborne
   and gets a plume, the same treatment the three home-page mascots get.

   Two loops, and both are earned by the drawing rather than added to it: the
   thruster, because she is visibly flying, and the arrowhead, because the
   climb is what the page is selling. Everything else holds still.

   All four position numbers below were measured off the 466x520 artwork, not
   eyeballed. Re-measure them if it is ever recropped or replaced:
       flame column   x 20% to 34%, centre 26%, running off the bottom edge
       arrowhead tip  x 78% to 96%, y 39% to 55%, centre (90%, 46%)
       bar baseline   y 95%
       her soles      none, she has none, that is the point
   ------------------------------------------------------------------------ */
.rankedge-seo .nova-rise{position:relative;width:min(100%,300px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* wider than .nova-stand's clamp because this artwork is 466x520 against
     that one's 1024x1536: near square rather than a tall figure, so matched
     widths would leave it reading as the smaller of the two mascots. Matched
     by height instead. */
.rankedge-seo .nova-rise{margin:0;justify-self:end;width:clamp(280px,min(29vw,44svh),400px)}}
.rankedge-seo .nova-rise img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance. .nova-stand comes up from below and settles with overshoot,
   because a standing figure has to arrive on her feet. This one is already
   flying, so she does what the home page mascots do and simply rises into
   place with no landing beat. */
.rankedge-seo .nova-rise.rv{
  transform:translateY(34px);
  transition:opacity .75s var(--ease),transform .9s var(--ease);
}
.rankedge-seo .nova-rise.rv.in{transform:none}
/* The idle loop. Fifth distinct period on the site, after 5.2s (services
   heading), 5.8s (the standing mascot), 6.5s (roof) and 7.4s (the car), so
   no two mascots are ever caught in lockstep and read as one recycled asset.
   The float lives on the <img> and the .rv entrance on the wrapper, so the
   two transforms never write to the same element and fight. */
.rankedge-seo .nova-rise img{
  animation:novaRise 6.9s ease-in-out infinite;
  animation-delay:-2.6s;
  will-change:transform;
}
@keyframes novaRise{
  0%,100%{transform:translateY(0) rotate(0deg)}
  50%{transform:translateY(-15px) rotate(-1.2deg)}
}
/* The thruster. Offset to 26% rather than the 50% the other plumes use,
   because she does not stand in the middle of this frame: the chart takes
   the right half and her nozzle sits at 26% across. Centred on the box it
   would glow under the bars instead of under her. It reaches past the bottom
   edge because the flame does too. Locked to her period so the plume swells
   as she rises, which is what stops it looking pasted on. */
.rankedge-seo .nova-rise::before{
  content:"";position:absolute;left:26%;bottom:-5%;width:28%;height:20%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.52) 0%,rgba(19,101,169,.22) 44%,transparent 72%);
  filter:blur(16px);
  animation:novaThrusterRise 6.9s ease-in-out infinite;
  animation-delay:-2.6s;
}
@keyframes novaThrusterRise{
  0%,100%{opacity:.55;transform:translateX(-50%) scale(1)}
  50%{opacity:.95;transform:translateX(-50%) scale(1.18)}
}
/* ---------- the arrowhead ----------
   Its own element for the same reason the magnifier lens is: an <img> is a
   replaced element and cannot carry pseudo-elements, and this needs two
   layers, the glow and the ring that leaves it.

   Sized square via aspect-ratio, so the box is 26% of the frame's WIDTH in
   both directions: 121px across on a 466px frame, which is 23.3% of its 520px
   height. Centring that on the arrow tip at (90%, 46%) is where top:34% and
   right:-3% come from. Behind the art on purpose (z-index 0 against the
   image's 1) so it reads as light coming off the arrow rather than a disc
   sitting on top of it. */
.rankedge-seo .nova-climb{
  position:absolute;z-index:0;pointer-events:none;
  top:34%;right:-3%;width:26%;aspect-ratio:1;border-radius:50%;
  background:radial-gradient(circle at center,rgba(91,216,255,.5) 0%,rgba(167,139,250,.18) 46%,transparent 70%);
  filter:blur(12px);
  animation:novaClimb 5.1s var(--ease-soft) infinite;
}
@keyframes novaClimb{
  0%,100%{opacity:.32;transform:scale(.9)}
  50%{opacity:1;transform:scale(1.12)}
}
/* One ring, not the lens's two. Two rings on the magnifier read as a sonar
   ping, which is a literal thing to say on a page about being found. Here
   there is nothing being pinged, so this is a single beat on the arrowhead
   and no more. Shares the glow's period deliberately: they are one event. */
.rankedge-seo .nova-climb::after{
  content:"";position:absolute;inset:0;border-radius:50%;
  border:1.5px solid rgba(91,216,255,.42);
  opacity:0;
  animation:novaClimbPing 5.1s var(--ease-soft) infinite;
}
@keyframes novaClimbPing{
  0%{transform:scale(.6);opacity:0}
  16%{opacity:.62}
  100%{transform:scale(1.95);opacity:0}
}
/* Nothing runs until she has arrived, same gate .nova-stand uses. */
.rankedge-seo .nova-climb,
.rankedge-seo .nova-climb::after,
.rankedge-seo .nova-rise img,
.rankedge-seo .nova-rise::before{animation-play-state:paused}
.rankedge-seo .nova-rise.in .nova-climb,
.rankedge-seo .nova-rise.in .nova-climb::after,
.rankedge-seo .nova-rise.in img,
.rankedge-seo .nova-rise.in::before{animation-play-state:running}
/* hidden tab, same hook the home page mascots use */
.rankedge-seo.paused .nova-rise img,
.rankedge-seo.paused .nova-rise::before,
.rankedge-seo.paused .nova-climb,
.rankedge-seo.paused .nova-climb::after{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-rise.rv,
.rankedge-seo .nova-rise.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-rise img,
.rankedge-seo .nova-rise::before{animation:none;transform:none}
.rankedge-seo .nova-rise::before{transform:translateX(-50%)}
.rankedge-seo .nova-climb{animation:none;opacity:.55}
.rankedge-seo .nova-climb::after{animation:none;opacity:0}}
/* ==========================================================================
   THE ORBITING MASCOT  (social media hero)

   Third of three, and the one that needed the least CSS.

     .nova-stand  magnifier, sonar ping        being found
     .nova-rise   arrowhead, one beat          climbing returns
     .nova-orbit  nothing added                the art already says it

   The earlier flying artwork carried no social cue at all, so the component
   written for it invented one: two rings expanding from her chest, standing
   in for reach. This artwork has six platform icons and two orbital rings
   drawn into it. A CSS ring leaving her chest would cut across an orbit the
   illustration already draws, at its own speed and its own angle, and read
   as a mistake. So there is no emanating element here at all. When the
   drawing does the job, the stylesheet should get out of the way.

   What is left is a float and a thruster, both measured off the 1024x1536
   master. Re-measure if it is ever re-cropped.
   ========================================================================== */
.rankedge-seo .nova-orbit{position:relative;width:min(100%,300px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* The frame is 2:3 like the SEO mascot, but the content is not: she sits in
     a ring of icons spanning 0.4% to 98.4% of the width, where the standing
     figure occupies a narrow column. Sized against that visual mass, so it
     lands between .nova-stand's 344px and .nova-rise's 400px rather than
     matching the frame ratio and reading as the biggest mascot on the site. */
.rankedge-seo .nova-orbit{margin:0;justify-self:end;width:clamp(270px,min(27vw,33svh),380px)}}
.rankedge-seo .nova-orbit img{position:relative;z-index:1;width:100%;height:auto}
/* She is seated and airborne, so she rises into place with no landing beat,
   the same entrance the chart mascot uses. */
.rankedge-seo .nova-orbit.rv{
  transform:translateY(34px);
  transition:opacity .75s var(--ease),transform .9s var(--ease);
}
.rankedge-seo .nova-orbit.rv.in{transform:none}
/* 5.5s, distinct from every other loop on the site: 4.4s and 5.1s (the two
   pings), 5.2s, 5.8s, 6.5s, 6.9s and 7.4s. No two mascots in lockstep.
   The float lives on the <img> and the .rv entrance on the wrapper, so the
   two transforms never write to the same element and fight. */
.rankedge-seo .nova-orbit img{
  animation:novaOrbit 5.5s ease-in-out infinite;
  animation-delay:-2.1s;
  will-change:transform;
}
@keyframes novaOrbit{
  0%,100%{transform:translateY(0) rotate(0deg)}
  50%{transform:translateY(-14px) rotate(-1deg)}
}
/* The thruster. Measured: her flame runs from 37.5% to 66.4% across, centred
   at 51.8%, and its tip stops at 86.5% down, leaving 13.5% of empty frame
   below it. So the plume sits just off centre and well clear of the bottom
   edge, not flush with it like the mascots whose flames run to the crop.
   Locked to her period, so the plume swells as she rises; a plume on its own
   clock reads as pasted on. */
.rankedge-seo .nova-orbit::before{
  content:"";position:absolute;left:51.8%;bottom:9%;width:30%;height:15%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.5) 0%,rgba(19,101,169,.22) 44%,transparent 72%);
  filter:blur(15px);
  animation:novaThrusterOrbit 5.5s ease-in-out infinite;
  animation-delay:-2.1s;
}
@keyframes novaThrusterOrbit{
  0%,100%{opacity:.55;transform:translateX(-50%) scale(1)}
  50%{opacity:.95;transform:translateX(-50%) scale(1.16)}
}
/* Nothing runs until she has arrived, same gate the other two use. */
.rankedge-seo .nova-orbit img,
.rankedge-seo .nova-orbit::before{animation-play-state:paused}
.rankedge-seo .nova-orbit.in img,
.rankedge-seo .nova-orbit.in::before{animation-play-state:running}
.rankedge-seo.paused .nova-orbit img,
.rankedge-seo.paused .nova-orbit::before{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-orbit.rv,
.rankedge-seo .nova-orbit.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-orbit img,
.rankedge-seo .nova-orbit::before{animation:none;transform:none}
.rankedge-seo .nova-orbit::before{transform:translateX(-50%)}}
/* ==========================================================================
   THE BRANDING MASCOT  (branding hero)

   Fourth of four, and the second one with her feet on the ground.

     .nova-stand  magnifier, sonar ping        being found
     .nova-rise   arrowhead, one beat          climbing returns
     .nova-orbit  nothing added                the art already says it
     .nova-brand  the mark she is holding      the identity itself

   She is drawn standing on both feet holding the Rank Edge mark, so she is
   built on .nova-stand's physics, not .nova-orbit's: a contact shadow rather
   than a plume, and an entrance that lands rather than rises. Floating a
   figure with visible shoes reads as a mistake.

   What glows is the mark. On a page about identity, the logo is the subject
   of the picture and the only thing worth animating; a ring leaving her chest
   would be the social page's idiom borrowed for a page that is not about
   reach.

   AND THE GLOW IS MAGENTA, not the cyan every other loop on the site uses.
   That is deliberate and it is the one place the rule bends: the object doing
   the glowing IS the magenta half of the brand gradient, and lighting it in
   cyan would fight the artwork it is sitting behind. Everywhere else, cyan.

   Every position below was measured off the 1145x1374 master, not eyeballed.
   Re-measure these four if it is ever recropped or replaced:
       her shoes      x 18.2% to 72.9%, centre 45.5%, soles at 97.6% down
       the mark       x 37.0% to 96.9%, y 27.7% to 77.7%, centre (67%, 52.7%)
   ========================================================================== */
.rankedge-seo .nova-brand{position:relative;width:min(100%,300px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* Between .nova-orbit's 380px and .nova-rise's 400px. The frame is 1145x1374,
     so h = w x 1.20: squarer than the two 2:3 masters and taller than the near
     square chart one. She also fills it, 4.3% to 96.9% across, because the mark
     she holds reaches most of the way to the right edge.
     The svh term follows the rule in the note above the breadcrumb: 50svh
     divided by this artwork's own 1.20 ratio is 42svh. */
.rankedge-seo .nova-brand{margin:0;justify-self:end;width:clamp(275px,min(28vw,42svh),390px)}}
.rankedge-seo .nova-brand img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance, taken from .nova-stand for the same reason: she has to arrive
   on her feet, so she comes up from below and settles with a touch of
   overshoot, scaled about her soles so the landing reads as weight. */
.rankedge-seo .nova-brand.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-brand.rv.in{transform:none}
/* The breath. A scaleY about her soles rather than the translate the airborne
   mascots use: translating a standing figure lifts her off her own shadow and
   reads as a hop.
   6.2s, distinct from every other loop on the site: 4.4s and 5.1s (the two
   pings), 5.2s, 5.5s, 5.8s, 6.5s, 6.9s and 7.4s. No two in lockstep. */
.rankedge-seo .nova-brand img{
  /* her soles, measured at 97.6% down the frame. Putting the origin at the
     box's bottom edge instead would pivot her around empty space. */
  transform-origin:50% 97.6%;
  animation:novaBrandBreathe 6.2s ease-in-out infinite;
  animation-delay:-1.4s;
  will-change:transform;
}
@keyframes novaBrandBreathe{
  0%,100%{transform:translateY(0) scaleY(1) rotate(0deg)}
  50%{transform:translateY(-3px) scaleY(1.008) rotate(.45deg)}
}
/* The contact shadow, answering the breath rather than ignoring it: on the way
   up it tightens and lightens. A contact shadow that holds still while the
   figure moves is what makes cheap mascot animation look pasted on.

   Measured, not guessed. Her lowest solid pixel is 97.6% down, so only 2.4% of
   frame sits below her and a shadow at the bottom edge would float clear of her
   soles. Her shoes span 18.2% to 72.9%, centring them at 45.5% rather than 50%:
   she stands with her weight off to one side and the mark held out to the
   other, so a shadow centred on the box would sit visibly right of her feet. */
.rankedge-seo .nova-brand::before{
  content:"";position:absolute;left:45.5%;bottom:1.2%;width:60%;height:4.5%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.66) 0%,rgba(3,5,14,.3) 52%,transparent 78%);
  filter:blur(9px);
  animation:novaBrandShadow 6.2s ease-in-out infinite;
  animation-delay:-1.4s;
}
@keyframes novaBrandShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.8;transform:translateX(-50%) scale(.93)}
}
/* ---------- the mark ----------
   Its own element rather than a pseudo, for the same reason the lens and the
   arrowhead are: an <img> is a replaced element and cannot carry one.

   The mark spans 37.0% to 96.9% across and 27.7% to 77.7% down, so its centre
   is (67%, 52.7%) and it is 59.9% of the frame wide by 50% tall. The glow is
   an ellipse a shade larger than that, so the blur covers the mark's outer
   bevel rather than stopping short of it.

   Behind the art (z-index 0 against the image's 1), so it reads as the mark
   catching light rather than a coloured disc laid over it.

   Slower than either ping, 5.9s against 4.4s and 5.1s. A logo catching the
   light is a slow thing; a fast pulse would read as a notification. */
.rankedge-seo .nova-emblem{
  position:absolute;z-index:0;pointer-events:none;
  left:67%;top:52.7%;width:66%;height:56%;
  transform:translate(-50%,-50%);border-radius:50%;
  background:radial-gradient(ellipse at center,rgba(214,66,201,.34) 0%,rgba(139,92,246,.2) 45%,transparent 72%);
  filter:blur(20px);
  animation:novaEmblem 5.9s var(--ease-soft) infinite;
}
@keyframes novaEmblem{
  0%,100%{opacity:.34;transform:translate(-50%,-50%) scale(.94)}
  50%{opacity:1;transform:translate(-50%,-50%) scale(1.06)}
}
/* Nothing glows until she has landed, same gate the other three use. */
.rankedge-seo .nova-emblem,
.rankedge-seo .nova-brand img,
.rankedge-seo .nova-brand::before{animation-play-state:paused}
.rankedge-seo .nova-brand.in .nova-emblem,
.rankedge-seo .nova-brand.in img,
.rankedge-seo .nova-brand.in::before{animation-play-state:running}
/* hidden tab, same hook the other mascots use */
.rankedge-seo.paused .nova-brand img,
.rankedge-seo.paused .nova-brand::before,
.rankedge-seo.paused .nova-emblem{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-brand.rv,
.rankedge-seo .nova-brand.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-brand img,
.rankedge-seo .nova-brand::before{animation:none;transform:none}
.rankedge-seo .nova-brand::before{transform:translateX(-50%)}
.rankedge-seo .nova-emblem{animation:none;opacity:.5;transform:translate(-50%,-50%)}}
/* ==========================================================================
   THE WORKING MASCOT  (web development hero)

   Fifth of five, and the second one seated in mid air.

     .nova-stand  magnifier, sonar ping        being found
     .nova-rise   arrowhead, one beat          climbing returns
     .nova-orbit  nothing added                the art already says it
     .nova-brand  the mark she is holding      the identity itself
     .nova-web    light off the laptop screen  the thing being built

   She is cross-legged on a lit thruster with a laptop open on her knees, so
   she is built on .nova-orbit's physics rather than the two standing mascots':
   a plume, and an entrance that rises rather than lands.

   WHAT GLOWS, AND WHY IT IS NOT THE MARK ON THE LID. The branding page already
   lights the Rank Edge mark, and it means "the identity" there. Lighting the
   same mark again on a page about building websites would be the art changing
   while the meaning stayed put, which is the test this project applies to
   every mascot placement. So what glows here is the SCREEN: we are looking at
   the back of an open laptop, and a lit screen spills light past the lid.

   That also decides the placement. The glow is centred on the lid rather than
   in front of it and sits BEHIND the artwork, so the lid itself occludes the
   middle of it and only the halo shows, around the lid's top and right edges
   where the frame is empty. Put in the gap between her chest and the lid it
   would have been hidden outright: her chest mark ends at 58.6% across and the
   lid begins at 59.7%, so there is no gap to put anything in.

   Cool white rather than the magenta the branding mascot uses. That one bends
   the site's cyan rule because the object glowing is itself magenta; a screen
   is not, so this one does not bend it.

   Every position below was measured off the 1086x1448 master, not eyeballed.
   Re-measure these if it is ever recropped or replaced:
       the flame     tapers from x 31-69% at y 80% to x 46-50% at the crop,
                     column centre 49%, and it runs off the bottom edge
       the lid       x 55.0% to 93.1%, y 44.2% to 64.9%, centre (74%, 54.6%)
       the lid mark  x 67.2% to 79.9%, y 51.2% to 59.2%
   ========================================================================== */
.rankedge-seo .nova-web{position:relative;width:min(100%,300px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* The frame is 1086x1448, so h = w x 1.333: taller than the branding master
     and shorter than the two 2:3 ones. At 380px that is 507px rendered, which
     lands between .nova-stand's 516 and .nova-orbit's 570 rather than making
     her the largest mascot on the site.
     The svh term follows the rule in the note above the breadcrumb: 50svh
     divided by this artwork's own 1.333 ratio is 38svh. */
.rankedge-seo .nova-web{margin:0;justify-self:end;width:clamp(275px,min(28vw,38svh),380px)}}
.rankedge-seo .nova-web img{position:relative;z-index:1;width:100%;height:auto}
/* She is seated and airborne, so she rises into place with no landing beat,
   the same entrance the chart and orbit mascots use. */
.rankedge-seo .nova-web.rv{
  transform:translateY(34px);
  transition:opacity .75s var(--ease),transform .9s var(--ease);
}
.rankedge-seo .nova-web.rv.in{transform:none}
/* 7.8s, distinct from every other loop on the site: 4.4s and 5.1s (the two
   pings), 5.2s, 5.5s, 5.8s, 5.9s, 6.1s, 6.2s, 6.5s, 6.9s and 7.4s. No two in
   lockstep. The slowest of the five on purpose: she is the only one sitting
   still and working, and a brisk bob would undercut that.
   The float lives on the <img> and the .rv entrance on the wrapper, so the two
   transforms never write to the same element and fight. */
.rankedge-seo .nova-web img{
  animation:novaWeb 7.8s ease-in-out infinite;
  animation-delay:-3.4s;
  will-change:transform;
}
@keyframes novaWeb{
  0%,100%{transform:translateY(0) rotate(0deg)}
  50%{transform:translateY(-13px) rotate(.8deg)}
}
/* The thruster. Measured: the flame column centres on 49% across, not 50%, and
   unlike the orbit mascot's it runs off the bottom edge of the frame rather
   than stopping short of it, so this plume reaches past the crop the way the
   magnifier and chart ones do. Locked to her period, so the plume swells as
   she rises; a plume on its own clock reads as pasted on. */
.rankedge-seo .nova-web::before{
  content:"";position:absolute;left:49%;bottom:-3%;width:26%;height:17%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.5) 0%,rgba(19,101,169,.22) 44%,transparent 72%);
  filter:blur(15px);
  animation:novaThrusterWeb 7.8s ease-in-out infinite;
  animation-delay:-3.4s;
}
@keyframes novaThrusterWeb{
  0%,100%{opacity:.55;transform:translateX(-50%) scale(1)}
  50%{opacity:.95;transform:translateX(-50%) scale(1.16)}
}
/* ---------- the screen ----------
   Its own element rather than a pseudo, for the same reason the lens, the
   arrowhead and the emblem are: an <img> is a replaced element and cannot
   carry one, and ::before is already the thruster.

   Centred on the lid at (74%, 54.6%) and sized past it, 52% of the frame wide
   against the lid's 38%, so the blurred edge clears the slab and the halo
   lands on the empty frame above and to the right of it. Behind the art
   (z-index 0 against the image's 1), which is what makes the lid occlude the
   centre and leaves only the spill visible.

   4.7s, and slower is wrong here as well as faster: a screen does not pulse,
   so this is a long, shallow breath stading in for a live display rather than
   anything that reads as a notification. */
.rankedge-seo .nova-screen{
  position:absolute;z-index:0;pointer-events:none;
  left:74%;top:54.6%;width:52%;height:30%;
  transform:translate(-50%,-50%);border-radius:50%;
  background:radial-gradient(ellipse at center,rgba(197,240,255,.34) 0%,rgba(91,216,255,.22) 42%,transparent 72%);
  filter:blur(18px);
  animation:novaScreen 4.7s var(--ease-soft) infinite;
}
@keyframes novaScreen{
  0%,100%{opacity:.46;transform:translate(-50%,-50%) scale(.96)}
  50%{opacity:1;transform:translate(-50%,-50%) scale(1.05)}
}
/* Nothing runs until she has arrived, same gate the other four use. */
.rankedge-seo .nova-screen,
.rankedge-seo .nova-web img,
.rankedge-seo .nova-web::before{animation-play-state:paused}
.rankedge-seo .nova-web.in .nova-screen,
.rankedge-seo .nova-web.in img,
.rankedge-seo .nova-web.in::before{animation-play-state:running}
/* hidden tab, same hook the other mascots use */
.rankedge-seo.paused .nova-web img,
.rankedge-seo.paused .nova-web::before,
.rankedge-seo.paused .nova-screen{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-web.rv,
.rankedge-seo .nova-web.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-web img,
.rankedge-seo .nova-web::before{animation:none;transform:none}
.rankedge-seo .nova-web::before{transform:translateX(-50%)}
.rankedge-seo .nova-screen{animation:none;opacity:.6;transform:translate(-50%,-50%)}}
/* ==========================================================================
   THE SHOOTING MASCOT  (photography and videography hero)

   Sixth of six, and the third with her feet on the ground.

     .nova-stand  magnifier, sonar ping        being found
     .nova-rise   arrowhead, one beat          climbing returns
     .nova-orbit  nothing added                the art already says it
     .nova-brand  the mark she is holding      the identity itself
     .nova-web    light off the laptop screen  the thing being built
     .nova-photo  the lens, once per cycle     the shutter firing

   She is standing on both feet with a camera raised, so she is built on
   .nova-stand's physics: a contact shadow rather than a plume, and an entrance
   that lands rather than rises.

   WHAT GLOWS, AND WHY IT IS NOT ANOTHER SLOW PULSE. Every other accent on this
   site breathes: the magnifier lens, the arrowhead, the brand mark, the laptop
   screen. A camera does not breathe, it fires. So this one sits almost dark
   and pops once, briefly, per cycle, and that difference is the whole point of
   the component. It is a shutter, not a heartbeat.

   ON PHOTOSENSITIVITY. One pop every 3.9 seconds is a quarter of a flash per
   second, against WCAG's three-per-second threshold, and the pop is a soft
   bloom rather than a hard white frame. It is also gone entirely under
   prefers-reduced-motion, where the lens is left with a steady glint instead.

   AND IT SITS IN FRONT OF THE ARTWORK, unlike the laptop screen glow, which
   sits behind. The laptop's lid is turned away from us, so its light can only
   be a halo. This lens is pointed straight down the barrel at the viewer, and
   light coming toward you is in front of what it comes from.

   THE ARTWORK ALREADY CARRIES A SHADOW, and it is left in place rather than
   worked around. It is a soft black wash at alpha 49, baked in from a render
   on a light background, and against this hero it composites to rgb(8,10,20)
   where the page behind it is rgb(10,12,24). Four values out of 255. It is not
   a contact shadow here, it is a rounding error, which is why the real one
   below is drawn in CSS on top of it. Nothing to remove: the two reinforce.

   Every position below was measured off the 1009x1559 master, not eyeballed.
   Re-measure these if it is ever recropped or replaced:
       her shoes     x 11.3% to 71.2%, centre 41.2%, soles at 90.5% down
       the lens      glass x 68.0% to 81.9%, y 27.3% to 43.4%, centre (75%, 35%)
       the artwork   fills x 3.5% to 88.1% only, so 12% of the right of the
                     frame is empty and she reads as sitting left of centre
   ========================================================================== */
.rankedge-seo .nova-photo{position:relative;width:min(100%,290px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* The frame is 1009x1559, h = w x 1.545, the tallest of the six masters. At
     350px that is 541px rendered, between .nova-stand's 516 and .nova-orbit's
     570, so she does not become the largest figure on the site by aspect ratio
     alone.
     The svh term follows the rule in the note above the breadcrumb: 50svh
     divided by this artwork's own 1.545 ratio is 32svh. */
.rankedge-seo .nova-photo{margin:0;justify-self:end;width:clamp(260px,min(26vw,32svh),350px)}}
.rankedge-seo .nova-photo img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance, taken from .nova-stand for the same reason: she has to arrive
   on her feet, so she comes up from below and settles with a touch of
   overshoot, scaled about her soles so the landing reads as weight. */
.rankedge-seo .nova-photo.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-photo.rv.in{transform:none}
/* The breath, and the slowest on the site at 8.4s against 5.8s and 6.2s for
   the other two standing mascots. That is deliberate rather than arbitrary:
   she is holding a camera to her eye, and holding still is what you do when
   you are about to take a photograph. A brisk bob would be someone who is
   going to miss the shot.
   Distinct from every other loop: 3.9s (this component's own shutter), 4.4s,
   4.7s, 5.1s, 5.2s, 5.5s, 5.8s, 5.9s, 6.1s, 6.2s, 6.5s, 6.9s, 7.4s and 7.8s. */
.rankedge-seo .nova-photo img{
  /* her soles, measured at 90.5% down the frame. The breath scales about this
     point, so putting it at the box's bottom edge would pivot her around the
     9.5% of empty frame below her and lift her heels. */
  transform-origin:50% 90.5%;
  animation:novaPhotoBreathe 8.4s ease-in-out infinite;
  animation-delay:-2.9s;
  will-change:transform;
}
@keyframes novaPhotoBreathe{
  0%,100%{transform:translateY(0) scaleY(1) rotate(0deg)}
  50%{transform:translateY(-3px) scaleY(1.007) rotate(.35deg)}
}
/* The contact shadow, answering the breath rather than ignoring it: on the way
   up it tightens and lightens. Measured, not guessed. Her shoes span 11.3% to
   71.2%, centring them at 41.2% rather than 50%, because she stands braced
   with the camera held out to her right; a shadow centred on the box would sit
   visibly right of her feet. Her soles are at 90.5%, so this sits at 6.8% from
   the bottom to put its centre just under them rather than down in the empty
   frame. */
.rankedge-seo .nova-photo::before{
  content:"";position:absolute;left:41.2%;bottom:6.8%;width:66%;height:4.5%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.62) 0%,rgba(3,5,14,.28) 52%,transparent 78%);
  filter:blur(9px);
  animation:novaPhotoShadow 8.4s ease-in-out infinite;
  animation-delay:-2.9s;
}
@keyframes novaPhotoShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.82;transform:translateX(-50%) scale(.94)}
}
/* ---------- the shutter ----------
   Its own element rather than a pseudo, for the same reason every other accent
   is: an <img> cannot carry one, and ::before is already the shadow.

   Centred on the front element at (75%, 35%) and 20% of the frame wide against
   the glass's own 13.9%, so the bloom covers the rim rather than stopping at
   it. z-index 2, above the image's 1: see the note at the top about why this
   one is in front.

   The timing is the component. It rests at .18 for most of the cycle, which is
   a lens catching a little ambient light, then pops to full over about 3% of
   8.4 seconds and falls away. Written as one keyframe run rather than two so
   there is a single flash per cycle, not a burst. */
.rankedge-seo .nova-shutter{
  position:absolute;z-index:2;pointer-events:none;
  left:75%;top:35%;width:20%;aspect-ratio:1;border-radius:50%;
  transform:translate(-50%,-50%);
  background:radial-gradient(circle at center,rgba(226,247,255,.85) 0%,rgba(91,216,255,.42) 38%,transparent 70%);
  filter:blur(9px);
  opacity:.18;
  animation:novaShutter 3.9s var(--ease-soft) infinite;
}
@keyframes novaShutter{
  0%,84%{opacity:.18;transform:translate(-50%,-50%) scale(.9)}
  88%{opacity:1;transform:translate(-50%,-50%) scale(1.2)}
  93%{opacity:.34;transform:translate(-50%,-50%) scale(1)}
  100%{opacity:.18;transform:translate(-50%,-50%) scale(.9)}
}
/* Nothing fires until she has landed, same gate the other five use. */
.rankedge-seo .nova-shutter,
.rankedge-seo .nova-photo img,
.rankedge-seo .nova-photo::before{animation-play-state:paused}
.rankedge-seo .nova-photo.in .nova-shutter,
.rankedge-seo .nova-photo.in img,
.rankedge-seo .nova-photo.in::before{animation-play-state:running}
/* hidden tab, same hook the other mascots use */
.rankedge-seo.paused .nova-photo img,
.rankedge-seo.paused .nova-photo::before,
.rankedge-seo.paused .nova-shutter{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-photo.rv,
.rankedge-seo .nova-photo.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-photo img,
.rankedge-seo .nova-photo::before{animation:none;transform:none}
.rankedge-seo .nova-photo::before{transform:translateX(-50%)}
/* no firing at all, and a steady glint left in its place so the lens does
     not go dead: this is the one accent whose whole character is a flash */
.rankedge-seo .nova-shutter{animation:none;opacity:.4;transform:translate(-50%,-50%)}}
/* ---------- pillar strip under the hero ----------
   Three claims, not a sequence, so they sit level with each other rather
   than in the numbered cascade the process steps use. */
.rankedge-seo .phero-pillars{
  margin-top:var(--s6);padding-top:var(--s4);
  border-top:1px solid var(--line-d);
  display:grid;gap:20px;grid-template-columns:1fr;
}
@media (min-width:720px){.rankedge-seo .phero-pillars{grid-template-columns:repeat(3,1fr);gap:30px}}
.rankedge-seo .pil{display:flex;gap:14px;align-items:flex-start}
.rankedge-seo .pil i{
  flex:none;margin-top:2px;width:27px;height:27px;border-radius:50%;
  display:grid;place-items:center;
  background:rgba(91,216,255,.12);border:1px solid rgba(91,216,255,.32);
}
.rankedge-seo .pil svg{width:12px;height:12px;display:block}
.rankedge-seo .pil b{display:block;font-size:1.02rem;font-weight:800;letter-spacing:-.015em}
/* scoped to the descriptor rather than written as a bare .pil span, so that
   wrapping the text in anything does not silently hand the title the muted
   colour and the 5px offset meant for the line underneath it */
.rankedge-seo .pil > div > span{display:block;margin-top:5px;font-size:14.5px;color:var(--muted-d)}
/* ---------- two-step process ----------
   site.css lays .proc out as a four-column staircase. A page whose process
   is two steps would otherwise leave half the row empty, so the pair gets
   its own column count and keeps the single 58px drop that makes the
   staircase read as one. */
@media (min-width:1060px){.rankedge-seo .proc.proc-2{grid-template-columns:repeat(2,1fr)}
.rankedge-seo .proc.proc-2 .step:nth-child(2){margin-top:58px}}
/* ==========================================================================
   THE BRAND VOICE WIDGET  (branding page only)

   Two sliders and a verdict. It is the only interactive component on any of
   the four service pages, and it is here because the branding page's own
   prose points at it by name.

   It is built as a plain pair of <input type="range"> elements rather than
   anything custom, which buys the whole keyboard story for free: arrow keys,
   Home and End, Page Up and Down, and a real focus ring, none of which a
   div-with-a-drag-handler would have without being written from scratch.

   The fill is painted with a gradient whose stop is driven by --v, a custom
   property the engine writes on input. That keeps the paint on the compositor
   and means the JS never touches a layout-triggering style.
   ========================================================================== */
.rankedge-seo .voice{
  margin-top:var(--s5);padding:28px;
  border:1px solid var(--line);border-radius:20px;background:var(--paper-2);
}
@media (min-width:720px){.rankedge-seo .voice{padding:34px 38px}}
.rankedge-seo .voice-h{
  font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
  color:var(--purple);margin-bottom:22px;
}
.rankedge-seo .voice-row+.voice-row{margin-top:22px}
.rankedge-seo .voice-row{display:grid;grid-template-columns:1fr;gap:9px;align-items:center}
@media (min-width:600px){/* label, low end, track, high end. The track is the only flexible column, so
     the two end words never squeeze it to nothing on a narrow card. */
.rankedge-seo .voice-row{grid-template-columns:auto auto 1fr auto;gap:14px}}
.rankedge-seo .voice-row > label{
  font-size:13px;font-weight:700;letter-spacing:.02em;color:var(--muted);
}
.rankedge-seo .voice-end{font-size:13.5px;font-weight:600;color:var(--muted)}
/* ---------- the slider ----------
   appearance:none strips the platform widget, which also strips the platform
   focus ring, so :focus-visible puts one back on the thumb explicitly. Without
   that the control is keyboard-operable and invisibly so. */
.rankedge-seo .voice-s{
  -webkit-appearance:none;appearance:none;
  width:100%;height:24px;margin:0;background:transparent;cursor:pointer;
  /* written by the engine on input; the default keeps the fill correct at the
     50% both sliders start on, for anyone without scripting */
  --v:50%;
}
.rankedge-seo .voice-s::-webkit-slider-runnable-track{
  height:6px;border-radius:999px;
  background:linear-gradient(90deg,var(--purple) 0%,var(--cyan) var(--v),var(--line) var(--v),var(--line) 100%);
}
.rankedge-seo .voice-s::-moz-range-track{
  height:6px;border-radius:999px;
  background:linear-gradient(90deg,var(--purple) 0%,var(--cyan) var(--v),var(--line) var(--v),var(--line) 100%);
}
.rankedge-seo .voice-s::-webkit-slider-thumb{
  -webkit-appearance:none;appearance:none;
  width:20px;height:20px;margin-top:-7px;border-radius:50%;
  background:var(--paper-2);border:2px solid var(--purple);
  box-shadow:0 2px 8px -2px rgba(46,27,92,.5);
  transition:transform .18s var(--ease),border-color .18s var(--ease-soft);
}
.rankedge-seo .voice-s::-moz-range-thumb{
  width:20px;height:20px;border-radius:50%;
  background:var(--paper-2);border:2px solid var(--purple);
  box-shadow:0 2px 8px -2px rgba(46,27,92,.5);
  transition:transform .18s var(--ease),border-color .18s var(--ease-soft);
}
.rankedge-seo .voice-s:hover::-webkit-slider-thumb{transform:scale(1.12)}
.rankedge-seo .voice-s:hover::-moz-range-thumb{transform:scale(1.12)}
.rankedge-seo .voice-s:focus{outline:none}
.rankedge-seo .voice-s:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 4px rgba(91,56,168,.28)}
.rankedge-seo .voice-s:focus-visible::-moz-range-thumb{box-shadow:0 0 0 4px rgba(91,56,168,.28)}
/* ---------- the verdict ---------- */
.rankedge-seo .voice-out{
  margin-top:26px;padding-top:22px;border-top:1px solid var(--line);
  font-size:15px;color:var(--muted);
  display:flex;flex-wrap:wrap;align-items:baseline;gap:10px;
}
/* The <output>, not a child of it. The handler writes the verdict with
   textContent, which replaces every child the element has, so anything styled
   inside would survive exactly until paint() first ran. display:inline-block
   because background-clip:text on a plain inline box paints per line fragment
   and the gradient restarts if the phrase ever wraps. */
.rankedge-seo .voice-out output{
  display:inline-block;
  font-size:clamp(1.25rem,2.4vw,1.6rem);font-weight:800;letter-spacing:-.025em;
  line-height:1.15;
  background:linear-gradient(100deg,var(--purple),#D642C9);
  -webkit-background-clip:text;background-clip:text;color:transparent;
}
@media (prefers-reduced-motion: reduce){.rankedge-seo .voice-s::-webkit-slider-thumb{transition:none}
.rankedge-seo .voice-s::-moz-range-thumb{transition:none}}
/* ---------- long-form brief ----------
   The body copy on a service page is genuinely long. Set at the width of a
   .head paragraph it would run to a wall of text, so the section splits
   into a short bold lead and a measured column beside it. */
.rankedge-seo .prose{display:grid;gap:var(--s4)}
@media (min-width:960px){.rankedge-seo .prose{grid-template-columns:.82fr 1.18fr;gap:var(--s6)}}
.rankedge-seo .prose-lead{
  font-size:clamp(1.14rem,1.7vw,1.36rem);font-weight:700;line-height:1.45;
  letter-spacing:-.018em;max-width:24ch;
}
.rankedge-seo .prose-body p{color:var(--muted);font-size:1.03rem}
.rankedge-seo .dark .prose-body p{color:var(--muted-d)}
.rankedge-seo .prose-body p+p{margin-top:18px}
/* ==========================================================================
   REVIEWS
   Structure follows the block on /about-us/: a summary bar, then three
   cards. The finish is this site's, not the widget's — 22px radii, square
   buttons, the same lift and shadow the service cards use — so it reads as
   part of the page instead of something pasted into it.
   ========================================================================== */
/* site.css builds .head left-aligned against a 62ch measure, which is right
   for a heading with a mascot or a grid beside it. This one is a centred
   announcement, so the block and each line inside it re-centre. The eyebrow
   needs its own rule: it is a flex row with a leading dash, and text-align
   does not move flex children. */
.rankedge-seo .head-mid{margin-left:auto;margin-right:auto;text-align:center}
.rankedge-seo .head-mid .eyebrow{justify-content:center}
.rankedge-seo .head-mid h2,
.rankedge-seo .head-mid p{margin-left:auto;margin-right:auto}
.rankedge-seo .rating-bar{
  display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
  gap:22px;margin-bottom:var(--s5);
  background:var(--paper-2);border:1px solid var(--line);border-radius:22px;
  padding:24px 28px;
}
.rankedge-seo .rating-score{display:flex;align-items:center;gap:16px}
.rankedge-seo .g-mark{width:36px;height:36px;flex:none;display:block}
/* its own class rather than a bare .rating-score b, which would also catch
   the "47" sitting inside the small print and blow it up to 2.9rem */
.rankedge-seo .rating-num{font-size:clamp(2.1rem,4vw,2.8rem);font-weight:900;letter-spacing:-.04em;line-height:1}
.rankedge-seo .rating-meta small{display:block;margin-top:5px;font-size:14px;color:var(--muted)}
.rankedge-seo .rating-meta small b{font-weight:800;color:var(--ink)}
.rankedge-seo .stars{color:#F5A623;font-size:15px;letter-spacing:2px;line-height:1}
.rankedge-seo .rating-actions{display:flex;flex-wrap:wrap;align-items:center;gap:12px}
.rankedge-seo .live{display:inline-flex;align-items:center;gap:8px;margin-right:4px;font-size:13px;font-weight:600;color:var(--muted)}
.rankedge-seo .live i{flex:none;width:7px;height:7px;border-radius:50%;background:#34A853;box-shadow:0 0 0 3px rgba(52,168,83,.16)}
/* a quieter button than .btn-p, for a pair of secondary outbound links that
   should not compete with the page's real calls to action */
.rankedge-seo .btn-o{background:transparent;border-color:var(--line);color:var(--ink);padding:12px 20px;font-size:14.5px}
.rankedge-seo .btn-o:hover{transform:translateY(-2px);border-color:var(--purple);color:var(--purple)}
.rankedge-seo .reviews{display:grid;gap:20px;grid-template-columns:1fr}
@media (min-width:760px){.rankedge-seo .reviews{grid-template-columns:repeat(3,1fr)}}
.rankedge-seo .review{
  background:var(--paper-2);border:1px solid var(--line);border-radius:22px;
  padding:26px 28px;display:flex;flex-direction:column;gap:15px;
  transition:transform .4s var(--ease),box-shadow .4s var(--ease),border-color .4s var(--ease-soft);
}
.rankedge-seo .review:hover{transform:translateY(-5px);box-shadow:0 22px 44px -28px rgba(46,27,92,.45);border-color:rgba(91,56,168,.28)}
.rankedge-seo .review header{display:flex;align-items:center;gap:14px}
/* --av is set per card in the markup, so one rule covers all three */
.rankedge-seo .avatar{
  flex:none;width:46px;height:46px;border-radius:50%;
  display:grid;place-items:center;
  background:var(--av,var(--purple));color:#fff;
  font-size:14.5px;font-weight:800;letter-spacing:.03em;
}
.rankedge-seo .review header b{display:block;font-size:1rem;font-weight:800;letter-spacing:-.01em}
.rankedge-seo .review time{display:block;margin-top:2px;font-size:13.5px;color:var(--muted)}
/* Deliberately not a <blockquote>. Themes almost always style that element
   heavily, and a WordPress theme was giving it a grey box, a blue left
   border, indigo italic text and its own margin, which fought this card
   and won. A plain paragraph carries no such baggage. */
.rankedge-seo .review .review-text{margin:0;color:var(--muted);font-size:15.5px;line-height:1.6}
/* three separate voices rather than a sequence, so they rise together */
.rankedge-seo .review.rv{transform:translateY(26px) scale(.97);
  transition:opacity .6s var(--ease),transform .8s cubic-bezier(.16,.84,.3,1.02)}
.rankedge-seo .review.rv.in{transform:none}
@media (max-width:520px){.rankedge-seo .rating-bar{flex-direction:column;align-items:flex-start;gap:18px}
.rankedge-seo .rating-actions{width:100%}
.rankedge-seo .btn-o{flex:1 1 auto;justify-content:center}}
/* ---------- postal addresses in the contact block ----------
   site.css styles that list through .ct-direct a, which is right for the
   phone and the email and wrong for an address, which is not a link. The
   addresses are paragraphs, so they need the link's typography without its
   hover, and they need the same scrim-beating text-shadow, because the
   galaxy behind them is bright in places. */
.rankedge-seo .ct-direct p{font-size:1.06rem;font-weight:700;display:flex;align-items:center;gap:12px}
.rankedge-seo .space .ct-direct p{text-shadow:0 2px 16px rgba(6,7,16,.9)}
/* an underlined text link, for the places a full button would be too loud */
.rankedge-seo .inline-cta{
  display:inline-flex;align-items:center;gap:9px;margin-top:26px;
  font-weight:700;text-decoration:none;color:var(--purple);
  border-bottom:2px solid rgba(91,56,168,.32);padding-bottom:3px;
  transition:border-color .3s var(--ease-soft),gap .3s var(--ease);
}
.rankedge-seo .inline-cta:hover{border-color:var(--purple);gap:15px}
.rankedge-seo .dark .inline-cta{color:var(--cyan);border-bottom-color:rgba(91,216,255,.34)}
.rankedge-seo .dark .inline-cta:hover{border-bottom-color:var(--cyan)}
/* ---------- inner page entrances ------------------------------------------
   site.css already gives .svc and .step an arrival. These deepen them for the
   inner pages: further to travel, and a settle that overshoots a little
   instead of easing flat, so the movement has a end to it rather than just
   stopping. Overridden here rather than edited in site.css, so the home page
   keeps the timings it was tuned with.

   Each family still moves the way its content reads. The service cards are
   one sequence, so they tip open from the top edge like pages. The process
   steps sit on a descending diagonal, so they arrive along it. The pillars
   are parallel claims, not an order, so they rise and expand together.
   -------------------------------------------------------------------------- */
.rankedge-seo .svc.rv{
  transform:perspective(1000px) rotateX(13deg) translateY(46px) scale(.965);
  transform-origin:top center;
  transition:opacity .62s var(--ease),transform .9s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .svc.rv.in{transform:perspective(1000px) rotateX(0deg) translateY(0) scale(1)}
.rankedge-seo .step.rv{
  transform:translate(-42px,-34px) rotate(-3deg) scale(.94);
  transition:opacity .66s var(--ease),transform .92s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .step.rv.in{transform:none}
.rankedge-seo .pil.rv{
  transform:translateY(16px) scale(.93);
  transition:opacity .6s var(--ease),transform .74s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .pil.rv.in{transform:none}
/* The .stg ladder in site.css steps at .07s, which is brisk for six cards
   across three columns. Widening it to .1s lets the wave actually read as it
   crosses the grid. Same ladder drives the accent-bar sweep further down. */
.rankedge-seo .svc-grid.stg .rv:nth-child(1){transition-delay:0s}
.rankedge-seo .svc-grid.stg .rv:nth-child(2){transition-delay:.10s}
.rankedge-seo .svc-grid.stg .rv:nth-child(3){transition-delay:.20s}
.rankedge-seo .svc-grid.stg .rv:nth-child(4){transition-delay:.30s}
.rankedge-seo .svc-grid.stg .rv:nth-child(5){transition-delay:.40s}
.rankedge-seo .svc-grid.stg .rv:nth-child(6){transition-delay:.50s}
.rankedge-seo .phero-pillars.stg .rv:nth-child(1){transition-delay:.06s}
.rankedge-seo .phero-pillars.stg .rv:nth-child(2){transition-delay:.16s}
.rankedge-seo .phero-pillars.stg .rv:nth-child(3){transition-delay:.26s}
/* ==========================================================================
   CARD MOTION
   Three families on this page: .svc, .step and .pil. The rule they all
   follow is the one already written into the home page, where the streak
   beside the car runs once on arrival and never again: an entrance may be
   theatrical, but nothing on a card loops forever. Sixteen cards quietly
   pulsing under a block of copy is a page nobody can read.

   So each family gets exactly two things, an arrival and a response to the
   cursor, and everything moves on transform or opacity so the compositor
   carries it.
   ========================================================================== */
/* ---------- cursor spotlight ----------
   --mx / --my are written by the pointer handler on the page. They default
   to the card's centre, so a card that has never been hovered, or a browser
   where the script never ran, still renders correctly.

   isolation is the load-bearing part. .svc and .step are position:relative
   with z-index auto, so they raise no stacking context of their own, and a
   z-index:-1 child would fall through to the band behind and vanish.
   Isolating them puts the wash above the card's own background and below
   its text, which is the same layering .reel uses for the showreel. */
.rankedge-seo .svc,
.rankedge-seo .step{isolation:isolate}
.rankedge-seo .svc::before,
.rankedge-seo .step::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;opacity:0;
  transition:opacity .45s var(--ease-soft);
}
.rankedge-seo .svc::before{background:radial-gradient(340px circle at var(--mx,50%) var(--my,50%),rgba(91,56,168,.14),transparent 64%)}
.rankedge-seo .step::before{background:radial-gradient(320px circle at var(--mx,50%) var(--my,50%),rgba(91,216,255,.14),transparent 64%)}
.rankedge-seo .svc:hover::before,
.rankedge-seo .step:hover::before{opacity:1}
/* the number and the step pill lean into the hover with the card */
.rankedge-seo .svc-n{transition:transform .38s var(--ease),color .38s var(--ease-soft)}
.rankedge-seo .svc:hover .svc-n{transform:translateY(-3px);color:var(--blue)}
.rankedge-seo .step-n{transition:transform .38s var(--ease),background .38s var(--ease-soft),border-color .38s var(--ease-soft)}
.rankedge-seo .step:hover .step-n{transform:translateY(-3px);background:rgba(91,216,255,.2);border-color:rgba(91,216,255,.55)}
/* ---------- the service card's accent bar ----------
   site.css parks this bar at scaleX(0) and hands it to hover. Here it also
   sweeps the full width once as the card lands: in from the left, out to
   the right, so six cards arriving read as a wave crossing the grid rather
   than six separate events.

   No fill-mode on purpose. When the sweep ends the bar drops back to the
   stylesheet's scaleX(0) and hover owns it again. */
@keyframes svcWipe{
  0%{transform:scaleX(0);transform-origin:left}
  52%{transform:scaleX(1);transform-origin:left}
  53%{transform:scaleX(1);transform-origin:right}
  100%{transform:scaleX(0);transform-origin:right}
}
.rankedge-seo .svc.rv.in::after{animation:svcWipe 1.05s var(--ease) 1}
/* offsets tuned to the .stg stagger in site.css, which steps at .07s, so the
   bar crosses each card just behind the card's own arrival */
.rankedge-seo .stg .svc:nth-child(1).rv.in::after{animation-delay:.15s}
.rankedge-seo .stg .svc:nth-child(2).rv.in::after{animation-delay:.22s}
.rankedge-seo .stg .svc:nth-child(3).rv.in::after{animation-delay:.29s}
.rankedge-seo .stg .svc:nth-child(4).rv.in::after{animation-delay:.36s}
.rankedge-seo .stg .svc:nth-child(5).rv.in::after{animation-delay:.43s}
.rankedge-seo .stg .svc:nth-child(6).rv.in::after{animation-delay:.50s}
/* ---------- the pillar checks draw themselves ----------
   14.4 units of path, rounded up to 15 so the dash always clears it. The
   tick is the one piece of this page that says "done", so it is worth
   watching it happen rather than finding it already there. */
.rankedge-seo .pil svg path{
  stroke-dasharray:15;stroke-dashoffset:15;
  transition:stroke-dashoffset .62s var(--ease) .3s;
}
.rankedge-seo .pil.rv.in svg path{stroke-dashoffset:0}
.rankedge-seo .pil i{transition:transform .4s var(--ease),background .4s var(--ease-soft),border-color .4s var(--ease-soft)}
.rankedge-seo .pil:hover i{transform:scale(1.16);background:rgba(91,216,255,.22);border-color:rgba(91,216,255,.6)}
/* ---------- reduced motion ----------
   The global damper in site.css collapses every duration to .001ms, which
   handles the loops but is a trap for the tick: with its transition killed
   the dashoffset would sit at 15 and the checks would simply never appear.
   Drawn state has to be stated outright. */
@media (prefers-reduced-motion: reduce){/* The deepened entrances above live in this file, which loads after
     site.css at equal specificity, so they beat that file's reduced-motion
     reset and would otherwise leave every card permanently tipped, offset
     and shrunk with motion turned off. Each one has to be named again here.
     This is the same trap site.css documents for its own entrance rules. */
.rankedge-seo .svc.rv,
.rankedge-seo .svc.rv.in,
.rankedge-seo .step.rv,
.rankedge-seo .step.rv.in,
.rankedge-seo .pil.rv,
.rankedge-seo .pil.rv.in,
.rankedge-seo .review.rv,
.rankedge-seo .review.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .pil svg path{stroke-dashoffset:0;transition:none}
.rankedge-seo .svc.rv.in::after,
.rankedge-seo .svc::after{animation:none}
.rankedge-seo .svc::before,
.rankedge-seo .step::before{transition:none}}
/* ==========================================================================
   ABOUT PAGE
   Four components, added for the About page and nothing else.

   Everything else that page needs already exists: .phero, .band, .head,
   .svc for the belief cards, .stats, .review, .faq, .head-art and the whole
   contact band are the home page's or the inner pages' and are reused
   untouched. What follows is only what had nowhere to live yet.
   ========================================================================== */
/* ---------- 1. THE ABOUT MASCOT ------------------------------------------
   Sixth pose on the site, and the second one standing on her own feet.

   She is drawn upright with one hand raised in a wave, so she is built on
   .nova-stand's physics rather than the airborne ones: a contact shadow
   instead of a thruster plume, an entrance that arrives from below and
   settles with weight, and a breath that grows her a fraction from the soles
   up rather than lifting her off her own shadow.

   What loops is her LED FACE. Each pose lights the one part of the drawing
   that carries the page's meaning, and the four already spoken for are the
   lens on the SEO page ("search"), the chest mark on the branding page
   ("the identity"), the open screen on the web page ("the build") and the
   shutter on the photography page. On a page about who we are, the thing
   that means it is the greeting, and the greeting is on her face.

   All the anchor numbers below are measured off the 1192x1319 master, not
   eyeballed. RE-MEASURE THEM IF THE ARTWORK IS EVER RE-CROPPED.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-wave{position:relative;width:min(100%,286px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* svh rather than vh so a phone's collapsing URL bar cannot resize her
     mid-scroll, matching the other five. */
.rankedge-seo .nova-wave{margin:0;justify-self:end;width:clamp(252px,min(26vw,34svh),352px)}}
.rankedge-seo .nova-wave img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance, lifted from .nova-stand because the problem is identical:
   a figure with ground contact has to arrive on her feet. Scaled about her
   soles, with a touch of overshoot, so the landing reads as weight. */
.rankedge-seo .nova-wave.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-wave.rv.in{transform:none}
/* The idle breath. Sixth period on the site: 5.8s is the SEO mascot's, and
   two standing figures breathing in step would read as one rig driving both,
   so this runs at 6.4s. Origin is her soles at 97.8% down the frame, because
   the master leaves only 2.2% of empty pixels below her. That is why this is
   97.8 and not .nova-stand's 94. */
.rankedge-seo .nova-wave img{
  transform-origin:50% 97.8%;
  animation:novaWaveBreathe 6.4s ease-in-out infinite;
  will-change:transform;
}
@keyframes novaWaveBreathe{
  0%,100%{transform:translateY(0) scaleY(1) rotate(0deg)}
  50%{transform:translateY(-3px) scaleY(1.008) rotate(-.45deg)}
}
/* The contact shadow, locked to the same period so it answers the breath
   rather than ignoring it. Tightens and lightens as she rises.
   Measured: her lowest solid pixel is 97.8% down, and her shoes span 23.2%
   to 78.0% across, which centres them at 50.6% — near enough centred, unlike
   the SEO mascot who stands with her weight off to one side. */
.rankedge-seo .nova-wave::before{
  content:"";position:absolute;left:50.6%;bottom:.6%;width:62%;height:4.6%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.64) 0%,rgba(3,5,14,.29) 52%,transparent 78%);
  filter:blur(9px);
  animation:novaWaveShadow 6.4s ease-in-out infinite;
}
@keyframes novaWaveShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.79;transform:translateX(-50%) scale(.93)}
}
/* ---------- her face ----------
   A span of its own rather than a pseudo-element, for the same reason
   .nova-lens and .nova-screen are: an <img> is a replaced element and cannot
   carry one, and ::before is already the shadow.

   It sits BEHIND the artwork (z-index 0 against the img's 1), so the visor
   occludes the middle of the glow and only the halo bleeding past its rim
   reads. Painting it on top would wash the LED dots out, and those dots are
   the whole point of the pose.

   Measured on the master: the dark visor runs 42.6% to 68.2% across and
   15.9% to 34.4% down, putting its centre at 55.4% across and 25.1% down at
   about 25.6% of the frame wide. The box below is a shade wider than the
   visor so the blur clears the rim rather than stopping at it. */
.rankedge-seo .nova-face{
  position:absolute;z-index:0;left:55.4%;top:25.1%;
  width:34%;aspect-ratio:1;transform:translate(-50%,-50%);
  border-radius:50%;pointer-events:none;
  background:radial-gradient(circle,rgba(91,216,255,.5) 0%,rgba(91,216,255,.22) 42%,transparent 70%);
  filter:blur(15px);
  animation:novaFace 6.4s ease-in-out infinite;
}
/* Locked to the breath's period and deliberately in phase with it: the glow
   swells as she draws herself up. A face that pulsed on its own clock would
   read as a warning light rather than a greeting. */
@keyframes novaFace{
  0%,100%{opacity:.5;transform:translate(-50%,-50%) scale(.94)}
  50%{opacity:.92;transform:translate(-50%,-50%) scale(1.06)}
}
/* Nothing loops until she has actually arrived, so the breath cannot play
   against an element still sliding up from its entrance. Same gate the other
   five run. */
.rankedge-seo .nova-face,
.rankedge-seo .nova-wave img,
.rankedge-seo .nova-wave::before{animation-play-state:paused}
.rankedge-seo .nova-wave.in .nova-face,
.rankedge-seo .nova-wave.in img,
.rankedge-seo .nova-wave.in::before{animation-play-state:running}
/* site.css pauses every mascot loop while the tab is hidden. */
.rankedge-seo.paused .nova-wave img,
.rankedge-seo.paused .nova-wave::before,
.rankedge-seo.paused .nova-face{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-wave.rv,
.rankedge-seo .nova-wave.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-wave img,
.rankedge-seo .nova-wave::before{animation:none;transform:none}
/* stated outright rather than left to the global damper: with its
     animation killed the glow would otherwise sit at the keyframe default
     and her face would go dark. */
.rankedge-seo .nova-face{animation:none;opacity:.62;transform:translate(-50%,-50%)}}
/* ---------- 2. THE SERVICE MARQUEE ---------------------------------------
   The live page runs a scrolling strip of the six service lines under its
   hero. Kept, and made to do a second job: every item here is a LINK to the
   service page it names, so the strip is also the internal linking from
   About out to the six pages that sell.

   Built from two identical lists side by side, the second aria-hidden, each
   half of the track. Translating the track by -50% lands item one of the
   copy exactly where item one of the original started, so the loop has no
   seam. The clone is hidden from assistive tech because a screen reader
   announcing twelve links where a sighted visitor sees six is a bug, not a
   decoration.
   -------------------------------------------------------------------------- */
.rankedge-seo .mq{
  overflow:hidden;position:relative;
  border-top:1px solid var(--line-d);border-bottom:1px solid var(--line-d);
  padding:16px 0;margin-top:var(--s5);
  /* the strip fades out at both ends rather than being cut off by the
     viewport edge, so items enter and leave instead of popping. */
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 9%,#000 91%,transparent);
          mask-image:linear-gradient(90deg,transparent,#000 9%,#000 91%,transparent);
}
.rankedge-seo .mq-track{display:flex;width:max-content;animation:mqRun 34s linear infinite}
@keyframes mqRun{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.rankedge-seo .mq-list{
  display:flex;align-items:center;gap:34px;
  list-style:none;margin:0;padding:0 17px;flex:none;
}
.rankedge-seo .mq-list li{display:flex;align-items:center;gap:34px;flex:none}
/* the separator dot, a pseudo-element so it never lands in the accessibility
   tree or in copied text. */
.rankedge-seo .mq-list li::after{
  content:"";flex:none;width:5px;height:5px;border-radius:50%;
  background:var(--cyan);opacity:.55;
}
.rankedge-seo .mq-list a{
  font-size:13px;font-weight:800;letter-spacing:.15em;text-transform:uppercase;
  color:var(--muted-d);text-decoration:none;white-space:nowrap;
  transition:color .3s var(--ease-soft);
}
.rankedge-seo .mq-list a:hover,
.rankedge-seo .mq-list a:focus-visible{color:var(--cyan)}
/* A moving target is hard to click and harder to read. The track stops
   whenever a pointer is over the strip or a keyboard lands on any link
   inside it, which is also what keeps the links reachable by tab. */
.rankedge-seo .mq:hover .mq-track,
.rankedge-seo .mq:focus-within .mq-track{animation-play-state:paused}
.rankedge-seo.paused .mq-track{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){/* Not merely paused: an endlessly moving strip is the textbook case for
     this query. It becomes a plain wrapping row of the six links, and the
     clone is dropped so they are not printed twice. */
.rankedge-seo .mq-track{animation:none;width:100%}
.rankedge-seo .mq-list{flex-wrap:wrap;justify-content:center;gap:18px}
.rankedge-seo .mq-list li{gap:18px}
.rankedge-seo .mq-clone{display:none}}
/* ---------- 3. THE STORY TIMELINE ----------------------------------------
   The live page hides three of its four milestones behind a year tab. All
   four are printed here instead, for the same reason the web development
   page unfolded its Design / Build tabs: between them the four entries carry
   about a hundred words, a tab keeps three quarters of that out of the
   page's crawlable text, and a company history is a sequence — the point of
   drawing one is seeing 2019 and 2025 at the same time.

   A rail with four nodes on it, horizontal once there is room and vertical
   on a phone, where four columns would leave each milestone about nine
   characters of measure.
   -------------------------------------------------------------------------- */
/* It is an <ol> because the four milestones are genuinely ordered and a
   screen reader should say so, which means the browser's own numbering and
   indent have to be cleared: the visible sequence is the years and the rail,
   and "1. 2019" reads as two competing numbers. list-style-position is set
   as well, because list-style:none alone still leaves the marker box in
   Safari when the item is also a grid child. */
.rankedge-seo .tl{
  display:grid;gap:var(--s4);margin-top:var(--s6);
  position:relative;grid-template-columns:1fr;
  list-style:none;margin-left:0;margin-right:0;padding:0;
}
.rankedge-seo .tl-i{list-style:none}
.rankedge-seo .tl-i::marker{content:""}
@media (min-width:860px){.rankedge-seo .tl{grid-template-columns:repeat(4,1fr);gap:28px}}
.rankedge-seo .tl-i{position:relative;padding-left:34px}
@media (min-width:860px){.rankedge-seo .tl-i{padding-left:0;padding-top:38px}}
/* The rail. A pseudo-element on each item rather than one line drawn behind
   the grid, so it inherits the gap and cannot drift out of step with the
   nodes when the columns resize. The last item's rail is dropped, or the
   line would run off the end of the row into nothing. */
/* Written for a light band and overridden for a dark one, rather than the
   other way round, because this is the only component on the page that has
   to survive being moved: the rail, the year and the body copy are the three
   things that would go invisible if the story band were ever flipped. */
.rankedge-seo .tl-i::before{
  content:"";position:absolute;background:var(--line);
  left:5px;top:14px;width:1px;height:calc(100% + var(--s4) - 14px);
}
.rankedge-seo .dark .tl-i::before{background:var(--line-d)}
@media (min-width:860px){.rankedge-seo .tl-i::before{left:14px;top:5px;width:calc(100% + 28px - 14px);height:1px}}
.rankedge-seo .tl-i:last-child::before{display:none}
/* The node. Sized and offset so its centre sits exactly on the rail: the
   rail is 1px at left:5px, and an 11px dot at left:0 centres at 5.5px. */
.rankedge-seo .tl-i::after{
  content:"";position:absolute;left:0;top:9px;width:11px;height:11px;
  border-radius:50%;background:var(--cyan);
  box-shadow:0 0 0 4px rgba(91,216,255,.16);
}
@media (min-width:860px){.rankedge-seo .tl-i::after{left:9px;top:0}}
.rankedge-seo .tl-yr{
  font-size:1.5rem;font-weight:900;letter-spacing:-.03em;line-height:1;
  color:var(--ink);margin-bottom:10px;
}
.rankedge-seo .dark .tl-yr{color:#fff}
/* :not(.tl-yr) is load-bearing, not tidiness. The year is a <p> too, so a
   bare ".tl-i p" is an element+class selector at 0-0-1-1 and outranks the
   0-0-1-0 ".tl-yr" above it: without the exclusion the year silently renders
   at 15px in body grey instead of 24px black. Same trap as .tm-role below. */
.rankedge-seo .tl-i p:not(.tl-yr){color:var(--muted);font-size:15px;margin:0}
.rankedge-seo .dark .tl-i p:not(.tl-yr){color:var(--muted-d)}
/* Staggered so the history arrives in the order it happened, rather than
   four milestones landing at once. */
.rankedge-seo .tl.stg .rv:nth-child(1){transition-delay:0s}
.rankedge-seo .tl.stg .rv:nth-child(2){transition-delay:.11s}
.rankedge-seo .tl.stg .rv:nth-child(3){transition-delay:.22s}
.rankedge-seo .tl.stg .rv:nth-child(4){transition-delay:.33s}
/* ---------- 4. THE TEAM --------------------------------------------------
   Four people, four cards. The live page puts them behind a tab switcher
   that shows one at a time, which is the same call the web page's Design /
   Build tabs made and is wrong here for a sharper reason: a visitor on an
   About page is working out who they would actually be dealing with, and a
   control that hides three of the four answers charges three clicks to
   answer one question.

   The avatar is .review's, reused with its --av custom property intact, so
   the initials block here and the one on the review cards further down are
   the same component and stay in step if either is ever restyled.
   -------------------------------------------------------------------------- */
.rankedge-seo .team{display:grid;gap:20px;grid-template-columns:1fr}
@media (min-width:620px){.rankedge-seo .team{grid-template-columns:1fr 1fr}}
@media (min-width:1080px){.rankedge-seo .team{grid-template-columns:repeat(4,1fr)}}
.rankedge-seo .tm{
  background:#fff;border:1px solid var(--line);border-radius:0;
  padding:28px 24px;position:relative;overflow:hidden;
  transition:transform .4s var(--ease),box-shadow .4s var(--ease),border-color .4s var(--ease-soft);
}
.rankedge-seo .tm:hover{
  transform:translateY(-5px);
  box-shadow:0 22px 44px -28px rgba(46,27,92,.45);
  border-color:rgba(91,56,168,.28);
}
/* the accent rule along the top, wiped in on hover. Same gesture as .svc's,
   so the two card types on this page read as one family. */
.rankedge-seo .tm::after{
  content:"";position:absolute;inset:0 0 auto 0;height:3px;
  background:linear-gradient(90deg,var(--purple),var(--cyan));
  transform:scaleX(0);transform-origin:0 50%;
  transition:transform .45s var(--ease);
}
.rankedge-seo .tm:hover::after{transform:scaleX(1)}
/* ---------- the portrait ----------
   The four headshots came off the live page and were re-cropped rather than
   dropped in as supplied: they arrived framed differently, one waist-up and
   three chest-up, on backgrounds ranging 13 luma points apart. Each is now
   cut square about its own head, so every face lands at the same size and
   height in the circle, and every background is corrected to one neutral
   grey. Four discs of slightly different brightness sitting in a row is the
   thing the eye picks up first on a dark band.

   .tm-photo and .tm .avatar share a box deliberately: the initials block is
   kept as the fallback for anyone joining before a headshot exists, and the
   two have to be swappable in the markup without touching this file. */
.rankedge-seo .tm-photo,
.rankedge-seo .tm .avatar{
  width:84px;height:84px;border-radius:50%;
  margin-bottom:18px;display:block;flex:none;
}
.rankedge-seo .tm .avatar{display:grid;place-items:center;font-size:25px}
.rankedge-seo .tm-photo{
  object-fit:cover;
  /* the crops' own background, so the circle is filled at the right tone
     before the image decodes rather than flashing card-dark */
  background:#CDD2D0;
  box-shadow:0 0 0 1px var(--line-d),0 8px 20px -10px rgba(0,0,0,.65);
  transition:box-shadow .4s var(--ease-soft);
}
/* the ring picks up the accent on hover, the same cyan the role line and the
   card border move to, so the whole card responds as one object */
.rankedge-seo .tm:hover .tm-photo{
  box-shadow:0 0 0 2px rgba(91,216,255,.5),0 8px 20px -10px rgba(0,0,0,.65);
}
@media (prefers-reduced-motion: reduce){.rankedge-seo .tm-photo{transition:none}}
.rankedge-seo .tm h3{font-size:1.12rem;font-weight:800;letter-spacing:-.015em;margin-bottom:5px}
.rankedge-seo .tm-role{
  font-size:11.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  color:var(--purple);margin-bottom:14px;
}
/* :not(.tm-role) for the same reason the timeline's copy rule carries one:
   the role is a <p>, so an unqualified ".tm p" would outrank ".tm-role" and
   drop it from 11.5px cyan caps to 14.5px body grey. */
.rankedge-seo .tm p:not(.tm-role){color:var(--muted);font-size:14.5px;margin:0}
/* The dark-band variant, which is the one this page actually uses. A solid
   white card on an ink band would punch four holes in it; a glass card at 4%
   keeps the band reading as one surface. The role flips from purple to cyan
   for the same reason every other .dark rule in site.css does: purple on ink
   fails contrast, cyan on ink clears it comfortably. */
.rankedge-seo .dark .tm{background:rgba(255,255,255,.045);border-color:var(--line-d)}
.rankedge-seo .dark .tm h3{color:#fff}
.rankedge-seo .dark .tm-role{color:var(--cyan)}
.rankedge-seo .dark .tm p:not(.tm-role){color:var(--muted-d)}
.rankedge-seo .dark .tm:hover{
  background:rgba(255,255,255,.07);
  border-color:rgba(91,216,255,.34);
  box-shadow:0 22px 44px -28px rgba(0,0,0,.7);
}
.rankedge-seo .tm.rv{
  transform:translateY(26px) scale(.97);
  transition:opacity .7s var(--ease),transform .8s var(--ease);
}
.rankedge-seo .tm.rv.in{transform:none}
.rankedge-seo .team.stg .rv:nth-child(1){transition-delay:0s}
.rankedge-seo .team.stg .rv:nth-child(2){transition-delay:.09s}
.rankedge-seo .team.stg .rv:nth-child(3){transition-delay:.18s}
.rankedge-seo .team.stg .rv:nth-child(4){transition-delay:.27s}
@media (prefers-reduced-motion: reduce){/* Both entrances are declared in this file, which loads after site.css at
     equal specificity and would otherwise beat that file's reduced-motion
     reset, leaving every card permanently offset and shrunk. Same trap the
     blocks above document. */
.rankedge-seo .tm.rv,
.rankedge-seo .tm.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .tl.stg .rv{transition-delay:0s}
.rankedge-seo .team.stg .rv{transition-delay:0s}
.rankedge-seo .tm::after{transition:none}}
/* ==========================================================================
   DIGITAL MARKETING PAGE

   Two components, added for the services hub and nothing else.

   Everything else that page needs already exists: .phero, .phero-pillars,
   .band, .head, .svc, .proc, .stats, .prose, .review, .faq, .head-art and the
   contact band are all the home page's or the inner pages' and are reused
   untouched. What follows is only what had nowhere to live yet.
   ========================================================================== */
/* ---------- 1. THE LAUNCHING MASCOT ---------------------------------------
   Seventh pose on the site, and the fourth airborne one.

     .nova-stand   magnifier, sonar ping        being found
     .nova-rise    arrowhead, one beat          climbing returns
     .nova-orbit   nothing added                the art already says it
     .nova-brand   the mark she is holding      the identity itself
     .nova-web     light off the laptop screen  the thing being built
     .nova-photo   the lens fires               the shutter
     .nova-wave    her LED face                 the greeting
     .nova-launch  the jet, and her travel      going up

   She is drawn at a climb: cape streaming, one fist raised, and a hard cyan
   jet running off her nozzle towards the bottom-left corner. So she is built
   on the airborne physics, a plume rather than a contact shadow, and an
   entrance that rises rather than lands.

   WHAT MAKES THIS COMPONENT DIFFERENT FROM THE OTHER THREE AIRBORNE ONES,
   and it is one idea expressed in two places: she is the only mascot on this
   site that TRAVELS. Every other pose hovers, bobbing straight up and down
   over a round pool of light. This one is angled and under power, so her idle
   loop moves her along her own flight line, up and to the right, and the
   plume is an elongated glow laid ALONG the jet rather than a circle sitting
   under a nozzle. A round plume beneath a 46-degree jet would have read as a
   spotlight she happened to be standing in.

   Nothing else is added. Her chest carries the Rank Edge mark, and lighting
   it is already what the branding page means by "the identity"; lighting it
   again here would be the art changing while the meaning stayed put, which is
   the test this project applies to every mascot placement.

   Every position below was measured off the 842x914 master, not eyeballed.
   RE-MEASURE THESE IF THE ARTWORK IS EVER RE-CROPPED:
       her figure    fills the frame, x 0.0% to 99.9%, y 0.3% to 94.7%
       the jet       axis 45.6 degrees off vertical, leaning left, running
                     from the nozzle at (38.3%, 69.9%) to its tip at
                     (12.5%, 93.8%): midpoint (25.4%, 81.8%), length 33.7%
                     of the frame height
       the chest mark  x 47.3% to 61.0%, y 47.0% to 59.3%
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-launch{position:relative;width:min(100%,296px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* The master is 842x914, so h = w x 1.086: the squarest artwork on the site
     after the paid-ads chart. Matched to .nova-rise's clamp, which is the
     nearest ratio at 1.12, and at the 400px ceiling that renders 434px tall,
     between .nova-wave's 389 and .nova-rise's 446. Sized down to match the
     tall figures' widths she would read as the smallest mascot on the site,
     because unlike them she fills her frame corner to corner.
     The svh term follows the rule in the note above the breadcrumb: 50svh
     divided by this artwork's own 1.086 ratio is 46svh. */
.rankedge-seo .nova-launch{margin:0;justify-self:end;width:clamp(282px,min(29vw,46svh),400px)}}
.rankedge-seo .nova-launch img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance. The other airborne mascots rise straight up into place. This
   one arrives along the axis she is flying on, from down and to the LEFT,
   which is where her own jet says she came from. Same duration and easing as
   .nova-rise, so the page still feels of a piece; only the direction differs. */
.rankedge-seo .nova-launch.rv{
  transform:translate(-11px,38px);
  transition:opacity .75s var(--ease),transform .9s var(--ease);
}
.rankedge-seo .nova-launch.rv.in{transform:none}
/* The idle loop. 7.1s, distinct from every other period on the site: 3.9s,
   4.4s, 4.7s, 5.1s, 5.2s, 5.5s, 5.8s, 5.9s, 6.1s, 6.2s, 6.4s, 6.5s, 6.9s,
   7.4s, 7.8s and 8.4s. No two mascots are ever caught in lockstep and read as
   one recycled asset.

   The travel is DIAGONAL, up and to the right, against a jet pointing down
   and to the left. That is the whole difference between this loop and the
   three other airborne ones, and it is why the two numbers are not equal:
   16px of lift against 6px of drift is roughly her jet's own 46-degree lean
   read backwards, flattened enough that she does not visibly slide across the
   column over a long scroll. The float lives on the <img> and the .rv
   entrance on the wrapper, so the two transforms never write to the same
   element and fight. */
.rankedge-seo .nova-launch img{
  animation:novaLaunch 7.1s ease-in-out infinite;
  animation-delay:-2.9s;
  will-change:transform;
}
@keyframes novaLaunch{
  0%,100%{transform:translate(0,0) rotate(0deg)}
  50%{transform:translate(6px,-16px) rotate(-1.4deg)}
}
/* The jet. Not the round pool the other three plumes use: this one is an
   ellipse centred on the flame's own midpoint at (25.4%, 81.8%) and turned to
   sit along its 45.6-degree axis. rotate(46deg) is clockwise, which swings the
   long end of the ellipse down and to the left, which is where the flame
   actually goes.

   Locked to her period so the jet swells as she climbs; a plume on its own
   clock reads as pasted on. The swell is ANISOTROPIC, 1.22 along the axis
   against 1.06 across it, because a rocket under more throttle throws a
   LONGER flame rather than a fatter one. Every other plume on the site scales
   evenly, and every other plume is under a mascot who is hovering. */
.rankedge-seo .nova-launch::before{
  content:"";position:absolute;left:25.4%;top:81.8%;width:19%;height:34%;
  transform:translate(-50%,-50%) rotate(46deg);
  border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.54) 0%,rgba(19,101,169,.22) 44%,transparent 72%);
  filter:blur(17px);
  animation:novaJet 7.1s ease-in-out infinite;
  animation-delay:-2.9s;
}
@keyframes novaJet{
  0%,100%{opacity:.55;transform:translate(-50%,-50%) rotate(46deg) scale(1)}
  50%{opacity:.95;transform:translate(-50%,-50%) rotate(46deg) scale(1.06,1.22)}
}
/* Nothing runs until she has arrived, the same gate the other six use. */
.rankedge-seo .nova-launch img,
.rankedge-seo .nova-launch::before{animation-play-state:paused}
.rankedge-seo .nova-launch.in img,
.rankedge-seo .nova-launch.in::before{animation-play-state:running}
/* hidden tab, same hook every other mascot uses */
.rankedge-seo.paused .nova-launch img,
.rankedge-seo.paused .nova-launch::before{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-launch.rv,
.rankedge-seo .nova-launch.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-launch img{animation:none;transform:none}
/* The plume's placement is restated rather than left to transform:none.
     Killing the animation alone is right; killing the transform would drop
     the translate that centres it AND the rotate that aligns it, and the glow
     would land square, half a frame down and to the right of the flame. Same
     trap .nova-brand and .nova-wave document. */
.rankedge-seo .nova-launch::before{animation:none;transform:translate(-50%,-50%) rotate(46deg)}}
/* ---------- 2. SERVICE CARDS THAT LINK ------------------------------------
   This is the only page on the site whose service grid is a set of DOORS. The
   six leaf pages describe what they themselves do; this one is their parent,
   so each of its six cards has to hand the visitor on to the page that covers
   it properly. Those six links are also the entire internal linking from the
   hub down into the pages that sell, which is most of the reason the hub
   exists at all.

   NOT A WHOLE-CARD OVERLAY. The usual trick is a stretched pseudo-element on
   the link covering the tile, so the whole card is clickable. It was tried and
   dropped: an overlay across the card also swallows text selection, and these
   cards carry three bullets each that somebody comparing services will want to
   read and copy. A visible link at the foot of the card costs one deliberate
   click and breaks nothing.

   Scoped to .svc-linked rather than applied to .svc, so the other six pages'
   grids keep the block layout they were built with.
   -------------------------------------------------------------------------- */
.rankedge-seo .svc-linked .svc{display:flex;flex-direction:column}
/* margin-top:auto pins the link to the bottom of the card, so the six sit on
   one line across a row however unequal the copy above them is. */
.rankedge-seo .svc-more{
  margin-top:auto;padding-top:22px;
  display:inline-flex;align-items:center;align-self:flex-start;gap:8px;
  font-size:14.5px;font-weight:800;letter-spacing:-.01em;
  color:var(--purple);text-decoration:none;
  transition:gap .3s var(--ease),color .3s var(--ease-soft);
}
.rankedge-seo .svc-more svg{flex:none}
/* Hovering anywhere on the card answers on the link too, so the tile reads as
   one object even though only the link itself is a hit area. */
.rankedge-seo .svc:hover .svc-more,
.rankedge-seo .svc-more:hover{color:var(--blue);gap:13px}
.rankedge-seo .svc-more:focus-visible{outline:2px solid var(--purple);outline-offset:4px;border-radius:4px}
@media (prefers-reduced-motion: reduce){.rankedge-seo .svc-more{transition:none}}
/* ==========================================================================
   CASE STUDIES PAGE

   Two components, added for the case studies page and nothing else.

   This is the shortest page on the site: hero, the portfolio grid, contact.
   No service grid, no process, no brief, no reviews, no FAQ. Everything it
   does use — .phero, .band, .head, .stats, and the whole contact band — is
   the home page's or the inner pages' and is reused untouched.
   ========================================================================== */
/* ---------- 1. THE LEANING MASCOT -----------------------------------------
   Eighth pose on the site, and the fourth with her feet on the ground.

     .nova-stand   magnifier, sonar ping        being found
     .nova-rise    arrowhead, one beat          climbing returns
     .nova-orbit   nothing added                the art already says it
     .nova-brand   the mark she is holding      the identity itself
     .nova-web     light off the laptop screen  the thing being built
     .nova-photo   the lens fires               the shutter
     .nova-wave    her LED face                 the greeting
     .nova-launch  the jet, and her travel      going up
     .nova-case    a light read across the bars the record, already made

   She is drawn standing with her legs crossed and one elbow resting on top of
   a finished bar chart, so she is built on .nova-stand's physics rather than
   the airborne ones: a contact shadow instead of a plume, an entrance that
   lands with weight, and a breath that grows her a fraction from the soles up.

   WHAT LOOPS, AND WHY IT IS NOT A PULSE. The paid-ads mascot already lights a
   chart, and what it lights there is the ARROWHEAD, one beat, meaning returns
   are climbing. That is a claim about the future. This page is the opposite
   claim: the work is done, the numbers are in, and she is leaning on them.
   Nothing here is rising, so nothing here should pulse.

   So the light TRAVELS instead: a soft band that passes left to right across
   the bar cluster once per cycle, the way an eye goes along a chart it is
   reading. It is the only directional light on the site, and it is the whole
   difference between this component and the one on the paid-ads page.

   It sits BEHIND the artwork (z-index 0 against the image's 1), so the bars
   themselves occlude it and what shows is the light passing THROUGH THE GAPS
   between them and around their edges. In front it would have been a cyan
   wash laid over solid blue, which reads as a rendering fault rather than as
   light.

   THE SHIPPED ARTWORK IS TRIMMED, and this is the first master that needed it.
   The supplied PNG is 1347x1167 with the figure sitting at x 17.1% to 86.3%:
   a sixth of the frame is empty on the left and an eighth on the right. Sized
   by its frame she rendered 300px of visible mascot inside a 435px box and
   read as much the smallest thing in any hero on the site, because every other
   master fills its own frame corner to corner.

   So the shipped file is cropped to the alpha bounding box: 933x1140, content
   at 0.1% to 99.9% on both axes, 32% fewer pixels, and h = w x 1.222 — which
   makes it a PORTRAIT frame after all, a hair squarer than the branding
   mascot's 1.20. Margin is the layout's job, and it is done by the clamp
   below rather than baked into the image.

   Every position was measured off the TRIMMED frame, not the supplied one, and
   not eyeballed. RE-MEASURE THESE IF THE ARTWORK IS EVER RE-CROPPED:
       her figure     x 0.1% to 99.9%, y 0.2% to 99.9%
       ground contact her shoe x 15.4% to 41.8%, the chart's base plate
                      reaching x 99%, lowest solid pixel 99.8% down
       the bars       x 44% to 96%, tallest bar's top 38.2% down, base
                      plate running to 99%
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-case{position:relative;width:min(100%,300px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* The trimmed frame is 933x1140, so h = w x 1.222, within a whisker of the
     branding mascot's 1.20, and this clamp is deliberately near that one's.
     At the 400px ceiling she renders 489px tall, between .nova-brand's 468 and
     .nova-web's 507, so she is neither the largest nor the smallest figure in
     the set.
     The svh term follows the rule in the note above the breadcrumb: 50svh
     divided by this artwork's own 1.222 ratio is 41svh. */
.rankedge-seo .nova-case{margin:0;justify-self:end;width:clamp(285px,min(29vw,41svh),400px)}}
.rankedge-seo .nova-case img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance, taken from .nova-brand for the same reason: a figure with
   ground contact has to arrive on her feet, so she comes up from below and
   settles with a touch of overshoot, scaled about her soles so the landing
   reads as weight rather than as a fade. */
.rankedge-seo .nova-case.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-case.rv.in{transform:none}
/* The breath. A scaleY about her soles rather than the translate the airborne
   mascots use: translating a standing figure lifts her off her own shadow and
   reads as a hop.

   9.2s, and the slowest loop on the site by some distance. Every other period
   is between 3.9s and 8.4s: 3.9, 4.4, 4.7, 5.1, 5.2, 5.5, 5.8, 5.9, 6.1, 6.2,
   6.4, 6.5, 6.9, 7.1, 7.4, 7.8 and 8.4. Slow is the point rather than just a
   way of staying distinct — she is leaning on a finished chart, not working,
   and a brisk breath would undo the one thing the pose is saying. */
.rankedge-seo .nova-case img{
  /* her soles, measured at 99.8% down the TRIMMED frame. The crop put the
     ground almost exactly on the box's bottom edge, so unlike the other
     standing mascots this origin is very nearly 50% 100% — but it is written
     out rather than rounded, because the next re-crop will move it again. */
  transform-origin:50% 99.8%;
  animation:novaCaseBreathe 9.2s ease-in-out infinite;
  animation-delay:-3.1s;
  will-change:transform;
}
@keyframes novaCaseBreathe{
  0%,100%{transform:translateY(0) scaleY(1) rotate(0deg)}
  50%{transform:translateY(-3px) scaleY(1.007) rotate(.3deg)}
}
/* The contact shadow, answering the breath rather than ignoring it: on the way
   up it tightens and lightens. A contact shadow that holds still while the
   figure moves is what makes cheap mascot animation look pasted on.

   WIDER THAN ANY OTHER SHADOW ON THE SITE, because this pose has TWO things
   on the ground and they are far apart: her shoe at x 15.4% to 41.8%, and the
   chart's base plate reaching x 99%. Centred at 57%, which is the midpoint of
   the pair, not of the box. A shadow sized for her feet alone would leave the
   chart floating. */
.rankedge-seo .nova-case::before{
  content:"";position:absolute;left:57%;bottom:0;width:88%;height:3.6%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.62) 0%,rgba(3,5,14,.28) 52%,transparent 78%);
  filter:blur(10px);
  animation:novaCaseShadow 9.2s ease-in-out infinite;
  animation-delay:-3.1s;
}
@keyframes novaCaseShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.82;transform:translateX(-50%) scale(.94)}
}
/* ---------- the read across the bars ----------
   Its own element rather than a pseudo, for the same reason the lens, the
   mark, the screen and her face are: an <img> is a replaced element and cannot
   carry one, and ::before is already the contact shadow.

   The box covers the bar cluster: x 44% to 96%, from just above the tallest
   bar's top at 38.2% down to the base plate at 99%. Given a little margin on
   each side so the blur has somewhere to go.

   The light itself is a gradient band inside a background 260% the width of
   the box, moved by background-position. Animating background-position rather
   than translating a child is what makes the sweep need no overflow container:
   the box never moves, so nothing can spill past the mascot and out over the
   hero copy.

   NOT LOCKED TO HER BREATH, unlike the branding mark and her LED face on the
   About page. Those two are one event with the breath — the figure draws
   herself up and the thing she means catches the light. A chart being read is
   not something she is doing, so it runs on its own clock at 8.1s. */
.rankedge-seo .nova-bars{
  position:absolute;z-index:0;pointer-events:none;
  left:41%;right:1%;top:33%;bottom:0;
  background-image:linear-gradient(102deg,
      transparent 0%,transparent 34%,
      rgba(91,216,255,.20) 43%,
      rgba(91,216,255,.46) 50%,
      rgba(91,216,255,.20) 57%,
      transparent 66%,transparent 100%);
  background-size:260% 100%;
  background-repeat:no-repeat;
  filter:blur(15px);
  animation:novaBars 8.1s var(--ease-soft) infinite;
}
/* Right to left across the box, which is the direction the bars ASCEND. A
   sweep running the other way would be reading the chart backwards, from the
   best number down to the worst, and on this page of all pages that is the
   wrong story to tell in the wrong order. */
@keyframes novaBars{
  0%       {background-position:118% 0;opacity:0}
  14%      {opacity:1}
  74%      {opacity:1}
  92%,100% {background-position:-18% 0;opacity:0}
}
/* Nothing loops until she has actually landed, so the breath cannot play
   against an element still sliding up from its entrance. Same gate the other
   seven run. */
.rankedge-seo .nova-bars,
.rankedge-seo .nova-case img,
.rankedge-seo .nova-case::before{animation-play-state:paused}
.rankedge-seo .nova-case.in .nova-bars,
.rankedge-seo .nova-case.in img,
.rankedge-seo .nova-case.in::before{animation-play-state:running}
/* hidden tab, same hook every other mascot uses */
.rankedge-seo.paused .nova-case img,
.rankedge-seo.paused .nova-case::before,
.rankedge-seo.paused .nova-bars{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-case.rv,
.rankedge-seo .nova-case.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-case img,
.rankedge-seo .nova-case::before{animation:none;transform:none}
.rankedge-seo .nova-case::before{transform:translateX(-50%)}
/* Parked mid-sweep at a low opacity rather than switched off. Killing the
     animation alone would leave the band at its 0% keyframe, which is
     opacity 0 AND off the right-hand edge, so the chart would go flat. Same
     trap .nova-brand and .nova-wave document for their own accents. */
.rankedge-seo .nova-bars{animation:none;opacity:.5;background-position:50% 0}}
/* ---------- 2. THE SHORTCODE HOST -----------------------------------------
   .sc-host is where a WordPress shortcode's output lands. On this page that
   is [re_portfolio_filter], the portfolio grid and its industry and service
   filters. Nothing on this site renders it except WordPress, so on the static
   page the container holds .sc-stub instead: a placeholder that says what
   belongs there. build.py replaces the whole block with a do_shortcode() call.

   WHY THIS NEEDS A RESET AT ALL, AND IT IS THE ONLY REASON.
   rankedge-inner-page.css is the design system with every selector rewritten
   to sit under .rankedge-seo. That seals it away from your header and footer,
   which is the whole point of the scoping — but the shortcode's output lands
   INSIDE the wrapper, so it is the one piece of third-party markup the design
   system can still reach. Five element-level rules would hit it:

       .rankedge-seo h1,h2,h3,h4  weight 800, line-height 1.06, tight tracking
       .rankedge-seo p            margin:0
       .rankedge-seo a            color:inherit          <- kills link colour
       .rankedge-seo img          display:block          <- breaks inline thumbs
       .rankedge-seo button       font:inherit

   `a{color:inherit}` is the one that actually bites: every link in the grid
   would come out the same colour as the body text.

   WHY :where() AND revert, WHICH IS NOT A HABIT EITHER.
   `revert` rolls each property back to the browser's own default, which is
   what plugin CSS is written against. :where() then holds this reset at the
   LOWEST specificity that can still beat the rules above, so the plugin's own
   styles win wherever they are more specific than a bare class. Scoped, this
   reset lands at (0,2,0) against the design system's (0,1,1), so it wins; a
   plugin rule like `.re-portfolio .item a` is (0,2,1) and beats it in turn.
   That ordering is the entire design of this block. Do not add classes to
   these selectors, and do not take the :where() off.

   FONT-FAMILY IS DELIBERATELY NOT REVERTED. Inheriting Urbanist is what makes
   the grid look like part of the page rather than a widget dropped into it.
   Only the metrics that would distort someone else's layout are rolled back.

   IF THE GRID STILL COMES OUT WRONG, the cause is almost certainly a shared
   CLASS name rather than an element rule — this design system uses names as
   generic as .btn, .head, .wrap, .light, .dark, .stat and .form. Find the
   colliding class in the browser inspector and add one rule here for it. The
   escape hatch, if it ever comes to that, is to move the shortcode out of the
   .rankedge-seo wrapper entirely in template.php.in, where nothing in this
   stylesheet can reach it at all.
   -------------------------------------------------------------------------- */
.rankedge-seo .sc-host{margin-top:var(--s5)}
.rankedge-seo .sc-host :where(p){margin:revert}
.rankedge-seo .sc-host :where(a){color:revert;text-decoration:revert}
.rankedge-seo .sc-host :where(img){display:revert}
.rankedge-seo .sc-host :where(h1,h2,h3,h4,h5,h6){font-weight:revert;line-height:revert;letter-spacing:revert;margin:revert}
.rankedge-seo .sc-host :where(ul,ol){list-style:revert;margin:revert;padding:revert}
.rankedge-seo .sc-host :where(button){font:revert}
/* Kept rather than reverted: an unconstrained image inside a grid is the one
   way third-party markup can force a horizontal scrollbar onto the page. */
.rankedge-seo .sc-host :where(img){max-width:100%;height:auto}
/* ---------- the static-only placeholder ----------
   Never reaches WordPress. build.py strips the whole SHORTCODE block out of
   the markup and emits do_shortcode() in its place, and asserts that no stub
   survived into the template. It exists so the static page — which is the
   copy anybody reviewing a change actually opens — says plainly what renders
   here rather than showing a gap.

   Styled as a note, not as content. It must never be mistakable for the real
   grid: dashed border, muted, and it says what it is on the first line. */
.rankedge-seo .sc-stub{
  border:1.5px dashed var(--line);border-radius:18px;
  padding:26px 24px;background:rgba(91,56,168,.03);
}
.rankedge-seo .sc-stub b{
  display:block;font-size:12px;font-weight:800;letter-spacing:.14em;
  text-transform:uppercase;color:var(--purple);margin-bottom:10px;
}
.rankedge-seo .sc-stub p{font-size:14.5px;color:var(--muted);max-width:70ch}
.rankedge-seo .sc-stub p + p{margin-top:10px}
.rankedge-seo .sc-stub code{
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:13.5px;background:rgba(91,56,168,.09);
  padding:2px 7px;border-radius:5px;color:var(--purple);
}
.rankedge-seo .sc-stub ul{
  list-style:none;margin:14px 0 0;padding:0;
  display:flex;flex-wrap:wrap;gap:8px;
}
.rankedge-seo .sc-stub li{
  font-size:12.5px;font-weight:700;color:var(--muted);
  border:1px solid var(--line);border-radius:999px;padding:5px 12px;
}
/* ---------- 3. STAT NUMBERS IN THE HERO -----------------------------------
   The case studies hero puts three .stat blocks in the strip the other inner
   pages fill with .pil pillars. That works with no new component, except for
   one thing: site.css gives .stat b the LIGHT ramp, purple to blue, and only
   swaps it for the cyan one under `.dark`. The inner-page hero is `.phero`,
   which is dark but is not a `.band.dark`, so the numbers came out purple on
   near-black at about 2:1.

   ADDING `dark` TO THE STRIP IS NOT THE FIX. That class also carries
   `background:var(--ink)`, which would drop a flat ink rectangle across the
   hero's own gradient. Two rules instead, and they only ever fire inside a
   .phero, so no other use of .stat on the site is touched.
   -------------------------------------------------------------------------- */
.rankedge-seo .phero .stat b{
  background:linear-gradient(100deg,var(--cyan),#A78BFA);
  -webkit-background-clip:text;background-clip:text;color:transparent;
}
.rankedge-seo .phero .stat span{color:var(--muted-d)}
/* ==========================================================================
   CONTACT PAGE
   Two components, added for the Contact page and nothing else.

   Everything else that page needs already exists and is reused untouched:
   .phero, .band, .head, .head-with-art, .head-art, .svc for the office and
   the reassurance cards, .proc / .step for what happens after you send,
   .stats, .review, .faq and the whole .ct contact band are the home page's
   or the inner pages'. What follows is only what had nowhere to live yet.
   ========================================================================== */
/* ---------- 1. THE CONTACT MASCOT ----------------------------------------
   Seventh pose on the site, and the fifth airborne one.

   She is drawn hovering on a lit thruster with a phone held to her ear and
   her free hand open, so she takes the FLYING physics, not the standing
   ones: a thruster bloom rather than a contact shadow, and an entrance that
   rises into place with no landing beat. Floating is what she is doing in
   the drawing; giving her a ground shadow would contradict it.

   What loops is the SIGNAL LEAVING THE PHONE. Each pose lights the one part
   of the drawing that carries the page's meaning, and the ones already
   spoken for are the lens on the SEO page ("search"), the chest mark on the
   branding page ("the identity"), the open screen on the web page ("the
   build"), the shutter on the photography page and the LED face on the
   About page ("the greeting"). On a page whose whole job is getting a
   message to us, the thing that means it is the call connecting.

   This is the one place .nova-orbit's rule does not apply. That component
   added no emanating element because the artwork already drew orbital
   rings, and a CSS ring crossing them would have read as a mistake. Here
   the artwork draws the handset and draws no signal at all, so the rings
   say something the drawing does not.

   All anchor numbers below are measured off the 1024x1536 master, not
   eyeballed. RE-MEASURE THEM IF THE ARTWORK IS EVER RE-CROPPED.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-call{position:relative;width:min(100%,292px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* The frame is 2:3, the same as the SEO and social masters, but her cape
     spans 5.3% to 97.9% of the width where a standing figure occupies a
     narrow column. Sized against that visual mass rather than the frame
     ratio, so she lands between .nova-stand's 344px and .nova-orbit's 380px
     instead of arriving as the largest mascot on the site.
     svh rather than vh so a phone's collapsing URL bar cannot resize her
     mid-scroll, matching the other six. */
.rankedge-seo .nova-call{margin:0;justify-self:end;width:clamp(262px,min(26.5vw,34svh),362px)}}
.rankedge-seo .nova-call img{position:relative;z-index:1;width:100%;height:auto}
/* She is airborne, so she rises into place with no landing beat, the same
   entrance .nova-orbit and the chart mascot use. */
.rankedge-seo .nova-call.rv{
  transform:translateY(34px);
  transition:opacity .75s var(--ease),transform .9s var(--ease);
}
.rankedge-seo .nova-call.rv.in{transform:none}
/* 6.6s. Distinct from every other loop on the site: 3.9, 4.4, 4.7, 5.1, 5.5,
   5.8, 5.9, 6.2, 6.4, 6.9, 7.1, 7.8, 8.1 and 8.4 are taken. No two mascots
   in lockstep, or the seven read as one rig driving all of them.
   The float lives on the <img> and the .rv entrance on the wrapper, so the
   two transforms never write to the same element and fight. */
.rankedge-seo .nova-call img{
  animation:novaCall 6.6s ease-in-out infinite;
  animation-delay:-1.7s;
  will-change:transform;
}
@keyframes novaCall{
  0%,100%{transform:translateY(0) rotate(0deg)}
  50%{transform:translateY(-13px) rotate(.9deg)}
}
/* The thruster bloom, sitting under the flame the artwork already draws.
   Measured: the hot core of the drawn plume runs 30.3% to 58.8% across with
   its mass centred at 39.7%, and the flame tip stops at 92.6% down, leaving
   7.4% of empty frame below it. So the bloom sits well off centre, tracking
   the plume as it angles down and to the left, and stops clear of the
   bottom edge rather than running flush to it.
   Locked to her period so the plume swells as she rises. A plume on its own
   clock reads as pasted on. */
.rankedge-seo .nova-call::before{
  content:"";position:absolute;left:41%;bottom:7%;width:28%;height:17%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.48) 0%,rgba(19,101,169,.2) 46%,transparent 73%);
  filter:blur(16px);
  animation:novaCallThruster 6.6s ease-in-out infinite;
  animation-delay:-1.7s;
}
@keyframes novaCallThruster{
  0%,100%{opacity:.55;transform:translateX(-50%) scale(1)}
  50%{opacity:.95;transform:translateX(-50%) scale(1.15)}
}
/* ---------- her signal ----------
   Her own element rather than a pseudo-element, for the same reason
   .nova-lens, .nova-screen and .nova-face are: an <img> is a replaced
   element and cannot carry one, and ::before is already the thruster.

   Positioned over the handset. Measured: the phone slab runs 28% to 38.5%
   across and 24% to 33% down, which puts its centre at 33.3% across and
   28.5% down. The box is 26% of the frame wide so the rings clear the
   handset rather than starting flush with its edge.

   It sits BEHIND the artwork (z-index 0 against the img's 1), so her helmet
   and the visor occlude the inner half of every ring and only the arc that
   clears her head reads. That is what makes it look like a signal leaving
   the phone rather than a hoop drawn across her face.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-call .nova-sig{
  position:absolute;z-index:0;pointer-events:none;
  left:33.3%;top:28.5%;width:26%;aspect-ratio:1;
  transform:translate(-50%,-50%);border-radius:50%;
  background:radial-gradient(circle at center,rgba(91,216,255,.42) 0%,rgba(91,216,255,.15) 48%,transparent 71%);
  filter:blur(10px);
  animation:novaSigGlow 4.2s var(--ease-soft) infinite;
}
@keyframes novaSigGlow{
  0%,100%{opacity:.32;transform:translate(-50%,-50%) scale(.9)}
  50%{opacity:1;transform:translate(-50%,-50%) scale(1.12)}
}
/* Two rings leaving the handset. They share the glow's period on purpose:
   the no-lockstep rule above stops separate CHARACTERS looking like one
   recycled asset, but here the pulse and the ring are one event, and
   staggering them would break it. Same reasoning as .nova-lens. */
.rankedge-seo .nova-call .nova-sig::before,
.rankedge-seo .nova-call .nova-sig::after{
  content:"";position:absolute;inset:0;border-radius:50%;
  border:1.5px solid rgba(91,216,255,.5);
  opacity:0;
  animation:novaSigPing 4.2s var(--ease-soft) infinite;
}
.rankedge-seo .nova-call .nova-sig::after{animation-delay:2.1s;border-color:rgba(167,139,250,.42)}
@keyframes novaSigPing{
  0%{transform:scale(.5);opacity:0}
  14%{opacity:.7}
  100%{transform:scale(2.05);opacity:0}
}
/* Nothing rings until she has arrived, the same gate all six others use. */
.rankedge-seo .nova-call img,
.rankedge-seo .nova-call::before,
.rankedge-seo .nova-call .nova-sig,
.rankedge-seo .nova-call .nova-sig::before,
.rankedge-seo .nova-call .nova-sig::after{animation-play-state:paused}
.rankedge-seo .nova-call.in img,
.rankedge-seo .nova-call.in::before,
.rankedge-seo .nova-call.in .nova-sig,
.rankedge-seo .nova-call.in .nova-sig::before,
.rankedge-seo .nova-call.in .nova-sig::after{animation-play-state:running}
/* hidden tab, same hook every other mascot uses */
.rankedge-seo.paused .nova-call img,
.rankedge-seo.paused .nova-call::before,
.rankedge-seo.paused .nova-call .nova-sig,
.rankedge-seo.paused .nova-call .nova-sig::before,
.rankedge-seo.paused .nova-call .nova-sig::after{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-call.rv,
.rankedge-seo .nova-call.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-call img,
.rankedge-seo .nova-call::before{animation:none;transform:none}
.rankedge-seo .nova-call::before{transform:translateX(-50%)}
.rankedge-seo .nova-call .nova-sig{animation:none;opacity:.6;transform:translate(-50%,-50%)}
.rankedge-seo .nova-call .nova-sig::before,
.rankedge-seo .nova-call .nova-sig::after{animation:none;opacity:0}}
/* ---------- 2. THE CHANNEL CARDS -----------------------------------------
   The three ways to reach a human, sitting directly under the hero: call,
   WhatsApp, email.

   This is the one genuinely new component the page needs. .svc is the right
   card everywhere else on the site, but it is an <article> that happens to
   contain text, and every one of these three has to be a LINK whose whole
   surface is the target. Nesting an <a> inside a .svc and stretching it with
   a pseudo-element would give the same pixels and a worse result: the card
   grows a second focus stop, and the visible focus ring lands on the text
   rather than on the box a keyboard user is actually about to activate.

   So the card IS the anchor. One tag, one focus stop, one ring around the
   thing being clicked.

   ON THE ORDER: call first, because a contact page's highest-intent visitor
   is the one who wants to talk to somebody now, and the form further down
   already serves the one who does not. WhatsApp second because it is the
   channel this market actually uses. Email last, because it is the slowest
   of the three and it is also the one people can find anywhere.
   -------------------------------------------------------------------------- */
.rankedge-seo .chan-grid{display:grid;gap:18px;grid-template-columns:1fr;margin-top:var(--s5)}
@media (min-width:720px){.rankedge-seo .chan-grid{grid-template-columns:repeat(3,1fr)}}
.rankedge-seo .chan{
  display:flex;align-items:flex-start;gap:16px;
  background:linear-gradient(160deg,#FFFFFF 0%,#FAF8FE 100%);
  border:1px solid var(--line);border-radius:22px;padding:26px;
  text-decoration:none;color:inherit;position:relative;overflow:hidden;
  transition:transform .4s var(--ease),box-shadow .4s var(--ease),border-color .4s var(--ease-soft),background .4s var(--ease-soft);
}
.rankedge-seo .chan::after{
  content:"";position:absolute;left:0;right:0;top:0;height:3px;
  background:linear-gradient(90deg,var(--purple),var(--blue));
  transform:scaleX(0);transform-origin:left;transition:transform .45s var(--ease);
}
.rankedge-seo .chan:hover{
  transform:translateY(-6px);
  box-shadow:0 24px 50px -30px rgba(46,27,92,.5);
  border-color:rgba(91,56,168,.34);
  background:linear-gradient(160deg,#FFFFFF 0%,#F4F0FD 100%);
}
.rankedge-seo .chan:hover::after{transform:scaleX(1)}
/* The focus ring is the card, not the text inside it. */
.rankedge-seo .chan:focus-visible{outline:3px solid var(--purple);outline-offset:3px}
/* The icon plate. The svg inside is drawn inline on the page so each card
   costs no request; this only sizes and tints the plate it sits on. */
.rankedge-seo .chan i{
  flex:none;display:grid;place-items:center;width:44px;height:44px;border-radius:13px;
  background:linear-gradient(150deg,rgba(91,56,168,.12),rgba(19,101,169,.12));
  border:1px solid rgba(91,56,168,.18);
}
.rankedge-seo .chan i svg{width:20px;height:20px;display:block}
.rankedge-seo .chan b{display:block;font-size:11.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:5px}
/* The value is the line people came to read, so it is the biggest thing in
   the card. It has to wrap on a narrow phone rather than overflow the
   rounded corner, hence the break rule: an email address is one long
   unbroken token to a line breaker. */
.rankedge-seo .chan span{display:block;font-size:1.12rem;font-weight:800;color:var(--ink);line-height:1.3;overflow-wrap:anywhere}
.rankedge-seo .chan small{display:block;margin-top:7px;font-size:13.5px;color:var(--muted);font-weight:500}
@media (prefers-reduced-motion: reduce){.rankedge-seo .chan{transition:none}
.rankedge-seo .chan:hover{transform:none}}
/* ---------- 3. THE ROUTING SELECT ----------------------------------------
   site.css styles .fld input and .fld textarea, because until now no form on
   the site had a third kind of control. The contact form adds one <select>,
   so it needs the same treatment or it renders as a raw OS dropdown sitting
   in the middle of six matched fields.

   Everything here is copied from the input rule so the two cannot drift,
   with three additions a select needs and an input does not:
   appearance:none to drop the platform's own arrow, a background-image arrow
   drawn as an inline SVG data URI to replace it, and padding-right to keep
   the longest option clear of that arrow.

   THE ARROW IS A BACKGROUND IMAGE, not a pseudo-element, because a <select>
   is a replaced element and cannot carry one. Same constraint that gave the
   mascots their spans.
   -------------------------------------------------------------------------- */
.rankedge-seo .fld select{
  width:100%;padding:14px 44px 14px 16px;
  border:1.5px solid var(--field-line);border-radius:12px;
  background-color:var(--paper);color:var(--ink);
  font-family:var(--font);font-size:16px;line-height:1.2;
  transition:border-color .2s,box-shadow .2s;
  -webkit-appearance:none;appearance:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%236B6A7B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 16px center;background-size:12px 8px;
}
.rankedge-seo .fld select:focus{border-color:var(--purple);box-shadow:0 0 0 3px rgba(91,56,168,.16);outline:none;background-color:#fff}
/* ---------- 4. A TWO-UP SERVICE GRID -------------------------------------
   .svc-grid goes one column, then two at 720px, then THREE at 1080px, which
   is right for the five and six card sets it was built for. The offices are
   a set of two, and on a wide screen they were landing in two of three
   columns with a third of the row empty beside them.

   Exactly the problem .proc-2 already solves for the two-step process on the
   SEO page, so this is that solution wearing the matching name: cap the grid
   at two columns and let the pair fill the row.
   -------------------------------------------------------------------------- */
@media (min-width:1080px){.rankedge-seo .svc-grid.svc-grid-2{grid-template-columns:repeat(2,1fr)}}
/* ==========================================================================
   INDUSTRIES PAGE
   One component, added for the Industries page and nothing else.

   Everything else that page needs already exists and is reused untouched:
   .phero, .band, .head, .head-with-art, .head-art, .svc-grid with .svc-linked
   and .svc-more for the seven industry cards (the same hub-to-leaf component
   the Digital Marketing page uses), .proc / .step, .stats, .review, .faq and
   the whole .ct contact band.
   ========================================================================== */
/* ---------- THE INDUSTRIES MASCOT ----------------------------------------
   Eighth pose on the site, and the sixth airborne one.

   THE FIRST LANDSCAPE MASTER, AND THE SECOND SHIPPED AS A CROP. The supplied
   artwork is 1536x1024, where every other mascot on the site is portrait. Its
   figure occupies only x 18.8% to 89.5% of that frame: there are 289px of
   empty transparent pixels down the left and 162 down the right, because the
   composition was drawn with room for copy beside her that this layout does
   not use. Dropped in whole she would have rendered about a third smaller
   than the other seven in the same column, with the gap doing nothing.

   So the shipped file is cropped to her own bounds plus 6px of breathing
   room, then resized to the site's usual 1024 cap: 1097x1009 becomes
   1024x942. The case studies mascot set that precedent; this is the second.
   EVERY PERCENTAGE BELOW IS MEASURED ON THE CROPPED 1024x942 FILE, not on the
   1536x1024 master. Re-crop the artwork and all of them move.

   She is drawn airborne on a lit thruster, a magnifying glass raised in one
   hand and the other pointing down and forward, so she takes the FLYING
   physics: a thruster bloom rather than a contact shadow, and an entrance
   that rises with no landing beat.

   WHAT LOOPS IS A GLINT TRAVELLING ACROSS THE GLASS, and the distinction
   from the SEO page matters because these two mascots hold the same prop.

     .nova-stand + .nova-lens   SEO page.     A pulse in the glass and TWO
                                              RINGS LEAVING IT: a sonar ping.
                                              Concentric, outward, repeating.
                                              It means BEING FOUND.
     .nova-scan + .nova-glass   this page.    A single highlight crossing the
                                              glass on a diagonal, the way
                                              light runs over a lens as it is
                                              aimed from one thing to the
                                              next. Linear, one-directional.
                                              It means LOOKING AT YOURS.

   Different motion and different meaning, on purpose. Reusing the ping here
   would say this page is about being found, which is the SEO page's argument,
   not this one's: this page's claim is that we examine your sector before we
   write a strategy for it.

   The sweep itself is the shape of effect .btn.sheen already runs on the hero
   button, driven by transform for the same reason: it stays on the compositor
   and never asks the main thread for a layout.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-scan{position:relative;width:min(100%,320px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* Sized WIDER than the portrait mascots on purpose. At 350px a 2:3 master
     renders about 525px tall; this one is 1.087:1 and would render 322px, so
     matching their width would have given her half their presence. 424px
     renders 390px tall, which sits alongside the About mascot's 389px.
     svh rather than vh so a phone's collapsing URL bar cannot resize her
     mid-scroll, matching the other seven. */
.rankedge-seo .nova-scan{margin:0;justify-self:end;width:clamp(300px,min(30vw,46svh),424px)}}
.rankedge-seo .nova-scan img{position:relative;z-index:1;width:100%;height:auto}
/* Airborne, so she rises into place with no landing beat, the same entrance
   .nova-orbit and .nova-call use. */
.rankedge-seo .nova-scan.rv{
  transform:translateY(34px);
  transition:opacity .75s var(--ease),transform .9s var(--ease);
}
.rankedge-seo .nova-scan.rv.in{transform:none}
/* 7.6s. Distinct from every other loop on the site: 3.9, 4.2, 4.4, 4.7, 5.1,
   5.5, 5.8, 5.9, 6.2, 6.4, 6.6, 6.9, 7.1, 7.8, 8.1 and 8.4 are taken. No two
   mascots in lockstep, or the eight read as one rig driving all of them.
   The float lives on the <img> and the .rv entrance on the wrapper, so the
   two transforms never write to the same element and fight. */
.rankedge-seo .nova-scan img{
  animation:novaScanFloat 7.6s ease-in-out infinite;
  animation-delay:-2.4s;
  will-change:transform;
}
@keyframes novaScanFloat{
  0%,100%{transform:translateY(0) rotate(0deg)}
  50%{transform:translateY(-12px) rotate(-.8deg)}
}
/* The thruster bloom, under the flame the artwork already draws. Measured on
   the cropped file: the nozzle sits at about 22% across and 76% down, and the
   flame runs DOWN AND TO THE LEFT from there, its tip reaching 8% across and
   96% down. So unlike every other mascot here the bloom is nowhere near the
   horizontal centre; it sits low and hard left, tracking that diagonal.
   Locked to her period so the plume swells as she rises. A plume on its own
   clock reads as pasted on. */
.rankedge-seo .nova-scan::before{
  content:"";position:absolute;left:16%;bottom:5%;width:26%;height:22%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.46) 0%,rgba(19,101,169,.19) 46%,transparent 73%);
  filter:blur(17px);
  animation:novaScanThruster 7.6s ease-in-out infinite;
  animation-delay:-2.4s;
}
@keyframes novaScanThruster{
  0%,100%{opacity:.55;transform:translateX(-50%) scale(1)}
  50%{opacity:.95;transform:translateX(-50%) scale(1.14)}
}
/* ---------- the glass ----------
   Her own element rather than a pseudo-element, for the reason every other
   mascot's prop is: an <img> is a replaced element and cannot carry one, and
   ::before is already the thruster.

   Measured on the cropped file: the pale glass disc spans 74% to 97% across
   and 12% to 39% down, which puts its centre at 85.5% across and 25.5% down
   and makes it about 23% of the frame wide. The box below is a shade wider so
   the glow covers the rim rather than stopping short of it.

   IT SITS IN FRONT OF THE ARTWORK, z-index 2 against the img's 1, and that is
   the opposite of .nova-lens, .nova-face and .nova-sig, which all sit behind.
   Those three are GLOWS, and a glow reads correctly as a halo bleeding out
   past the edge of something opaque. This is a REFLECTION, and the glass in
   the drawing is opaque, so a highlight behind it would be completely hidden.
   mix-blend-mode:screen keeps it brightening what is underneath rather than
   painting over it, which is what stops it looking like a sticker.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-scan .nova-glass{
  position:absolute;z-index:2;pointer-events:none;
  left:85.5%;top:25.5%;width:24%;aspect-ratio:1;
  transform:translate(-50%,-50%);
  border-radius:50%;overflow:hidden;
  mix-blend-mode:screen;
  background:radial-gradient(circle at 38% 34%,rgba(191,236,255,.30) 0%,rgba(91,216,255,.10) 45%,transparent 72%);
  animation:novaGlassGlow 5.4s var(--ease-soft) infinite;
}
@keyframes novaGlassGlow{
  0%,100%{opacity:.55}
  50%{opacity:1}
}
/* The glint. A soft diagonal bar that crosses the disc and then holds off
   frame for most of the cycle, so the eye catches a single pass rather than
   a strobe. Clipped by the parent's border-radius and overflow, so it can be
   a plain rectangle and still read as light on a round lens.

   It shares the glow's period on purpose: the no-lockstep rule above stops
   separate CHARACTERS looking like one recycled asset, but the brightening
   and the pass are one event here, and staggering them would break it. Same
   reasoning .nova-lens gives for its ping. */
.rankedge-seo .nova-scan .nova-glass::before{
  content:"";position:absolute;top:-30%;left:0;width:52%;height:160%;
  background:linear-gradient(115deg,transparent,rgba(255,255,255,.72),transparent);
  animation:novaGlassSweep 5.4s var(--ease-soft) infinite;
}
@keyframes novaGlassSweep{
  0%{transform:translateX(-150%) skewX(-18deg)}
  46%,100%{transform:translateX(330%) skewX(-18deg)}
}
/* Nothing sweeps until she has arrived, the same gate all seven others use. */
.rankedge-seo .nova-scan img,
.rankedge-seo .nova-scan::before,
.rankedge-seo .nova-scan .nova-glass,
.rankedge-seo .nova-scan .nova-glass::before{animation-play-state:paused}
.rankedge-seo .nova-scan.in img,
.rankedge-seo .nova-scan.in::before,
.rankedge-seo .nova-scan.in .nova-glass,
.rankedge-seo .nova-scan.in .nova-glass::before{animation-play-state:running}
/* hidden tab, same hook every other mascot uses */
.rankedge-seo.paused .nova-scan img,
.rankedge-seo.paused .nova-scan::before,
.rankedge-seo.paused .nova-scan .nova-glass,
.rankedge-seo.paused .nova-scan .nova-glass::before{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-scan.rv,
.rankedge-seo .nova-scan.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-scan img,
.rankedge-seo .nova-scan::before{animation:none;transform:none}
.rankedge-seo .nova-scan::before{transform:translateX(-50%)}
/* the glass keeps its resting brightness; only the pass is dropped, because
     a highlight parked mid-lens with no motion reads as a smudge */
.rankedge-seo .nova-scan .nova-glass{animation:none;opacity:.7}
.rankedge-seo .nova-scan .nova-glass::before{animation:none;opacity:0}}
/* ---------- A THREE-STEP STAIRCASE ---------------------------------------
   .proc goes one column, two at 640px, then FOUR at 1060px, which is right
   for the four-step process the home page and three inner pages run. The
   Industries approach has three, and on a four-column grid they left a
   quarter of the row empty with the staircase stopping short of it.

   Exactly the problem .proc-2 already solves for the two-step process on the
   four service pages that carry one, so this is that solution with one more
   step: cap the grid at three columns and continue the descending diagonal
   across them at the same 58px rise per step.

   WHY THE THREE ARE A STAIRCASE AND NOT THREE .svc CARDS, which would have
   fitted the three-column grid with no new CSS at all: the section directly
   above it is already a seven-card .svc-grid, and two card grids stacked back
   to back read as one long undifferentiated list. The staircase also lets
   this band be dark, which is what keeps the industry cards and the reviews
   from being two light bands with nothing between them.
   -------------------------------------------------------------------------- */
@media (min-width:1060px){.rankedge-seo .proc.proc-3{grid-template-columns:repeat(3,1fr)}
.rankedge-seo .proc.proc-3 .step:nth-child(2){margin-top:58px}
.rankedge-seo .proc.proc-3 .step:nth-child(3){margin-top:116px}}
/* ==========================================================================
   PRODUCT VIDEOS PAGE
   Three components, added for the Product Videos page and nothing else.

   Everything else that page needs already exists and is reused untouched:
   .phero and .phero-pillars for the hero, .band, .head, .head-mid, .proc /
   .step in its .proc-3 form (the Industries staircase), .stats, and the whole
   .ct contact band.

   NO .faq ON THIS PAGE. It is the second page after Case Studies to carry no
   FAQ block, and its row in build.py says so with 'faq': False. A gallery
   answers its own questions by playing.
   ========================================================================== */
/* ---------- 1. THE PRODUCT VIDEOS MASCOT ---------------------------------
   Ninth pose on the site, and only the THIRD standing one. She is drawn on
   her feet, weight slightly off to one side, an action camera raised on a
   grip in one hand and the other open toward the viewer.

   SHE TAKES THE STANDING PHYSICS, not the flying ones five of the others
   use: a contact shadow rather than a thruster bloom, a breath that scales
   about her soles rather than a float that translates the whole figure, and
   an entrance that comes up from below and settles with overshoot. Giving a
   figure with visible ground contact a thruster bloom would contradict the
   drawing, the same way a ground shadow would contradict the airborne ones.

   WHAT LOOPS IS THE CAMERA RECORDING, and the choice matters because this is
   the fourth mascot on the site whose prop is a lens of some kind. The ones
   already spoken for:

     .nova-stand + .nova-lens    SEO page.      A pulse in the glass and two
                                                rings leaving it: a sonar
                                                ping. It means BEING FOUND.
     .nova-scan  + .nova-glass   Industries.    A single highlight crossing
                                                the glass on a diagonal. It
                                                means LOOKING AT YOURS.
     .nova-photo + .nova-shutter Photography.   A shutter pop. It means ONE
                                                FRAME, CAPTURED.
     .nova-roll  + .nova-rec     this page.     A bloom breathing in the lens
                                                and a red record light on the
                                                body, both on a slow steady
                                                cycle rather than a pop. It
                                                means STILL ROLLING.

   The distinction from the photography page is the one worth protecting. A
   shutter is an instant; this page sells thirty-second cuts. So this loop has
   no snap in it anywhere: it eases in and eases out with no hard edge, which
   is what a recording indicator actually does.

   All anchor numbers below are measured off the 1024x1536 master, not
   eyeballed. RE-MEASURE THEM IF THE ARTWORK IS EVER RE-CROPPED.
   Measured: her lowest solid pixel is 98.0% down, so there is almost no empty
   frame beneath her. This is the tightest-cropped master on the site and the
   shadow numbers below reflect that. Her shoes span 10.3% to 81.5%, centring
   them at 45.9% rather than 50%: she stands with her weight off to one side,
   and a shadow centred on the box would sit visibly to her right.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-roll{position:relative;width:min(100%,286px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* The frame is 2:3, the same as the SEO, social and contact masters. Her
     raised arm and the camera push her silhouette wider than a plain standing
     figure, but the camera is at the TOP of the frame rather than out to the
     side, so the visual mass is still a narrow column. Sized to land between
     .nova-stand's 344px and .nova-call's 362px.
     svh rather than vh so a phone's collapsing URL bar cannot resize her
     mid-scroll, matching the other eight. */
.rankedge-seo .nova-roll{margin:0;justify-self:end;width:clamp(256px,min(26vw,34svh),352px)}}
.rankedge-seo .nova-roll img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance. She arrives on her feet, so she comes up from below and
   settles with a touch of overshoot, scaled about her soles so the landing
   reads as weight rather than a fade. Same curve .nova-stand uses. */
.rankedge-seo .nova-roll.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-roll.rv.in{transform:none}
/* The breath. A scaleY about her soles rather than the translate the airborne
   mascots use: translating a standing figure lifts her off her own shadow and
   reads as a hop.

   7.3s. Every other period on the site is taken (3.9, 4.2, 4.4, 4.6, 4.7, 5.1,
   5.2, 5.4, 5.5, 5.8, 5.9, 6.1, 6.2, 6.4, 6.5, 6.6, 6.9, 7.1, 7.4, 7.6, 7.8,
   8.1 and 8.4), and no two mascots may run in lockstep or the nine read as one
   rig driving all of them.

   The breath lives on the <img> and the .rv entrance on the wrapper, so the
   two transforms never write to the same element and fight. */
.rankedge-seo .nova-roll img{
  /* her soles, measured at 98.0% down the frame */
  transform-origin:50% 98%;
  animation:novaRollBreathe 7.3s ease-in-out infinite;
  will-change:transform;
}
@keyframes novaRollBreathe{
  0%,100%{transform:translateY(0) scaleY(1) rotate(0deg)}
  50%{transform:translateY(-3px) scaleY(1.008) rotate(.4deg)}
}
/* The contact shadow, answering the breath rather than ignoring it: it
   tightens and lightens as she rises. A contact shadow that holds still while
   the figure moves is what makes cheap mascot animation look pasted on.
   Locked to her period on purpose.

   Only 2% of the frame sits below her soles, against 5.7% on the SEO master,
   so this shadow sits lower and flatter than that one or it would ride up
   over her shoes. */
.rankedge-seo .nova-roll::before{
  content:"";position:absolute;left:45.9%;bottom:.4%;width:66%;height:4.2%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.66) 0%,rgba(3,5,14,.3) 52%,transparent 78%);
  filter:blur(9px);
  animation:novaRollShadow 7.3s ease-in-out infinite;
}
@keyframes novaRollShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.8;transform:translateX(-50%) scale(.93)}
}
/* ---------- the camera, recording ----------
   Its own element rather than a pseudo-element, for the same reason
   .nova-lens, .nova-screen, .nova-face and .nova-sig are: an <img> is a
   replaced element and cannot carry one, and ::before is already the shadow.

   Positioned over the action camera in the 1024x1536 artwork. Measured, not
   eyeballed: the camera body runs 79.6% to 97.3% across and 2.7% to 13.4%
   down. Its glass lens sits in the right half of that body, centred at 93.3%
   across and 6.4% down and about 6.2% of the frame wide; the red record light
   is on the body's lower right at roughly 95.6% across, 10.8% down.

   The box below is centred on the LENS rather than on the body, because the
   bloom belongs in the glass, and it is a shade wider than the glass so the
   blur covers the barrel rather than stopping at its rim.

   IT SITS IN FRONT of the artwork, z-index 2 against the img's 1, unlike the
   contact mascot's signal which sits behind her helmet on purpose. Nothing
   occludes a camera lens pointed at the viewer, and putting the bloom behind
   the drawing would hide it completely: the camera is at the very top of the
   frame with no part of her in front of it.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-roll .nova-rec{
  position:absolute;z-index:2;pointer-events:none;
  left:93.3%;top:6.4%;width:11%;aspect-ratio:1;
  transform:translate(-50%,-50%);border-radius:50%;
  background:radial-gradient(circle at center,rgba(91,216,255,.5) 0%,rgba(91,216,255,.18) 45%,transparent 70%);
  filter:blur(9px);
  /* 2.9s, and NO SNAP ANYWHERE IN IT. Eased both ways with the peak held
     across the middle is what a recording indicator does; the photography
     mascot's shutter pops because a shutter is an instant, and borrowing that
     here would say this page sells stills. */
  animation:novaRecGlow 2.9s ease-in-out infinite;
}
@keyframes novaRecGlow{
  0%,100%{opacity:.34;transform:translate(-50%,-50%) scale(.92)}
  50%{opacity:1;transform:translate(-50%,-50%) scale(1.1)}
}
/* The record light itself, on the camera body rather than in the lens. A
   second, much smaller dot offset down and to the right of the bloom, sitting
   where the artwork draws the red button: 95.6% across, 10.8% down, expressed
   here relative to the .nova-rec box it hangs off.

   RED, and the only red on any mascot on the site. That is the point: a
   record light is red everywhere, and making it brand cyan would cost the
   whole meaning to gain nothing. */
.rankedge-seo .nova-roll .nova-rec::after{
  content:"";position:absolute;
  left:76%;top:118%;width:26%;aspect-ratio:1;
  transform:translate(-50%,-50%);border-radius:50%;
  background:radial-gradient(circle at center,rgba(255,86,86,.95) 0%,rgba(255,64,64,.5) 42%,transparent 72%);
  filter:blur(2.5px);
  animation:novaRecDot 2.9s ease-in-out infinite;
}
@keyframes novaRecDot{
  0%,100%{opacity:.45;transform:translate(-50%,-50%) scale(.86)}
  50%{opacity:1;transform:translate(-50%,-50%) scale(1.06)}
}
/* Nothing rolls until she has arrived, the same gate all eight others use. */
.rankedge-seo .nova-roll img,
.rankedge-seo .nova-roll::before,
.rankedge-seo .nova-roll .nova-rec,
.rankedge-seo .nova-roll .nova-rec::after{animation-play-state:paused}
.rankedge-seo .nova-roll.in img,
.rankedge-seo .nova-roll.in::before,
.rankedge-seo .nova-roll.in .nova-rec,
.rankedge-seo .nova-roll.in .nova-rec::after{animation-play-state:running}
/* hidden tab, same hook every other mascot uses */
.rankedge-seo.paused .nova-roll img,
.rankedge-seo.paused .nova-roll::before,
.rankedge-seo.paused .nova-roll .nova-rec,
.rankedge-seo.paused .nova-roll .nova-rec::after{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-roll.rv,
.rankedge-seo .nova-roll.rv.in{opacity:1;transform:none;transition:none}
.rankedge-seo .nova-roll img{animation:none;transform:none}
.rankedge-seo .nova-roll::before{animation:none;transform:translateX(-50%)}
/* the bloom and the dot stay LIT rather than vanishing. A camera with no
     record light reads as switched off, which is the wrong still frame. */
.rankedge-seo .nova-roll .nova-rec{animation:none;opacity:.75;transform:translate(-50%,-50%)}
.rankedge-seo .nova-roll .nova-rec::after{animation:none;opacity:.9;transform:translate(-50%,-50%)}}
/* ---------- 2. THE VIDEO GRID --------------------------------------------
   Five vertical reels, and the shape of the tile is the whole component.

   WHY THE TILE IS A <button> AND NOT AN <article> WITH A LINK INSIDE.
   Same call .chan made on the Contact page, for the same reason: every one of
   these is a control whose entire surface is the target. Nesting a trigger
   inside a card and stretching it with a pseudo-element gives identical pixels
   and a worse result, because the card grows a second focus stop and the
   visible focus ring lands on the text rather than on the box that is about to
   be activated. So the tile IS the button. One tag, one focus stop, one ring
   around the thing being clicked.

   It is a button rather than an anchor because it opens a dialog rather than
   navigating. An <a href="#..."> that does not change the page is a link that
   lies to a screen reader, and the fallback for no-JS is handled properly
   below with a real href instead. See the markup note on .vtile-fallback.

   WHY 9:16 IS DECLARED ON THE FIGURE AND NOT LEFT TO THE IMAGE.
   The poster is 540x960 and would size itself correctly once decoded, but
   these five are the entire visible surface of the page's main section. If
   they size themselves on decode the section reflows five times on a cold
   load, and on a slow connection it reflows while somebody is reading the
   heading above it. aspect-ratio reserves all five boxes before a byte of
   poster arrives, so the page paints its final height immediately.

   ON THE COLUMN COUNT, WHICH IS THE THIRD VERSION OF THIS RULE. The number of
   videos is not fixed and the grid must not care what it is.

     v1  repeat(auto-fit,minmax(168px,1fr))
         Collapsed to ONE column on a 375px phone: two 168px tracks plus the
         gap need 354px and only 331px are available inside the wrap. One
         column of 9:16 is a 589px tile, and a visitor scrolls past one
         product at a time.
     v2  repeat(2,1fr) / repeat(3,1fr) / repeat(5,1fr)
         Fixed counts, which fixed the phone and encoded "five" into the
         stylesheet. With three videos the row was three stretched tiles; with
         six it would have been five and a widow.
     v3  auto-fill with a TRACK CEILING, below.

   auto-FILL rather than auto-FIT, and it is the whole trick. auto-fit
   collapses the empty tracks and stretches the items to fill the row, so
   three videos on a wide screen would render as three enormous tiles and five
   as five normal ones: the same component changing size with the content.
   auto-fill keeps the empty tracks, so a tile is the same size whether there
   are three of them or thirty, and a short row simply ends early.

   THE max() IN THE TRACK SIZE IS THE CEILING. minmax(190px,1fr) on its own
   still lets a track grow without limit on a very wide screen; capping the
   lower bound near the natural tile width holds a poster at a sane size and
   lets the row spill to a second line instead. 1240px of wrap gives five
   tracks at about 217px, which is where the tiles were designed.
   -------------------------------------------------------------------------- */
.rankedge-seo .vgrid{
  display:grid;gap:14px;
  grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
}
@media (min-width:760px){.rankedge-seo .vgrid{gap:22px;grid-template-columns:repeat(auto-fill,minmax(190px,1fr))}}
.rankedge-seo .vtile{
  display:block;width:100%;padding:0;margin:0;text-align:left;
  background:none;border:0;color:inherit;cursor:pointer;
  -webkit-appearance:none;appearance:none;
}
/* The ring goes on the FRAME rather than on the button, so it hugs the poster
   instead of the button box, which is taller by the height of the caption. */
.rankedge-seo .vtile:focus-visible{outline:none}
.rankedge-seo .vtile:focus-visible .vtile-frame{outline:3px solid var(--cyan);outline-offset:3px}
/* display:block, and it is load-bearing rather than tidiness. Every element
   inside a .vtile is a <span>, because a <button> may only contain phrasing
   content and a <div> in there is invalid markup that browsers reparent. A
   <span> defaults to display:inline, and an inline box ignores aspect-ratio
   and overflow and gives its absolutely-positioned children a degenerate
   containing block: measured before this line, the frame reported 214x485
   inside a 436px tall button, the caption came out 28px wide, and the play
   badge's left:50% landed 25px to the LEFT of the tile. Blockifying it fixes
   all three at once, and it must stay on any span this component positions
   against. */
.rankedge-seo .vtile-frame{
  display:block;
  position:relative;aspect-ratio:9/16;overflow:hidden;
  border-radius:16px;background:var(--ink-2);
  border:1px solid var(--line-d);
  transition:transform .45s var(--ease),box-shadow .45s var(--ease);
}
.rankedge-seo .vtile-frame img{
  width:100%;height:100%;object-fit:cover;
  transition:transform .6s var(--ease),filter .45s var(--ease);
}
/* The lift is on the FRAME and the scale on the IMG, so the poster grows
   inside a box that is not itself growing and the corner radius stays put.
   One transform each, and neither element is written to twice. */
.rankedge-seo .vtile:hover .vtile-frame,
.rankedge-seo .vtile:focus-visible .vtile-frame{
  transform:translateY(-4px);
  box-shadow:0 18px 40px -18px rgba(3,5,14,.75);
}
.rankedge-seo .vtile:hover .vtile-frame img{transform:scale(1.045)}
/* The scrim. A gradient rather than a flat wash, because the caption sits over
   the bottom third of the poster and the top two thirds should stay as bright
   as the artwork. Ends fully transparent well before the top so it never dulls
   the frame a poster was chosen for. */
.rankedge-seo .vtile-frame::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(to top,rgba(6,8,22,.9) 0%,rgba(6,8,22,.55) 22%,rgba(6,8,22,0) 52%);
}
/* The play affordance. Centred, and it grows on hover rather than appearing on
   hover: a play badge that is only visible on hover tells a touch device
   nothing, and touch is most of the traffic for vertical video. */
.rankedge-seo .vtile-play{
  position:absolute;z-index:1;left:50%;top:46%;
  width:52px;height:52px;transform:translate(-50%,-50%);
  border-radius:50%;display:grid;place-items:center;
  background:rgba(10,12,24,.55);border:1.5px solid rgba(255,255,255,.55);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  transition:transform .4s var(--ease),background .4s var(--ease),border-color .4s var(--ease);
}
.rankedge-seo .vtile-play svg{width:17px;height:17px;margin-left:3px;fill:#fff}
.rankedge-seo .vtile:hover .vtile-play,
.rankedge-seo .vtile:focus-visible .vtile-play{
  transform:translate(-50%,-50%) scale(1.12);
  background:rgba(91,216,255,.9);border-color:rgba(255,255,255,.9);
}
.rankedge-seo .vtile:hover .vtile-play svg,
.rankedge-seo .vtile:focus-visible .vtile-play svg{fill:var(--ink)}
/* The caption, over the foot of the poster. Inside the frame rather than under
   it so five tiles of unequal title length still line up along the bottom. */
.rankedge-seo .vtile-cap{
  display:block;
  position:absolute;z-index:1;left:0;right:0;bottom:0;padding:14px 14px 15px;
}
/* Sized in clamp rather than at one size, because the tile is 155px wide on a
   phone and 214px on a wide screen, and a title set for the second wraps to
   three lines in the first. */
.rankedge-seo .vtile-cap b{
  display:block;color:#fff;font-weight:700;
  font-size:clamp(.84rem,2.4vw,.95rem);line-height:1.28;
  letter-spacing:-.01em;
}
.rankedge-seo .vtile-cap span{
  display:block;margin-top:4px;color:var(--muted-d);
  font-size:clamp(.7rem,1.9vw,.78rem);font-weight:600;
  letter-spacing:.06em;text-transform:uppercase;
}
@media (max-width:759px){.rankedge-seo .vtile-cap{padding:11px 11px 12px}}
@media (prefers-reduced-motion: reduce){.rankedge-seo .vtile-frame,
.rankedge-seo .vtile-frame img,
.rankedge-seo .vtile-play{transition:none}
.rankedge-seo .vtile:hover .vtile-frame,
.rankedge-seo .vtile:focus-visible .vtile-frame{transform:none}
.rankedge-seo .vtile:hover .vtile-frame img{transform:none}
.rankedge-seo .vtile:hover .vtile-play,
.rankedge-seo .vtile:focus-visible .vtile-play{transform:translate(-50%,-50%)}}
/* The no-JS path. The script removes this link and turns the tile into a
   button on boot; without the script it is the only thing here that works, so
   it is a real anchor to the file itself rather than a disabled control.
   Visually it is the tile: the anchor wraps the same frame. */
.rankedge-seo .vtile-fallback{display:block;text-decoration:none;color:inherit}
/* ---------- 3. THE LIGHTBOX ----------------------------------------------
   One dialog, reused for all five videos. Not five hidden players: five
   <video> elements in the DOM is five sets of decoder state, and even at
   preload="none" it is five elements the browser tracks for no reason. The
   dialog holds ONE <video> and its src is swapped as it opens.

   NOTHING IS FETCHED UNTIL A TILE IS CLICKED. The <video> ships with no src
   attribute at all — not an empty one, which some browsers resolve against
   the page URL and request. The script sets .src on open and REMOVES it on
   close, which is what actually stops a paused 30s reel continuing to buffer
   in the background. See the note on closing in scroll-cinematic.js.

   WHY IT IS NOT <dialog>. showModal() would give the top layer, the backdrop
   and the focus trap for free, and on the static site it would be the right
   answer. On WordPress it is not: this markup renders inside the page, under
   whatever Elementor header is on top of it, and a <dialog> in the top layer
   escapes every stacking context including the one the scoped stylesheet
   relies on. The whole design system is scoped under .rankedge-seo precisely
   so nothing here can paint outside it. A fixed-position div stays inside that
   contract; the trap and the Escape handling are about twenty lines and are
   written out in the engine.
   -------------------------------------------------------------------------- */
/* z-index 400, and the number is not arbitrary. A modal that something else
   paints over is not modal: at the first value tried, 120, the site nav sat on
   top of the backdrop and stayed clickable, so a visitor could open the menu
   or navigate away with the dialog still up and the page scroll-locked behind
   it. This design's own chrome runs .nav at 200, the mobile menu veil at 310
   and the sheet at 320, so 400 clears all three with room left.

   ON WORDPRESS THE HEADER IS NOT OURS. Elementor headers commonly sit
   anywhere from 100 to 999, so if the dialog ever opens UNDER the live header,
   this is the number to raise, and it is the only one that needs raising. */
.rankedge-seo .vlb{
  position:fixed;inset:0;z-index:400;
  display:grid;place-items:center;padding:24px;
  background:rgba(4,6,16,.86);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  opacity:0;transition:opacity .28s var(--ease);
}
/* [hidden] is the state, and it needs the !important because the rule above
   sets display:grid on the same element and would otherwise win on order. */
.rankedge-seo .vlb[hidden]{display:none!important}
.rankedge-seo .vlb.open{opacity:1}
/* The stage. Height-first rather than width-first, because a 9:16 video on a
   landscape screen is bounded by the viewport height, not its width. min() of
   the two keeps it correct in both orientations without a media query. */
.rankedge-seo .vlb-stage{
  position:relative;
  width:min(calc(86svh * 9 / 16), calc(100vw - 48px));
  max-height:86svh;aspect-ratio:9/16;
  border-radius:18px;overflow:hidden;
  background:#000;box-shadow:0 40px 90px -30px rgba(0,0,0,.9);
  transform:scale(.965) translateY(8px);
  transition:transform .34s var(--ease);
}
.rankedge-seo .vlb.open .vlb-stage{transform:none}
.rankedge-seo .vlb-stage video{width:100%;height:100%;object-fit:contain;display:block;background:#000}
/* Close. INSIDE the stage's top-right corner, at every size.

   The first version put it above the stage at top:-52px, on the reasoning
   that a control outside the frame never covers the footage. That reasoning
   is right and the placement is still wrong, because the space it needs is
   not there: the stage is 86svh tall and centred, so a 900px viewport leaves
   63px above it and the button wants 96px. Measured at 1280x900 it landed at
   y=20 underneath the site header, which paints over it. Any fix that keeps
   it outside is a fight with the viewport height on every screen.

   So it sits in the corner, over about 44px of a 9:16 frame, which is where
   TikTok, Reels and Shorts all put theirs. One rule, no media query, and it
   is in the same place whatever the screen is doing. It carries its own dark
   disc so it reads over a bright frame as well as a dark one.

   44px square: this is the one control on the page a thumb has to find in a
   hurry, and it is the minimum target that does not need aiming. */
.rankedge-seo .vlb-close{
  position:absolute;z-index:2;top:10px;right:10px;
  width:44px;height:44px;border-radius:50%;
  display:grid;place-items:center;cursor:pointer;
  background:rgba(6,8,22,.72);border:1px solid rgba(255,255,255,.34);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  color:#fff;transition:background .3s var(--ease),transform .3s var(--ease);
}
.rankedge-seo .vlb-close:hover{background:rgba(6,8,22,.92);transform:scale(1.07)}
.rankedge-seo .vlb-close:focus-visible{outline:3px solid var(--cyan);outline-offset:3px}
.rankedge-seo .vlb-close svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.2;fill:none}
/* The title, over the HEAD of the stage. Two placements were tried and both
   were wrong before this one: under the stage, which has no reliable space on
   a laptop for the same reason the close button had none, and over the foot of
   the stage, which collided with the video's own control bar. The foot cannot
   be made safe by padding, because the height of that control bar is decided
   by the browser and differs between them. The head of the stage has exactly
   one obstacle and it is the close button above, whose size this file sets.

   Screen readers get this text through the dialog's aria-labelledby, so it is
   the sighted half of one label rather than a second, separate one. It is
   never display:none, because hiding it would take the dialog's accessible
   name with it. */
.rankedge-seo .vlb-title{
  position:absolute;z-index:2;left:0;right:0;top:0;margin:0;
  /* 64px of right padding clears the close button (44px + its 10px inset,
     plus a little air) so a long title wraps before it reaches it, and the
     same value on the left keeps the text optically centred in the stage
     rather than centred in the space left over. */
  padding:18px 64px 30px;
  text-align:center;color:#fff;font-weight:700;font-size:1rem;line-height:1.3;
  background:linear-gradient(to bottom,rgba(6,8,22,.85) 0%,rgba(6,8,22,.45) 55%,transparent 100%);
  pointer-events:none;
}
@media (prefers-reduced-motion: reduce){.rankedge-seo .vlb,
.rankedge-seo .vlb-stage,
.rankedge-seo .vlb-close{transition:none}
.rankedge-seo .vlb-stage{transform:none}}
/* While the dialog is open the page behind it must not scroll. The class goes
   on the wrapper for the scoped WordPress build and on <body> for the static
   one; both are set by the engine, and setting a class it does not use costs
   nothing. overflow:clip rather than hidden so the page cannot be scrolled
   programmatically either. */
.rankedge-seo.vlb-lock{overflow:hidden}
/* ==========================================================================
   SEO FOR TRAVEL AGENCIES  —  the thirteenth inner page, and the first
   INDUSTRY CHILD. Two components, both new, both added by that page.

   Everything else it uses already existed: .phero, .svc-grid, .proc, .stats,
   .reviews, .faq and the contact band are all shared, which is the point of
   the set. The six industry pages that follow this one should need NOTHING
   NEW AT ALL — if the second one adds a third component, the shape of these
   pages has not been settled yet.
   ========================================================================== */
/* --------------------------------------------------------------------------
   1. .nova-soar  —  the travel page's mascot
   The ninth mascot placement, and the FIRST THAT REUSES AN EXISTING POSE.
   nova-fly.webp was drawn for the Social Media page, replaced there by
   nova-social, and has been unused in assets/ since. See the long note beside
   the <img> in the page about swapping it for a drawn-for-purpose one.

   The artwork is 560x608, the smallest master on the site, so this component
   caps at 380px rather than the 424px .nova-scan allows itself. At 380 CSS px
   a 2x phone asks for 760 of 560 available; at 424 it would ask for 848 and
   the softness would start to show on her face.

   WHAT LOOPS IS A CONTRAIL, and the distinction from the other two mascots
   holding a "search" prop matters, because all three could have used a glow:

     .nova-stand + .nova-lens   SEO page.        A pulse and TWO RINGS leaving
                                                 the glass: a sonar ping.
                                                 It means BEING FOUND.
     .nova-scan  + .nova-glass  Industries hub.  A single highlight crossing
                                                 the glass on a diagonal.
                                                 It means LOOKING AT YOURS.
     .nova-soar  + .nova-trail  this page.       A dashed route streaming out
                                                 behind her, the line a flight
                                                 path draws on a map.
                                                 It means A JOURNEY, WITH A
                                                 DESTINATION.

   Different motion, different meaning, on purpose. A ping here would argue
   the SEO page's case; a glint would argue the hub's. This page's claim is
   that a traveller is already on their way to somewhere and the only question
   is whose website they find on the way.

   MEASURED ON THE SHIPPED FILE, not guessed. The cyan thruster flame occupies
   x 0%-50%, y 58%-94%, centroid 23.8%, 79.9%, running DOWN AND TO THE LEFT
   from a nozzle at roughly 28%, 65%. Every percentage below tracks that
   diagonal, which is why the bloom is nowhere near the horizontal centre.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-soar{position:relative;width:min(100%,300px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* svh rather than vh so a phone's collapsing URL bar cannot resize her
     mid-scroll, matching the other eight. The 380px cap is this file's, not
     the design system's: see the note above about the master's size. */
.rankedge-seo .nova-soar{margin:0;justify-self:end;width:clamp(280px,min(28vw,44svh),380px)}}
.rankedge-seo .nova-soar img{position:relative;z-index:1;width:100%;height:auto}
/* Airborne, so she rises into place with no landing beat, the same entrance
   .nova-orbit, .nova-call and .nova-scan use. */
.rankedge-seo .nova-soar.rv{
  transform:translateY(34px);
  transition:opacity .75s var(--ease),transform .9s var(--ease);
}
.rankedge-seo .nova-soar.rv.in{transform:none}
/* 7.3s. Distinct from every other loop on the site: 3.9, 4.2, 4.4, 4.7, 5.1,
   5.5, 5.8, 5.9, 6.2, 6.4, 6.6, 6.9, 7.1, 7.6, 7.8, 8.1 and 8.4 are taken.
   No two mascots in lockstep, or the nine read as one rig driving all of them.
   The float lives on the <img> and the .rv entrance on the wrapper, so the two
   transforms never write to the same element and fight.

   A LONGER DRIFT THAN THE OTHERS, 16px against the usual 12. She is the only
   mascot on the site drawn mid-flight with no ground reference, so a small
   float reads as a hover rather than as travel. */
.rankedge-seo .nova-soar img{
  animation:novaSoarFloat 7.3s ease-in-out infinite;
  animation-delay:-1.7s;
  will-change:transform;
}
@keyframes novaSoarFloat{
  0%,100%{transform:translate(0,0) rotate(0deg)}
  50%{transform:translate(6px,-16px) rotate(-1.1deg)}
}
/* The thruster bloom, under the flame the artwork already draws. Placed on
   the measurements above: low and hard left, tracking the down-left diagonal.
   Locked to her period so the plume swells as she rises; a plume on its own
   clock reads as pasted on. */
.rankedge-seo .nova-soar::before{
  content:"";position:absolute;left:18%;bottom:4%;width:30%;height:24%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(91,216,255,.5) 0%,rgba(19,101,169,.2) 45%,transparent 72%);
  filter:blur(18px);
  animation:novaSoarThruster 7.3s ease-in-out infinite;
  animation-delay:-1.7s;
}
@keyframes novaSoarThruster{
  0%,100%{opacity:.52;transform:translateX(-50%) scale(1)}
  50%{opacity:.95;transform:translateX(-50%) scale(1.16)}
}
/* ---------- the contrail ----------
   An inline SVG rather than the <span> the other mascots' props use, because
   this one is an ARC and a span cannot be one. An <img> is a replaced element
   and cannot carry a pseudo-element either, and ::before here is already the
   thruster, which is the same pair of reasons every prop in this file is its
   own element.

   It deliberately OVERFLOWS the wrapper on the left and the bottom. A trail
   that stops neatly at the edge of the image box reads as a decoration; one
   that leaves the frame reads as something she is outrunning. Nothing clips
   it: .phero-in sets no overflow, and the page wrapper's overflow-x:clip is
   several levels up and wider than this.

   z-index 0 puts it behind the artwork (which is z-index 1) so the trail
   passes under her nozzle rather than over her foot. */
.rankedge-seo .nova-trail{
  position:absolute;z-index:0;pointer-events:none;
  left:-15%;bottom:-7%;width:66%;height:auto;
  overflow:visible;
}
/* The dashes stream AWAY from her, down and to the left. The path is drawn
   bottom-left to upper-right, so an INCREASING dashoffset walks the pattern
   back toward the start, which is the direction the trail should move.

   22 is the full dasharray period (9 + 13), so the loop is seamless: at
   offset 22 every dash sits exactly where its neighbour started.

   1.9s and linear, unlike every other loop in this file, and both are
   deliberate. This is the one piece of motion on the site that is meant to
   read as SPEED rather than as breathing, and an eased stream visibly slows
   at each end, which is exactly the tell that gives away a loop. */
.rankedge-seo .nova-trail path{
  animation:novaTrail 1.9s linear infinite;
  will-change:stroke-dashoffset;
}
@keyframes novaTrail{
  from{stroke-dashoffset:0}
  to{stroke-dashoffset:22}
}
.rankedge-seo.paused .nova-soar img,
.rankedge-seo.paused .nova-soar::before,
.rankedge-seo.paused .nova-trail path{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-soar img,
.rankedge-seo .nova-soar::before{animation:none}
.rankedge-seo .nova-soar.rv{transition:opacity .75s var(--ease)}
/* The trail stays, it just stops moving. It is the only thing on the page
     that says "travel" at a glance, so deleting it under reduced motion would
     cost the hero its meaning; holding it still costs nothing. */
.rankedge-seo .nova-trail path{animation:none}}
/* --------------------------------------------------------------------------
   2. .kwt  —  the keyword opportunity table
   The section the travel page exists for, and the first TABLE in this design
   system. The six industry pages after it will each want one, so this is
   written as a component rather than as one page's markup.

   IT IS A REAL <table> AND THAT IS THE POINT. Three columns with a shared
   meaning down each one is tabular data by definition, and a screen reader
   navigating it wants "Sri Lanka tour packages, Monthly searches, 2,900+"
   rather than a wall of unlabelled text. A grid of divs cannot give that
   without a pile of ARIA restating what <table> already means.

   BELOW 720px IT BECOMES CARDS, AND THE MARKUP CARRIES EXPLICIT ARIA ROLES
   SO THAT IS SAFE. This is the part worth reading before copying the
   component, because it is the part that is easy to get wrong.

   Overriding `display` on a table element drops its role from the
   accessibility tree in most browsers: a <tr> set to display:grid stops being
   a row, and its <td>s stop being cells. That is true of display:block, which
   is the usual way this pattern is written, and it is equally true of the
   display:grid used here — grid does NOT get an exemption, and assuming it
   did was the first version of this component.

   So the fix is not in this file. Every element of the table in the markup
   carries its role written out: role="table", "rowgroup", "row",
   "columnheader", "rowheader", "cell". Those attributes are redundant on
   desktop and LOAD-BEARING on a phone, so do not tidy them away.

   The other half is data-label, which is also load-bearing: the header row is
   display:none below the breakpoint, and each cell regrows its own label from
   that attribute. A cell without one loses its header on a phone.
   -------------------------------------------------------------------------- */
.rankedge-seo .kwt{
  margin-top:var(--s5);
  border:1px solid var(--line);
  border-radius:20px;
  background:var(--paper-2);
  overflow:hidden;
}
.rankedge-seo .kwt table{width:100%;border-collapse:collapse;text-align:left}
/* COLUMN WIDTHS, and they are the difference between a table and a list with
   lines through it. Left to itself the browser sizes all three columns to
   their content, which on a 1160px band leaves the keyword column at about a
   third of the width and a quarter of the row empty after the intent chip.

   So the two data columns are pinned and the keyword column takes everything
   left over. Both data columns are RIGHT-aligned against the table's right
   edge, which is what makes the volumes read as a ranked column: tabular-nums
   lines the digits up, and right alignment puts the thousands in the same
   place whether the number is 390 or 2,900+.

   The header cells have to carry the same alignment as the body cells or the
   column reads as broken at a glance, which is why the rule names both. */
.rankedge-seo .kwt th:nth-child(2),
.rankedge-seo .kwt td:nth-child(2){width:170px;text-align:right}
.rankedge-seo .kwt th:nth-child(3),
.rankedge-seo .kwt td:nth-child(3){width:170px;text-align:right}
/* Bottom, not top. The caption carries the provenance of the six numbers
   above it, and provenance is a footnote: it should be read after the data,
   not before it. It stays a <caption> rather than becoming a <p> underneath
   so it is announced as belonging to this table. */
.rankedge-seo .kwt caption{
  caption-side:bottom;
  padding:16px 22px 18px;
  border-top:1px solid var(--line);
  background:var(--paper);
  color:var(--muted);
  font-size:13px;line-height:1.5;text-align:left;
}
.rankedge-seo .kwt caption b{color:var(--purple);font-weight:700}
.rankedge-seo .kwt thead th{
  padding:15px 22px;
  border-bottom:1px solid var(--line);
  background:var(--paper);
  font-size:11.5px;font-weight:800;letter-spacing:.11em;text-transform:uppercase;
  color:var(--muted);
}
.rankedge-seo .kwt tbody th,
.rankedge-seo .kwt tbody td{
  padding:17px 22px;
  border-top:1px solid var(--line);
  font-size:15.5px;
  vertical-align:middle;
}
.rankedge-seo .kwt tbody tr:first-child th,
.rankedge-seo .kwt tbody tr:first-child td{border-top:0}
.rankedge-seo .kwt tbody th{font-weight:700;color:var(--ink)}
/* tabular-nums so the volumes line up on their digits down the column. It is
   the whole reason a table beats three stacked lists here: 2,900+ over 1,900
   over 1,300 only reads as a ranked list if the digits share a grid. */
.rankedge-seo .kwt tbody td b{
  font-weight:800;font-size:1.05rem;color:var(--ink);
  font-variant-numeric:tabular-nums;
}
/* Hover tint. Pointer-only: on touch there is no hover state to enter and the
   rule would just be dead weight in the cascade. */
@media (hover:hover) and (pointer:fine){.rankedge-seo .kwt tbody tr{transition:background .25s var(--ease)}
.rankedge-seo .kwt tbody tr:hover{background:rgba(91,56,168,.045)}}
/* The intent chips. THREE COLOURS BECAUSE THERE ARE THREE ANSWERS, and they
   are ordered by how close the searcher is to paying: green is ready now,
   purple is reading, blue is a different conversation entirely.

   THE CLASS NAMES ARE SECTOR-NEUTRAL ON PURPOSE. The first version called the
   green one .kwt-book, after the travel page's "Booking-ready" label, and
   that name was wrong the moment the e-commerce page needed to say
   "Buyer-ready" for the identical tier. Five more sectors follow, and each
   has its own word for it: instruct, enrol, enquire. The CLASS names the
   TIER, the visible text names the sector's word for it.

   Colour is NOT the only carrier — each chip says what it is in words. A page
   that encoded intent as colour alone would fail for anyone colour-blind, and
   there is no legend to fall back on. */
.rankedge-seo .kwt-i{
  display:inline-block;white-space:nowrap;
  padding:6px 13px;border-radius:999px;
  font-size:12.5px;font-weight:700;letter-spacing:.01em;
  border:1px solid;
}
.rankedge-seo .kwt-hot{color:#12704A;background:rgba(26,125,79,.09);border-color:rgba(26,125,79,.26)}
.rankedge-seo .kwt-res{color:var(--purple);background:rgba(91,56,168,.08);border-color:rgba(91,56,168,.24)}
.rankedge-seo .kwt-b2b{color:var(--blue);background:rgba(19,101,169,.08);border-color:rgba(19,101,169,.24)}
/* The line under the table that says what the table means. Sized and
   coloured as a standfirst rather than as body copy, because it is doing a
   heading's job: it is the sentence somebody quotes back to you. */
.rankedge-seo .kwt-note{
  margin:var(--s4) auto 0;max-width:62ch;text-align:center;
  color:var(--muted);font-size:1.02rem;line-height:1.65;
}
/* ---------- phone: rows become cards ----------
   720px matches .svc-grid's first breakpoint, so the table restacks on the
   same screens the card grids above it do. */
@media (max-width:719px){.rankedge-seo .kwt{background:transparent;border:0;border-radius:0;overflow:visible}
.rankedge-seo .kwt table{border-collapse:separate;border-spacing:0}
/* The header row is removed from BOTH trees deliberately. Its three labels
     reappear on every cell below via data-label, so leaving it for screen
     readers would have them announce each header twice. display:none, not
     visibility or a clip rect. */
.rankedge-seo .kwt thead{display:none}
.rankedge-seo .kwt tbody tr{
    display:grid;grid-template-columns:1fr auto;gap:2px 14px;align-items:center;
    margin-bottom:12px;padding:16px 18px;
    border:1px solid var(--line);border-radius:16px;background:var(--paper-2);
  }
.rankedge-seo .kwt tbody th,
.rankedge-seo .kwt tbody td{padding:0;border:0;font-size:15px}
/* The desktop column widths and right alignment have to go, or the two
     data cells keep a 170px floor inside a grid track that is now sized by
     the card. Left-aligned here because in card form each cell sits under its
     own label rather than in a shared column, and a right-aligned value under
     a left-aligned label reads as a mistake. The chip is pushed to the end of
     its row by justify-self below, which is a different mechanism. */
.rankedge-seo .kwt tbody td:nth-child(2),
.rankedge-seo .kwt tbody td:nth-child(3){width:auto;text-align:left}
/* The keyword spans the full width and reads as the card's title; the
     volume and the intent chip share the line under it. */
.rankedge-seo .kwt tbody th{grid-column:1 / -1;margin-bottom:8px;font-size:16px;line-height:1.35}
.rankedge-seo .kwt tbody td:last-child{justify-self:end}
/* The labels the hidden thead used to carry. Only on the two cells that
     need one: the keyword is the card's title and needs no "Keyword:" over
     it, so its th sets its label to none rather than the rule listing cells
     one by one. */
.rankedge-seo .kwt tbody td::before{
    content:attr(data-label);display:block;margin-bottom:4px;
    font-size:10.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
    color:var(--muted);
  }
.rankedge-seo .kwt tbody td:last-child::before{text-align:right}
.rankedge-seo .kwt tbody th::before{content:none}
.rankedge-seo .kwt caption{
    padding:14px 18px;margin-top:4px;
    border:1px solid var(--line);border-radius:14px;background:var(--paper);
  }}
/* A KEYWORD THAT IS A PATTERN RATHER THAN A TERM.
   The e-commerce page's table carries two rows whose live wording is
   "[Product] price in Sri Lanka" — a placeholder the client's own page never
   substituted. It is not a mistake to carry across: the row genuinely
   describes a SHAPE of search that repeats across a catalogue, and its volume
   is the aggregate across products rather than any one of them. Naming a
   single product there would attach an aggregate number to one term, which
   would be wrong in a way the brackets are not.

   So the placeholder stays and is styled as one: muted, in its own type, so
   it reads as "substitute your product here" rather than as an unfinished
   page. Law firms will want the same for "[practice area] lawyer Colombo".
   ------------------------------------------------------------------------ */
.rankedge-seo .kwt-var{
  color:var(--purple);font-weight:800;
  font-size:.92em;letter-spacing:.005em;
  padding:1px 5px;margin-right:1px;
  border:1px dashed rgba(91,56,168,.4);border-radius:6px;
  background:rgba(91,56,168,.05);
  white-space:nowrap;
}
/* ==========================================================================
   SEO FOR E-COMMERCE  —  the fourteenth inner page, and the SECOND INDUSTRY
   CHILD. One component, and that is the whole point of it.

   The travel page's notes predicted that the six industry pages after it
   should need NOTHING NEW in the design system, and that if the second one
   added a component the shape of these pages had not been settled. This page
   adds .nova-shop, which is a MASCOT, and every page in this set has always
   had its own — and .kwt-var above, which is four lines for a placeholder.
   Its problem cards, service grid, staircase, stats, keyword table, reviews,
   FAQ and contact band are all the travel page's components, unchanged.
   So the prediction held.
   ========================================================================== */
/* --------------------------------------------------------------------------
   .nova-shop  —  the e-commerce page's mascot
   The tenth mascot placement, and the FIRST GROUNDED ONE SINCE THE SEO PAGE.
   She stands on a stack of shipping boxes holding a laptop, with a storefront
   panel floating beside her, so she takes .nova-stand's vocabulary rather
   than the airborne one: a landing entrance with overshoot, a breath that
   scales about her contact point, and a contact shadow that answers it.

   MEASURED ON THE SHIPPED CROP, not on the master, and the difference
   matters. The supplied artwork is 1024x1536 with about 3% of empty margin
   on every side; the shipped file is cropped to her own alpha bounds plus 6px
   and comes out 991x1458, which is already under the 1024px width cap so
   nothing was resized. Every percentage below is measured on that crop.

   HER GROUND CONTACT IS THE BOXES, NOT HER SOLES, which is why the shadow
   here is not .nova-stand's. Measured: the lowest solid pixel is 99.5% down
   (the crop runs to her shadow's edge), and the bottom 12% of the frame spans
   1.5% to 81.0% across, centred at 41.3%. So she stands well left of centre
   and a shadow centred on the box would sit visibly to her right.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-shop{position:relative;width:min(100%,282px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* 350px, between the SEO page's 344 and the travel page's 380. The crop is
     991x1458, a 0.68 ratio, so at 350px she renders 515px tall — within a few
     pixels of the SEO mascot's 516 at 344px, which is the point: these two
     are the grounded pair and they should read as the same size on screen.
     svh rather than vh so a phone's collapsing URL bar cannot resize her
     mid-scroll, matching the other nine. */
.rankedge-seo .nova-shop{margin:0;justify-self:end;width:clamp(262px,min(26vw,42svh),350px)}}
.rankedge-seo .nova-shop img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance, from .nova-stand: she arrives on her feet rather than fading
   upward, scaled about her contact point so the landing reads as weight. */
.rankedge-seo .nova-shop.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-shop.rv.in{transform:none}
/* 6.8s. Distinct from every other loop on the site: 3.9, 4.2, 4.4, 4.7, 5.1,
   5.5, 5.8, 5.9, 6.2, 6.4, 6.6, 6.9, 7.1, 7.3, 7.6, 7.8, 8.1 and 8.4 are
   taken. No two mascots in lockstep, or the ten read as one rig driving all
   of them.

   A SCALE ABOUT HER CONTACT POINT, not a translate, for .nova-stand's reason:
   translating a standing figure lifts her off her own shadow and reads as a
   hop. The origin is 41% 99%, her measured contact centre, not the 50% 94%
   the SEO mascot uses — she is stood on boxes that are off to her left. */
.rankedge-seo .nova-shop img{
  transform-origin:41% 99%;
  animation:novaShopBreathe 6.8s ease-in-out infinite;
  animation-delay:-2.9s;
  will-change:transform;
}
@keyframes novaShopBreathe{
  0%,100%{transform:translateY(0) scaleY(1) rotate(0deg)}
  50%{transform:translateY(-3px) scaleY(1.007) rotate(.4deg)}
}
/* The contact shadow, under the boxes. left:41% and width:76% come from the
   measurement above; a shadow centred on the frame would float to her right
   and the stack would look like it was hovering. Same period as the breath,
   deliberately locked to it: a contact shadow that holds still while the
   figure moves is what makes cheap mascot animation look pasted on. */
.rankedge-seo .nova-shop::before{
  content:"";position:absolute;left:41%;bottom:0.6%;width:76%;height:4.6%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.6) 0%,rgba(3,5,14,.28) 52%,transparent 78%);
  filter:blur(10px);
  animation:novaShopShadow 6.8s ease-in-out infinite;
  animation-delay:-2.9s;
}
@keyframes novaShopShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.82;transform:translateX(-50%) scale(.94)}
}
/* ---------- the catalogue lighting up ----------
   Her prop, and the fourth distinct one in the set. The three before it:

     .nova-stand + .nova-lens   SEO page.        Concentric rings leaving a
                                                 lens: a sonar ping, repeating
                                                 and outward. BEING FOUND.
     .nova-scan  + .nova-glass  Industries hub.  One highlight crossing a lens
                                                 on a diagonal. Linear.
                                                 LOOKING AT YOURS.
     .nova-soar  + .nova-trail  Travel.          Dashed route streaming behind
                                                 her. A JOURNEY.
     .nova-shop  + .nova-index  this page.       Four product tiles blooming
                                                 ONE AFTER ANOTHER, top-left to
                                                 bottom-right. A SEQUENCE.
                                                 YOUR WHOLE CATALOGUE, RANKING.

   THE MOTION IS A STAGGER, and that is the point: it is the only prop here
   that is a sequence rather than a single repeating gesture, because it is
   the only page whose argument is about SCALE. This page sells optimisation
   applied across thousands of product pages rather than one at a time — its
   own FAQ says "improvements scale across the catalog rather than page by
   page" — and four tiles lighting in turn is that sentence as motion.
   A pulse or a sweep here would have argued a neighbouring page's case.

   ONE SVG rather than four spans, for the reason every prop in this file is
   its own element: an <img> is a replaced element and cannot carry a
   pseudo-element, and ::before here is already the shadow. Four ellipses in
   one SVG is one node in the markup and one paint.

   preserveAspectRatio="none" with a 0-100 viewBox makes the viewBox units
   read as PERCENTAGES of the wrapper, so the four centres below are the
   measured percentages typed straight in. The cost is that a circle would
   render as an ellipse, so every ry is its rx times the crop's 0.6797 ratio
   and the blooms come out round. Change the artwork and that factor changes
   with it. */
.rankedge-seo .nova-index{
  position:absolute;z-index:2;inset:0;pointer-events:none;
  width:100%;height:100%;
}
.rankedge-seo .nova-index ellipse{
  animation:novaIndexBloom 4.6s ease-in-out infinite;
  transform-box:fill-box;transform-origin:center;
  will-change:opacity,transform;
}
/* Reading order: shirt, shoe, bag, headphones. 0.34s apart, so the sweep
   takes just over a second and the rest of the 4.6s cycle is quiet. Present,
   not busy — it sits beside a hero people are trying to read. */
.rankedge-seo .nova-index ellipse:nth-of-type(1){animation-delay:0s}
.rankedge-seo .nova-index ellipse:nth-of-type(2){animation-delay:.34s}
.rankedge-seo .nova-index ellipse:nth-of-type(3){animation-delay:.68s}
.rankedge-seo .nova-index ellipse:nth-of-type(4){animation-delay:1.02s}
/* Held at 0 for most of the cycle, so what reads is four discrete events
   rather than four things breathing. The scale is small on purpose: over a
   near-white tile the opacity does the work and a big scale would smear it. */
@keyframes novaIndexBloom{
  0%,100%{opacity:0;transform:scale(.72)}
  6%     {opacity:.85;transform:scale(1)}
  20%    {opacity:.5;transform:scale(1.04)}
  34%    {opacity:0;transform:scale(1.08)}
}
.rankedge-seo.paused .nova-shop img,
.rankedge-seo.paused .nova-shop::before,
.rankedge-seo.paused .nova-index ellipse{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-shop img,
.rankedge-seo .nova-shop::before{animation:none}
.rankedge-seo .nova-shop.rv{transition:opacity .7s var(--ease)}
/* The blooms stop AND clear, unlike the travel page's trail which is held
     still. A trail frozen mid-flight still means "a journey"; a bloom frozen
     mid-fade is just a smudge over a product, and freezing all four at once
     would say the opposite of a sequence. */
.rankedge-seo .nova-index{display:none}}
/* ==========================================================================
   SEO FOR REAL ESTATE  —  the fifteenth inner page, and the THIRD INDUSTRY
   CHILD. One component, and it is a mascot, which every page in this set has
   always had. Nothing else in the design system moved to take a third sector:
   the problem cards, service grid, staircase, stats, keyword table, reviews,
   FAQ and contact band are all the travel page's, unchanged, and .kwt needed
   no edit at all this time — not even a new chip tier.
   ========================================================================== */
/* --------------------------------------------------------------------------
   .nova-estate  —  the real estate page's mascot
   The eleventh mascot placement, and the SECOND GROUNDED ONE IN A ROW. She
   stands with a magnifying glass raised in one hand and a laptop in the
   other, with a lit house model on the ground beside her, so she takes
   .nova-stand's vocabulary: a landing entrance with overshoot, a breath that
   scales about her contact point, and a contact shadow that answers it.

   MEASURED ON THE SHIPPED CROP. The master is 1024x1536 with a lot of empty
   margin — 9.6% at the top and 7.4% at the foot, the most of any hero here.
   The shipped file is cropped to her alpha bounds plus 6px and comes out
   1004x1288, under the 1024px width cap, so like the e-commerce mascot this
   is a crop with NO resize. Every percentage below is measured on that crop.

   SHE IS THE WIDEST-RATIO PORTRAIT IN THE SET at 0.7795, against the
   e-commerce mascot's 0.68 and the SEO page's 0.667, because the house model
   sits BESIDE her rather than under her. At a matched width she would render
   noticeably shorter than the other two grounded mascots, so the cap here is
   390px rather than 350: 390 x 0.7795 gives 500px tall, within 15px of the
   e-commerce mascot's 515 and the SEO page's 516. The three grounded
   mascots should read as the same size on screen, not as the same number in
   a stylesheet.

   HER GROUND CONTACT IS HER FEET AND THE HOUSE'S PLINTH TOGETHER, which is
   why the shadow here is neither .nova-stand's nor .nova-shop's. Measured:
   the lowest solid pixel is 99.4% down and the bottom 10% of the frame spans
   6.0% to 99.3% across, centred at 52.6%. That is nearly the full width of
   the frame, and a shadow sized for a figure alone would leave the house
   visibly floating.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-estate{position:relative;width:min(100%,300px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* svh rather than vh so a phone's collapsing URL bar cannot resize her
     mid-scroll, matching the other ten. See the note above on why this cap is
     390 and not the e-commerce page's 350. */
.rankedge-seo .nova-estate{margin:0;justify-self:end;width:clamp(280px,min(29vw,40svh),390px)}}
.rankedge-seo .nova-estate img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance, from .nova-stand: she arrives on her feet rather than fading
   upward, scaled about her contact point so the landing reads as weight. */
.rankedge-seo .nova-estate.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-estate.rv.in{transform:none}
/* 5.3s. Distinct from every other loop on the site: 3.9, 4.2, 4.4, 4.6, 4.7,
   5.1, 5.5, 5.8, 5.9, 6.2, 6.4, 6.6, 6.8, 6.9, 7.1, 7.3, 7.6, 7.8, 8.1 and
   8.4 are taken. No two mascots in lockstep, or the eleven read as one rig.

   A SMALLER BREATH THAN THE OTHER TWO GROUNDED MASCOTS, 1.005 against 1.007
   and 1.008, and the house is the reason. Her breath scales the whole <img>,
   the house included, and a building that visibly swells reads as wrong in a
   way a swelling robot does not. This is the largest scale that still moves
   her and leaves the house looking like masonry. */
.rankedge-seo .nova-estate img{
  transform-origin:52% 99%;
  animation:novaEstateBreathe 5.3s ease-in-out infinite;
  animation-delay:-1.3s;
  will-change:transform;
}
@keyframes novaEstateBreathe{
  0%,100%{transform:translateY(0) scaleY(1) rotate(0deg)}
  50%{transform:translateY(-3px) scaleY(1.005) rotate(.3deg)}
}
/* The contact shadow, under her feet AND the house's plinth. left:52% and
   width:90% come from the measurement above; this is the widest shadow in
   the set because it is the only one covering two objects. Same period as
   the breath, deliberately locked to it. */
.rankedge-seo .nova-estate::before{
  content:"";position:absolute;left:52%;bottom:0.5%;width:90%;height:4.2%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.58) 0%,rgba(3,5,14,.26) 54%,transparent 79%);
  filter:blur(11px);
  animation:novaEstateShadow 5.3s ease-in-out infinite;
  animation-delay:-1.3s;
}
@keyframes novaEstateShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.84;transform:translateX(-50%) scale(.95)}
}
/* ---------- the lights coming on ----------
   Her prop, and the fifth distinct one in the set. The four before it:

     .nova-stand  + .nova-lens    SEO page.       Concentric rings leaving a
                                                  lens: a sonar ping.
                                                  BEING FOUND.
     .nova-scan   + .nova-glass   Industries hub. One highlight crossing a
                                                  lens. LOOKING AT YOURS.
     .nova-soar   + .nova-trail   Travel.         Dashed route streaming
                                                  behind her. A JOURNEY.
     .nova-shop   + .nova-index   E-commerce.     Four tiles blooming in
                                                  turn. A SEQUENCE, meaning
                                                  YOUR WHOLE CATALOGUE.
     .nova-estate + .nova-lights  this page.      The house's windows warming
                                                  up. A slow swell, and the
                                                  only WARM one in the set.
                                                  A PROPERTY FOUND.

   SHE IS HOLDING A MAGNIFYING GLASS AND IT IS DELIBERATELY NOT ANIMATED.
   That prop already carries two meanings on this site — the SEO page's lens
   pings, the Industries hub's glass catches a highlight — and a third lens
   treatment would have this page arguing a neighbour's case in a slightly
   different accent. The house is the only object in the drawing that is
   about PROPERTY, so the house is what moves. Same call the e-commerce
   mascot made with three drawn props and one animated.

   IT IS THE ONLY AMBER THING ON THE SITE. Every other prop is cyan or
   purple, the brand's own accents. Warm light is what "somebody is home"
   looks like, and there is no cool version of that. It is the one place a
   colour outside the palette earns its place.

   MIX-BLEND-MODE: SCREEN IS DOING THE MASKING, and that is the trick worth
   understanding before copying this. A flat amber wash over the house would
   fog the roof and the walls as much as the windows. Screen adds light
   rather than paint, so it is self-masking: over the near-black roof it does
   almost nothing, over the white render it does nothing at all because white
   is already at the ceiling, and over the amber windows — the only bright
   warm pixels in the region — it lifts them. What reads is the interior
   getting brighter, not a haze sitting on top of a photograph.

   Measured on the shipped crop: the warm window pixels span 58.9% to 96.0%
   across and 71.4% to 92.6% down, centroid 73.6%, 83.9%. The ellipse below
   is that region, so it is deliberately WIDER THAN TALL and does NOT get the
   ratio compensation the e-commerce blooms need — those had to come out
   round, this one has to match a house's footprint. */
.rankedge-seo .nova-lights{
  position:absolute;z-index:2;inset:0;pointer-events:none;
  width:100%;height:100%;
  mix-blend-mode:screen;
}
.rankedge-seo .nova-lights ellipse{
  animation:novaLightsWarm 7.9s ease-in-out infinite;
  will-change:opacity;
}
/* 7.9s, the slowest loop on the site, and slow is the point. Every other
   prop here is an event — a ping, a sweep, a bloom. This one is a room
   settling: it comes up over about three seconds, holds, and eases back.
   Anything quicker reads as a flicker, which is what a fault looks like, not
   what a home looks like. */
@keyframes novaLightsWarm{
  0%,100%{opacity:.16}
  46%    {opacity:.92}
  62%    {opacity:.86}
}
.rankedge-seo.paused .nova-estate img,
.rankedge-seo.paused .nova-estate::before,
.rankedge-seo.paused .nova-lights ellipse{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-estate img,
.rankedge-seo .nova-estate::before{animation:none}
.rankedge-seo .nova-estate.rv{transition:opacity .7s var(--ease)}
/* Held, not hidden, unlike the e-commerce blooms. Those are four discrete
     events and freezing them all lit says the opposite of a sequence; this
     is one steady glow, so stopping it mid-swell still means exactly what it
     meant while it was moving. Pinned just under its peak. */
.rankedge-seo .nova-lights ellipse{animation:none;opacity:.8}}
/* ==========================================================================
   SEO FOR CONSTRUCTION  —  the sixteenth inner page, and the FOURTH INDUSTRY
   CHILD. One component, and it is a mascot. Nothing else in the design system
   moved: the keyword table took no edit for the second sector running, and
   its third "hot" label — "Lead-ready", after "Booking-ready" and
   "Buyer-ready" — dropped straight into the existing .kwt-hot with no new
   class, which is exactly what that rename at sector two was for.
   ========================================================================== */
/* --------------------------------------------------------------------------
   .nova-build  —  the construction page's mascot
   The twelfth mascot placement, and the THIRD GROUNDED ONE IN A ROW. She
   stands in a hard hat with a rolled blueprint under one arm and a floating
   tablet in the other hand, so she takes .nova-stand's vocabulary: a landing
   entrance with overshoot, a breath that scales about her contact point, and
   a contact shadow that answers it.

   ===== THIS IS THE HEAVIEST HERO IN THE SET AND IT IS THE ONLY ONE CAPPED
   BELOW ITS OWN CROP WIDTH. Both facts have the same cause and it is worth
   recording, because the next detailed master will raise the same question.

   Measured detail energy on the supplied masters, mean |px - blur(1.0)| over
   opaque pixels:

       mascot_const        6.92     <- this one
       mascot_products     4.42
       mascot-realestate   3.68
       mascot_industry     3.68
       mascot_ecommerce    3.37
       mascot_contact      3.22

   About 1.7x the next busiest, from three things the drawing genuinely
   contains: the LED dot-matrix face, the bar chart on the tablet, and the
   blueprint roll's stack of parallel edges. That is drawing, not grain, so
   denoising it would be deleting the picture.

   WEBP QUALITY IS NOT THE LEVER. At 780px wide: q78 gives 255KB, q82 268KB,
   q86 283KB. Thirteen percent of the file for four quality points, so
   dropping quality buys almost nothing and costs fidelity on the one image
   above the fold. DIMENSION is the lever, so the large candidate is capped at
   780px rather than the crop's own 901. The slot is 350px, making it 2.23x
   where every other hero ships 2.8x to 3.0x — still comfortably retina, and
   331KB becomes 283KB.

   It is still the heaviest hero here. If that ever matters more than the
   detail, the fix is a simpler master, not a smaller number in this file.

   MEASURED ON THE SHIPPED FILE, 780x1142, ratio 0.6830 — the same family as
   the e-commerce mascot's 0.68, so it takes the same 350px cap and renders
   512px tall, within four pixels of hers.

   HER GROUND CONTACT IS HER FEET ALONE, unlike the two grounded mascots
   before her: nothing else in this drawing touches the floor. Measured, the
   bottom 10% of the frame spans 15.6% to 80.1% across, centred at 47.9%.
   -------------------------------------------------------------------------- */
.rankedge-seo .nova-build{position:relative;width:min(100%,286px);margin:var(--s4) auto 0;justify-self:center}
@media (min-width:1000px){/* svh rather than vh so a phone's collapsing URL bar cannot resize her
     mid-scroll, matching the other eleven. */
.rankedge-seo .nova-build{margin:0;justify-self:end;width:clamp(264px,min(26vw,42svh),350px)}}
.rankedge-seo .nova-build img{position:relative;z-index:1;width:100%;height:auto}
/* Her entrance, from .nova-stand: she arrives on her feet rather than fading
   upward, scaled about her contact point so the landing reads as weight. */
.rankedge-seo .nova-build.rv{
  transform:translateY(30px) scale(.965);transform-origin:50% 100%;
  transition:opacity .7s var(--ease),transform .95s cubic-bezier(.16,.84,.3,1.02);
}
.rankedge-seo .nova-build.rv.in{transform:none}
/* 6.1s. Distinct from every other loop on the site: 3.9, 4.2, 4.4, 4.6, 4.7,
   5.1, 5.3, 5.5, 5.6, 5.8, 5.9, 6.2, 6.4, 6.6, 6.8, 6.9, 7.1, 7.3, 7.6, 7.8,
   7.9, 8.1 and 8.4 are taken. No two mascots in lockstep, or the twelve read
   as one rig driving all of them.

   NO ROTATION IN THIS BREATH, and it is the only mascot loop without one. She
   is holding a tablet flat in an open palm; a figure that rocks a degree
   either way while balancing something on her hand looks like she is about to
   drop it. The lift alone carries the breath. */
.rankedge-seo .nova-build img{
  transform-origin:48% 99%;
  animation:novaBuildBreathe 6.1s ease-in-out infinite;
  animation-delay:-2.2s;
  will-change:transform;
}
@keyframes novaBuildBreathe{
  0%,100%{transform:translateY(0) scaleY(1)}
  50%{transform:translateY(-3px) scaleY(1.007)}
}
/* The contact shadow, under her feet. Narrower than the real estate page's,
   which had to cover a house as well; this is a figure standing alone, so it
   is sized to the 64.5% measured above plus a little spill. Same period as
   the breath, deliberately locked to it. */
.rankedge-seo .nova-build::before{
  content:"";position:absolute;left:48%;bottom:0.5%;width:70%;height:4.4%;
  transform:translateX(-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(3,5,14,.62) 0%,rgba(3,5,14,.28) 52%,transparent 78%);
  filter:blur(10px);
  animation:novaBuildShadow 6.1s ease-in-out infinite;
  animation-delay:-2.2s;
}
@keyframes novaBuildShadow{
  0%,100%{opacity:1;transform:translateX(-50%) scale(1)}
  50%{opacity:.83;transform:translateX(-50%) scale(.94)}
}
/* ---------- the chart building itself ----------
   Her prop, and the sixth distinct one in the set:

     .nova-stand  + .nova-lens    SEO page.       Rings leaving a lens.
                                                  BEING FOUND.
     .nova-scan   + .nova-glass   Industries hub. A highlight crossing a lens.
                                                  LOOKING AT YOURS.
     .nova-soar   + .nova-trail   Travel.         Dashes streaming behind her.
                                                  A JOURNEY.
     .nova-shop   + .nova-index   E-commerce.     Four tiles blooming in turn.
                                                  A CATALOGUE.
     .nova-estate + .nova-lights  Real estate.    A house warming up. Slow.
                                                  A PROPERTY FOUND.
     .nova-build  + .nova-rise    this page.      Seven bars GROWING UP from a
                                                  baseline, left to right.
                                                  A PIPELINE THAT BUILDS.

   THE MOTION IS VERTICAL GROWTH, and that is what makes it this page's and
   not the e-commerce page's. Both are staggered sequences, but those are
   opacity blooms on four separate tiles — enumeration, one catalogue item
   after another. These are bars getting TALLER from a shared floor, which is
   accumulation, and it means the two things this page is about at once: a
   chart going up, and something being built. Its own copy is "SEO builds a
   steady stream of inbound project inquiries you don't have to chase", which
   is a pipeline accruing rather than a list being counted.

   SOFT-EDGED, AND THAT IS A ROBUSTNESS DECISION AS MUCH AS A VISUAL ONE. The
   tablet in the artwork sits at a slight angle, so hard-edged rectangles laid
   over drawn bars would show every pixel of misregistration. A soft vertical
   gradient anchored at the baseline reads as the bar glowing as it grows, and
   a pixel or two of drift reads as glow rather than as a mistake.

   Measured on the shipped 780x1142 file. The chart baseline is at 31.4% down
   and the seven bar centres and tops are:

       cx 73.5  top 28.8      cx 86.9  top 23.2
       cx 76.8  top 27.9      cx 90.3  top 22.8
       cx 80.2  top 26.5      cx 93.7  top 20.8
       cx 83.5  top 25.3

   viewBox units are percentages because preserveAspectRatio is "none". Widths
   are NOT ratio-compensated: a bar should be a bar, not a square. */
.rankedge-seo .nova-rise{
  position:absolute;z-index:2;inset:0;pointer-events:none;
  width:100%;height:100%;
}
.rankedge-seo .nova-rise rect{
  transform-box:fill-box;transform-origin:50% 100%;
  animation:novaRiseGrow 5.6s ease-in-out infinite;
  will-change:transform,opacity;
}
/* Left to right, 0.13s apart, so the whole chart builds in under a second and
   the rest of the 5.6s cycle is quiet. Present, not busy: it sits beside a
   hero people are trying to read. */
.rankedge-seo .nova-rise rect:nth-of-type(1){animation-delay:0s}
.rankedge-seo .nova-rise rect:nth-of-type(2){animation-delay:.13s}
.rankedge-seo .nova-rise rect:nth-of-type(3){animation-delay:.26s}
.rankedge-seo .nova-rise rect:nth-of-type(4){animation-delay:.39s}
.rankedge-seo .nova-rise rect:nth-of-type(5){animation-delay:.52s}
.rankedge-seo .nova-rise rect:nth-of-type(6){animation-delay:.65s}
.rankedge-seo .nova-rise rect:nth-of-type(7){animation-delay:.78s}
/* Grows, holds, then fades where it stands rather than shrinking back down.
   A bar that retracts says the number went down again, which is the opposite
   of what this page is selling. */
@keyframes novaRiseGrow{
  0%     {transform:scaleY(0);opacity:0}
  9%     {transform:scaleY(1);opacity:.95}
  38%    {transform:scaleY(1);opacity:.85}
  56%    {transform:scaleY(1);opacity:0}
  100%   {transform:scaleY(1);opacity:0}
}
.rankedge-seo.paused .nova-build img,
.rankedge-seo.paused .nova-build::before,
.rankedge-seo.paused .nova-rise rect{animation-play-state:paused!important}
@media (prefers-reduced-motion: reduce){.rankedge-seo .nova-build img,
.rankedge-seo .nova-build::before{animation:none}
.rankedge-seo .nova-build.rv{transition:opacity .7s var(--ease)}
/* Hidden, like the e-commerce blooms and unlike the house glow. Eight bars
     frozen at full height is a chart with a highlight sitting on it for no
     reason; the drawn chart underneath already says everything this prop
     says, so the honest reduced-motion state is simply the artwork. */
.rankedge-seo .nova-rise{display:none}}
/* ============================================================
   WORDPRESS ADJUSTMENTS
   Appended by the build, after the scoped design system, so it
   wins on source order. These are the differences between the
   standalone page and the same page living inside WordPress.
   ============================================================ */

/* ---------- clearance under the site header ----------
   Measured on the live site rather than assumed: the header's outer Elementor
   container is position:absolute, so the header contributes ZERO height to the
   flow and floats over the top of the page. The wrapper starts at y=0 and the
   hero runs underneath the menu. That means the hero's top padding is not
   decoration, it is the only thing keeping the breadcrumb out from under the
   navigation.

   An earlier build cut this padding back to the ordinary band rhythm, on the
   assumption that an Elementor header sits in normal flow and pushes the page
   down. For this site that was wrong, and on phones it left 28px between the
   hamburger and the breadcrumb.

   So the clearance is stated as "header height plus the usual band padding".
   --re-header-h is the one number to change if the header is ever resized;
   76px matches the current build, where the logo bottoms out at 65px.

   If you ever move to a header that IS in normal flow, set --re-header-h to 0
   and the hero returns to plain band spacing on its own. */
.rankedge-seo{ --re-header-h: 76px; }

.rankedge-seo .phero{ padding-top: calc(var(--re-header-h) + var(--s6)); }
@media (min-width:900px){
  .rankedge-seo .phero{ padding-top: calc(var(--re-header-h) + var(--s7)); }
}

/* The short-screen trim. inner.css carries this same rule, but this block is
   appended after the whole design system and therefore outranks it on source
   order, so a version that only lived upstream would be silently overridden
   here and the padding would stay at its tall-screen value on WordPress.
   Anything in this file that touches .phero padding has to restate it.

   180px of top clearance is a fair share of a tall screen and an unfair one
   of a 700px laptop, where it was helping push the hero's call to action past
   the fold. */
@media (min-width:1000px) and (max-height:900px){
  .rankedge-seo .phero{
    padding-top: calc(var(--re-header-h) + var(--s6));
    padding-bottom: var(--s6);
  }
  .rankedge-seo .phero-pillars{ margin-top: var(--s5); padding-top: var(--s4); }
}

/* The wrapper is a div in the middle of a WordPress page, not the document
   body, so it must not try to own the page background or the viewport. */
.rankedge-seo{ background: transparent; }

/* ---------- the theme's select replacement, switched off ------------------
   THE SYMPTOM: on the contact page the "What is it about?" dropdown rendered
   about 133px wide beside six fields that are the full width of the form, and
   the NEXT field's label wrapped up alongside it instead of starting on its
   own line.

   THE CAUSE, measured on the live page rather than guessed. The theme ships
   jQuery Nice Select (its CSS is in seomy-core.css). On DOM ready its script
   writes style="display:none" INLINE on every <select> and inserts its own
   <div class="nice-select"> after it. That div is float:left with width:auto,
   so it shrink-to-fits its longest option, and a float is out of normal flow,
   which is what pulled the following label up beside it.

   Measured at 1280px: .fld 495px, the text inputs 495px, .nice-select 133px.
   At a narrow viewport shrink-to-fit is capped by the available width, so the
   bug hides itself on a phone and only shows on a desktop.

   THE FIX IS TO PUT THE NATIVE <select> BACK, not to restyle the widget:

     - .fld select already has a complete treatment in inner.css, matching the
       other six controls exactly: our border, our 12px radius, our focus ring
       and an inline-SVG chevron. Restyling the widget would mean rebuilding
       all of that on the theme's markup AND on its dropdown <ul>, which is a
       lot more surface for a worse result.
     - A native <select> is better for accessibility than a div[tabindex="0"],
       and on a phone it opens the real OS picker.
     - The widget's markup belongs to the theme and can change under us on any
       theme update. The <select> is ours.

   WHY !important, WHICH IS NOT A HABIT HERE. The declaration being beaten is
   an INLINE style written by the plugin's JavaScript, and an inline
   declaration outranks every ordinary author rule no matter how specific. An
   author !important is the only thing above it. It also means this keeps
   working if the script runs again later, which a specificity fight would not.

   Hiding the widget does not break the plugin: it syncs widget -> select on
   click, and nothing can click an element with display:none. The form reads
   the <select>, which is what the visitor is now using directly.

   IF THE THEME EVER DROPS NICE SELECT, both rules become harmless no-ops:
   .nice-select stops existing and the select is display:block anyway.
   -------------------------------------------------------------------------- */
.rankedge-seo .fld select{ display: block !important; }
.rankedge-seo .nice-select{ display: none !important; }
