/* VOYCE static mirror — overrides.
   Undoes the parts of Squarespace's CSS that assume its JavaScript runtime is present. */

/* 1. Content is hidden by `animation: hideContent 2s` until the platform JS marks the body
      as booted. The body's tweak-global-animations-* classes are stripped at build time,
      but force visibility so nothing can stay blank. */
body section > .content-wrapper,
body .page-section > .content-wrapper { opacity: 1 !important; animation: none !important; }

[data-animation-role] { opacity: 1 !important; }
[data-animation-role="image"].preFlex,
[data-animation-role="image"] { clip-path: none !important; transform: none !important; }
.sqs-animation, .sqs-animation.loaded { opacity: 1 !important; transform: none !important; }

/* 2. WebGL image effects (liquid / film-grain / parallax) are not reproduced; the crisp
      underlying image is shown instead. Hide any leftover empty effect canvas. */
canvas.sqs-image-fx-canvas,
[data-fx] > canvas:empty { display: none !important; }
.sqs-image-fx img, .fluid-image-container img, img[data-fx] { opacity: 1 !important; visibility: visible !important; }

/* 3. Native video blocks built by voyce.js */
.voyce-native-video {
  display: block; width: 100%; height: 100%;
  object-fit: cover; background: #000;
}
.sqs-native-video { position: relative; overflow: hidden; }

/* 4. Mobile menu, opened by voyce.js */
body.voyce-menu-open .header-menu {
  opacity: 1 !important; visibility: visible !important;
  height: 100vh !important; display: block !important; pointer-events: auto !important;
  transform: none !important;
}
body.voyce-menu-open .header-menu-bg { opacity: 1 !important; }
.header-menu {
  /* Live transitions BOTH visibility and opacity, on different durations, with Material's
     standard easing. Measured on live at 390:
       transition-property: visibility, opacity
       transition-duration: 0.6s, 0.4s
       transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1), cubic-bezier(0.4, 0, 0.2, 1)
     Ours was `opacity .2s ease` - a guess that differed on all three. Shared chrome, so it
     was a style diff on every page at every viewport; openstate and point 4 both caught it. */
  transition: visibility 0.6s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 5. Accordions
   Live's own transplanted CSS already carries the whole mechanism:
       .accordion-item__dropdown        { display: none; overflow: hidden }
       .accordion-item__dropdown--open  { display: block }
   Two rules here used to reimplement that against an invented .accordion-item--active
   class, and the closed rule `.accordion-item .accordion-item__dropdown` (0,2,0) outranked
   live's `.accordion-item__dropdown--open` (0,1,0) - so even once the right class was set,
   the panel stayed display:none. Removed: voyce.js now drives live's own classes. */
.accordion-item__click-target, .accordion-item__title { cursor: pointer; }

/* 6. Images that were waiting on the platform loader */
img[data-src], img.loaded { opacity: 1 !important; }

/* 7. Section backgrounds baked in from the live rendered DOM (see bakebg.mjs).
      Squarespace's SectionWrapperController normally builds these at runtime. */
.section-background { position: absolute; inset: 0; overflow: hidden; }
.section-background > div { position: absolute; inset: 0; }
.section-background img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 1 !important; visibility: visible !important;
}
.voyce-bg-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block; border: 0;
}

/* 8. Rasterized WebGL section backgrounds (see rasterize.mjs). BackgroundGradient and
      BackgroundIsometric generate their artwork on a canvas with no source image, so a
      captured frame from the live site stands in for them. */
.voyce-bgfx {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 1 !important; visibility: visible !important;
}

/* ─────────────────────────────────────────────────────────────────────────────
   Scroll-driven header state — live's own rules, transplanted verbatim.

   Live carries EIGHT rules binding #header to the va-nav* body classes. Our harvest captured
   only the five-rule `va-navauto` family, and only as an inline <style> that the site-wide
   injection wrote into 11 of 27 pages. The `va-navscroll` family was missing entirely, which
   caused two visible defects:

     - index never hid its header on scroll. Its path is not in live's PAGES list so it never
       gets `va-navauto`, and the only hide rule we had required it. Live hides it through
       `body.va-navscroll.va-navhide`, which needs no va-navauto.
     - the header stayed IN the viewport once hidden. Live does not merely fade it, it slides
       it away with translateY(-100%). Measured at y=900: live #header top=-224, ours top=0.
       Ten invisible header items therefore sat over our content where live's had left.

   Read from live's rendered DOM via CSSOM, not from served HTML, because this site rewrites
   itself at runtime. Placed in voyce.css so it reaches all 27 pages rather than the 11 that
   happened to carry the inline copy. The `:not(.va-navhide)` rule is what makes the header
   visible at rest, and it is deliberately `!important` on live because it has to beat the
   va-navauto hide.
   ───────────────────────────────────────────────────────────────────────────── */
body.va-navscroll #header { transition: transform .35s, opacity .35s; }
body.va-navscroll.va-navhide #header { transform: translateY(-100%) !important; opacity: 0; }
body.va-navscroll:not(.va-navhide) #header { transform: none !important; opacity: 1 !important; }
body.va-navscroll #header:focus-within { transform: none !important; opacity: 1 !important; }
body.va-navsolid #header { background: rgba(0, 0, 0, .82); backdrop-filter: blur(12px); }
body.va-navsolid #header .header-background { opacity: 0; }
@media (min-width: 1024px) { body.va-navsolid #header { background: rgba(0, 0, 0, .6); } }
@media (prefers-reduced-motion: reduce) { body.va-navscroll #header { transition: none; } }


/* ─────────────────────────────────────────────────────────────────────────────
   FAIL-SAFE READABILITY — every page must be readable with JavaScript disabled.

   Live ships 3-6 preFade elements and its runtime adds the rest, so live fails VISIBLE. We
   ship 1,129 reveal classes in the HTML, so without this we fail INVISIBLE - which is exactly
   how one broken observer once blanked the whole site.

   TWO MISTAKES ALREADY MADE HERE, both corrected:
     1. The first version lived in voyce-refine.css, which is linked on two pages. The guarantee
        has to hold on all 27.
     2. It was scoped to `body.voyce-refine` - a class that voyce.js ADDS. With JS disabled the
        class never appears, so the rule never matched and the fail-safe was inert. Measured:
        86 of 99 reveal elements still hidden with JS off. A guard must never depend on the
        thing whose absence it guards against.

   `.voyce-js` is set on <html> by voyce.js. If the script does not run, it is never set, and
   everything below stays visible. Verified with javaScriptEnabled:false.
   ───────────────────────────────────────────────────────────────────────────── */
html:not(.voyce-js) .preFade,
html:not(.voyce-js) .preSlide,
html:not(.voyce-js) [class*="preFade"],
html:not(.voyce-js) [class*="preSlide"] {
  opacity: 1 !important;
  transform: none !important;
  visibility: visible !important;
}

/* ─────────────────────────────────────────────────────────────────────────────
   REDUCED MOTION — the reveal must COMPLETE, not just stop.

   Suppressing animation alone is not enough here. Our reveal elements start at opacity 0 and
   are made visible BY the animation, so a reader who asks for reduced motion would otherwise
   get a page with content permanently invisible - the strictest readers punished hardest.

   This began scoped to body.voyce-refine, i.e. two pages. Measured on the other 25:
   recording-studio left 12 of 36 reveals hidden and pricing 19 of 29. Same class of mistake as
   the JS-off fail-safe, which was also scoped too narrowly at first: a guarantee about
   readability has to be site-wide or it is not a guarantee.

   Verified with reducedMotion:'reduce'.
   ───────────────────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .preFade, .preSlide,
  [class*="preFade"], [class*="preSlide"] {
    opacity: 1 !important;
    transform: none !important;
    visibility: visible !important;
    transition: none !important;
    animation: none !important;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   preSlide -> slideIn. Live's SECOND reveal family, never implemented here.

   Measured on live's virgin /academy (no scroll): 75 preSlide elements against our 0.
   Live tags them on structural elements - links, list-item titles, carousel media, blocks -
   while preFade handles text (p, accordion titles). No element carries both.

   Pre-state is opacity 0 plus a translateY that VARIES per element (measured 14.13, 17.01,
   26.7, 29.67, 36.29, 126.41px; 50 of 75 have no translation and fade only). The per-element
   distance is carried on data-vslide by slide-transplant.mjs and applied via a custom property,
   so nothing is invented and elements without a measured distance simply fade, as on live.

   Duration .6s and timing-function ease match live; the per-element delay already ships as an
   inline style on the element, exactly as live does it.

   Applied ONLY when .voyce-js is present, so a JS failure leaves content visible.
   ───────────────────────────────────────────────────────────────────────────── */
html.voyce-slide .preSlide {
  opacity: 0;
  transform: translateY(var(--vslide, 0px));
  transition: transform .6s ease, opacity .6s ease;
  will-change: transform, opacity;
}
html.voyce-slide .preSlide.slideIn {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  html.voyce-js .preSlide,
  html.voyce-slide .preSlide.slideIn {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}


/* ─────────────────────────────────────────────────────────────────────────────
   WHY THE GATE IS `voyce-slide` AND NOT `voyce-js`.

   A REAL defect, seen in the owner's browser while every automated probe passed: assets ship
   under stable filenames (/assets/js/voyce.js), so a browser can hold a CACHED older script
   while receiving freshly built HTML. That older script still sets `voyce-js`, which was enough
   to trigger the preSlide hiding rule - but it has no idea preSlide exists, so it never adds
   slideIn. The content was hidden forever.

   Every probe missed it because probes append ?cb=<timestamp> and always fetch a fresh script.
   The owner's browser does not.

   `voyce-slide` is set ONLY by the build of voyce.js that can actually reveal these elements.
   Any older cached script leaves the class unset, the rule never applies, and the content stays
   visible. A hiding rule must never be reachable by code that cannot un-hide.
   ───────────────────────────────────────────────────────────────────────────── */


/* ═══════════════════════════════════════════════════════════════════════════
   ARMED-STATE MODEL — content is visible until the runtime that can reveal it says otherwise.

   THE DEFECT THIS REPLACES, measured three ways on the deployed preview:
       normal load ................ 0/17 hidden   (looked fine)
       STALE CACHED voyce.js ...... 19/27 hidden  (dead page)
       JavaScript disabled ........ 2/13 hidden

   Assets ship under stable filenames, so a browser can hold an OLD voyce.js while receiving new
   HTML. The old script set `voyce-js`, which was enough to disable the fail-safe and leave
   `.preFade{opacity:0}` (transplanted from live) in force - but it had no idea how to reveal
   anything. The owner saw a blank page while every probe passed, because probes append
   ?cb=<timestamp> and always fetch a fresh script.

   The fix is architectural, not another gate: the HIDDEN state is now applied BY THE RUNTIME,
   to each element it is actually managing, via `.vr-armed`. Anything that cannot reveal cannot
   arm, so it cannot hide. No JS, stale JS, partial JS or a thrown exception all end the same
   way - readable content.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Beat live's transplanted `.preFade{opacity:0}` whenever the runtime has not armed it. */
.preFade:not(.vr-armed),
.preSlide:not(.vr-armed),
[class*="preFade"]:not(.vr-armed),
[class*="preSlide"]:not(.vr-armed) {
  opacity: 1 !important;
  transform: none !important;
  visibility: visible !important;
}

/* Armed by the runtime: hold back, then transition in. */
.preFade.vr-armed  { opacity: 0 !important; transition: opacity .6s ease; }
.preSlide.vr-armed {
  opacity: 0 !important;
  transform: translateY(var(--vslide, 0px)) !important;
  transition: transform .6s ease, opacity .6s ease;
}
.preFade.vr-armed.fadeIn   { opacity: 1 !important; }
.preSlide.vr-armed.slideIn { opacity: 1 !important; transform: none !important; }

@media (prefers-reduced-motion: reduce) {
  .preFade.vr-armed, .preSlide.vr-armed,
  .preFade.vr-armed.fadeIn, .preSlide.vr-armed.slideIn {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }
}


/* ─────────────────────────────────────────────────────────────────────────────
   HEADER — same rule, same reasoning as the armed-state model above.

   Live's per-page inline script adds `va-navhide` to <body> at load, and it is OUR navScroll()
   that removes it below y=80. If our runtime does not run - stale cached script, thrown
   exception, JS disabled - nothing removes it and the entire header sits at opacity 0. Measured
   under a stale-JS simulation: 10 header items invisible, including every nav link.

   So the hide now requires `voyce-nav`, set only by the build that can also un-hide. Anything
   that cannot reveal the header cannot hide it either.
   ───────────────────────────────────────────────────────────────────────────── */
html:not(.voyce-nav) body.va-navauto.va-navhide #header,
html:not(.voyce-nav) body.va-navscroll.va-navhide #header,
html:not(.voyce-nav) body:has(#vacad).va-navhide #header {
  opacity: 1 !important;
  transform: none !important;
}


/* ─────────────────────────────────────────────────────────────────────────────
   TextShape decorations — live's hand-drawn strokes under highlighted words.

   Live injects, for each <span class="sqsrte-text-highlight">, a positioned
   <div class="TextShape-node"> holding an <svg><path>. On /access that is a black "marker"
   under "Underground" and a RED "underlineCurve" under "Getting Shit Done?". Our build injected
   none, so the red line the owner kept pointing at simply did not exist.

   It was invisible to every check I ran because I compared computed styles - colour, decoration,
   border, background, pseudo-elements, hover - on an element that was styled identically on both
   sides. The difference was a MISSING SIBLING ELEMENT, which no amount of style comparison can
   reveal. Only the side-by-side picture showed it.

   Stroke colour and width come from live's own values, harvested per data-text-attribute-id.
   Geometry is recomputed at runtime so the stroke follows reflow rather than being pinned to the
   width live happened to render at.
   ───────────────────────────────────────────────────────────────────────────── */
.TextShape-node {
  position: absolute;
  z-index: -1;
  pointer-events: none;
  opacity: 1;
}
.TextShape-node svg { width: 100%; height: 100%; overflow: visible; display: block; }
.TextShape-node svg path {
  fill: none;
  stroke: var(--stroke, currentColor);
  stroke-width: var(--stroke-width, .1em);
  stroke-linecap: var(--stroke-linecap, square);
  stroke-linejoin: var(--stroke-linejoin, bevel);
}

/* The static map stands in for live's interactive block; at minimum it must behave like a
   control the visitor can use, not a dead picture. */
/* The embed sits over the static fallback image and carries LIVE'S OWN filter, measured from
   live's map block: invert(0.92) grayscale(1) contrast(0.9). That is what makes an ordinary
   light Google map read as the dark map on the site - no custom Google style, no API key. */
.voyce-map-shell {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  /* The API path renders tiles as same-document <img>, so this filter actually paints -
     which is precisely why live can be dark and an iframe embed cannot. */
  /* LIVE'S OWN VALUES, measured from its map block. A filter set directly on a cross-origin
     iframe reports in computed style but does not paint; on the container it does. */
  filter: invert(0.92) grayscale(1) contrast(0.9);
}
.voyce-map-embed {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
.voyce-map-open {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: 3;
  font-size: .78rem;
  letter-spacing: .04em;
  padding: 7px 12px;
  color: #D8D8D8;
  background: rgba(0,0,0,.72);
  border: 1px solid rgba(255,255,255,.22);
  text-decoration: none;
}
.voyce-map-open:hover, .voyce-map-open:focus-visible { color: #fff; border-color: #fff; }
.voyce-map-link:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }

/* SECTION DIVIDER FAIL-SAFE. The served markup ships `<path d="M0,0"/>` for every section
   divider, and clip-path with objectBoundingBox units clips a zero-area path to NOTHING - the
   whole section, image included, paints black. voyce.js writes live's real path and only then
   sets `voyce-divider` on <html>. Until it does, the clip is off: the section renders as a plain
   rectangle with no wave, but its content is VISIBLE. Same armed-state model as the reveals and
   the nav - the fail-safe must never depend on the thing whose failure it covers. */
html:not(.voyce-divider) .section-border { clip-path: none !important; }

/* NATIVE GOOGLE FORMS (academy student registration, ACCESS) ------------------
   The questions are rendered in the site's own form markup by gform-native.mjs and reuse
   Squarespace's existing field classes, so the bulk of the styling is inherited rather than
   written here. Only three things have no equivalent anywhere on the site: the free-text box
   that belongs to an "Other" choice, an invalid-field state, and the wrapper's own spacing. */
.voyce-gform-wrapper { margin-top: 1.2rem; }
.voyce-gform .field-list > .form-item { margin-bottom: 1.4rem; }

/* The "Other" box is inert until Other is actually chosen - it stays in the flow so the layout
   does not jump, but reads as unavailable and is skipped by the submit builder. */
.voyce-gform .voyce-gf-other-text {
  margin-top: .5rem;
  opacity: .45;
  transition: opacity .18s ease;
}
.voyce-gform .voyce-gf-other.is-on .voyce-gf-other-text { opacity: 1; }
.voyce-gform .voyce-gf-other-text[disabled] { cursor: not-allowed; }

/* Validation happens in our code, because Google's endpoint is opaque and cannot report back. */
.voyce-gform .voyce-gf-invalid .radio-label,
.voyce-gform .voyce-gf-invalid input,
.voyce-gform .voyce-gf-invalid textarea { border-color: rgba(255,120,120,.75) !important; }
.voyce-gform .voyce-gf-invalid .title span:first-child { color: rgba(255,150,150,.95); }

@media (prefers-reduced-motion: reduce) {
  .voyce-gform .voyce-gf-other-text { transition: none; }
}

/* The two native forms sit in containers built for something else: student-registration's
   `.va-frame` was a WHITE card designed to host a white Google iframe, and ACCESS's form
   replaced a button inside a centred button container. Both were measured on the deployed page -
   va-frame bg rgb(255,255,255), access wrapper text-align:center - and neither suits a dark,
   left-aligned form. Scoped with :has() so only the container actually holding one is affected. */
.va-frame:has(.voyce-gform-host) {
  background: transparent !important;
  border: 0 !important;
  padding: 0 !important;
}
.voyce-gform-host, .voyce-gform-host .form-wrapper { text-align: start; }
.sqs-block-button-container:has(.voyce-gform-host) { text-align: start; }

/* Two defects the rendered measurement caught, one of which no screenshot could show.

   1. CHECKBOX MARKS. Squarespace hides .radio-mark/.radio-outline with a rule scoped to
      `.form-item.radio`, and the control is drawn by the label instead. A checkbox fieldset is
      `.form-item.checkbox`, so that rule misses it and the two spans rendered as empty 24x26
      boxes beside every option - measured `checkbox 24x26 display=block` against radio's
      `0x0 display=none`.

   2. BLACK INPUT TEXT. student-registration's form sits in a container built to frame a white
      Google iframe, and its inputs inherited color rgb(0,0,0) - black text on a dark form, so
      anything typed would have been invisible. /connect and /access both measure
      rgb(217,217,217). Nothing in a screenshot of an EMPTY form reveals this; only reading the
      computed colour does. */
.voyce-gform .form-item.checkbox .radio-mark,
.voyce-gform .form-item.checkbox .radio-outline { display: none !important; }

/* CORRECTION. The first attempt set `color` directly on the inputs and lost - Squarespace's own
   rule is `.sqs-site-style-form .field-list input:not([type=...]) { color:
   var(--dynamic-input-text-color, #000) }`, which outranks it on specificity. More to the point
   it was the wrong fix: the colour is driven by a VARIABLE whose fallback is #000, and the
   student page simply never set it. Supply the variable instead of fighting the rule.
   /connect resolves it to hsla(0,0%,85.1%,1) - the accent token - so it is expressed as the
   token and follows a theme change rather than pinning a hex. */
.voyce-gform-host input::placeholder,
.voyce-gform-host textarea::placeholder { color: hsla(var(--accent-hsl), .45); }

/* 10. Header navigation — restraint, using the tokens that already exist
   ---------------------------------------------------------------------------
   The nav TYPE is already exactly the brand spec (docs/BRAND.md: Inter 400,
   16px/24px, +0.16px, lowercase) and is deliberately NOT touched. Size, weight,
   family, tracking and case stay put - changing them loses a documented
   signature rather than gaining polish.

   What was flat was hierarchy. custom.css sets

       .header-nav-item a { color: var(--voyce-white) !important }

   so all nine arms render at #fff, the token BRAND.md reserves for "headings,
   emphasis", while warning that pushing text to pure white "would make the
   pages harsher than the brand". Nine peers at full emphasis leaves nothing to
   move toward. At rest they now sit on --voyce-secondary (#D8D8D8, the
   documented body tone, explicitly "not pure white"); hover, focus and the
   current page bring --voyce-white forward.

   NO NEW VALUES. Both colours are existing --voyce-* custom properties, so this
   follows the palette instead of introducing a second one. !important matches
   custom.css's own convention and is required to answer its !important rule -
   without it this loses regardless of specificity, which is exactly what the
   first attempt did.

   THE ARMS STAY PEERS. BRAND.md requires studios / academy / records / films /
   symphony / lab / go360 to read as separate arms rather than flattening into
   one service list, so nothing here groups or subordinates any of them. The
   only distinction drawn is which page you are currently on.

   Timing matches the header's own .35s so the bar behaves as one object. */
#header .header-nav-item a,
#header .header-menu-nav-item a {
  color: var(--voyce-muted) !important;
  transition: color .35s ease, text-decoration-color .35s ease;
}
#header .header-nav-item a:hover,
#header .header-nav-item a:focus-visible,
#header .header-menu-nav-item a:hover,
#header .header-menu-nav-item a:focus-visible,
#header .header-nav-item--active a,
#header .header-menu-nav-item--active a {
  color: var(--voyce-white) !important;
}

/* The active underline sat on the baseline as a browser default. Given offset
   and drawn in the rules token it reads as a considered mark. text-decoration
   rather than a pseudo-element, so it keeps following the glyphs. */
#header .header-nav-item a {
  text-decoration-thickness: 1px;
  text-underline-offset: .42em;
  text-decoration-color: transparent;
}
#header .header-nav-item--active a {
  text-decoration-line: underline;
  text-decoration-color: var(--voyce-white);
}
#header .header-nav-item a:hover,
#header .header-nav-item a:focus-visible {
  text-decoration-line: underline;
  text-decoration-color: var(--voyce-line);
}
@media (prefers-reduced-motion: reduce) {
  #header .header-nav-item a,
  #header .header-menu-nav-item a { transition: none; }
}

/* 11. The scrolled bar gets the glass treatment the tokens already describe.
   --voyce-glass-fill and --voyce-glass-line are defined in custom.css and were
   not used anywhere on the header. The solid bar already blurs; adding the
   hairline gives it an edge so it reads as a surface sitting above the page
   rather than a black band fading into it. */
body.va-navsolid #header {
  border-bottom: 1px solid var(--voyce-glass-line);
}

/* ─────────────────────────────────────────────────────────────────────────────
   PROPORTION — the design360 doctrine, applied to the web.

   The book is explicit: "Column: 7.11 in, divided into three" and "Margins,
   scale and spacing. The part nobody names and everybody feels, and the reason
   a page of this document and a page of the rate card read as one company."

   7.11in / 3 = 2.37in per sub-column. Running text in the books sits across two
   of the three, ~4.74in, which at book type is ~68 characters. Measured on the
   site before this rule, body copy ran at a median of 88-102 characters and
   worst lines hit 160 - roughly double the comfortable measure, which is where
   the eye starts losing the line return.

   Capped by `ch`, so the measure follows the type size rather than a fixed px
   width, and the cap is a MAXIMUM - narrower columns inside the fluid grid keep
   whatever width they already had. Nothing is widened, nothing is re-aligned;
   centred paragraphs stay centred inside their cap via the auto margins below.

   Deliberately scoped to running text. Headings, hero lockups, nav, footer,
   tables and the catalogue grids are untouched - they are not reading measure.
   ───────────────────────────────────────────────────────────────────────────── */
/* Scoped to `main p`, NOT to `.sqs-html-content`. The first attempt assumed every
   paragraph lived in a Squarespace text block; measuring the DOM showed the widest
   running text on /studios is `p.vb-m` inside a hand-authored `section.vb` (960px) and
   `li.accordion-item` inside an accordion (700px) - neither is in `.sqs-html-content`,
   so the rule matched nothing that mattered. Cap by what the element IS.
   `li` stays limited to prose lists: capping an accordion row would constrain the row,
   not its reading measure. */
/* The content root is `main#page` - ONE element, not a main inside #page. So
   `#page main p` can never match and the first two attempts capped nothing.
   Verified on the DOM: the widest paragraph matches `#page p` and `main p`,
   and does NOT match `#page main p`. */
#page p { max-width: 68ch; }
#page .sqs-html-content > ul > li,
#page .sqs-html-content > ol > li { max-width: 68ch; }

/* never a reading measure - left to their own layout */
#page nav p,
#page footer p,
#page table p,
#page .sqs-block-button-container p { max-width: none; }

/* a capped paragraph that was centred must stay centred, not drift left */
#page .sqs-html-content > p[style*="text-align: center"],
#page .sqs-html-content > p[style*="text-align:center"] { margin-left: auto; margin-right: auto; }

/* ─────────────────────────────────────────────────────────────────────────────
   TAP TARGETS — a 44px minimum hit area, with the visual size left alone.

   Measured across pages: the seven nav links render 27px tall on every page,
   .vp-toggle on /pricing 24px, and the social icons on /connect are smaller
   again. All are under the 44px minimum in WCAG 2.5.5 and Apple's HIG, and a
   mis-tap on a control reads to a visitor as "broken", not "I missed".

   Growing the elements would push the header taller and change the pricing
   table's rhythm, so the HIT AREA is expanded instead with a centred
   pseudo-element: clicks land on the link, nothing moves, nothing is redrawn.
   Verified safe before applying - none of these elements already use ::before
   or ::after (both computed to content:none), and the header is 224px tall, so
   a 44px target has room.

   Inline links inside running text are deliberately untouched: WCAG exempts
   them, and padding them out would wreck the line rhythm the measure cap just
   fixed. */
.header-nav-item > a,
.header-menu-nav-item > a,
#page .vp-toggle,
#page .sqs-svg-icon--wrapper { position: relative; }

.header-nav-item > a::after,
.header-menu-nav-item > a::after,
#page .vp-toggle::after,
#page .sqs-svg-icon--wrapper::after {
  content: ""; position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: 44px;
}

/* ─────────────────────────────────────────────────────────────────────────────
   SCALE — "one factor, every size a multiple" (design360, "the system").

   Measured cause of the drift: /record-label renders 19 distinct type sizes,
   /academy 15, /connect 14, against /studios' 7. They are not 19 decisions.
   They are ~6 separately-authored component families each choosing its own
   clamp(): .vr-* 72px, .ve-name 46px, .va-np-lead 30px, .vd-sub and .ve-sub
   17px, .va-np-genre 18px, .vb-*, .vu-*, .vn-*. Every family is internally
   consistent and none shares a scale with the others, so the page has no
   hierarchy for the eye to follow.

   One ladder, factor ~1.27, anchored on the 16px body already in use and
   matching the steps /studios already lands on (11 / 16 / 28 / 68). Declared
   once here so the next component inherits it instead of inventing a size -
   which is the book's actual requirement: "the rules that let someone else
   make the next asset without asking you".

   Declaring these changes nothing on its own. Components are migrated onto
   them deliberately, starting with the ones authored in this session. ───── */
:root{
  --vt-label:   11px;
  --vt-small:   clamp(12px, 1.0vw, 13px);
  --vt-body:    clamp(14px, 1.25vw, 16px);
  --vt-lead:    clamp(16px, 1.5vw, 21px);
  --vt-sub:     clamp(21px, 2.6vw, 28px);
  --vt-head:    clamp(24px, 3.4vw, 44px);
  --vt-display: clamp(34px, 6vw, 68px);
}

/* components authored this session, now on the ladder */
#page .vn-k{ font-size: var(--vt-label) }
#page .vn-p{ font-size: var(--vt-body) }
#page .vn-h{ font-size: var(--vt-head) }
#page .vn-cta{ font-size: var(--vt-small) }
#page .vu-k{ font-size: var(--vt-label) }
#page .vu-p, #page .vu-body p{ font-size: var(--vt-body) }
#page .vu-h{ font-size: var(--vt-display) }
#page .vu-item h2{ font-size: var(--vt-sub) }
#page .vu-state{ font-size: var(--vt-label) }
