/* ============================================================
   TOKENS — lifted from hs-pos-pro/public/index.html :root, then generalised
   when a second theme arrived.

   ⛔ THESE ARE ROLE RULES, NOT COLOUR FACTS, AND THE DIFFERENCE MATTERS.  The
   earlier version of this block asserted "yellow is never text on cream" and
   "text ON yellow is ALWAYS ink".  Both were true, neither survives a theme
   swap, and a rule stated as a hex fact is one nobody can apply to the next
   palette.  Restated as roles:

     - --accent is a SURFACE.  It is never text, except on --ink, where it has
       its own token (--accent-on-ink) precisely because a colour that works as
       a chip and a colour that works as text on a dark ground are not the same
       colour in every palette.
     - text ON --accent is ALWAYS --accent-ink, which inverts between themes.
     - --accent-ink-dim / -soft are the two dimmed variants of --accent-ink,
       BAKED.  ⛔ Never reintroduce opacity to get them: opacity composites text
       toward the surface, so the same declaration raises contrast on a light
       chip and destroys it on a dark one.
     - --ink-rgb / --paper-rgb are channel twins used by every rgba() that needs
       the ground at partial alpha.  ⛔ They MUST track their hex.
     - a theme declares EVERY colour token.  An omitted one does not error, it
       inherits the default theme's value.

   None of that is enforceable by reading.  tools/check-contrast.py asserts all
   of it, measures every text-on-surface pairing in every theme against AA, and
   carries a --selftest that proves it can go red first.
     python tools/check-contrast.py --selftest && python tools/check-contrast.py
   ============================================================ */
:root{
  --ink:#000818;              /* the logo's own night -- text, header, press */
  --ink-rgb:0,8,24;           /* ⛔ MUST equal --ink.  check-contrast.py asserts it */
  --paper:#F0F0F0;            /* the silver ramp -- the ground         */
  --paper-rgb:240,240,240;    /* ⛔ MUST equal --paper                 */
  --card:#FFFFFF;
  --line:#D2D2D2;             /* hairline on silver                    */
  --line-ink:rgba(240,240,240,.28);   /* hairline on the ink header    */
  --muted:#5A6270;            /* 5.39:1 on silver                      */
  --muted-ink:#A8B4C6;        /* 9.55:1 on the navy                    */
  /* ⛔ TWO ACCENT TOKENS, NOT ONE, AND THE SPLIT IS WHAT MAKES THIS PALETTE
     POSSIBLE AT ALL.  No blue is symmetric: a blue dark enough to carry white
     is 3.28:1 as text on navy, and a blue light enough to read on navy cannot
     carry white.  Serving both roles from one token is how a theme ships
     invisible text.
     The retired yellow got away with one token because #FFE600 is 14.54:1
     against ink in BOTH directions -- as a chip under ink text AND as text on
     the dark header.  That is the exception, not the rule, which is why the
     split is permanent and why the alternate below must declare both. */
  --accent:#0057E0;           /* the CHIP -- white on it 6.11:1        */
  --accent-ink:#FFFFFF;       /* text ON the chip is white, always     */
  --accent-on-ink:#3399FF;    /* accent AS TEXT, ONLY ever on --ink, 6.81:1 */
  --accent-ink-dim:#DCE8FB;   /* 4.94:1 -- .card .lab on the chip      */
  --accent-ink-soft:#F2F6FE;  /* 5.64:1 -- .card p on the chip         */
  --tint:#E6EEFF;             /* pale blue — a STATE, never a button   */
  --ok:#10693C; --warn:#8A5200; --bad:#9B1C1C;
  --radius:14px;
  --tap:48px;                 /* ⛔ minimum a finger can hit            */
  --wrap:1240px;

  /* ══════ THE HOUSE PHONE SHELL ══════
     Two numbers, each read in more than one place, and that is the whole
     reason they are variables rather than literals.

     ⛔ --m-gutter DRIVES BOTH HERO GAPS.  The brief is that the gap from the
     header down to the inset image equals the gap from the screen edge in to
     it.  Two separate numbers cannot stay equal -- somebody nudges the side
     padding for a longer headline and the top gap stops matching, invisibly.
     It also positions the dock, so the dock lines up with the hero card
     rather than merely looking as though it does.

     ⛔ --dock-h IS READ IN THREE PLACES: the bar is GIVEN this height, the
     body is padded by it, and the sheet clears it.  On a sibling build the
     bar sized itself and the variable was only used for the page padding --
     so it was an ESTIMATE, it was 8px short, and the footer sat under the
     dock with nothing erroring. */
  --m-gutter:16px;
  --dock-h:68px;

  /* BRAND METAL AND BRAND LIGHT -- declared ONCE and NOT theme-flipped.  This
     is the same call check-contrast.py already states for --brand-blue: they
     are non-text brand values with nothing sitting on top of them, so they
     have no pairing to measure and COLOUR_TOKENS deliberately does not demand
     them of every theme.  The logo does not change when the palette does, so
     neither does its metal or its halo.
     MEASURED, AND IT DECIDES WHERE EVERY ONE OF THESE MAY BE USED: this
     silver ramp is 1.04:1 -> 1.39:1 against --paper and 12.18:1 -> 17.65:1
     against --ink.  On the cream sections the brightest AND the darkest parts
     of the metal are both the same value as the page under them -- chrome
     there is not subtle, it is absent.  Metal reads as metal because it is a
     bright thing in a dark room; the ramp's own internal step is only 1.45:1,
     so it is the surround doing the work and not the gradient.  Every use
     below is on ink, and that is a measurement rather than a preference. */
  --metal-1:#E6E6E6;
  --metal-3:#FAFAFA;
  --brand-blue:#006FFF;       /* NON-TEXT ONLY -- 4.14:1 on ink, 4.50 on navy */
  --brand-blue-rgb:0,111,255;
  /* the hairline.  Transparent at BOTH ends on purpose: a rule that runs edge
     to edge reads as a border, and a border is exactly what this replaces. */
  --metal-rule:linear-gradient(90deg,transparent 0%,
      rgba(230,230,230,.18) 14%, var(--metal-1) 36%, var(--metal-3) 50%,
      var(--metal-1) 64%, rgba(230,230,230,.18) 86%, transparent 100%);
}

/* ============================================================
   THE DEFAULT PALETTE -- Connected Spaces silver + blue.
   ⛔ THIS RECORD DOCUMENTS THE TOKENS IN :root ABOVE, NOT THE BLOCK BELOW.
   Blue/silver was the ALTERNATE until 2026-09-20, when Mario made it the
   default.  It is Connected Spaces' OWN brand; the yellow it replaced was the
   house palette this build was forked with.  Not one value moved when the
   block did -- the measurement below is still why each is what it is.
   Every value below was read off their own logo, then moved only as far as
   the arithmetic forced.  The measured source colours are:
     signature blue #006FFF   wordmark gradient #0050F0 -> #0096FA
     silver ramp    #D2D2D2 #E6E6E6 #F0F0F0 #FAFAFA
     ground         #000410 #000818 #000C1C

   ⛔ THE SIGNATURE BLUE CANNOT BE THE ACCENT, AND THAT IS ARITHMETIC RATHER
   THAN TASTE.  The accent is a CHIP carrying text in five places, and #006FFF
   measures:
       white text ON it        4.45:1   FAIL
       ink text ON it          4.14:1   FAIL
       as text on the silver   3.91:1   FAIL
       as text on this navy    4.50:1   clears by 0.004, which is not a margin
   So it survives as --brand-blue, used ONLY where nothing sits on top of it,
   and the chip is the same hue darkened until white clears: 216.7 deg against
   the brand's 213.9 deg.  Same move ancer-pools made (#1A75BC -> #3C9AE4) and
   ironfield (#F00000 -> #FF3333): a brand colour that fails as text needs a
   lifted sibling, not a redefinition of what "accessible" means.

   ⛔ --accent-ink IS WHITE HERE AND INK IN THE DEFAULT THEME.  That inversion
   is the whole reason the accent had to become two tokens.  All eleven card
   icons follow it for free -- .card .ico svg is fill:currentColor resolving to
   .card .ico's color:var(--accent-ink) -- so they flip to white on blue at
   6.11:1 with no edit at all.

   ⛔ THE HEADER AND FOOTER STAY DARK IN BOTH THEMES, AND THAT IS A DEPENDENCY
   OF A BINARY ASSET, NOT A COLOUR PREFERENCE.  The logo is an additive glow on
   black: on a light surface its chrome vanishes and the glow reads as a
   printing fault.  It is not a recolourable mark and no dark-on-light variant
   can be derived by inverting it.  Lightening --ink breaks a picture.

   Measured, all ten pairings, 0 failures:
     ink/paper 17.58   ink/card 20.04   muted/paper 5.39   muted/card 6.15
     paper/ink 17.58   muted-ink/ink 9.55
     accent-ink/accent 6.11   dim/accent 4.94   soft/accent 5.64
     accent-on-ink/ink 6.81
   ⚠ Blue has materially less headroom than yellow everywhere on the light-on-
   dark side (4.94-6.81 against yellow's 6.57-16.90), and this file sets
   -webkit-font-smoothing:antialiased, which thins light glyphs on dark.  If it
   reads weak on a real screen the lever is darkening --accent toward #0047B8 --
   NOT brightening the dim token, which has a hard ceiling of 6.11 at pure white.
   ============================================================ */

/* ⛔ THE RETIRED HOUSE PALETTE, KEPT ON PURPOSE AND NOT DELETED.
   check-contrast.py section A asserts at least two theme blocks -- "a guard
   that still passes once its subject has been deleted is not a guard" -- so
   deleting this turns the gate red.  It also costs nothing: an alternate
   INHERITS every token it does not set, so this block is only the overrides.
   ⛔ --accent-ink INVERTS between the two themes.  Both must declare it, or
   whichever forgets renders 3.28:1 in five places with nothing going red.
   That is section C, and it is the highest-value assertion in the guard. */
:root[data-theme="yellow"]{
  --ink:#141414;              /* text, the dark header, the press      */
  --ink-rgb:20,20,20;         /* ⛔ MUST equal --ink                    */
  --paper:#F7F5EF;            /* the cream ground                      */
  --paper-rgb:247,245,239;    /* ⛔ MUST equal --paper                  */
  --card:#FFFFFF;
  --line:#DEDCD6;             /* hairline on cream                     */
  --line-ink:rgba(247,245,239,.28);
  --muted:#606060;            /* 5.77:1 on cream                       */
  --muted-ink:#A8B1BF;        /* 8.51:1 on ink (was recorded 8.65 -- remeasured) */
  /* ⛔ BRIGHTER ON MARIO'S CALL, AND THE CHANGE THAT DOES THE WORK IS
     SATURATION, NOT LUMINANCE.  #FFE34D carries a blue channel of 77, which
     is what made it read mustard.  Dropping it to 0 is the visible step.
     Measured both directions, because yellow is ALSO text here (.press and
     .story .stat b, both on ink):
       ink on #FFE34D 14.34:1  ->  ink on #FFE600 14.54:1
       #FFE34D on ink 14.34:1  ->  #FFE600 on ink 14.54:1
     Both improved, so nothing had to be traded.  One step further is
     #FFF200 (15.75:1) if he wants more; past that it goes green. */
  --accent:#FFE600;           /* the press and the money               */
  --accent-ink:#141414;       /* ⛔ INVERTED against the default theme  */
  --accent-on-ink:#FFE600;    /* accent AS TEXT, and ONLY ever on --ink */
  --accent-ink-dim:#564F0E;   /* was accent-ink @ opacity .72 -- .card .lab */
  --accent-ink-soft:#3E3A10;  /* was accent-ink @ opacity .82 -- .card p  */
  --tint:#FFF5CB;             /* pale yellow — a STATE, never a button */
}

/* ⛔ SCALE EXTENSION, RECORDED DELIBERATELY.
   The POS sanctioned scale is 13/14/16/18/23/30/40/42px and its own
   guard enforces membership. That scale was written for a COUNTER
   SCREEN read at arm's length. This is a marketing homepage whose
   reference sets its hero near 90px. Rather than silently violate the
   bar, the scale is EXTENDED for display type only: 56 / 72 / 90.
   Body copy stays inside the sanctioned set. */
:root{ --d1:90px; --d3:56px; --d4:42px; --d5:30px; }

*{box-sizing:border-box}
html,body{margin:0}
body{
  background:var(--paper); color:var(--ink);
  font:16px/1.55 "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing:antialiased;
}
/* FLAT — bar.md mechanism 5. Nothing on this page casts a shadow. */
img,video{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
.wrap{max-width:var(--wrap);margin:0 auto;padding:0 24px}
h1,h2,h3{margin:0;font-weight:800;letter-spacing:-.02em;line-height:1.05}
p{margin:0}

/* ---------- header: ink, like the POS header ---------- */
.hdr{position:sticky;top:0;z-index:50;background:var(--ink);color:var(--paper);
     border-bottom:1px solid var(--line-ink)}
/* THE HAIRLINE IS ADDITIVE AND THE OLD BORDER STAYS UNDER IT ON PURPOSE.
   --metal-rule is a gradient held in a custom property; an engine that cannot
   resolve it drops the background and leaves the 1px --line-ink border exactly
   as it shipped, so the header degrades to what it was rather than to nothing.
   bottom:-1px lands the gradient ON that border, so the box model does not move
   by a pixel -- .hdr .wrap's 76px and the sticky offset are untouched.
   The header is the only ink surface on screen at ALL times, which is why the
   cheapest chrome on the site belongs here. */
.hdr::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:1px;
            background:var(--metal-rule);pointer-events:none}
.hdr .wrap{display:flex;align-items:center;gap:28px;height:76px}
/* ⛔ THE HEADER IS TYPE, NOT THE LOGO IMAGE, AND THAT IS A MEASUREMENT.  The
   lockup is 11.3:1; at the 36px this row allows it needs ~226px against roughly
   160px of free space at 390 once the quote pill and the burger are placed --
   it would overflow the phone header, which is the failure this file already
   documents for the nav.  At 40px tall the full logo is illegible anyway.
   The wordmark's own proportion is width/cap-height 9.81 against Inter 800's
   ~11.5, so the tracking is pulled in to -.05em to sit closer to the artwork. */
.brand{font-size:23px;font-weight:800;letter-spacing:-.05em;margin-right:auto}
/* ⛔ nowrap ON THE NAME ONLY.  Putting it on .brand pinned the 59-character
   sub-line to one line too, which forced the header 126px past a 390px viewport
   and 155px past a 900px one -- the whole document scrolled sideways.  The NAME
   must never break mid-word; the sub-line may wrap. */
.brand .b1,.brand .b2{white-space:nowrap}
/* ⛔ min-width:0 SO THE BRAND CAN ACTUALLY SHRINK.  A flex item defaults to
   min-width:auto, which refuses to go below its min-content -- so at 320 the row
   overflowed instead of wrapping.  Measured: the pre-existing header fitted 320
   with EXACTLY zero margin (24+108+12+105+12+35+24 = 320), so a brand name one
   character longer would have done this whatever it said.  With this the two
   words break onto two lines, which is fine -- what must never happen is a break
   INSIDE a word, and that is what the nowrap above prevents. */
.brand{min-width:0}
.brand .b2{margin-left:.28em}
/* ⛔ THE TWO-TONE SPLIT IS BLUE ONLY, DELIBERATELY -- AND BLUE IS NOW DEFAULT.
   A blue SPACES on the yellow alternate would be the only blue on that page:
   it reads as a bug, and it puts the brand's own colour into the palette being
   compared against.  So the rule is UNCONDITIONAL and the alternate stands it
   down, rather than the alternate switching it on.
   ⛔ check-contrast.py section E pins color:var(--accent-on-ink) at exactly 5
   uses.  This is one of them; color:inherit below adds none. */
.brand .b2{color:var(--accent-on-ink)}
:root[data-theme="yellow"] .brand .b2{color:inherit}
.brand small{display:block;font-size:13px;font-weight:400;color:var(--muted-ink);letter-spacing:0}
/* ⛔ THE SUB-LINE STANDS DOWN IN ONE NARROW BAND, AND ONLY THERE.  Between 861
   and 1023 the row carries the brand, the full nav, the phone, the theme control
   AND the quote pill -- it was already tuned to the edge before the theme control
   existed, and the sub-line pushed it 33px past the viewport.  Below 861 the nav
   collapses to the dock and the room comes back; above 1023 there was never a
   problem.  Measured, not guessed: 0 document overflow at every width from 320 to
   1920 afterwards. */
/* ⛔ AND IT STANDS DOWN BELOW 390 TOO, for the opposite reason: there it does not
   overflow, it WRAPS -- to 3 lines at 360 and 4 at 320, taking the header from its
   designed 69px to 90px.  390 is the width the phone shell was measured at and the
   sub-line fits in 2 lines there, holding 69px exactly.  Narrower than that the
   name alone carries it; the tagline is still in the hero and the footer. */
@media (max-width:389px){ .brand small{display:none} }
@media (min-width:861px) and (max-width:1023px){
  .brand small{display:none}
  /* ⛔ AND THE ROW ITSELF HAS TO GIVE, because hiding the sub-line was not
     enough: the brand name alone is 228px and the row still ran 33px past the
     viewport.  This band is the one place the full nav, the phone, the theme
     control and the quote pill are ALL present at once -- it was already at its
     limit before the theme control was added to it. */
  .hdr .wrap{gap:18px}
  .nav{gap:16px}
  .pill-o{padding:0 12px}
}
.nav{display:flex;gap:26px;align-items:center}
.nav a{font-size:14px;font-weight:600;color:var(--paper);opacity:.86}
.nav a:hover{opacity:1}
/* ---------- the Solutions drop-down ----------
   ⛔ THE MARKUP SHIPPED WITHOUT THIS BLOCK and the panel rendered as eleven
   links inline across the header.  Pure CSS: :hover for a mouse,
   :focus-within for Tab.  The panel is the ink ground, so it reads as part of
   the header and every text/surface pairing is one the contrast guard already
   measures (--paper on --ink). */
.nav-drop{position:relative;display:flex;align-items:center;height:76px}
.nav-drop>a::after{content:"";display:inline-block;margin-left:6px;vertical-align:2px;
  border:4px solid transparent;border-top-color:currentColor;border-bottom:0}
.nav-drop-panel{position:absolute;top:100%;left:-18px;min-width:260px;
  display:none;flex-direction:column;padding:8px 0;
  background:var(--ink);border:1px solid var(--line-ink);border-top:0;
  border-radius:0 0 var(--radius) var(--radius);box-shadow:0 18px 40px rgba(0,0,0,.35)}
.nav-drop:hover .nav-drop-panel,.nav-drop:focus-within .nav-drop-panel{display:flex}
.nav-drop-panel a{display:block;padding:10px 18px;font-size:14px;white-space:nowrap}
.nav-drop-panel a:hover,.nav-drop-panel a:focus{background:rgba(var(--paper-rgb),.08);opacity:1}
/* the yellow pill = "where you are" / the primary action (POS 'Sell') */
.pill{background:var(--accent);color:var(--accent-ink);font-size:14px;font-weight:700;
      padding:0 20px;height:var(--tap);display:inline-flex;align-items:center;
      border-radius:var(--radius)}
/* the theme control.  ⛔ It is an <a href> FIRST and a handler second: if the
   script fails to mount it still works, costing a reload instead of being
   instant.  ⛔ It is deliberately NOT tinted with --accent -- a control that
   changes colour with the thing it is comparing contaminates the comparison. */
.theme-t{gap:8px}
.theme-t .tv,.sheet [data-theme-toggle] .tv{font-weight:400;color:var(--muted-ink)}
.pill-o{border:1px solid var(--line-ink);color:var(--paper);font-size:14px;font-weight:700;
        padding:0 20px;height:var(--tap);display:inline-flex;align-items:center;border-radius:var(--radius)}
.burger{display:none;background:none;border:1px solid var(--line-ink);color:var(--paper);
        border-radius:10px;width:var(--tap);height:var(--tap);font-size:18px}

/* ---------- hero ---------- */
.hero{position:relative;min-height:88svh;display:flex;align-items:flex-end;overflow:hidden;background:var(--ink)}
.hero video,.hero .poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.hero .scrim{position:absolute;inset:0;
  background:linear-gradient(90deg,rgba(var(--ink-rgb),.88) 0%,rgba(var(--ink-rgb),.62) 42%,rgba(var(--ink-rgb),.15) 78%,rgba(var(--ink-rgb),0) 100%)}
.hero .wrap{position:relative;padding-bottom:92px;padding-top:120px}
.hero h1{font-size:var(--d1);color:var(--paper)}
.hero p.sub{font-size:var(--d3);font-weight:400;color:var(--paper);letter-spacing:-.02em;
            line-height:1.1;margin-top:6px;max-width:16ch}
.hero .cta{margin-top:36px;display:flex;gap:14px;flex-wrap:wrap}
/* the ink press with yellow text — POS 'Charge', bar.md mechanism 2 */
.press{background:var(--ink);color:var(--accent-on-ink);font-size:16px;font-weight:800;
       height:var(--tap);padding:0 26px;display:inline-flex;align-items:center;
       border-radius:var(--radius);border:1px solid var(--line-ink)}
.press-y{background:var(--accent);color:var(--accent-ink);border:0}
.press-o{background:transparent;color:var(--paper);border:1px solid rgba(var(--paper-rgb),.5)}

/* ---------- section furniture ---------- */
section{padding:104px 0}
.eyebrow{font-size:13px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
.stitle{font-size:var(--d4);margin-top:14px;max-width:22ch}
.stitle b{font-weight:800}
.stitle .thin{font-weight:400}

/* ---------- 11 solution cards: a CAROUSEL, which is how Nice runs them ----
   Measured in Nice's homepage.css:
     .solutions-cards__slide{padding:40px;max-width:520px;border-radius:10px}
     .solutions-cards .swiper-button-prev/next{width:40px;height:40px}
     .solutions-cards .swiper-button-prev/next{display:none}  (<=767px)
   ie. a swiper with arrows on desktop and swipe-only on a phone.  The first
   build flattened that into a static 4-up grid -- which is most of why it
   read as a spec sheet rather than a brand.

   COLOUR: the POS palette has ink, cream and one yellow, and no third hue to
   spend.  So the colour is the PHOTOGRAPH -- each card carries the picture
   from that category's own page on niceforyou.com -- and yellow stays what
   it is everywhere else on this page: the accent you point at. */
/* ⛔ NO PARTIAL CARDS.  The first build sized a card at clamp(280px,25vw,420px)
   -- a width with no relationship to the track it sits in -- so whatever was
   left over after the last whole card became a sliver of the next one.  That
   is not a "there is more" affordance, it just reads as unfinished, which is
   exactly how Mario read it.
   The width is DERIVED from the track instead, so N cards and their gaps fill
   it exactly and there is nothing left over to cut.  A percentage flex-basis
   resolves against the flex container's CONTENT box, which is what makes this
   land on the pixel rather than near it:
     .wrap 1240 - 48 padding = 1192, minus the track's own 8 = 1184
     3 cards = (1184 - 40) / 3 = 381.33   and   3*381.33 + 40 = 1184 exactly
   11 cards three at a time is 8 steps, and the last step lands on cards 9-11
   whole -- so there is no partial at the END of the track either, which is
   the half of this that a fixed card width gets wrong even when it fits. */
.carou{position:relative;margin-top:44px;--per:3;--gap:20px}
.track{display:flex;gap:var(--gap);overflow-x:auto;scroll-snap-type:x mandatory;
       scroll-behavior:smooth;padding:4px 4px 10px;
       scrollbar-width:none;-ms-overflow-style:none}
.track::-webkit-scrollbar{display:none}
.card{flex:0 0 calc((100% - (var(--per) - 1) * var(--gap)) / var(--per));
      scroll-snap-align:start;
      background:var(--card);border:1px solid var(--line);border-radius:var(--radius);
      overflow:hidden;display:flex;flex-direction:column;
      transition:border-color .18s ease}
.card:hover,.card:focus-visible{border-color:var(--accent);outline:none}
.card .shot{position:relative;aspect-ratio:16/10;overflow:hidden;background:var(--ink)}
.card .shot img{width:100%;height:100%;object-fit:cover;transition:transform .5s ease}
.card:hover .shot img{transform:scale(1.05)}
/* the icon rides the photograph on a yellow tab -- it keeps Nice's own
   iconography (9 of 11 of which were white-on-white until they were fixed)
   and is the one place the accent shows on a card at rest */
.card .ico{position:absolute;left:18px;bottom:0;width:56px;height:56px;
           background:var(--accent);color:var(--accent-ink);
           border-radius:var(--radius) var(--radius) 0 0;
           display:flex;align-items:center;justify-content:center;padding:11px}
.card .ico svg{width:100%;height:100%;fill:currentColor;display:block}
.card .body{padding:22px 24px 24px;display:flex;flex-direction:column;flex:1;
            transition:background .18s ease}
.card:hover .body,.card:focus-visible .body{background:var(--accent)}
/* ⛔ THE CHIP FLIPS ON :hover AND ON :focus-visible, SO EVERY TEXT RULE MUST DO
   BOTH.  It used to paint the chip on focus and recolour the text on hover only,
   so tabbing to a card gave --muted on the accent: 4.96:1 in yellow (survivable,
   which is why nobody saw it) and 1.01:1 the moment the accent is a dark blue --
   invisible text, reachable by keyboard alone.
   ⛔ AND h3 / .more CARRIED NO color AT ALL.  They inherited --ink, which is
   correct on a white card by accident and is 3.28:1 the moment --accent-ink has to
   invert to white.  A missing declaration does not error; it inherits.
   ⛔ THE opacity IS GONE AND MUST NOT COME BACK.  opacity composites text TOWARD
   the surface, so dimming dark-on-light RAISES contrast and dimming light-on-dark
   DESTROYS it -- the same declaration, opposite outcomes, and no token-pair guard
   can see it because the pair it would measure is not the pixel that gets painted.
   The two dim tokens are the composited pixels, baked:
     ink @.72 over #FFE600 -> #564F0E   (6.57:1, byte-identical to today)
     ink @.82 over #FFE600 -> #3E3A10   (8.62:1, byte-identical to today) */
.card .lab{font-size:13px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
.card:hover .lab,.card:focus-visible .lab{color:var(--accent-ink-dim)}
.card h3{font-size:18px;margin-top:6px}
.card:hover h3,.card:focus-visible h3{color:var(--accent-ink)}
.card p{font-size:14px;color:var(--muted);margin-top:12px;flex:1}
.card:hover p,.card:focus-visible p{color:var(--accent-ink-soft)}
.card .more{font-size:14px;font-weight:700;margin-top:18px}
.card:hover .more,.card:focus-visible .more{color:var(--accent-ink)}
.card .more::after{content:" \2192"}
/* arrows: Nice puts them after the track, right-aligned.  40px there, 48 here
   because --tap is the minimum a finger can hit and that rule is not ours to
   relax. */
.carou-nav{display:flex;gap:14px;justify-content:flex-end;margin-top:28px}
.cbtn{width:var(--tap);height:var(--tap);border-radius:100px;background:transparent;
      border:1px solid var(--line);color:var(--ink);font-size:20px;line-height:1;
      display:flex;align-items:center;justify-content:center;cursor:pointer;
      transition:background .16s ease,border-color .16s ease}
.cbtn:hover{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}

/* ---------- 8 product spotlights ----------
   MEASURED, not chosen.  Nice: max-height 543px, media and text 50% each,
   picture aspect-ratio 1.56132, and their .container is width:100% -- full
   bleed.  The first build put these inside the 1240px wrap with a 64px gap,
   which computes to a 564x301 image where Nice's is ~848x543: 2.9x less
   picture.  That is exactly what Mario saw.
     1776px outer - 80px padding = 1696 content; 1696/2 = 848 wide;
     848 / 1.56132 = 543 tall -- ie. lands precisely on Nice's own cap. */
.prod-wrap{max-width:1776px;margin:0 auto;padding:0 40px}
.spot{display:grid;grid-template-columns:1fr 1fr;gap:0;align-items:stretch;margin-top:104px}
.spot:first-of-type{margin-top:56px}
.spot.flip .media{order:2}
/* ⛔ PRE-EXISTING, NOT FROM THIS ROUND -- old.html overflows by the same 12px,
   so it shipped in the build that is already deployed.
   .spot is align-items:stretch, so the media was stretched to the row height
   that the TEXT column sets, and aspect-ratio then derived its WIDTH from that
   height.  At 900px: text 300.7 tall -> 300.7 * 1.56132 = 469.5 wide, in a
   426px track, and the document scrolls sideways.
   It only looked correct at desktop BY COINCIDENCE: there max-height:543 caps
   the stretch, and 543 * 1.56132 = 848, which is exactly the track width.  A
   layout that lands right at one viewport and blows out at the others is not
   a layout, it is an accident.
   width:100% + align-self:start makes the WIDTH drive and the height follow,
   which gives the identical 848x543 at desktop and fits every other width. */
.spot .media{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;
             width:100%;align-self:start;
             aspect-ratio:1.56132;max-height:543px}
.spot .media img{width:100%;height:100%;object-fit:cover}
/* ⛔ A FOCAL POINT IS A PROPERTY OF THE PHOTOGRAPH, NOT OF THE VIEWPORT, so
   this sits OUTSIDE the media query and applies at every width.  `cover`
   centres what it keeps, which is only ever correct when the subject is
   centred -- and a marketing banner's subject usually is not.
   Measured on the speaker banner (2000x1150): the speaker occupies source
   x 1230-2000, ie. it runs off the right edge of the photograph itself.  A
   centred window ending at x 1575 showed under half of it on a phone, and
   even the desktop window clipped it at x 1899.  Right-aligning the window
   is the only thing that shows the whole product, at BOTH widths. */
.spot .media img.fx-r{object-position:100% 50%}
.spot>div:last-child{display:flex;flex-direction:column;align-items:flex-start;
                     justify-content:center;padding:0 0 0 72px}
.spot.flip>div:last-child{padding:0 72px 0 0}
.spot h3{font-size:var(--d4);max-width:16ch}
.spot .lab{font-size:13px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--muted);
           margin-bottom:12px;display:block}
.spot p{font-size:16px;color:var(--muted);margin-top:18px;max-width:46ch}
.spot .press{margin-top:32px}
.spot .cta{margin-top:32px;display:flex;gap:14px;flex-wrap:wrap}
.spot .cta .press{margin-top:0}

/* ---------- his story band (NOT Nice's corporate story) ---------- */
/* THE LOGO'S OWN LIGHT, NOT THE THEME'S.  The halo is part of the artwork --
   the webp is 38% soft glow -- so it stays --brand-blue in BOTH palettes, the
   same way the logo image itself does.  It is a background with nothing on top
   of it, so --brand-blue's 4.14:1 is not a text figure and no pairing applies.
   This band is the largest flat ink surface on the page and carried nothing at
   all before this. */
.story{background:var(--ink);color:var(--paper);position:relative}
.story::before{content:"";position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(46% 88% at 20% 64%,
    rgba(var(--brand-blue-rgb),.17) 0%,
    rgba(var(--brand-blue-rgb),.05) 45%, transparent 70%)}
.story .wrap{position:relative;display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:center}
.story h2{font-size:var(--d4);color:var(--paper);max-width:18ch}
.story p{font-size:16px;color:var(--muted-ink);margin-top:20px;max-width:52ch}
.story .stat{display:flex;gap:40px;margin-top:36px}
.story .stat b{display:block;font-size:var(--d5);color:var(--accent-on-ink);font-variant-numeric:tabular-nums}
.story .stat span{font-size:13px;color:var(--muted-ink);letter-spacing:.06em;text-transform:uppercase}
.story img{border-radius:var(--radius)}

/* ---------- the brand band ---------- */
/* THE ONE PLACE THE LOGO IS SHOWN THE WAY IT WAS DRAWN.  Before this the
   artwork appeared exactly ONCE on the whole page -- the footer, 260px wide,
   the last thing anybody sees before they leave -- and 38% of that file is
   soft glow made for a near-black ground it was never given.  This band is
   that ground.
   INK ONLY, and that is load-bearing rather than a colour preference: the
   asset has no dark-on-light variant and none can be derived by inverting it,
   and its silver measures 1.04:1 against the cream.  Lighten this band and the
   logo does not look worse, it disappears. */
.brandband{position:relative;background:var(--ink);padding:76px 24px;text-align:center;
           border-top:1px solid var(--line-ink);border-bottom:1px solid var(--line-ink)}
.brandband::before,.brandband::after{content:"";position:absolute;left:0;right:0;
           height:1px;background:var(--metal-rule);pointer-events:none}
.brandband::before{top:-1px}
.brandband::after{bottom:-1px}
.brandband .glow{position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(58% 120% at 50% 46%,
    rgba(var(--brand-blue-rgb),.20) 0%,
    rgba(var(--brand-blue-rgb),.07) 40%, transparent 72%)}
.brandband img{position:relative;display:inline-block;width:min(400px,74vw);height:auto}
.brandband p{position:relative;margin:20px auto 0;font-size:12px;font-weight:700;
             letter-spacing:.16em;text-transform:uppercase;color:var(--muted-ink)}
@media (max-width:760px){
  .brandband{padding:56px 20px}
  .brandband p{letter-spacing:.1em;font-size:11px}
}

/* ---------- closing CTA ---------- */
/* ---------- request a quote (the form Sage can fill) ----------
   Ink text on --card and --paper only; --ok / --bad carry the result line.
   ⛔ font-size 16px on the fields is load-bearing: under 16px iOS Safari
   zooms the page on focus and leaves it zoomed. */
.quote{background:var(--paper)}
.quote .wrap{display:grid;grid-template-columns:1fr 1.15fr;gap:64px;align-items:start}
.quote-intro p{font-size:16px;color:var(--muted);margin-top:20px;max-width:46ch}
.quote-intro a{color:var(--ink);font-weight:700;text-decoration:underline}
.quote-form{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);
            padding:32px;display:grid;gap:18px}
.qf-pair{display:grid;grid-template-columns:1fr 1fr;gap:18px}
.qf-row{display:grid;gap:6px;min-width:0}
.qf-row label{font-size:14px;font-weight:700;color:var(--ink)}
.quote-form input,.quote-form textarea{font:inherit;font-size:16px;color:var(--ink);
  background:var(--card);border:1px solid var(--line);border-radius:10px;
  min-height:var(--tap);padding:10px 14px;width:100%;box-sizing:border-box}
.quote-form textarea{resize:vertical;min-height:110px}
.quote-form input:focus,.quote-form textarea:focus{outline:2px solid var(--accent);outline-offset:1px;border-color:var(--accent)}
.qf-hint{font-size:13px;color:var(--muted);margin:-8px 0 0}
.quote-form .press{justify-self:start;cursor:pointer}
.quote-form .press:disabled{opacity:.6;cursor:progress}
.qf-status{font-size:15px;font-weight:600;margin:0;padding:12px 14px;border-radius:10px;
           border:1px solid var(--line);color:var(--ink);background:var(--paper)}
.qf-status a{color:inherit;text-decoration:underline}
.qf-ok{color:var(--ok);border-color:var(--ok)}
.qf-bad{color:var(--bad);border-color:var(--bad);background:var(--card)}
@media (max-width:860px){
  .quote .wrap{grid-template-columns:1fr;gap:28px}
  .quote-form{padding:20px}
  .qf-pair{grid-template-columns:1fr}
  .quote-form .press{justify-self:stretch;justify-content:center}
}

.cta-band{position:relative;overflow:hidden;background:var(--ink)}
/* ⛔ object-position IS LOAD-BEARING HERE, NOT A NUDGE.  This band is a fixed
   ~460px tall and as wide as the viewport, so cover shows only a shallow
   horizontal window of the source and throws the rest away:
     3200x1792 source -> rendered 1920x1075 -> 615px of it overflows
     centred (the default 50%), the visible window is source y 512..1279
   and her eyes sit ON the top edge with the crown of her head cut off.
   30% lifts the window to roughly source y 307..1074, which puts her whole
   face (about y 400..850) comfortably inside.
   ⛔ Do NOT "fix" this by cropping the source file instead -- the band's
   aspect ratio changes with every viewport width, so a baked-in crop is only
   ever correct at one of them. */
.cta-band img{position:absolute;inset:0;width:100%;height:100%;
              object-fit:cover;object-position:50% 30%}
.cta-band .scrim{position:absolute;inset:0;
  background:linear-gradient(90deg,rgba(var(--ink-rgb),.9) 0%,rgba(var(--ink-rgb),.6) 50%,rgba(var(--ink-rgb),.1) 100%)}
.cta-band .wrap{position:relative;padding:132px 24px}
.cta-band h2{font-size:var(--d3);color:var(--paper);max-width:14ch}
.cta-band .press{margin-top:32px}

/* ---------- footer: ink ---------- */
.ftr{background:var(--ink);color:var(--paper);padding:72px 0 40px}
.ftr .cols{display:grid;grid-template-columns:1.4fr 1fr 1fr 1fr;gap:40px}
.ftr h4{font-size:14px;font-weight:700;margin:0 0 16px}
.ftr a,.ftr p{font-size:14px;color:var(--muted-ink);display:block;margin-bottom:10px}
.ftr a:hover{color:var(--paper)}
.ftr p.sarea{line-height:1.6}
/* ⛔ THE LOGO IS A SIBLING OF .fbrand, NOT ITS CONTENT.  See the markup.
   ⛔ AND IT ONLY EVER SITS ON A DARK GROUND.  It is an additive glow on black:
   on --paper or --card the silver chrome vanishes and the glow reads as a
   printing fault.  It is not a recolourable mark and no dark-on-light variant
   can be derived by inverting it, which makes 'the header and footer stay ink'
   a dependency of a binary asset rather than a colour preference. */
.ftr .flogo{display:block;width:min(260px,72%);height:auto;margin-bottom:14px}
.ftr .fbrand{font-size:23px;font-weight:800;letter-spacing:-.03em;margin-bottom:18px}
.ftr .base{border-top:1px solid var(--line-ink);margin-top:48px;padding-top:24px;
           display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
.ftr .base p{font-size:13px;margin:0}
.disc{font-size:13px;color:var(--muted-ink);max-width:70ch;margin-top:12px}

/* ══════════════════════ THE HOUSE PHONE SHELL ══════════════════════
   Slim header · inset hero · copy in a card under it · app dock at the foot.
   Every client site ships this from commit 1; it is template, not polish.

   🔴 IT IS ALSO A BUG FIX HERE, AND THAT IS THE REAL REASON IT IS URGENT.
   Before this block, at <=860px the page did  .nav,.pill-o{display:none}
   and showed a burger -- and grep found the string "burger" exactly three
   times in this file: the CSS, that display rule, and the button markup.
   THERE WAS NO HANDLER.  So every phone visitor had NO navigation at all:
   Solutions, Products, About and Contact were unreachable except by
   scrolling the whole page, and the page looked completely fine.  That is
   the documented failure -- a nav that fails to mount does not degrade,
   it DELETES itself, and nothing goes red.

   ⛔ .hero-media EXISTS SO THE DESKTOP HERO IS BYTE-FOR-BYTE UNCHANGED.
   The video was position:absolute directly inside .hero.  To inset it on a
   phone it needs a box of its own -- so the wrapper takes the absolute
   positioning on desktop (identical result) and becomes a real, flowing,
   inset card below 860.  Restyling the <video> itself would have meant two
   different desktop layouts to keep in step. */
.hero-media{position:absolute;inset:0}

/* ---------- the dock: hidden until the header's own breakpoint ----------
   ⛔ ONE BREAKPOINT, SHARED WITH THE HEADER (860).  The header already
   hides .nav and .pill-o there.  A second number gives a band of widths
   rendering both navigations -- or neither, which is what shipped. */
.dock{display:none}
.dock a,.dock button{
  -webkit-tap-highlight-color:transparent;
  background:none;border:0;padding:0;text-decoration:none;cursor:pointer;
  display:grid;justify-items:center;gap:4px;
  color:var(--muted-ink);font-size:10.5px;font-weight:700;
  letter-spacing:.05em;text-transform:uppercase;font-family:inherit;
  transition:color .2s ease}
.dock .ic{width:30px;height:30px;display:grid;place-items:center}
/* ⛔ INLINE SVG ON currentColor, NEVER AN EMOJI.  A colour emoji ignores
   `color`, so a glyph inside a tinted control renders in its own palette --
   that shipped once elsewhere as a red telephone in a red button and read
   as a rendering fault.  The contrast guard cannot see it either: it
   measures text against a background, and an emoji is a picture. */
.dock svg{width:21px;height:21px;stroke:currentColor;fill:none;
          stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
.dock .on{color:var(--accent-on-ink)}

/* ---------- the menu sheet: the ONLY phone route to most of this page ---- */
.sheet-scrim{position:fixed;inset:0;background:rgba(var(--ink-rgb),.55);
             opacity:0;pointer-events:none;transition:opacity .22s ease;
             z-index:2147483600}
.sheet-scrim.open{opacity:1;pointer-events:auto}
.sheet{position:fixed;left:0;right:0;bottom:0;z-index:2147483602;
       background:var(--ink);color:var(--paper);
       border-top-left-radius:20px;border-top-right-radius:20px;
       border-top:1px solid var(--line-ink);
       transform:translateY(102%);transition:transform .26s cubic-bezier(.3,.9,.3,1);
       /* ⛔ THIS DOES NOT "CLEAR THE DOCK", AND THE EARLIER COMMENT SAYING SO WAS
          WRONG -- measured 2026-09-16: the sheet is z 2147483602 against the
          dock's 2147483601, so elementFromPoint at the dock's own centre returns
          the SHEET.  The sheet covers the bar completely while it is open.
          What the padding really earns:
            1. env(safe-area-inset-bottom) keeps the last row off an iPhone's
               home indicator, which is the only reason a link at the very
               bottom edge of a full-height sheet is safe at all;
            2. it reserves the dock's own footprint, so the day somebody raises
               the dock above the sheet the links still clear it rather than
               hiding under it.
          ⛔ It is DERIVED from --dock-h, never a re-typed number -- a literal
          here goes stale the moment the bar's height changes, silently. */
       padding:10px 0 calc(var(--dock-h) + var(--m-gutter) + env(safe-area-inset-bottom,0px))}
.sheet.open{transform:translateY(0)}
.sheet .grip{width:40px;height:4px;border-radius:99px;background:var(--line-ink);
             margin:6px auto 10px}
.sheet a{display:flex;align-items:center;justify-content:space-between;
         padding:0 22px;min-height:var(--tap);
         font-size:16px;font-weight:600;color:var(--paper)}
.sheet a+a{border-top:1px solid var(--line-ink)}
.sheet a.cta{color:var(--accent-on-ink)}
/* the Solutions group inside the phone sheet -- a <details>, so it opens with
   no script.  ⛔ With it open the sheet is taller than a small phone, so the
   sheet scrolls rather than pushing its last rows under the screen edge. */
.sheet{max-height:calc(100svh - 24px);overflow-y:auto;overscroll-behavior:contain}
.sheet-drop{border-bottom:1px solid var(--line-ink)}
.sheet-drop summary{display:flex;align-items:center;justify-content:space-between;
  padding:0 22px;min-height:var(--tap);font-size:16px;font-weight:600;
  color:var(--paper);cursor:pointer;list-style:none}
.sheet-drop summary::-webkit-details-marker{display:none}
.sheet-drop summary::after{content:"";border:5px solid transparent;
  border-top-color:currentColor;border-bottom:0;transition:transform .2s}
.sheet-drop[open] summary::after{transform:rotate(180deg)}
.sheet-drop a{padding-left:38px;font-size:15px;color:var(--muted-ink)}
.sheet-drop a:first-of-type{color:var(--paper)}

/* ⛔ PRINT.  The footer logo is light-on-dark art; printed, the page drops the
   ink ground and the logo goes with it -- an invisible rectangle where the brand
   should be.  The wordmark beside it is plain text and survives, so the picture
   is simply removed rather than fought with. */
@media print{ .ftr .flogo{display:none} }

/* ---------- responsive ---------- */
@media (max-width:1400px){
  .spot>div:last-child{padding:0 0 0 48px}
  .spot.flip>div:last-child{padding:0 48px 0 0}
}
@media (max-width:1100px){
  :root{--d1:62px;--d3:38px;--d4:34px;--d5:26px}
  .prod-wrap{padding:0 24px}
}
@media (max-width:860px){
  .nav,.pill-o{display:none}
  .burger{display:inline-flex;align-items:center;justify-content:center}
  /* ⛔ THE STICKY HEADER OVERFLOWED ON EVERY MOBILE SCREEN AND NO NUMBER
     CAUGHT IT.  .hdr .wrap is height:76px, but at 390px a 23px brand wraps
     to two lines, so the block inside measures ~110px and "Authorized Nice
     Dealer" rendered BELOW the black bar -- on the hero photo, on the cream,
     and on the CTA image, on all five screens, because the header follows
     you down the page.  The audit reported header {h:77} and called it
     healthy: a height cannot see its own content spilling out of it.  Only
     a screenshot could, which is why this shipped once already.
     Fixed twice over -- the type shrinks so it should not wrap at all, AND
     the bar sizes to its content, so a longer client name than the demo one
     still cannot overflow it.
     (This comment deliberately does not spell the demo name out: fill-client.py
     ends with  assert page.count("PLACE" "HOLDER") == 0  after the swap, so a
     stray one in a comment would make that tool refuse and blame the page.) */
  .hdr .wrap{height:auto;min-height:68px;gap:12px;padding-top:9px;padding-bottom:9px}
  .brand{font-size:16px;line-height:1.16}
  .brand small{font-size:11px;margin-top:1px}
  .pill{padding:0 15px;font-size:13px;white-space:nowrap}
  /* a finger is --tap (48px).  These measured 22px, and the footer phone 17px. */
  .ftr a{padding:14px 0;margin-bottom:0}
  /* the footer phone carries style="display:inline" in the markup, and an
     inline style beats an ordinary stylesheet rule -- overriding it is the
     one legitimate use of !important.  An inline box also under-reports its
     own height (it measured 39px carrying 11px of padding), so it has to
     become inline-block before the target is really --tap tall. */
  .ftr p a{display:inline-block!important;padding:14px 4px}
  .story .wrap,.ftr .cols{grid-template-columns:1fr;gap:32px}
  /* Nice stacks these on a phone and squares the picture:
     .media-text-short__container{flex-direction:column}
     .media-text-short__media picture{aspect-ratio:1} */
  .spot{grid-template-columns:1fr;gap:0;margin-top:64px}
  .spot.flip .media{order:0}
  /* ⛔ NOT A SQUARE, AND THAT IS A DELIBERATE DEPARTURE FROM NICE.  Their
     phone layout squares the picture (.media-text-short__media picture
     {aspect-ratio:1}) and that is right for THEIR sources, which are composed
     for it.  Ours are wide marketing banners -- 1.56 to 2.38 -- with the
     product hard against one edge, so a square window keeps only the middle
     of each one and cuts the product in half.
     Measured at 390px before this change, as a share of source WIDTH kept:
       speakers 57%   ELAN 45%   E+ sensors 42%   MCX 53%   Smart 535 53%
     The same images on desktop keep 66-100%, which is the whole reason this
     was invisible until somebody opened it on a phone.
     4/3 lifts those five to 77 / 61 / 56 / 70 / 70%. */
  .spot .media{aspect-ratio:4/3;max-height:none}
  /* ⛔ ONE BANNER WANTS THE SQUARE BACK, AND IT IS NOT AN EXCEPTION TO THE
     RULE ABOVE -- IT IS THE SAME RULE READ OFF A DIFFERENT COMPOSITION.  This
     photograph isolates the panel in the middle of a white wall, with a man at
     source x 0-600 and a woman at x 2200-2900.  4/3 opens the window to
     x 589-2411, which reaches 211px past where her sleeve starts: the phone
     showed the product SMALLER than before and two disembodied sleeves at the
     frame edges.  1:1 closes it to x 818-2182, clear of both of them by
     construction rather than by luck.
     ⛔ Measured on every one of the eight before shipping the wider box.
     This was the only image the widening made worse, so it keeps what it had.
     Do not "tidy" it into the rule above. */
  .spot .media.m-sq{aspect-ratio:1}
  .spot>div:last-child,.spot.flip>div:last-child{padding:24px 0 0}
  .ftr .cols{gap:28px}
  /* ⛔ THE BAND HAS TO GET SHORTER OR HER FACE CANNOT FIT IN IT, AND NO
     object-position CAN RESCUE IT.  At 390px this box is 390x446 -- TALLER
     than it is wide -- while the source is 3200x1792, so `cover` crops
     HORIZONTALLY and shows a 1568px-wide window of it.  Her face spans source
     x 2128-2752; the centred window ends at x 2384, which lands between her
     eyes, so the phone showed exactly one of them.
     ⛔ Note this also makes the desktop-tuned `50% 30%` inert here: on a
     phone there is no vertical overflow to position, so the 30% does nothing
     and the crop is decided entirely on the axis nobody had considered.
     Her face is 40% of that window's width and the headline plus the call
     button occupy the left ~69% of the band, so NO horizontal position fits
     her whole face AND clears the copy.  The only variable left is the band's
     own height: at ~280px the window widens to ~2500 source px and her face
     lands at 71-96% of the frame -- whole, and clear of the text.
     ⛔ Do not put this back to 132px and "nudge" object-position instead.
     That was measured, and it cannot work. */
  .cta-band .wrap{padding:50px 24px}
  section{padding:72px 0}
  :root{--d1:44px;--d3:30px;--d4:30px;--d5:23px}
  .hero p.sub{max-width:none}

  /* ══ HERO: the picture is INSET, the copy sits in a card UNDER it ══
     ⛔ NOTHING IS WRITTEN OVER THE PHOTOGRAPH ANY MORE, which is why the
     scrim goes.  Text over a moving video is the one thing on a phone whose
     contrast cannot be measured at build time -- the frame under the
     headline changes 30 times a second.
     ⛔ `vh` FIRST, `svh` SECOND.  On iOS `vh` resolves against the LARGE
     viewport, so a 50vh hero is TALLER than half the screen on load and
     then silently resizes as you scroll.  vh is only the fallback for
     engines that do not know svh; svh is the one that must win. */
  .hero{display:block;min-height:0;overflow:visible;background:var(--paper)}
  .hero .hero-media{
    position:relative;inset:auto;
    height:50vh;height:50svh;
    margin:var(--m-gutter);
    border:1px solid var(--line);border-radius:18px;overflow:hidden}
  .hero .scrim{display:none}
  /* ⛔ .wrap is max-width + auto margins + 24px padding.  The card has to
     override all THREE or it inherits a centred column and the side gutter
     stops matching the one above it -- which is the single thing
     --m-gutter exists to keep equal. */
  .hero .wrap{
    max-width:none;
    margin:0 var(--m-gutter);
    padding:22px 20px 26px;
    background:var(--card);color:var(--ink);
    border:1px solid var(--line);border-radius:18px}
  .hero h1,.hero p.sub{color:var(--ink)}
  .hero .cta{margin-top:22px}
  .hero .press-o{border-color:var(--line);color:var(--ink)}

  /* ══ THE DOCK ══ */
  .dock{
    display:grid;grid-template-columns:repeat(4,1fr);align-items:center;
    position:fixed;left:var(--m-gutter);right:var(--m-gutter);
    /* the safe area lives in the OFFSET, not in padding: with
       box-sizing:border-box a safe-area padding would eat the bar's own
       height and --dock-h would stop being the truth. */
    bottom:calc(var(--m-gutter) + env(safe-area-inset-bottom,0px));
    height:var(--dock-h);
    /* ⛔ ABOVE THE CONCIERGE (she sits at 2147483600).  Iris is bottom-right
       and her box overlaps this bar; one z-index lower and she swallows two
       of the four tabs.  The site's own navigation outranks a widget
       standing on it. */
    z-index:2147483601;
    background:var(--ink);border:1px solid var(--line-ink);border-radius:20px;
    box-shadow:0 14px 34px rgba(var(--ink-rgb),.34)}

  /* ⛔ THE PAGE MUST GET SHORTER, NOT JUST GAIN A FLOATING BAR.  Without
     this the last ~68px of every page sits permanently under the dock --
     and a screenshot taken at default scroll looks perfect, so nothing
     catches it but scrolling to the very bottom on a real phone. */
  body{padding-bottom:calc(var(--dock-h) + (var(--m-gutter) * 2) + env(safe-area-inset-bottom,0px))}
}
/* how many WHOLE cards fit.  Nothing here is a card WIDTH -- the width is
   derived from --per above, so these can never disagree with the track. */
@media (max-width:1000px){ .carou{--per:2} }
@media (max-width:700px) { .carou{--per:1} }

/* ⛔ THE ARROWS COME BACK ON A PHONE, AND THAT IS A DELIBERATE DEPARTURE FROM
   NICE.  They hide theirs below 767px because their card PEEKS -- the sliver
   of the next card IS the "there is more, swipe me" signal, so the arrows are
   redundant.  Removing the peek (Mario's call) removes that signal, and an
   11-card carousel showing one whole card with no affordance reads as a
   single static image.  Dropping both would be copying half a decision. */
@media (max-width:767px){ .carou-nav{justify-content:center;margin-top:20px} }
@media (prefers-reduced-motion:reduce){ *{transition:none!important;animation:none!important} }

/* ═══════════ SAGE STATUS — signal bars ═══════════
   Ported from usawoundtx's SiteConcierge.tsx (SageStatus), same platform,
   same "sage" sphere avatar, so the same box geometry applies: her sphere
   spans 29.7%-68% of #live-agent's height, centred at 49.6% across, and the
   pill sits just above it. Rendered INSIDE #live-agent by the vanilla-JS
   port of that component, at the bottom of the concierge <script> block in
   index.html (lifted into every generated page by tools/build-pages.py).
   ⛔ Bar/bubble colours are fixed literals, not theme tokens (aside from the
   idle bar, which reads --accent-on-ink so it still matches both themes) --
   the pill floats over live video of any brightness, so it stays dark
   regardless of --ink/--paper. */
.sage-bars{position:absolute;left:49.6%;bottom:calc(70.3% + 5px);transform:translateX(-50%);
  display:flex;align-items:flex-end;gap:3px;padding:5px 8px;border-radius:999px;
  background:rgba(10,10,10,.78);box-shadow:0 4px 12px rgba(10,10,10,.28);pointer-events:none}
.sage-bars i{display:block;width:4px;border-radius:1px;background:var(--accent-on-ink);opacity:.9;
  transform-origin:bottom}
.sage-bars i:nth-child(1){height:5px}
.sage-bars i:nth-child(2){height:8px}
.sage-bars i:nth-child(3){height:11px}
.sage-bars i:nth-child(4){height:14px}
.sage-bars.is-listening i{background:#6fe3a0;opacity:1}
.sage-bars.is-talking i{background:#fff;opacity:1;animation:sage-level .9s ease-in-out infinite}
.sage-bars.is-talking i:nth-child(2){animation-delay:-.3s}
.sage-bars.is-talking i:nth-child(3){animation-delay:-.6s}
.sage-bars.is-talking i:nth-child(4){animation-delay:-.15s}
.sage-bars.is-connecting i{opacity:.25;animation:sage-seek 1.2s steps(1) infinite}
.sage-bars.is-connecting i:nth-child(2){animation-delay:.15s}
.sage-bars.is-connecting i:nth-child(3){animation-delay:.3s}
.sage-bars.is-connecting i:nth-child(4){animation-delay:.45s}
@keyframes sage-level{0%,100%{transform:scaleY(.45)}50%{transform:scaleY(1)}}
@keyframes sage-seek{0%{opacity:.25}15%,80%{opacity:1}}

.sage-say{position:absolute;right:18%;bottom:calc(70.3% + 38px);pointer-events:none}
.sage-bubble{position:relative;display:block;width:max-content;max-width:min(240px, calc(100vw - 40px));
  padding:9px 13px;border-radius:14px;background:var(--ink);color:#fff;
  font-size:14px;font-weight:600;line-height:1.35;white-space:normal;text-align:left;
  box-shadow:0 10px 26px rgba(10,10,10,.3);animation:sage-pop .35s ease-out both}
.sage-bubble::after{content:"";position:absolute;right:18px;bottom:-6px;width:12px;height:12px;
  background:var(--ink);transform:rotate(45deg);border-radius:2px}
.sage-bubble.is-tap{pointer-events:auto;cursor:pointer}
@keyframes sage-pop{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

@media (max-width:767px){
  /* her phone box sits beside the dock, so the line floats above the box */
  .sage-say{right:6px;bottom:calc(100% + 10px)}
  .sage-bubble{max-width:min(230px, calc(100vw - 32px))}
  .sage-bubble::after{right:42px}
}
@media (prefers-reduced-motion:reduce){
  .sage-bars i,.sage-bubble{animation:none!important}
  .sage-bars.is-connecting i{opacity:1}
}

/* ═══════════ SERVICE PAGES — /solutions/<slug>/ ═══════════
   Built by tools/build-pages.py from data/solutions.json.  Nothing here
   applies to the homepage: every selector is scoped under a body carrying
   data-solution, or under a .sol- class the homepage does not use.

   ⛔ ONE ACCENT PER PAGE, DECLARED HERE AND NOWHERE ELSE, the way Nice
   colour-codes its own solution families.  The house palette (ink, paper,
   the blue) stays in charge of the header, nav, footer and body on every
   page; --page only ever colours a SURFACE: the hero chip, the top rule on
   each feature card, the list markers and the question band.
   ⛔ --page IS NEVER TEXT.  Measured 2026-09-20: every one of the eleven
   reads 2.3-4.4:1 against --ink, so as text on a dark ground most of them
   fail.  Text ON a --page surface is --page-ink, which is white, and white
   clears 4.5:1 on all eleven.  tools/check-contrast.py section F enforces
   both, and fails if a slug here drifts from data/solutions.json.
   ⚠ av-solutions is the tightest at 4.52:1 -- do not lighten it.
   sun-shade #007FA3 is Nice's own colour for that family, read out of their
   production stylesheet; the rest follow the table Mario approved. */
body[data-solution="control-systems"]{--page:#02695D;--page-ink:#FFFFFF}
body[data-solution="security"]{--page:#0057E0;--page-ink:#FFFFFF}
body[data-solution="access-control"]{--page:#004A93;--page-ink:#FFFFFF}
body[data-solution="door-openers"]{--page:#007B5F;--page-ink:#FFFFFF}
body[data-solution="gate-operators"]{--page:#49812C;--page-ink:#FFFFFF}
body[data-solution="hostile-vehicle-mitigation"]{--page:#A6093D;--page-ink:#FFFFFF}
body[data-solution="audio-solutions"]{--page:#6B3FA0;--page-ink:#FFFFFF}
body[data-solution="av-solutions"]{--page:#087CBF;--page-ink:#FFFFFF}
body[data-solution="power-management"]{--page:#CF4520;--page-ink:#FFFFFF}
body[data-solution="sun-shade-motors-solutions"]{--page:#007FA3;--page-ink:#FFFFFF}
body[data-solution="builder-services"]{--page:#687783;--page-ink:#FFFFFF}

.sol-crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:8px;
  font-size:14px;font-weight:600;color:var(--paper);margin-bottom:18px}
.sol-crumbs a{color:inherit;text-decoration:underline;text-underline-offset:3px}
.sol-crumbs span[aria-hidden]{color:inherit}
.sol-chip{display:inline-block;background:var(--page);color:var(--page-ink);
  font-size:13px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  padding:7px 12px;border-radius:999px;margin-bottom:16px}

.sol-intro{padding-bottom:40px}
.sol-intro h2{font-size:var(--d4);max-width:22ch;margin-top:12px;text-wrap:balance}
.sol-intro .lede{font-size:20px;line-height:1.6;max-width:62ch;margin-top:22px;color:var(--ink)}
.sol-caveat{margin-top:22px;max-width:62ch;padding:14px 18px;border-left:4px solid var(--page);
  background:var(--card);color:var(--ink);font-size:16px;line-height:1.55}

.sol-h{font-size:var(--d4);margin-bottom:32px;text-wrap:balance}
.sol-features{padding-top:40px}
.sol-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}
/* four cards break 3+1 in a three-up grid; the generator tags the count so four sit 2+2 */
.sol-grid.n4{grid-template-columns:repeat(2,minmax(0,1fr))}
/* the homepage sub is one short line at display size; here it is a sentence */
.sol-hero p.sub{font-size:clamp(20px,2vw,26px);line-height:1.45;letter-spacing:0;max-width:40ch}
.sol-item{background:var(--card);border:1px solid var(--line);border-top:4px solid var(--page);
  border-radius:14px;padding:26px 24px}
.sol-item h3{font-size:20px;line-height:1.3;color:var(--ink)}
.sol-item p{margin-top:12px;font-size:16px;line-height:1.6;color:var(--muted)}

.sol-decides{background:var(--card);border-block:1px solid var(--line)}
.sol-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.3fr);gap:48px;align-items:start}
.sol-split h2{font-size:var(--d4);margin-top:12px;text-wrap:balance}
.sol-decides ul{list-style:none;display:grid;gap:14px;padding:0;margin:0}
.sol-decides li{position:relative;padding-left:28px;font-size:18px;line-height:1.5;color:var(--ink)}
.sol-decides li::before{content:"";position:absolute;left:0;top:.55em;width:12px;height:12px;
  border-radius:3px;background:var(--page)}

.sol-faq details{border-bottom:1px solid var(--line);max-width:860px}
.sol-faq details:first-of-type{border-top:1px solid var(--line)}
.sol-faq summary{cursor:pointer;list-style:none;padding:20px 40px 20px 0;position:relative;
  font-size:19px;font-weight:700;color:var(--ink)}
.sol-faq summary::-webkit-details-marker{display:none}
.sol-faq summary::after{content:"+";position:absolute;right:6px;top:16px;font-size:26px;
  font-weight:400;color:var(--ink)}
.sol-faq details[open] summary::after{content:"\2212"}
.sol-faq details p{padding:0 0 22px;max-width:70ch;font-size:17px;line-height:1.6;color:var(--muted)}
.sol-faq summary:focus-visible{outline:3px solid var(--accent);outline-offset:3px}

.sol-ask{background:var(--page);color:var(--page-ink)}
.sol-ask h2{font-size:var(--d3);color:var(--page-ink);max-width:18ch;text-wrap:balance}
.sol-ask p{margin-top:16px;font-size:19px;line-height:1.55;color:var(--page-ink);max-width:52ch}
.sol-ask .cta{margin-top:30px;display:flex;gap:14px;flex-wrap:wrap}
.sol-ask .press-w{background:transparent;color:var(--page-ink);border:1px solid var(--page-ink)}

.sol-links{display:flex;flex-wrap:wrap;gap:10px}
.sol-links a{display:inline-block;padding:11px 16px;border:1px solid var(--line);border-radius:999px;
  background:var(--card);color:var(--ink);font-size:15px;font-weight:600}
.sol-links a:hover,.sol-links a:focus-visible{border-color:var(--ink)}

@media (max-width:860px){
  .sol-crumbs{color:var(--muted)}
  .sol-split{grid-template-columns:1fr;gap:24px}
  .sol-grid,.sol-grid.n4{grid-template-columns:1fr}
  .sol-intro .lede{font-size:18px}
}

/* ═══════════ SERVICE PAGES — Nice solution-page layout ═══════════
   Proportions measured off Nice's solutions-single-page.css (2026-09-21):
   hero = full viewport less the header, min 600px; boxed text-on-image
   blocks 700px tall, 10px radius, 40px side inset, text column ~39% wide;
   product cards 600x370 images in a light band, 40px gap, 100px padding.
   Colours are the house palette, not Nice's blue.                       */
.np-hero{position:relative;height:calc(100svh - 60px);min-height:600px;background:var(--ink);overflow:hidden}
.np-hero-media,.np-hero-media video,.np-hero-media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.np-hero-media::after{content:"";position:absolute;inset:0;background:rgba(0,0,0,.42)}
.np-hero-inner{position:relative;height:100%;display:flex;flex-direction:column;justify-content:center;padding-top:72px;color:#fff}
.np-hero-label{font-size:15px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;margin-bottom:20px}
.np-hero h1{font-size:var(--d1);max-width:16ch;color:#fff}
.np-hero-sub{font-size:22px;line-height:1.4;margin-top:20px;max-width:40ch}
.np-crumbs{border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:var(--card)}
.np-crumbs ul{display:flex;flex-wrap:wrap;list-style:none;margin:0 auto;padding-top:20px;padding-bottom:20px}
.np-crumbs li{font-size:13px;letter-spacing:.03em;color:var(--ink)}
.np-crumbs li:not(:last-child)::after{content:"›";margin:0 16px;color:var(--muted)}
.np-crumbs a{color:var(--ink);text-decoration:none}
.np-text{margin:100px 0}
.np-narrow{max-width:900px}
.np-text h2{font-size:clamp(33px,3.2vw,46px);line-height:1.1;color:var(--ink);border-left:4px solid var(--accent);padding-left:18px}
.np-text p{font-size:20px;line-height:1.4;color:var(--muted);margin-top:28px;max-width:62ch}
.np-lab{font-size:13px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;margin-bottom:10px}
.np-bg{position:relative;margin:100px 40px;min-height:700px;border-radius:10px;overflow:hidden;background:var(--ink)}
.np-bg-media{position:absolute;inset:0}
.np-bg-media img{width:100%;height:100%;object-fit:cover;display:block}
.np-bg::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.62) 0%,rgba(0,0,0,.35) 55%,rgba(0,0,0,.15) 100%);z-index:1}
.np-bg.reverse::before{background:linear-gradient(270deg,rgba(0,0,0,.62) 0%,rgba(0,0,0,.35) 55%,rgba(0,0,0,.15) 100%)}
.np-bg-text{position:absolute;top:50%;transform:translateY(-50%);left:6.94%;width:39.1%;z-index:2;color:#fff}
.np-bg.reverse .np-bg-text{left:auto;right:10.9%}
.np-bg-text h3{font-size:clamp(33px,3.2vw,46px);line-height:1.1;color:#fff}
.np-bg-text p:not(.np-lab){font-size:20px;line-height:1.4;margin:40px 0;color:#fff;max-width:46ch}
.np-cards{margin:100px 0}
.np-cards + .np-cards{margin-top:-100px}
.np-cards + .np-cards .np-cards-grid{padding-top:0}
.np-cards-grid{margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(330px,1fr));gap:40px;padding:100px 40px;background:var(--paper)}
.np-card{display:flex;flex-direction:column;background:var(--card);border-radius:10px;overflow:hidden;border:1px solid var(--line)}
.np-card.reverse{flex-direction:column-reverse}
.np-card-body{display:flex;flex-direction:column;flex-grow:1;padding:40px 40px 25px}
.np-card.reverse .np-card-body{padding:25px 40px 40px}
.np-card h3{font-size:clamp(28px,2.1vw,30px);line-height:1.1;color:var(--ink);margin-bottom:20px}
.np-card p{flex-grow:1;color:var(--muted);font-size:20px;line-height:1.4}
.np-card .press{align-self:flex-start;margin-top:40px}
.np-card-img img{display:block;width:100%;height:auto;aspect-ratio:1/1;object-fit:cover}
.np-contact{position:relative;margin-top:100px;min-height:600px;background:var(--ink);overflow:hidden}
.np-contact-media{position:absolute;inset:0}
.np-contact-media img{width:100%;height:100%;object-fit:cover}
.np-contact-media::after{content:"";position:absolute;inset:0;background:rgba(0,0,0,.45)}
.np-contact-text{position:relative;min-height:600px;display:flex;flex-direction:column;justify-content:center;color:#fff}
.np-contact-text h2{font-size:var(--d3);color:#fff;max-width:14ch}
.np-contact-text .cta{margin-top:40px;display:flex;gap:14px;flex-wrap:wrap}
@media (max-width:1023px){
  .np-hero{height:610px;min-height:0}
  .np-text{margin:40px 0}
  .np-bg{margin:40px var(--m-gutter);min-height:0;display:flex;flex-direction:column}
  .np-bg::before{display:none}
  .np-bg-media{position:relative;aspect-ratio:1}
  .np-bg-text,.np-bg.reverse .np-bg-text{position:relative;top:auto;transform:none;left:auto;right:auto;width:auto;padding:32px 24px}
  .np-bg-text p:not(.np-lab){margin:20px 0 28px}
  .np-cards{margin:40px 0}
  .np-cards + .np-cards{margin-top:-40px}
  .np-cards-grid{grid-template-columns:1fr;gap:20px;padding:40px var(--m-gutter)}
  .np-card-body{padding:20px 20px 5px}.np-card.reverse .np-card-body{padding:5px 20px 20px}
  .np-contact,.np-contact-text{min-height:460px}
}
.np-bg .press-w{background:#fff;color:var(--ink);border:0}
@media (max-width:1023px){ .np-bg .press-w{background:var(--paper)} }

/* ---- service pages: the four section types Nice's other solution pages use.
   Sizes are Nice's (solutions-single-page.css): split = 50/50, square media,
   10px radius, 100px rhythm; video strip 600px tall, 10px gaps; carousel on
   the accent band, 500px white cards; tabs underline in the accent. ---- */
.np-split{margin:100px 0}
.np-split-in{max-width:1440px;margin:0 auto;padding:0 40px;display:flex;align-items:center}
.np-split.reverse .np-split-in{flex-direction:row-reverse}
.np-split-media{width:50%;flex:none}
.np-split-media img{display:block;width:100%;height:auto;aspect-ratio:1/1;object-fit:cover;border-radius:10px}
.np-split-text{width:50%;padding:0 6.94% 0 10.9%}
.np-split.reverse .np-split-text{padding:0 10.9% 0 6.94%}
.np-split-text h3{font-size:clamp(33px,3.2vw,46px);line-height:1.1;color:var(--ink)}
.np-split-text p:not(.np-lab){font-size:20px;line-height:1.4;color:var(--muted);margin:40px 0 0}
.np-split-text .np-lab{color:var(--muted)}
.np-split-text .press{margin-top:40px}
.np-vs{margin:100px 0}
.np-vs-row{list-style:none;display:flex;gap:10px;height:600px;padding-top:0;padding-bottom:0}
.np-vs-item{position:relative;flex:1 1 0;min-width:0;border-radius:10px;overflow:hidden;cursor:pointer;
  transition:flex-grow .3s cubic-bezier(.55,.085,.68,.53);background:var(--ink)}
.np-vs-item img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.np-vs-item::before{content:"";position:absolute;inset:0;z-index:1;background:rgba(0,0,0,.6);transition:background .3s}
.np-vs-text{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:40px;color:#fff;opacity:0;transition:opacity .2s}
.np-vs-text h3{font-size:clamp(28px,2.1vw,30px);line-height:1.1;color:#fff}
.np-vs-text p{font-size:20px;line-height:1.4;margin-top:20px;color:#fff;max-width:40ch}
.np-vs-item.on,.np-vs-row:hover .np-vs-item:hover,.np-vs-item:focus-within,.np-vs-item:focus{flex-grow:4}
.np-vs-row:hover .np-vs-item.on:not(:hover){flex-grow:1}
.np-vs-item.on::before,.np-vs-item:hover::before,.np-vs-item:focus::before{background:linear-gradient(0deg,rgba(0,0,0,.65) 0%,rgba(0,0,0,.1) 60%)}
.np-vs-row:hover .np-vs-item.on:not(:hover)::before{background:rgba(0,0,0,.6)}
.np-vs-item.on .np-vs-text,.np-vs-item:hover .np-vs-text,.np-vs-item:focus .np-vs-text{opacity:1;transition-delay:.25s}
.np-vs-row:hover .np-vs-item.on:not(:hover) .np-vs-text{opacity:0;transition-delay:0s}
.np-car{margin:100px 0;background:var(--accent);color:var(--accent-ink)}
.np-car-in{max-width:1440px;margin:0 auto;padding:100px 0 100px 40px;display:flex;align-items:center;gap:40px}
.np-car-title{flex:0 0 440px;padding-left:6.94%;font-size:clamp(33px,3.2vw,46px);line-height:1.1;color:var(--accent-ink)}
.np-car-row{list-style:none;display:flex;gap:20px;overflow-x:auto;scroll-snap-type:x mandatory;padding:0 40px 10px 0;min-width:0}
.np-car-item{flex:0 0 min(500px,80vw);scroll-snap-align:start;background:var(--card);color:var(--ink);border-radius:10px;padding:40px}
.np-car-item h3{font-size:clamp(24px,1.8vw,28px);line-height:1.15;color:var(--ink);margin-bottom:20px}
.np-car-item p{font-size:20px;line-height:1.4;color:var(--muted)}
.np-tabs{margin-top:100px;margin-bottom:100px;max-width:1200px}
.np-tabs h2{font-size:clamp(33px,3.2vw,46px);line-height:1.1;color:var(--ink);text-align:center}
.np-tabs input{position:absolute;opacity:0;pointer-events:none}
.np-tab-bar{display:flex;justify-content:center;flex-wrap:wrap;border-bottom:1px solid var(--line);margin:40px 0}
.np-tab-bar label{position:relative;min-width:150px;padding:20px;text-align:center;font-size:16px;color:var(--muted);cursor:pointer}
.np-tab-bar label::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:3px;background:var(--accent);transform:scaleX(0);transition:transform .4s}
.np-tab-panel{display:none;align-items:center;gap:40px}
.np-tab-panel img{width:50%;flex:none;aspect-ratio:1/1;object-fit:cover;border-radius:10px}
.np-tab-text{flex:1}
.np-tab-text h3{font-size:clamp(28px,2.1vw,30px);line-height:1.1;color:var(--ink)}
.np-tab-text p{font-size:20px;line-height:1.4;color:var(--muted);margin-top:20px}
.np-tabs input:nth-of-type(1):checked~.np-tab-bar label:nth-child(1){color:var(--ink);font-weight:700}
.np-tabs input:nth-of-type(1):checked~.np-tab-bar label:nth-child(1)::after{transform:scaleX(1)}
.np-tabs input:nth-of-type(1):checked~.np-tab-panels .np-tab-panel:nth-child(1){display:flex}
.np-tabs input:nth-of-type(1):focus-visible~.np-tab-bar label:nth-child(1){outline:2px solid var(--accent);outline-offset:-4px}
.np-tabs input:nth-of-type(2):checked~.np-tab-bar label:nth-child(2){color:var(--ink);font-weight:700}
.np-tabs input:nth-of-type(2):checked~.np-tab-bar label:nth-child(2)::after{transform:scaleX(1)}
.np-tabs input:nth-of-type(2):checked~.np-tab-panels .np-tab-panel:nth-child(2){display:flex}
.np-tabs input:nth-of-type(2):focus-visible~.np-tab-bar label:nth-child(2){outline:2px solid var(--accent);outline-offset:-4px}
.np-tabs input:nth-of-type(3):checked~.np-tab-bar label:nth-child(3){color:var(--ink);font-weight:700}
.np-tabs input:nth-of-type(3):checked~.np-tab-bar label:nth-child(3)::after{transform:scaleX(1)}
.np-tabs input:nth-of-type(3):checked~.np-tab-panels .np-tab-panel:nth-child(3){display:flex}
.np-tabs input:nth-of-type(3):focus-visible~.np-tab-bar label:nth-child(3){outline:2px solid var(--accent);outline-offset:-4px}
.np-tabs input:nth-of-type(4):checked~.np-tab-bar label:nth-child(4){color:var(--ink);font-weight:700}
.np-tabs input:nth-of-type(4):checked~.np-tab-bar label:nth-child(4)::after{transform:scaleX(1)}
.np-tabs input:nth-of-type(4):checked~.np-tab-panels .np-tab-panel:nth-child(4){display:flex}
.np-tabs input:nth-of-type(4):focus-visible~.np-tab-bar label:nth-child(4){outline:2px solid var(--accent);outline-offset:-4px}
.np-tabs input:nth-of-type(5):checked~.np-tab-bar label:nth-child(5){color:var(--ink);font-weight:700}
.np-tabs input:nth-of-type(5):checked~.np-tab-bar label:nth-child(5)::after{transform:scaleX(1)}
.np-tabs input:nth-of-type(5):checked~.np-tab-panels .np-tab-panel:nth-child(5){display:flex}
.np-tabs input:nth-of-type(5):focus-visible~.np-tab-bar label:nth-child(5){outline:2px solid var(--accent);outline-offset:-4px}
.np-tabs input:nth-of-type(6):checked~.np-tab-bar label:nth-child(6){color:var(--ink);font-weight:700}
.np-tabs input:nth-of-type(6):checked~.np-tab-bar label:nth-child(6)::after{transform:scaleX(1)}
.np-tabs input:nth-of-type(6):checked~.np-tab-panels .np-tab-panel:nth-child(6){display:flex}
.np-tabs input:nth-of-type(6):focus-visible~.np-tab-bar label:nth-child(6){outline:2px solid var(--accent);outline-offset:-4px}
.np-tabs input:nth-of-type(7):checked~.np-tab-bar label:nth-child(7){color:var(--ink);font-weight:700}
.np-tabs input:nth-of-type(7):checked~.np-tab-bar label:nth-child(7)::after{transform:scaleX(1)}
.np-tabs input:nth-of-type(7):checked~.np-tab-panels .np-tab-panel:nth-child(7){display:flex}
.np-tabs input:nth-of-type(7):focus-visible~.np-tab-bar label:nth-child(7){outline:2px solid var(--accent);outline-offset:-4px}
.np-tabs input:nth-of-type(8):checked~.np-tab-bar label:nth-child(8){color:var(--ink);font-weight:700}
.np-tabs input:nth-of-type(8):checked~.np-tab-bar label:nth-child(8)::after{transform:scaleX(1)}
.np-tabs input:nth-of-type(8):checked~.np-tab-panels .np-tab-panel:nth-child(8){display:flex}
.np-tabs input:nth-of-type(8):focus-visible~.np-tab-bar label:nth-child(8){outline:2px solid var(--accent);outline-offset:-4px}
@media (max-width:1023px){
  .np-split{margin:40px 0}
  .np-split-in,.np-split.reverse .np-split-in{flex-direction:column;padding:0 var(--m-gutter)}
  .np-split-media,.np-split-text{width:100%}
  .np-split-text,.np-split.reverse .np-split-text{padding:24px 0 0}
  .np-split-text p:not(.np-lab){margin-top:20px}
  .np-split-text .press{margin-top:24px}
  .np-vs{margin:40px 0}
  .np-vs-row{flex-direction:column;height:auto;padding:0 var(--m-gutter)}
  .np-vs-item,.np-vs-item.on,.np-vs-row:hover .np-vs-item.on:not(:hover){flex:none;height:420px}
  .np-vs-item::before,.np-vs-row:hover .np-vs-item.on:not(:hover)::before{background:linear-gradient(0deg,rgba(0,0,0,.7) 0%,rgba(0,0,0,.1) 65%)}
  .np-vs-text,.np-vs-row:hover .np-vs-item.on:not(:hover) .np-vs-text{opacity:1;padding:24px}
  .np-car{margin:40px 0}
  .np-car-in{flex-direction:column;align-items:stretch;padding:40px 0;gap:24px}
  .np-car-title{flex:none;padding:0 var(--m-gutter)}
  .np-car-row{padding:0 var(--m-gutter) 10px}
  .np-car-item{padding:24px;flex-basis:82vw}
  .np-tabs{margin-top:40px;margin-bottom:40px}
  .np-tab-bar{justify-content:flex-start;flex-wrap:nowrap;overflow-x:auto;margin:24px 0}
  .np-tab-bar label{min-width:auto;white-space:nowrap;padding:16px}
  .np-tab-panel,.np-tabs input:checked~.np-tab-panels .np-tab-panel{flex-direction:column;gap:20px}
  .np-tab-panel img{width:100%}
}
