/* home.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
.hero { position: relative; padding: 156px 0 64px; text-align: center; overflow: hidden; background: var(--hero-bg); }
.hero .wrap { position: relative; z-index: 1; }
/* The hero floor — a perspective grid with amber bolts running its lines — is
   gone: the frame's first section is one flat rectangle (1315:699, #EAEDF1 from
   0 to 1342) with no pattern in it at all. --hero-bg stays #edeff3, which is 3
   points off the frame's value and is the hero grey ds-check already sanctions.
   #heroGrid went with it; shell.js's bolt spawner guards on the element and is
   inert without it. */
.hero h1 { line-height: var(--lh-h1-hero); font-size: var(--fs-h1-hero); font-weight: 400; margin-bottom: 24px; }   /* the frame's gap to the lead (1317:180 ends 289, 1317:181 starts 313) */
/* first line bold against the 400 second line — 700 is the weight staging renders
   the hero at, and DMSans-Bold is one of the four faces we ship */
.hero h1 .h1-strong { font-weight: 700; }
.hero h1 .slash { font-weight: 700; }   /* the frame's "/" is part of the bold run (style 225) */
.hero h1 .h1-light { font-weight: 400; }
/* same reason as .hero-proof: the muted grey measures 4.32:1 on --hero-bg, and DS 04
   only sanctions it where contrast holds. Hierarchy here comes from size, which is
   what DS 05 asks for anyway -- 22px under a 48px headline. */
/* The frame stacks the two reassurances under the button as separate centred lines,
   reply first (1317:183 at y=444, 1317:182 at y=468) — the page had them spliced into
   one sentence with the reply second and turned into a link. They are 24px apart in
   the frame, which is one line pitch: 4px of margin between two 20px lines. */
.hero .hero-reply { margin: 32px 0 0; font-size: var(--fs-descriptor); line-height: var(--lh-descriptor); color: var(--ink); }
.hero .hero-reply + .hero-proof { margin-top: 4px; }
.hero .lead { font-size: var(--fs-subtitle); line-height: var(--lh-lead); color: var(--ink); max-width: 560px; 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); }
/* The strip runs edge to edge. In the frame its container is 1514 wide inside a
   1440 board (1315:847, x=29 to x=1543) — it starts near the left edge and the marks
   carry on past the right one. Ours was capped at the page's 1380 and centred, which
   made it a contained row with the logos stopping short of the screen on both sides.
   The label keeps the 32px the frame gives it; everything to its right is marquee. */
.trust { padding: 32px 0 64px; background: var(--hero-bg); }   /* the frame's rectangle covers the strip too, flat, and cuts to white at the section edge */
.trust-row { display: flex; align-items: center; gap: 48px; padding-left: 32px; }
/* Navy at 16, which is what the frame draws (1315:848) — and what the flat ground
   now requires: --slate on --hero-bg measures 4.32:1, under the 4.5 this size needs.
   The gradient used to carry the label over a lighter stretch and hide that; §04
   qualifies the muted tone with "where contrast remains sufficient", and here it
   is not. */
.trust-label { flex-shrink: 0; font-family: var(--ff); font-weight: 500; font-size: var(--fs-p-small); color: var(--ink); }
/* The switcher itself is the system component in base.css (DS §10). Only where it
   sits belongs to this page. */
.toggle { margin-top: 24px; }
/* 32px of padding, like the bento and the CTA band below it: 1380 on 24 started the
   portfolio 8px left of the nav logo and of every other wide section on the page */
#work .wrap { max-width: 1340px; padding-left: 32px; padding-right: 32px; }
.work-toggle-wrap { text-align: center; margin: 32px 0 64px; }
.work-split { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 64px; align-items: stretch; }
.work-col { display: flex; flex-direction: column; }
.work-split::after { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: var(--card-border); }
.work-col { min-width: 0; }
/* 16/400, the frame's metadata size (1317:346 and 1317:368) — the row had been at
   18/500, a weight the frame never uses here and which made the industry compete
   with the client name under it. */
.work-label { display: flex; align-items: center; gap: 16px; height: 46px; border-bottom: 1px solid var(--card-border); font-size: var(--fs-p-small); font-weight: 400; color: var(--ink); }
/* Where the client is, said the way /work and every case study already say it: the
   flag rides immediately before the industry, at the metadata's own size and colour,
   with no pill or plate around it. 12px in from the flag, 16px out to the icon — so
   the flag reads as belonging to the words, not as a third item in the row.
   The box is the design's (Figma 1317:345 is 36x20, square): 20px tall, and the
   width follows the artwork's own 10:7 rather than the placeholder's 9:5, because
   stretching a national flag to fit a box is the one thing you cannot do here.
   Square, like the frame: --r-pill was drawing the flags as lozenges, and §08's
   smallest radius is 8, which would round a 20px flag almost as badly. */
.wl-geo { display: inline-flex; align-items: center; gap: var(--s-1); min-width: 0; }
.wl-flag { width: 29px; height: 20px; flex-shrink: 0; display: block;
  box-shadow: 0 0 0 1px rgba(4, 17, 58, 0.10); }
.work-cats { justify-content: space-between; color: var(--text-dim); }
.work-cats .wc-dev { display: inline-flex; color: var(--muted); }
.work-cats .wc-dev svg { width: 24px; height: 24px; }
/* The client name is the 32px role in the frame (1317:356), not the 48px section
   headline — at 48 it outweighed "We're letting our work do the talking" above it. */
.work-split h3 { line-height: var(--lh-h3); font-size: var(--fs-h3); font-weight: 400; margin: 48px 0 24px; }
.work-split .work-desc { font-size: var(--fs-p-small); line-height: var(--lh-p); color: var(--text-dim); max-width: 455px; }
.work-split .work-cta { align-self: flex-start; margin-top: auto; }   /* styling comes from .btn-book */
.work-shot { margin-top: 24px; border-radius: var(--r-md); overflow: hidden; border: 1px solid var(--card-border); box-shadow: var(--shadow); background: #fff; }
.work-chrome { display: flex; align-items: center; gap: 7px; padding: 16px 16px; background: var(--light-bg); border-bottom: 1px solid var(--card-border); }
.work-chrome i { width: 11px; height: 11px; border-radius: 50%; background: var(--mid-neutral); }
.work-chrome .wc-bar { flex: 1; height: 22px; border-radius: var(--r-pill); background: #fff; margin-left: 16px; }
.work-screen { aspect-ratio: 16 / 10; display: grid; place-items: center; padding: 48px; background: linear-gradient(160deg, #ffffff, var(--light-bg)); }
.work-screen img { max-width: 60%; max-height: 110px; width: auto; object-fit: contain; }
.work-screen .wv-name { line-height: var(--lh-h3); font-size: var(--fs-h3); font-weight: 500; color: var(--ink); text-align: center; }
/* A clip fills the browser frame edge to edge — the padding exists to give a
   logo room to breathe, and a recording of the product needs no such help. */
.work-screen.has-video { padding: 0; }
.work-screen video { width: 100%; height: 100%; object-fit: cover; display: block; }
.work-panel { position: relative; }
.port-arrows { position: absolute; top: 0; height: 48px; right: calc(50% + 32px); display: flex; align-items: center; gap: var(--s-1); z-index: 3; }
.port-arrows .sl-arrow { width: 40px; height: 40px; }
.port-arrows .sl-arrow svg { width: 16px; height: 16px; }
.port-nav { display: flex; align-items: center; justify-content: center; margin-top: 48px; }
.work-dots i.on { background: var(--amber); width: 22px; border-radius: var(--r-pill); }
#services .wrap { max-width: 1440px; }
/* The plane hangs well below the panel and its flight path runs off the left edge.
   overflow:hidden would need ~300px of dead space under the section to avoid cutting
   it; overflow-x:clip contains the sideways overflow without becoming a scroll
   container, so the vertical overhang stays free and no horizontal scrollbar appears. */
#services { position: relative; overflow-x: clip; }
/* max-width:100% from base.css was capping this at the panel width (436px), so the
   graphic rendered at ~63% of its size */
.ay-plane { position: absolute; right: 199px; bottom: -288px; width: 690px; max-width: none; height: auto; z-index: 2; pointer-events: none; }
/* stacked, the panel is full width and anything hanging off its bottom lands on top
   of the toggle. Drop .ay-left out of the positioning chain so the plane anchors to
   the section instead and flies below the whole block. */
/* 940, not 980: between the two the grid was still two columns while the plane
   had already switched to its stacked position, and it landed on the copy */
@media (max-width: 940px) {
  #services { padding-bottom: 150px; }
  #services .ay-left { position: static; }
  .ay-plane { width: 380px; right: -70px; bottom: 20px; }
}
@media (max-width: 480px) {
  .ay-plane { width: 300px; right: -50px; bottom: 30px; }
}
.ay-left, .ay-right { position: relative; z-index: 1; }
/* minmax(0, …), not bare fr: an fr track cannot shrink below its own min-content,
   so the widest word inside the left column was setting its width. Once the CTA
   label started changing with the selected tab ("Start your project" vs "Talk to a
   creative partner"), that made the column — and with it the service list beside
   it, and the height of the whole section — move on every tab switch. The split is
   the layout's decision now, not the longest label's. */
.ay-grid { display: grid; grid-template-columns: minmax(0, 0.66fr) minmax(0, 1.34fr); gap: 64px; align-items: stretch; }
.ay-left { display: flex; flex-direction: column; background: var(--grad-card); border-radius: var(--r-lg); padding: 48px 48px 64px; }
.ay-left h2 { line-height: var(--lh-h2-bigger); font-size: var(--fs-h2-bigger); font-weight: 400; margin-bottom: 120px; }
.ay-left p { font-size: var(--fs-p-small); line-height: var(--lh-p); color: var(--text-dim); margin-top: auto; margin-bottom: 24px; max-width: 340px; }
.ay-left .btn { align-self: flex-start; }  /* don't stretch to the column width */
/* No top inset: the two columns start on the same line. The switcher is the first
   thing in the right column and the panel is the first thing in the left one, so
   64px here dropped the tabs below the panel's top edge and the section read as
   two blocks that had slipped past each other. */
.ay-right { padding-top: 0; }
.ay-switch { display: flex; align-items: center; gap: 16px; margin-bottom: 32px; flex-wrap: wrap; }
.ay-switch-label { font-size: var(--fs-p); color: var(--ink); }
.ay-right .toggle { margin: 0; }
/* the services tabs used to run 16px taller than the work tabs two sections above —
   §10 gives every instance the same internal padding and height, and the amber fill
   is the only active cue the component is allowed. What carries the state for a
   visitor who cannot separate amber from white is aria-selected, which home.js sets
   on every switch (DS §10 + §17). */
.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; }
.svc p { font-size: var(--fs-descriptor); line-height: var(--lh-descriptor); color: var(--text-dim); }
.svc-arrow { flex-shrink: 0; }
/* The spacer is a spacer, not a runway. It used to collapse its flex-grow on hover,
   which marched the arrow the full width of the row — up to 300px — towards the
   title. §14 asks for restrained, purposeful movement; a 300px slide across the card
   is the opposite, and it made the arrow race the cursor. The gap is fixed now and
   the arrow only turns, with 3px of travel to say "outbound". Same duration, same
   easing, same 45° turn — only the distance changed. */
.svc-gap { flex: 1 1 0; min-width: 12px; }
.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: translateX(3px) rotate(45deg); }
@media (prefers-reduced-motion: reduce) {
  .svc:not(.is-soon):hover .svc-arrow { transform: rotate(45deg); }
}
/* The three lists have different lengths (8 / 6 / 6), so display:none made the
   column — and with it the panel on the left — jump 46px on every tab switch.
   Stack them in one grid cell instead: the block is always as tall as the longest
   list and only the active one is painted. */
.svc-stack { display: grid; }
.svc-stack > .svc-grid { grid-area: 1 / 1; align-content: start; }   /* rows keep their natural height instead of stretching to fill the tallest tab */
.svc-grid[hidden] { display: grid; visibility: hidden; pointer-events: none; }
@media (max-width: 1280px) {
  .svc-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Clients — the frame's 6x4 logo grid (1317:384) with the middle 2x2 merged into
   the proof card. The stylesheet has carried these rules the whole time; the markup
   went missing before the repo had history, which left the page with the sentence
   as a one-line paragraph under the project carousel and no grid at all.
   The rules between cells are drawn with box-shadow rather than borders, so the
   last column and last row leave no stray line hanging off the edge — overflow
   clips the right-hand one and the 1px of padding keeps the bottom one. */
.clients { padding: 64px 0 96px; }
/* The grid has to line up with the nav bar, and it did not: this wrap was 1340 wide
   with 32px of its own padding, so the lattice drew at 1276 against the bar's 1340 —
   32px short on each side. .nav-inner is 1340 inside header.nav's 24px, so 1388 and
   24 here puts the two on the same edge at every width, not just at 1440. */
.clients .wrap { max-width: 1388px; padding-left: 24px; padding-right: 24px; }
/* The lattice ends flush against nothing, which reads as a cut. Fading it out at the
   four edges is the same move .ai-rule already makes on its hairlines. The fade is
   40px sideways and 20 top and bottom — enough to soften the line ends, short of
   the 24px the outer cells pad their logos by. */
.clients-grid { display: grid; grid-template-columns: repeat(6, 1fr); overflow: hidden; padding-bottom: 1px;
  --edge-x: 40px; --edge-y: 20px;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--edge-x), #000 calc(100% - var(--edge-x)), transparent 100%),
                      linear-gradient(180deg, transparent 0, #000 var(--edge-y), #000 calc(100% - var(--edge-y)), transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(90deg, transparent 0, #000 var(--edge-x), #000 calc(100% - var(--edge-x)), transparent 100%),
              linear-gradient(180deg, transparent 0, #000 var(--edge-y), #000 calc(100% - var(--edge-y)), transparent 100%);
  mask-composite: intersect; }
.client, .clients-cta { box-shadow: 1px 0 0 var(--card-border), 0 1px 0 var(--card-border); }
/* The frame's cell is 216.7 wide and its mark 178 of that (1315:1794), so the mark
   gets 20 either side and the rest of the cell. Ours had 24 of padding AND a further
   82% cap on top of it, which left the mark 140 — smaller than the same logo in the
   marquee at the top of the page. */
.client { display: grid; place-items: center; min-height: 106px; padding: var(--s-2) var(--s-3); }   /* 425/4 in the frame */
.client img { max-width: 100%; max-height: 60px; width: auto; height: auto; object-fit: contain; opacity: .82; transition: opacity var(--dur) ease, transform var(--dur) ease; }
.client:hover img { opacity: 1; transform: scale(1.05); }
.clients-cta { grid-column: 3 / span 2; grid-row: 2 / span 2; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px; padding: 32px; text-align: center; }
/* 24px, not the 32 of an h3: the frame sets this at 24 and it has two columns to
   live in, not a section's full measure */
.clients-cta p { font-size: var(--fs-h5); font-weight: 700; color: var(--ink); line-height: var(--lh-h5); max-width: 340px; }
.passion { text-align: center; }
.passion-title { line-height: var(--lh-h2); font-size: var(--fs-h2); font-weight: 400; color: var(--ink); margin-bottom: 16px; }
.passion-lottie { width: 340px; max-width: 78%; margin: 4px auto 16px; }
.passion p { font-size: var(--fs-p); color: var(--text-dim); max-width: 520px; margin: 0 auto 32px; }
/* The section carried its own blue-grey gradient, a 3px amber top rule and a row of
   dots borrowed from a slider that isn't there — three inventions the rest of the
   site does not use. It now reads like every other statement section: the canonical
   grey wash, the .overline kicker, the h2.bigger headline role, and the same
   outlined button the "Firstly, it's about you" block above it uses. */
.expect { position: relative; overflow: hidden; }
/* The default 1180 wrap started the copy 100px right of the portfolio above it and
   the bento below, so the section read as indented. 1380 on 32px padding is the
   content edge this page settles on — the same rule .cta-lightbulb and the bento
   carry, lining up with the nav card. */
.expect .wrap { max-width: 1388px; padding-left: 24px; padding-right: 24px; }   /* the nav's edge, like .clients and .bynum */
.expect-grid { display: grid; grid-template-columns: 1fr 1.05fr; gap: 64px; align-items: center; }
/* ── Expect · Access · Find ───────────────────────────────────────────────────
   The three statements staging runs as a carousel, and the only slider on the site
   that has to hand the last slide back to the first. It does not run on a track:
   the slides are stacked in a single grid cell and cross-dissolve in place, so
   3 → 1 is the same 400ms as 1 → 2 — there is no rewind to unwind and no offset to
   snap back, which is where a looping track shows its seam. The cell is as tall as
   the tallest slide, so the section never changes height either. */
/* The section clips, not the stack: clipping at .ex-stack meant the slide entered
   and left at the wrap edge, so only the middle column appeared to move while the
   section sat still around it. Clipping the section and travelling a full viewport
   width sends the whole thing off the page and brings the next one on.
   The travel is 100% of the slide, not 100vw: at a viewport's travel the two are
   more than a screen apart and the section is empty at the midpoint. At its own
   width the outgoing slide's trailing edge meets the incoming one's leading edge,
   so something is always on screen. */
/* Full-bleed, so a slide is as wide as the section it travels across. At column
   width the two are 1340 wide in a 1440 viewport, so the outgoing slide finished at
   -1290..50 and left its last 50px sitting in the gutter — the previous slide, still
   on screen. At viewport width there is no gutter for it to sit in. The copy keeps
   the column: each slide pads itself back to 1340, or to 24px when the page is
   narrower than that. */
.ex-stack { display: grid; width: 100vw; margin-left: calc(50% - 50vw); }
.ex-slide { padding-inline: max(24px, calc(50vw - 670px)); }
/* A slide, not a cross-dissolve. The stack is one grid cell, so the slides need a
   direction of their own: the incoming one comes in from the right, the outgoing
   one leaves to the left, and everything else waits off-stage right. home.js marks
   the one being replaced with .is-out for exactly the length of the move. */
.ex-slide { grid-area: 1 / 1; visibility: hidden; transform: translateX(100%);
  transition: transform var(--dur-slow) var(--ease), visibility 0s linear var(--dur-slow); }
.ex-slide.is-on { visibility: visible; transform: none;
  transition: transform var(--dur-slow) var(--ease), visibility 0s; }
.ex-slide.is-out { visibility: visible; transform: translateX(-100%);
  transition: transform var(--dur-slow) var(--ease), visibility 0s linear var(--dur-slow); }
/* The dots sit at the foot of the copy column, under the button — the design's
   arrangement (Figma 1317:405), not beside a kicker. An earlier pass took this
   section's shape from the staging carousel, which puts the word and the dots on
   one line and makes the word a 14px kicker; the approved frame does neither.
   --mid-neutral (#AAB2C0), not the frame's #9FAEC5: the two are a point apart and
   the first is the token §04 names. */
.ex-dots { display: flex; align-items: center; gap: 0; margin-top: 64px; margin-left: -6px; }
/* 12px of dot, as the frame draws it, but the control is bigger than the mark:
   content-box plus padding grows the hit area to 24 under a mouse and 44 under a
   finger (§17) while background-clip keeps the painted dot at 12. */
.ex-dot { width: 12px; height: 12px; box-sizing: content-box; padding: 6px; border: none; border-radius: 50%;
  background: var(--mid-neutral); background-clip: content-box; cursor: pointer;
  transition: background var(--dur) ease; }
/* background-color, not the background shorthand: the shorthand resets
   background-clip to border-box, so the active dot painted its whole 24px hit
   area amber instead of the 12px mark the frame draws. */
.ex-dot.on { background-color: var(--amber); }
/* The dwell, drawn. 6px on the section's own top edge, full-bleed past the wrap —
   it is the section that is advancing, not the column. It runs as an animation
   rather than a transition so a pointer entering the section can freeze it exactly
   where it stands with animation-play-state and resume from there; a transition
   would need its computed width read back and re-applied to do the same. */
.ex-prog { position: absolute; top: 0; left: 0; right: 0; height: 6px; overflow: hidden; z-index: 2; }
.ex-prog i { display: block; width: 100%; height: 100%; background: var(--amber);
  transform: scaleX(0); transform-origin: left center; }
.expect.is-running .ex-prog i { animation: ex-prog var(--ex-dwell) linear forwards; }
@keyframes ex-prog { from { transform: scaleX(0); } to { transform: scaleX(1); } }
/* Nothing advances on its own under reduced motion, so there is no progress to draw. */
@media (prefers-reduced-motion: reduce) {
  .expect.is-running .ex-prog i { animation: none; }
}
/* the two Lottie slides sit in the same slot the paper plane does, at the same cap.
   The box is reserved before the animation arrives so the stack does not resize
   when it does. */
/* All three slides carry the same artwork box in the frame — 358x410, the same node
   at the same size on Expect, Access and Find (1317:392, 1317:417, 1317:442). The
   Lottie slides were being forced square at 380, which drew them 30px shorter than
   the plane beside them and made the column change height and weight between slides.
   One box, one ratio, and the art fits inside it. */
.iso-anim { width: 100%; max-width: 358px; aspect-ratio: 358 / 410; }
.iso-anim svg { display: block; width: 100%; height: 100%; }
@media (prefers-reduced-motion: reduce) {
  /* No travel here, so .is-out cannot be "the one on its way out" — with transform
     none and visibility visible it sat squarely behind the incoming slide for as
     long as the class lived, which is the previous slide showing through. Reduced
     motion wants a cut: the outgoing one goes straight to hidden. */
  .ex-slide { transform: none; transition: none; }
  .ex-slide.is-out { transform: none; visibility: hidden; transition: none; }
}
@media (hover: none) and (pointer: coarse) {
  .ex-dots { gap: 0; }
  .ex-dot { padding: 16px; }   /* 12 + 32 = 44 square, not 44 tall and 24 wide */
}
/* Expect / Access / Find is the headline here and the statement under it is the
   28px role — the hierarchy the approved frame draws (1317:412 is 48, 1317:413 is
   28). The build had these the other way round, with the word as a kicker.
   Weight comes from §05's scale, not the frame: the frame draws the 28 at 400 and
   the document gives that role 500. Gaps are the frame's, and all land on §07's
   scale already — 64 under the word, 8 under the statement, 64 under the copy. */
.expect h2 { line-height: var(--lh-h2-bigger); font-size: var(--fs-h2-bigger);
  font-weight: var(--fw-h2-bigger); margin-bottom: 64px; }
.expect h3 { line-height: var(--lh-h4); font-size: var(--fs-h4); font-weight: 400;
  color: var(--ink); margin-bottom: 8px; }
/* body copy (18/1.7), the frame's size — the 20px subtitle role was a size up */
.expect p { font-size: var(--fs-p); line-height: var(--lh-p); color: var(--text-dim);
  max-width: 610px; margin-bottom: 64px; }
/* the Figma brand element (paper plane, flight path, unfolded sheet) — this slot
   used to hold two skewed CSS cards standing in for it. Capped at 380: at its full
   451 it outweighed the headline it is supposed to sit beside. */
/* pushed to the far edge of its column, not centred in it — the gap between the copy
   and the plane is what makes the two columns read as one composition */
.iso { position: relative; display: flex; justify-content: flex-end; }
/* the plane's own file is 451x501, a slightly different ratio from the frame's box,
   so it is contained inside the box rather than setting it */
.iso img { width: 100%; max-width: 358px; aspect-ratio: 358 / 410; height: auto; object-fit: contain; object-position: right center; display: block; }
/* the three card sections share the page's wide content edge — the blog was still
   on the default 1180 wrap, which started it 100px right of the reviews above it */
#reviews .wrap, #blog .wrap { max-width: 1340px; padding-left: 32px; padding-right: 32px; }
/* The frame's card row is 1236 wide (1317:627) — narrower than the projects and
   client bands above it, which is deliberate. Getting there means the wrap has to
   carry the gutters as well: 1236 of cards plus 2x72 of control gutter plus the
   wrap's own 2x32 of padding. The row lands on the frame's 396-per-card and the
   controls still shrink inwards with the viewport instead of running off it. */
#reviews .wrap { max-width: 1444px; }
/* The arrows sit beside the cards, not on them. In the frame the card row runs
   102 -> 1338 and the two controls are at x=46 and x=1362 (1317:718, 1317:715), so
   each clears the cards by 24px. They had been hung 22px INTO the row, which put a
   48px disc over the first card's stars and the last card's edge.
   72px of gutter is the whole of it: 48 of control plus that 24 of clearance. At
   1440 it leaves the row exactly the frame's 1236. */
.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); }
.sl-arrow.ti-arrow:disabled { transform: translateY(-50%); }  /* keep vertical centring even when disabled (overrides the shared .sl-arrow:disabled transform:none) */
.ti-prev { left: 0; }
.ti-next { right: 0; }
/* The review card itself moved to base.css — Work Master (1432:4568) draws the
   same component, and work.css never loads this sheet. Only the slider mechanics
   above are still the homepage's own. */
/* Footer CTA — "Got an idea? Let's make it real." The section itself now lives in
   form.css, which every family that carries a contact form loads; only the
   lightbulb's own animation is left here. */
/* only the filament fuses animate (subtle), like staging — the rays stay static */
@keyframes lightbulb-fuse { 0% { stroke-dasharray: 12 12; } 100% { stroke-dasharray: 12 32; } }
@media (max-width: 680px) {
  /* align-items:flex-start let the marquee size to its 6500px track instead of the
     row width, so the logo strip ran off the page */
  .trust-row { flex-direction: column; gap: 16px; align-items: stretch; padding-left: 0; }
  .trust-label { align-self: center; }   /* label centred over the logo strip, matching the rest of the stacked hero */
}
@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 { display: flex; align-items: center; justify-content: center; gap: 8px; }
.port-dots i { width: 8px; height: 8px; border-radius: 50%; background: var(--card-border); cursor: pointer; transition: background var(--dur) ease, border-color var(--dur) ease, color var(--dur) ease, width var(--dur) ease, border-radius var(--dur) ease; }
/* background-color, not the background shorthand: on a touch device .port-dots i
   takes 16px of invisible tap padding held off the paint by background-clip:
   content-box, and a shorthand here resets that clip to border-box — which painted
   the active dot's whole 38x40 target solid amber. Longhand leaves the clip alone. */
.port-dots i.on { background-color: 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; }
.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%; }
}
@media (max-width: 940px) {
  .clients-grid { grid-template-columns: repeat(3, 1fr); }
  .clients-cta { grid-column: 1 / -1; grid-row: auto; }
  .client { min-height: 116px; }
  /* minmax(0,1fr), not 1fr: an auto minimum lets a wide child (the pill toggle)
     stretch the track past the viewport instead of wrapping */
  .work-split, .ay-grid, .expect-grid { grid-template-columns: minmax(0, 1fr); gap: 32px; }
  /* The frame's 64px rhythm is drawn against a 48px headline in a 607px column.
     Stacked on a phone the headline is 38-42 and the column is the screen, and the
     same 64 leaves the word marooned above its own sentence. One step down the
     scale (§07) keeps the grouping the frame intends. */
  .expect h2 { margin-bottom: 32px; }
  .expect p { margin-bottom: 32px; }
  .ex-dots { margin-top: 32px; }
  /* the column divider only makes sense while the split is actually two columns —
     stacked, it draws a stray line straight through the copy */
  .work-split::after { display: none; }
  /* the arrows are parked left of the divider on desktop; stacked, that lands them
     on top of the project label. Move them above the card, like the blog slider. */
  .work-panel { display: flex; flex-direction: column; }
  .port-arrows { position: static; order: -1; height: auto; justify-content: flex-end; margin-bottom: 16px; }
  .port-arrows .sl-arrow { width: 48px; height: 48px; }
  .port-arrows .sl-arrow svg { width: 16px; height: 16px; }
  /* wrapping is layout; the radius is the component's and stays the component's */
  .toggle { flex-wrap: wrap; justify-content: center; }
  .ay-left { padding: 48px 24px 48px; }
  .ay-left h2 { margin-bottom: 24px; }
  .ay-right { padding-top: 0; }
}
@media (max-width: 680px) {
  .clients-grid { grid-template-columns: repeat(2, 1fr); }
  /* The paper plane is decoration, and stacked it costs a phone a screenful of empty
     space under the panel — 150px of padding reserved for it, plus its own overhang.
     Both go on mobile. */
  .ay-plane { display: none; }
  #services { padding-bottom: 0; }
  /* Four logos above the CTA and four below it, two rows of two each. The other
     twelve are not dropped — home.js cycles them through these eight tiles. The grid
     has 21 children: 1-8 are logos, 9 is the CTA, 10-21 are the rest. */
  .clients-grid .client:nth-child(n+5):nth-child(-n+8),
  .clients-grid .client:nth-child(n+14) { display: none; }
  .client img { transition: opacity var(--dur) ease; }
  /* "View case study" sat flush against .work-desc — margin-top: auto resolves to 0
     once the card is content-height, which it is on a phone — and it was a full .btn
     at 16px/48px next to the sticky bar's 14px/44px, so it read as the louder of the
     two. 24px of air, and the DS's own small-with-arrow geometry, which lands it at
     exactly the sticky button's height and type size. */
  .work-split .work-cta.btn { margin-top: var(--s-3); min-height: 44px;
    font-size: var(--fs-btn-sm); padding: 13px 40px 13px var(--s-2); }
  .hero { padding: 120px 0 48px; }
  /* home.css is enqueued after shell.css, so its unscoped `.svc-grid { 1fr 1fr }`
     was beating shell's mobile rule and the services list stayed two-up */
  .svc-grid, .bynum-grid { grid-template-columns: minmax(0, 1fr); }
  /* one card fills the screen, so the -22px overhang would sit on top of the quote.
     Lift the arrows out of the card into a row above it, like the blog slider. */
  .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; }
}
/* 1388 and 24, which is .nav-inner's 1340 plus header.nav's padding — 1340 with
   32px inside it drew 1276, the same 64px short the client grid was. */
.wrap:has(.bynum-grid) { max-width: 1388px; padding-left: 24px; padding-right: 24px; }
.bynum-grid { display: grid; grid-template-columns: 1fr 1.05fr; gap: 64px; align-items: center; }
.bynum-title { font-size: var(--fs-h3); font-weight: 500; line-height: var(--lh-h3); color: var(--ink); margin-bottom: 24px; }
.bynum-title .am { color: var(--amber-text); }
.bynum-text { font-size: var(--fs-p); color: var(--text-dim); }
/* The three tracks exist so the two rows can mirror each other: the wide card is
   cols 1-2 on top and cols 2-3 underneath, which needs the outer tracks equal and
   the middle one narrow. 1.5 : 1 : 1.5 puts the cards on the frame's own split
   (1317:736 is 386 wide beside 1317:745 at 222); the previous 1.25 : .9 : .9 drew
   the Clutch card 43px narrower than that, and the review chip inside it had
   nowhere to sit — the label wrapped and the arrow hung outside the pill. */
.bynum-bento { display: grid; grid-template-columns: 1.5fr 1fr 1.5fr; grid-auto-rows: 1fr; gap: 16px; grid-template-areas: "big big clutch" "exp weeks weeks"; }
.bnum-card { position: relative; background: var(--off-white); border: 1px solid transparent; border-radius: var(--r-lg); padding: 24px 32px; display: flex; flex-direction: column; justify-content: center; }
/* 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. */
.bnum-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; }

.bnum-card.big { grid-area: big; }
/* Three of these four cards are a number and a label — nothing to click, nowhere to
   go. They used to lift and cast a shadow under the cursor anyway, which is the
   affordance §13 reserves for a surface that does something. Only the Clutch card
   holds a link, so only the Clutch card answers the pointer. */
.bnum-card.clutch { grid-area: clutch; justify-content: center; align-items: flex-start; gap: 2px;
  transition: box-shadow var(--dur) ease; }
.bnum-card.clutch:hover { box-shadow: var(--shadow); }
.bnum-card.exp { grid-area: exp; }
.bnum-card.weeks { grid-area: weeks; }
.bnum-num { font-family: var(--ff); font-size: var(--fs-h2-bigger); font-weight: 500; color: var(--ink); line-height: 1; }
.bnum-lbl { font-size: var(--fs-p-small); color: var(--text-dim); margin-top: 8px; }
.bnum-rating { display: flex; align-items: center; gap: 7px; }
.bnum-rating b { font-family: var(--ff); line-height: var(--lh-h2); font-size: var(--fs-h2); font-weight: 500; color: var(--ink); }
.bnum-rating svg { color: var(--clutch-red); }   /* Clutch's star is Clutch's red (Figma 1317:764), not our amber */
.bnum-clutch-name { font-family: var(--ff); font-weight: 600; line-height: var(--lh-h5); font-size: var(--fs-h5); color: var(--ink); margin: 2px 0 16px; }
/* 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. */
/* Clutch's action, in Clutch's own clothes — the design's review chip (Figma
   1317:749), not a member of our button family at all. That is the point: §09's
   family is how Inventiff asks for a conversion, and an outbound link to a review
   site must not borrow it. So this one steps out of the family deliberately —
   white pill, soft navy shadow, no border, 13px sentence-case label and the Clutch
   wordmark we already ship doing the identifying. It ranks visibly below "Get on
   board" while still reading as a control.
   Earlier passes had this as an amber Primary, then as a red-outlined member of the
   family; both were still our button wearing a costume. This is Clutch's.
   The label is navy: --clutch-red is 3.8:1 on white and never sets type here — the
   only red is the dot inside the wordmark, which is artwork, not a label. */
.clutch-cta { display: inline-flex; align-items: center; gap: 8px; font-family: var(--ff);
  font-size: var(--fs-btn-sm); font-weight: 400; line-height: 1; color: var(--navy); text-decoration: none;
  background: var(--white); border-radius: var(--r-pill); padding: 13px 12px; white-space: nowrap;
  box-shadow: 0 2px 8px rgba(4, 17, 58, 0.08);
  transition: box-shadow var(--dur) ease, transform var(--dur) ease; }
.clutch-cta img { display: block; flex-shrink: 0; }   /* a squashed Clutch logo is worse than a wrapped one */
.clutch-cta .cc-ar { width: 10px; height: 10px; flex-shrink: 0; }
/* Subtle, as §14 asks and the brief repeats: the chip lifts a little and its shadow
   deepens. No colour change, no morph, no arrow march — those belong to our family. */
.clutch-cta:hover { box-shadow: 0 4px 14px rgba(4, 17, 58, 0.14); transform: translateY(-1px); }
.clutch-cta:active { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .clutch-cta:hover { transform: none; }
}
/* 40px of box under a mouse, 44 under a finger (§17) — the chip is 36px drawn, so
   the target is grown with padding, not with width/height, which is the mistake the
   footer social marks made. */
@media (hover: none) and (pointer: coarse) {
  .clutch-cta { padding-top: 17px; padding-bottom: 17px; }
}
/* 1366, not 940: the bento is drawn at 629px wide and the review chip inside the
   Clutch card needs about 150 of the 222 that card gets there. Two columns hold
   that down to roughly 1330 and then start squeezing the chip — at 1100 the card
   is 175 wide and there is no type size that rescues it. So the section stacks
   while the bento can still be its designed size, and the bento keeps that size
   rather than stretching to the full measure once it is on its own row. */
@media (max-width: 1366px) {
  .bynum-grid { grid-template-columns: minmax(0, 1fr); gap: 32px; }
  .bynum-bento { max-width: 860px; }
  .bynum-text { max-width: 720px; }
}
@media (max-width: 680px) {
  .bynum-bento { grid-template-columns: 1fr 1fr; grid-template-areas: "big big" "clutch clutch" "exp weeks"; }
}
@media (max-width: 480px) { /* two 128px cards with 56px of padding leave nothing for the number */
  .bynum-bento { grid-template-columns: minmax(0, 1fr); grid-template-areas: "big" "clutch" "exp" "weeks"; }
  .bnum-card { padding: 24px; }
}

/* Touch targets: these controls draw at the size the design file specifies,
   which is comfortable under a mouse and short of the 44px a finger needs.
   Adding a minimum height leaves the padding and the look untouched. */
@media (hover: none) and (pointer: coarse) {
  /* the dots are clickable, and 8px is a quarter of what a finger needs. The dot
     keeps its size; the padding around it becomes the target. */
  .port-dots { gap: 0; }
  .port-dots i { padding: 16px 8px; background-clip: content-box; box-sizing: content-box; }
}

/* .work-proof is gone with the paragraph it styled. An earlier pass had cut the
   client grid as "a second copy of the logos" and kept only its sentence, on the
   grounds that the marquee under the hero already shows them — but the frame runs
   both: the marquee is the "Trusted by:" strip at y=1262 and the grid is its own
   section at y=3596. The sentence lives in the grid's centre card again. */

/* MB-07: a clamped review with no way to open it left the visitor a single exit —
   Clutch. The clamp stays (nine full reviews would be a wall of text), the way out
   of it is now on the page. */
.testi .body.is-open { -webkit-line-clamp: unset; }
.testi-more { margin-top: 8px; background: none; border: none; padding: 4px 0; cursor: pointer;
  font-family: var(--ff); font-size: var(--fs-p-small); font-weight: 600; color: var(--ink);
  text-decoration: underline; text-underline-offset: 3px; }
.testi-more:hover { text-decoration-color: var(--amber); }
/* .testi-more is built by home.js, not by the markup, and .ti-more is the static
   link beside it — two different controls. Both are raised in base.css so every
   family that ships a review card gets them, not just this one. */

/* RS-03 is answered by the gutter itself now: the controls live inside the slider's
   own padding, so they can no longer reach past the wrap however narrow it gets, and
   below 680 they move to a row of their own. The gutter stays 72 the whole way: at 56
   a 48px control left four pixels of daylight, which is not a gap, it is a near miss. */

/* the portfolio's prev/next are 40px of artwork; on a finger they need 44 */
@media (hover: none) and (pointer: coarse) {
  .port-arrows .sl-arrow { width: 44px; height: 44px; }
}
