/* Public-site DEFAULT theme — the published stylesheet.
 *
 * Written byte-for-byte to each site bucket at /assets/css/app.css and referenced
 * site-relative. Each theme owns its own source file under
 * css/<theme>/app.css; selecting a theme (SiteConfig.siteTemplate ->
 * publicsite.ResolveTheme) selects both the base-layout variant AND the bundle
 * compiled from the matching source here.
 *
 * NOT COMPILED — SHIPPED VERBATIM. This file is embedded by publicsite.CSSBundle
 * and written byte-for-byte to each site bucket at /assets/css/app.css. Nothing
 * compiles it: the only Tailwind build in this repo (`yarn build:css`) targets the
 * ADMIN stylesheet. This file's header claimed a "deploy-time build (Group 7)
 * pre-compiles" it until 2026-07-29; that step was never built, and the three
 * `@tailwind` directives that sat at the top were inert text the browser dropped.
 *
 * TWO CONSEQUENCES, both deliberate now that they are stated rather than accidental:
 *   - There is NO CSS reset on published sites. Browser defaults apply — in-body
 *     headings, for instance, take the browser's own `1.5em` rather than a
 *     preflight-normalised size. Every rule below therefore states what it needs.
 *   - There are NO Tailwind utilities. A utility class in a public-site template
 *     resolves NOWHERE: not on the published site, and not in the preview iframe,
 *     which loads this bundle and nothing else. Public templates may use only the
 *     `ao-` prefixed classes, enforced by TestPublicSiteTemplatesUseOnlyAoClasses.
 *
 * Compiling these bundles remains a live option, but it is a DESIGN DECISION, not
 * housekeeping: adding a reset changes the rendered output of every existing live
 * site, so it needs before/after evidence and an owner's call — not a quiet commit.
 *
 * The `ao-*` rules below style the frozen, sanitised contentHtml emitted by
 * app/richtext.
 *
 * SHARED `--ao-*` TOKEN CONTRACT (spec 2026-07-03-inline-stylesheet-editor):
 * every per-site design token drives a stable `--ao-*` custom property. The
 * default for each is carried INLINE as the var() FALLBACK (e.g.
 * `var(--ao-ink, #1f2937)`), so with NO tokens set the rendered result is
 * byte-for-byte today's look; publicsite.CSSBundleWithOverride appends a single
 * `:root { --ao-*: ...; }` block AFTER this bundle, which wins by cascade. The
 * variable names are the shared set in publicsite/style_variables.go, identical
 * across both themes — a future dark-mode variant is just a parallel `:root`
 * re-declaration of the same names, nothing here changes. Layout defaults are the
 * mobile-first small-screen base; the breakpoints below step them up.
 */

/* --- Public-site chrome -------------------------------------------------- */

/* A jump to a heading anchor scrolls rather than cutting, so the reader keeps
   the sense of where the target sits on the page. The reduced-motion branch
   returns the instant jump: a reader who asks the operating system for less
   movement is asking for exactly this movement to stop. */
html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

.ao-site {
  font-family: var(--ao-font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: var(--ao-font-size-base, 1rem);
  line-height: var(--ao-line-height, 1.6);
  color: var(--ao-ink, #1f2937);
  background: var(--ao-surface, #ffffff);
}

.ao-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  padding: 1rem 1.5rem;
  border-bottom: 2px solid var(--ao-rule, #e5e7eb);
}

.ao-brand {
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: 1.5rem;
  font-weight: 700;
  text-decoration: none;
  color: var(--ao-ink-strong, #111827);
}

/* THE SITE LOGO STANDS IN THE HOME LINK when the site supplies one. It is sized
   by HEIGHT alone, as the Logo strip marks are, so the artwork keeps its own
   aspect ratio and no wide lockup crops. The height is one literal in both
   theme bundles: the header is one shape for every site, so the Site logo reads
   at one weight whatever the theme. The cap stops a very wide lockup pushing the
   nav off a narrow header. The image is a block, so the link box holds no text
   baseline space under it. It declares no background and no padding, so the
   header keeps the page surface colour behind it. */
.ao-brand img {
  height: 5rem;
  width: auto;
  max-width: 100%;
  display: block;
}

/* A header that holds the Site logo drops its vertical padding, so the taller
   lockup does not push the masthead down. A header with the text home link
   keeps its padding, so a site with no Site logo reads as before. */
.ao-header:has(.ao-brand img) {
  padding-block: 0;
}

.ao-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.ao-nav a {
  color: var(--ao-accent, #374151);
  text-decoration: none;
}

.ao-nav a:hover {
  text-decoration: underline;
}

.ao-content {
  max-width: 64rem;
  margin: 0 auto;
  padding: 2rem var(--ao-page-margin, 1.5rem);
}

/* --- Page shell: single column, or two-column with a right section nav ---- */
.ao-page-wrap { max-width: var(--ao-content-max-width, 48rem); margin: 0 auto; }
.ao-page-wrap.ao-has-aside {
  max-width: none;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 12rem;
  gap: 2.75rem;
  align-items: start;
}
.ao-page-main { min-width: 0; }

.ao-breadcrumb { margin: 0 0 1.25rem; font-size: 0.85rem; }
.ao-breadcrumb ol { list-style: none; display: flex; flex-wrap: wrap; gap: 0.45rem; margin: 0; padding: 0; }
.ao-breadcrumb li:not(:last-child)::after { content: "/"; margin-left: 0.45rem; color: var(--ao-ink-faint, #9ca3af); }
.ao-breadcrumb a { color: var(--ao-muted, #6b7280); text-decoration: none; }
.ao-breadcrumb a:hover { text-decoration: underline; }
.ao-breadcrumb [aria-current="page"] { color: var(--ao-ink-strong, #111827); }

.ao-section-nav { position: sticky; top: 1.5rem; border-left: 2px solid var(--ao-rule, #e5e7eb); padding-left: 1.25rem; }
.ao-section-nav-title {
  margin: 0 0 0.6rem; font-size: 0.72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--ao-muted, #6b7280);
}
.ao-section-nav ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.ao-section-nav a { display: block; padding: 0.3rem 0; color: var(--ao-ink, #374151); text-decoration: none; }
.ao-section-nav a:hover { text-decoration: underline; }
.ao-section-nav a.ao-active { font-weight: 700; color: var(--ao-ink-strong, #111827); }
/* The heading links the section's landing page but keeps the heading's look:
   the plain aside link rule above would give it block padding and body ink. */
.ao-section-nav-title a { display: inline; padding: 0; color: inherit; font-size: inherit; }
.ao-section-nav-title a[aria-current="page"] { color: var(--ao-ink-strong, currentColor); }

/* --- Blog taxonomy: post meta, categories, tag cloud, archive ------------- */
.ao-post-meta { font-size: 0.82rem; color: var(--ao-muted, #6b7280); margin: 0.3rem 0 0; }
.ao-post-meta a { color: var(--ao-muted, #4b5563); text-decoration: none; }
.ao-post-meta a:hover { text-decoration: underline; }
.ao-meta-sep { margin: 0 0.45rem; opacity: 0.5; }
.ao-meta-cat { font-weight: 600; }
.ao-listing-lead { margin: 0 0 1.5rem; color: var(--ao-muted, #6b7280); }
.ao-blog-aside .ao-taxo { margin-bottom: 1.75rem; }
.ao-taxo-list { list-style: none; margin: 0; padding: 0; }
.ao-taxo-list li { display: flex; justify-content: space-between; gap: 0.5rem; padding: 0.18rem 0; }
.ao-taxo-list a { color: var(--ao-ink, #374151); text-decoration: none; }
.ao-taxo-list a:hover { text-decoration: underline; }
.ao-taxo-count { color: var(--ao-ink-faint, #9ca3af); font-size: 0.78rem; }
.ao-tag-cloud { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.7rem; margin: 0; line-height: 1.5; }
.ao-tag-cloud a { color: var(--ao-muted, #4b5563); text-decoration: none; }
.ao-tag-cloud a:hover { text-decoration: underline; }
.ao-tag-1 { font-size: 0.8rem; opacity: 0.85; }
.ao-tag-2 { font-size: 0.95rem; }
.ao-tag-3 { font-size: 1.1rem; }
.ao-tag-4 { font-size: 1.3rem; font-weight: 600; }
.ao-tag-5 { font-size: 1.55rem; font-weight: 700; }

.ao-footer {
  padding: 1.5rem;
  border-top: 2px solid var(--ao-rule, #e5e7eb);
  color: var(--ao-muted, #6b7280);
  font-size: 0.875rem;
}

/* The legal identity line (registered name, company number, registered office,
   VAT number) closes the footer, one step smaller and one ink step fainter
   than the copyright line above it. */
/* A browser data detector can still wrap part of the legal line in an anchor
   of its own (older iOS constructs one even with detection off). Whatever put
   it there, it reads as the line around it, not as browser-default blue. */
.ao-footer-legal a {
  color: inherit;
  text-decoration: none;
}

.ao-footer-legal {
  margin-top: 0.5rem;
  font-size: 0.8125rem;
  color: var(--ao-ink-faint, #9ca3af);
}

/* THE FOOTER'S LINK BAND. Two columns open the footer: the site's sections, and
   the routes to its owner. Each column draws only if it holds a link, so a site
   with no contact route shows the navigation column alone.

   The band is layout, not motion, so it sits outside both motion guards. */
.ao-footer-columns {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem 3rem;
  margin-bottom: 1.25rem;
}

.ao-footer-column ul {
  display: grid;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Each link reads as the footer text around it, and grows its own tap area so a
   thumb reaches it without magnifying the page. */
.ao-footer-column a {
  display: inline-block;
  padding: 0.2rem 0;
  color: inherit;
  text-decoration: none;
}

.ao-footer-column a:hover {
  text-decoration: underline;
}

.ao-footer-column a:focus-visible {
  outline: 2px solid var(--ao-accent, #2563eb);
  outline-offset: 3px;
}

/* --- Page + post layout -------------------------------------------------- */

/* THE MIDDLE STEP. The page title is an ordinary heading: it takes
   `--ao-heading-scale`, the multiplier it shares with `.ao-blog-list-title`.

   THE DISPLAY STEP IS NOT HERE. `--ao-display-scale` and `--ao-display-tracking`
   bind to the hero REGION (`.ao-region--hero`, further down this file) and to
   nothing else. A page asks for a display element by declaring a hero region in
   its template, so the token follows the ROLE rather than a class every page
   carries. Binding the display step here as well would give one page two
   competing display sizes, which is why this rule reads the heading step only.

   WHAT AN OPERATOR SEES. A site that sets no tokens renders exactly as before:
   2rem, `normal` tracking. A site that has set a display scale sees its page
   titles at the theme size and its hero regions at the display size — the
   rendering change page templates accept deliberately, so that "extreme
   contrast, nothing in the middle" is expressible at all. */
.ao-page-title {
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: calc(2rem * var(--ao-heading-scale, 1));
  font-weight: 700;
  line-height: 1.2;
  /* No letter-spacing was declared here before 2026-07-28, and nothing in this
     element's ancestor chain sets tracking (the only literals are on the
     section-nav title and the nav/eyebrow rules, which are not ancestors), so
     the inherited value was `normal`. `normal` is therefore the
     behaviour-preserving fallback: an untokenised site computes exactly what it
     computes today. */
  letter-spacing: var(--ao-heading-tracking, normal);
  margin: 0 0 1rem;
  /* Headings take ink-strong; body copy takes ink. Until 2026-07-28 this rule
     declared no colour at all and simply INHERITED ink from .ao-site, so a page
     title, its subheadings and its body copy were all exactly the same colour —
     the ink ramp existed in the token set but nothing on a normal page consumed
     the top of it. The fallback is .ao-site's own ink default, so an untokenised
     site renders identically to before. */
  color: var(--ao-ink-strong, #1f2937);
}

.ao-blog-date {
  display: block;
  color: var(--ao-muted, #6b7280);
  font-size: 0.875rem;
  margin-bottom: 1.5rem;
}

.ao-blog-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

/* The other consumer of the heading step. These titles keep `--ao-heading-scale`
   and must never see the display multiplier: a display scale of ~5.6 would drag
   them to ~112px, which is the collapse the second token exists to prevent. */
.ao-blog-list-title {
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: calc(1.25rem * var(--ao-heading-scale, 1));
  font-weight: 600;
  letter-spacing: var(--ao-heading-tracking, normal);
  margin: 0;
}

.ao-blog-link {
  text-decoration: none;
  color: var(--ao-ink-strong, #111827);
}

.ao-blog-link:hover .ao-blog-list-title {
  text-decoration: underline;
}

/* --- Sanitised contentHtml (the `ao-` classes app/richtext emits) -------- */

.ao-page-body .ao-paragraph {
  margin: 0 0 1rem;
}

/* Links inside authored body copy. Until 2026-07-28 this theme had NO rule here
   at all, so Tailwind preflight's `a { color: inherit; text-decoration: inherit }`
   won and a link in a paragraph was visually IDENTICAL to the text around it —
   no colour, no underline, no affordance whatsoever. On the light theme that was
   merely bad; the dark palette made it obvious. Colour alone would not be enough
   either (WCAG 1.4.1 — do not distinguish by colour alone), so the underline is
   restored explicitly rather than left to the reset. */
.ao-page-body a {
  color: var(--ao-accent, #2563eb);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-thickness: from-font;
}

.ao-page-body a:hover {
  text-decoration-thickness: 2px;
}

/* In-body headings (the `ao-heading` class goldmark emits for h1-h6).
   RECORDED DECISION (spec 2026-07-28-dark-design-tokens, task 3.5): this rule
   deliberately gains NO `font-size`, so in-body headings still do not respond to
   `--ao-heading-scale`. Declaring one would not be behaviour-preserving (Tailwind
   preflight resets h1-h6 to `font-size: inherit`, so they render at body size
   today) and one selector covers all six levels, so a single value would flatten
   the hierarchy rather than express it. A per-level type scale is spec B's work.
   See the full reasoning in publicsite/style_variables.go. They DO gain tracking. */
.ao-page-body .ao-heading {
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: var(--ao-heading-tracking, normal);
  margin: 1.5rem 0 0.75rem;
  color: var(--ao-ink-strong, #1f2937);
}

/* A heading the build gave an id is the target of a fragment jump, and a sticky
   chapters rail sits above the page. The margin keeps the heading clear of it,
   so a jump never lands with the heading under the rail. */
.ao-page-body .ao-heading[id] {
  scroll-margin-block-start: 2rem;
}

/* THE HEADING ANCHOR. Every id-carrying heading holds one empty link to its own
   address. The glyph is drawn here rather than written into the markup: a link
   with no text node cannot reach the search index or a derived card lead.

   IT IS HIDDEN, NEVER REMOVED. `opacity: 0` keeps the link in the tab order, so
   a keyboard reader reaches it and the focus rules below reveal it. `display:
   none` and `visibility: hidden` would both take it out of that order, and the
   link would be reachable by pointer alone.

   THE SCOPE IS DELIBERATE. A site title and an index-card title carry
   `.ao-heading` too, and neither gains an affordance. */
.ao-page-body .ao-heading-anchor {
  opacity: 0;
  margin-inline-start: 0.35em;
  text-decoration: none;
  color: var(--ao-muted, #6b7280);
}

.ao-page-body .ao-heading-anchor::before {
  content: "#";
}

.ao-page-body .ao-heading:hover .ao-heading-anchor,
.ao-page-body .ao-heading:focus-within .ao-heading-anchor,
.ao-page-body .ao-heading-anchor:focus-visible {
  opacity: 1;
}

.ao-page-body .ao-heading-anchor:focus-visible {
  outline: 2px solid var(--ao-accent, #2563eb);
  outline-offset: 2px;
}

/* The padding replaces the browser's 40px list padding and holds the markers.
   A left margin added to that padding pushed each list in by about 60px. */
.ao-page-body .ao-list {
  margin: 0 0 1rem;
  padding-left: 1.25rem;
}

.ao-page-body .ao-quote {
  border-left: 3px solid var(--ao-accent, #2563eb);
  padding-left: 1rem;
  font-style: italic;
  margin: 0 0 1rem;
}

.ao-page-body .ao-code {
  background: var(--ao-surface-raised, #f3f4f6);
  color: var(--ao-ink-raised, var(--ao-ink, #1f2937));
  border-radius: 0.5rem;
  padding: 1rem;
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.875rem;
}

.ao-page-body .ao-image {
  margin: 1.5rem 0;
}

.ao-page-body .ao-image img {
  max-width: 100%;
  height: auto;
  border-radius: 0.5rem;
}

.ao-page-body .ao-image figcaption {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  color: var(--ao-muted, #6b7280);
}

/* Video embeds: a sole-link YouTube paragraph renders as a responsive
   16:9 no-cookie iframe. */
.ao-page-body .ao-video-embed {
  margin: 1.5rem 0;
}

.ao-page-body .ao-video-embed iframe {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: 0.5rem;
}

/* Content-template blocks (text / imageText / textImage). Image+text rows stack
   vertically on mobile with the IMAGE ON TOP, and sit side by side from the
   medium breakpoint up. */
.ao-block {
  margin: var(--ao-block-spacing, 1.5rem) 0;
}

.ao-block--image-text,
.ao-block--text-image {
  display: flex;
  flex-direction: column;
  gap: var(--ao-image-text-gap, 1.5rem);
}

/* On mobile the text-image row (image second in the DOM) reorders the image on
   top, so EVERY image+text row stacks image-first on small screens. */
.ao-block--text-image > .ao-block-media {
  order: -1;
}

.ao-block-media,
.ao-block-text {
  min-width: 0;
}

.ao-block-media .ao-image {
  margin: 0;
}

.ao-image-img {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: 0.5rem;
}

.ao-image-caption {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  color: var(--ao-muted, #6b7280);
}

.ao-image-link {
  display: inline-block;
}

@media (min-width: 768px) {
  .ao-block--image-text,
  .ao-block--text-image {
    flex-direction: row;
    align-items: flex-start;
    gap: 2rem;
  }

  /* The media column takes the split ratio; the text column takes 1. A ratio of
     1 (the default) is the original even 1:1 split — behaviour-preserving. */
  .ao-block--image-text > .ao-block-media,
  .ao-block--text-image > .ao-block-media {
    flex: var(--ao-image-text-ratio, 1) 1 0;
  }
  .ao-block--image-text > .ao-block-text,
  .ao-block--text-image > .ao-block-text {
    flex: 1 1 0;
  }

  /* Restore the source order on desktop: text-image shows text then image. */
  .ao-block--text-image > .ao-block-media {
    order: 0;
  }
}

/* --- Editorial blocks: pull quote, profile card, call to action ---------- */
/* The three editorial block types. Each is a small composition inside the
   reading column rather than a page section, so every value below is a var()
   fallback or a multiple of the shared block-spacing token, and a site's own
   tokens restyle all three with no CSS written by anybody.

   THE WHOLE CARD IS THE TARGET, exactly as it is for a derived index card. A
   profile card and a call to action draw no button and no "read more"
   affordance: each renders as ONE link, so a second element that looked
   interactive would promise a second destination that does not exist. */

/* THE PULL QUOTE sets a large quotation mark beside the quote, with the
   attribution name and the smaller role line under it. The mark is drawn by CSS,
   so no quotation character is written into content, and the empty alternative
   text after the slash keeps it decorative: the blockquote is already announced
   as a quotation, and a screen reader would otherwise read the mark as well.

   A NUMBERED REGION DRAWS ITS COUNTER ON THIS SAME PSEUDO-ELEMENT and wins by
   specificity, so a quote inside one takes the number instead of the mark. That
   is the right precedence: the operator asked the region for the motif. */
.ao-pull-quote {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: calc(var(--ao-block-spacing, 1.5rem) * 0.4) calc(var(--ao-block-spacing, 1.5rem) * 0.5);
}

.ao-pull-quote::before {
  content: "\201C" / "";
  grid-column: 1;
  grid-row: 1;
  font-family: var(--ao-font-display, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
  font-size: 3rem;
  line-height: 0.8;
  color: var(--ao-accent, #2563eb);
}

.ao-pull-quote-text {
  grid-column: 2;
  margin: 0;
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: calc(1.375rem * var(--ao-heading-scale, 1));
  line-height: 1.35;
  letter-spacing: var(--ao-heading-tracking, normal);
  color: var(--ao-ink-strong, #111827);
}

/* The quote is markdown, so it arrives as paragraphs. They take the quote's own
   size, and the last one closes the blockquote with no trailing space. */
.ao-pull-quote-text .ao-paragraph {
  margin: 0 0 calc(var(--ao-block-spacing, 1.5rem) * 0.4);
}

.ao-pull-quote-text .ao-paragraph:last-child {
  margin-bottom: 0;
}

.ao-pull-quote-attribution {
  grid-column: 2;
  display: grid;
  gap: calc(var(--ao-block-spacing, 1.5rem) * 0.1);
}

/* The name carries the block's one optional link. A linked name keeps the
   content-link colour from `.ao-page-body a`, which outweighs this rule, so it
   still reads as a link; an unlinked name takes strong ink. */
.ao-pull-quote-name {
  font-family: var(--ao-font-meta, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: var(--ao-heading-tracking, 0.08em);
  text-transform: uppercase;
  color: var(--ao-ink-strong, #111827);
}

.ao-pull-quote-role {
  font-size: 0.85rem;
  color: var(--ao-muted, #6b7280);
}

/* THE PROFILE CARD stacks the portrait, the name, the role line and the bio.
   The identity is the card's ONE link, so the hover and focus states sit on it
   rather than on the whole block, and the bio stays outside it as ordinary prose
   that may carry links of its own. */
.ao-profile-card {
  display: grid;
  align-content: start;
  gap: calc(var(--ao-block-spacing, 1.5rem) * 0.5);
}

.ao-profile-card-identity {
  display: grid;
  align-content: start;
  justify-items: start;
  gap: calc(var(--ao-block-spacing, 1.5rem) * 0.25);
  text-decoration: none;
  color: inherit;
}

/* A linked identity is an anchor inside the page body, so `.ao-page-body a`
   outweighs the rule above and would paint the whole identity as underlined
   link text. A descendant cannot cancel an ancestor's text-decoration, so the
   override must sit on the anchor itself and carry more specificity than the
   prose-link rule. The state below restores the underline on the name alone,
   which is the card's only link affordance. */
.ao-page-body a.ao-profile-card-identity {
  text-decoration: none;
  color: inherit;
}

/* NOTHING CROPS THE PORTRAIT: it keeps its source aspect ratio. The cap stops a
   large upload filling the reading column. */
.ao-profile-card-portrait {
  display: block;
  max-width: 10rem;
}

.ao-profile-card-name {
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: calc(1.125rem * var(--ao-heading-scale, 1));
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: var(--ao-heading-tracking, normal);
  color: var(--ao-ink-strong, #111827);
}

.ao-profile-card-role {
  font-family: var(--ao-font-meta, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
  font-size: 0.78rem;
  letter-spacing: var(--ao-heading-tracking, 0.08em);
  text-transform: uppercase;
  color: var(--ao-muted, #6b7280);
}

.ao-profile-card-bio {
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--ao-ink, #1f2937);
}

.ao-profile-card-bio .ao-paragraph:last-child {
  margin-bottom: 0;
}

/* The identity carries no permanent underline, so the pointer and the keyboard
   both need a state that says it is one link. */
a.ao-profile-card-identity:hover .ao-profile-card-name,
a.ao-profile-card-identity:focus-visible .ao-profile-card-name {
  text-decoration: underline;
}

a.ao-profile-card-identity:focus-visible {
  outline: 2px solid var(--ao-accent, #2563eb);
  outline-offset: 4px;
}

/* THE CALL TO ACTION stacks the invitation (the heading and the optional
   label), then the supporting text. A rule above it opens the block instead of
   a box, and the label alone carries the button frame, so the block reads as an
   editorial invitation and still says plainly where it goes. The invitation
   is the block's ONE link, so the hover and focus states sit on it rather than
   on the whole block, and the supporting text stays outside it as ordinary
   prose that may carry links of its own. */
.ao-cta {
  display: grid;
  align-content: start;
  gap: calc(var(--ao-block-spacing, 1.5rem) * 0.35);
  padding-top: calc(var(--ao-block-spacing, 1.5rem) * 0.75);
  border-top: 2px solid var(--ao-rule, #e5e7eb);
}

.ao-cta-action {
  display: grid;
  align-content: start;
  justify-items: start;
  gap: calc(var(--ao-block-spacing, 1.5rem) * 0.35);
  text-decoration: none;
  color: inherit;
}

/* The same anchor override the profile card needs, for the same reason: the
   invitation is an anchor inside the page body, so `.ao-page-body a` outweighs
   the rule above and would paint the heading and the label as underlined link
   text. A descendant cannot cancel an ancestor's text-decoration, so the
   override must sit on the anchor itself. */
.ao-page-body a.ao-cta-action {
  text-decoration: none;
  color: inherit;
}

.ao-cta-heading {
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: calc(1.25rem * var(--ao-heading-scale, 1));
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: var(--ao-heading-tracking, normal);
  color: var(--ao-ink-strong, #111827);
}

.ao-cta-text {
  color: var(--ao-ink, #1f2937);
}

.ao-cta-text .ao-paragraph:last-child {
  margin-bottom: 0;
}

/* THE LABEL IS THE BUTTON. The block renders ONE anchor around the heading and
   the label, so the two can never become two controls. The frame therefore goes
   on the label, and the whole invitation stays one link with the hover and focus
   states it already holds.

   The frame follows .ao-search-button, the one button shape these bundles hold,
   so a reader meets one button language across the site.

   The air inside the button reads the block spacing token, so a site that opens
   up its blocks opens up its buttons with them.

   Both bundles state the frame word for word, so ONE fallback literal serves
   both themes, as the deck progress bar already does. A fallback paints only on
   a site that sets neither token. The face, the step, the tracking and the ink
   fallback stay theme-owned, and the parity guard names each one. */
.ao-cta-label {
  font-family: var(--ao-font-meta, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: var(--ao-heading-tracking, 0.08em);
  text-transform: uppercase;
  color: var(--ao-ink-on-accent, #ffffff);
  background: var(--ao-accent, #2563eb);
  border: 1px solid var(--ao-accent, #2563eb);
  border-radius: 0.375rem;
  padding: calc(var(--ao-block-spacing, 1.5rem) * 0.3) calc(var(--ao-block-spacing, 1.5rem) * 0.6);
}

/* The invitation carries no permanent underline, so the pointer and the keyboard
   both need a state that says it is one link. */
a.ao-cta-action:hover .ao-cta-heading,
a.ao-cta-action:focus-visible .ao-cta-heading {
  text-decoration: underline;
}

a.ao-cta-action:focus-visible {
  outline: 2px solid var(--ao-accent, #2563eb);
  outline-offset: 4px;
}

/* --- Template regions ---------------------------------------------------- */
/* One rule per role in the CLOSED region-role vocabulary (publicsite/
   region_role.go). A template is operator-authored data, so these rules are the
   whole reason operator authoring is safe: every role is styled and accessible
   the MOMENT a template using it is created, with no CSS written by anybody.

   The vocabulary is closed because a freely named region would be a class no
   theme has heard of — an unstyled div here, and silently unstyled in
   production, because these bundles are never Tailwind-compiled. Every literal
   below is only a var() fallback, so an untokenised site is unaffected and a
   site's own tokens restyle its regions automatically.

   A ROLE IS NOT A LAYOUT. These rules give each role its typography, spacing and
   rules. Where a region sits, how its blocks flow into columns, how much air
   divides them, what colour its canvas paints and whether it escapes the content
   measure are the LAYOUT KEYS, styled in the section that follows this one. */

.ao-region {
  margin: 0 0 var(--ao-block-spacing, 1.5rem);
}

.ao-region:last-child {
  margin-bottom: 0;
}

.ao-region--masthead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  padding-bottom: 1rem;
  margin-bottom: 2rem;
  border-bottom: 2px solid var(--ao-rule, #e5e7eb);
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  letter-spacing: var(--ao-heading-tracking, normal);
  color: var(--ao-ink-strong, #111827);
}

/* THE DISPLAY STEP LIVES HERE, AND NOWHERE ELSE. `--ao-display-scale`,
   `--ao-display-tracking` and `--ao-font-display` bind to this ROLE, which is
   what a page declares when it wants a display element. `.ao-page-title` reads
   none of the three: one page, one display element.

   THE DISPLAY FACE. `--ao-font-display` falls back THROUGH `--ao-font-heading`,
   so a site that sets only a heading font still gets it here, and a site that
   wants a different face for its display element sets one without touching its
   headings. That third font role is what a hero region gives an operator to
   bind a face to.

   The clamp keeps a display heading usable: clamp(MIN, MID, MAX) is max(MIN,
   min(MID, MAX)), MAX is the multiplied size and MIN is min(literal,
   multiplied), so with no token set both ends are the plain 2rem base and the
   fluid middle term cannot bite. A scale below 1 still shrinks the hero; the
   fluid middle only takes effect once the multiplied size would overflow a
   narrow viewport, and it keeps a rem term so text zoom still works. Display
   tracking falls back THROUGH heading tracking, so a site that sets only the
   latter still gets it on the display step. */
.ao-region--hero {
  margin: 0 0 2.5rem;
  font-family: var(--ao-font-display, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
  font-size: clamp(
    min(2rem, calc(2rem * var(--ao-display-scale, 1))),
    calc(1rem + 12vw),
    calc(2rem * var(--ao-display-scale, 1))
  );
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: var(--ao-display-tracking, var(--ao-heading-tracking, normal));
  color: var(--ao-ink-strong, #111827);
}

/* The hero region carries the display size itself, so the prose inside it
   inherits that size rather than fighting it with a block-level rule. */
.ao-region--hero .ao-paragraph,
.ao-region--hero .ao-heading {
  font-size: inherit;
  line-height: inherit;
  letter-spacing: inherit;
  margin: 0 0 0.5rem;
}

/* The meta row is the small multi-column strip set above the content: a few
   short labelled facts read across the page, not a column of prose. Each block
   placed in it becomes one column, and the row wraps rather than shrinking its
   columns, so a narrow screen reads it as stacked pairs.

   ITS FACE IS ITS OWN. `--ao-font-meta` falls back THROUGH `--ao-font-heading`,
   so the strip follows the site's heading face until an operator gives it one
   of its own — which is what the small-type role of the reference design needs
   without disturbing the headings.

   IT TAKES THE HEADING TRACKING AND NO DISPLAY MULTIPLIER. Multiplying a small
   uppercase strip by a display scale of ~5.6 is exactly the failure the display
   scale was separated from the heading scale to prevent: one multiplier reaching
   both cannot say "extreme contrast, nothing in the middle". */
.ao-region--meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 2.5rem;
  margin: 0 0 2rem;
  font-family: var(--ao-font-meta, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: var(--ao-heading-tracking, 0.08em);
  color: var(--ao-muted, #6b7280);
}

.ao-region--meta-row .ao-paragraph {
  margin: 0;
}

.ao-region--body {
  max-width: var(--ao-content-max-width, 48rem);
  color: var(--ao-ink, #1f2937);
}

/* The rail is a secondary column of supporting material. Its PLACEMENT beside
   the body is layout work; here it is a narrower, quieter measure that reads
   correctly even when it stacks. */
.ao-region--rail {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  max-width: 22rem;
  padding-top: 1rem;
  border-top: 1px solid var(--ao-rule, #e5e7eb);
  font-size: 0.9rem;
  color: var(--ao-muted, #6b7280);
}

/* The chapters rail lists links to the page's own headings. It stays in view
   while the reader scrolls the article, so it is the sticky element rather than
   the list inside it.

   A LONG PAGE CAN OUTRUN THE VIEWPORT. The rail scrolls inside its own height
   once its list is taller than the screen, so the last chapter stays reachable
   instead of falling off the bottom. */
.ao-region--chapters {
  position: sticky;
  top: 1.5rem;
  align-self: start;
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
  font-size: 0.9rem;
  color: var(--ao-muted, #6b7280);
}

/* The rail's own contents, on the treatment `.ao-section-nav` already gives the
   blog's section list, so the two rails read as one idea in this theme.

   THE LINK SELECTOR NAMES THE PAGE BODY because a chapter link is an anchor
   inside it, and `.ao-page-body a` would otherwise paint the whole list as
   underlined accent-colour prose. */
.ao-chapters {
  border-left: 2px solid var(--ao-rule, #e5e7eb);
  padding-left: 1.25rem;
}

.ao-chapters-title {
  margin: 0 0 0.6rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--ao-heading-tracking, 0.08em);
  color: var(--ao-muted, #6b7280);
}

.ao-chapters ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.ao-page-body a.ao-chapter-link {
  display: block;
  padding: 0.3rem 0;
  color: var(--ao-ink, #374151);
  text-decoration: none;
}

.ao-page-body a.ao-chapter-link:hover,
.ao-page-body a.ao-chapter-link:focus-visible {
  text-decoration: underline;
}

.ao-page-body a.ao-chapter-link:focus-visible {
  outline: 2px solid var(--ao-accent, #2563eb);
  outline-offset: 2px;
}

.ao-region--footer {
  margin-top: 3rem;
  padding-top: 1.25rem;
  border-top: 2px solid var(--ao-rule, #e5e7eb);
  font-size: 0.875rem;
  color: var(--ao-muted, #6b7280);
}

/* --- Region layout: placement, rhythm, gap, canvas, bleed, numbering ------ */
/* The rules above give each region ROLE its typography. These give each LAYOUT
   KEY its geometry. Both vocabularies are closed and declared in Go
   (publicsite/region_role.go, publicsite/region_layout.go), which derives every
   class from a stored key, so a template an operator writes can only ever ask
   for a look styled here. Every literal below is a var() fallback or a plain
   length, so a site's own tokens restyle its regions with no CSS written by
   anybody.

   THE REGION ELEMENT IS THE GRID. A published region holds its blocks directly,
   so the rhythm and the gap sit on the region itself. Edit mode inserts one
   block container between the two; that container passes its children straight
   through (the rule at the end of this section), so the admin lays a region out
   on the SAME grid the published page uses. */

/* THE PLACEMENT GRID. The page body becomes a two-column grid when one of its
   regions asks for a column, and stays in block flow when none does. A page that
   places no rail therefore keeps the margins that collapse through a block-flow
   body, which is what makes every template written before placement existed
   render exactly as it did.

   Rows flow in the template's order, and the flow NEVER back-fills. A
   back-filling flow drops a later region into a gap left in column one, so the
   page would read in an order the operator never wrote, and assistive technology
   would follow that order too.

   The column ratio is the theme's decision. The operator places a region; how
   wide each column runs is set here and has no key. */
.ao-page-body--composed:has(> :is(.ao-region--place-main, .ao-region--place-side)) {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 18rem);
  grid-auto-flow: row;
  column-gap: 2.75rem;
  align-items: start;
}

/* A region that states no placement spans both columns, so a template that names
   no placement keeps the full width it has always had. */
.ao-page-body--composed > .ao-region {
  grid-column: 1 / -1;
}

.ao-page-body--composed > .ao-region--place-full {
  grid-column: 1 / -1;
}

.ao-page-body--composed > .ao-region--place-main {
  grid-column: 1;
}

/* The side column takes the row its main region opened, because auto-placement
   fills the free cell beside it. Two sequences need no rule of their own: a main
   region with no side after it keeps its row and leaves column two empty, and a
   second side region flows into column two below the first. A side region with
   no main region before it is rejected when the template is saved, and renders
   full width if a stored one is ever read back. */
.ao-page-body--composed > .ao-region--place-side {
  grid-column: 2;
}

/* A rail needs room beside the body, so a page that places one widens its own
   measure. A page that places none keeps the reading measure exactly. */
.ao-page-wrap:has(.ao-region--place-side) {
  max-width: none;
}

/* A grid item forms its own formatting context, so the block margins at a
   region's top and bottom edges stop collapsing out of it and would add to the
   space between two regions. Removing those two margins keeps that space at the
   region's own bottom margin, exactly as a block-flow body renders it. */
.ao-page-body--composed:has(> :is(.ao-region--place-main, .ao-region--place-side)) > .ao-region > .ao-block:first-child {
  margin-block-start: 0;
}

.ao-page-body--composed:has(> :is(.ao-region--place-main, .ao-region--place-side)) > .ao-region > .ao-block:last-child {
  margin-block-end: 0;
}

/* THE SPLIT HERO. A template that places its HERO region in the MAIN column
   asks for the poster composition: the display statement fills the left of the
   whole viewport and the supporting column reads beside it. Three rules make
   it, and every one keys on that hero-in-main pair, so an ordinary main+side
   page (a body with a rail) keeps the reading measure exactly.

   THE PAGE ESCAPES ITS MEASURE. The content column's own max-width would hold
   the poster to the reading measure, so the split page lifts it and keeps only
   the page margin. The supporting column carries the page's body copy rather
   than a thumbnail list, so it reads the split-column-width token; absent, it
   grows with the viewport and stops before its line length turns uncomfortable.

   THE STATEMENT SIZES AGAINST THE VIEWPORT, not against the display-scale
   token: the token cannot know the column, and a fixed step wraps a long
   statement onto five ragged lines. 7vw holds a statement of this site's shape
   at about three lines across desktop widths and keeps growing on wider
   screens; the clamp floors it above the heading step on a phone and caps it
   before it outgrows the pane on a very wide screen. */
main.ao-content:has(.ao-region--hero.ao-region--place-main) {
  max-width: none;
}

.ao-page-body--composed:has(> .ao-region--hero.ao-region--place-main) {
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--ao-split-column-width, clamp(21rem, 30vw, 36rem)));
  column-gap: 4rem;
}

.ao-region--hero.ao-region--place-main {
  font-size: clamp(2.75rem, 7vw, 9rem);
}

/* THE LEDE. The first supporting paragraph beside a split hero is the page's
   opening line, so it steps up and takes the body face's italic. The rule
   reads the FIRST text block only, so the paragraphs after it keep the body
   step and the page keeps a hierarchy rather than a second column of display
   type. */
.ao-page-body--composed:has(> .ao-region--hero.ao-region--place-main) > .ao-region--place-side > .ao-block--text:first-child .ao-paragraph {
  font-size: 1.25rem;
  font-style: italic;
  line-height: 1.55;
}

/* THE BANNER PAGE. A template that places its HERO region FULL and also places
   a region in the MAIN column asks for the third composition: a Banner across
   the whole page width, the full-width rows below it, and the main and side
   regions as two equal columns beneath those.

   THE KEY STATES NO ORDER AND NO ADJACENCY. A Logo strip region sits between
   the Banner and the main region on a live site, so each selector below asks
   only that both regions are children of the same composed body.

   THE KEY NAMES BOTH PLACEMENTS, every time. A hero placed full with no main
   region is the ordinary reading page, which keeps the reading measure and the
   body cap; naming the hero alone would lift the measure of every page on
   every site.

   THE PAGE ESCAPES ITS MEASURE TWICE. The content column caps at the theme's
   own width and the page wrap caps at the reading measure, so both lift here.
   The rail rule above lifts the wrap for a side region alone, and a banner page
   may place a main region with no side region beside it. */
main.ao-content:has(.ao-region--hero.ao-region--place-full):has(.ao-region--place-main) {
  max-width: none;
}

.ao-page-wrap:has(.ao-region--hero.ao-region--place-full):has(.ao-region--place-main) {
  max-width: none;
}

/* THE TWO COLUMNS RUN AT ONE WIDTH, so neither the argument nor the proof
   leads. The gap matches the split hero gap, because the two compositions open
   the same air between two columns. */
.ao-page-body--composed:has(> .ao-region--hero.ao-region--place-full):has(> .ao-region--place-main) {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: 4rem;
}

/* EACH COLUMN FILLS ITS OWN TRACK. Two rules elsewhere would leave the two
   columns at two widths: a body region carries the theme's reading cap, and a
   theme that centres its body region carries auto inline margins, which collapse
   a grid item to the width of its content rather than the width of its track.
   Both go here, so the pair reads as two columns of one width. */
.ao-page-body--composed:has(> .ao-region--hero.ao-region--place-full) > :is(.ao-region--place-main, .ao-region--place-side) {
  max-width: none;
  margin-inline: 0;
}

/* THE LEFT COLUMN KEEPS A READING LINE LENGTH. Half of a wide page runs longer
   than a comfortable measure, so the argument caps at the site's own reading
   measure. The rule follows the one above, which lifts both columns, so the cap
   lands on the left column alone.

   THE CAP READS THE TOKEN WITH NO FALLBACK. The two theme bundles hold two
   different measures, and a literal fallback would hold one theme's column at
   the other theme's line length. CONSEQUENCE: a site that sets no
   content-max-width token leaves this column uncapped, and the copy then fills
   its half of the page. */
.ao-page-body--composed:has(> .ao-region--hero.ao-region--place-full) > .ao-region--place-main {
  max-width: var(--ao-content-max-width);
}

/* A FULL-PLACED REGION SPANS THE WHOLE PAGE. A body-role region caps at the
   reading measure everywhere else, which held the Logo strip and the Page cards
   in a narrow column on the left of a wide page while the right half stayed
   empty. Here a region placed full runs to the same edges as the Banner above
   it. The inline margins go to zero for the reason the columns state: a centred
   body region collapses to the width of its content.

   :where() HOLDS THIS RULE AT ONE CLASS OF SPECIFICITY, on purpose. It must beat
   the body cap stated before it, and it must LOSE to the full-bleed escape
   stated after it. A full-bleed region sets its own width and its own negative
   inline margins to reach the viewport edge, and a stronger rule here would pull
   it back to the page and push the page sideways. */
:where(.ao-page-body--composed:has(> .ao-region--hero.ao-region--place-full):has(> .ao-region--place-main)) > .ao-region--place-full {
  max-width: none;
  margin-inline: 0;
}

/* THE STATEMENT SIZES AGAINST THE VIEWPORT, not against the display-scale
   token: the token cannot know the width of the page. 5.5vw holds a first line
   of about 28 characters above 720px, so the claim reads as two lines on a wide
   screen; a longer claim wraps onto a third. The clamp floors the step above
   the heading size on a phone, where the claim wraps over three or four lines,
   and caps it before it outgrows a very wide screen. The split hero keeps its
   own 7vw step. */
.ao-page-body--composed:has(> .ao-region--place-main) > .ao-region--hero.ao-region--place-full {
  font-size: clamp(2.75rem, 5.5vw, 9rem);
}

/* THE CLAIM BREAKS AT AN EVEN LENGTH. Balanced wrapping stops the last line
   holding one word, which is what a headline of two lines needs. The hero block
   keeps its heading and its paragraph, and both take the treatment, so they
   read as one headline over two lines. */
.ao-page-body--composed:has(> .ao-region--place-main) > .ao-region--hero.ao-region--place-full .ao-heading,
.ao-page-body--composed:has(> .ao-region--place-main) > .ao-region--hero.ao-region--place-full .ao-paragraph {
  text-wrap: balance;
}

/* THE LEDE. The opening line of a banner page sits in the main region, under
   the Banner, so the lede follows the copy rather than the column beside it.
   The rule reads the FIRST text block only, so the paragraphs after it keep the
   body step. */
.ao-page-body--composed:has(> .ao-region--hero.ao-region--place-full) > .ao-region--place-main > .ao-block--text:first-child .ao-paragraph {
  font-size: 1.25rem;
  font-style: italic;
  line-height: 1.55;
}

/* AN EMPTY REGION KEEPS ITS PLACE AND LOSES ITS SPACE. A template names its
   regions for the whole site, and a page that assigns one no blocks still
   publishes the region's element — that structure is the template's contract.
   But the empty box's own margins, padding and whitespace line box stacked
   into dead space at the top of every page on a many-region template, so all
   three go to zero when the region holds no element. The rule deliberately
   sets no display: the composed body's box model is a guarded contract, and
   a zero-space block changes nothing it protects. */
.ao-page-body--composed > .ao-region:not(:has(*)) {
  margin: 0;
  padding: 0;
  font-size: 0;
  line-height: 0;
}

/* THE COLUMN RHYTHM. A region declares ONE repeating column pattern and its
   blocks flow into it in the order they sit in the page body. A pattern longer
   than the block count leaves the remaining columns empty and the region still
   renders. Nothing aligns the insides of the items: captions of different
   heights simply sit at different heights, which is what a grid does and what
   the reference composition wants.

   THE SINGLE READING COLUMN leaves the region in block flow, so a region that
   states it renders exactly as a region that states no rhythm at all. */
.ao-region--rhythm-full {
  grid-template-columns: minmax(0, 1fr);
}

.ao-region--rhythm-halves,
.ao-region--rhythm-thirds,
.ao-region--rhythm-third-third-full,
.ao-region--rhythm-two-thirds-third {
  display: grid;
  grid-auto-flow: row;
  gap: var(--ao-block-spacing, 1.5rem);
  align-items: start;
}

.ao-region--rhythm-halves {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.ao-region--rhythm-thirds,
.ao-region--rhythm-third-third-full,
.ao-region--rhythm-two-thirds-third {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* The two irregular patterns place one item of each cycle themselves. A derived
   index card is a direct child of its region, exactly as a block is, so the same
   rules size it and the index page needs no vocabulary of its own. The last
   selector of each group counts the edit-mode wrapper, which sits between two
   inserters, so the admin shows an item at the width the published page gives
   it. */
.ao-region--rhythm-third-third-full > .ao-block:nth-child(3n),
.ao-region--rhythm-third-third-full > .ao-index-card:nth-child(3n),
.ao-region--rhythm-third-third-full > .ao-page-blocks > .ao-edit-block:nth-child(6n) {
  grid-column: 1 / -1;
}

.ao-region--rhythm-two-thirds-third > .ao-block:nth-child(2n + 1),
.ao-region--rhythm-two-thirds-third > .ao-index-card:nth-child(2n + 1),
.ao-region--rhythm-two-thirds-third > .ao-page-blocks > .ao-edit-block:nth-child(4n + 2) {
  grid-column: span 2;
}

/* THE LOGO STRIP lays a row of client marks side by side at one even size. It
   is the one row-shaped rhythm that NEVER scrolls sideways: a reader must see
   every mark without a gesture, so the still strip WRAPS to more rows of
   smaller marks on a narrow screen instead of running off the side of the page.

   EACH CELL TAKES ITS OWN WIDTH. A cell that grew to take a share of the row
   spread six marks across a wide band with large gaps between them. The mark
   sets the width of its cell now, so the marks read as one even run whatever
   the band measures.

   EACH MARK SITS IN AN EVEN BOX. The image is contained inside a box of one
   height, so a wide wordmark and a square badge read at one weight and nothing
   crops. The height is a STEP of the shared block-spacing token, as the gap
   steps are, so a site that retunes its spacing retunes the strip with it.

   THE TRACK STANDS BETWEEN THE REGION AND ITS MARKS, in both modes, so every
   rule below reaches the marks THROUGH it. The read-only page also draws a
   hidden copy of the marks inside that track. Here the track is a pass-through
   box and the copy draws nothing, so an editor and a reader who asked for no
   motion read the client list once, in the wrapping strip.

   The figure and its link become plain blocks here, so the mark reads at the
   even height rather than at its own artwork height. The image rule names the
   figure as well, because the site-wide `height: auto` rule for a body image is
   more specific than the mark class alone and would undo the even box. */
.ao-region--rhythm-logo-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--ao-block-spacing, 1.5rem) * 1.5);
}

.ao-region--rhythm-logo-strip > .ao-region-track {
  display: contents;
}

/* Edit mode inserts its block container between the track and the marks. The
   site-wide pass-through rule reaches a container the REGION holds directly, and
   the track now stands in that place, so the strip states the pass-through for
   its own container. Without it an editor meets one mark per row. */
.ao-region--rhythm-logo-strip > .ao-region-track > .ao-page-blocks {
  display: contents;
}

.ao-region--rhythm-logo-strip .ao-region-clones {
  display: none;
}

.ao-region--rhythm-logo-strip .ao-block,
.ao-region--rhythm-logo-strip .ao-page-blocks > .ao-edit-block {
  flex: 0 0 auto;
  min-width: 0;
}

.ao-region--rhythm-logo-strip .ao-image,
.ao-region--rhythm-logo-strip .ao-image-link {
  display: block;
  margin: 0;
}

.ao-region--rhythm-logo-strip .ao-image .ao-image-img {
  width: auto;
  max-width: 100%;
  height: calc(var(--ao-block-spacing, 1.5rem) * 2.5);
  object-fit: contain;
}

/* THE SNAPPING ROW is the one rhythm that scrolls. Its items run off the side of
   the page in a single row and snap as the reader scrolls them sideways. The row
   scrolls at every width, so it takes no narrow-screen collapse: a gallery reads
   as a row on a phone exactly as it does on a wide screen.

   BOTH PINNED KEYS SHARE EVERY RULE HERE. The pinned row and the slide deck are
   this same row until a browser proves it can run a scroll-driven animation, so
   the three keys ride one selector list rather than three copies of it. A pinned
   key takes its own treatment only inside the reduced-motion and view-timeline
   guards further down this file.

   EACH TRACK IS ONE FIXED WIDTH. The stacking rhythms size a column with a zero
   minimum, so a wide image cannot burst it. A zero minimum here would let the
   row shrink its items until they all fit, so the row would never overflow,
   never scroll and never snap. The clamp holds each item under 60% of the row's
   own width, so the next item always shows at the edge and says the row
   continues, and no item ever grows wider than the window. */
.ao-region--rhythm-snap-row,
.ao-region--rhythm-pinned-row,
.ao-region--rhythm-slide-deck {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: clamp(14rem, 60%, 26rem);
  gap: var(--ao-block-spacing, 1.5rem);
  align-items: start;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--ao-page-margin, 1.5rem);
}

/* Each item states where the row comes to rest. The three-part selector is the
   one the other column rhythms use: a published block, a derived index card and
   the edit-mode wrapper, which sits inside the block container the region passes
   straight through. Each pinned key repeats all three through its pane and its
   track, which stand between the region and its items in every tier. */
.ao-region--rhythm-snap-row > .ao-block,
.ao-region--rhythm-snap-row > .ao-index-card,
.ao-region--rhythm-snap-row > .ao-page-blocks > .ao-edit-block,
.ao-region--rhythm-pinned-row > .ao-region-pin > .ao-region-track > .ao-block,
.ao-region--rhythm-pinned-row > .ao-region-pin > .ao-region-track > .ao-index-card,
.ao-region--rhythm-pinned-row > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block,
.ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-block,
.ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-index-card,
.ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-slide,
.ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block {
  scroll-snap-align: start;
}

/* A row takes keyboard focus, because a scroll container a reader cannot reach
   is a gallery they cannot see the end of. The ring says where focus is.

   THE RING NAMES NO THEME COLOUR. Both bundles carry this rule word for word, so
   a gallery focuses the same way whichever bundle a site ships. currentColor is
   the region's own ink, which every canvas key already pairs with its surface,
   so the ring stays readable on a raised or an accent canvas as well. */
.ao-region--rhythm-snap-row:focus-visible,
.ao-region--rhythm-pinned-row:focus-visible,
.ao-region--rhythm-slide-deck:focus-visible {
  outline: 2px solid var(--ao-accent, currentColor);
  outline-offset: 4px;
}

/* The pane and the track exist in the markup of every tier, because the markup
   cannot know which tier the reader's browser will apply. Here they have no box
   at all, so their children become grid items of the region exactly as a
   snapping row's children are. */
.ao-region--rhythm-pinned-row > .ao-region-pin,
.ao-region--rhythm-pinned-row > .ao-region-pin > .ao-region-track,
.ao-region--rhythm-slide-deck > .ao-region-pin,
.ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track {
  display: contents;
}

/* THE SLIDE LINK is inert outside the pinned deck tier: in the stacked
   fallback and reduced-motion renders it would be an empty, zero-size anchor —
   focusable noise — and those tiers reach the child pages through the nav.
   The deck tier turns it into the whole slide's click-through overlay. */
.ao-region--rhythm-slide-deck .ao-slide > .ao-slide-link {
  display: none;
}

/* THE GAP STEPS multiply the shared block-spacing token: tight halves it, normal
   is the token itself and wide doubles it. One step drives both the grid gap of a
   region that runs columns and the block margin of a region that stacks, so a
   step means the same air either way and no new length token is needed. */
.ao-region--gap-tight {
  gap: calc(var(--ao-block-spacing, 1.5rem) * 0.5);
}

.ao-region--gap-normal {
  gap: var(--ao-block-spacing, 1.5rem);
}

.ao-region--gap-wide {
  gap: calc(var(--ao-block-spacing, 1.5rem) * 2);
}

.ao-region--gap-tight .ao-block,
.ao-region--gap-tight .ao-index-card {
  margin-block: calc(var(--ao-block-spacing, 1.5rem) * 0.5);
}

.ao-region--gap-normal .ao-block,
.ao-region--gap-normal .ao-index-card {
  margin-block: var(--ao-block-spacing, 1.5rem);
}

.ao-region--gap-wide .ao-block,
.ao-region--gap-wide .ao-index-card {
  margin-block: calc(var(--ao-block-spacing, 1.5rem) * 2);
}

/* A region that runs columns spaces its items with the grid gap alone. The block
   margin that spaces a stacked block would add to that gap. */
.ao-region--rhythm-halves .ao-block,
.ao-region--rhythm-thirds .ao-block,
.ao-region--rhythm-third-third-full .ao-block,
.ao-region--rhythm-two-thirds-third .ao-block,
.ao-region--rhythm-snap-row .ao-block,
.ao-region--rhythm-pinned-row .ao-block,
.ao-region--rhythm-slide-deck .ao-block,
.ao-region--rhythm-halves .ao-index-card,
.ao-region--rhythm-thirds .ao-index-card,
.ao-region--rhythm-third-third-full .ao-index-card,
.ao-region--rhythm-two-thirds-third .ao-index-card,
.ao-region--rhythm-snap-row .ao-index-card,
.ao-region--rhythm-pinned-row .ao-index-card,
.ao-region--rhythm-slide-deck .ao-index-card {
  margin-block: 0;
}

/* A mark carries no stacked margin either. This rule follows the gap steps, so
   a strip that also states a gap key keeps its even row. */
.ao-region--rhythm-logo-strip .ao-block {
  margin-block: 0;
}

/* THE CANVAS COLOURS pair a surface token with the ink token that stays readable
   on it, because a surface chosen without its ink is unreadable text. Accent is
   the one surface in the colour contract with no ink of its own, so it pairs
   with ink-on-accent. */
.ao-region--canvas-default {
  background: var(--ao-surface, #ffffff);
  color: var(--ao-ink, #1f2937);
}

.ao-region--canvas-raised {
  background: var(--ao-surface-raised, #f3f4f6);
  color: var(--ao-ink-raised, var(--ao-ink, #1f2937));
  padding: var(--ao-block-spacing, 1.5rem);
}

.ao-region--canvas-accent {
  background: var(--ao-accent, #2563eb);
  color: var(--ao-ink-on-accent, #ffffff);
  padding: var(--ao-block-spacing, 1.5rem);
}

/* The paired ink reaches the headings and links inside the region too. A heading
   would otherwise keep the page ink, and a link would keep the accent colour it
   is now painted on — invisible in both cases. */
.ao-region--canvas-raised .ao-heading {
  color: var(--ao-ink-raised, var(--ao-ink-strong, #111827));
}

.ao-region--canvas-accent .ao-heading,
.ao-region--canvas-accent .ao-paragraph,
.ao-region--canvas-accent a {
  color: var(--ao-ink-on-accent, #ffffff);
}

/* THE LOGO STRIP CARRIES AIR ABOVE AND BELOW ITS MARKS in every tier, more
   above than below, so the band stands clear of the text before it. The rule
   sits after the canvas rules, because a raised or accent canvas states its own
   even padding and this one must win for the top and bottom edges. */
.ao-region--rhythm-logo-strip {
  padding-block: calc(var(--ao-block-spacing, 1.5rem) * 1.5) var(--ao-block-spacing, 1.5rem);
}

/* FULL BLEED escapes the content measure to the viewport edge and does nothing
   else: no scrolling track and no carousel. The region keeps its place in the
   flow and the negative inline margins pull it back out of the centred ancestors
   around it, so no wrapper element is needed. It is valid on a full-width region
   alone — a region holding one grid column cannot also reach the viewport edge —
   which the template save rejects and the render path ignores, so nothing here
   styles that combination.

   THE MEASURE INSIDE THE BAND is one rule with two branches on the rhythm and no
   third case. This branch is the single reading column: the padding keeps the
   content on the content measure, so a band of copy never runs the width of a
   wide screen, and never falls below the page margin on a narrow one.

   border-box holds that padding INSIDE the declared 100vw. Under the default
   content-box the padding would add to the width, the band would grow wider than
   the viewport, and the page would scroll sideways. border-box alone is not
   enough: 100vw still counts the scrollbar gutter, so the `.ao-site` clip rule
   below covers that last sliver. Both rules are needed. */
.ao-region--full-bleed {
  box-sizing: border-box;
  width: 100vw;
  max-width: 100vw;
  margin-inline: calc(50% - 50vw);
  padding-inline: max(var(--ao-page-margin, 1.5rem), calc((100vw - var(--ao-content-max-width, 48rem)) / 2));
}

/* The second branch: a region running columns takes the page margin alone,
   because its own columns already divide the width. */
.ao-region--full-bleed.ao-region--rhythm-halves,
.ao-region--full-bleed.ao-region--rhythm-thirds,
.ao-region--full-bleed.ao-region--rhythm-third-third-full,
.ao-region--full-bleed.ao-region--rhythm-two-thirds-third {
  padding-inline: var(--ao-page-margin, 1.5rem);
}

/* A full-bleed region is exactly one viewport wide, which is a hair wider than
   the page whenever a vertical scrollbar is present. Clipping that hair keeps
   the escape from ever producing a horizontal scrollbar. Clip rather than
   hidden: hidden would make this a scroll container and stop the section nav
   sticking. */
.ao-site {
  overflow-x: clip;
}

/* THE NUMBERING MOTIF numbers a region's items 01, 02, 03 with a CSS counter, so
   no number is written into content and re-ordering the blocks re-numbers them.
   The empty alternative text after the slash keeps the counter DECORATIVE: a
   screen reader reads the items once rather than a second, meaningless list.

   The number is taken out of flow, so it never becomes a flex or grid item of
   the block it labels; the item's top padding is the room it sits in. */
.ao-region--numbered {
  counter-reset: ao-region-item;
}

.ao-region--numbered > .ao-block,
.ao-region--numbered > .ao-index-card,
.ao-region--numbered > .ao-page-blocks > .ao-edit-block {
  position: relative;
  padding-top: 2rem;
}

.ao-region--numbered > .ao-block::before,
.ao-region--numbered > .ao-index-card::before,
.ao-region--numbered > .ao-page-blocks > .ao-edit-block::before {
  counter-increment: ao-region-item;
  content: counter(ao-region-item, decimal-leading-zero) / "";
  position: absolute;
  top: 0;
  left: 0;
  font-family: var(--ao-font-meta, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
  font-size: 0.78rem;
  letter-spacing: var(--ao-heading-tracking, 0.08em);
  color: var(--ao-muted, #6b7280);
}

/* THE MOTION MOTIF brings a region in as the reader scrolls it into view. It is
   an enhancement and nothing more, which is why every scroll-driven rule sits
   inside two guards. A reader who asks the operating system for reduced motion
   matches nothing here at all. A browser with no scroll-driven animations
   matches the load fade alone, which runs on a clock rather than on the scroll.

   NOTHING IS HIDDEN BY CLASS. The start state lives in the keyframes alone, so
   it exists only while an animation is running. A class rule holding `opacity:
   0` would survive wherever the animation never runs, and the content would
   simply be invisible.

   THE MOVEMENT IS VERTICAL ONLY. A horizontal transform on a full-bleed region
   pushes the page wider than the viewport and makes the document scroll
   sideways; a scale or a rotation reads as a slideshow rather than an article.

   EVERY CONSTANT IS A FIXED LITERAL, never a var(). An operator picks how a
   region arrives; the theme decides how far it travels and over what range. A
   `--ao-*` custom property would say the opposite: every variable these bundles
   read is a per-site style token, and every style token owes the drawer a
   control. Motion has none by design.

   THE DURATION IS STATED because a view timeline takes its progress from the
   range rather than from a clock, and some engines still refuse to run an
   animation with no duration declared. */
@media (prefers-reduced-motion: no-preference) {
  /* THE KEYFRAMES SIT OUTSIDE THE @supports GUARD, because the load fade below
     runs in every engine and Safari and Firefox ship no view timelines. A
     keyframes rule is inert until an animation names it, so an engine that
     reads these steps and matches no scroll-driven rule still applies no
     motion it cannot run. */
  @keyframes ao-region-fade {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

  /* The lift is 2.5rem. A shorter travel, spread over the whole arrival window,
     is not seen as movement at all: it reads as a change in brightness. */
  @keyframes ao-region-rise {
    from {
      opacity: 0;
      transform: translateY(2.5rem);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }

  /* THE FIRST REGION FADES ON A CLOCK. It is the one region certainly on screen
     at scroll 0, and a view timeline cannot animate an element that is already
     fully in view: the entry phase is over before the reader arrives. Regions
     are direct children of the composed body, so :first-child needs no class of
     its own.

     The longhands are written out, never the `animation` shorthand. The
     shorthand's reset of the timeline and of the range still differs between
     engines, and those two resets are the whole point of this rule.

     No :not() and no second guard are needed. This selector holds three
     compounds against the one of the scroll-driven rule below, so in a browser
     with view timelines the cascade gives the first region the clock and every
     later region the reader's scroll. */
  .ao-page-body--composed > .ao-region--motion-fade:first-child {
    animation-name: ao-region-fade;
    animation-duration: 600ms;
    animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
    animation-fill-mode: both;
    animation-timeline: auto;
    animation-range: normal;
  }

  /* THE LOGO STRIP LOOPS ON A CLOCK. The marks move left for as long as the page
     is open, so the loop reads no timeline and sits outside the @supports guard,
     beside the load fade above. A browser that resolves no view timeline still
     runs it, and a reader who asked for reduced motion still matches nothing
     here at all.

     THE TRACK BECOMES THE ROW HERE. Outside this guard the track is a
     pass-through box and the region lays the marks out as a wrapping strip. A
     translate needs one box to move, so here the track takes the row back, in a
     single line of its own width, and the region clips whatever runs past its
     edge.

     THE COPY OF THE MARKS DRAWS HERE AND NOWHERE ELSE. The still strip shows
     each mark once. Inside this guard the track carries two identical sets, so
     the copy arrives exactly where the author's set started and the wrap shows
     no seam.

     THE TRAVEL IS HALF THE TRACK PLUS HALF A GAP. Two sets make the track twice
     the width of one set, PLUS the one gap that falls between the sets. Half the
     track alone lands half a gap short, and the marks jump sideways at every
     wrap.

     THE CLIP MAKES NO SCROLL CONTAINER. The row runs wider than the page, so
     without the clip the document itself would widen and scroll sideways. clip
     cuts the overhang and offers the reader no gesture. */
  @keyframes ao-logo-strip-loop {
    from {
      transform: translateX(0);
    }
    to {
      transform: translateX(calc(-50% - calc(var(--ao-block-spacing, 1.5rem) * 1.5) / 2));
    }
  }

  .ao-region--rhythm-logo-strip {
    overflow: clip;
  }

  .ao-region--rhythm-logo-strip > .ao-region-track {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    gap: calc(var(--ao-block-spacing, 1.5rem) * 1.5);
    width: max-content;
    animation-name: ao-logo-strip-loop;
    animation-duration: 45s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
  }

  .ao-region--rhythm-logo-strip .ao-region-clones {
    display: contents;
  }

  /* TWO TRIGGERS STOP THE STRIP, because every mark is a link. A pointer reader
     rests on the band to read a mark; a keyboard reader tabs into it and hovers
     nothing at all. The loop PAUSES rather than stops: a paused animation keeps
     its position, so the strip carries on from where the reader held it instead
     of jumping back to the first mark. */
  .ao-region--rhythm-logo-strip:hover > .ao-region-track,
  .ao-region--rhythm-logo-strip:focus-within > .ao-region-track {
    animation-play-state: paused;
  }

  /* A FOCUSED MARK STAYS WHOLE AND IN VIEW. The pause alone cannot promise that.
     A reader tabs through the marks in the order the author placed them, and the
     clip has already cut the ones that ran past the edge; a clipping box scrolls
     no child of its own into view, because it is no scroll container. So while a
     mark holds focus the strip draws as the still wrapping strip a reduced-motion
     reader sees: the track gives up its box, which drops the translate with it,
     the copy of the marks stands down, and the region stops clipping so the whole
     focus ring paints.

     THIS RESETS NOTHING. The loop keeps running on the track throughout, paused
     at the position the reader stopped it, and the row returns to that position
     when focus leaves the strip. */
  .ao-region--rhythm-logo-strip:focus-within {
    overflow: visible;
  }

  .ao-region--rhythm-logo-strip:focus-within > .ao-region-track {
    display: contents;
  }

  .ao-region--rhythm-logo-strip:focus-within .ao-region-clones {
    display: none;
  }

  @supports (animation-timeline: view()) {
    /* ONE NAMED TIMELINE drives every scroll-driven rule in this bundle, and
       this rule is its only declaration. The anonymous view() binds to the
       nearest ancestor scroll container and takes that container's axis, so an
       item inside a scrolling row reads a timeline with no block-axis overflow
       and never moves. A named view timeline resolves by name up the ancestor
       chain, the declaring element included, and needs no timeline-scope. It is
       therefore the only mechanism that survives a scrolling row.

       The name is declared on the REGION element alone. An element with
       `display: contents` has no box and can carry no timeline, and edit mode
       inserts one such element between a region and its blocks.

       A region carrying both a motion key and a pinned rhythm matches two
       compounds of this one list. That is still one declaration and one
       timeline: the arrival rules read it over the entry phase, and the pinned
       sweep and the deck's cross-fades read it over the contain phase. */
    .ao-region--motion-fade, .ao-region--motion-rise, .ao-region--motion-stagger, .ao-region--rhythm-pinned-row, .ao-region--rhythm-slide-deck {
      view-timeline: --ao-region-view block;
    }

    /* Fade and rise animate the region element. Stagger animates the region's
       ITEMS, so each item is driven by its own position and needs no delay
       chain. Its selector is the numbering motif's three-part one: edit mode
       inserts a `display: contents` container between the region and its
       blocks, and an animation on such an element does nothing at all, so a
       two-part selector would silently stop working in the preview.

       THE ARRIVAL WINDOW IS MEASURED AGAINST THE READER'S WINDOW. `entry 40vh`
       is an absolute offset into the entry phase, so the arrival completes over
       40% of the viewport whatever the region's height. A `cover` percentage
       scales with the region instead, which stretches a short travel over
       hundreds of pixels of scroll and turns it into a drift. vh rather than
       dvh, because a range offset must not move when a mobile toolbar
       collapses.

       THE TIMING FUNCTION EASES OUT. Linear progress reads as a slider being
       dragged; an ease-out puts most of the travel early, so the region reads
       as arriving. */
    .ao-region--motion-fade,
    .ao-region--motion-rise,
    .ao-region--motion-stagger > .ao-block,
    .ao-region--motion-stagger > .ao-index-card,
    .ao-region--motion-stagger > .ao-page-blocks > .ao-edit-block {
      animation-duration: auto;
      animation-timeline: --ao-region-view;
      animation-range: entry 0% entry 40vh;
      animation-fill-mode: both;
      animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
    }

    .ao-region--motion-fade {
      animation-name: ao-region-fade;
    }

    .ao-region--motion-rise,
    .ao-region--motion-stagger > .ao-block,
    .ao-region--motion-stagger > .ao-index-card,
    .ao-region--motion-stagger > .ao-page-blocks > .ao-edit-block {
      animation-name: ao-region-rise;
    }

    /* THE STAGGER LADDER gives each item a 20vh window and starts it 5vh after
       the item before, so each rise reads as a step of a wave rather than as
       part of one long ramp. The tail rule holds every item past the fourth at
       the last rung, so however many items a region holds the whole wave still
       ends at `entry 40vh`.

       Each rung repeats the numbering motif's three-part selector. Edit mode
       interleaves one inserter between every pair of blocks, so the edit-mode
       index is twice the item index, in the same way the numbering motif
       answers a 3n rhythm with 6n. */
    .ao-region--motion-stagger > .ao-block:nth-child(2),
    .ao-region--motion-stagger > .ao-index-card:nth-child(2),
    .ao-region--motion-stagger > .ao-page-blocks > .ao-edit-block:nth-child(4) {
      animation-range: entry 5vh entry 25vh;
    }

    .ao-region--motion-stagger > .ao-block:nth-child(3),
    .ao-region--motion-stagger > .ao-index-card:nth-child(3),
    .ao-region--motion-stagger > .ao-page-blocks > .ao-edit-block:nth-child(6) {
      animation-range: entry 10vh entry 30vh;
    }

    .ao-region--motion-stagger > .ao-block:nth-child(4),
    .ao-region--motion-stagger > .ao-index-card:nth-child(4),
    .ao-region--motion-stagger > .ao-page-blocks > .ao-edit-block:nth-child(8) {
      animation-range: entry 15vh entry 35vh;
    }

    .ao-region--motion-stagger > .ao-block:nth-child(n + 5),
    .ao-region--motion-stagger > .ao-index-card:nth-child(n + 5),
    .ao-region--motion-stagger > .ao-page-blocks > .ao-edit-block:nth-child(n + 10) {
      animation-range: entry 20vh entry 40vh;
    }

    /* THE PINNED SWEEPING ROW. The region becomes a tall wrapper, the pane inside
       it pins to the viewport for exactly as long as the wrapper covers it, and
       the reader's downward scroll translates the track sideways. The reader sees
       a gallery pass by while the section holds still, and finds no sideways
       scrollbar to hunt for.

       THE WRAPPER IS NO SCROLL CONTAINER. `overflow: visible` is required: a
       scroll container here would trap the sticky pane in its own scroll box and
       the pin would never take. The plain row above sets overflow-x, so this rule
       must state the reset rather than leave it inherited.

       THE HEIGHT IS THE SCROLL THE SWEEP IS SPREAD OVER. dvh rather than vh,
       because the wrapper and the pane must match the visible viewport exactly or
       the pin slips as a mobile toolbar collapses. */
    .ao-region--rhythm-pinned-row {
      display: block;
      height: 200dvh;
      overflow: visible;
      scroll-snap-type: none;
    }

    /* THE HEIGHT LADDER keeps the sweep speed roughly constant. A fixed height
       sweeps a long track fast and a short one slowly, so each rung lengthens the
       scroll with the item count. The rungs cascade by source order, so they are
       written in ascending order and the last matching rung wins.

       Each :has() is wrapped in :where(), so a rung keeps the specificity of one
       class. The edit-mode stylesheet can then switch the whole pin off with an
       ordinary two-class selector.

       The count is of the TRACK's children, so it counts published blocks and
       derived index cards alike. Edit mode needs no second chain, because edit
       mode never reaches this tier. */
    .ao-region--rhythm-pinned-row:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 4))) {
      height: 260dvh;
    }

    .ao-region--rhythm-pinned-row:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 6))) {
      height: 320dvh;
    }

    .ao-region--rhythm-pinned-row:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 8))) {
      height: 380dvh;
    }

    .ao-region--rhythm-pinned-row:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 10))) {
      height: 440dvh;
    }

    /* THE PANE CLIPS, IT NEVER HIDES. `overflow: hidden` makes the pane a scroll
       container, which captures the timeline lookup and stops the sticky. `clip`
       cuts the track's overhang without making a scroll box, which is the same
       choice the site wrapper makes for the same reason.

       The pane is one viewport high, so the wrapper's own height decides how long
       it stays pinned. It is an inline-size container, so the sweep can measure
       its travel against the pane rather than against the window.

       BOTH PINNED KEYS WANT THIS SAME BOX, so they ride one rule. The row sweeps
       its track sideways inside the pane and the deck stacks its slides in one
       cell of it, but each key pins the same one-viewport pane. A slide taller
       than the pane is clipped here, which is the accepted cost of a pane one
       viewport high. */
    .ao-region--rhythm-pinned-row > .ao-region-pin,
    .ao-region--rhythm-slide-deck > .ao-region-pin {
      display: grid;
      align-content: center;
      position: sticky;
      top: 0;
      height: 100dvh;
      overflow: clip;
      container-type: inline-size;
    }

    /* THE PROGRESS BAR scrubs the deck's whole contain phase along the foot of
       the pane, in the accent, so the reader sees how far through the deck the
       scroll has carried them. It is a REAL ELEMENT in the pin's markup, not a
       pseudo-element: the pseudo-carve belongs to the flourish layers alone.
       It reads the same timeline as everything else, so it can never disagree
       with the slides about progress. */
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-deck-progress {
      position: absolute;
      inset-inline-start: 0;
      inset-block-end: 0;
      block-size: 3px;
      inline-size: 100%;
      background: var(--ao-accent, #2563eb);
      transform: translateX(-100%);
      animation-name: ao-deck-progress;
      animation-duration: auto;
      animation-timeline: --ao-region-view;
      animation-range: contain 0% contain 100%;
      animation-fill-mode: both;
      animation-timing-function: linear;
    }

    /* The bar TRANSLATES rather than scales: sideways travel is the one
       horizontal movement these bundles allow, and only inside a clipping box —
       which the pin is. A scaling transform would step outside the motif. */
    @keyframes ao-deck-progress {
      from {
        transform: translateX(-100%);
      }
      to {
        transform: translateX(0);
      }
    }

    /* THE TRACK IS THE BOX THAT MOVES, and it scrolls nothing itself: the
       reader's page scroll is the only input. It restates its column width
       because the plain row sizes an item at 60% of the row, and a percentage
       cannot resolve against a max-content width. 32cqw also shows about three
       images at once, which is what makes a sweep read as a sweep. */
    .ao-region--rhythm-pinned-row > .ao-region-pin > .ao-region-track {
      display: grid;
      grid-auto-flow: column;
      grid-auto-columns: clamp(14rem, 32cqw, 26rem);
      gap: var(--ao-block-spacing, 1.5rem);
      align-items: center;
      width: max-content;
      padding-inline: var(--ao-page-margin, 1.5rem);
      animation-name: ao-region-sweep;
      animation-duration: auto;
      animation-timeline: --ao-region-view;
      animation-range: contain 0% contain 100%;
      animation-fill-mode: both;
      animation-timing-function: linear;
    }

    /* THE SWEEP IS A SCRUB, so it keeps linear: it follows the reader's scroll
       one to one and reverses with it. `contain 0%` to `contain 100%` is exactly
       the interval in which the wrapper covers the viewport, which is exactly the
       interval in which the pane is pinned. No other range keeps the two in step.

       THE TRAVEL IS MEASURED IN cqw AGAINST THE PANE, never in dvw against the
       window. A region that is not full bleed is narrower than the window, and a
       window-measured sweep would run the track past its own end and leave a
       blank pane. This is the one horizontal transform in this bundle, and it is
       safe because the pane clips: the track cannot widen the page. */
    @keyframes ao-region-sweep {
      from {
        transform: translateX(0);
      }
      to {
        transform: translateX(calc(100cqw - 100%));
      }
    }

    /* THE FOCUS RING MOVES TO THE PANE. An outline drawn around a wrapper two or
       more viewports tall is not a focus indicator: the reader sees no edge of
       it. The pane is the box the reader is looking at, so the ring goes there.
       The inset offset keeps the ring inside the clip. */
    .ao-region--rhythm-pinned-row:focus-visible,
    .ao-region--rhythm-slide-deck:focus-visible {
      outline: none;
    }

    .ao-region--rhythm-pinned-row:focus-visible > .ao-region-pin,
    .ao-region--rhythm-slide-deck:focus-visible > .ao-region-pin {
      outline: 2px solid var(--ao-accent, currentColor);
      outline-offset: -4px;
    }

    /* THE SLIDE DECK reuses the pin and the track, and stacks the region's items
       in ONE cell of that track. Exactly one slide fills the pane at a time. The
       reader's scroll is the only input, so every cross-fade scrubs with the
       scroll and reverses when the reader goes back up.

       THE WRAPPER IS NO SCROLL CONTAINER. `overflow: visible` is required: a
       scroll container here would trap the sticky pane in its own scroll box and
       the pin would never take. The plain row above sets overflow-x, so this rule
       must state the reset rather than leave it inherited.

       THE DECK KEEPS A WRAPPER RULE OF ITS OWN rather than joining the pinned
       row's. Two guards read the pinned wrapper by its exact selector, and a
       joined selector list would tell them that wrapper declares no height and no
       overflow at all. */
    .ao-region--rhythm-slide-deck {
      display: block;
      height: 200dvh;
      overflow: visible;
      scroll-snap-type: none;
    }

    /* THE HEIGHT LADDER IS WRITTEN BY THE BUILD, from the page's own longest
       deck, and delivered as a small style element in the page head. The rule
       above is this bundle's whole share of the height: a deck of one slide
       reads it and matches no rung at all.

       A rung gives each slide one viewport of scroll and the pin one more, so a
       rung's height is its slide count plus one, in viewports. The contain phase
       is the wrapper height minus one viewport, which is exactly the interval in
       which the pane is pinned. A deck of N slides therefore has N viewports of
       contain phase, and slide k owns the k-th of them.

       A hand-written ladder stops at the count someone typed, and a longer deck
       then holds every further slide in the last rung's window. The build reads
       the real slide count instead, so a deck of any length gets a rung of its
       own. */

    /* THE DECK'S TRACK MOVES NOTHING. It states no grid-auto-flow, no max-content
       width and no animation: all three lay a filmstrip out sideways and belong
       to the sweep. It states no height either, because the pane's
       align-content: center sizes its one implicit row to the content, so a
       percentage height would resolve to auto. The pane centres the track. */
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track {
      display: grid;
      width: 100%;
      justify-items: center;
      /* THE ROW IS THE LANDSCAPE FRAME. Every slide stacks in this ONE cell, so
         a per-slide height would stretch to the tallest slide and the deck
         would read portrait wherever one long text slide does. 56.25cqw is the
         pane's own 16:9 height, so the deck reads as a widescreen stage that
         centres in the pane — and on a portrait device centres in the tall
         viewport. A slide taller than the frame overflows the row edge in
         visible calm rather than losing text. The dvh cap covers a wide window
         shorter than its own 16:9 frame. */
      grid-auto-rows: min(56.25cqw, 92dvh);
      /* border-box holds the inline padding INSIDE the declared 100%, in the
         manner of the slide rule below: under the default content-box the
         track stands 2 page margins wider than the pane, and the pane's clip
         cuts the right edge off every slide (the last digit of a slide number,
         the last word of a full line). */
      box-sizing: border-box;
      padding-inline: var(--ao-page-margin, 1.5rem);
    }

    /* EVERY SLIDE CLAIMS THE SAME CELL. Without grid-area a grid with no template
       gives each child an implicit row of its own, and the deck would read as a
       stack down the page rather than as one pane.

       The selector repeats the numbering motif's three-part chain, through the
       pane and the track: a > chain from the region reaches no item inside the
       track, and edit mode inserts one further pass-through container.

       TWO ANIMATIONS PER SLIDE. The slides are stacked, so a slide must fade OUT
       as well as in, or every slide the reader has passed would still be painted
       under the current one.

       THE FILL-MODE PAIR IS THE WHOLE MECHANISM. The fade-out is second in the
       list, so it wins wherever it contributes a value. `forwards` stops it
       contributing BEFORE its own window, where the fade-in must be free to run.
       `both` on the fade-in holds a slide at opacity 0 until its own window
       arrives.

       The duration, the timeline and the timing function state ONE value each. An
       animation longhand list repeats to match the length of animation-name, so
       one value covers both animations and cannot fall out of step with the
       other. Both fades run linear because both are scrubs: they follow the
       reader's scroll one to one and reverse with it. */
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-block,
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-index-card,
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-slide,
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block {
      grid-area: 1 / 1;
      width: 100%;
      margin: 0;
      animation-name: ao-region-slide-in, ao-region-slide-out;
      animation-duration: auto;
      animation-timeline: --ao-region-view;
      animation-timing-function: linear;
      animation-fill-mode: both, forwards;
      animation-range: contain 0dvh contain 40dvh, contain 100dvh contain 140dvh;
    }

    /* THE FIRST SLIDE NEVER FADES IN. It is visible before the pin takes, and the
       shared rule's backwards fill at opacity 0 would leave the pane blank for the
       whole viewport in which the section arrives. `none` still occupies its slot
       in the list, so the fade-out keeps the second range and the second fill
       mode.

       Edit mode interleaves one inserter between every pair of blocks and one
       above the first, so the edit-mode index is twice the slide index. */
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-block:nth-child(1),
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-index-card:nth-child(1),
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-slide:nth-child(1),
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block:nth-child(2) {
      animation-name: none, ao-region-slide-out;
    }

    /* THE CONTAIN-PHASE LADDER IS WRITTEN BY THE BUILD as well, and it starts at
       index two: the shared rule above already serves slide one.

       Slide k fades in over the 40dvh that starts at (k - 1) * 100dvh, and fades
       out over the 40dvh that starts at k * 100dvh. Slide k's fade-out window IS
       slide k + 1's fade-in window, and that equality is what makes the pair a
       cross-fade rather than two events with a blank pane between them. Each rung
       therefore states its own index alone and never the count of slides.

       Every offset names the contain phase in dvh. A phase-relative percentage
       would move with the slide count, and any other unit would drift against the
       wrapper as a mobile toolbar collapses.

       THE LAST SLIDE OF ANY DECK NEVER FADES OUT. Its fade-out window starts at
       the end of the contain phase, so that animation stays in its before phase,
       contributes nothing, and the slide holds while the section scrolls away.
       That falls out of the arithmetic and needs no rule of its own. */

    /* THE TWO FADES CARRY NAMES OF THEIR OWN. ao-region-slide-in repeats the steps
       of ao-region-fade on purpose: the timing-function guard keys on the
       animation name, so a scrubbed fade needs its own name. Sharing the arrival
       fade's name would let an eased arrival turn linear unnoticed.

       Neither rule declares a transform, so the sideways exception stays with the
       sweep and every slide keeps its place in the accessibility tree. */
    /* THE FADES CARRY POINTER-EVENTS AS WELL AS OPACITY. Opacity hides a
       slide from the eye but not from the pointer: every slide (and its
       overlay link) stacks in the same cell, so without this the topmost DOM
       slide would swallow every click meant for the visible one. It is
       pointer-events and DELIBERATELY NOT visibility: visibility would pull a
       faded slide out of the accessibility tree, and the deck keeps every
       slide readable and focusable. Pointer-events animates discretely — a
       slide is clickable for any part of its fade and inert the rest of the
       pass — which is exactly the hit-testing the stack needs. */
    @keyframes ao-region-slide-in {
      from {
        opacity: 0;
        pointer-events: none;
      }
      to {
        opacity: 1;
        pointer-events: auto;
      }
    }

    @keyframes ao-region-slide-out {
      from {
        opacity: 1;
        pointer-events: auto;
      }
      to {
        opacity: 0;
        pointer-events: none;
      }
    }

    /* THE SLIDE IMAGE IS CAPPED AGAINST THE PANE. This is the one place a motion
       rule reaches inside a block, and it is stated deliberately: the shipped
       image rule caps the width alone, so a portrait photograph is taller than a
       one-viewport pane and the pane clips it. The cap leaves the rest of the pane
       for the caption, which follows the image inside the figure and needs no rule
       of its own.

       object-fit is defensive rather than decoration. The shipped image rule
       leaves the width auto, so a browser already holds the aspect ratio when
       max-height clamps the height. `contain` keeps that true if a later rule ever
       gives the image an explicit width or height, and it crops nothing.

       .ao-image-img is the class every image the block components render carries,
       so one class reaches a standalone image block, an image-and-text block and a
       derived index card. */
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-block .ao-image-img,
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-index-card .ao-image-img,
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-slide .ao-image-img,
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block .ao-image-img {
      /* The cap tracks the landscape frame (56.25cqw tall), leaving room for
         the title band — a viewport-measured cap would overflow the frame on a
         portrait device where the frame is far shorter than the screen. */
      max-height: min(42cqw, 66dvh);
      object-fit: contain;
    }

    /* THE SLIDE WRAPPER holds one DERIVED slide: one block of one navigation
       child, taken from the child's own page. It exists because the child title
       band must sit inside the slide, and the shared block component owns the
       block's own markup. It takes a position of its own here, so the band can
       be placed against the slide rather than laid out after the block.

       THE WRAPPER FILLS THE PANE, which is one viewport tall, and it centres its
       block. Both are needed for one band position: a wrapper sized to its own
       block would put the band under a photograph and straight over a short
       paragraph, so the band would move with every slide. The foot padding is the
       band's own room, so no block is ever drawn under the words. border-box
       holds that padding INSIDE the viewport height, in the manner of the
       full-bleed rule: under the default content-box the wrapper would stand
       taller than the pane and the pane would clip the band.

       THE ELEMENT IS IN THE MARKUP AND ONLY ITS POSITION IS A CSS MATTER, which
       is why this rule sits inside the guards and the element does not. The
       reduced-motion tier, the fallback tier and the edit tier all render the
       band in normal flow, after the block, and all read the child's name. */
    /* THE SLIDE LINK overlays the whole slide as its click-through to the
       child page the slide quotes. It sits above the block content — a link
       inside a quoted block is not reachable from the deck, which is the
       accepted cost of one whole-slide target; the child page shows the
       block's own links. The title band stays plain text beneath it. */
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-slide > .ao-slide-link {
      display: block;
      position: absolute;
      inset: 0;
      z-index: 1;
    }

    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-slide {
      position: relative;
      box-sizing: border-box;
      display: grid;
      align-content: center;
      /* The slide fills the track's landscape row (the frame lives there — see
         the track rule above). The block padding is tighter at the head than
         the foot, so content compresses upward and the foot keeps the title
         band's room. */
      min-block-size: 100%;
      padding-block: 0.75rem 3.25rem;
    }

    /* THE CHILD TITLE BAND names the child page the next run of slides came
       from, so the reader is told whose work they are looking at. It is drawn
       across the FOOT of the slide, over the block, and painted with the theme's
       own surface token, so the words stay readable over a photograph and over
       prose alike.

       Placing it against the slide keeps it in the same place whether the first
       block carries an image or not, which gives the reader one consistent
       signal down the whole deck.

       THE BAND NAMES NO THEME COLOUR AND NO THEME FONT, in the manner of the
       focus ring above. Both bundles carry this rule word for word, so a deck
       bands its slides the same way whichever bundle a site ships. The two
       tokens carry the reader's own canvas as their fallback, so a site with no
       colour record still gets a readable band. */
    .ao-region--rhythm-slide-deck > .ao-region-pin > .ao-region-track > .ao-slide > .ao-slide-title {
      position: absolute;
      inset-inline: 0;
      inset-block-end: 0;
      margin: 0;
      padding: 0.65rem 1rem;
      background: var(--ao-surface, Canvas);
      color: var(--ao-ink-strong, CanvasText);
      /* The band names whose work the slides are: it reads as a title beside
         the flourish's slide number, not as a caption under it. */
      font-size: clamp(1rem, 2.4cqw, 1.5rem);
      font-weight: 600;
      line-height: 1.3;
    }

    /* THE FLOURISH SECTION decorates the CHANGE of slide. Every layer is a
       pseudo-element, so a flourish adds no markup, no entry in the accessibility
       tree and no word the search index can read. Two presets draw one layer over
       the whole slideshow and two draw on each slide, and each animates on the
       deck's own timeline.

       THE ADDED CONDITION IS INERT. The deck already ships `animation-range:
       contain 0dvh contain 40dvh` and runs, so an engine that renders the deck
       satisfies `animation-range: contain 0dvh` as well. The nested at-rule is
       here to DELIMIT the section by text: the parity guard cuts it out before it
       compares the two bundles character for character, and a comment cannot
       delimit anything because every guard strips comments before it parses. An
       engine that fails the query draws no flourish and renders the deck
       unharmed. */
    @supports (animation-timeline: view()) and (animation-range: contain 0dvh) {
      /* A PANE PRESET DRAWS ON THE TRACK. Its layer is one box over the slides
         themselves, so the decoration stays inside the slideshow. The pane is a
         different box: it is one viewport tall and as wide as the region, so a
         layer bound to the pane crosses whatever the page places beside the deck
         and covers the height of the window.

         THE TRACK NEEDS A POSITION OF ITS OWN for that, and it takes one here
         rather than in the deck's shared track rule: the parity guard compares
         that rule character for character and a deck guard reads it. Without
         this the layers below would resolve against the pane, which is the
         nearest positioned ancestor. */
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-sweep-line, .ao-region--flourish-wash) > .ao-region-pin > .ao-region-track {
        position: relative;
      }

      /* THE LAYER IS ONE BOX OVER THE TRACK. It measures its travel in cqw, which
         reads the pane: the pane declares container-type: inline-size and the
         track spans it, so one pass crosses the slides exactly once.

         A PANE PRESET MAY DRAW TWO LAYERS. The track owns one ::before and one
         ::after, and both take this same box, so both selectors share one rule
         rather than a second copy of the property list. The sweep line draws on
         both; the wash draws on the ::before alone.

         position: absolute is required rather than cosmetic. The track is a grid
         container, so a layer would otherwise become a grid item and take a cell
         of its own beside the slides.

         pointer-events: none is required too. A layer covers every slide and
         would otherwise swallow every click on a link inside one.

         opacity: 0 is the resting state, so a layer whose animation never runs is
         invisible.

         THE TRACK OVERHANGS THE PANE on the trailing edge, by its own inline
         padding, and the pane clips there. A layer loses those last pixels
         exactly as a slide photograph does. That is the deck's own geometry, and
         the remedy belongs to the shared slide rule rather than to a nudged
         flourish literal.

         THIS RULE NAMES NO ANIMATION, no range and no iteration count. The rule
         below names one, and it tests for a SECOND SLIDE before it does, which is
         what keeps a deck of one slide silent: such a deck carries no animation
         at all and draws nothing. The range and the pass count come from the
         build, which writes a rung for each slide count the page really has. */
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-sweep-line, .ao-region--flourish-wash) > .ao-region-pin > .ao-region-track::before,
      .ao-region--rhythm-slide-deck.ao-region--flourish-sweep-line > .ao-region-pin > .ao-region-track::after {
        content: "";
        position: absolute;
        inset-block: 0;
        inset-inline-start: 0;
        inline-size: 100%;
        z-index: 1;
        pointer-events: none;
        opacity: 0;
        animation-duration: auto;
        animation-timeline: --ao-region-view;
        animation-fill-mode: both;
      }

      /* THE SWEEP LINE crosses the slides as they change. The travel is measured
         in cqw, which reads the PANE, never in dvw against the window: a region
         narrower than the window would run the line past its own end and leave
         the slides unmarked. The sideways translate is safe because the pane
         clips, so the line cannot widen the page. */
      .ao-region--rhythm-slide-deck.ao-region--flourish-sweep-line > .ao-region-pin > .ao-region-track::before {
        inline-size: 2px;
        background: var(--ao-accent, #374151);
      }

      /* THE FLICKER LINES are the sweep line's second layer: a field of thin
         horizontal rules that blink as the vertical line crosses. They keep the
         track's full width and never translate, so the sideways exception stays
         with the sweep line alone.

         The gradient states its own stop positions in pixels rather than in
         percentages, so the spacing of the rules is the same on a short slide and
         on a tall one. */
      .ao-region--rhythm-slide-deck.ao-region--flourish-sweep-line > .ao-region-pin > .ao-region-track::after {
        background: repeating-linear-gradient(to bottom, var(--ao-accent, #374151) 0 1px, transparent 1px 7px);
      }

      /* THE WASH deepens through each cross-fade and clears before the slide
         holds. Opacity is the one property it animates, so its peak lives in the
         keyframes rather than in this rule. */
      .ao-region--rhythm-slide-deck.ao-region--flourish-wash > .ao-region-pin > .ao-region-track::before {
        background: radial-gradient(115% 90% at 50% 50%, rgba(0, 0, 0, 0) 42%, var(--ao-ink, #1f2937) 100%);
      }

      /* THE PASS IS NAMED HERE, AND ONLY FOR A DECK THAT CHANGES SLIDE. The
         build writes the range and the pass count from the page's own longest
         deck, one rung per slide count, and it writes nothing at all for a deck
         of one slide. This rule therefore tests for a second slide before it
         names an animation, so a one-slide deck still carries none and draws
         nothing.

         THE BUILD'S RUNG NAMES NO FLOURISH KEY. It sets the range and the pass
         count on the deck track's ::before and its ::after together, so one
         ladder serves every preset here and every authored record, and the
         flicker can no longer drift a window away from its own line.

         The :has() is wrapped in :where(), so the rule weighs its two classes
         alone and the edit-mode stylesheet still beats it. Every flourish runs
         linear because every flourish is a scrub: it follows the reader's scroll
         one to one and reverses with it. */
      .ao-region--rhythm-slide-deck.ao-region--flourish-sweep-line:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 2))) > .ao-region-pin > .ao-region-track::before {
        animation-name: ao-flourish-sweep-line;
        animation-timing-function: linear;
      }

      /* THE FLICKER IS THE SWEEP LINE'S SECOND LAYER, and the two must fire
         together. Both test the same slide count and both read the one generated
         rung, so neither can drift a window or a pass away from the other. */
      .ao-region--rhythm-slide-deck.ao-region--flourish-sweep-line:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 2))) > .ao-region-pin > .ao-region-track::after {
        animation-name: ao-flourish-flicker;
        animation-timing-function: linear;
      }

      .ao-region--rhythm-slide-deck.ao-region--flourish-wash:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 2))) > .ao-region-pin > .ao-region-track::before {
        animation-name: ao-flourish-wash;
        animation-timing-function: linear;
      }

      /* THE SLIDE LAYER belongs to its slide. position: relative goes on a rule of
         its own, scoped to the two slide presets, so the deck's shared slide rule
         stays exactly as it is: the parity guard compares that rule character for
         character and two deck guards read it. */
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-block,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-index-card,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-slide,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block {
        position: relative;
      }

      /* animation-range: inherit IS THE WHOLE TIMING MECHANISM of the slide layer,
         and it replaces a second copy of the contain-phase ladder. A pseudo-element
         inherits its originating element's computed range, so `inherit` takes the
         slide's own fade-in window followed by its fade-out window. Each preset
         states ONE animation-name, and every animation-* list truncates to the
         length of animation-name, so the fade-in window alone applies. The slide's
         own ladder stays the one source of the arithmetic, and a retune of the deck
         retunes the flourish with it. */
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-block::after,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-index-card::after,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-slide::after,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block::after {
        content: "";
        position: absolute;
        pointer-events: none;
        opacity: 0;
        animation-duration: auto;
        animation-timeline: --ao-region-view;
        animation-fill-mode: both;
        animation-range: inherit;
      }

      /* THE ACCENT BAR arrives with its slide on opacity and a short lift alone. It
         neither scales nor clips: both properties sit outside the allowed keyframe
         list, and the slide box clips no overhang. */
      .ao-region--rhythm-slide-deck.ao-region--flourish-accent-bar > .ao-region-pin > .ao-region-track > .ao-block::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-accent-bar > .ao-region-pin > .ao-region-track > .ao-index-card::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-accent-bar > .ao-region-pin > .ao-region-track > .ao-slide::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-accent-bar > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block::after {
        animation-name: ao-flourish-accent-bar;
        animation-timing-function: linear;
        inset-block-end: 0;
        inset-inline-start: 0;
        block-size: 3px;
        inline-size: 38%;
        background: var(--ao-accent, #374151);
      }

      /* THE BAR ALTERNATES ITS SIDE BY INDEX, which is where per-slide variety
         comes from. No block carries a field. Edit mode interleaves one inserter
         above the first block and one between every pair, so the edit-mode index is
         twice the slide index and :nth-child(4n) selects the slides the published
         chains select with :nth-child(even). */
      .ao-region--rhythm-slide-deck.ao-region--flourish-accent-bar > .ao-region-pin > .ao-region-track > .ao-block:nth-child(even)::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-accent-bar > .ao-region-pin > .ao-region-track > .ao-index-card:nth-child(even)::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-accent-bar > .ao-region-pin > .ao-region-track > .ao-slide:nth-child(even)::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-accent-bar > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block:nth-child(4n)::after {
        inset-inline-start: auto;
        inset-inline-end: 0;
      }

      /* THE NUMERAL IS DRAWN WITH A COUNTER OF ITS OWN, never the numbering
         motif's ao-region-item. A region may carry both motifs, and one counter
         shared by two would number neither correctly. The reset sits on the track
         and carries the flourish class, so the deck's own track guard still reads
         the rule it was written for. */
      .ao-region--rhythm-slide-deck.ao-region--flourish-numeral > .ao-region-pin > .ao-region-track {
        counter-reset: ao-flourish-slide;
      }

      /* The empty alternative text after the slash keeps the number DECORATIVE, so
         a screen reader is not read a second, meaningless list. The numeral tops
         out at a low opacity, so it sits behind the reading rather than in front of
         it. It takes no negative z-index: the slide creates no stacking context, so
         a negative index would drop the number behind the pane's own background. */
      .ao-region--rhythm-slide-deck.ao-region--flourish-numeral > .ao-region-pin > .ao-region-track > .ao-block::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-numeral > .ao-region-pin > .ao-region-track > .ao-index-card::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-numeral > .ao-region-pin > .ao-region-track > .ao-slide::after,
      .ao-region--rhythm-slide-deck.ao-region--flourish-numeral > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block::after {
        animation-name: ao-flourish-numeral;
        animation-timing-function: linear;
        counter-increment: ao-flourish-slide;
        content: counter(ao-flourish-slide, decimal-leading-zero) / "";
        inset-block-start: 0;
        inset-inline-end: 0;
        font-family: var(--ao-font-meta, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
        font-size: clamp(3rem, 12cqw, 8rem);
        line-height: 1;
        color: var(--ao-muted, #6b7280);
      }

      /* THE FIRST SLIDE CARRIES NO LAYER. Its own window is the interval in which
         the section arrives, and nothing may run there. It still increments the
         counter, so slide two reads 02 and the numbering stays true. */
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-block:nth-child(1)::after,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-index-card:nth-child(1)::after,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-slide:nth-child(1)::after,
      .ao-region--rhythm-slide-deck:is(.ao-region--flourish-accent-bar, .ao-region--flourish-numeral) > .ao-region-pin > .ao-region-track > .ao-page-blocks > .ao-edit-block:nth-child(2)::after {
        animation-name: none;
      }

      /* EACH PANE KEYFRAMES GOES QUIET AT THE CROSS-FADE BOUNDARY. One pass is one
         slide advance long, and the boundary is the cross-fade length divided by
         that advance. The steps beyond it rest at nothing, so the pane is quiet
         while a slide holds. */
      @keyframes ao-flourish-sweep-line {
        0% {
          opacity: 0;
          transform: translateX(0);
        }
        10% {
          opacity: 1;
        }
        40% {
          opacity: 0;
          transform: translateX(100cqw);
        }
        100% {
          opacity: 0;
          transform: translateX(100cqw);
        }
      }

      /* THE FLICKER STEPS ALTERNATE BETWEEN NOTHING AND A LOW VALUE, so the lines
         blink rather than fade. The steps are uneven on purpose: an even spacing
         reads as a pulse, and a flicker reads as one only when the gaps differ.
         Opacity is the one property here, so the field of lines holds its place
         and the sideways exception stays with the sweep line. */
      @keyframes ao-flourish-flicker {
        0% {
          opacity: 0;
        }
        8% {
          opacity: 0.5;
        }
        12% {
          opacity: 0;
        }
        18% {
          opacity: 0.32;
        }
        26% {
          opacity: 0;
        }
        34% {
          opacity: 0.18;
        }
        40% {
          opacity: 0;
        }
        100% {
          opacity: 0;
        }
      }

      @keyframes ao-flourish-wash {
        0% {
          opacity: 0;
        }
        20% {
          opacity: 0.3;
        }
        40% {
          opacity: 0;
        }
        100% {
          opacity: 0;
        }
      }

      @keyframes ao-flourish-accent-bar {
        from {
          opacity: 0;
          transform: translateY(0.75rem);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      @keyframes ao-flourish-numeral {
        from {
          opacity: 0;
          transform: translateY(1.5rem);
        }
        to {
          opacity: 0.16;
          transform: translateY(0);
        }
      }
    }
  }
}

/* Edit mode inserts one block container between a region and its blocks. It
   passes its children straight through, so the region's own grid still lays the
   blocks out and the admin shows the composition the published page will have.
   The scope is deliberate: the legacy single-body stack carries the same class
   outside any region and keeps its own box. */
.ao-region > .ao-page-blocks {
  display: contents;
}

/* An inserter is edit-mode chrome rather than composition, so it takes a row of
   its own instead of a cell of the rhythm. */
.ao-region > .ao-page-blocks > .ao-block-inserter {
  grid-column: 1 / -1;
}

/* The snapping row runs its items along the columns, so a row of its own means
   the whole track: one inserter would stretch across every item in the row.
   Here the inserter takes the next column instead. */
.ao-region--rhythm-snap-row > .ao-page-blocks > .ao-block-inserter {
  grid-column: auto;
}

/* THE DERIVED INDEX CARD. A page on an index template renders one card for each
   of its navigation children, and each card is a DIRECT child of the region, so
   the rhythm, gap and numbering rules above size it exactly as they size a
   block. A card therefore declares no width and no grid column of its own: the
   operator composes the grid with the keys that already exist.

   THE WHOLE CARD IS THE TARGET. It draws no border, no button and no "read more"
   affordance, so the composition reads as images with captions rather than as a
   row of panels. The stacked margin matches a block's, so a card in a
   single-column region sits on the same rhythm as the prose above it.

   NOTHING CROPS THE IMAGE. Each card keeps its source image's own aspect ratio,
   and a caption sits under its own image at its own height — the region grid
   already aligns its items to the start, which is what makes the captions vary
   as the reference composition does. */
.ao-index-card {
  display: grid;
  align-content: start;
  gap: calc(var(--ao-block-spacing, 1.5rem) * 0.35);
  margin: var(--ao-block-spacing, 1.5rem) 0;
  text-decoration: none;
  color: inherit;
}

/* The card is an anchor inside the page body, so `.ao-page-body a` outweighs
   `.ao-index-card` and would paint the whole card as underlined accent-colour
   link text. A descendant cannot cancel an ancestor's text-decoration, so the
   override must sit on the anchor itself and carry more specificity than the
   prose-link rule. The hover/focus state below restores the underline on the
   title alone, which is the card's only link affordance. */
.ao-page-body a.ao-index-card {
  text-decoration: none;
  color: inherit;
}

.ao-index-card-image {
  display: block;
  width: 100%;
  height: auto;
}

.ao-index-card-eyebrow {
  font-family: var(--ao-font-meta, var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
  font-size: 0.78rem;
  letter-spacing: var(--ao-heading-tracking, 0.08em);
  text-transform: uppercase;
  color: var(--ao-muted, #6b7280);
}

.ao-index-card-title {
  font-family: var(--ao-font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ao-ink-strong, #111827);
}

.ao-index-card-lead {
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--ao-ink, #1f2937);
}

/* The card carries no permanent underline, so the pointer and the keyboard both
   need a state that says the whole card is one link. */
.ao-index-card:hover .ao-index-card-title,
.ao-index-card:focus-visible .ao-index-card-title {
  text-decoration: underline;
}

.ao-index-card:focus-visible {
  outline: 2px solid var(--ao-accent, #2563eb);
  outline-offset: 4px;
}

/* THE NARROW-SCREEN COLLAPSE. Below this theme's breakpoint every region spans
   one column, so a rail stacks under the body it sat beside and every rhythm
   reads as a single stack. The operator sets one set of layout choices and the
   theme decides how they degrade: there is no per-breakpoint control and no
   device tab.

   THE BUNDLE CARRIES ONE NARROW TIER. Every below-breakpoint rule sits here, so
   a reader of this file finds the whole phone treatment in one place. */
@media (max-width: 720px) {
  /* The page rail stacks under the body it sat beside, so the reading column
     keeps the whole width on a phone. */
  .ao-page-wrap.ao-has-aside { display: block; }
  .ao-section-nav { position: static; margin-top: 2rem; border-left: 0; border-top: 2px solid var(--ao-rule, #e5e7eb); padding: 1.25rem 0 0; }

  /* THE HEADER STACKS AND CENTRES ON A PHONE. The Site logo sits centred over
     the nav, the nav keeps its items on one line at a tighter gap and a smaller
     size, and the search form takes the whole width. A header that holds the
     Site logo has no padding of its own, so the logo link carries a little air
     above itself. */
  .ao-header {
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    text-align: center;
  }
  .ao-brand:has(img) { margin-top: 0.5rem; }
  .ao-nav ul { justify-content: center; gap: 0.75rem; font-size: 0.95rem; }
  .ao-header .ao-header-search { width: 100%; padding-block: 0.75rem 1.25rem; }
  .ao-header-search .ao-search-input { flex: 1 1 auto; min-width: 0; }

  /* The footer's two columns stack on a phone, so each list keeps the whole
     width and no label breaks across lines. */
  .ao-footer-columns { flex-direction: column; }

  /* THE STILL LOGO STRIP WRAPS, IT NEVER SCROLLS. A reader must see every mark
     without a gesture, so the mark box steps down here in the same proportion
     as the wide tier and more marks wrap onto each row. Each cell still takes
     the width of its own mark, so the strip grows a row rather than a
     scrollbar. */
  .ao-region--rhythm-logo-strip {
    gap: calc(var(--ao-block-spacing, 1.5rem) * 0.75);
  }

  .ao-region--rhythm-logo-strip .ao-image .ao-image-img {
    height: calc(var(--ao-block-spacing, 1.5rem) * 1.5625);
  }

  .ao-page-body--composed:has(> :is(.ao-region--place-main, .ao-region--place-side)) {
    grid-template-columns: minmax(0, 1fr);
  }

  .ao-page-body--composed > .ao-region--place-main,
  .ao-page-body--composed > .ao-region--place-side {
    grid-column: 1 / -1;
  }

  /* THE BANNER PAGE COLLAPSES HERE TOO. Its own column rule out-specifies the
     collapse above, so the two equal columns would survive on a phone unless
     this tier restates the single track. The stacked order stays the template's
     order, because the grid still flows by row. */
  .ao-page-body--composed:has(> .ao-region--hero.ao-region--place-full):has(> .ao-region--place-main) {
    grid-template-columns: minmax(0, 1fr);
  }

  .ao-region--rhythm-halves,
  .ao-region--rhythm-thirds,
  .ao-region--rhythm-third-third-full,
  .ao-region--rhythm-two-thirds-third {
    grid-template-columns: minmax(0, 1fr);
  }

  .ao-region--rhythm-third-third-full > .ao-block:nth-child(3n),
  .ao-region--rhythm-third-third-full > .ao-index-card:nth-child(3n),
  .ao-region--rhythm-third-third-full > .ao-page-blocks > .ao-edit-block:nth-child(6n),
  .ao-region--rhythm-two-thirds-third > .ao-block:nth-child(2n + 1),
  .ao-region--rhythm-two-thirds-third > .ao-index-card:nth-child(2n + 1),
  .ao-region--rhythm-two-thirds-third > .ao-page-blocks > .ao-edit-block:nth-child(4n + 2) {
    grid-column: 1 / -1;
  }

  /* The chapters rail stacks into the reading column here, exactly as the blog's
     section nav does. A sticky element in a single stack pins itself over the
     article the reader is trying to read. */
  .ao-region--chapters {
    position: static;
    max-height: none;
  }
}

/* --- Live-site search (spec 2026-07-10-live-site-search) ------------------ */
/* The header input, the /search/ results page and the "Did you mean" banner are
   styled ENTIRELY via the shared --ao-* token contract (every literal is only a
   var() fallback), so a site's own style tokens restyle them automatically with
   no hard-coded colours or fonts. */

/* Accessible visually-hidden label — present for screen readers / keyboard,
   removed from the visual masthead. */
/* THE PAGE CHROME SHOWS NO TITLE ON THIS THEME. A standard page's h1 chrome
   restates the record title above whatever heading the operator authored in the
   page body, so every page opened with two titles (owner decision, 2026-08-27:
   "the section title on each page is extraneous — just the page heading defined
   in the page content"). The title element STAYS in the markup for assistive
   technology, search engines and the browser reader view; only its paint goes.
   Scoped to article.ao-page, so blog posts and listings keep their titles even
   on this bundle. */
/* A SLIDE-DECK PAGE SHOWS NO SECTION RAIL. The deck already walks the whole
   section — every slide names and links its child — so the rail beside it
   restates the deck and competes with it for width (owner decision,
   2026-08-27). The child pages the slides link to keep the rail; this rule
   reads the deck rhythm off the page body, so it follows the template rather
   than any hand-kept page list. */
.ao-page-wrap:has(.ao-region--rhythm-slide-deck) > aside.ao-section-nav {
  display: none;
}

.ao-page-wrap.ao-has-aside:has(.ao-region--rhythm-slide-deck) {
  display: block;
}

/* AN INDEX PAGE SHOWS NO SECTION RAIL. The cards already name and link every
   child of the section, so the rail beside them lists the same children twice.
   The child pages the cards link to keep the rail; this rule reads the card off
   the page wrap, so it follows the template rather than any hand-kept page
   list. */
.ao-page-wrap:has(.ao-index-card) > aside.ao-section-nav {
  display: none;
}

/* The wrap is a two-column grid on any page that states an aside. A rule that
   hides the aside alone leaves the empty 12rem track beside the cards, so the
   wrap returns to a single column here.

   WIDTH IS NOT MOTION, so both rules above sit outside the reduced-motion
   branch and outside the view-timeline guard. Every reader takes the same
   layout. */
.ao-page-wrap.ao-has-aside:has(.ao-index-card) {
  display: block;
}

/* A DECK PAGE IS A STAGE, NOT A COLUMN OF BODY TEXT. Every rule below is keyed
   on the same page-level `:has()` the rail rule above uses, so the layout
   follows the template and needs no page list and no new key.

   WIDTH IS NOT MOTION, so every rule below sits outside the reduced-motion
   branch and outside the view-timeline guard. The pinned deck, the fallback snapping
   row, a reduced-motion reader and edit mode take the same layout. */
main.ao-content:has(.ao-region--rhythm-slide-deck) {
  /* The column releases its measure, in the manner of the split hero, so the
     deck inside it can reach the window edge. The page wrap is untouched, so
     every other region on the page keeps the reading measure. */
  max-width: none;
  /* The deck starts at the header's bottom rule. The column's own top padding
     would push the first slide below the fold. The inline padding stays, so
     the regions below the deck keep the page margin. */
  padding-block-start: 0;
}

/* The breadcrumb is page chrome, and on a deck page it stands between the
   header and the first slide. It is hidden in the manner of the section rail
   above. Chrome carries no block, so hiding it leaks nothing into the search
   index. */
main.ao-content:has(.ao-region--rhythm-slide-deck) nav.ao-breadcrumb {
  display: none;
}

/* THE DECK ALONE ESCAPES THE READING MEASURE. The margin pulls the region out
   to each window edge and the width fills what the margin opens, which is the
   full-bleed key's own escape. border-box holds the track's inline padding
   INSIDE the declared width, so the escape adds no horizontal scrollbar, and
   the `.ao-site` clip covers the scrollbar gutter that 100vw still counts.

   The body role caps a region at the reading measure, so the escape must
   raise that cap as well: a 100vw width under a 48rem cap moves the deck and
   never widens it. The cap follows the width, in the manner of the full-bleed
   key.

   The top margin goes with the escape: a gap above the pane would put the
   first slide below the fold again. */
main.ao-content:has(.ao-region--rhythm-slide-deck) .ao-region--rhythm-slide-deck {
  box-sizing: border-box;
  width: 100vw;
  max-width: 100vw;
  margin-inline: calc(50% - 50vw);
  margin-block-start: 0;
}

/* SLIDE TEXT SCALES WITH THE PANE, in the manner of the child title band: body
   size on a stage reads as a caption. The pane declares an inline-size
   container, so the query unit measures the stage rather than the window.

   The floor is the site base size, so a phone loses no legibility. The cap
   holds a full paragraph inside the 16:9 frame on a very wide screen: the
   frame is 56.25cqw tall, and text above this cap runs past its foot.

   One size covers both elements. An in-body heading already renders at body
   size and states its rank by weight, so a second size here would invent a
   type scale this theme does not have. */
main.ao-content:has(.ao-region--rhythm-slide-deck) .ao-region--rhythm-slide-deck p,
main.ao-content:has(.ao-region--rhythm-slide-deck) .ao-region--rhythm-slide-deck :is(h1, h2, h3, h4, h5, h6) {
  font-size: clamp(1rem, 2.4cqw, 1.75rem);
  /* SLIDE TEXT KEEPS A READING MEASURE ON A WIDE STAGE. A line that runs the
     whole window is too long to read at any size. The measure is stated in em,
     so it grows with the scaled size above and the line holds about the same
     count of characters at every width. It is em and not ch on purpose: a ch
     is a different width in the heading font than in the body font, so a ch
     measure gives the heading and the paragraph two boxes of different widths
     and their left edges drift apart. An em measure gives them one box, so
     the heading and the paragraph share a left edge. 36em is 60 characters of
     the body font. The auto margins split the space left over between the two
     sides, so the words stand in the middle of the stage and the space beside
     them grows with the window. Inside a narrower box, such as the text column
     of an image-and-text block, the measure exceeds the box and the text takes
     the box. */
  max-width: 36em;
  margin-inline: auto;
}

/* THE PAGE TITLE IS HIDDEN ON A DECK PAGE. A painted h1 above the deck pushes
   the first slide below the fold, which is the gap this layout closes. The
   title element STAYS in the markup for assistive technology, for search
   engines and for the browser reader view. Only its paint goes. The
   declarations are the accessible visually-hidden set this file already uses.

   THE DEFAULT BUNDLE ALREADY HIDES EVERY PAGE TITLE, so on that bundle this
   rule repeats a site-wide rule. The repeat is deliberate. The deck-page parity
   guard byte-compares every rule outside the motion guards that names the deck
   class, so both bundles must carry this rule word for word. */
main.ao-content:has(.ao-region--rhythm-slide-deck) .ao-page-title {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* A REGION PLACED BESIDE THE DECK DRAWS NOTHING. A template may place its
   chapters region in the side column, and the composed body then runs two
   columns. A rail beside a window-wide deck competes with the deck for the
   window, exactly as the section rail does. So a deck page hides the side
   region and runs the composed body as one column, and the deck and every
   region below it take the whole width.

   The rule reads the PLACEMENT, not the role, so it covers whatever an
   operator places beside a deck. In practice that is the chapters rail. The
   rail restates the deck's own slide links, so the reader loses no route. A
   hidden region's words still enter the search index, and these words are the
   deck's own words, so nothing new leaks. That is the reason the section rail
   rule gives as well. */
main.ao-content:has(.ao-region--rhythm-slide-deck) .ao-region--place-side {
  display: none;
}

main.ao-content:has(.ao-region--rhythm-slide-deck) .ao-page-body--composed {
  display: block;
}

/* A SLIDE IMAGE STANDS IN THE MIDDLE OF THE STAGE. The image cap in the deck
   tier keeps the aspect ratio, so a landscape photograph held to the cap is
   narrower than the slide, and the base image rule draws it as a block at its
   own width. Left of centre it reads as a column again. The auto margins
   centre it in its block, and in the media column of an image-and-text block.
   The rule sits outside the guards, so the fallback row centres its images
   as well. */
main.ao-content:has(.ao-region--rhythm-slide-deck) .ao-region--rhythm-slide-deck .ao-image-img {
  margin-inline: auto;
}

article.ao-page > .ao-page-title {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.ao-visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* The form carries its own vertical air, more below than above, because a
   header that holds the Site logo drops its padding and the form would
   otherwise sit on the header rule. */
.ao-header-search { display: flex; align-items: center; gap: 0.4rem; padding-block: 0.5rem 0.75rem; }

.ao-search-input {
  font-family: var(--ao-font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: 0.95rem;
  color: var(--ao-ink, #1f2937);
  background: var(--ao-surface, #ffffff);
  border: 1px solid var(--ao-rule, #e5e7eb);
  border-radius: 0.375rem;
  padding: 0.4rem 0.65rem;
}
.ao-search-input:focus-visible {
  outline: 2px solid var(--ao-accent, #2563eb);
  outline-offset: 1px;
  border-color: var(--ao-accent, #2563eb);
}

.ao-search-button {
  font-family: var(--ao-font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: 0.9rem;
  color: var(--ao-surface, #ffffff);
  background: var(--ao-accent, #2563eb);
  border: 1px solid var(--ao-accent, #2563eb);
  border-radius: 0.375rem;
  padding: 0.4rem 0.85rem;
  cursor: pointer;
}
.ao-search-button:hover { filter: brightness(0.95); }
.ao-search-button:focus-visible {
  outline: 2px solid var(--ao-accent, #2563eb);
  outline-offset: 2px;
}

.ao-search-page-form { margin: 0 0 1.5rem; }
.ao-search-page .ao-search-label { display: block; margin: 0 0 0.4rem; font-weight: 600; color: var(--ao-ink, #1f2937); }
.ao-search-field { display: flex; gap: 0.5rem; max-width: 32rem; }
.ao-search-field .ao-search-input { flex: 1 1 auto; }

.ao-search-suggestion { margin: 0 0 1.25rem; color: var(--ao-muted, #6b7280); font-size: 0.95rem; }
.ao-search-suggestion a { color: var(--ao-accent, #2563eb); }

.ao-search-results { display: flex; flex-direction: column; gap: 1.25rem; }
.ao-search-result-title { font-size: 1.05rem; margin: 0 0 0.2rem; }
.ao-search-result-title a { color: var(--ao-accent, #2563eb); text-decoration: none; }
.ao-search-result-title a:hover { text-decoration: underline; }
.ao-search-result-url { font-size: 0.8rem; color: var(--ao-muted, #6b7280); margin: 0 0 0.3rem; }
.ao-search-excerpt { margin: 0; color: var(--ao-ink, #1f2937); }
.ao-search-excerpt mark, .ao-search-hl {
  background: var(--ao-accent, #2563eb);
  color: var(--ao-surface, #ffffff);
  padding: 0 0.1em;
  border-radius: 0.15rem;
}
.ao-search-empty, .ao-search-noscript { color: var(--ao-muted, #6b7280); }

/* Self-hosted curated webfaces (spec 2026-07-28-dark-design-tokens).
   Composed by ONE helper for every surface. On a SITE's stylesheet these rules
   appear only because a saved `font` token references a curated webfont key — a
   site on system stacks composes none of this. The admin style drawer serves the
   full curated set instead, so its specimen can render a face the operator has
   picked and not yet saved. */
@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("../fonts/space-grotesk-latin-wght-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Literata";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("../fonts/literata-latin-standard-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Literata";
  font-style: italic;
  font-weight: 200 900;
  font-display: swap;
  src: url("../fonts/literata-latin-standard-italic.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Per-site style-token override (spec 2026-07-03-inline-stylesheet-editor). */
:root {
  --ao-ink: #adb3ab;
  --ao-muted: #959d94;
  --ao-accent: #70d766;
  --ao-surface: #101310;
  --ao-rule: #2f362e;
  --ao-ink-strong: #f2f5f1;
  --ao-ink-faint: #81887f;
  --ao-surface-raised: #191c19;
  --ao-ink-raised: #adb3ab;
  --ao-ink-on-accent: #ffffff;
  --ao-font-body: "Literata", Georgia, Cambria, "Times New Roman", Times, serif;
  --ao-font-heading: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --ao-font-display: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --ao-display-scale: 3.5;
  --ao-display-tracking: -0.02em;
}

/* Authored flourishes for this site. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    @supports (animation-timeline: view()) and (animation-range: contain 0dvh) {
      .ao-region--rhythm-slide-deck.ao-region--flourish-custom-tide > .ao-region-pin > .ao-region-track {
        position: relative;
      }

      .ao-region--rhythm-slide-deck.ao-region--flourish-custom-tide > .ao-region-pin > .ao-region-track::before {
        content: "";
        position: absolute;
        inset-block: 0;
        inset-inline-start: 0;
        inline-size: 100%;
        pointer-events: none;
        opacity: 0;
        animation-duration: auto;
        animation-timeline: --ao-region-view;
        animation-fill-mode: both;
        z-index: 1;
        background: repeating-linear-gradient(to bottom, transparent 0px, transparent 2px, var(--ao-accent, #70d766) 2px, var(--ao-accent, #70d766) 3px);
        inline-size: 100%;
        block-size: 100%;
      }

      .ao-region--rhythm-slide-deck.ao-region--flourish-custom-tide:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 2))) > .ao-region-pin > .ao-region-track::before {
        animation-name: ao-flourish-custom-tide-track-1;
        animation-timing-function: linear;
      }

      .ao-region--rhythm-slide-deck.ao-region--flourish-custom-tide > .ao-region-pin > .ao-region-track::after {
        content: "";
        position: absolute;
        inset-block: 0;
        inset-inline-start: 0;
        inline-size: 100%;
        pointer-events: none;
        opacity: 0;
        animation-duration: auto;
        animation-timeline: --ao-region-view;
        animation-fill-mode: both;
        z-index: 1;
        background: var(--ao-accent, #70d766);
        inset-block-start: 0;
        block-size: 100%;
        inline-size: 1px;
      }

      .ao-region--rhythm-slide-deck.ao-region--flourish-custom-tide:where(:has(> .ao-region-pin > .ao-region-track > :nth-child(n + 2))) > .ao-region-pin > .ao-region-track::after {
        animation-name: ao-flourish-custom-tide-track-2;
        animation-timing-function: linear;
      }

      @keyframes ao-flourish-custom-tide-track-1 {
        0% { opacity: 0; }
        8% { opacity: 0.28; }
        13% { opacity: 0.07; }
        19% { opacity: 0.32; }
        25% { opacity: 0.1; }
        32% { opacity: 0.24; }
        40% { opacity: 0; }
        100% { opacity: 0; }
      }

      @keyframes ao-flourish-custom-tide-track-2 {
        0% { opacity: 0; transform: translateX(-1cqw); }
        10% { opacity: 0.9; transform: translateX(12cqw); }
        30% { opacity: 0.9; transform: translateX(85cqw); }
        40% { opacity: 0; transform: translateX(100cqw); }
        100% { opacity: 0; }
      }

    }
  }
}
