/* tech.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* Tech Master's hero (1355:168). Its ground is the #EEF1F5 wash the services panel
   and the review cards carry — --grad-card — not the flat hero grey, and there is no
   grid floor in it: 1355:169 is one gradient rectangle and nothing else. */
.hero { position: relative; padding: 156px 0 64px; text-align: center; overflow: hidden; background: var(--grad-card); }
.hero .wrap { position: relative; z-index: 1; }
/* 54/500 — §05's h1-big, which is exactly what the frame draws (1355:177). The hero
   role at 48/400 is the homepage's, and the homepage keeps it in home.css. */
.hero h1 { line-height: var(--lh-h1-big); font-size: var(--fs-h1-big); font-weight: var(--fw-h1-big); margin-bottom: 16px; }
/* 16 and navy (1355:179), not the 22px muted lead. Both sizes are roles the document
   names; the frame picks the smaller one for this hero and darkens it, which is what
   lets a two-line subtitle sit under a 54px headline without competing with it.
   783 is the frame's measure. */
.hero .lead { font-size: var(--fs-subtitle); line-height: var(--lh-lead); color: var(--ink); max-width: 783px; margin: 0 auto 32px; }
.hero-visual { position: relative; margin: 48px auto 0; max-width: 1000px; background: var(--hero-bg); border: none; box-shadow: none; }
.hero-visual video { display: block; width: 100%; height: auto; }
.hero-visual::after { content: ""; position: absolute; inset: 0; pointer-events: none;
    background:
      linear-gradient(to bottom, var(--hero-bg), var(--hero-bg-t) 14px),
      linear-gradient(to top, var(--hero-bg), var(--hero-bg-t) 14px),
      linear-gradient(to right, var(--hero-bg), var(--hero-bg-t) 56px),
      linear-gradient(to left, var(--hero-bg), var(--hero-bg-t) 56px); }
/* Same strip the homepage carries and the same frame treatment: 1355:824 is 1514
   wide inside a 1440 board, so it runs edge to edge, and its label is navy at 16
   (1355:825) rather than muted at 18 — which is also what the flat ground requires,
   since --slate measures 4.32:1 on it. */
.trust { padding: 32px 0 64px; background: var(--grad-card); }
.trust-row { display: flex; flex-direction: column; justify-content: center; gap: 48px; padding-left: 32px; }
.trust-label { flex-shrink: 0; font-family: var(--ff); font-weight: 500; font-size: var(--fs-p-small); color: var(--ink); }
/* the switcher is the system component in base.css (DS §10) — only its placement
   is this page's business */
.toggle { margin-top: 24px; }
.work-card .cats { line-height: var(--lh-h6); font-size: var(--fs-h6); color: var(--amber-text); font-weight: 600; margin-bottom: 16px; }
.work-dots i.on { background: var(--amber); width: 22px; border-radius: var(--r-pill); }
#services .wrap { max-width: 1440px; }
.ay-right .toggle { margin: 0; }
/* one internal padding for every instance of the switcher (§10) */
.svc-grid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 32px; row-gap: 0; }
.svc { display: flex; align-items: center; gap: 16px; padding: 24px 2px; border-bottom: 1px solid var(--card-border); cursor: pointer; }
.svc h3 { line-height: var(--lh-h5); font-size: var(--fs-h5); font-weight: 400; margin-bottom: 4px; }   /* 24/400 (1355:1010) */
.svc p { font-size: var(--fs-descriptor); line-height: var(--lh-descriptor); color: var(--ink); }
.svc-arrow { flex-shrink: 0; }
.svc-gap { flex: 1 1 0; min-width: 12px; transition: flex-grow var(--dur-morph) var(--ease); }
.svc:not(.is-soon):hover .svc-gap { flex-grow: 0; }
.svc-txt { flex: 0 1 auto; }
.svc-arrow { width: 24px; height: 24px; color: var(--ink); transition: transform var(--dur-morph) var(--ease); }
.svc:not(.is-soon):hover .svc-arrow { transform: rotate(45deg); }
.svc-grid[hidden] { display: none; }
@media (max-width: 1280px) {
  .svc-grid { grid-template-columns: minmax(0, 1fr); }
}

.clients { padding: 64px 0 96px; }
.clients .wrap { max-width: 1440px; }
.client { box-shadow: 2px 0 0 var(--card-border), 0 2px 0 var(--card-border); }
.client { display: grid; place-items: center; min-height: 138px; padding: 16px 24px; }
.client img { max-width: 86%; max-height: 78px; width: auto; height: auto; object-fit: contain; opacity: .82; transition: opacity var(--dur), transform var(--dur); }
.client:hover img { opacity: 1; transform: scale(1.05); }
.expect { position: relative; overflow: hidden; background: linear-gradient(200deg, var(--off-white) 30%, var(--light-bg) 120%); border-top: 3px solid var(--amber); }
.expect .dots { display: inline-flex; gap: 7px; align-items: center; margin: 8px 0 24px; }
.expect .dots i { width: 8px; height: 8px; border-radius: 50%; background: var(--mid-neutral); }
.expect .dots i:first-child { background: var(--amber); width: 20px; border-radius: var(--r-pill); }
.expect h3 { line-height: var(--lh-h4); font-size: var(--fs-h4); font-weight: 500; margin-bottom: 16px; }
.expect p { font-size: var(--fs-p); color: var(--text-dim); max-width: 440px; margin-bottom: 24px; }
.iso span.dashed { background: none; border: 1.5px dashed var(--slate); box-shadow: none; opacity: 0.5; }
/* The Clutch review card lives in base.css — the homepage and Work Master draw
   the same component. A whole older copy of it sat here: white card, amber
   stars, the title at 500 and the reviewer role at 16, and because tech.css
   loads after base.css it won every colliding property on /work. The two
   landing pages that still use the old markup carry their own copy in
   landing.css and never load this sheet. */
@media (max-width: 680px) {
  /* align-items:flex-start let the marquee size to its own 3500px track instead of
     the row width, so the logo strip ran off the page — the same fix home.css
     already carries. No tech page used the strip until the IoT one, so it sat here
     unnoticed. */
  .trust-row { flex-direction: column; gap: 24px; align-items: stretch; }
  .trust-label { align-self: flex-start; }
}
@keyframes marquee { to { transform: translateX(-50%); } }
.sl-arrow { width: 48px; height: 48px; border-radius: 50%; border: 1px solid var(--card-border); background: #fff; color: var(--ink); display: grid; place-items: center; cursor: pointer; transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease, color var(--dur-fast) ease; }
.sl-arrow:hover { background: var(--soft-hover); border-color: var(--navy); color: var(--navy); }
.sl-arrow:disabled { cursor: default; transform: none; background: var(--soft-hover); color: var(--muted-text); border-color: var(--light-border); }
.port-dots i.on { background: var(--amber); width: 22px; border-radius: var(--r-pill); }
.work-visual .wv-name { color: #fff; line-height: var(--lh-h3); font-size: var(--fs-h3); font-weight: 500; text-align: center; }
/* The review slider frame. tech.css already owns .track and .sl-arrow; these are the
   only pieces that lived in home.css, which neither family loads. Same geometry as the
   homepage: 48px of control plus 24px of clearance on each side, so the arrows sit
   beside the cards rather than on top of the first card's stars. */
.testi-slider { position: relative; padding: 0 72px; }
.ti-arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 4; }
.sl-arrow.ti-arrow:hover { transform: translateY(-50%) scale(1.08); }
/* keep the vertical centring when disabled — .sl-arrow:disabled sets transform: none */
.sl-arrow.ti-arrow:disabled { transform: translateY(-50%); }
.ti-prev { left: 0; }
.ti-next { right: 0; }
.track { display: flex; gap: 24px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth; padding: 4px 4px 8px; margin: -4px -4px 0; -ms-overflow-style: none; scrollbar-width: none; }
.track::-webkit-scrollbar { display: none; }
.track > * { scroll-snap-align: start; flex: 0 0 calc((100% - 40px) / 3); }
@media (max-width: 940px) {
  .track > * { flex-basis: calc((100% - 20px) / 2); }
}
@media (max-width: 680px) {
  .track > * { flex-basis: 88%; }
  /* one card fills the screen, so an overhanging arrow would sit on the quote */
  .testi-slider { display: flex; flex-wrap: wrap; align-items: center; padding: 0; }
  .ti-arrow, .sl-arrow.ti-arrow:hover, .sl-arrow.ti-arrow:disabled { position: static; transform: none; }
  .ti-prev { order: 1; margin-left: auto; }
  .ti-next { order: 2; margin-left: 8px; }
  .testi-slider .track { order: 3; flex: 0 0 100%; min-width: 0; margin-top: 16px; }
}
@media (max-width: 940px) {
  .client { min-height: 116px; }
  .testi-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 680px) {
  .hero { padding: 120px 0 48px; }
}
.bnum-card.weeks { grid-area: weeks; }
/* the small button from the system (.inff-button.small): 14/500, 0.24px tracking,
   uppercase. It used to draw 16/600 with no tracking and no caps — the one amber
   pill on the page that did not match the others. */
/* .bnum-see is retired — it was a fifth amber pill with its own size, weight and
   hover, sitting beside the site's one button family. Markup now says
   .btn .btn-primary .btn-sm, so it inherits the shape morph and arrow march. */
.hero, .sec, .process { --panel-grad: var(--grad-card); }
/* The frame's hero ground is #EEF1F5 washed 5% -> 60% (1355:169) — the same wash the
   services panel and the review cards carry, which is --grad-card. This rule sits
   after the one at the top of the file and was quietly winning with its own
   off-white -> light-bg ramp, so the ground never actually changed when I set it
   there. It keeps the padding, which is what it is really for. */
/* The band is one height on every page. Fixed padding could not do that: the hero
   carries four blocks on /tech and seven on /agency/website-development — pill,
   h1, lead, actions, proof, xref, note — so its content runs 352 to 493 and a
   min-height only ever sets a floor. The padding is a minimum now and the content
   centres in what is left, which lands every page on 730. A column, not a row:
   two pages put .hero-proof outside .wrap, and as a row that line became a
   second item beside the copy, hanging off the right edge. */
.hero { background: var(--grad-card); padding: var(--hero-pt) 0 var(--hero-pb);
  min-height: var(--hero-min); display: flex; align-items: center; }
.hero > .wrap { width: 100%; }
/* this override sits after the mobile block above, so it has to restate the phone
   padding — 210px left a dead band between the bar and the first line of copy */
@media (max-width: 680px) {
  .hero { padding: 120px 0 64px; min-height: 0; }
}
/* The hero floor — a perspective grid with amber bolts running its lines. I had
   taken it out because the frame draws the hero as one flat rectangle; Victor wants
   it back, so the frame is overruled here on purpose.
   It is absolutely positioned, so it stays out of the hero's flex flow, and its
   height is a percentage of the hero — 78% of the 730 the band now stands at, rising
   from the floor and masked out before it reaches the copy. */
/* top/bottom rather than height: 78%. Both resolve to the same 569 against the
   hero's 730 — the offsets are simply the pair that cannot be caught out by a box
   sized from min-height. Note the floor measures 190 by getBoundingClientRect and
   569 by offsetHeight: the rect is post-transform, and rotateX(57deg) through a
   760px perspective is meant to foreshorten it. */
.hero-grid { position: absolute; left: -55%; right: -55%; top: 22%; bottom: 0;
    /* longer focal length + gentler tilt: the horizon lands above the CTAs, so the
       lines stay legible all the way up to where the copy begins */
    transform: perspective(760px) rotateX(57deg);
    transform-origin: bottom center;
    -webkit-mask-image: linear-gradient(to top, #000 8%, rgba(0,0,0,0.6) 42%, rgba(0,0,0,0.25) 70%, transparent 95%);
    mask-image: linear-gradient(to top, #000 8%, rgba(0,0,0,0.6) 42%, rgba(0,0,0,0.25) 70%, transparent 95%);
    pointer-events: none; z-index: 0; overflow: hidden;
    background-image:
      repeating-linear-gradient(90deg, rgba(4,17,58,0.10) 0 1px, transparent 1px 64px),
      repeating-linear-gradient(0deg,  rgba(4,17,58,0.10) 0 1px, transparent 1px 64px); }
.bolt { position: absolute; border-radius: var(--r-pill); will-change: transform; }
.bolt.h { height: 3px; background: linear-gradient(90deg, rgba(255,189,14,0) 0%, rgba(255,189,14,0.45) 50%, rgba(255,210,90,1) 88%, #fffbe8 100%); box-shadow: 0 0 12px rgba(255,189,14,0.85), 0 0 26px rgba(255,189,14,0.45); }
.bolt.h.rev { background: linear-gradient(270deg, rgba(255,189,14,0) 0%, rgba(255,189,14,0.45) 50%, rgba(255,210,90,1) 88%, #fffbe8 100%); }
.bolt.v { width: 3px; background: linear-gradient(180deg, rgba(255,189,14,0) 0%, rgba(255,189,14,0.45) 50%, rgba(255,210,90,1) 88%, #fffbe8 100%); box-shadow: 0 0 12px rgba(255,189,14,0.85), 0 0 26px rgba(255,189,14,0.45); }
.bolt.v.rev { background: linear-gradient(0deg, rgba(255,189,14,0) 0%, rgba(255,189,14,0.45) 50%, rgba(255,210,90,1) 88%, #fffbe8 100%); }
@media (prefers-reduced-motion: reduce) {
  .bolt { display: none; }
}
/* The frame's pill is 53x29 with its label at 13 (1355:189/191). 13 is on no scale
   in the document, so the label takes the 12px step and the padding brings the pill
   back to the frame's height. Radius likewise: the frame says 5, §08 starts at 8. */
.hero-pill { display: inline-block; background: #fff; border-radius: var(--r-sm); padding: var(--s-1) var(--s-2); line-height: 1.2; font-size: var(--fs-p-smallest); color: var(--ink); box-shadow: var(--shadow-sm); margin-bottom: 24px; }
.hero p.lead { max-width: 783px; }   /* the frame's measure for the hero subtitle (1355:179) */
.lede-actions { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-top: 32px; }
/* Equal columns forced "99.9%" (232px on its own) to share a 323px cell with its
   label, leaving 75px for a word that needs 100. Flex lets each stat take what it
   needs and the narrow ones give up their slack — same approach as .stats-3. */
/* nowrap above the fold-to-two-up breakpoint: every stat stays on one line and the
   labels give up width instead, which is what widening them to the frame's 200px
   had broken — four stats with wide numerals (80+, 2.5x, 99.9%, 10+) no longer fit
   and the fourth dropped to a second row. */
/* nowrap, and mandatory: above the two-up breakpoint the band is one row or it is
   nothing. With the labels no longer crushable that means a band which will not fit
   has one stat too many — the answer is to drop a stat, not to let the row break.
   Below 1280 the media block turns this into a two-column grid, where wrap is moot. */
.stats { display: flex; flex-wrap: nowrap; justify-content: space-between; gap: 24px 32px; align-items: center; }
/* 0 1 auto, deliberately: an equal basis gives every stat the same width, which
   starves the ones with a wide numeral — "2.5x average conversion lift" and
   "99.9% uptime maintained" both broke to three lines under it. Natural widths
   with proportional shrink is the right shape; where a label still does not fit,
   the label is too long. min-width:0, or a flex item will not shrink below its
   content. */
/* min-width: min-content, not 0. At 0 a stat may shrink below its own longest word,
   and with overflow-wrap: break-word on the label that is what produced "threat/s"
   and "cybersecuri/ty" at widths between the breakpoints. At min-content the label
   can never be crushed narrower than its widest word, so the two lines the markup
   authors with <br> are the two lines that render. */
.stats .stat { flex: 0 1 auto; min-width: min-content; }
/* nowrap: the label must never drop under the number. It shrinks and wraps its own
   text instead, which is why it carries min-width:0 below. */
.stat { display: flex; align-items: center; gap: 8px 16px; justify-content: center; flex-wrap: nowrap; }
.stat b { line-height: var(--lh-h2-big);
  font-size: var(--fs-h2-big); font-weight: 500; color: var(--ink); flex-shrink: 0; }
/* flex-basis 0 is the point: wrapping is decided from the hypothetical size, so a
   label with a wide max-content (e.g. "uptime maintained" next to "99.9%") used to
   push itself onto a second line. With basis 0 it grows into whatever room is left
   and wraps its own text instead, keeping number and label on one row. */
/* tabular figures only while the digits are changing — see tech.js */
.stat b.is-counting { font-variant-numeric: tabular-nums; }
/* white-space: nowrap is what makes "two rows" a guarantee rather than a coincidence.
   A <br> is a hard break and still fires under nowrap, so the label renders as exactly
   the two lines it is authored with — the browser is no longer allowed to invent a
   third. With nowrap the min-content width is the wider of those two lines, not the
   longest word, so the flex band cannot squeeze a line into wrapping either. Both
   halves of the rule (band on one row, label on two) fall out of that. Dropping the
   200px cap is part of it: a cap under nowrap only produces overflow, never a wrap. */
.stat span { flex: 0 1 auto; min-width: min-content; white-space: nowrap; overflow-wrap: normal;
  font-size: var(--fs-h5); font-weight: 400; color: var(--ink); line-height: 1.2; }
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 64px; align-items: start; }
.split-visual { border-radius: var(--r-lg); background: linear-gradient(150deg, var(--light-bg), var(--soft-hover)); aspect-ratio: 6 / 7; max-height: 660px; }
/* the technology pages carry three accordion rows where a service page carries
   six — the tall visual left a half-screen of white space beside them */
.split-compact .split-visual { aspect-ratio: 5 / 4; max-height: 420px; }
.acc-item { border-bottom: 1px solid var(--card-border); }
.acc-head { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 24px; background: none; border: none; cursor: pointer; padding: 32px 4px; font-family: var(--ff); line-height: var(--lh-h4); font-size: var(--fs-h4); font-weight: 500; color: var(--ink); text-align: left; }
.acc-head i { position: relative; width: 20px; height: 20px; flex-shrink: 0; }
.acc-head i::before, .acc-head i::after { content: ""; position: absolute; background: var(--ink); transition: transform var(--dur), opacity var(--dur); }
.acc-head i::before { left: 0; top: 9px; width: 20px; height: 1.6px; }
.acc-head i::after { left: 9px; top: 0; width: 1.6px; height: 20px; }
.acc-item.open .acc-head i::after { transform: scaleY(0); opacity: 0; }
.acc-body { max-height: 0; overflow: hidden; transition: max-height var(--dur-slow) var(--ease); }
.acc-item.open .acc-body { max-height: 260px; }
.acc-body-in { padding: 0 4px 32px; }
.acc-body h3 { line-height: var(--lh-h5); font-size: var(--fs-h5); font-weight: 500; margin-bottom: 16px; }
.acc-body p { font-size: var(--fs-p); color: var(--text-dim); }
.quote-panel { background: var(--panel-grad); border: 1px solid var(--card-border); border-radius: var(--r-lg); padding: var(--s-6) 8% var(--s-6); text-align: center; }
.quote-panel > p { font-size: var(--fs-h4); font-style: italic; color: var(--ink); line-height: 1.45; margin-bottom: 48px; }
.quote-panel .qn { line-height: var(--lh-h5); font-size: var(--fs-h5); font-style: italic; color: var(--ink); margin-bottom: 6px; }
.quote-panel .qr { font-size: var(--fs-p-small); color: var(--text-dim); }
.process { padding-bottom: 8px; }
.proc-pin { position: relative; }
.proc-sticky { padding: 0 0 8px; background: var(--bg); }
.proc-sticky.is-fixed { position: fixed; top: 130px; left: 0; right: 0; z-index: 2; }
.proc-sticky.is-end { position: absolute; bottom: 0; left: 0; right: 0; }
.proc-sticky .sec-head { margin-bottom: 48px; }
.proc-rail { position: relative; overflow: hidden; padding-top: 96px; }
/* The frame runs the rail through the middle of each marker — Line 9 sits at y=1786
   and the step indicator spans 1753 to 1819, so the line crosses its centre and the
   marker's white ring masks it. The marker centre here is 96 of padding minus the
   dot's 67 plus half its 34, which is 46. At 54 the rail crossed the numerals low. */
.proc-rail::before { content: ""; position: absolute; top: 46px; left: 0; right: 0; height: 1px; background: var(--card-border); }
.proc-track { display: flex; align-items: stretch; gap: 24px; padding: 0 var(--tl-pad, 2.5vw); width: max-content; will-change: transform; }
/* --dot-c is the marker's centre, and the stem reads it rather than carrying its own
   number: the two had drifted 5.5px apart, so the line under each numeral hung to the
   right of it on all 22 pages that draw this component. */
.tl { position: relative; flex: 0 0 var(--tl-w, 24%); --dot-c: 48px; }
/* The frame's marker, layer for layer (1361:1743-1746): a 66px WHITE disc, a 48px
   amber one at 35% over it, a 34px solid amber core, and the numeral on that core.
   I read the two amber ellipses as one and filled the whole 48 solid — which is not
   what it draws at all. Built from the core outwards instead: 34 of amber, +7 of
   amber at 35% takes it to 48, +9 of white takes it to 66. */
.tl .dot { position: absolute; top: -67px; left: calc(var(--dot-c) - 17px); width: 34px; height: 34px; border-radius: 50%;
    border: none;
    background: var(--amber);
    box-shadow: 0 0 0 7px rgba(255,189,14,0.35),   /* 34 -> 48, the amber at 35% */
                0 0 0 16px #fff,                    /* 48 -> 66, the white disc */
                0 0 0 17px var(--amber),            /* its 1px amber rim (1361:1743) */
                0 0 14px rgba(255,189,14,0.30);
    /* White, as the frame draws it (1361:1746). This is a knowing exception to §17:
       white on #FFBD0E measures 1.67:1 where the rule asks 4.5 at 18px, and Victor
       took that call on 2026-08-26 after seeing the number. page-audit counts .dot
       under __excluded rather than reporting it, so the exception stays visible in
       one place instead of turning the audit permanently red.
       It is the numeral only. Amber still takes navy everywhere else in the system —
       the Primary Button and the active tab both. */
    display: grid; place-items: center; font-weight: 700; font-size: var(--fs-p); color: #fff;
    z-index: 0; }
.tl .stem { position: absolute; top: -18px; left: var(--dot-c); transform: translateX(-50%);
            width: 1px; height: 18px; background: var(--card-border); }
.tl-card { position: relative; background: var(--panel-grad);
            border: 1px solid transparent; border-radius: var(--r); padding: 32px 32px 32px; height: 100%; }
/* the frame strokes its cards with a gradient rather than a flat line — #D9DBE1 at the
   top fading to nothing at the bottom, top to bottom (handles 0.5,0 -> 0.5,1). The
   border stays in the box at 1px transparent so nothing shifts; the ring is a masked
   pseudo-element, which is the only way to stroke a rounded box with a gradient. */
.tl-card::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 1px; pointer-events: none;
  background: linear-gradient(180deg, var(--card-border) 0%, var(--card-border-0) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude; }

/* 28/500 — the frame's process step (1361:1714's titles sit at 28), which is §05's
   h4. It had been the 32px h3, one step above what the frame draws. */
.tl-card h3 { line-height: var(--lh-h4); font-size: var(--fs-h4); font-weight: 500; margin-bottom: 24px; }
.tl-card p { font-size: var(--fs-descriptor); line-height: var(--lh-p); color: var(--ink); }
.tech-panel { background: var(--panel-grad); border: 1px solid var(--card-border); border-radius: var(--r-lg); padding: 64px 48px; text-align: center; }
.tech-panel h2 { line-height: var(--lh-h2); font-size: var(--fs-h2); font-weight: 400; margin-bottom: 48px; }
.tech-wall { display: flex; flex-wrap: wrap; justify-content: center; gap: 32px 24px; max-width: 1080px; margin: 0 auto; }
/* The frame draws the wall as marks alone, all of them navy and nothing under them
   (1365:1779). Ours were the vendors' own colours with a 16px caption below, which
   made every item 78px tall against the frame's 48 — the reason they read as larger
   even though the mark itself is 48 in both.
   The caption goes; the img's alt already carries the name for a screen reader and
   for the link.
   The colour does NOT: the frame's wall is navy throughout, and a filter cannot get
   there. Half of these marks are knockouts — the TS and JS squares, MySQL, MongoDB —
   where the glyph is white ON the shape rather than a hole through it, so flattening
   them to one colour turns them into solid blobs. Matching the frame needs
   single-colour mark files, which is asset work, not a rule. The vendors' own marks
   stay until those exist. */
.tech-item { width: 48px; }
.tech-item { display: flex; flex-direction: column; align-items: center; }
.tech-item img { width: 48px; height: 48px; object-fit: contain; display: block; }
.tech-item small { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* tech-wall entries link to /technologies/<name> — an anchor has to inherit the
   div's typography or the wall picks up link colour and underlines */
a.tech-item { color: inherit; text-decoration: none; transition: transform var(--dur) ease; }
a.tech-item:hover { transform: translateY(-3px); }
a.tech-item:hover small { color: var(--ink); }
a.tech-item:hover .tech-chip { border-color: var(--navy); }
@media (prefers-reduced-motion: reduce) { a.tech-item, a.tech-item:hover { transform: none; transition: none; } }
.tech-more { margin-top: 32px; font-size: var(--fs-p-small); color: var(--muted); }
.head-row { display: flex; align-items: center; justify-content: space-between; gap: 32px; flex-wrap: wrap; margin-bottom: 48px; }
.head-row h2 { line-height: var(--lh-h2); font-size: var(--fs-h2); font-weight: 400; }
.head-row p { font-size: var(--fs-subtitle); line-height: var(--lh-subtitle); color: var(--text-dim); }
.bento { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; margin-bottom: 24px; align-items: stretch; }
.bento-label { background: var(--panel-grad); border: 1px solid var(--card-border); border-radius: var(--r); display: grid; place-items: end center; padding: 32px 24px; font-weight: 600; font-size: var(--fs-p); color: var(--ink); text-align: center; min-height: 210px; }
.bento-img { border-radius: var(--r); background: linear-gradient(140deg, var(--light-border), var(--mid-neutral)); min-height: 210px; }
.bento-card { position: relative; grid-column: span 2; background: #fff; border: 1px solid transparent; border-radius: var(--r); padding: 32px 32px; }
/* the frame strokes its cards with a gradient rather than a flat line — #D9DBE1 at the
   top fading to nothing at the bottom, top to bottom (handles 0.5,0 -> 0.5,1). The
   border stays in the box at 1px transparent so nothing shifts; the ring is a masked
   pseudo-element, which is the only way to stroke a rounded box with a gradient. */
.bento-card::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 1px; pointer-events: none;
  background: linear-gradient(180deg, var(--card-border) 0%, var(--card-border-0) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude; }

.bento-card .kicker { font-size: var(--fs-h4); font-weight: 500; color: var(--ink); line-height: var(--lh-h4); }
.bento-card .num { display: block; font-size: var(--fs-h2-big); font-weight: 500; color: var(--ink); line-height: var(--lh-h2-big); margin-bottom: 16px; }
.bento-card h3 { font-size: var(--fs-h5); line-height: var(--lh-h5); font-weight: 700; margin-bottom: 16px; }
.bento-card p { font-size: var(--fs-p); color: var(--text-dim); }
.why-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
/* The frame's card is white on a white ground and its edge is what separates the
   two — #EFF0F3 in the render (1365:1810). Ours had border:none, so on a near-white
   section the cards had nothing to stand on and the section read as one flat white
   field. --card-border is the token for exactly this. */
.why { position: relative; background: #fff; border: 1px solid transparent; border-radius: var(--r); padding: 32px 32px 32px; }
/* the frame strokes its cards with a gradient rather than a flat line — #D9DBE1 at the
   top fading to nothing at the bottom, top to bottom (handles 0.5,0 -> 0.5,1). The
   border stays in the box at 1px transparent so nothing shifts; the ring is a masked
   pseudo-element, which is the only way to stroke a rounded box with a gradient. */
.why::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 1px; pointer-events: none;
  background: linear-gradient(180deg, var(--card-border) 0%, var(--card-border-0) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude; }
.why .ic { color: var(--amber); margin-bottom: 24px; display: block; width: 48px; height: 48px; }
/* the mark fills the box it is given — the inline SVGs carry a viewBox and no
   width/height, so without this they fall back to the replaced-element default
   and draw at whatever the intrinsic size resolves to instead of the frame's 44 */
.why .ic svg { display: block; width: 100%; height: 100%; }
.why h3, .why h4 { line-height: var(--lh-h4); font-size: var(--fs-h4); font-weight: 500; margin-bottom: 16px; }
.why p { font-size: var(--fs-p-small); color: var(--ink); }

/* ── The tinted card ──────────────────────────────────────────────────────────
   The masters draw two card grounds, not one. "What you get either way" (tech
   1365:1836) and "What AI-native actually changes" (1428:3085) are the white card
   above. "How the work is scoped" (1432:3332), "The choices that decide the
   outcome" (1431:3272), "What we stand for" (1432:3732) and Work's "Three ways we
   get involved" (1432:4445) are a filled card: 628x269 at r24, 36 of padding, a
   44px mark and a 32px title over a 15px descriptor whose first line is bold.
   The frame fills it #D3DBE8 under a 50px background blur, which resolves over
   white to the surface §04 already names — so it is --off-white, flat, with no
   backdrop-filter to make the card a containing block for anything inside it. */
.why.tint { background: var(--off-white); border-color: transparent; border-radius: var(--r-lg); padding: var(--s-4); box-shadow: none; }
.why.tint::before { content: none; }
.why.tint .ic { width: 44px; height: 44px; margin-bottom: 24px; }
.why.tint h3 { line-height: var(--lh-h3); font-size: var(--fs-h3); font-weight: 400; margin-bottom: 24px; }
.why.tint p { font-size: var(--fs-descriptor); line-height: 1.5; color: var(--ink); }
.why.tint p b { display: block; font-weight: 600; }
/* Work's three routes are the same card, centred, ending on a link instead of copy */
.why.tint.mid { padding: var(--s-3); text-align: center; }
.why.tint.mid .ic { margin: 0 auto 24px; }
.why.tint.mid h3 { line-height: var(--lh-h4); font-size: var(--fs-h4); font-weight: var(--fw-h4); margin-bottom: 24px; }
.why.tint.mid p { font-size: var(--fs-p-small); color: var(--text-dim); margin-bottom: 24px; }
.why.tint.mid .link-all { justify-content: center; margin-top: auto; font-size: var(--fs-p-small); }
.why.tint.mid { display: flex; flex-direction: column; }

/* ── "See all …" ──────────────────────────────────────────────────────────────
   The frame ends a section head with a labelled link and a 30px arrow (1428:1855),
   not with a Secondary Button — the button is for an action, this is a way out to
   another page. 20/400 navy, underlined, which is the direction §09 wants a navy
   link to take on hover (it cannot darken, so it keeps the rule and moves the arrow). */
.link-all { display: inline-flex; align-items: center; gap: var(--s-1); flex-shrink: 0;
  line-height: var(--lh-subtitle); font-size: var(--fs-subtitle); color: var(--ink);
  text-decoration: underline; text-underline-offset: 4px; }
.link-all svg { width: 24px; height: 24px; flex-shrink: 0; transition: transform var(--dur) var(--ease); }
.link-all:hover { text-decoration-color: var(--amber); }
.link-all:hover svg { transform: translateX(3px); }
/* the contact block and its form now live in form.css — one component,
   loaded by every family that renders it, including the homepage */
.more-grid { display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 32px; }
.more-link { display: flex; align-items: center; gap: 16px; padding: 24px 4px; border-bottom: 1px solid var(--card-border); line-height: var(--lh-h4); font-size: var(--fs-h4); font-weight: 500; color: var(--ink); transition: color var(--dur); }
.more-gap { flex: 1 1 0; min-width: 14px; transition: flex-grow var(--dur-morph) var(--ease); }
.more-link > span:first-child { min-width: 0; }   /* wraps only when the title genuinely doesn't fit — nowrap clipped the arrow off the last column */
.more-link:hover .more-gap { flex-grow: 0; }
.more-link:first-child, .more-grid > *:nth-child(-n+3) { border-top: 1px solid var(--card-border); }
.more-link svg { width: 24px; height: 24px; flex-shrink: 0; transition: transform var(--dur-morph) var(--ease); }
.more-link:hover svg { transform: rotate(45deg); }
@media (max-width: 1100px) {
  .proc-pin { height: auto !important; }
  .proc-sticky { position: static !important; }
  .proc-rail { overflow: visible; }
  .proc-track { width: auto; overflow-x: auto; scrollbar-width: none; scroll-snap-type: x mandatory; transform: none !important; }
  .proc-track::-webkit-scrollbar { display: none; }
  .tl { flex-basis: 42%; scroll-snap-align: start; }
  .more-grid { grid-template-columns: 1fr 1fr; }   /* 3 columns stop fitting the longest service name well before 940px */
  .more-grid > *:nth-child(3) { border-top: none; }   /* the top border marks the first row — which is now 2 wide, not 3 */
}
@media (max-width: 940px) {
  /* two per row from here down, so the stats never squeeze on a tablet */
  .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 32px 24px; align-items: start; }
  .stat span { max-width: none; }
  .stat { flex: 0 1 46%; }
  .split { grid-template-columns: 1fr; gap: 32px; }
  .split-visual { aspect-ratio: 16 / 10; }
  .bento { grid-template-columns: 1fr; }
  .bento-card { grid-column: auto; }
  .why-grid, .more-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 680px) {
  .tl { flex-basis: 84%; }
  .why-grid, .more-grid { grid-template-columns: 1fr; }
  .more-grid > *:nth-child(2) { border-top: none; }
  /* .why-grid.cols-2 and .pairs both carry two class selectors, so they outrank
     the plain .why-grid rule above and need collapsing explicitly */
  .why-grid.cols-2, .pairs { grid-template-columns: 1fr; }
  .pairs .pair:nth-child(2) { border-top: none; }   /* one column — only the first row has a top edge */
  /* Two stats per row, label under the number. .stats is flex, so the old
     grid-template-columns here was inert — hence the ragged 1+1+1 wrap. */
  .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 32px 24px; align-items: start; }
  /* centred in the cell, not flush left. The grid was already symmetric — 24px of
     wrap on each side — but the content hugged the left of each column, so the band
     read as pushed to one side: the left column started hard against the edge while
     the right one trailed off well short of it. */
  .stat { flex-direction: column; align-items: center; justify-content: flex-start; gap: 4px; text-align: center; }
  .stat span { max-width: none; }   /* the label owns the full column now, not the leftover beside the number */
}
.tech-chip { display:grid;place-items:center;height:46px;padding: 0 16px;border-radius:var(--r-sm);background:var(--bg-alt);line-height: var(--lh-h6); font-size: var(--fs-h6);font-weight:600;color:var(--ink);white-space:nowrap; }
.tech-text { width:auto; }

/* --- page-specific overrides of base/shell --- */
/* 1280 + the wrap's own 24 of padding each side. Every frame in the file draws
   its content 1280 wide at x=80 — the masters, the case template, the portfolio
   and the About pages alike. This sheet had opened it to 1428, which put ~100px
   more width under every grid and made each element render smaller against the
   page than the design does. */
.wrap { max-width: 1328px; }
.sec-head { max-width: 900px; }
.sec-head h2 { font-weight: 400; line-height: var(--lh-h2); font-size: var(--fs-h2); }
/* Every section subtitle in all five master frames is #667085 — including
   1355:1085, the node this rule already cited while painting it navy. A
   subtitle supports the heading above it; §04 gives supporting text the muted
   tone. base.css already had it right, so only the weight override stays. */
.sec-head p { font-weight: 400; font-size: var(--fs-subtitle); line-height: var(--lh-subtitle); }
/* Full-width, left-aligned section head ("More services", "Related work"). It was
   21 copies of an inline max-width/text-align plus a clamp(30px,4vw,46px) headline
   that belonged to no step of the scale; the role is the theme's h2.bigger. */
.sec-head.big h2 { line-height: var(--lh-h2-bigger); font-size: var(--fs-h2-bigger); font-weight: var(--fw-h2-bigger); }
.sec-head.flush { max-width: none; text-align: left; margin-bottom: 32px; }
/* 36, not 48. Every section head in the frame is 36/400 — "What we build" (1355:1084),
   "How an engagement runs" and "What you get either way" alike — which is §05's h2 and
   the size the homepage's heads already use. .flush keeps its alignment and measure;
   only the size it was bumping is gone. */
.sec-head.flush h2 { font-size: var(--fs-h2); font-weight: 400; line-height: var(--lh-h2); }

/* review scroll-highlight: words fade from light grey to ink as the panel scrolls by */
.quote-panel p .w { color: #cdd5e1; transition: color var(--dur) ease; }
.quote-panel p .w.on { color: var(--ink); }

/* hero-proof lives in base.css; only the dark AI hero needs an override */
.ai-dark .hero-proof { color: var(--ai-dim); }
.ai-dark .hero-proof a { text-decoration-color: rgba(255,255,255,0.25); }
.ai-dark .hero-proof a:hover { color: #fff; }
.ai-dark .hero-proof b { color: #fff; }

/* Accreditations wall — badge over caption, as published by the certifying bodies. */
.cert-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: 8px; }
.cert { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 16px;
  padding: 24px 16px 24px; background: #fff; border: 1px solid var(--card-border); border-radius: var(--r);
  transition: box-shadow var(--dur) ease, transform var(--dur) ease, border-color var(--dur) ease; }
.cert:hover { box-shadow: var(--shadow-sm); border-color: var(--mid-neutral); }
.cert img { width: 80px; height: 80px; object-fit: contain; }
.cert span { font-size: var(--fs-p-smallest); line-height: 1.45; color: var(--text-dim); }
@media (max-width: 480px) {
  .cert-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; }
  .cert { padding: 24px 8px 16px; gap: 16px; }
  .cert img { width: 64px; height: 64px; }
}

/* the accordion trigger is the section heading — wrapping it in h2 keeps the
   document outline consecutive without changing a pixel */
.acc-h { margin: 0; font: inherit; }

/* ── Sales agent ────────────────────────────────────────────────────────────
   The design file's "Sales Agent - v2" frame, which sits on the tech, agency and
   about templates alike. It lived in agency.css; moved here so the tech family can
   use it too — agency.css loads after this file, so those pages are unaffected. */
.founders { display: grid; gap: 24px; }
.founder { position: relative; text-align: center; background: var(--bg-soft); border: 1px solid transparent;
  border-radius: var(--r-lg); padding: 64px 32px 48px; }
/* the frame strokes its cards with a gradient rather than a flat line — #D9DBE1 at the
   top fading to nothing at the bottom, top to bottom (handles 0.5,0 -> 0.5,1). The
   border stays in the box at 1px transparent so nothing shifts; the ring is a masked
   pseudo-element, which is the only way to stroke a rounded box with a gradient. */
.founder::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 1px; pointer-events: none;
  background: linear-gradient(180deg, var(--card-border) 0%, var(--card-border-0) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude; }

/* the same 279 plate the Sales Agent card carries in the frame — this was 168 */
.founder .fphoto { width: 279px; height: 279px; border-radius: 50%; margin: 0 auto 24px; display: block;
  background: radial-gradient(circle at 50% 40%, var(--off-white) 0%, var(--light-bg) 70%); }
/* the frame runs the founder line at the h2-bigger role, centred (1432:3974) */
.founder h3 { line-height: var(--lh-h2-bigger); font-size: var(--fs-h2-bigger); font-weight: var(--fw-h2-bigger); margin-bottom: 16px; }
.founder p { line-height: var(--lh-h5); font-size: var(--fs-h5); font-weight: 400; color: var(--ink); margin-bottom: 24px; }
/* .btn-chat is retired: it was an amber pill at 700/16 with its own hover and no
   arrow march — a page-specific button beside the site's one button family. Its
   markup now says .btn .btn-primary (DS §09). */
.founder .btn svg { width: 16px; height: 16px; }

/* ── IoT template additions ─────────────────────────────────────────────────
   Sections the "IoT Software" frame (design file → IoT Software Development,
   node 1062:1449) needs and the tech template did not have yet. All three reuse
   the existing scale and palette — no new type sizes, no new colours. */

/* hero headline in two weights, the way .ai-hero h1 .hl already does it */
.hero h1 .hl { font-weight: 700; }

/* ── technology pages: the long-form sections ─────────────────────────────── */
/* running prose, wider measure than .sec-head but still short enough to read */
/* Two paragraphs — and every one of the 63 technology pages has exactly two. As a
   single 920px column they stopped 460px short of the section head and the link grid
   above them, which reads as an unfinished block; and they still ran to about 102
   characters, well past a comfortable measure. Two columns at the section's own width
   answer both: the text reaches the end of the line like everything else on the page,
   and each column lands near 73 characters. The 1fr 1fr on a 64px gutter is .split's,
   which is how this page already does two columns (DS §18). */
.prose { display: grid; grid-template-columns: 1fr 1fr; gap: 0 64px; }
.prose p { font-size: var(--fs-p); color: var(--text-dim); line-height: 1.65; }
@media (max-width: 940px) {
  .prose { grid-template-columns: 1fr; gap: 16px; }
}
/* the fit / does-not-fit pair, and the four-card "what goes wrong" row */
.why-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.fit-list { list-style: none; display: grid; gap: 16px; }
.fit-list li { position: relative; padding-left: 32px; font-size: var(--fs-p); color: var(--text-dim); line-height: 1.45; }
.fit-list li::before { content: ""; position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 50%;
  background: var(--amber); -webkit-mask: var(--tick) center / 12px no-repeat; mask: var(--tick) center / 12px no-repeat; }
.fit-list-no li::before { background: var(--muted); -webkit-mask-image: var(--cross); mask-image: var(--cross); }
:root { --tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z'/%3E%3C/svg%3E");
  --cross: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M19 6.4 17.6 5 12 10.6 6.4 5 5 6.4l5.6 5.6L5 17.6 6.4 19l5.6-5.6 5.6 5.6 1.4-1.4-5.6-5.6z'/%3E%3C/svg%3E"); }
/* "what we run alongside it" — a definition list, two columns on desktop */
.pairs { display: grid; grid-template-columns: 1fr 1fr; gap: 0 48px; }
.pair { display: grid; gap: 6px; padding: 24px 0; border-bottom: 1px solid var(--card-border); }
.pairs .pair:nth-child(-n+2) { border-top: 1px solid var(--card-border); }
.pair dt { line-height: var(--lh-h4); font-size: var(--fs-h4); font-weight: 500; color: var(--ink); }
.pair dd { font-size: var(--fs-p-small); color: var(--text-dim); line-height: 1.45; }
/* the comparison accordion is full width — it has no visual beside it */
.acc-wide { max-width: 1000px; }

/* the frame's "End-to-end IoT development" row is four of the .why cards, not three */
.why-grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
.why-grid.cols-4 .why { padding: 32px 24px 32px; }
/* the four-column variant used to set its heading one step LARGER than the
   three-column one, in narrower cards. It takes the same h4 as its sibling. */
.why-grid.cols-4 .why p { font-size: var(--fs-p-small); }

/* "Building products that work as one": copy and numbered points beside the art */
.pillars { display: grid; grid-template-columns: 1fr 1fr; gap: 64px; align-items: center; }
.pillars h2 { line-height: var(--lh-h2-bigger); font-size: var(--fs-h2-bigger); font-weight: 400; margin-bottom: 32px; }
/* the design runs this column on a 1.6 line and a 147px pitch between the points */
.pillars > div > p { font-size: var(--fs-p); line-height: 1.6; color: var(--text-dim); margin-bottom: 64px; max-width: 600px; }
.pillar { display: grid; grid-template-columns: 66px 1fr; gap: 0 24px; margin-bottom: 64px; }
.pillar:last-child { margin-bottom: 0; }
/* four concentric circles in the design: a 66px white disc ringed in amber, a 48px
   amber wash at 35%, a 34px solid amber core, and the numeral in white on top.
   One element: the ring is the border, the two inner discs are colour stops. */
.pillar i { grid-row: 1 / 3; width: 64px; height: 64px; border-radius: 50%; display: grid; place-items: center;
  border: 1px solid var(--amber); font-style: normal; color: #fff;
  /* the 35% ring has to fall on the white disc, as it does in the design — without
     the colour underneath it composites over the section wash and goes grey */
  background-color: #fff;
  background-image: radial-gradient(circle,
    var(--amber) 0 16.5px,
    rgba(255,189,14,0.35) 17.5px 23.5px,
    #fff 24.5px 100%);
  font-weight: 700; font-size: var(--fs-p); line-height: 1; }
/* the design wraps these at a ~500px measure; the site column is wider, so cap it
   or the lines break in different places than the frame does */
.pillar b, .pillar span { max-width: 478px; }
.pillar b { font-size: var(--fs-p); font-weight: 700; color: var(--ink); line-height: 1.6; }
.pillar span { font-size: var(--fs-p); color: var(--text-dim); line-height: 1.6; }
/* the frame is a 20px-radius rectangle with the artwork as its fill and nothing
   behind it — the export keeps its transparency, so the section shows through */
.pillars-art { border-radius: var(--r-lg); overflow: hidden; background: #fff; }
.pillars-art img { display: block; width: 100%; height: auto; }

/* "Our work": three case cards, as the masters draw them (agency 1428:1874 and the
   same component on AI, About and the three Work rows): 411x428 at r24, a 409x237
   plate on top, then a 32px body holding the title, one meta line and the CTA. The
   card had carried a category row with a country flag, a rule, and a paragraph of
   copy above the button; the frame has none of them, and the title leads. */
.wcase-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
.wcase { position: relative; display: flex; flex-direction: column; background: #fff; border: 1px solid transparent;
  border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow); transition: transform var(--dur-fast), box-shadow var(--dur-fast); }
/* the frame strokes its cards with a gradient rather than a flat line — #D9DBE1 at the
   top fading to nothing at the bottom, top to bottom (handles 0.5,0 -> 0.5,1). The
   border stays in the box at 1px transparent so nothing shifts; the ring is a masked
   pseudo-element, which is the only way to stroke a rounded box with a gradient. */
.wcase::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 1px; pointer-events: none;
  background: linear-gradient(180deg, var(--card-border) 0%, var(--card-border-0) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude; }

.wcase:hover { box-shadow: var(--shadow-lg); }
.wcase-img { aspect-ratio: 409 / 237; overflow: hidden; background: var(--bg-alt); }
.wcase-img img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* the reel puts a <video> in this box, and the rule above only reached <img>: the
   clip laid out at its own 720x417 width/height attributes inside a 409x237 box,
   so overflow:hidden cropped it to a magnified top-left corner with the --bg-alt
   ground showing through wherever the poster did not reach. */
.wcase-img video { display: block; width: 100%; height: 100%; object-fit: cover; }
.wcase-body { display: flex; flex-direction: column; flex: 1; padding: var(--s-4); }
.wcase h3 { line-height: var(--lh-h3); font-size: var(--fs-h3); font-weight: 400; margin-bottom: var(--s-1); }
/* the sector and what the thing is, on one line — 16/400 muted (1428:1879) */
.wcase-meta { font-size: var(--fs-p-small); color: var(--text-dim); margin-bottom: 24px; }
.wcase .btn-book { align-self: flex-start; margin-top: auto; }

@media (max-width: 1100px) {
  .why-grid.cols-4 { grid-template-columns: 1fr 1fr; }
  .wcase-grid { grid-template-columns: 1fr 1fr; }
  .pillars { gap: 48px; }
}
@media (max-width: 940px) {
  .why-grid.cols-4, .wcase-grid { grid-template-columns: 1fr; }
  .pillars { grid-template-columns: 1fr; gap: 32px; }
  .pillars-art { order: -1; }
}

/* CRO-01: the reviews carousel used to end each card with its own link to Clutch —
   nine doors out of the page. The proof stays on the page; one link leaves it. */
/* .testi-all is declared beside the card in base.css */

/* Responsive collapses, kept last on purpose.
   .why-grid.cols-2 and .pairs are two-class selectors declared further up this file
   than the @media block at 680 that collapses them, and a media query adds no
   specificity — so the later two-column rule won and the cards stayed side by side
   all the way down to 320, where a card's text box is 58px and every heading and
   bold run overflowed it. Collapsing them here, after their base rules, is the fix
   the four earlier same-weight collisions in this project should have taught. */
@media (max-width: 680px) {
  .why-grid.cols-2, .pairs { grid-template-columns: 1fr; }
  /* same collision: shell.css collapses .testi-grid at 680, but this file's
     two-column rule at 940 is later in the cascade and outlived it, so the review
     cards stayed paired down to 320 — where a bare 1fr track cannot shrink below
     its own min-content and the second card ran off the page entirely. */
  .testi-grid { grid-template-columns: minmax(0, 1fr); }
}

/* 34px of underlined type is a fine-pointer target; on a phone it is short of the
   44 the rest of the site holds to. */
@media (hover: none) and (pointer: coarse) {
  .link-all { min-height: 44px; }
}

/* The stats band goes two-up from 1279 rather than 940: at 980 four stats with wide
   numerals shared the row so narrowly that even "store uptime" broke to three lines.
   1023 is the nav's own breakpoint, so the page changes shape in one place. Kept last
   because .stats.stats-3 in agency.css carries two classes and would otherwise win. */
@media (max-width: 1279px) {
  .stats, .stats.stats-3 { display: grid; grid-template-columns: 1fr 1fr; align-items: start; gap: 32px 24px; }
  .stats .stat span, .stats.stats-3 .stat span { max-width: none; }
}

