/* Shared type, spacing and motion tokens. */
:root {
  --ink: #161513;
  --body: #444;
  --muted: #666;
  --line: #b8b6b2;
  --hair: #ececec;
  --surface-rest: #fbfaf8;
  --paper: #f4f2ed;
  --surface-hover: #eceae3;
  --green: #8aa03d;
  --amber: #deb624;
  --hover: #222;
  --serif: "Source Serif 4", Georgia, serif;
  --sans: "Source Sans 3", "Source Sans Pro", sans-serif;
  --mono: "JetBrains Mono", monospace;
  --number: var(--serif);
  --arrow-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1 6h10M6 1l5 5-5 5' fill='none' stroke='black' stroke-width='2'/%3E%3C/svg%3E");
  --ease: cubic-bezier(.22, 1, .36, 1);
  --ease-exit: cubic-bezier(.4, 0, 1, 1);
  --enter: 480ms;
  --exit: 320ms;
  --feedback: 180ms;
  --service-enter: 1400ms;
  --service-exit: 300ms;
  --figure-size: 36px;
  --radius: 16px;
  --gutter: 6vw;
  --grid-gap: 3vw;
}
* { box-sizing: border-box; }
/* The white belongs to the root, never to the body: the cursor ground sits under in-flow content at
   z-index -1, and a body background would paint over it everywhere but the collapsed top margin. */
html { background: #fff; }
html, body { margin: 0; padding: 0; }
html { scrollbar-color: var(--green) #fff; scrollbar-width: thin; }
body { font: 400 17px/1.6 var(--sans); color: var(--body); -webkit-font-smoothing: antialiased; overflow-x: clip; }
a { color: inherit; text-decoration: none; transition: color var(--feedback); }
a:not(.logo):not(.btn):not(.contact), a.contact .contact-v { text-decoration: underline; text-decoration-color: var(--green); text-decoration-thickness: 1px; text-underline-offset: 4px; transition: color var(--feedback), text-decoration-color var(--feedback); }
a:not(.logo):not(.btn):is(:hover, :focus-visible), a.contact:is(:hover, :focus-visible) .contact-v { color: var(--green); text-decoration-color: var(--green); }
button, input, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
button, a, input, textarea, [tabindex="0"] { -webkit-tap-highlight-color: transparent; }
:focus-visible { outline: 2px solid var(--green); outline-offset: 6px; }
p { margin: 0; }
h1, h2, h3 { color: var(--ink); margin: 0; font-family: var(--serif); font-weight: 600; letter-spacing: -.02em; }
[data-h]:focus, [data-subh]:focus { outline: none; }
::selection { background: var(--muted); color: #fff; }
[data-spacer] { width: 100%; }

@keyframes chrome-enter {
  from { opacity: 0; transform: translateY(-12px); }
  to { opacity: 1; transform: none; }
}
.chrome { position: fixed; top: 24px; z-index: 20; }
.logo { color: var(--ink); left: var(--gutter); display: flex; align-items: center; gap: 18px; animation: chrome-enter 650ms var(--ease) both; }
/* The box is the tulip alone and the ® overhangs it (see `motion.js`): the image's padding is
   transparent, so nothing is clipped. `--logo-img` repeats `motion.js`'s sizing, the heading's
   font-size × 0.64 / 0.59, so the first paint already has the final logo and the deferred script
   finds nothing to change (Jan, 2026-09-23: a fixed 68 px logo painted first, then jumped). The
   blog button's top is the same estimate of the baseline match `motion.js` then makes exact. */
:root { --logo-img: calc(clamp(44px, 6.4vw, 96px) * 64 / 59); }
.logo-box { position: relative; width: calc(var(--logo-img) * .74); height: calc(var(--logo-img) * .542); }
.logo-box img { position: absolute; display: block; width: var(--logo-img); height: var(--logo-img); left: calc(var(--logo-img) * -.13); top: calc(var(--logo-img) * -.229); }
.logo-name { font: 600 20px/1 var(--serif); letter-spacing: -.01em; white-space: nowrap; }
.blog { top: max(24px, calc(7px + var(--logo-img) * .271)); right: var(--gutter); display: flex; flex-direction: column; align-items: flex-end; gap: 28px; animation: chrome-enter 650ms var(--ease) 120ms both; }
.hint { position: fixed; right: var(--gutter); bottom: 6vh; z-index: 20; display: flex; align-items: center; gap: 12px; }
.hint i { display: block; width: 40px; height: 1px; background: var(--ink); transform-origin: right; animation: chrome-enter 600ms var(--ease) 500ms both; }

.btn { --fill-color: var(--hover); --arrow-color: #fff; display: inline-flex; align-items: center; justify-content: center; gap: 12px; border-radius: 999px; padding: 11px 20px 10px; font-size: 12px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; border: 1px solid var(--green); background: var(--green); color: #fff; transition: background var(--feedback), border-color var(--feedback), color var(--feedback), transform 160ms var(--ease); }
.btn:is(:hover, :focus-visible) { border-color: var(--hover); color: #fff; }
/* Press feedback is its own budget, shorter than the colour it travels with. */
.btn:active { transform: scale(.97); }
/* The outline pill stands on white, never on nothing (Jan, 2026-09-07): the contour ground is a fixed
   canvas at z-index -1, so a transparent fill let the lines run straight through the button and it read
   as see-through. Written as the literal the root ground uses rather than as a surface token, because
   matching that ground is the whole point and a token that moves would reopen the leak. */
.btn.outline { --arrow-color: var(--muted); background: #fff; color: var(--ink); border-color: var(--ink); }
.btn.neutral { --arrow-color: var(--muted); background: var(--line); border-color: var(--line); color: var(--ink); }
/* The black fill takes the whole foreground with it, arrow included. */
.btn:is(.outline, .neutral):is(:hover, :focus-visible) { --arrow-color: #fff; border-color: var(--hover); color: #fff; }
/* Every arrow uses the same stroke; external destinations rotate it upward. */
.ar { --arrow-angle: 0deg; vertical-align: -1px; position: relative; display: inline-block; width: 12px; height: 12px; flex-shrink: 0; overflow: hidden; font-size: 0; font-style: normal; line-height: 1; letter-spacing: 0; color: var(--arrow-color, var(--muted)); transition: color var(--feedback), transform 240ms var(--ease); }
.ar::before, .ar::after { content: ''; position: absolute; inset: 0; background: currentColor; mask: var(--arrow-mask) center / contain no-repeat; transform: rotate(var(--arrow-angle)); }
.ar::after { opacity: 0; }
.ar-external { --arrow-angle: -45deg; --flight-y: -14px; }
.ar-up { --arrow-angle: -90deg; }
@keyframes arrow-out { to { translate: var(--flight-x, 14px) var(--flight-y, 0px); opacity: 0; } }
@keyframes arrow-in { from { translate: calc(-1 * var(--flight-x, 14px)) calc(-1 * var(--flight-y, 0px)); opacity: 0; } to { translate: 0 0; opacity: 1; } }
:is(.btn, a, .nav-btn:enabled):is(:hover, :focus-visible) .ar::before { animation: arrow-out 220ms var(--ease) forwards; }
:is(.btn, a, .nav-btn:enabled):is(:hover, :focus-visible) .ar::after { animation: arrow-in 300ms var(--ease) 160ms both; }
/* A tap on a touch screen fires a hover the finger never asked for, and it sticks there until
   something else is tapped, so the flight plays and the button is left mid-gesture. Withdraw it from
   pointers that cannot hover; keyboard focus is a real intent and keeps it. */
@media (hover: none), (pointer: coarse) {
  :is(.btn, a, .nav-btn:enabled):hover:not(:focus-visible) .ar::before,
  :is(.btn, a, .nav-btn:enabled):hover:not(:focus-visible) .ar::after { animation: none; }
}
/* One fill treatment for cards, panels, countdown tiles and buttons. */
.soft-fill { position: relative; isolation: isolate; }
.soft-fill::before { content: ''; position: absolute; inset: 0; z-index: -1; border-radius: inherit; pointer-events: none; background: var(--fill-color, var(--surface-hover)); clip-path: circle(0 at var(--pointer-x, 50%) var(--pointer-y, 50%)); transition: clip-path 520ms var(--ease); }
/* A region answers the pointer crossing its own edge, and only that. The slow fill follows the
   pointer into the form and stays for as long as it rests there, whether or not a field inside is
   being typed in; something coming alive inside never changes the ground on its own, so tabbing in
   from the keyboard leaves it exactly where it was (Jan, 2026-09-07). */
.soft-fill:is(:hover, :focus-visible, .is-pointer-fill)::before, .hl:focus-visible .soft-fill::before { clip-path: circle(145% at var(--pointer-x, 50%) var(--pointer-y, 50%)); }
.soft-fill.is-pointer-leaving::before { transition: none; }

.label { font: 12px/1.5 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.badges { display: flex; justify-content: flex-end; gap: 56px; }
/* Both badge groups share one slot below the button; only one is ever on screen. */
.badge-window { display: grid; }
.badge-window > * { grid-area: 1 / 1; }
.badge { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
.badge .label { text-align: right; }
/* Badge values and timeline years share type, with separate size and colour. */
.badge-v, .year { font-family: var(--number); font-size: var(--figure-size); font-weight: 600; font-optical-sizing: none; font-variation-settings: "opsz" 40; font-variant-numeric: lining-nums tabular-nums; line-height: 1; letter-spacing: -.02em; }
.badge-v { color: var(--ink); font-size: clamp(44px, 3.6vw, 56px); }
.year { color: var(--line); }
.year.today { color: var(--ink); }
.badge-v [data-count] { display: inline-block; min-width: 2ch; text-align: right; }

.h { position: fixed; left: var(--gutter); top: 0; z-index: 8; opacity: 0; font-size: clamp(44px, 6.4vw, 96px); line-height: 1; white-space: nowrap; max-width: 88vw; }
/* The scrolling build re-transforms these on every frame, so say so once rather than letting the
   browser promote and demote each of them as the scroll starts and stops. Reading flow and the
   no-script fallback leave them where they stand and pay nothing for the hint. */
.story :is(.h, .c, .subhead, [data-badges] > *) { will-change: transform; }
.story .row { will-change: translate; }
/* `.subsection-title` is the comparison page's heading role; the homepage uses `.subhead`. */
.subhead, .subsection-title { color: var(--line); font: 400 clamp(20px, 1.8vw, 26px)/1.15 var(--sans); letter-spacing: .04em; text-transform: uppercase; }
.subhead { position: fixed; left: var(--gutter); top: 0; z-index: 8; overflow: clip; padding-bottom: .15em; }
/* Waiting out of sight is a state of its own, not a side effect of the clip. A subheading that has
   not been asked for yet is unpaintable in any engine, the way `.h` is by its own `opacity: 0` —
   relying on `overflow: clip` alone put the opening statement on screen before the heading had
   started moving (Jan, 2026-09-12). The roll itself is scroll-driven like every other travel here,
   so the span carries no transition: it rolls down over its lead-in and back up over the scene's
   departure, and reading upward runs that backwards. */
.subhead > span { display: block; transform: translateY(-120%); visibility: hidden; }
.subhead.on > span { transform: none; visibility: visible; }
.c { position: fixed; left: var(--gutter); top: 0; z-index: 9; width: 88vw; opacity: 0; }
.grid { display: grid; gap: var(--grid-gap); align-items: start; }
.cols-4 { grid-template-columns: 1fr 1.6fr 1fr 1fr; }
.cols-4-even { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.stack { display: flex; flex-direction: column; }
.slot { display: grid; align-items: start; }
.slot > * { grid-area: 1 / 1; min-width: 0; }
.gap-lg { gap: 28px; }
.intro-content { gap: 56px; }
/* Row two keeps the row-one column width and starts at the middle of its second column. */
.work-content { gap: 52px; --work-col: calc((100% - 3 * var(--grid-gap)) / 4); }
.work-item { gap: 16px; }
.work-offset { grid-template-columns: calc(var(--work-col) * 1.5) repeat(2, minmax(0, var(--work-col))); }
.work-offset > :first-child { grid-column: 2; }
.intro-outside { gap: 28px; align-items: flex-start; }
.intro-cta { display: flex; }
/* Repository pills sit inside the Open source reveal; a long name wraps rather than leaving the column. */
.work-cta { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px; }
.work-cta .btn { max-width: 100%; white-space: normal; text-align: center; }
.contact-content { gap: 6vw; }

/* A travelling region carries no transition of its own: `motion.js` writes where it stands on every
   frame, from the scroll position and nothing else, so the way back up is the way down reversed
   (Jan, 2026-09-20). The declarations here are only where a region waits before the first frame is
   painted and where reading flow, the no-script fallback and print leave it. `.on` is reading flow's
   word for a reveal being up; the scrolling build writes inline styles, which outrank it. */
.r { opacity: 0; transform: translateX(60vw); pointer-events: none; }
.r.from-left { transform: translateX(-60vw); }
.r.from-bottom, .r.from-bottom-back { transform: translateY(60vh); }
.r.lr { transform: translateX(-60vw); }
.r.from-top { transform: translateY(-40vh); }
.r.on { opacity: 1; transform: none; pointer-events: auto; }

.photo { width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: contain; display: block; border-radius: var(--radius); background: var(--surface-hover); filter: grayscale(1) contrast(1.02); }
[data-panel].r { opacity: 1; visibility: hidden; transform: translateY(-40px); transition: none; }
/* The offer shows one square card at a time on the left (mini summary, big name, lead) with the chosen
   service's copy beside it in narrow columns. A prev and next pair under the card names the neighbouring
   service beside a plain arrow; both are always present and the edge one is disabled. */
.offer-content { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 72px; align-items: start; }
.service-menu { width: min(340px, 100%); min-width: 0; }
.service-tabs { display: grid; }
.service-tabs > .hl { grid-area: 1 / 1; }
.hl { border: 0; padding: 1px 0 0; background: none; text-align: left; min-width: 0; }
.hl.active { position: relative; z-index: 1; }
.hl:not(.active) { pointer-events: none; }
/* Everything in the offer moves vertically and one thing at a time (Jan, 2026-09-21): the card drops
   in from the top with the menu and its copy follows; between services the card stands still while
   the copy goes up, its face changes, and the next copy drops in; the offer leaves copy first, then
   the card. All of it is placed by the scroll position — the offer step the reader is on —
   so a replacement waits the one it replaces out by arriving later in that step rather than by a
   latched delay, and the whole swap runs backwards when the reader does. Every service card wears the
   same fill, so the standing card never changes colour as one service hands over to the next. The
   transition here is the hover fill only. */
.card { background: var(--surface-rest); border-radius: var(--radius); padding: 28px; aspect-ratio: 1 / 1; display: flex; flex-direction: column; gap: 10px; transition: background var(--feedback); }
.service-tabs .card { background: var(--paper); }
.hl:not(.active) .card { visibility: hidden; }
.card .service-name { font-size: clamp(36px, 3vw, 44px); }
/* The mini summary hangs off the card's right edge, always over two lines. */
.card-note { text-align: right; }
.card-lead { margin-top: auto; }
.service-nav { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-top: 14px; }
.nav-btn { border: 0; border-radius: 0; background: none; padding: 7px 0 6px; display: inline-flex; align-items: center; gap: 10px; font: 12px/1.5 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--ink); cursor: pointer; transition: color var(--feedback), opacity var(--feedback); }
.nav-btn:disabled { color: var(--muted); opacity: .4; cursor: default; }
.ar-back { --arrow-angle: 180deg; --flight-x: -14px; }
.service-details { min-width: 0; overflow: clip; }
.service-copy { display: grid; grid-template-columns: repeat(2, minmax(0, 300px)); gap: 40px; }
.service-copy > .grid { display: contents; }
/* A service may carry a third column level with the other two; Design uses it for how Jan designs. Below 1100 px the third drops under the two. */
.service-copy.three { grid-template-columns: repeat(3, minmax(0, 240px)); gap: 32px; }
.service-copy .label { display: block; margin-bottom: 12px; }
.h3 { color: var(--ink); font-family: var(--serif); font-weight: 500; font-size: 26px; line-height: 1.15; letter-spacing: -.01em; }

.timeline-content { width: 94vw; }
/* A hundred pixels to the year, measured from 2000, so a column is only as wide as the run to its
   next same-side point. The markup carries the years and nothing else: `motion.js` derives every
   pixel of the axis from them and writes --point-x here, along with the span and the width below.
   The declared values are what the rail measures today, so it still draws if the script never runs. */
.row { --rail-y: 190px; --rail-span: 2700px; --rail-width: 2880px; position: relative; width: var(--rail-width); height: 380px; }
.row-line { position: absolute; left: 0; top: var(--rail-y); height: 1px; width: var(--rail-span); background: var(--line); }
/* A point enters from beyond the right edge, the distance every other reveal on the page travels, and
   settles on the rail as it crosses in (Jan, 2026-09-08: with an 80 px nudge the points stood on the
   rail as one static block; B in timeline-options.html). A point on screen when the rail arrives
   crosses in on its own scroll window, after the one before (Jan, 2026-10-01); a later one as the row
   carries its place in. The scroll position places it either way and it retreats through the same
   edge on the way back — no transition, like every other travel here. */
.pt { position: absolute; left: var(--point-x, 0px); top: 0; height: 100%; width: 280px; opacity: 0; transform: translateX(60vw); }
.pt.on { opacity: 1; transform: none; }
.dot { position: absolute; left: 0; top: var(--rail-y); width: 12px; height: 12px; border-radius: 50%; background: var(--ink); box-shadow: 0 0 0 2px #fff; transform: translate(-50%, -50%); }
.dot.today { width: 16px; height: 16px; background: var(--green); }
/* Lining figures carry a left side bearing the dot does not; the whole block moves onto it. */
.pt-text { position: absolute; left: -6px; width: 100%; }
.pt-text.up { top: var(--rail-y); transform: translateY(calc(-100% - 24px)); }
.pt-text.down { top: calc(var(--rail-y) + 24px); }
.pt p { margin-top: 10px; }
/* The closing invitation waits above its own clipped window until Today clears the rail's right side.
   It opens on the content's two-thirds line and stays a fifth of the screen wide, so it stops short
   of the content edge; the row it sits in is a gutter wider than the content, so that line is measured
   from the viewport rather than from the row. */
.timeline-aside { position: absolute; left: calc((100vw - 2 * var(--gutter)) * 2 / 3); top: 0; width: 20vw; overflow: clip; padding-bottom: 6px; }
.timeline-aside-body { display: flex; flex-direction: column; align-items: flex-start; gap: 18px; transform: translateY(calc(-100% - 10px)); }
.timeline-aside.on .timeline-aside-body { transform: none; }

.contact-form { margin: 0; min-width: 0; }
.form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; background: var(--paper); border-radius: var(--radius); padding: 32px; color: var(--body); }
.field { display: grid; gap: 6px; }
.field-wide { grid-column: 1 / -1; }
.field-label { color: var(--body); font-weight: 500; }
.required-mark { margin-left: 3px; }
.field input, .field textarea { width: 100%; min-width: 0; border: 0; border-bottom: 1px solid var(--line); background: transparent; padding: 8px 0; border-radius: 0; resize: vertical; color: var(--ink); transition: border-color var(--feedback); }
.field input:hover, .field textarea:hover { border-bottom-color: var(--body); }
.field input:focus, .field textarea:focus { outline: none; border-bottom: 2px solid var(--body); padding-bottom: 7px; }
.field-error { --arrow-color: var(--amber); display: flex; align-items: center; gap: 10px; color: var(--amber); font-size: 16px; line-height: 1.4; }
.field-error[hidden] { display: none; }
.field-error .ar { width: 14px; height: 14px; }
.form-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 20px; padding-top: 20px; }
.form-actions .btn { flex-shrink: 0; min-width: 112px; }
.form-note { flex: 1 1 220px; margin: 0; color: var(--body); font-weight: 500; }
.form-note:not(.visible) { display: none; }
.form-actions .btn .ar[hidden] { display: none; }
.contact { display: grid; gap: 6px; }
.contact-v { font-size: 22px; font-weight: 500; line-height: 1.3; overflow-wrap: anywhere; }
/* Legal opens the footer as one heading over two columns, the studio and its policies, set apart from
   the four link columns by a wide gap and no rule; © and the theme switch close it on a line of their
   own with no rule above (Jan, 2026-10-02, WEBSITES #60: the old Legal column held six unrelated things;
   the hairlines were then rejected and the gap asked to grow significantly). */
.footer { grid-column: 1 / -1; margin-top: 64px; display: grid; grid-template-columns: auto repeat(4, minmax(max-content, 1fr)); gap: 32px 3vw; padding: 20px 0 0; border-top: 1px solid var(--hair); font-size: 15px; line-height: 1.5; }
.footer .stack { gap: 6px; }
.footer-legal { display: grid; grid-template-columns: auto auto; align-content: start; column-gap: 40px; margin-right: 10vw; }
.footer-legal .label { grid-column: 1 / -1; }
.footer-base { grid-column: 1 / -1; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px 24px; padding-top: 16px; color: var(--muted); }
.footer a:not(.logo):not(.btn) { text-decoration: none; }
.footer a:not(.logo):not(.btn):is(:hover, :focus-visible) { text-decoration: underline; }
.footer .ar { margin-left: 4px; vertical-align: -1px; }
/* The internal footer links wear the body-copy link underline instead of an arrow (Jan, 2026-10-01). */
.footer .footer-legal :is(a[href], .footer-theme label) { text-decoration: underline; text-decoration-color: var(--green); text-decoration-thickness: 1px; text-underline-offset: 4px; }
.footer .label { margin-bottom: 4px; }
.footer .muted { color: var(--muted); }
/* The theme switch: a dot in the colour it switches away from, ink on paper (Jan, 2026-09-23), and
   beside it the theme it switches to, which `theme.js` keeps current (Jan, 2026-10-02). */
/* The theme switch sits under Cookies in the Legal block, word first and dot after, in the same type as
   the policy links (Jan, 2026-10-02, WEBSITES #60). */
.footer-theme { display: inline-flex; align-items: center; gap: 8px; justify-self: start; }
.footer-theme label { cursor: pointer; }
.footer-theme label { transition: color var(--feedback); }
.footer-theme:hover label { color: var(--green); }
.theme-dot { display: block; width: 1rem; height: 1rem; padding: 0; border: 0; border-radius: 50%; background: var(--ink); cursor: pointer; }
.theme-dot:focus-visible { outline: 1px solid var(--green); outline-offset: 3px; }
/* The footer stands on the bottom of the screen at every height (Jan, 2026-09-08: on a tall screen it
   hung under the form with a band of white beneath). The contact scene is at least as tall as the room
   under its heading less the 24 px the page ends on, and its first row takes the spare height, so the
   footer is carried down rather than left where the form stops. `--content-top` is the scene's own
   measured top, written by `motion.js` at layout; a form taller than the screen keeps its natural height. */
.story .contact-content { min-height: calc(100vh - var(--content-top, 0px) - 24px); grid-template-rows: 1fr auto; }

/* Natural reading flow on small screens, without a long virtual scroll runway. */
.flow .chrome { position: absolute; }
.flow .logo-box { width: 50px !important; height: 36.8px !important; }
.flow .logo-box img { width: 68px !important; height: 68px !important; left: -9px !important; top: -15.6px !important; }
/* Its full-width empty area must not intercept the logo in desktop reading flow. */
.flow .blog { position: relative; inset: auto; margin: 30px var(--gutter) 0; pointer-events: none; }
.flow .blog > * { pointer-events: auto; }
/* Reading flow keeps the personal figures under the button; the blog counts stay chrome-only. */
.flow .blog [data-badges], .flow .hint, .flow [data-spacer] { display: none; }
.flow .badge-window { display: block; }
.flow section { padding: 76px var(--gutter); scroll-margin-top: 24px; }
.flow section:first-of-type { padding-top: 64px; }
/* The page ends under the footer, never on a band of white: reading flow closes on the same small
   space the scrolling build leaves below it, rather than on a full section rhythm. */
.flow #contact { padding-bottom: 24px; }
.flow .h, .flow .subhead, .flow .c { position: relative; inset: auto; width: 100%; max-width: none; opacity: 1; visibility: visible; transform: none; clip-path: none; }
.flow .subhead { margin-bottom: 28px; }
.flow .subhead { transform: translateY(-1rem); }
.flow .subhead > span { transform: none; visibility: visible; transition: none; }
.flow #hello [data-scene] + [data-scene] { margin-top: 76px; }
.flow .h { white-space: normal; margin-bottom: 40px; }
.flow .r, .flow .pt { opacity: 1; transform: none; pointer-events: auto; transition: none; }
.flow .slot { display: block; }
.flow #offer .slot { display: grid; }
.flow [data-card][data-state="pre"] { display: none; }
.flow .row { width: 100%; height: auto; padding-left: 20px; }
.flow .row-line { top: 0; left: 0; width: 1px; height: 100%; }
.flow .pt { position: relative; left: auto; width: 100%; height: auto; margin-bottom: 32px; }
.flow .dot { left: -20px; top: 20px; }
.flow .pt-text { position: static; transform: none; }
.flow .timeline-aside { position: static; width: auto; overflow: visible; margin-top: 40px; }
.flow .timeline-aside-body { transform: none; transition: none; }
.flow [data-card] { max-height: none !important; }

@media (max-width: 1100px) and (min-width: 901px) {
  .offer-content { grid-template-columns: 300px minmax(0, 1fr); gap: 40px; }
  .service-menu { width: 300px; }
  .badges { gap: 28px; }
}
@media (max-width: 1100px) {
  .service-copy.three { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .service-copy.three p:last-child { grid-column: 1 / -1; }
}
/* Below 1360 px Legal takes the full width above the four link columns, so the wide gap never
   squeezes a link onto two lines (Jan, 2026-10-02, WEBSITES #60). */
@media (max-width: 1360px) {
  .footer { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .footer-legal { grid-column: 1 / -1; justify-content: start; padding: 0 0 24px; margin: 0; }
}
@media (max-width: 900px) {
  :root { --gutter: 6vw; }
  .cols-4, .cols-2 { grid-template-columns: minmax(0, 1fr); gap: 28px; }
  .cols-4-even { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 36px; }
  .badges { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px; }
  .badge { align-items: flex-start; }
  .badge .label { text-align: left; }
  .work-offset { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 36px; }
  .work-offset > :first-child { grid-column: auto; }
  .offer-content { grid-template-columns: 1fr; gap: 28px; }
  .service-copy { gap: 28px; }
  .photo { max-width: 520px; }
  .intro-content, .work-content { gap: 40px; }
  .footer { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 20px; }
  /* Legal keeps the full width above the links. */
  .footer-legal { grid-column: 1 / -1; justify-content: start; padding: 0 0 24px; margin: 0; }
}
@media (max-width: 560px) {
  .logo { gap: 10px; }
  .logo-name { font-size: 18px; }
  .flow .blog { margin-top: 92px; align-items: flex-start; }
  .flow section:first-of-type { padding-top: 48px; }
  .flow section { padding-top: 48px; padding-bottom: 48px; }
  .flow .h { font-size: 44px; margin-bottom: 28px; }
  .cols-4-even, .work-offset { grid-template-columns: 1fr; }
  .card, .form { padding: 24px; }
  .card .service-name { font-size: 36px; }
  .service-copy, .service-copy.three { grid-template-columns: 1fr; gap: 24px; }
  .form-actions { gap: 12px; }
  .form { grid-template-columns: 1fr; }
  .footer { grid-template-columns: 1fr 1fr; gap: 28px 20px; }
  .footer > :first-child { grid-column: 1 / -1; }
  .footer-legal { column-gap: 28px; }
}
/* Phones and tablets run the scroll story too (Jan, 2026-10-02, WEBSITES #8): only reduced motion reads
   as flow. The badge slot keeps its desktop row under the blog button, right-aligned, and `motion.js`
   lowers Hello's heading under it wherever the two would meet. Where the header row has no room for
   the blog's address beside the name, `motion.js` sets the pill on its own row under the logo; it keeps
   its full name (Jan, 2026-10-02, WEBSITES #8). */
@media (max-width: 900px) {
  /* The slot's box is as wide as the badge row and reaches over the logo; only its parts take a tap. */
  .story .blog { pointer-events: none; }
  .story .blog > * { pointer-events: auto; }
  .story .badges { display: flex; gap: 24px; }
  .story .badge { align-items: flex-end; }
  .story .badge .label { text-align: right; }
}
@media (max-width: 560px) {
  .story .blog { gap: 16px; }
  .story .badges { gap: 16px; }
  .story .badge-v { font-size: 32px; }
  .story .badge .label { font-size: 10px; }
  /* The card gives up its square for its content's height, so the copy under it has room to read. */
  .story .service-menu { width: 100%; }
  .story .card { aspect-ratio: auto; padding: 20px; gap: 6px; }
  .story .card-lead { margin-top: 8px; }
  /* Narrower points on a tighter axis (`motion.js`), and the invitation takes the content width once
     Today has left, there being no free half of the screen beside the rail. */
  .story .pt { width: 240px; }
  .story .timeline-aside { left: 0; width: 88vw; }
}
/* Reduced motion means gentler, not silent. Removing every transition also removed the colour and
   opacity that tell you a control answered — a link changing under the pointer, a button taking its
   fill, a field's focus line, the form's reply — and those carry meaning rather than movement, so
   they stay and everything spatial goes. The whole declaration is replaced rather than only its
   property list: a shorter list of durations is cycled against a longer list of properties, so
   restricting the properties alone leaves each element's own timings paired with whatever now sits
   at that index — measured here as a 240 ms colour fade on an arrow and a 520 ms one on a button.
   One duration for the lot is both predictable and the only thing left worth timing. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: opacity var(--feedback), color var(--feedback), background-color var(--feedback), border-color var(--feedback), box-shadow var(--feedback), text-decoration-color var(--feedback) !important;
    scroll-behavior: auto !important;
  }
  .btn:hover, .btn:active, .btn:hover .ar { transform: none; }
}
/* `?curve` only, and nothing renders without it: a switch for judging the exit curve on the real
   page, because which one is right is a question about how a departure feels rather than one the
   code can answer for itself. */
.curve-switch { position: fixed; right: 12px; bottom: 12px; z-index: 99; padding: 9px 14px 8px; border: 1px solid var(--ink); border-radius: 999px; background: #fff; color: var(--ink); font: 500 11px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
/* The content remains readable if JavaScript cannot load. */
html:not(.story):not(.flow) .chrome { position: relative; inset: auto; margin: 24px var(--gutter); }
html:not(.story):not(.flow) section { margin: 64px var(--gutter); }
html:not(.story):not(.flow) #contact { margin-bottom: 24px; }
html:not(.story):not(.flow) .h, html:not(.story):not(.flow) .subhead, html:not(.story):not(.flow) .c { position: relative; inset: auto; width: auto; opacity: 1; visibility: visible; }
html:not(.story):not(.flow) .r { opacity: 1; transform: none; pointer-events: auto; }
html:not(.story):not(.flow) [data-panel] { visibility: visible; }
html:not(.story):not(.flow) .slot { display: block; }
html:not(.story):not(.flow) .slot > * { margin-bottom: 28px; }

html:not(.story):not(.flow) .subhead > span { transform: none; visibility: visible; }
html:not(.story):not(.flow) .timeline-aside { position: static; width: auto; overflow: visible; margin-top: 40px; }
html:not(.story):not(.flow) .timeline-aside-body { transform: none; }
/* The rail is placed by the script that also scrolls it, so without one the points have no axis to
   stand on. They read as the same vertical list reading flow uses, rather than stacking on the
   left edge of a row three screens wide. */
html:not(.story):not(.flow) .row { width: 100%; height: auto; padding-left: 20px; }
html:not(.story):not(.flow) .row-line { top: 0; left: 0; width: 1px; height: 100%; }
html:not(.story):not(.flow) .pt { position: relative; left: auto; width: 100%; height: auto; margin-bottom: 32px; }
html:not(.story):not(.flow) .dot { left: -20px; top: 20px; }
html:not(.story):not(.flow) .pt-text { position: static; transform: none; }

/* Contact delivery and keyboard access share the existing typography. */
/* The section nav is for screen readers and robots, not a visible menu: its list takes no room, and
   each link shows only while it holds keyboard focus, in the top-left corner. Its Contact link took
   over from the old skip link (Jan, 2026-10-02, WEBSITES #8). */
.section-nav a { position: fixed; top: 12px; left: 12px; z-index: 1000; padding: 12px 18px; background: #fff; color: var(--ink); transform: translateY(-180%); opacity: 0; pointer-events: none; }
.section-nav a:focus { transform: none; opacity: 1; pointer-events: auto; }
.section-nav, .section-nav ul { position: fixed; top: 0; left: 0; margin: 0; padding: 0; list-style: none; }
address { font-style: normal; }
.contact-trap { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.form-note.is-error { color: #a1382e; }
.btn:disabled { cursor: wait; opacity: .7; }
/* The privacy page wears the homepage's own parts: the logo, the main heading and subhead roles,
   .h3 section titles beside their copy, and the same footer (Jan, 2026-10-01, WEBSITES #58). Its
   vertical rhythm repeats `motion.js`'s: the logo at 24 px, a header band of 0.64 × the heading size
   (`--logo-img` × .59), the heading 60 px under it, the subhead 8 px under that and the copy 48 px
   lower; the Close pill takes the blog button's place and first-paint top. No rules between sections. */
.legal-head { position: relative; display: flex; align-items: flex-start; height: calc(24px + var(--logo-img) * .59); padding: 24px var(--gutter) 0; }
.legal-head .logo { width: max-content; }
.legal-close { position: absolute; top: max(24px, calc(7px + var(--logo-img) * .271)); right: var(--gutter); }
.legal-page { padding: 0 var(--gutter); }
.legal-page h1 { font-size: clamp(44px, 6.4vw, 96px); line-height: 1; margin: 60px 0 8px; }
.legal-sub { color: var(--line); font: 400 clamp(20px, 1.8vw, 26px)/1.15 var(--sans); letter-spacing: .04em; text-transform: uppercase; margin: 0 0 48px; }
body .legal-page section.legal-section { margin: 0; }
.legal-section { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: var(--grid-gap); padding: 36px 0; }
.legal-section h2 { text-align: right; }
.legal-section p { margin: 0 0 14px; max-width: 64ch; }
.legal-section p:last-child { margin-bottom: 0; }
.legal-updated { display: block; padding-top: 24px; }
.legal-footer { margin: 72px var(--gutter) 48px; }
@media (max-width: 900px) {
  .legal-section { grid-template-columns: 1fr; gap: 14px; padding: 28px 0; }
  .legal-section h2 { text-align: left; }
}
html:not(.story):not(.flow) [data-panel].r { transform: none; }
html:not(.story):not(.flow) .hl .card { transform: none; visibility: visible; }
html:not(.story):not(.flow) .service-tabs { display: grid; }
html:not(.story):not(.flow) .service-tabs > .hl { grid-area: auto; }
html:not(.story):not(.flow) .pt { opacity: 1; transform: none; }
html:not(.story):not(.flow) .h { white-space: normal; }
html:not(.story):not(.flow) .badge-window { display: block; }
html:not(.story):not(.flow) .service-nav { display: none; }
@media (max-width: 900px) {
  .offer-content { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 740px) {
  html:not(.story):not(.flow) .grid { grid-template-columns: 1fr; }
  html:not(.story):not(.flow) .footer { grid-template-columns: 1fr; }
  /* The legal pages run no story script, so they keep the homepage's two phone columns. */
  html:not(.story):not(.flow) .legal-footer { grid-template-columns: 1fr 1fr; }
}
@media print {
  .chrome, .hint, .cf-shell, .section-nav, [data-spacer], .contact-form, .service-nav { display: none !important; }
  html, body { height: auto !important; overflow: visible !important; }
  [data-sec], [data-scene], [data-h], [data-subh], [data-subh] > span, [data-c], [data-r], [data-panel], .pt, .pt-text, .row, .timeline-aside, .timeline-aside-body { position: static !important; height: auto !important; width: auto !important; transform: none !important; opacity: 1 !important; visibility: visible !important; clip-path: none !important; }
  [data-sec] { margin: 24px 0 !important; }
  .row { display: block !important; }
  .offer-content, .slot { display: block !important; height: auto !important; overflow: visible !important; }
  [data-panel] { display: grid !important; }
  .pt, [data-panel] { margin-bottom: 16px; break-inside: avoid; }
  .row-line, .dot, .service-menu { display: none !important; }
}
