/* ==========================================================================
   AKBARGFX — public site stylesheet
   Lumora layout system on Akbar's existing brand tokens.

   Architecture note: this file is rem-based, and the root font-size scales
   with the viewport (see "Adaptive grid" below). It is the only stylesheet the
   public shell links; the previous px + clamp() sheet used zero rem, so the two
   approaches could never share a page, and it has been removed rather than left
   loading alongside. /admin/* keeps loading admin.css and is unaffected by the
   root font-size scaling, because that rule ships only in this file.
   ========================================================================== */

/* ---- Reset ------------------------------------------------------------ */
* { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  overflow-x: hidden;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
ul, ol { list-style: none; }
img, svg, video, canvas { display: block; }
input, textarea, select, button { font-family: inherit; }
/* The [hidden] attribute is the single source of truth for the loader, nav
   overlay, modal and the honeypot field. Components here set display on
   themselves, which would otherwise out-specify the UA's [hidden] rule. */
[hidden] { display: none !important; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ==========================================================================
   Adaptive grid
   Root font-size tracks the viewport so the whole rem-based layout stays
   proportional. Each band is 16 * 100 / baseWidth vw for design base widths
   of 1920 / 1440 / 1024 / 360. Above 1920px a small runtime script
   (lumora.js) interpolates upward with a damping coefficient.

   Order matters: for a narrow viewport every query matches, so the last one
   wins — hence largest base first.

   The max()/clamp() bounds are a deliberate departure from a pure ratio.
   Unbounded, the 1024 band puts the root at 10px around 640px wide (body copy
   at ~9px) while the 360 band puts it at 28px just below (body copy at ~25px)
   — both unreadable, in opposite directions, either side of one breakpoint.
   Clamping costs a little proportional purity between roughly 390-900px and
   buys legibility across every real phone and tablet.
   ========================================================================== */
/* Single continuous curve from 360px to 1920px+, replacing the four stepped
   bands above. The old per-breakpoint max()/vw rules each targeted 16px at
   their own design-width boundary, but every boundary crossing produced a
   2-3px discontinuity in the OTHER direction from the viewport change
   (e.g. root jumped 13px -> 16px at 1440px as the viewport got narrower) —
   that's the visible "jump" in every rem-sized element site-wide, since the
   whole layout is rem-based. One clamp() removes every discontinuity and
   nudges the ceiling up slightly (16px -> 18px) for a more confident large-
   viewport scale, per the editorial-feel brief. */
html { font-size: clamp(15px, 0.19vw + 14.3px, 18px); }

/* The header is fixed, so anything the browser scrolls to itself — a `:target`
   from a link with a fragment, or the restored position on a reload — has to
   stop short of it or land underneath. One header-height of padding on the
   scroll container does that for every such jump at once; the scripted
   scrollToId() applies the same offset for the ones it drives. */
html { scroll-padding-top: 5rem; }

/* ==========================================================================
   Tokens
   Colours and type are Akbar's existing brand values; the spatial system,
   radii and motion curves come from the Lumora layout.
   ========================================================================== */
:root {
  /* Brand surfaces. Re-valued from the warm paper set to the neutral greys the
     current design actually uses: page #fcfcfc, one light surface #f3f3f7 worn
     by the logo tiles, service cards and review cards alike, and one hairline
     #e7e7e7 for section dividers and card strokes. Names are unchanged, so
     every rule below and in the case-study vocabulary follows automatically. */
  --bg: #FCFCFC;
  --bg-alt: #F3F3F7;
  --white: #FFFFFF;
  --ink: #0A0A0A;
  --ink-soft: #343434;
  --ink-faint: #8A8A84;
  --line: #E7E7E7;
  --line-strong: #C4C4C9;
  --surface-2: #E9E9EE;

  /* Case-study panel surfaces. Deliberately literals rather than aliases of the
     four tokens above: a dark panel re-declares --ink and friends for everything
     inside it, and a var() written inside a custom property is substituted where
     that property is USED — so `--panel-dark: var(--ink)` would resolve to the
     panel's own inverted --ink and paint itself white. */
  --panel-site: #FCFCFC;
  --panel-light: #F3F3F7;
  --panel-dark: #0A0A0A;
  --panel-accent: #6757FF;

  /* Brand accent */
  --accent: #6757FF;
  --accent-soft: #968BFF;
  --accent-light: #EDEAFF;
  --accent-lighter: #F6F4FF;
  /* On near-black panels the core accent is too dark to read; the soft tint
     carries the same hue with enough luminance to pass contrast. */
  --accent-on-dark: #968BFF;
  /* Two desaturated accents from the design: the service icon plate, and the
     resting state of the review stars (they brighten to --accent on hover). */
  --accent-icon: #ACA5EC;
  --accent-muted: #8882AD;

  /* Type */
  --font-display: 'Mona Sans Variable', 'Mona Sans', sans-serif;
  --font-body: 'Mona Sans Variable', 'Mona Sans', sans-serif;
  /* Review quotes are the one place the design leaves the sans — an italic
     serif, set small, to read as a human voice rather than UI copy. */
  --font-quote: 'Inria Serif', Georgia, 'Times New Roman', serif;
  --fw-display: 680;
  --fw-heading: 660;
  --fw-subhead: 640;
  --fw-label: 600;
  --fw-medium: 500;
  --fw-body: 400;

  /* Radii — 24 / 16 / 12px at the 1440px root size, matching radius/lg,
     radius/md and the logo-tile radius in the design's variable set. */
  --radius-pill: 9999px;
  --radius-card: 1.4rem;
  --radius-card-sm: 1rem;
  --radius-control: 0.7rem;
  /* Single source of truth for the shared left/right content boundary used
     by the header, footer, hero and every section "-in" wrapper. Slightly
     larger than before (1.25rem/2rem -> 1.5rem/2.25rem) per feedback that
     content felt tighter than the header/footer margin — previously every
     one of these was a separately hand-typed literal, so a future edit to
     just one of them was exactly how a real mismatch could creep in even
     though today they all happen to agree. */
  --gutter: 1.5rem;
  --gutter-lg: 2.25rem;

  /* Layout. The design's content column is 1240px inside 100px gutters at a
     1440px viewport, where the root size resolves to ~17.04px. Every "-in"
     wrapper is `max-width: var(--shell)` PLUS an inline gutter, so the shell
     has to carry both: 1240 + 2 x 2.25rem = 77.25rem lands the column on
     1240px and the outer margin on 100px without a single hard-coded width. */
  --shell: 77.25rem;
  --text-watermark: 13rem;

  /* Motion — spring equivalents from the source design */
  --ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
  /* Every hover/state interaction in the design is one prototype setting:
     Smart Animate, 300ms, ease-out-cubic. Two tokens instead of retyping it. */
  --ease-out-cubic: cubic-bezier(0.33, 1, 0.68, 1);
  --t-state: 300ms;
  /* Entrances. One length for every scroll reveal on the site, so no section
     can fade at a different speed from the section above it. Deliberately much
     shorter than the 0.8-0.9s these transitions used to carry: at that length
     the reveal is still resolving after the reader has arrived at the content
     and begun reading it, which reads as the page lagging rather than as the
     section arriving. */
  --t-reveal: 400ms;
}

/* The footer wordmark is sized in rem like everything else, but at small widths
   a 13rem word is wider than the viewport and clips to an unreadable fragment.
   Stepping it down keeps it legible as the closing mark. */
@media (max-width: 1024px) { :root { --text-watermark: 8rem; } }
@media (max-width: 640px)  { :root { --text-watermark: 4.5rem; } }

/* Locked by lumora.js while the loader, nav overlay or modal is open.
   Kept as a class rather than inline styles so the three can't fight. */
html.lm-locked { position: relative; overflow: hidden; height: 100%; }

.shell { max-width: var(--shell); margin-inline: auto; }
/* Shared boundary for anything that isn't already inside a `.shell` element.
   Header and footer inners were the two containers in the whole site that
   never got the `.shell` treatment, so past ~1408px viewport width their
   content pinned near the true edge while hero/about/work/contact content
   stayed centred in the shell column below/above them — the visible
   left/right misalignment between header, footer and everything else.
   The footer sets its own, wider boundary (see the Footer section): the
   design deliberately runs it to a 64px margin rather than the 100px the
   sections use, which is part of what makes it read as the closing statement
   rather than one more band. */
.lm-header-in { max-width: var(--shell); margin-inline: auto; }

.lm-skip {
  position: absolute; left: -9999px;
}
.lm-skip:focus {
  position: fixed; left: 1rem; top: 1rem; z-index: 130;
  border-radius: var(--radius-control);
  background: var(--ink); padding: 0.5rem 1rem;
  font-size: 0.875rem; color: var(--white);
}

/* ==========================================================================
   Reveal system
   One IntersectionObserver in lumora.js adds .is-in to each [data-reveal]
   element the first time it scrolls into view, then never again.
   --d  delay (ms)          --ry translateY start
   --ro final opacity       --rs scale start

   Whole elements only. Headings used to be cut into per-line spans, and one
   paragraph treatment into per-word spans, by an applySplits() pass in
   lumora.js; each child then eased in behind the last on a 110-120ms ladder.
   Two consequences. A two-line heading was still assembling a quarter of a
   second after it began, on top of a 0.9s transition — and the eye follows the
   ladder, so what registered was an effect being performed on the text rather
   than a section arriving. A section is now one fade: heading, supporting line
   and content each resolve as single blocks, and the <br> a CMS heading carries
   is simply a line break again rather than a splitting instruction.
   ========================================================================== */
[data-reveal] {
  opacity: 0;
  transform: translateY(var(--ry, 1.25rem)) scale(var(--rs, 1));
  transition:
    opacity var(--t-reveal) var(--ease-soft) var(--d, 0ms),
    transform var(--t-reveal) var(--ease-soft) var(--d, 0ms);
  will-change: opacity, transform;
}
[data-reveal].is-in {
  opacity: var(--ro, 1);
  transform: none;
}

/* ==========================================================================
   Shared components
   ========================================================================== */

/* ---- Eyebrow ---- */
.lm-eyebrow {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: 0.875rem; font-weight: var(--fw-medium);
  color: rgba(10, 10, 10, 0.7);
}
.lm-eyebrow::before {
  content: ""; width: 0.375rem; height: 0.375rem;
  border-radius: var(--radius-pill);
  background: rgba(10, 10, 10, 0.5);
  flex: none;
}
.lm-eyebrow.is-light { color: rgba(255, 255, 255, 0.7); }
.lm-eyebrow.is-light::before { background: rgba(255, 255, 255, 0.6); }
.lm-eyebrow.is-boxed {
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 0.375rem 1rem;
}

/* ---- Pill button ----
   The design ships three variants at one size (52px tall, 28px side padding),
   and the arrow is a bare 16px glyph on the same baseline as the label — not a
   badge inside its own circle. `.lm-btn-badge` is kept as the arrow's wrapper
   so the renderer's markup and its data-arrow hook stay untouched. */
.lm-btn { display: inline-block; }
.lm-btn > .lm-btn-in {
  display: inline-flex; align-items: center; gap: 0.5rem;
  border-radius: var(--radius-pill);
  font-size: 0.9375rem; font-weight: var(--fw-label); line-height: 1.1;
  padding: 1rem 1.65rem;
  transition:
    background-color var(--t-state) var(--ease-out-cubic),
    box-shadow var(--t-state) var(--ease-out-cubic),
    color var(--t-state) var(--ease-out-cubic),
    filter var(--t-state) var(--ease-out-cubic);
  white-space: nowrap;
}
/* Slightly tighter on the arrow side so the glyph reads as part of the label
   block rather than floating toward the edge. */
.lm-btn.has-arrow > .lm-btn-in { padding-right: 1.45rem; }
.lm-btn-dark > .lm-btn-in { background: #161618; color: var(--white); }
/* The stroke is drawn inside the pill, as an inset ring rather than a border.
   A border sits outside the padding box and adds to the element, so the
   secondary CTA was 3px taller and wider than the filled buttons standing next
   to it — the two hero CTAs are meant to be the same pill at two weights, and
   at 52px a 3px difference in one of them is visible along their shared
   baseline. An inset shadow paints in the same place a stroke aligned to the
   inside of the shape does in the design, and costs the layout nothing. It also
   means the ring animates as a colour rather than as geometry. */
.lm-btn-outline > .lm-btn-in {
  box-shadow: inset 0 0 0 2px #68686C;
  background: transparent; color: #161618;
}
.lm-btn-accent > .lm-btn-in {
  background: linear-gradient(110deg, #6555FF, #8482FF);
  color: var(--white);
}

.lm-btn-badge {
  width: 1em; height: 1em; flex: none;
  display: grid; place-items: center;
}
.lm-btn-badge svg { transition: transform var(--t-state) var(--ease-out-cubic); }

/* Hover states are pointer-only — the source design disables them on touch,
   where :hover latches after a tap and leaves elements stuck mid-animation.
   No scale: in the design every button changes fill and nothing moves. */
@media (hover: hover) and (pointer: fine) {
  .lm-btn:hover .lm-btn-badge[data-arrow="right"] svg { transform: translate(3px, 0); }
  .lm-btn:hover .lm-btn-badge[data-arrow="up-right"] svg { transform: translate(2px, -2px); }
  /* Black turns purple; purple deepens. The design layers #0a0a95 at 15% over
     the gradient, which brightness() reproduces without a second paint layer. */
  .lm-btn-dark:hover > .lm-btn-in { background: var(--accent); }
  .lm-btn-accent:hover > .lm-btn-in { filter: brightness(0.93); }
  .lm-btn-outline:hover > .lm-btn-in {
    background: #161618; box-shadow: inset 0 0 0 2px #161618; color: var(--white);
  }
}

/* ---- Tag chip ---- */
.lm-chip {
  display: inline-flex; align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
  color: var(--ink);
}
.lm-chip.is-light { border-color: rgba(255, 255, 255, 0.25); color: var(--white); }

/* ---- Animated link ---- */
.lm-alink { display: inline-flex; }
.lm-alink > span {
  display: inline-block;
  opacity: 0.65;
  transition: transform 0.35s var(--ease-snap), opacity 0.35s var(--ease-snap);
}
@media (hover: hover) and (pointer: fine) {
  .lm-alink:hover > span { transform: translateX(4px); opacity: 1; }
  .lm-alink.is-legal > span { opacity: 0.7; }
  .lm-alink.is-legal:hover > span { transform: translateX(3px); opacity: 1; }
}

/* Icon sizing convention: every inlined SVG is 1em square and inherits
   colour, so an icon is sized by setting font-size on its container. */
.lm-ico { width: 1em; height: 1em; }

/* ==========================================================================
   Page loader
   ========================================================================== */
.lm-loader {
  position: fixed; inset: 0; z-index: 120;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2rem;
  background: var(--ink); color: var(--white);
  border-radius: 0 0 var(--radius-card) var(--radius-card);
  transform: translateY(0);
  transition: transform 0.7s var(--ease-spring);
}
.lm-loader.is-out { transform: translateY(-100%); }

.lm-loader-center {
  display: flex; flex-direction: column; align-items: center; gap: 1.25rem;
  text-align: center;
  transition: opacity 0.55s var(--ease-spring), transform 0.55s var(--ease-spring);
}
.lm-loader.is-out .lm-loader-center { opacity: 0; transform: translateY(-12px); }

.lm-loader-brand {
  display: flex; align-items: center; gap: 0.5rem;
  font-weight: var(--fw-heading); font-size: 1.5rem;
}
.lm-loader-brand img { height: 1.875rem; width: auto; }
.lm-loader-tagline {
  max-width: 24ch; font-size: 0.875rem; line-height: 1.5;
  color: rgba(255, 255, 255, 0.55);
}
.lm-loader-progress { width: min(22rem, 72vw); display: flex; flex-direction: column; gap: 0.75rem; }
.lm-loader-track { height: 1px; background: rgba(255, 255, 255, 0.15); }
.lm-loader-fill {
  height: 100%; width: 0%;
  background: var(--accent-on-dark);
  transition: width 0.1s ease-out;
}
.lm-loader-meta {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 0.75rem; font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.45);
}
.lm-loader-count { font-variant-numeric: tabular-nums; color: rgba(255, 255, 255, 0.8); }

@media (min-width: 640px) { .lm-loader-brand { font-size: 1.875rem; } }

/* ==========================================================================
   Header
   ========================================================================== */
/* The bar rides along instead of scrolling away. On a page this long, Work,
   About, Contact and the CTA would otherwise only be reachable by scrolling
   back to the top.

   `fixed` rather than `sticky` because the header is already out of flow in the
   design — the hero and every page header reserve its height in their own top
   padding — so promoting it shifts nothing and there is no in-flow parent whose
   overflow could cancel a sticky. */
.lm-header {
  position: fixed; inset-inline: 0; top: 0; z-index: 50;
  /* Transparent over the hero, exactly as designed: at rest the header has no
     surface of its own. The page's own background arrives once content is
     actually passing underneath — the same background, not a tint or a blur, so
     the bar reads as the top of the page rather than a panel floating above it.
     Declared here in its off state so the transition has something to
     interpolate from.

     No hairline along the bottom edge. The bar and the page share one
     background, so a rule between them draws a border around nothing — it only
     announced where the fixed element ended, which is exactly what a header
     that is meant to read as part of the page should not do. */
  background-color: transparent;
  transition: background-color var(--t-state) var(--ease-out-cubic);
}
.lm-header.is-stuck { background-color: var(--bg); }
/* Block padding is written out rather than left to the symmetric
   `padding: var(--gutter)` shorthand it used to use. The gutter is a horizontal
   token shared with every section on the page, so it cannot be tuned for the
   bar's height without moving every left and right edge on the site with it.
   1.1rem against the 40px CTA puts the header at ~78px on desktop, down from
   92px. */
.lm-header-in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.5rem; padding: 1.1rem var(--gutter);
}
.lm-brand { display: inline-flex; }
.lm-brand-in {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 1.375rem; font-weight: var(--fw-heading); letter-spacing: -0.01em;
}
/* Was 1.25rem (~20px) — small enough to read as a favicon-scale afterthought
   rather than a mark. This is the one element on the page a visitor sees on
   every single screen, so it carries the most weight-per-pixel of any fix
   here. */
.lm-brand-in img { height: 1.75rem; width: auto; }
@media (min-width: 1024px) {
  /* A touch larger once the header has room for it — header padding/height
     is untouched, so proportions stay the same, the mark just reads a
     little more confidently at desktop viewing distance. */
  .lm-brand-in img { height: 2rem; }
}
/* The mark holds still on hover. It is the site's own identity rather than a
   control, and a logo that flinches when the pointer passes over it reads as
   a widget. The anchor is still a link — the cursor and the focus ring say so. */

.lm-nav { display: none; }
.lm-nav ul { display: flex; align-items: center; gap: 0.5rem; }
.lm-nav button, .lm-nav a { display: inline-flex; align-items: center; gap: 0.25rem; }
/* The nav item is a small uppercase label that carries both its active and its
   hover state in colour alone — no pill, no surface behind it. The padding
   stays so the tap target keeps its size and the row's rhythm is unchanged;
   only the ink moves. */
.lm-nav span.lm-nav-label {
  display: inline-block;
  padding: 0.55rem 0.85rem;
  font-size: 0.76rem; font-weight: var(--fw-label); line-height: 1;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: #6E6E6E;
  transition: color var(--t-state) var(--ease-out-cubic);
}
/* Active and hover resolve to the same ink, so hovering the page you are
   already on is a no-op instead of a step back to a lighter grey. At rest,
   exactly one dark label marks the current page. */
.lm-nav [aria-current="page"] .lm-nav-label { color: var(--ink); }
@media (hover: hover) and (pointer: fine) {
  .lm-nav li:hover .lm-nav-label { color: var(--ink); }
}
.lm-nav-caret { font-size: 0.75rem; opacity: 0.6; }

.lm-header-right { display: flex; align-items: center; gap: 0.75rem; }

.lm-header-cta { display: none; }
/* The one button in the design that isn't the full 52px: the header's is 40px,
   which is what the bar's ~78px height is built around — the button is the
   tallest thing in it, so the block padding either side of it sets the total. */
.lm-header-cta > .lm-btn-in { padding-block: 0.65rem; }
@media (min-width: 640px) { .lm-header-cta { display: inline-flex; } }
/* The overlay menu button duplicates the inline nav once it appears at
   1024px, so it's desktop-only dead weight past that point — the CTA takes
   over as the sole header-right element on large screens. Below 1024px the
   inline nav is hidden (see .lm-nav below), so the menu button stays: it's
   the only way to reach Work/About/Contact on mobile and tablet. */
@media (min-width: 1024px) { .lm-menu-btn { display: none; } }

.lm-menu-btn {
  border: 1px solid var(--line);
  background: var(--white);
  border-radius: var(--radius-control);
  transition:
    background-color var(--t-state) var(--ease-out-cubic),
    border-color var(--t-state) var(--ease-out-cubic);
}
.lm-menu-btn:hover { background: var(--bg-alt); border-color: var(--line-strong); }
.lm-menu-btn > span {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.5rem 1rem;
  font-size: 0.75rem; font-weight: var(--fw-label);
  text-transform: uppercase; letter-spacing: 0.05em;
}
.lm-menu-word { display: none; }
/* The button's own font-size is a 0.75rem uppercase label; the icon would
   inherit it and shrink to ~9px, so it gets its own size. */
.lm-menu-btn .lm-ico { width: 1.125rem; height: 1.125rem; }
.lm-navmenu-close .lm-ico { width: 0.875rem; height: 0.875rem; }

@media (min-width: 640px) {
  .lm-header-in { padding: 1.1rem var(--gutter-lg); }
  .lm-menu-word { display: inline; }
}
@media (min-width: 1024px) { .lm-nav { display: flex; } }

/* ==========================================================================
   Nav overlay
   ========================================================================== */
.lm-navmenu {
  position: fixed; inset: 0; z-index: 115;
  display: flex; flex-direction: column;
  background: var(--ink); color: var(--white);
  opacity: 0;
  transition: opacity 0.4s var(--ease-spring);
}
.lm-navmenu.is-open { opacity: 1; }
.lm-navmenu[hidden] { display: none; }

.lm-navmenu-top {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.25rem;
}
.lm-navmenu-brand {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 1.125rem; font-weight: var(--fw-heading);
}
.lm-navmenu-brand img { height: 1.75rem; width: auto; }
.lm-navmenu-close {
  display: inline-flex; align-items: center; gap: 0.5rem;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: var(--radius-control);
  padding: 0.5rem 1rem;
  font-size: 0.75rem; font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.7);
  transition: color 0.3s, border-color 0.3s;
}
.lm-navmenu-close:hover { border-color: rgba(255, 255, 255, 0.4); color: var(--white); }

.lm-navmenu-body { flex: 1; display: flex; flex-direction: column; justify-content: center; }
.lm-navmenu-body ul { display: flex; flex-direction: column; gap: 0.25rem; }
.lm-navmenu-item {
  display: flex; align-items: baseline; gap: 1rem; width: 100%;
  padding-block: 0.5rem; text-align: left;
  font-size: 2.25rem; font-weight: var(--fw-heading); letter-spacing: -0.02em;
  transform: translateY(1rem); opacity: 0;
  transition: transform 0.5s ease-out, opacity 0.5s ease-out;
  transition-delay: var(--d, 0ms);
}
.lm-navmenu.is-open .lm-navmenu-item { transform: none; opacity: 1; }
.lm-navmenu-index {
  font-size: 1rem; font-weight: var(--fw-body);
  color: rgba(255, 255, 255, 0.3);
  transition: color 0.3s;
}
.lm-navmenu-label {
  color: rgba(255, 255, 255, 0.7);
  transition: color 0.3s;
}
.lm-navmenu-item:hover .lm-navmenu-index { color: var(--accent-on-dark); }
.lm-navmenu-item:hover .lm-navmenu-label { color: var(--white); }

.lm-navmenu-foot {
  display: flex; flex-direction: column; gap: 0.75rem;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  padding: 1.5rem 1.25rem;
  font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.025em;
  color: rgba(255, 255, 255, 0.45);
}
.lm-navmenu-foot button { color: rgba(255, 255, 255, 0.7); transition: color 0.3s; }
.lm-navmenu-foot button:hover { color: var(--white); text-decoration: underline; }

@media (min-width: 640px) {
  .lm-navmenu-top { padding: 1.5rem 2rem; }
  .lm-navmenu-item { font-size: 3.75rem; }
  .lm-navmenu-foot { flex-direction: row; align-items: center; justify-content: space-between; padding-inline: 2rem; }
}

/* ==========================================================================
   Request modal
   ========================================================================== */
.lm-modal {
  position: fixed; inset: 0; z-index: 110;
  display: flex; align-items: flex-end; justify-content: center;
  padding: 1rem;
  background: rgba(10, 10, 10, 0.3);
  backdrop-filter: blur(16px);
  opacity: 0;
  transition: opacity 0.4s var(--ease-spring);
}
.lm-modal.is-open { opacity: 1; }
.lm-modal[hidden] { display: none; }

.lm-modal-panel {
  position: relative; width: 100%; max-width: 32rem;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--white);
  padding: 1.5rem;
  box-shadow: 0 25px 50px -12px rgba(10, 10, 10, 0.25);
  outline: 1px solid var(--line);
  transform: translateY(28px);
  transition: transform 0.45s var(--ease-spring);
}
.lm-modal.is-open .lm-modal-panel { transform: none; }
.lm-modal.is-closing .lm-modal-panel { transform: translateY(18px); }

.lm-modal-close {
  position: absolute; right: 1rem; top: 1rem;
  width: 2.25rem; height: 2.25rem;
  display: grid; place-items: center;
  border-radius: var(--radius-pill);
  background: var(--bg-alt); color: rgba(10, 10, 10, 0.6);
  font-size: 0.875rem;
  transition: background-color 0.3s, color 0.3s;
}
.lm-modal-close:hover { background: var(--surface-2); color: var(--ink); }

.lm-modal-head { display: flex; flex-direction: column; gap: 0.375rem; margin-bottom: 1.5rem; }
.lm-modal-kicker {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: 0.875rem; font-weight: var(--fw-medium);
  color: rgba(10, 10, 10, 0.6);
}
.lm-modal-kicker::before {
  content: ""; width: 0.375rem; height: 0.375rem; flex: none;
  border-radius: var(--radius-pill); background: var(--accent);
}
.lm-modal-title {
  font-size: 1.5rem; font-weight: var(--fw-heading); letter-spacing: -0.01em;
  padding-right: 2.5rem;
}

.lm-form { display: flex; flex-direction: column; gap: 1rem; }
.lm-field { display: flex; flex-direction: column; gap: 0.375rem; }
.lm-field > span {
  font-size: 0.75rem; font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.025em;
  color: rgba(10, 10, 10, 0.5);
}
.lm-input {
  width: 100%;
  border: 1px solid var(--line);
  background: var(--bg-alt);
  border-radius: var(--radius-control);
  padding: 0.75rem 1rem;
  font-size: 0.875rem;
  color: var(--ink);
  outline: none;
  transition: border-color 0.25s, background-color 0.25s;
}
.lm-input:focus { border-color: rgba(10, 10, 10, 0.3); background: var(--white); }
textarea.lm-input { resize: none; }

.lm-form-foot {
  margin-top: 0.5rem;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
.lm-form-note { font-size: 0.75rem; color: rgba(10, 10, 10, 0.45); }
.lm-form-error {
  font-size: 0.8125rem; color: #B3261E;
  min-height: 0; /* collapses when empty so the panel doesn't jump */
}
.lm-form-error:empty { display: none; }

.lm-modal-success {
  display: none;
  flex-direction: column; align-items: center; text-align: center;
  gap: 1rem; padding-block: 2rem;
}
.lm-modal.is-sent .lm-modal-success { display: flex; }
.lm-modal.is-sent .lm-modal-head,
.lm-modal.is-sent .lm-form { display: none; }
.lm-success-badge {
  width: 3.5rem; height: 3.5rem;
  display: grid; place-items: center;
  border-radius: var(--radius-pill);
  background: var(--ink); color: var(--accent-on-dark);
  font-size: 1.5rem;
}
.lm-success-badge img { height: 1.5rem; width: auto; }
.lm-modal-success h2 { font-size: 1.5rem; font-weight: var(--fw-heading); }
.lm-modal-success p { max-width: 32ch; font-size: 0.875rem; color: rgba(10, 10, 10, 0.6); }

@media (min-width: 640px) {
  .lm-modal { align-items: center; }
  .lm-modal-panel { padding: 2rem; }
  .lm-modal-title { font-size: 1.875rem; }
}

/* ==========================================================================
   Hero
   A centred column: headline, the brand logo wall, then the supporting line
   and its two CTAs. Nothing here moves on its own — the section's only motion
   is the per-tile hover state further down.
   ========================================================================== */
.lm-hero { background: var(--bg); }
/* One gap does duty above and below the logo wall — the wall is the middle of
   three children, so tightening it pulls the headline and the supporting line
   toward the marks at the same time and keeps the column symmetric. */
.lm-hero-in {
  display: flex; flex-direction: column; align-items: center;
  gap: 1.9rem;
  padding: 6.4rem var(--gutter) 4rem;
}
.lm-hero-h1 {
  text-align: center;
  /* 80px at a 1440px viewport, where the 5.55vw term and the 4.7rem ceiling
     meet exactly; above that the ceiling keeps growing gently with the root
     size, and the floor holds the headline readable on a 360px phone. */
  font-size: clamp(2.2rem, 5.55vw, 4.7rem);
  font-weight: var(--fw-heading);
  line-height: 1.125; letter-spacing: -0.016em;
}
/* The supporting line and its two CTAs share the design's 456px measure and
   sit centred under the wall. */
.lm-hero-foot {
  display: flex; flex-direction: column; align-items: center; gap: 1.9rem;
  width: 100%; max-width: 27rem;
}
.lm-hero-text {
  text-align: center;
  font-size: 0.9375rem; line-height: 1.6; color: var(--ink-soft);
}
.lm-hero-ctas { display: flex; flex-wrap: wrap; justify-content: center; gap: 1.17rem; }

/* ---- Logo wall ----
   Four columns of 215x111 tiles with 20px gaps, centred in the content column.
   The tile is the stable frame: it never scales, moves or resizes, and because
   the grid track is fixed a hovered tile cannot nudge a neighbour. */
.lm-logogrid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.17rem;
  width: 100%; max-width: 54rem;
}
.lm-logotile {
  aspect-ratio: 215 / 111;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--bg-alt);
  transition: background-color var(--t-state) var(--ease-out-cubic);
}
.lm-logotile > a { display: block; height: 100%; }
.lm-logotile-in {
  display: grid; place-items: center;
  width: 100%; height: 100%;
  padding: 0.9rem;
}

.lm-logotile img {
  max-width: 62%; max-height: 44%;
  width: auto; height: auto;
  object-fit: contain;
  /* The design holds every mark at one dark grey until it is hovered. A filter
     does that to any source file; a mask + solid colour would flatten a JPEG
     logo with no alpha into a solid grey rectangle. */
  filter: grayscale(1) contrast(1.05) brightness(0.62);
  transition:
    filter var(--t-state) var(--ease-out-cubic),
    transform var(--t-state) var(--ease-out-cubic);
}
/* Keyboard equivalent of the hover state — and the same reveal on touch, where
   :hover latches after a tap and would leave one tile stuck white. The scale is
   on the mark alone, about its own centre, so the tile stays exactly put. */
.lm-logotile:focus-within { background: var(--white); }
.lm-logotile:focus-within img { filter: none; transform: scale(1.077); }
@media (hover: hover) and (pointer: fine) {
  .lm-logotile:hover { background: var(--white); }
  .lm-logotile:hover img { filter: none; transform: scale(1.077); }
}

/* On a phone the four-column wall becomes two, so eight marks turn into four
   rows of tiles between the headline and the buttons — enough vertical space to
   push both CTAs off the first screen. The first four still say what the wall is
   for; the rest are hidden here rather than dropped upstream, so one cached HTML
   response keeps serving every viewport and the tile count stays a matter of
   layout instead of a server-side guess about the device. */
@media (max-width: 639px) {
  .lm-logotile:nth-child(n + 5) { display: none; }
}

@media (min-width: 640px) {
  .lm-hero-in { padding-inline: var(--gutter-lg); }
  .lm-logogrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .lm-hero-in { gap: 2.6rem; padding: 7.4rem var(--gutter-lg) 6.6rem; }
}

/* ==========================================================================
   Section shell
   Every band below the hero shares one frame: the shell column, a hairline
   above it — the design rules a 1px line across the 1240px content column, not
   the full viewport, which is why it lives on the inner element — and a
   vertical rhythm that is deliberately shorter on phones rather than a scaled
   copy of the desktop spacing.
   ========================================================================== */
.lm-work-in,
.lm-designer-in,
.lm-services-in,
.lm-reviews-in,
.lm-faq-in {
  border-top: 1px solid var(--line);
  padding: var(--band-t, 3.4rem) var(--gutter) var(--band-b, 3.4rem);
}
@media (min-width: 640px) {
  .lm-work-in,
  .lm-designer-in,
  .lm-services-in,
  .lm-reviews-in,
  .lm-faq-in { padding-inline: var(--gutter-lg); }
}
@media (min-width: 1024px) {
  .lm-work-in { --band-t: 4.1rem; --band-b: 4.1rem; }
  .lm-designer-in, .lm-services-in { --band-t: 5.9rem; --band-b: 8.2rem; }
  .lm-reviews-in { --band-t: 7rem; --band-b: 7rem; }
  .lm-faq-in { --band-t: 5.9rem; --band-b: 5.9rem; }
}

/* ---- Section head ----
   `width: fit-content` rather than a max-width, so the left-align option in
   Site Settings (which emits width:auto; margin-inline:0) can cancel it. */
.lm-sechead { text-align: center; margin-bottom: 1.9rem; }
.lm-sechead-title {
  width: fit-content; margin-inline: auto;
  font-size: calc(clamp(1.85rem, 3.2vw, 2.7rem) * var(--h-scale, 1));
  font-weight: var(--fw-display);
  line-height: 1.06; letter-spacing: -0.022em;
}
.lm-sechead-sub {
  margin: 0.5rem auto 0; max-width: 42ch;
  font-size: 1.06rem; font-weight: var(--fw-medium); color: var(--ink-soft);
}

/* ==========================================================================
   The Designer
   Copy left, portrait right, 700/508 in the design. The portrait rests
   de-saturated and returns to colour on hover: one animatable property, no
   second copy of the image, and no information is carried by the colour.
   ========================================================================== */
.lm-designer { background: var(--bg); }
.lm-designer-row { display: grid; grid-template-columns: 1fr; gap: 1.88rem; }
.lm-designer-copy { display: flex; flex-direction: column; gap: 1.76rem; }
.lm-designer-id { display: flex; flex-direction: column; gap: 0.4rem; }
.lm-designer-name {
  font-size: 1.64rem; font-weight: var(--fw-medium);
  line-height: 1.25; letter-spacing: -0.004em;
}
.lm-designer-role {
  font-size: 0.9375rem; font-weight: var(--fw-medium);
  color: #616161;
}
.lm-designer-text {
  max-width: 62ch;
  font-size: 0.9375rem; line-height: 1.5; color: var(--ink);
  /* On top of the column's own gap, so the break lands only here. The three
     children of the copy column are name/role, this paragraph, and the button;
     widening the shared gap to separate the last two would have pushed the name
     off the paragraph it introduces by the same amount. */
  margin-bottom: 1.2rem;
}
.lm-designer-text .lm-dim { color: var(--ink-faint); }
/* Smaller than the page's other buttons, and deliberately scoped to this column
   rather than to the outline variant: the hero's secondary CTA is the same
   variant and has to stay at full size next to the primary one. Everything here
   is in rem, so it tracks the root scale like the rest of the type — about 44px
   on a desktop and about 39px on a phone, which is the same size band the
   header's own CTA already occupies. */
.lm-designer-copy .lm-btn > .lm-btn-in {
  padding: 0.8rem 1.4rem;
  font-size: 0.875rem;
}
.lm-designer-copy .lm-btn.has-arrow > .lm-btn-in { padding-right: 1.2rem; }
.lm-designer-portrait {
  overflow: hidden;
  aspect-ratio: 508 / 390;
  border-radius: var(--radius-card);
  background: var(--bg-alt);
}
.lm-designer-portrait img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
  filter: grayscale(1);
  transition: filter var(--t-state) var(--ease-out-cubic);
}
@media (hover: hover) and (pointer: fine) {
  .lm-designer-portrait:hover img { filter: none; }
}
@media (max-width: 767px) {
  /* No hover to invite on a phone, so the portrait simply reads in colour. */
  .lm-designer-portrait img { filter: none; }
}
/* Stacked, the portrait leads. The section is an introduction to a person, and a
   face above a name reads as one; a name above a face reads as a caption
   arriving before the thing it captions. Declared unconditionally and undone at
   the two-column breakpoint rather than fenced behind `max-width: 1023px`, which
   would leave every fractional width between 1023 and 1024 — a zoomed page, a
   scrollbar's worth of viewport — matching neither query and stacking the column
   in its markup order. The markup itself is untouched, so the name is still what
   a screen reader reaches first. */
.lm-designer-portrait { order: -1; }
@media (min-width: 1024px) {
  .lm-designer-row { grid-template-columns: 1.38fr 1fr; align-items: start; }
  /* Side by side, `order` would put the portrait in the first column. */
  .lm-designer-portrait { order: 0; }
}

/* ==========================================================================
   Create band
   ========================================================================== */
.lm-band { background: var(--bg); }
.lm-band ul {
  display: flex; flex-direction: column; gap: 0.75rem;
  padding: 2.5rem var(--gutter);
}
.lm-band li { flex: 1; }
.lm-band-tile {
  display: grid; place-items: center;
  height: 6rem;
  border-radius: var(--radius-pill);
  font-size: 1.875rem; font-weight: var(--fw-medium);
  transition: transform 0.35s var(--ease-snap);
}
@media (hover: hover) and (pointer: fine) {
  .lm-band-tile:hover { transform: scale(1.03); }
}
.lm-band-light { background: var(--bg-alt); color: var(--ink); }
.lm-band-accent { background: linear-gradient(to bottom right, var(--accent-soft), var(--accent)); color: var(--white); }
.lm-band-dark { background: var(--ink); color: var(--white); font-size: 2.25rem; }
.lm-band-ghost { background: var(--bg-alt); color: var(--ink-faint); }

@media (min-width: 640px) {
  .lm-band ul { flex-direction: row; gap: 1rem; padding-inline: var(--gutter-lg); }
  .lm-band-tile { height: 10rem; font-size: 2.25rem; }
  .lm-band-dark { font-size: 3rem; }
}

/* ==========================================================================
   Selected works
   A 2x2 wall of image-only tiles: 606x434.5 at 28px gaps in the design, so
   the aspect ratio and the gap are what's encoded here rather than any pixel
   size, and the pair of columns collapses to one below 768.

   Nothing in the tile changes size on hover. The two things that appear —
   the caption band and the corner control — are already laid out at rest and
   only cross from opacity 0 to 1, so the image cannot shift and no
   neighbouring tile can be displaced. The image itself is deliberately left
   alone: no zoom, no pan.

   The band is 34.3% tall, matching the design's 149px box, but its gradient
   STARTS fully transparent and only reaches 60% black at the very bottom
   edge. So although the box overlaps the lower third geometrically, the top
   of the image stays visually unobstructed — the darkening is confined to
   roughly the bottom fifth, which is what the brief asks for.
   ========================================================================== */
.lm-work { background: var(--bg); }
.lm-workgrid { display: grid; grid-template-columns: 1fr; gap: 1.64rem; }
.lm-workcard {
  position: relative; display: block; overflow: hidden;
  aspect-ratio: 606 / 434.5;
  border-radius: var(--radius-card);
  background: var(--surface-2);
}
.lm-workcard-img,
.lm-workcard-mark {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
}
.lm-workcard-img { object-fit: cover; }
/* Projects with no cover yet: the brand mark on the dark tile, so the wall
   still reads as a grid of four instead of one hole in it. */
.lm-workcard-mark { display: grid; place-items: center; background: var(--ink); }
.lm-workcard-mark img { width: 34%; max-width: 8rem; height: auto; opacity: 0.85; }

/* 34px circle, 20px in from the top-right corner. The arrow rests a quarter
   turn anticlockwise and rotates up to level as the control fades in. */
.lm-workcard-badge {
  position: absolute; top: 1.17rem; right: 1.17rem; z-index: 2;
  width: 2rem; height: 2rem;
  display: grid; place-items: center;
  border-radius: var(--radius-pill);
  background: #171719; color: var(--white);
  opacity: 0;
  transition: opacity var(--t-state) var(--ease-out-cubic);
}
.lm-workcard-badge svg {
  width: 0.62rem; height: 0.62rem;
  transform: rotate(-90deg);
  transition: transform var(--t-state) var(--ease-out-cubic);
}
.lm-workcard-bar {
  position: absolute; inset-inline: 0; bottom: 0; z-index: 1;
  min-height: 34.3%;
  display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem;
  padding: 1.64rem;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%);
  opacity: 0;
  transition: opacity var(--t-state) var(--ease-out-cubic);
}
.lm-workcard-name {
  font-size: 1.06rem; font-weight: var(--fw-medium); line-height: 1.3;
  color: var(--white);
}
.lm-workcard-cat {
  flex: none;
  font-size: 0.7rem; font-weight: var(--fw-medium); line-height: 1.6;
  color: #B4B4B4;
}
@media (hover: hover) and (pointer: fine) {
  .lm-workcard:hover .lm-workcard-bar,
  .lm-workcard:hover .lm-workcard-badge { opacity: 1; }
  .lm-workcard:hover .lm-workcard-badge svg { transform: none; }
}
/* Keyboard equivalent for the hover reveal. */
.lm-workcard:focus-visible .lm-workcard-bar,
.lm-workcard:focus-visible .lm-workcard-badge { opacity: 1; }
.lm-workcard:focus-visible .lm-workcard-badge svg { transform: none; }
/* Touch pointers have no hover to give, so the caption is simply always
   there: the title and category stay readable and a tap still just follows
   the link, with no tap-to-reveal step in the way. */
@media (hover: none) {
  .lm-workcard-bar,
  .lm-workcard-badge { opacity: 1; }
  .lm-workcard-badge svg { transform: none; }
}
/* The button is centred under a full-width grid of four large images, so it needs
   more clear space beneath them than a row-to-row gap would give it — at the
   grid's own 1.17rem rhythm it read as a fifth item in the grid rather than as
   the section's closing action. */
.lm-work-foot { display: flex; justify-content: center; padding-top: 3rem; }

/* Closing CTA band, still emitted by the optional `cta` section. */
.lm-portfolio-title {
  width: fit-content; margin-inline: auto;
  font-size: calc(clamp(1.85rem, 3.2vw, 2.7rem) * var(--h-scale, 1));
  font-weight: var(--fw-display); line-height: 1.06; letter-spacing: -0.022em;
}
.lm-card-tags {
  margin-top: 1.25rem;
  display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem;
}

@media (min-width: 768px) {
  .lm-workgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ==========================================================================
   Service & expertise
   Four equal cards, 240x280 at 26px gaps, the row centred in the shell.

   The active card is whichever one the pointer or keyboard focus is on —
   there is no timer, no preselected card and no "next/previous", so this is a
   state change rather than a carousel. Inactive cards are left completely
   alone: they don't dim, desaturate or shrink, they simply aren't the one
   showing its detail.

   The card box never changes: `min-height` plus `align-content: center` fixes
   the height, and the swap is done entirely inside it by collapsing the icon
   to zero height while the detail opens. That keeps the row from reflowing —
   nothing outside the card moves, and no page scroll is disturbed. Only
   heights, opacities and one transform animate; `align-content` itself is
   never animated, which is why the icon carries the collapse.
   ========================================================================== */
.lm-services { background: var(--bg); }
.lm-servicegrid {
  display: grid; grid-template-columns: 1fr; gap: 1.53rem;
  max-width: 60.9rem; margin-inline: auto;
}
/* The link is the card, so the list item is only there to be stretched by the
   grid — that's what keeps all four cards the same height in a row. */
.lm-service-li { display: flex; }
.lm-service {
  position: relative; flex: 1;
  display: grid; align-content: center; justify-items: center;
  min-height: 16.4rem;
  border: 1px solid #E5E5E5; border-radius: var(--radius-card);
  padding: 1.7rem 1.47rem;
  background: var(--bg-alt);
  text-align: center;
  transition: background-color var(--t-state) var(--ease-out-cubic),
              border-color var(--t-state) var(--ease-out-cubic),
              box-shadow var(--t-state) var(--ease-out-cubic);
}
/* The 10x10 outlined square the design sits just above the bottom edge. */
.lm-service::after {
  content: ""; position: absolute; bottom: 0.59rem; left: 50%;
  width: 0.59rem; height: 0.59rem; margin-left: -0.295rem;
  border: 2px solid #BEBEBE; border-radius: 0.12rem;
}
.lm-service-icon {
  overflow: hidden;
  width: 4.7rem; height: 4.7rem;
  display: grid; place-items: center;
  margin-bottom: 1.41rem;
  border-radius: var(--radius-pill);
  background: var(--accent-icon); color: var(--white);
  font-size: 2rem;
  transition: height var(--t-state) var(--ease-out-cubic),
              margin-bottom var(--t-state) var(--ease-out-cubic),
              opacity var(--t-state) var(--ease-out-cubic),
              transform var(--t-state) var(--ease-out-cubic);
}
.lm-service-head {
  width: 100%;
  /* Transparent at rest so revealing the rule can't shift the text by a pixel. */
  border-bottom: 1px solid transparent;
  transition: padding-bottom var(--t-state) var(--ease-out-cubic),
              border-color var(--t-state) var(--ease-out-cubic);
}
.lm-service-head h3 {
  font-size: 1.41rem; font-weight: var(--fw-display);
  line-height: 1.2; letter-spacing: -0.01em; color: var(--ink);
}
/* The disclosure animates `grid-template-rows` from `0fr` to `1fr` rather than
   `max-height` from 0 to a fixed value. `max-height` has to be told a number,
   and the number has to clear the tallest description any service might be
   given, so it always overshoots: at 10rem against copy that measures 78-104px
   the card finished visibly opening about 180ms into a 300ms transition and then
   eased through 66px of empty space it had already stopped showing — while the
   text was still fading in behind it. The whole settling half of out-cubic, the
   part that makes the move read as considered, was spent where nobody could see
   it. `1fr` resolves to whatever the paragraph actually is, so the box stops
   growing at the same moment the opacity lands, and CMS copy of any length is
   safe from clipping. */
.lm-service-body {
  overflow: hidden;
  display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 0fr;
  width: 100%;
  font-size: 0.88rem; line-height: 1.733; color: #404040;
  opacity: 0;
  transition: grid-template-rows var(--t-state) var(--ease-out-cubic),
              opacity var(--t-state) var(--ease-out-cubic);
}
/* Required by the technique: without it the `auto` minimum of the row floors at
   the paragraph's own height and the closed state is never actually closed. */
.lm-service-body > * { min-height: 0; }
/* A one-line description reads as a sentence under the centred title; a list
   is set flush-left but the block itself stays optically centred, so neither
   version needs the text alignment to change mid-transition. */
.lm-service-list {
  width: fit-content; margin-inline: auto;
  display: flex; flex-direction: column;
  text-align: left;
}
.lm-service-list li { display: flex; align-items: baseline; gap: 0.45rem; }
.lm-service-list svg {
  width: 0.72rem; height: 0.72rem; flex: none;
  color: var(--accent);
}
/* Keyboard equivalent: focusing the card puts it in exactly the hover state. */
.lm-service:focus-visible .lm-service-icon {
  height: 0; margin-bottom: 0; opacity: 0; transform: scale(0.85);
}
.lm-service:focus-visible .lm-service-head {
  padding-bottom: 0.94rem; border-bottom-color: #E7E5F7;
}
.lm-service:focus-visible .lm-service-body { grid-template-rows: 1fr; opacity: 1; }
.lm-service:focus-visible {
  background: var(--white); border-color: #E1DCF9;
  box-shadow: 0 1rem 2.35rem rgba(10, 10, 10, 0.06);
}
@media (hover: hover) and (pointer: fine) {
  .lm-service:hover .lm-service-icon { height: 0; margin-bottom: 0; opacity: 0; transform: scale(0.85); }
  .lm-service:hover .lm-service-head { padding-bottom: 0.94rem; border-bottom-color: #E7E5F7; }
  .lm-service:hover .lm-service-body { grid-template-rows: 1fr; opacity: 1; }
  .lm-service:hover {
    background: var(--white); border-color: #E1DCF9;
    box-shadow: 0 1rem 2.35rem rgba(10, 10, 10, 0.06);
  }
}
/* Touch: there is no hover to offer, so the detail is simply always open and
   the icon stays with it. A tap follows the card's link, exactly as it looks
   like it should — no tap-to-reveal step competing with navigation. Cards are
   free to be as tall as their content here, which is why the fixed height and
   the collapse both stand down. */
@media (hover: none) {
  .lm-service { min-height: 0; }
  .lm-service-head { padding-bottom: 0.94rem; border-bottom-color: #E7E5F7; }
  .lm-service-body { grid-template-rows: 1fr; opacity: 1; padding-top: 0.94rem; }
}

@media (min-width: 640px) {
  .lm-servicegrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .lm-servicegrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ==========================================================================
   Recommendations
   Three quiet cards, 392x240 at 24px gaps. This is the most restrained state
   change on the page by design: the hovered card lifts from the tinted
   surface to white, its stars warm from grey-violet to the accent, and its
   role line steps back so the quote stays the loudest thing on the card.
   Nothing scales and nothing moves — and the stars deliberately stay smaller
   and quieter than the quote they sit beside.
   ========================================================================== */
.lm-reviews { background: var(--bg); }
.lm-reviewgrid { display: grid; grid-template-columns: 1fr; gap: 1.41rem; }
.lm-review {
  display: flex; flex-direction: column; justify-content: space-between; gap: 1.17rem;
  min-height: 14.1rem;
  border: 1px solid var(--line); border-radius: var(--radius-card-sm);
  padding: 1.17rem 1.41rem;
  background: #F5F6FB;
  transition: background-color var(--t-state) var(--ease-out-cubic);
}
.lm-review-quote {
  font-family: var(--font-quote); font-style: italic;
  font-size: 0.94rem; line-height: 1.375; color: var(--ink);
}
.lm-review-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  border-top: 1px solid var(--line);
  padding-top: 1.17rem;
}
.lm-review-who { display: flex; align-items: center; gap: 0.7rem; min-width: 0; }
.lm-review-dp {
  width: 2.58rem; height: 2.58rem; flex: none;
  border: 1px solid #E5E5E5; border-radius: var(--radius-pill);
  background: #F0EFEA;
  object-fit: cover;
}
.lm-review-id { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.lm-review-name { font-size: 0.88rem; font-weight: var(--fw-medium); line-height: 1.4; }
.lm-review-role {
  font-size: 0.76rem; font-weight: var(--fw-medium); line-height: 1.4;
  color: var(--ink);
  transition: color var(--t-state) var(--ease-out-cubic);
}
.lm-review-stars {
  flex: none; display: inline-flex; gap: 0.1rem;
  font-size: 0.94rem; color: var(--accent-muted);
  transition: color var(--t-state) var(--ease-out-cubic);
}
@media (hover: hover) and (pointer: fine) {
  .lm-review:hover { background: var(--white); }
  .lm-review:hover .lm-review-role { color: var(--ink-faint); }
  .lm-review:hover .lm-review-stars { color: #7A67F5; }
}

@media (min-width: 640px) {
  .lm-reviewgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .lm-reviewgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ==========================================================================
   FAQ
   Visual left, accordion right — 440 against 723 with a 77px channel.

   The picture is tinted the way the design builds it: the photograph is
   partly desaturated and a flat violet is laid over it in `hue` blend mode,
   so the image keeps its own light and shade while taking the accent's
   colour. `isolation: isolate` confines that blend to the figure instead of
   letting it reach the page behind.
   ========================================================================== */
.lm-faq { background: var(--bg); }
.lm-faq-row { display: grid; grid-template-columns: 1fr; gap: 2.2rem; align-items: start; }
.lm-faq-visual {
  position: relative; overflow: hidden; isolation: isolate;
  aspect-ratio: 440 / 365;
  border-radius: var(--radius-card-sm);
  background: var(--bg-alt);
}
.lm-faq-visual img {
  width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(0.36);
}
.lm-faq-tint {
  position: absolute; inset: 0;
  background: #B6AFFF;
  mix-blend-mode: hue;
  pointer-events: none;
}

@media (min-width: 1024px) {
  .lm-faq-row { grid-template-columns: 1fr 1.643fr; gap: 4.5rem; }
}

/* ==========================================================================
   Stats
   ========================================================================== */
.lm-stats { background: var(--bg); }
.lm-stats-in { padding: 0 var(--gutter) 5rem; }
.lm-stats-panel {
  border-radius: var(--radius-card);
  background: var(--ink);
  padding: 3rem 1.5rem;
  color: var(--white);
}
.lm-stats-title {
  margin-top: 1rem; max-width: 20ch;
  font-size: calc(1.875rem * var(--h-scale, 1)); font-weight: var(--fw-medium); letter-spacing: -0.01em;
}
.lm-stats-grid {
  margin-top: 3.5rem;
  display: grid; grid-template-columns: repeat(2, 1fr);
  column-gap: 2rem; row-gap: 3rem;
}
.lm-stat-value {
  font-size: 3rem; font-weight: var(--fw-heading); letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.lm-stat-label {
  margin-top: 0.75rem; font-size: 0.875rem; line-height: 1.5;
  color: rgba(255, 255, 255, 0.55);
}

@media (min-width: 640px) {
  .lm-stats-in { padding-inline: var(--gutter-lg); }
  .lm-stats-panel { padding: 4rem 2rem; }
  .lm-stat-value { font-size: 3.75rem; }
}
@media (min-width: 768px) {
  .lm-stats-panel { padding-inline: 4rem; }
  .lm-stats-title { font-size: calc(2.25rem * var(--h-scale, 1)); }
  .lm-stat-value { font-size: 4.5rem; }
}
@media (min-width: 1024px) {
  .lm-stats-in { padding-bottom: 7rem; }
  .lm-stats-grid { grid-template-columns: repeat(4, 1fr); }
}

/* ==========================================================================
   Footer
   The closing statement: near-black, the CTA and the link columns on one row,
   then the oversized wordmark, then the legal bar.

   Its content column is wider than the sections' — a 64px margin against
   their 100px — so it carries its own max-width rather than --shell. 1312 of
   content inside 2 x 64 is 1440 at the 1440px root size, which is why the cap
   is 84.5rem: the design exactly at 1440, and centred beyond it.

   The wordmark used to be absolutely positioned and bled off the bottom edge.
   It is a normal row in the flow now, as the design has it, which is also what
   lets it push the legal bar rather than sit behind it.
   ========================================================================== */
.lm-footer { position: relative; overflow: hidden; background: var(--ink); color: var(--white); }
.lm-footer-in {
  max-width: 84.5rem; margin-inline: auto;
  display: flex; flex-direction: column; gap: 2.35rem;
  padding: 4rem var(--gutter) 1.76rem;
}
.lm-footer-top {
  display: flex; flex-wrap: wrap; gap: 3.52rem;
  padding-bottom: 2.7rem;
}
.lm-footer-cta { flex: 1 1 22rem; display: flex; flex-direction: column; gap: 1.17rem; }
.lm-footer-heading {
  font-size: clamp(2.1rem, 4.4vw, 3.29rem);
  font-weight: var(--fw-display); line-height: 1.06; letter-spacing: -0.012em;
  color: #EDEDED;
}
.lm-footer-tagline {
  max-width: 34ch;
  font-size: 0.82rem; line-height: 1.6; color: #8A8A8A;
}
.lm-footer-actions {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 2.58rem;
  padding-top: 1.17rem;
}
.lm-footer-contact { display: flex; flex-direction: column; gap: 0.5rem; }
/* Read as a rule under the address rather than a box around it: every other
   divider in the design is a hairline, and there is no square-cornered
   outline anywhere else on the page. */
.lm-footer-mail {
  display: inline-flex; align-items: baseline; gap: 0.47rem;
  border-bottom: 1px solid #595959;
  padding-block: 0.7rem;
  font-size: 1.41rem; font-weight: var(--fw-medium); line-height: 1.25;
  letter-spacing: -0.008em; color: var(--white);
  transition: border-color var(--t-state) var(--ease-out-cubic);
}
.lm-footer-mail-arrow {
  flex: none; font-size: 1.35rem; color: var(--ink-faint);
  transition: transform var(--t-state) var(--ease-out-cubic),
              color var(--t-state) var(--ease-out-cubic);
}
@media (hover: hover) and (pointer: fine) {
  .lm-footer-mail:hover { border-color: var(--white); }
  .lm-footer-mail:hover .lm-footer-mail-arrow { transform: translateX(3px); color: var(--white); }
}
.lm-footer-cols {
  flex: 1 1 22rem;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2.82rem;
}
/* The padding stays although the rule that used to sit on top of it has moved:
   it is what keeps the three column titles level with each other and with the
   contact block opposite, and dropping it would lift them 9px. */
.lm-footer-col { padding-top: 0.53rem; }
/* The hairline belongs under the column title, between the label and the links
   it introduces, rather than above it where it read as a rule the title was
   hanging off. Full column width, because the h3 is a block. */
.lm-footer-coltitle {
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  padding-bottom: 0.55rem;
  margin-bottom: 0.35rem;
  font-size: 0.7rem; font-weight: var(--fw-label); line-height: 1;
  text-transform: uppercase; letter-spacing: 0.054em;
  color: #818181;
}
.lm-footer-col li,
.lm-footer-col a {
  font-size: 0.82rem; font-weight: var(--fw-medium); line-height: 2.35;
  color: #BBBBBB;
}
.lm-footer-col a { transition: color var(--t-state) var(--ease-out-cubic); }
@media (hover: hover) and (pointer: fine) {
  .lm-footer-col a:hover { color: var(--white); }
}
.lm-footer-legal {
  display: flex; flex-direction: column; align-items: center; justify-content: space-between;
  gap: 1rem;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  padding-top: 1.29rem;
  font-size: 0.76rem; font-weight: var(--fw-medium); color: #8A8A8A;
}
.lm-footer-legal-links { display: flex; flex-wrap: wrap; gap: 1.64rem; }
/* The oversized mark: a row of its own, centred, at ~73% of the footer
   column. Both forms composite to the design's #474749 over the near-black
   ground — the word carries that colour directly, the white logo is dimmed
   to the same value. */
.lm-footer-watermark {
  padding-top: 1.41rem;
  text-align: center; pointer-events: none; user-select: none;
}
/* `margin-inline: auto` and not the parent's `text-align: center`: the reset at
   the top of this file makes every img a block, and text-align has no effect on
   one — so a 73%-wide block sat flush against the left edge of the footer. The
   word form below is inline text and is centred by the text-align; this is the
   same job for the replaced-element form. */
.lm-footer-watermark img {
  width: 73%; max-width: 56rem; height: auto;
  margin-inline: auto;
  opacity: 0.14;
}
/* Text form of the same watermark, used when "Oversized watermark word" is set
   in Site Settings → Navigation & footer. Sized off the viewport rather than a
   fixed value so a long word still fits at 360px, and clipped by the footer's
   own overflow instead of widening the page. */
.lm-footer-watermark span {
  display: block;
  font-weight: var(--fw-display); line-height: 0.8;
  font-size: clamp(3.5rem, 15vw, var(--text-watermark));
  letter-spacing: -0.03em;
  color: rgba(71, 71, 73, 0.5);
  white-space: nowrap;
}

@media (min-width: 640px) {
  .lm-footer-in { padding-inline: 3.76rem; }
  .lm-footer-legal { flex-direction: row; }
}
@media (min-width: 1024px) {
  /* Desktop keeps its own top padding, but a larger one than the shorthand
     above: the footer is the last thing on the page and the heading is the
     first thing in it, so the black needs a run-up before the type starts.
     This was 0.94rem, which put the heading 16px below the edge and read as
     the section having been cut off rather than begun. */
  .lm-footer-in { padding-top: 3.5rem; }
  .lm-footer-cols { flex: 0 1 35.9rem; grid-template-columns: repeat(3, minmax(0, 10.1rem)); }
}

/* ==========================================================================
   Inner pages — Work, About, Contact, case studies
   These reuse the class names blocks.js already emits (.section, .case-*,
   .content-slot, .reveal) so the block renderer needs no changes.
   ========================================================================== */

/* A shared draft link renders the real page, so the only thing distinguishing it
   is this bar. Deliberately loud and above the fixed site header, because the
   failure mode it prevents is a client reviewing a draft they believe is live. */
.lm-draftbar {
  position: relative;
  z-index: 60;
  padding: 0.75rem var(--gutter);
  background: var(--ink);
  color: var(--bg);
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
  text-align: center;
}

/* The header is absolutely positioned over the hero on the home page; inner
   pages have no hero plate, so they reserve the space instead. */
.lm-pagehero {
  padding: 8rem var(--gutter) 3rem;
  border-bottom: 1px solid var(--line);
}
.lm-pagehero h1 {
  max-width: 20ch;
  /* Was 2.25rem -> 3.25rem at 640px, flat after. */
  font-size: clamp(2.25rem, 1.75rem + 2.5vw, 4.25rem);
  font-weight: var(--fw-heading);
  /* Was 1.02 — same clipping issue as .lm-hero-h1 above. */
  line-height: 1.08; letter-spacing: -0.02em;
}
.hero-tagline {
  margin-top: 1.25rem; max-width: 52ch;
  font-size: 1.125rem; line-height: 1.6;
  color: var(--ink-soft);
}
.eyebrow {
  font-size: 0.75rem; font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-faint);
  margin-bottom: 1.25rem;
}
.t-meta { font-size: 0.75rem; letter-spacing: 0.025em; }

.section { padding: 4rem var(--gutter); }
.section-head { margin-bottom: 2rem; }
.section-head h2 {
  /* Was 1.875rem -> 2.25rem at 640px, flat after. */
  font-size: clamp(1.875rem, 1.6rem + 1.1vw, 2.5rem);
  font-weight: var(--fw-heading); letter-spacing: -0.015em;
  max-width: 22ch;
}
.section-intro { max-width: 52ch; font-size: 1rem; line-height: 1.6; color: var(--ink-soft); }

/* Case-study blocks used to come out of the block renderer as a bare
   `.section > div`, and this is where the block region got its max-width back.
   The renderer now emits its own band (`.case-block` / `.case-block-in`, further
   down) because a block needs to be able to opt *out* of the band to go
   full-bleed, which a shared `.section` rule cannot express. */

@media (min-width: 640px) {
  .lm-pagehero { padding: 9rem var(--gutter-lg) 3.5rem; }
  .section { padding: 5rem var(--gutter-lg); }
}

/* Shared scroll reveal for markup that predates the data-reveal system: the
   case-study blocks carry it. Same length as [data-reveal] above, from the same
   token, so a case study's blocks cannot arrive at a different speed from the
   sections on the home page. */
.reveal {
  opacity: 0; transform: translateY(1.25rem);
  transition: opacity var(--t-reveal) var(--ease-soft), transform var(--t-reveal) var(--ease-soft);
}
.reveal.is-in { opacity: 1; transform: none; }

/* ---- Work grid ----
   The Work index renders the same .lm-workcard tiles as the home page's
   Selected Works grid, from the same render helper, so there is nothing to
   style here. The older .agr-project-* treatment it used to have — image, then
   a permanent title with an arrow glyph below it — has gone with it; two card
   idioms for the same object was the whole problem. See "Selected works" above
   for the tile, its hover band and its corner control. */

/* ---- About page ---- */
.about-grid { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
.about-grid-title {
  font-size: 0.75rem; font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-faint);
}
.about-portrait {
  margin-top: 1.5rem; width: 100%; max-width: 20rem;
  border-radius: var(--radius-card-sm);
  overflow: hidden; background: var(--bg-alt);
}
.about-portrait img { width: 100%; height: auto; }
.bio-lead {
  font-size: 1.375rem; font-weight: var(--fw-medium);
  line-height: 1.45; letter-spacing: -0.01em;
}
.bio-para { margin-top: 1.25rem; font-size: 1rem; line-height: 1.7; color: var(--ink-soft); }

.capability-list { border-top: 1px solid var(--line); }
.capability-row {
  display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
  border-bottom: 1px solid var(--line);
  padding: 1.5rem 0.25rem;
  transition: padding 0.4s var(--ease-spring), background-color 0.4s var(--ease-spring);
}
.capability-row h4 {
  font-size: 1.25rem; font-weight: var(--fw-medium); letter-spacing: -0.01em;
}
.capability-row p {
  margin-top: 0.375rem; max-width: 48ch;
  font-size: 0.875rem; line-height: 1.6; color: var(--ink-soft);
}
.project-arrow { color: var(--ink-faint); font-size: 1rem; flex: none; }
@media (hover: hover) and (pointer: fine) {
  a.capability-row:hover {
    background: var(--bg-alt);
    padding-left: 1rem; padding-right: 1rem;
  }
  a.capability-row:hover .project-arrow { color: var(--accent); }
}
@media (min-width: 1024px) {
  .about-grid { grid-template-columns: 20rem 1fr; gap: 4rem; }
  .bio-lead { font-size: 1.5rem; }
}

/* ---- Contact page ---- */
.contact-lead-link {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: 1.125rem; font-weight: var(--fw-medium); letter-spacing: -0.01em;
}
.contact-lead-link:hover { color: var(--accent); }
.btn-arrow { display: inline-block; transition: transform 0.3s var(--ease-snap); }
.contact-lead-link:hover .btn-arrow { transform: translateX(3px); }

.acc-sublist { display: flex; flex-direction: column; gap: 0.625rem; }
.acc-sublist li {
  position: relative; padding-left: 1.25rem;
  font-size: 1rem; line-height: 1.6; color: var(--ink-soft);
}
.acc-sublist li::before {
  content: ""; position: absolute; left: 0; top: 0.6em;
  width: 0.375rem; height: 0.375rem; border-radius: var(--radius-pill);
  background: var(--accent);
}

.contact-form { max-width: 44rem; }
.form-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
.form-field { display: flex; flex-direction: column; gap: 0.375rem; margin-bottom: 1rem; }
.form-field label {
  font-size: 0.75rem; font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.025em;
  color: rgba(10, 10, 10, 0.5);
}
.form-field input,
.form-field select,
.form-field textarea {
  width: 100%;
  border: 1px solid var(--line);
  background: var(--bg-alt);
  border-radius: var(--radius-control);
  padding: 0.75rem 1rem;
  font-size: 0.875rem; color: var(--ink);
  outline: none;
  transition: border-color 0.25s, background-color 0.25s;
}
.form-field textarea { resize: vertical; min-height: 8rem; }
.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus { border-color: rgba(10, 10, 10, 0.3); background: var(--white); }
.form-footer {
  margin-top: 0.5rem;
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem;
}
.form-status { font-size: 0.875rem; color: var(--ink-soft); }
.form-status.is-error { color: #B3261E; }
.btn {
  display: inline-flex; align-items: center; gap: 0.625rem;
  border-radius: var(--radius-pill);
  padding: 0.875rem 1.75rem;
  font-size: 0.875rem; font-weight: var(--fw-medium);
  transition: transform 0.35s var(--ease-snap), opacity 0.3s;
}
.btn-solid { background: var(--ink); color: var(--white); }
.btn[disabled] { opacity: 0.6; cursor: default; }
@media (hover: hover) and (pointer: fine) {
  .btn:not([disabled]):hover { transform: scale(1.03); }
}
@media (min-width: 640px) { .form-grid { grid-template-columns: 1fr 1fr; } }

/* ==========================================================================
   Case study
   The block renderer (worker/src/render/blocks.js) emits exactly the classes
   below. Two conventions keep this section navigable:

     .case-*   structure — one class per element the renderer produces.
     .is-*     a setting the owner chose, and ONLY when it differs from the
               default, so the base rule carries the defaults and there is no
               `.is-align-left` here to write or to leave dead.

   Every layout decision lives in this file. The renderer writes no inline
   layout, so a phone can stack what a desktop puts side by side — the v11
   image+text block set `flex-direction:row-reverse` inline and no breakpoint
   could undo it.
   ========================================================================== */

.case-meta-grid {
  margin-top: 2.5rem;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1.5rem;
  max-width: 44rem;
}
.case-meta-grid > div { display: flex; flex-direction: column; gap: 0.375rem; }
.case-meta-grid span {
  font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-faint);
}
.case-meta-grid strong { font-size: 1rem; font-weight: var(--fw-medium); }

/* The cover plate above the blocks, rendered by routes/public.js. */
.case-cover {
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--bg-alt);
}
.case-cover.is-placeholder { aspect-ratio: 16 / 9; }
.content-slot {
  width: 100%; height: 100%; min-height: 12rem;
  display: grid; place-items: center;
  background: linear-gradient(135deg, var(--bg-alt), var(--surface-2));
  color: var(--line-strong);
  border-radius: inherit;
}
.content-slot svg { width: 2rem; height: 2rem; }

.case-next-link {
  font-size: 1.5rem; font-weight: var(--fw-medium); letter-spacing: -0.01em;
}
.case-next-link:hover { color: var(--accent); }

/* ---- the block band -----------------------------------------------------
   The section owns the gutter and the vertical rhythm; `.case-block-in` owns
   the measure. `is-w-full` drops both, which is how a gallery goes edge to
   edge without the negative-margin trick a shared wrapper would have needed.

   Spacing is a named step resolved through --pt/--pb, so the five steps are
   re-scaled for wider screens in ONE place instead of eight media-query
   overrides. */
.case-block {
  --sp-none: 0rem;
  --sp-small: 1.5rem;
  --sp-medium: 3rem;
  --sp-large: 5rem;
  --sp-huge: 7.5rem;
  --t: 1;
  padding-inline: var(--gutter);
  padding-top: var(--pt, var(--sp-none));
  padding-bottom: var(--pb, var(--sp-medium));
}
.case-block-in { max-width: var(--shell); margin-inline: auto; }

.case-block.is-w-narrow .case-block-in { max-width: 46rem; }
.case-block.is-w-medium .case-block-in { max-width: 62rem; }
.case-block.is-w-full { padding-inline: 0; }
.case-block.is-w-full .case-block-in { max-width: none; }

.case-block.is-pt-small { --pt: var(--sp-small); }
.case-block.is-pt-medium { --pt: var(--sp-medium); }
.case-block.is-pt-large { --pt: var(--sp-large); }
.case-block.is-pt-huge { --pt: var(--sp-huge); }
.case-block.is-pb-none { --pb: var(--sp-none); }
.case-block.is-pb-small { --pb: var(--sp-small); }
.case-block.is-pb-large { --pb: var(--sp-large); }
.case-block.is-pb-huge { --pb: var(--sp-huge); }

.case-block.is-align-center { text-align: center; }
.case-block.is-align-right { text-align: right; }
/* A centred block centres its measure too, or the text would sit centred
   inside a column still hugging the left edge. */
.case-block.is-align-center .case-heading,
.case-block.is-align-center .case-prose,
.case-block.is-align-center .case-quote { margin-inline: auto; }

/* Text size is a multiplier, so one control moves the heading, the body and
   the pull-quote together and each keeps its own responsive curve. */
.case-block.is-t-small { --t: 0.875; }
.case-block.is-t-large { --t: 1.15; }
.case-block.is-t-display { --t: 1.5; }

@media (min-width: 640px) {
  .case-block {
    --sp-small: 2rem; --sp-medium: 4rem; --sp-large: 7rem; --sp-huge: 11rem;
    padding-inline: var(--gutter-lg);
  }
}

/* ---- panels: background + padding ---------------------------------------
   ONE treatment, worn by a whole block (`.case-block-in`), by a column
   (`.case-col`) or by either half of an image+text (`.case-split-media` /
   `-body`). The marker class is emitted only when the owner asked for something,
   so a block saved before panels existed carries no `.case-panel` and no rule
   here can reach it.

   The section keeps owning the rhythm BETWEEN blocks (--pt/--pb) and the panel
   owns the space INSIDE its colour, which is why the two never fight over the
   same property.

   Padding is a named step resolved through --pad-t/-r/-b/-l and re-scaled in
   three bands, exactly as the block band above re-scales --sp-*. The phone steps
   are much smaller than the desktop ones on purpose: "large" is generous at the
   width it was picked at, and shrinking a 5rem desktop inset mechanically would
   leave a 60ch measure sitting inside 4rem of nothing. */
.case-panel {
  --pad-xs: 0.5rem;
  --pad-small: 1rem;
  --pad-medium: 1.5rem;
  --pad-large: 2rem;
  --pad-xl: 2.5rem;
  --pad-xxl: 3rem;
  /* Reset rather than inherit, so a column panel inside a block panel starts from
     nothing — and so `none` is the one step that needs no class of its own. */
  --pad-t: 0rem; --pad-r: 0rem; --pad-b: 0rem; --pad-l: 0rem;
  padding: var(--pad-t) var(--pad-r) var(--pad-b) var(--pad-l);
  border-radius: var(--radius-card);
}
/* Both of these already drop the section's inline gutter, so their panel meets the
   viewport edge and a rounded corner would cut a notch out of it. */
.case-block.is-w-full > .case-panel,
.case-spacer > .case-panel { border-radius: 0; }

.case-panel.is-pad-t-xs { --pad-t: var(--pad-xs); }
.case-panel.is-pad-t-small { --pad-t: var(--pad-small); }
.case-panel.is-pad-t-medium { --pad-t: var(--pad-medium); }
.case-panel.is-pad-t-large { --pad-t: var(--pad-large); }
.case-panel.is-pad-t-xl { --pad-t: var(--pad-xl); }
.case-panel.is-pad-t-xxl { --pad-t: var(--pad-xxl); }
.case-panel.is-pad-r-xs { --pad-r: var(--pad-xs); }
.case-panel.is-pad-r-small { --pad-r: var(--pad-small); }
.case-panel.is-pad-r-medium { --pad-r: var(--pad-medium); }
.case-panel.is-pad-r-large { --pad-r: var(--pad-large); }
.case-panel.is-pad-r-xl { --pad-r: var(--pad-xl); }
.case-panel.is-pad-r-xxl { --pad-r: var(--pad-xxl); }
.case-panel.is-pad-b-xs { --pad-b: var(--pad-xs); }
.case-panel.is-pad-b-small { --pad-b: var(--pad-small); }
.case-panel.is-pad-b-medium { --pad-b: var(--pad-medium); }
.case-panel.is-pad-b-large { --pad-b: var(--pad-large); }
.case-panel.is-pad-b-xl { --pad-b: var(--pad-xl); }
.case-panel.is-pad-b-xxl { --pad-b: var(--pad-xxl); }
.case-panel.is-pad-l-xs { --pad-l: var(--pad-xs); }
.case-panel.is-pad-l-small { --pad-l: var(--pad-small); }
.case-panel.is-pad-l-medium { --pad-l: var(--pad-medium); }
.case-panel.is-pad-l-large { --pad-l: var(--pad-large); }
.case-panel.is-pad-l-xl { --pad-l: var(--pad-xl); }
.case-panel.is-pad-l-xxl { --pad-l: var(--pad-xxl); }

/* The five surfaces. `--panel-custom` is the owner's own colour, written inline by
   the renderer as a validated #rrggbb and by nothing else — the same contract as
   --focal and --swatch. */
.case-panel.is-bg-site { background: var(--panel-site); }
.case-panel.is-bg-light { background: var(--panel-light); }
.case-panel.is-bg-dark { background: var(--panel-dark); }
.case-panel.is-bg-accent { background: var(--panel-accent); }
.case-panel.is-bg-custom { background: var(--panel-custom, transparent); }

/* A dark surface re-declares the ink scale for everything inside it, and a light
   one declares it back. Both directions are needed: without the second group a
   light column panel nested in a dark block panel would keep white text on a pale
   background. A panel with padding but no background declares neither, so it
   inherits whatever context it is in. `is-ink-light` is set by the renderer from
   the custom colour's luminance, because the stylesheet cannot measure a value it
   is handed at runtime. */
.case-panel.is-bg-dark,
.case-panel.is-bg-accent,
.case-panel.is-ink-light {
  --ink: #FFFFFF;
  --ink-soft: rgba(255, 255, 255, 0.72);
  --ink-faint: rgba(255, 255, 255, 0.5);
  --line: rgba(255, 255, 255, 0.16);
  --line-strong: rgba(255, 255, 255, 0.28);
  --bg-alt: rgba(255, 255, 255, 0.08);
  --surface-2: rgba(255, 255, 255, 0.14);
  --accent: var(--accent-on-dark);
  color: var(--ink);
}
/* On a saturated surface the accent tint has nowhere to stand out, so a link there
   is plain white. */
.case-panel.is-bg-accent,
.case-panel.is-ink-light { --accent: #FFFFFF; }

/* Restating the light scale is what makes a light panel nested in a dark one
   readable again, so these have to be literals rather than inherited values.
   They track the :root palette above — re-valued with it from the old warm
   paper set to the design's neutral greys, because a case study is meant to be
   the same designed system as the homepage, not a second one. */
.case-panel.is-bg-site,
.case-panel.is-bg-light,
.case-panel.is-bg-custom:not(.is-ink-light) {
  --ink: #0A0A0A;
  --ink-soft: #343434;
  --ink-faint: #8A8A84;
  --line: #E7E7E7;
  --line-strong: #C4C4C9;
  --bg-alt: #F3F3F7;
  --surface-2: #E9E9EE;
  --accent: #6757FF;
  color: var(--ink);
}

@media (min-width: 640px) {
  .case-panel {
    --pad-small: 1.5rem; --pad-medium: 2.5rem; --pad-large: 4rem;
    --pad-xl: 5.5rem; --pad-xxl: 7rem;
  }
}
@media (min-width: 1024px) {
  .case-panel { --pad-large: 5rem; --pad-xl: 7rem; --pad-xxl: 9rem; }
}

/* ---- block typography --------------------------------------------------- */
/* The heading level is the owner's choice (h2/h3/h4) and carries its own size,
   which is why these are element-qualified: a level is semantic structure, and
   the size follows from it rather than being picked separately. */
.case-eyebrow { margin-bottom: 0.75rem; }
.case-heading {
  font-weight: var(--fw-heading); letter-spacing: -0.015em; line-height: 1.14;
  max-width: 26ch;
}
h2.case-heading { font-size: calc(clamp(1.875rem, 1.6rem + 1.1vw, 2.5rem) * var(--t)); }
h3.case-heading { font-size: calc(clamp(1.375rem, 1.25rem + 0.5vw, 1.625rem) * var(--t)); }
h4.case-heading {
  font-size: calc(clamp(1.0625rem, 1rem + 0.3vw, 1.1875rem) * var(--t));
  font-weight: var(--fw-medium); letter-spacing: -0.01em;
}

.case-prose {
  max-width: 68ch;
  font-size: calc(1rem * var(--t)); line-height: 1.75; color: var(--ink-soft);
}
.case-prose p + p { margin-top: 1.25rem; }
.case-prose strong { font-weight: var(--fw-medium); color: var(--ink); }
.case-prose em { font-style: italic; }
.case-prose a {
  color: var(--ink);
  text-decoration: underline; text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}
.case-prose a:hover { color: var(--accent); text-decoration-color: var(--accent); }
.case-heading + .case-prose { margin-top: 1rem; }

.case-caption {
  margin-top: 0.75rem;
  font-size: 0.8125rem; line-height: 1.5; color: var(--ink-faint);
}

/* ---- images ------------------------------------------------------------- */
/* Ratio, fit and focal point are the three controls Phase 10 asks for. A ratio
   implies a crop, so each ratio rule sets object-fit itself rather than relying
   on a separate class the owner did not choose. */
.case-media-img { width: 100%; height: auto; }
.case-media-img.is-r-square { aspect-ratio: 1 / 1; object-fit: cover; }
.case-media-img.is-r-4-3 { aspect-ratio: 4 / 3; object-fit: cover; }
.case-media-img.is-r-3-2 { aspect-ratio: 3 / 2; object-fit: cover; }
.case-media-img.is-r-16-9 { aspect-ratio: 16 / 9; object-fit: cover; }
.case-media-img.is-r-portrait { aspect-ratio: 4 / 5; object-fit: cover; }
.case-media-img.is-fit-contain { object-fit: contain; }
.case-media-img.is-f-top { object-position: 50% 0%; }
.case-media-img.is-f-bottom { object-position: 50% 100%; }
.case-media-img.is-f-left { object-position: 0% 50%; }
.case-media-img.is-f-right { object-position: 100% 50%; }
/* --focal is the one inline value the renderer writes: a validated "x% y%" the
   owner dragged, which is content, not layout. */
.case-media-img.is-f-custom { object-position: var(--focal, 50% 50%); }

/* ---- gallery ------------------------------------------------------------
   One grid, eight layouts. Every multi-column layout is a single column below
   768px — a phone gets a different composition, not a shrunken desktop one.
   The rounding lives on the image rather than the figure so a caption can sit
   outside the clipped box. */
.case-gallery { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.case-gallery-item { min-width: 0; }
.case-gallery-item .case-media-img,
.case-gallery-item .content-slot {
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--bg-alt);
}
.case-gallery.is-g-full { gap: 0; }
.case-gallery.is-g-full .case-media-img { border-radius: 0; }
/* Masonry is the one layout grid cannot express without fixed row tracks, so
   it drops to columns; `break-inside` is what keeps a figure whole. */
.case-gallery.is-g-masonry { display: block; column-gap: 1.25rem; }
.case-gallery.is-g-masonry .case-gallery-item { margin-bottom: 1.25rem; break-inside: avoid; }

/* The single mobile override in the whole block vocabulary: pair the images
   two-up on a phone instead of stacking them. Details, logo crops and packaging
   shots read fine at half a phone width, and eight of them stacked is a scroll
   nobody finishes.

   This is the one max-width layout query in an otherwise mobile-first
   stylesheet, and that is deliberate rather than a lapse: the setting means "on
   phones", so bounding it at 767px is what guarantees it cannot reach the
   desktop arrangement. Unbounded it would tie with `.case-gallery.is-g-*` on
   specificity and lose to it on source order — but only for the layouts that
   have a rule in the min-width blocks below. `single`, `full` and `masonry` have
   no grid-template-columns there, so nothing would ever override it, and a
   full-bleed image would sit at two columns at every width. */
@media (max-width: 767px) {
  .case-gallery.is-m-2 { grid-template-columns: repeat(2, 1fr); gap: .75rem; }
  /* Masonry is columns, not grid, at this width — grid-template-columns above is
     inert against `display: block`, so it needs saying the other way. */
  .case-gallery.is-g-masonry.is-m-2 { columns: 2; column-gap: .75rem; }
  .case-gallery.is-g-masonry.is-m-2 .case-gallery-item { margin-bottom: .75rem; }
}

@media (min-width: 640px) {
  .case-gallery { gap: 1.5rem; }
  .case-gallery.is-g-masonry { column-gap: 1.5rem; }
}
@media (min-width: 768px) {
  .case-gallery.is-g-two-equal { grid-template-columns: repeat(2, 1fr); }
  .case-gallery.is-g-two-large-small { grid-template-columns: 1.618fr 1fr; }
  .case-gallery.is-g-two-small-large { grid-template-columns: 1fr 1.618fr; }
  .case-gallery.is-g-three-equal { grid-template-columns: repeat(3, 1fr); }
  /* Featured grid: the first image leads at full width, the rest follow as a
     row beneath it — the rhythm change a case study needs between chapters. */
  .case-gallery.is-g-featured-grid { grid-template-columns: repeat(2, 1fr); }
  .case-gallery.is-g-featured-grid .case-gallery-item:first-child { grid-column: 1 / -1; }
  .case-gallery.is-g-masonry { columns: 2; }
}
@media (min-width: 1024px) {
  .case-gallery.is-g-featured-grid { grid-template-columns: repeat(3, 1fr); }
  .case-gallery.is-g-masonry { columns: 3; }
}

/* ---- image + text -------------------------------------------------------
   The image is always first in the source, so a phone leads with the picture
   and the desktop "image on the right" is a swap of two grid items — never a
   different DOM and never an inline direction the breakpoints cannot reach. */
.case-split { display: grid; grid-template-columns: 1fr; gap: 1.5rem; align-items: start; }
.case-split-media,
.case-split-body { min-width: 0; }
.case-split-media .case-media-img,
.case-split-media .content-slot {
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--bg-alt);
}
.case-split-body .case-prose { max-width: 56ch; }

@media (min-width: 768px) {
  .case-split { grid-template-columns: 1fr 1fr; gap: 2.5rem; }
  /* is-col-* sizes the MEDIA column; half is the default and needs no rule. */
  .case-block.is-col-third .case-split { grid-template-columns: 1fr 2fr; }
  .case-block.is-col-twothirds .case-split { grid-template-columns: 2fr 1fr; }
  .case-block.is-col-full .case-split { grid-template-columns: 1fr; }
  .case-split.is-media-right .case-split-media { order: 2; }
  .case-block.is-valign-center .case-split { align-items: center; }
  .case-block.is-valign-bottom .case-split { align-items: end; }
}
@media (min-width: 1024px) { .case-split { gap: 4rem; } }

/* ---- columns ------------------------------------------------------------ */
.case-cols { display: grid; grid-template-columns: 1fr; gap: 2rem; align-items: start; }
.case-cols.is-cols-1 { grid-template-columns: 1fr; }
.case-col { min-width: 0; }
/* A column is already narrow, so the prose measure would fight it. */
.case-col .case-prose { max-width: none; }
.case-col-media { margin-bottom: 1rem; }
.case-col-media .case-media-img,
.case-col-media .content-slot {
  overflow: hidden;
  border-radius: var(--radius-card-sm);
  background: var(--bg-alt);
}
@media (min-width: 768px) {
  .case-cols { gap: 2.5rem; }
  .case-cols.is-cols-2 { grid-template-columns: repeat(2, 1fr); }
  .case-cols.is-cols-3 { grid-template-columns: repeat(3, 1fr); }
  .case-block.is-valign-center .case-cols { align-items: center; }
  .case-block.is-valign-bottom .case-cols { align-items: end; }
}

/* ---- spacer, divider, quote, video -------------------------------------- */
/* A spacer is a block that is nothing but its own padding, so it wants no
   gutter and no measure. */
.case-spacer { padding-inline: 0; }
.case-rule { height: 0; border: 0; border-top: 1px solid var(--line); }

.case-quote { max-width: 42ch; }
.case-quote-text {
  font-size: calc(clamp(1.5rem, 1.3rem + 0.9vw, 1.875rem) * var(--t));
  font-weight: var(--fw-medium); line-height: 1.35; letter-spacing: -0.01em;
}
.case-quote-cite {
  margin-top: 1.25rem;
  display: flex; flex-direction: column; gap: 0.125rem;
  font-size: 0.875rem;
}
.case-quote-name { font-weight: var(--fw-medium); color: var(--ink-soft); }
.case-quote-role { font-size: 0.8125rem; color: var(--ink-faint); }

/* The letterbox is a literal, not var(--ink): inside a dark panel --ink is white,
   and a video with a white surround is a different block. */
.case-video {
  overflow: hidden;
  border-radius: var(--radius-card);
  background: #0A0A0A;
  aspect-ratio: 16 / 9;
}
.case-video-frame { width: 100%; height: 100%; border: 0; }

/* ---- brand identity -----------------------------------------------------
   `background` is a named treatment, not a colour picker: a mark shown on an
   arbitrary owner-chosen colour is how a portfolio ends up with a logo nobody
   can read. `none` is the default and needs no rule. */
.case-logo {
  display: grid; place-items: center;
  padding: 3rem var(--gutter);
  border-radius: var(--radius-card);
}
/* The plate is the owner's choice about the MARK, so it names a fixed surface and
   ignores the ink scale around it — a light plate stays light inside a dark panel,
   which is the entire point of putting a logo on one. */
.case-logo.is-bg-light { background: var(--panel-light); }
.case-logo.is-bg-dark { background: var(--panel-dark); }
.case-logo.is-bg-brand { background: var(--panel-accent); }
/* Overrides .case-media-img's width:100% — a mark is sized by its height so
   wide and tall lockups sit at the same optical weight. */
.case-logo-img { width: auto; max-width: 100%; max-height: 9rem; }
.case-logo.is-size-small .case-logo-img { max-height: 5rem; }
.case-logo.is-size-large .case-logo-img { max-height: 15rem; }
@media (min-width: 640px) { .case-logo { padding: 4.5rem var(--gutter-lg); } }

/* Logo variations — a hairline grid, so the sheet reads as one specimen board
   rather than a row of unrelated tiles. The 1px gap IS the rule. */
.case-logogrid {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: 1px; background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.case-logogrid-cell {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1rem;
  padding: 2rem 1rem;
  background: var(--white);
}
.case-logogrid.is-bg-light .case-logogrid-cell { background: var(--panel-light); }
.case-logogrid.is-bg-dark .case-logogrid-cell { background: var(--panel-dark); }
.case-logogrid.is-bg-brand .case-logogrid-cell { background: var(--panel-accent); }
.case-logogrid-mark { display: grid; place-items: center; min-height: 5rem; }
.case-logogrid-mark .case-logo-img { max-height: 5rem; }
.case-logogrid-label {
  font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-faint);
}
.case-logogrid.is-bg-dark .case-logogrid-label { color: rgba(255, 255, 255, 0.55); }
@media (min-width: 768px) { .case-logogrid { grid-template-columns: repeat(4, 1fr); } }

/* Palette — large swatches, because a brand colour presented as a small chip
   reads as a UI token instead of a decision. */
.case-palette { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
.case-swatch { display: flex; flex-direction: column; gap: 0.75rem; }
.case-swatch-chip {
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-card-sm);
  background: var(--swatch, var(--bg-alt));
  box-shadow: inset 0 0 0 1px rgba(10, 10, 10, 0.08);
}
.case-swatch-meta { display: flex; flex-direction: column; gap: 0.125rem; }
.case-swatch-name { font-size: 0.9375rem; font-weight: var(--fw-medium); }
.case-swatch-code {
  font-size: 0.75rem; letter-spacing: 0.02em;
  color: var(--ink-faint); font-variant-numeric: tabular-nums;
}
@media (min-width: 640px) {
  .case-palette { grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
}
@media (min-width: 1024px) { .case-palette { grid-template-columns: repeat(4, 1fr); } }

/* Type specimen — a ruled list, each row pairing the naming with the letters
   themselves at a size worth looking at. */
.case-type { border-top: 1px solid var(--line); }
.case-type-row {
  display: grid; grid-template-columns: 1fr; gap: 1rem;
  padding-block: 2rem;
  border-bottom: 1px solid var(--line);
}
.case-type-meta { display: flex; flex-direction: column; gap: 0.25rem; }
.case-type-name { font-size: 1.125rem; font-weight: var(--fw-medium); letter-spacing: -0.01em; }
.case-type-role {
  font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-faint);
}
.case-type-preview { min-width: 0; }
.case-type-specimen {
  font-size: calc(clamp(3rem, 1.5rem + 7vw, 6.5rem) * var(--t));
  font-weight: var(--fw-heading); line-height: 1; letter-spacing: -0.03em;
  overflow-wrap: anywhere;
}
.case-type-sample {
  margin-top: 1rem; max-width: 52ch;
  font-size: 1rem; line-height: 1.65; color: var(--ink-soft);
}
@media (min-width: 768px) {
  .case-type-row { grid-template-columns: 13rem 1fr; gap: 2.5rem; padding-block: 2.5rem; }
}

/* Before / after — two halves side by side from 640px up, stacked below, with
   the label riding on the image so the pairing survives the stack. */
.case-ba { display: grid; grid-template-columns: 1fr; gap: 1rem; }
.case-ba-half {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--bg-alt);
}
.case-ba-half.is-placeholder { aspect-ratio: 4 / 3; }
.case-ba-label {
  position: absolute; top: 0.75rem; left: 0.75rem;
  padding: 0.375rem 0.75rem;
  border-radius: var(--radius-pill);
  background: rgba(10, 10, 10, 0.7);
  color: var(--white);
  font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.09em;
}
@media (min-width: 640px) { .case-ba { grid-template-columns: repeat(2, 1fr); gap: 1.25rem; } }


/* ---- Accordion (homepage FAQ) ----
   `initAccordion()` in lumora.js owns the open/close behaviour, keeps one panel
   open per list, and swaps the icon's character between + and −. The button is
   a real <button>, so Enter and Space already work and the state is carried on
   aria-expanded.

   That attribute is what the open styling hangs off here. It previously had no
   rule at all, so the only signal a panel was open was the glyph swapping to a
   minus — easy to miss on a 28px control. Filling the circle makes the open row
   unmistakable without needing a line of JavaScript. */
.acc-list { border-top: 1px solid #DEDDD7; }
.acc-item { border-bottom: 1px solid #DEDDD7; }
.acc-question {
  width: 100%;
  display: flex; align-items: center; justify-content: space-between; gap: 1.29rem;
  padding-block: 1.76rem;
  text-align: left;
}
.acc-question h3 {
  font-size: 1.385rem; font-weight: var(--fw-display);
  line-height: 1.271; letter-spacing: -0.008em;
  color: var(--ink);
  transition: color var(--t-state) var(--ease-out-cubic);
}
.faq-icon {
  width: 1.64rem; height: 1.64rem; flex: none;
  display: grid; place-items: center;
  border: 1px solid #C7C6BE; border-radius: var(--radius-pill);
  font-size: 0.94rem; line-height: 1; color: var(--ink-faint);
  transition: background-color var(--t-state) var(--ease-out-cubic),
              border-color var(--t-state) var(--ease-out-cubic),
              color var(--t-state) var(--ease-out-cubic);
}
@media (hover: hover) and (pointer: fine) {
  .acc-question:hover h3 { color: var(--ink-faint); }
  .acc-question:hover .faq-icon { border-color: var(--ink); color: var(--ink); }
}
.acc-question[aria-expanded="true"] .faq-icon {
  background: var(--ink); border-color: var(--ink); color: var(--white);
}
/* The panel opens by animating `grid-template-rows` from `0fr` to `1fr`, the
   same mechanism `.lm-service-body` uses, driven purely by `.is-open` on the
   item. It used to animate `max-height` from 0 to a pixel value that
   `initAccordion()` measured with `scrollHeight` and wrote to the element's
   inline style on every click. Three problems, and all of them showed:

   `scrollHeight` is a forced synchronous layout, taken in the same click
   handler that had just toggled a class — so the first frame of the transition
   was the one competing with a reflow, and that is the frame a reader judges
   the whole move by.

   The measured height is only right for the width it was measured at. Resize
   the window with a panel open, or let a webfont land a moment late, and the
   inline pixel value no longer matches the text; it clips or leaves a gap, and
   nothing recomputes it.

   And an inline `max-height` on the way open versus an empty string on the way
   closed meant the two directions were not the same transition run backwards.
   `1fr` resolves to whatever the answer actually measures at the width it is
   actually at, in both directions, with no JavaScript and no measurement. */
.acc-answer {
  overflow: hidden;
  display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--t-state) var(--ease-out-cubic),
              opacity var(--t-state) var(--ease-out-cubic);
}
/* Required by the technique: without it the row's `auto` minimum floors at the
   answer's own height and the closed state is never actually closed. */
.acc-answer > * { min-height: 0; }
.acc-item.is-open .acc-answer { grid-template-rows: 1fr; opacity: 1; }
/* The gap under the answer is a block INSIDE the inner element, not padding on
   it. `min-height: 0` above zeroes a grid item's content box but cannot touch
   its padding — padding is part of the border box, so a padded grid item floors
   the `0fr` track at the padding's own height. With `padding-bottom` here every
   closed panel measured 1.76rem (~30px at 1440px) instead of nothing, which the
   old `max-height: 0` clipped and this technique does not. As a child box it
   collapses with the paragraph and `overflow: hidden` clips it, and the open
   panel measures exactly what it did before. */
.acc-answer-inner::after { content: ''; display: block; height: 1.76rem; }
.acc-answer p { max-width: 62ch; font-size: 0.94rem; line-height: 1.65; color: var(--ink-soft); }

/* ==========================================================================
   Reduced motion
   Springs, the count-up and the canvas brush all stand down, and nothing on the
   page travels. What survives is paint: colour, opacity, filter — including the
   scroll reveals, which fade in place rather than being skipped.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  /* "Reduce" means reduce *motion*, not remove every transition.
     This block used to say `transition-duration: 0.001ms !important`, which
     also killed the colour and opacity fades that carry every state on the
     site — the logo tile's grey-to-white, the work tile's overlay band, the
     designer portrait leaving greyscale, the header taking on its surface, the
     nav ink, the accordion's chrome. Nothing moved and nothing eased either,
     so every interaction landed as a hard cut. A cross-fade is what the
     guidance offers as the *replacement* for motion, not something to strip
     alongside it.

     So rather than zeroing the duration, the property list is narrowed to the
     paint-only properties. Everything that moves or resizes — transform,
     padding, width, height, grid-template-rows, bottom — falls out of the list
     and jumps straight to its final value, which is the behaviour this query
     exists for. Duration and easing are then pinned to the design system's own
     state tokens: without that, the durations declared per rule would be
     re-matched against this new property list by position and a property could
     inherit a length meant for a different one.

     Add a paint-only property here when you start transitioning one. Leave a
     motion property out on purpose. */
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-property: opacity, color, background-color, border-color, filter, box-shadow !important;
    transition-duration: var(--t-state) !important;
    transition-timing-function: var(--ease-out-cubic) !important;
  }

  /* The exceptions, and the reason they are exceptions.

     What this query protects against is movement the reader did not ask for and
     cannot avoid — things that travel across the page, arrive while being read,
     or take the scroll away. The hovers below are none of that: each is a few
     pixels of change inside a box whose outer bounds are fixed, and each one
     only happens because a pointer was deliberately put on it. The logo tile's
     mark grows 7% about its own centre inside a tile that does not move; the
     service card discloses its text inside a fixed `min-height`, so the row and
     everything under it stay exactly where they are.

     Leaving them in the blanket rule above was worse than either extreme. The
     paint half of each hover eased over 300ms while the geometry half of the
     same hover jumped on frame one — the tile's background faded to white while
     the logo snapped to its new size, the card's surface lifted while its icon
     vanished and its text appeared instantly. One state change arriving as two
     events at different speeds is what reads as broken.

     So each of these restates its own property list. Duration and easing still
     come from the `*` rule above, which is why only the list is repeated here —
     and why the list has to be repeated exactly, since transition properties
     are matched by position. Page-level movement stays neutralised: nothing
     travels, the nav overlay's items are simply present, and the smooth-scroll
     hijack never starts. */
  .lm-logotile img { transition-property: filter, transform !important; }
  .lm-service-icon { transition-property: height, margin-bottom, opacity, transform !important; }
  .lm-service-head { transition-property: padding-bottom, border-color !important; }
  .lm-service-body { transition-property: grid-template-rows, opacity !important; }
  /* The two arrows are the same class of thing: a glyph nudged a couple of
     pixels inside its own box, on a control the reader is pointing at. They are
     here because with the four above eased and these two still cutting, the
     arrow would become the one thing on the page that snaps. */
  .lm-workcard-badge svg { transition-property: transform !important; }
  .lm-btn-badge svg { transition-property: transform !important; }
  /* The FAQ panel. Same argument as the service card, and it is the one the
     `*` rule treated worst: `max-height` — now `grid-template-rows` — is a size
     property, so it fell out of the list and jumped, while the icon it sits
     under kept easing its fill over 300ms. Pressing one control and getting two
     answers at two different speeds is what "not smooth" describes. It is also
     the clearest case for an exception in the whole file: nothing here happens
     unless the reader presses a button asking for exactly this, and what they
     are asking for is to watch the answer appear. */
  .acc-answer { transition-property: grid-template-rows, opacity !important; }
  /* Entrances fade but do not travel. `transform` is left out of the list
     above, so the reveal's translateY never runs — the element is at its final
     position from the first frame and only its opacity moves, which is the
     cross-fade this query asks for in place of movement rather than a third
     thing that is neither.

     These used to force `opacity` to its final value here as well, which meant
     every section on the site was simply present, with no entrance at all. That
     is more than the query asks for: a fade is not motion. Note the base rules
     start these at `opacity: 0`, so the reveal now depends on lumora.js in this
     mode too, exactly as it already did in every other mode. */
  [data-reveal], .reveal { transform: none; }
  .lm-navmenu-item { opacity: 1; transform: none; }
}
