/* theme: harbor v1.5.0 */
:root {
  --color-primary: #2563eb;
  --color-primary-foreground: #ffffff;
  --color-secondary: #eef1f7;
  --color-secondary-foreground: #11152a;
  --color-accent: #fbbf24;
  --color-accent-foreground: #11152a;
  --color-destructive: #dc2626;
  --color-destructive-foreground: #ffffff;
  --color-success: #16a34a;
  --color-warning: #d97706;
  --color-background: #f7f8fb;
  --color-foreground: #11152a;
  --color-card: #ffffff;
  --color-card-foreground: #11152a;
  --color-muted: #5f6a82;
  --color-border: #dde2ec;
  --color-foreground-dark: #f7f8fb;
  --color-muted-dark: #c2c9d8;
  --color-border-dark: #2b3656;
  --color-card-dark: #1b2440;
  --color-card-foreground-dark: #f7f8fb;
  --font-sans: 'Inter', 'Inter Fallback', ui-sans-serif, system-ui, sans-serif;
  --font-display: 'Space Grotesk', 'Inter', ui-sans-serif, sans-serif;
  --font-serif: ui-serif, Georgia, serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  --text-base: 1rem;
  --font-weight-heading: 600;
  --leading-body: 1.6;
  --leading-heading: 1.05;
  --tracking-heading: -0.025em;
  --text-h1: clamp(2.5rem, 5vw, 3.5rem);
  --text-h2: clamp(1.875rem, 3.75vw, 3rem);
  --text-h3: 1.75rem;
  --text-h4: 1.25rem;
  --text-h5: 1.125rem;
  --text-h6: 1rem;
  --shadow-sm: 0 1px 2px rgba(17,21,42,0.06);
  --shadow-md: 0 6px 16px -4px rgba(17,21,42,0.12);
  --shadow-lg: 0 16px 32px -8px rgba(17,21,42,0.16);
  --radius-sm: 0.375rem;
  --radius-md: 0.625rem;
  --radius-lg: 1.25rem;
  --radius-full: 9999px;
  --border-width: 1px;
  --motion-fast: 150ms;
  --motion-normal: 300ms;
  --motion-slow: 600ms;
  --motion-ease: cubic-bezier(0.4, 0, 0.2, 1);
  --motion-distance: 1rem;
  --space-xs: 0.5rem;
  --space-sm: 1rem;
  --space-md: 1.5rem;
  --space-lg: 2rem;
  --space-xl: 3rem;
  --space-2xl: 4rem;
  --space-3xl: 5rem;
  --space-4xl: 6rem;
  --space-5xl: 8rem;
  --block-space-heading: var(--space-xs);
  --block-space-text: var(--space-sm);
  --block-space-richtext: var(--space-sm);
  --block-space-card: var(--space-lg);
  --block-space-grid: var(--space-lg);
  --block-space-image: var(--space-md);
  --block-space-table: var(--space-md);
  --block-space-embed: var(--space-md);
  --block-space-carousel: var(--space-md);
  --block-space-callout: var(--space-md);
  --block-space-accordion: var(--space-md);
  --block-space-tabs: var(--space-md);
  --block-space-scrollarea: var(--space-md);
  --block-space-video: var(--space-md);
  --block-space-catalogue: var(--space-md);
  --block-space-form: var(--space-md);
  --block-space-button: var(--space-xs);
  --block-space-flex: 0;
  --block-space-gradient: 0;
  --block-space-border: 0;
  --block-space-separator: 0;
}

/* theme-block-rules.css */
/*
 * theme-block-rules.css
 *
 * Static, repo-managed selectors that map data-cms-block markers onto
 * theme tokens (CSS custom properties resolved via :root in the
 * compiled theme CSS).
 *
 * This file is concatenated into every compiled theme's CSS, so all
 * themes get the same rule shape — they only differ in the variable
 * values they ship. Blocks stay dumb (they emit data-cms-block); the
 * theme system owns the visual mapping.
 *
 * Adding a new selector here is part of the BLOCK / RENDERER contract,
 * not a theme concern. Theme authors override the variables.
 */

/* ---- Heading ---- */
/* Tailwind 4 ships no default heading sizes — without these every
 * heading collapses to body size (16px). The scale is a sane
 * baseline that authors / themes can override per token later.
 *
 * `color: inherit` so headings inside a dark Container (e.g. the hero
 * with background-color set inline via styleHints) flip to the
 * Container's text colour automatically. The body rule below seeds
 * the page-level default. */
[data-cms-block="heading"] {
    color: inherit;
    /* Headings use the theme's display font when it defines one, else the
     * body font. Themes set --font-display via a 'font-display' token. */
    font-family: var(--font-display, var(--font-sans));
    /* Weight, line-height and tracking are theme tokens so a theme can feel
     * typographically distinct, not just a different font. */
    font-weight: var(--font-weight-heading, 700);
    line-height: var(--leading-heading, 1.15);
    letter-spacing: var(--tracking-heading, -0.02em);
    /* Heading rhythm: bottom margin from the theme's per-block spacing token
     * (Themes → Block spacing), falling back to a tight default. See the
     * "Per-block bottom spacing" section below for the whole system. */
    margin: 0 0 var(--block-space-heading, 0.3rem);
}
/* Headings balance their line lengths. A greedy wrap fills line one to the
 * measure and leaves a stub on line two ("…& IT support — built and" / "fixed
 * in Bath"); balance splits the same words evenly, which is where a designer
 * would put the break and what bathit does on every h1/h2. Behind a token so a
 * theme can opt out with --heading-wrap: wrap. Ignored by browsers that don't
 * support it, and only ever affects a heading that wraps at all. */
[data-cms-block="heading"] { text-wrap: var(--heading-wrap, balance); }

/* Per-level sizes are tokens (--text-h1..h6); the clamp() fallbacks are the
 * Default theme's responsive scale for when no theme stylesheet is loaded. */
h1[data-cms-block="heading"] { font-size: var(--text-h1, clamp(2rem, 4.5vw, 3.75rem)); }
h2[data-cms-block="heading"] { font-size: var(--text-h2, clamp(1.5rem, 3vw, 2.25rem)); }
h3[data-cms-block="heading"] { font-size: var(--text-h3, 1.5rem); }
h4[data-cms-block="heading"] { font-size: var(--text-h4, 1.25rem); }
h5[data-cms-block="heading"] { font-size: var(--text-h5, 1.125rem); }
h6[data-cms-block="heading"] { font-size: var(--text-h6, 1rem); }

/* Body / text colour comes from the theme, but the public page's
 * <body class="bg-white text-gray-900"> overrides it. Force the
 * theme's surface so the active theme actually paints the page. */
body {
    background-color: var(--color-background);
    color: var(--color-foreground);
    font-family: var(--font-sans);
    line-height: var(--leading-body, 1.6);
}

/* ---- Rich text / Text ---- */
/* Inherit so a parent Container's text colour (via styleHints) wins.
 * The body baseline below provides the page default. */
[data-cms-block="richtext"],
[data-cms-block="text"] {
    color: inherit;
    font-family: var(--font-sans);
}

/* ---- Per-block bottom spacing (theme-controlled vertical rhythm) ----
 * Every stacked block gets a bottom margin from its theme spacing token
 * (Themes → Block spacing → --block-space-<type>, a step on the --space-*
 * scale). It's a stylesheet rule driven by a theme var — never a block prop —
 * so the editor can't change it per instance and every page stays consistent.
 * Bottom-only keeps spacing predictable in block flow, flex and grid alike (no
 * leading gap, no margin-collapse surprises). The fallback after each var is
 * the Default theme's value, so themes synced before this token shipped (and
 * imported themes that omit it) still get sensible spacing. */
[data-cms-block="text"] { margin: 0 0 var(--block-space-text, 1rem); }
/* Rich text has its own token, so it falls back to Text's value rather than
 * sharing it — otherwise the "Rich text" spacing control would be inert. */
[data-cms-block="richtext"] { margin: 0 0 var(--block-space-richtext, var(--block-space-text, 1rem)); }
[data-cms-block="card"] { margin-bottom: var(--block-space-card, 2rem); }
[data-cms-block="grid"] { margin-bottom: var(--block-space-grid, 2rem); }
[data-cms-block="flex"] { margin-bottom: var(--block-space-flex, 0); }
[data-cms-block="image"] { margin-bottom: var(--block-space-image, 1.5rem); }
[data-cms-block="table"] { margin-bottom: var(--block-space-table, 1.5rem); }
[data-cms-block="embed"] { margin-bottom: var(--block-space-embed, 1.5rem); }
[data-cms-block="carousel"] { margin-bottom: var(--block-space-carousel, 1.5rem); }
[data-cms-block="callout"] { margin-bottom: var(--block-space-callout, 1.5rem); }
[data-cms-block="accordion"] { margin-bottom: var(--block-space-accordion, 1.5rem); }
[data-cms-block="tabs"] { margin-bottom: var(--block-space-tabs, 1.5rem); }
[data-cms-block="scrollarea"] { margin-bottom: var(--block-space-scrollarea, 1.5rem); }
[data-cms-block="video"] { margin-bottom: var(--block-space-video, 1.5rem); }
[data-cms-block="catalogue"] { margin-bottom: var(--block-space-catalogue, 1.5rem); }
[data-cms-block="form"] { margin-bottom: var(--block-space-form, 1.5rem); }
[data-cms-block="gradient"] { margin-bottom: var(--block-space-gradient, 0); }
[data-cms-block="border"] { margin-bottom: var(--block-space-border, 0); }
[data-cms-block="button"] { margin-bottom: var(--block-space-button, 0.5rem); }
[data-cms-block="separator"] { margin-bottom: var(--block-space-separator, 0); }

/* ---- The eyebrow's status dot ----
 * "● BATH · BRISTOL · SOUTH WEST · EST. 1996" — a small filled circle before a
 * label, on a large share of marketing heroes. It belongs to the eyebrow rather
 * than being a block of its own: a dot with no label means nothing, and the
 * eyebrow idiom (small + uppercase + tracked + accent) already lives on Text.
 *
 * Keyed on the attribute alone, not on the block type: a BREADCRUMB wears the
 * same dot, and its first crumb is a Link rather than a Text.
 *
 * A ::before rather than a child element because the canvas makes a paragraph
 * contentEditable and assigns el.textContent to keep it in step —
 * which would delete a child node on the next keystroke. A pseudo-element
 * cannot be typed over and needs no DOM.
 *
 * Sized in em so it tracks the type size, and filled with currentColor so it
 * follows the label's colour unless --marker-color says otherwise. */
[data-marker="dot"]::before {
    content: '';
    display: inline-block;
    width: 0.66em;
    height: 0.66em;
    margin-right: 0.66em;
    border-radius: 9999px;
    background: var(--marker-color, currentColor);
    /* Sits on the BASELINE, not `middle`. `vertical-align: middle` centres on
     * the x-height, and an eyebrow is uppercase — so the dot came out visibly
     * low against the caps. Measured against the source (which centres it with
     * flex): baseline puts the dot's centre 0.55px above the label's, where the
     * source has 0.50px. It also scales, because both the dot and the cap height
     * are in em. */
    vertical-align: baseline;
    /* The dot is decoration; keep it out of the accessible name. */
    speak: never;
}

/* The character BETWEEN crumbs — "HOME / WEB SERVICES". On the source it is a
 * ::after on the <li>; pseudo-element content is not in the DOM but IS readable
 * from the computed style, so the importer carries it across rather than losing
 * it. Drawn here for the same reason as the dot: no extra block, nothing an
 * author can accidentally type over, and it cannot end up in the link's
 * accessible name. Muted, because it is punctuation. */
[data-cms-block="link"][data-separator]::after {
    content: attr(data-separator);
    margin-inline: 0.5em;
    opacity: 0.45;
    text-decoration: none;
    display: inline-block;
}

/* ---- No trailing margin on the LAST block in a stack ----
 * Every rule above gives a block a BOTTOM margin, and nothing took it back off
 * the last one — so each container, grid cell and Section ended with a phantom
 * gap the design never asked for, and because containers nest, the gaps stacked:
 * a Section closing with a Container closing with a Text carried three of them.
 * This is why imported pages came out consistently taller and looser than their
 * source, and why per-block spacing felt like it "did too much".
 *
 * Bottom-margin-only rhythm is the right model; the last-child reset is the
 * other half of it, and is what every prose system does for the same reason.
 *
 * Two selectors, public and canvas, so the editor keeps matching the page:
 *  - public: blocks are direct children of their container (the data-cms-space
 *    wrapper only appears when an author sets an override), so :last-child on
 *    the block is exactly right. The :not() guard keeps it from firing on the
 *    canvas, where a block is always the only child of its craft wrapper and it
 *    would otherwise zero EVERY margin.
 *  - canvas: the wrapper is the child that can be last, so test that instead.
 *
 * Both are wrapped in :where() to hold specificity at or below the
 * `[data-cms-space="…"]` overrides below, which must still win. */
:where(:not([data-craft-block])) > [data-cms-block]:last-child { margin-bottom: 0; }
:where([data-craft-block]:last-child) > [data-cms-block] { margin-bottom: 0; }

/* A button is an INLINE box, so a run of them flows and WRAPS — and with only a
 * bottom margin the buttons in a row butt up against each other edge-to-edge
 * (nothing separates two adjacent inline boxes). Give them the same gap
 * horizontally as they already have vertically, so a row reads as buttons
 * rather than one segmented bar.
 *
 * Gated on `data-inline` (ButtonRenderer.buttonIsInline), NOT applied to every
 * button: a full-width button is `width: 100%`, so a margin-right would push it
 * past its container, and a centre/right-aligned one places itself with auto
 * margins that this would fight. Those two render as blocks and own their line,
 * so they need no horizontal gap at all. */
[data-cms-block="button"][data-inline="true"] { margin-right: var(--block-space-button, 0.5rem); }

/* Inside a gap-managed container (Grid cell, Card inner, Flex), the container's
 * own `gap` is the only spacing — drop every block's bottom margin so nothing
 * stacks on top of the gap. Descendant (not child) selector so it works through
 * the editor's per-block wrapper divs too, keeping canvas and public in sync. */
[data-cms-flow="gap"] [data-cms-block] {
    margin-bottom: 0;
    /* The button's inline horizontal gap above, for the same reason — a Flex
     * row already spaces its children, so keeping it would double the gap. */
    margin-right: 0;
}

/* Per-instance bottom spacing: the inspector's "Bottom spacing" select can
 * override the theme default for one block (to fix a layout) — "Theme default"
 * keeps the theme value, "None" removes it, or pick any step from the scale.
 * Stored as data-cms-space on the block's immediate parent in both paths
 * (public: a display:contents wrapper; editor: the CraftBlock wrapper), so a
 * child combinator targets exactly that block, not its descendants. Listed
 * AFTER the gap-reset so an explicit choice wins even inside a Card/Flex;
 * (0,2,0) also beats the single-attribute per-block default rules above. */
[data-cms-space="none"] > [data-cms-block] { margin-bottom: 0; }
[data-cms-space="xs"]   > [data-cms-block] { margin-bottom: var(--space-xs, 0.5rem); }
[data-cms-space="sm"]   > [data-cms-block] { margin-bottom: var(--space-sm, 1rem); }
[data-cms-space="md"]   > [data-cms-block] { margin-bottom: var(--space-md, 1.5rem); }
[data-cms-space="lg"]   > [data-cms-block] { margin-bottom: var(--space-lg, 2rem); }
[data-cms-space="xl"]   > [data-cms-block] { margin-bottom: var(--space-xl, 3rem); }
[data-cms-space="2xl"]  > [data-cms-block] { margin-bottom: var(--space-2xl, 4rem); }
[data-cms-space="3xl"]  > [data-cms-block] { margin-bottom: var(--space-3xl, 5rem); }

/* ---- Text blocks as flex items ---- */
/* A text block dropped into a Flex row was collapsing to its longest word —
 * an empty Rich Text shrank to ~1 character wide, so you couldn't even click
 * into it. Make text-bearing blocks fill (and shrink/wrap within) the flex
 * track instead. Scoped to a Flex parent's DIRECT children so it can't touch
 * Cards/grids/Sections (other markers) and is inert in a content-height flex
 * column (no free space to grow into). Two selector families: the public
 * render's block markers, and the editor's per-block wrapper which carries
 * data-craft-block — so canvas and public stay in sync. */
[data-cms-block="flex"] > [data-cms-block="richtext"],
[data-cms-block="flex"] > [data-cms-block="heading"],
[data-cms-block="flex"] > [data-cms-block="text"],
[data-cms-block="flex"] > [data-craft-block="richtext"],
[data-cms-block="flex"] > [data-craft-block="heading"],
[data-cms-block="flex"] > [data-craft-block="text"] {
    flex: 1 1 auto;
    min-width: 0;
}

/* ---- Link ---- */
[data-cms-block="link"] {
    color: var(--color-primary);
}

[data-cms-block="link"]:hover {
    text-decoration: underline;
}

/* ---- Button ---- */
[data-cms-block="button"] {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    border-radius: var(--radius-md);
    padding: 0.5rem 1rem;
    font-family: var(--font-sans);
    font-weight: 500;
    transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}

[data-cms-block="button"][data-variant="primary"] {
    background-color: var(--color-primary);
    color: var(--color-primary-foreground);
}

[data-cms-block="button"][data-variant="secondary"] {
    background-color: var(--color-secondary);
    color: var(--color-secondary-foreground);
}

[data-cms-block="button"][data-variant="accent"] {
    background-color: var(--color-accent);
    color: var(--color-accent-foreground);
}

[data-cms-block="button"][data-variant="outline"] {
    border: 1px solid var(--color-border);
    color: var(--color-foreground);
    background-color: transparent;
}

[data-cms-block="button"][data-variant="ghost"] {
    color: var(--color-foreground);
    background-color: transparent;
}

/* ---- Card ---- */
[data-cms-block="card"] {
    background-color: var(--color-card, var(--color-background));
    color: var(--color-card-foreground, var(--color-foreground));
    border: var(--border-width, 1px) solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}

/* ---- Badge ---- */
[data-cms-block="badge"] {
    display: inline-flex;
    align-items: center;
    border-radius: var(--radius-full);
    padding: 0.125rem 0.5rem;
    font-size: 0.75rem;
    font-weight: 500;
    background-color: var(--color-secondary);
    color: var(--color-secondary-foreground);
}

/* ---- Callout ---- */
[data-cms-block="callout"] {
    border-radius: var(--radius-md);
    border-left: 4px solid var(--color-primary);
    padding: 1rem;
    background-color: color-mix(in oklab, var(--color-primary) 8%, var(--color-background));
    color: var(--color-foreground);
}

/* ---- Separator / Border ---- */
[data-cms-block="separator"],
[data-cms-block="border"] {
    border-color: var(--color-border);
}

/* ---- Container / Flex / Grid / Spacer / ScrollArea / Accordion / Tabs ---- */
/* Layout primitives are mostly Tailwind-driven; we don't restyle them
 * here, but we still emit the marker so plugins / themes can target
 * them by variant via:
 *   [data-cms-block="flex"][data-variant="hero"] { ... }
 */


/* motion.css */
/* ============================================================================
 * Motion — the animation engine.
 *
 * Every rule here targets `[data-anim-*] > *`, i.e. the CHILD of the marker
 * element, never the marker itself. That is the whole trick: the marker is the
 * `display: contents` wrapper the renderer already emits for `bottomSpacing`,
 * so it generates NO BOX and cannot affect layout — while :hover still works,
 * because the hover is hit-tested against the child, which does have a box.
 *
 * An "Animation block" you drop things into would instead put a real box in the
 * tree; inside a Flex or Grid that box becomes a flex/grid item and breaks the
 * container's gap flow, the last-child margin reset, and Card's media bleed.
 *
 * Only transform / opacity / filter / box-shadow are animated. Nothing here can
 * reflow the page, so motion can never reintroduce a spacing bug.
 * ==========================================================================*/

/* Speed and easing come from the theme (--motion-*, see ThemeTokens), so one
 * theme setting restyles every animation on the site. The literals are the
 * Default theme's values, for a theme synced before these tokens shipped. */
[data-anim-hover],
[data-anim-reveal],
[data-anim-loop] {
    --anim-duration: var(--motion-normal, 300ms);
    --anim-ease: var(--motion-ease, cubic-bezier(0.4, 0, 0.2, 1));
    --anim-delay: 0ms;
    /* Strength, as a multiplier on the theme's travel distance. */
    --anim-scale: 1;
}

[data-anim-speed='fast']   { --anim-duration: var(--motion-fast, 150ms); }
[data-anim-speed='normal'] { --anim-duration: var(--motion-normal, 300ms); }
[data-anim-speed='slow']   { --anim-duration: var(--motion-slow, 600ms); }

[data-anim-strength='subtle'] { --anim-scale: 0.5; }
[data-anim-strength='medium'] { --anim-scale: 1; }
[data-anim-strength='strong'] { --anim-scale: 1.75; }

[data-anim-delay='short']  { --anim-delay: 80ms; }
[data-anim-delay='medium'] { --anim-delay: 160ms; }
[data-anim-delay='long']   { --anim-delay: 320ms; }

/* The distance a reveal travels, and how far a hover lifts. */
[data-anim-hover] > *,
[data-anim-reveal] > * {
    --anim-travel: calc(var(--motion-distance, 1rem) * var(--anim-scale));
}

/* ---- Hover / focus -------------------------------------------------------
 * Pure CSS, no JS. `:focus-visible` as well as `:hover` so the effect is
 * reachable from the keyboard — a hover-only affordance is invisible to anyone
 * not using a mouse. */
[data-anim-hover] > * {
    transition:
        transform var(--anim-duration) var(--anim-ease),
        box-shadow var(--anim-duration) var(--anim-ease),
        filter var(--anim-duration) var(--anim-ease);
}

/* `!important` on the shadow, and only here. Card, Section and Grid set
 * box-shadow as an INLINE style, which beats any stylesheet rule no matter how
 * specific — so a lift on a Card raised it without ever casting a shadow, which
 * is most of what makes a lift read as one. The transform needed no such help
 * because nothing sets that inline. Narrow by design: hover/focus state only,
 * one property, on a block whose author explicitly asked for the effect. */
[data-anim-hover='lift'] > *:hover,
[data-anim-hover='lift'] > *:focus-visible {
    transform: translateY(calc(var(--anim-travel) * -0.25));
    box-shadow: var(--shadow-lg, 0 10px 25px rgb(0 0 0 / 0.12)) !important;
}

[data-anim-hover='sink'] > *:hover,
[data-anim-hover='sink'] > *:focus-visible {
    transform: translateY(calc(var(--anim-travel) * 0.15));
}

[data-anim-hover='grow'] > *:hover,
[data-anim-hover='grow'] > *:focus-visible {
    transform: scale(calc(1 + 0.02 * var(--anim-scale)));
}

[data-anim-hover='glow'] > *:hover,
[data-anim-hover='glow'] > *:focus-visible {
    box-shadow: 0 0 calc(var(--anim-travel) * 1.5) var(--color-primary, #3b82f6) !important;
}

/* A directional nudge — the arrow on a "Visit site →" card. */
[data-anim-hover='nudge'] > *:hover,
[data-anim-hover='nudge'] > *:focus-visible {
    transform: translateX(calc(var(--anim-travel) * 0.25));
}

[data-anim-hover='tilt'] > *:hover,
[data-anim-hover='tilt'] > *:focus-visible {
    transform: rotate(calc(0.6deg * var(--anim-scale))) scale(calc(1 + 0.01 * var(--anim-scale)));
}

/* ---- Reveal on scroll ----------------------------------------------------
 * Content is VISIBLE by default. The hiding rules are gated on
 * [data-motion-ready], which the reveal hook sets on the document root only
 * once an IntersectionObserver is actually running — so if JS never runs, or
 * the observer is unsupported, or the effect throws, the page reads normally
 * instead of being blank. Getting this backwards is the classic
 * scroll-animation bug, and an invisible-content bug is the exact failure mode
 * that cost this project the most time.
 *
 * Elements already in the viewport are marked `in` in the same effect, before
 * paint, so there is no flash — only content below the fold is ever hidden. */
[data-motion-ready] [data-anim-reveal]:not([data-anim-in]) > * {
    opacity: 0;
    transition:
        opacity var(--anim-duration) var(--anim-ease) var(--anim-delay),
        transform var(--anim-duration) var(--anim-ease) var(--anim-delay);
}

[data-motion-ready] [data-anim-reveal='fade-up']:not([data-anim-in]) > *    { transform: translateY(var(--anim-travel)); }
[data-motion-ready] [data-anim-reveal='fade-down']:not([data-anim-in]) > *  { transform: translateY(calc(var(--anim-travel) * -1)); }
[data-motion-ready] [data-anim-reveal='fade-left']:not([data-anim-in]) > *  { transform: translateX(var(--anim-travel)); }
[data-motion-ready] [data-anim-reveal='fade-right']:not([data-anim-in]) > * { transform: translateX(calc(var(--anim-travel) * -1)); }
[data-motion-ready] [data-anim-reveal='scale-in']:not([data-anim-in]) > *   { transform: scale(calc(1 - 0.06 * var(--anim-scale))); }

[data-anim-reveal][data-anim-in] > * {
    opacity: 1;
    transform: none;
    transition:
        opacity var(--anim-duration) var(--anim-ease) var(--anim-delay),
        transform var(--anim-duration) var(--anim-ease) var(--anim-delay);
}

/* STAGGER — reveal a container's children in sequence instead of the container
 * as one lump. This is the setting that makes a grid of cards feel designed.
 * Capped at 12 so a long list can't end up with a multi-second tail. */
[data-anim-stagger][data-anim-in] > * > * { animation: cms-anim-stagger var(--anim-duration) var(--anim-ease) both; }
[data-anim-stagger][data-anim-in] > * > *:nth-child(1)  { animation-delay: calc(var(--anim-delay) + 0ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(2)  { animation-delay: calc(var(--anim-delay) + 60ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(3)  { animation-delay: calc(var(--anim-delay) + 120ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(4)  { animation-delay: calc(var(--anim-delay) + 180ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(5)  { animation-delay: calc(var(--anim-delay) + 240ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(6)  { animation-delay: calc(var(--anim-delay) + 300ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(7)  { animation-delay: calc(var(--anim-delay) + 360ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(8)  { animation-delay: calc(var(--anim-delay) + 420ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(9)  { animation-delay: calc(var(--anim-delay) + 480ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(10) { animation-delay: calc(var(--anim-delay) + 540ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(11) { animation-delay: calc(var(--anim-delay) + 600ms); }
[data-anim-stagger][data-anim-in] > * > *:nth-child(n + 12) { animation-delay: calc(var(--anim-delay) + 660ms); }

@keyframes cms-anim-stagger {
    from { opacity: 0; transform: translateY(var(--anim-travel)); }
    to   { opacity: 1; transform: none; }
}

/* ---- Ambient loops ------------------------------------------------------- */
[data-anim-loop='float'] > * { animation: cms-anim-float calc(var(--anim-duration) * 10) var(--anim-ease) var(--anim-delay) infinite; }
[data-anim-loop='pulse'] > * { animation: cms-anim-pulse calc(var(--anim-duration) * 6) var(--anim-ease) var(--anim-delay) infinite; }
[data-anim-loop='spin'] > *  { animation: cms-anim-spin calc(var(--anim-duration) * 8) linear var(--anim-delay) infinite; }

@keyframes cms-anim-float {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(calc(var(--motion-distance, 1rem) * var(--anim-scale) * -0.4)); }
}

@keyframes cms-anim-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: calc(1 - 0.3 * var(--anim-scale)); }
}

@keyframes cms-anim-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

/* ---- Reduced motion ------------------------------------------------------
 * A hard global off-switch. Anyone who has asked their OS for less movement
 * gets a static page — including the reveal rules, so nothing is left hidden
 * because the transition it was waiting on never ran. Not negotiable, and not
 * something an author can override per block. */
@media (prefers-reduced-motion: reduce) {
    [data-anim-hover] > *,
    [data-anim-reveal] > *,
    [data-anim-loop] > *,
    [data-anim-stagger] > * > * {
        transition: none !important;
        animation: none !important;
        transform: none !important;
        opacity: 1 !important;
    }
}

