Skip to content

Scrolling logo marquee

A seamless loop built from a duplicated, aria-hidden list.

Marketingintermediatelogosmarqueeanimationloop

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import { Container } from '@/components/ui/layout'
import { companyLogos } from '@/content/demo'

/**
 * Scrolling logo marquee
 *
 * The list is duplicated and translated by exactly −50%, which is what makes
 * the loop seamless. The duplicate is `aria-hidden`, so the names are
 * announced once rather than twice, and the whole animation stops under
 * `prefers-reduced-motion`.
 */
export default function MarqueeLogos() {
  return (
    <section className="overflow-hidden border-b border-line bg-surface-sunken py-14">
      <Container>
        <p className="label-caps text-center text-ink-subtle">
          Trusted by fictional teams everywhere
        </p>
      </Container>

      <div className="relative mt-8 flex overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]">
        <ul className="animate-marquee flex shrink-0 items-center gap-14 pr-14">
          {companyLogos.map((logo) => (
            <li
              key={logo}
              className="display-type text-lg font-medium whitespace-nowrap text-ink-subtle"
            >
              {logo}
            </li>
          ))}
        </ul>
        <ul className="animate-marquee flex shrink-0 items-center gap-14 pr-14" aria-hidden="true">
          {companyLogos.map((logo) => (
            <li
              key={`${logo}-duplicate`}
              className="display-type text-lg font-medium whitespace-nowrap text-ink-subtle"
            >
              {logo}
            </li>
          ))}
        </ul>
      </div>
    </section>
  )
}

components/blocks/sections/logos/marquee.tsx

import { Container } from '@/components/ui/layout'
import { companyLogos } from '@/content/demo'

/**
 * Scrolling logo marquee
 *
 * The list is duplicated and translated by exactly −50%, which is what makes
 * the loop seamless. The duplicate is `aria-hidden`, so the names are
 * announced once rather than twice, and the whole animation stops under
 * `prefers-reduced-motion`.
 */
export default function MarqueeLogos() {
  return (
    <section className="overflow-hidden border-b border-line bg-surface-sunken py-14">
      <Container>
        <p className="label-caps text-center text-ink-subtle">
          Trusted by fictional teams everywhere
        </p>
      </Container>

      <div className="relative mt-8 flex overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]">
        <ul className="animate-marquee flex shrink-0 items-center gap-14 pr-14">
          {companyLogos.map((logo) => (
            <li
              key={logo}
              className="display-type text-lg font-medium whitespace-nowrap text-ink-subtle"
            >
              {logo}
            </li>
          ))}
        </ul>
        <ul className="animate-marquee flex shrink-0 items-center gap-14 pr-14" aria-hidden="true">
          {companyLogos.map((logo) => (
            <li
              key={`${logo}-duplicate`}
              className="display-type text-lg font-medium whitespace-nowrap text-ink-subtle"
            >
              {logo}
            </li>
          ))}
        </ul>
      </div>
    </section>
  )
}

app/globals.css

@import 'tailwindcss';

/* ==========================================================================
   FOUNDRY DESIGN TOKENS
   --------------------------------------------------------------------------
   Every visual decision in Foundry resolves to a CSS custom property declared
   in this file. Three orthogonal axes compose the final look:

     1. scheme   -> [data-theme="light" | "dark"]      (surface + ink)
     2. palette  -> [data-palette="editorial" | ...]   (accent, type, shape)
     3. density  -> [data-density="compact" | ...]     (rhythm, control size)

   Selectors are attribute-based rather than :root-only so any subtree — a
   component preview frame, a starter shell — can opt into its own theme.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. SCHEME — light (default)
   -------------------------------------------------------------------------- */
:root,
[data-theme='light'] {
  color-scheme: light;

  /* Surfaces: warm off-white paper stock rising to pure white cards. */
  --color-canvas: #faf8f3;
  --color-surface: #ffffff;
  --color-surface-raised: #ffffff;
  --color-surface-sunken: #f2efe7;
  --color-surface-inverse: #17171a;
  --color-code-surface: #16161a;
  --color-code-gutter: #1e1e24;

  /* Ink: graphite rather than pure black. */
  --color-ink: #17171a;
  --color-ink-strong: #000000;
  --color-ink-muted: #63615c;
  --color-ink-subtle: #8c8981;
  --color-ink-inverse: #f7f6f2;
  --color-code-ink: #e6e6ec;

  /* Lines: restrained, warm. */
  --color-border: #e4dfd4;
  --color-border-strong: #cfc9bb;
  --color-border-subtle: #eee9df;

  /* Accent: electric indigo. */
  --color-accent: #3b41d8;
  --color-accent-hover: #3036c2;
  --color-accent-active: #262ba6;
  --color-accent-ink: #ffffff;
  --color-accent-soft: #ecedfd;
  --color-accent-soft-ink: #2c31ad;
  --color-accent-line: #c3c6f6;

  /* Status. Each carries a surface, an ink and a line so status never relies
     on hue alone — icon + label always accompany the colour. */
  --color-success: #197a4b;
  --color-success-soft: #e6f5ec;
  --color-success-line: #b3ddc6;
  --color-warning: #8a5a06;
  --color-warning-soft: #fdf2dd;
  --color-warning-line: #ecd39a;
  --color-danger: #b4231f;
  --color-danger-soft: #fdeceb;
  --color-danger-line: #f2c0bd;
  --color-info: #1a5f95;
  --color-info-soft: #e8f2fb;
  --color-info-line: #b6d5ee;

  /* Elevation: paper-like, never glowing. */
  --shadow-xs: 0 1px 1px rgb(23 23 26 / 0.04);
  --shadow-sm: 0 1px 2px rgb(23 23 26 / 0.06), 0 1px 1px rgb(23 23 26 / 0.04);
  --shadow-md: 0 4px 10px -2px rgb(23 23 26 / 0.08), 0 2px 4px -2px rgb(23 23 26 / 0.05);
  --shadow-lg: 0 12px 28px -8px rgb(23 23 26 / 0.14), 0 4px 10px -4px rgb(23 23 26 / 0.06);
  --shadow-overlay: 0 24px 60px -18px rgb(23 23 26 / 0.28), 0 8px 20px -10px rgb(23 23 26 / 0.12);

  --color-scrim: rgb(23 23 26 / 0.42);
  --color-grid-line: rgb(23 23 26 / 0.05);
}

/* --------------------------------------------------------------------------
   1b. SCHEME — dark (authored, not inverted)
   -------------------------------------------------------------------------- */
[data-theme='dark'] {
  color-scheme: dark;

  --color-canvas: #101012;
  --color-surface: #171719;
  --color-surface-raised: #1d1d21;
  --color-surface-sunken: #0b0b0d;
  --color-surface-inverse: #f7f6f2;
  --color-code-surface: #0d0d0f;
  --color-code-gutter: #141417;

  --color-ink: #ecebe7;
  --color-ink-strong: #ffffff;
  --color-ink-muted: #a3a19b;
  --color-ink-subtle: #7d7b75;
  --color-ink-inverse: #17171a;
  --color-code-ink: #e6e6ec;

  --color-border: #2a2a2f;
  --color-border-strong: #3d3d44;
  --color-border-subtle: #202024;

  --color-accent: #7f87ff;
  --color-accent-hover: #949bff;
  --color-accent-active: #a8adff;
  --color-accent-ink: #101024;
  --color-accent-soft: #1c1d3a;
  --color-accent-soft-ink: #b2b7ff;
  --color-accent-line: #343670;

  --color-success: #4ec98a;
  --color-success-soft: #10261b;
  --color-success-line: #245c40;
  --color-warning: #e0ab52;
  --color-warning-soft: #2a1f0c;
  --color-warning-line: #5e4718;
  --color-danger: #f4837c;
  --color-danger-soft: #2c1413;
  --color-danger-line: #66302d;
  --color-info: #6fb2e8;
  --color-info-soft: #0f2130;
  --color-info-line: #244a68;

  --shadow-xs: 0 1px 1px rgb(0 0 0 / 0.3);
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4), 0 1px 1px rgb(0 0 0 / 0.3);
  --shadow-md: 0 4px 10px -2px rgb(0 0 0 / 0.5), 0 2px 4px -2px rgb(0 0 0 / 0.4);
  --shadow-lg: 0 12px 28px -8px rgb(0 0 0 / 0.6), 0 4px 10px -4px rgb(0 0 0 / 0.4);
  --shadow-overlay: 0 24px 60px -18px rgb(0 0 0 / 0.75), 0 8px 20px -10px rgb(0 0 0 / 0.5);

  --color-scrim: rgb(0 0 0 / 0.66);
  --color-grid-line: rgb(255 255 255 / 0.05);
}

/* --------------------------------------------------------------------------
   2. SHARED NON-SCHEME TOKENS
   -------------------------------------------------------------------------- */
:root {
  /* Typography scale — a modular 1.2 ratio anchored at 16px. */
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, ui-serif, serif;
  --font-sans:
    ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;

  --text-2xs: 0.6875rem;
  --text-xs: 0.75rem;
  --text-sm: 0.8125rem;
  --text-base: 0.9375rem;
  --text-md: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.375rem;
  --text-2xl: 1.75rem;
  --text-3xl: 2.125rem;
  --text-4xl: 2.75rem;
  --text-5xl: 3.5rem;
  --text-6xl: 4.5rem;

  --leading-tight: 1.12;
  --leading-snug: 1.28;
  --leading-normal: 1.6;
  --leading-relaxed: 1.75;

  --tracking-tighter: -0.03em;
  --tracking-tight: -0.015em;
  --tracking-normal: 0em;
  --tracking-wide: 0.04em;
  --tracking-label: 0.11em;

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* Spacing — 4px base grid. */
  --space-0: 0px;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-20: 5rem;
  --space-24: 6rem;
  --space-32: 8rem;

  /* Radius — deliberately restrained. */
  --radius-none: 0px;
  --radius-xs: 2px;
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 10px;
  --radius-xl: 14px;
  --radius-full: 9999px;

  /* Borders */
  --border-width: 1px;
  --border-width-strong: 2px;

  /* Motion */
  --duration-instant: 80ms;
  --duration-fast: 140ms;
  --duration-normal: 220ms;
  --duration-slow: 360ms;
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-entrance: cubic-bezier(0.05, 0.7, 0.1, 1);
  --ease-exit: cubic-bezier(0.3, 0, 0.8, 0.15);

  /* Focus */
  --focus-ring-width: 2px;
  --focus-ring-offset: 2px;
  --focus-ring-color: var(--color-accent);

  /* Layout constants */
  --layout-header-height: 3.5rem;
  --layout-sidebar-width: 16.5rem;
  --layout-toc-width: 14rem;
  --layout-content-max: 78rem;
  --layout-prose-max: 46rem;

  /* Breakpoints (documented + consumed by the tokens page) */
  --breakpoint-sm: 40rem;
  --breakpoint-md: 48rem;
  --breakpoint-lg: 64rem;
  --breakpoint-xl: 80rem;
  --breakpoint-2xl: 96rem;

  /* Density (default) */
  --density-control-height: 2.25rem;
  --density-control-height-sm: 1.875rem;
  --density-control-height-lg: 2.75rem;
  --density-control-padding-x: 0.875rem;
  --density-gap: 0.75rem;
  --density-stack: 1rem;
  --density-section-y: 5rem;
  --density-card-padding: 1.25rem;
  --density-row-height: 2.75rem;
}

/* --------------------------------------------------------------------------
   3. PALETTES — visible personality shifts, not hue-only tweaks

   Each palette has a light form and a dark form. The dark form is matched two
   ways on purpose: the compound selector covers a palette set on the same
   element as the scheme (the page root, a preview iframe's document), and the
   descendant selector covers a palette scoped to a subtree while the scheme
   stays on <html> — which is exactly what a starter shell does. Without the
   second form a starter would keep its light surfaces under dark ink.
   -------------------------------------------------------------------------- */
[data-palette='editorial'] {
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, ui-serif, serif;
  --radius-sm: 3px;
  --radius-md: 4px;
  --radius-lg: 6px;
  --radius-xl: 8px;
  --tracking-display: -0.02em;
}

[data-palette='corporate'] {
  --font-display: var(--font-sans);
  --radius-sm: 3px;
  --radius-md: 4px;
  --radius-lg: 6px;
  --radius-xl: 8px;
  --tracking-display: -0.01em;
  --color-accent: #17558f;
  --color-accent-hover: #12466f;
  --color-accent-active: #0d3a5d;
  --color-accent-soft: #e7f0f8;
  --color-accent-soft-ink: #124168;
  --color-accent-line: #b3cee6;
  --color-canvas: #f6f7f9;
  --color-surface-sunken: #eceff3;
  --color-border: #dde2e9;
  --color-border-strong: #c3cbd6;
  --color-border-subtle: #e8ebf0;
}

[data-theme='dark'][data-palette='corporate'],
[data-theme='dark'] [data-palette='corporate'] {
  --color-accent: #6aaae4;
  --color-accent-hover: #83bbef;
  --color-accent-active: #9ac8f3;
  --color-accent-ink: #06192a;
  --color-accent-soft: #10222f;
  --color-accent-soft-ink: #9ccbf0;
  --color-accent-line: #1f4258;
  --color-canvas: #0d1013;
  --color-surface: #14181c;
  --color-surface-raised: #1a1f24;
  --color-surface-sunken: #080b0e;
  --color-border: #242b31;
  --color-border-strong: #364049;
  --color-border-subtle: #1c2126;
}

[data-palette='minimal'] {
  --font-display: var(--font-sans);
  --radius-xs: 0px;
  --radius-sm: 0px;
  --radius-md: 0px;
  --radius-lg: 0px;
  --radius-xl: 0px;
  --tracking-display: -0.025em;
  --color-accent: #17171a;
  --color-accent-hover: #000000;
  --color-accent-active: #000000;
  --color-accent-ink: #ffffff;
  --color-accent-soft: #f0efec;
  --color-accent-soft-ink: #17171a;
  --color-accent-line: #d6d3cc;
  --color-canvas: #ffffff;
  --color-surface-sunken: #f6f5f2;
  --shadow-sm: none;
  --shadow-md: none;
  --shadow-lg: 0 1px 0 var(--color-border);
}

[data-theme='dark'][data-palette='minimal'],
[data-theme='dark'] [data-palette='minimal'] {
  --color-accent: #f2f1ed;
  --color-accent-hover: #ffffff;
  --color-accent-active: #ffffff;
  --color-accent-ink: #101012;
  --color-accent-soft: #202024;
  --color-accent-soft-ink: #f2f1ed;
  --color-accent-line: #35353b;
  --color-canvas: #0a0a0b;
  --color-surface: #131315;
  --color-surface-sunken: #050506;
}

[data-palette='modern'] {
  --font-display: var(--font-sans);
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;
  --radius-xl: 22px;
  --tracking-display: -0.035em;
  --color-accent: #0a7a6a;
  --color-accent-hover: #086356;
  --color-accent-active: #065347;
  --color-accent-soft: #e2f4f0;
  --color-accent-soft-ink: #06584c;
  --color-accent-line: #a8ded5;
  --color-canvas: #f7f8f8;
  --color-surface-sunken: #edf0ef;
  --color-border: #e0e5e4;
  --color-border-strong: #c6cecc;
}

[data-theme='dark'][data-palette='modern'],
[data-theme='dark'] [data-palette='modern'] {
  --color-accent: #43d5bb;
  --color-accent-hover: #5fe0c9;
  --color-accent-active: #7ae8d4;
  --color-accent-ink: #04201b;
  --color-accent-soft: #0c2a25;
  --color-accent-soft-ink: #6fdcc6;
  --color-accent-line: #17564a;
  --color-canvas: #0a0f0e;
  --color-surface: #111817;
  --color-surface-raised: #16201e;
  --color-surface-sunken: #060a09;
  --color-border: #1f2b29;
  --color-border-strong: #2f403d;
}

[data-palette='warm'] {
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, ui-serif, serif;
  --radius-sm: 5px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 18px;
  --tracking-display: -0.018em;
  --color-accent: #b04a1c;
  --color-accent-hover: #963d15;
  --color-accent-active: #7d3210;
  --color-accent-soft: #fbeade;
  --color-accent-soft-ink: #8f3a13;
  --color-accent-line: #eec4a6;
  --color-canvas: #fbf6ef;
  --color-surface: #fffdfa;
  --color-surface-sunken: #f4ebdf;
  --color-border: #e8dbc9;
  --color-border-strong: #d3c0a7;
  --color-border-subtle: #f0e6d8;
}

[data-theme='dark'][data-palette='warm'],
[data-theme='dark'] [data-palette='warm'] {
  --color-accent: #f09256;
  --color-accent-hover: #f5a470;
  --color-accent-active: #f8b689;
  --color-accent-ink: #2a1206;
  --color-accent-soft: #2c1a0e;
  --color-accent-soft-ink: #f0a778;
  --color-accent-line: #5c3419;
  --color-canvas: #14100c;
  --color-surface: #1b1611;
  --color-surface-raised: #221c16;
  --color-surface-sunken: #0e0b08;
  --color-border: #2f2720;
  --color-border-strong: #443830;
  --color-border-subtle: #241e18;
}

/* --------------------------------------------------------------------------
   4. DENSITY
   -------------------------------------------------------------------------- */
[data-density='compact'] {
  --density-control-height: 1.875rem;
  --density-control-height-sm: 1.625rem;
  --density-control-height-lg: 2.25rem;
  --density-control-padding-x: 0.625rem;
  --density-gap: 0.5rem;
  --density-stack: 0.625rem;
  --density-section-y: 3rem;
  --density-card-padding: 0.875rem;
  --density-row-height: 2.125rem;
}

[data-density='relaxed'] {
  --density-control-height: 2.625rem;
  --density-control-height-sm: 2.25rem;
  --density-control-height-lg: 3.125rem;
  --density-control-padding-x: 1.125rem;
  --density-gap: 1rem;
  --density-stack: 1.5rem;
  --density-section-y: 7rem;
  --density-card-padding: 1.75rem;
  --density-row-height: 3.25rem;
}

/* --------------------------------------------------------------------------
   5. TAILWIND THEME BRIDGE
   Every token above becomes a first-class Tailwind utility.
   -------------------------------------------------------------------------- */
@theme inline {
  --color-canvas: var(--color-canvas);
  --color-surface: var(--color-surface);
  --color-surface-raised: var(--color-surface-raised);
  --color-surface-sunken: var(--color-surface-sunken);
  --color-surface-inverse: var(--color-surface-inverse);
  --color-code-surface: var(--color-code-surface);
  --color-code-gutter: var(--color-code-gutter);
  --color-code-ink: var(--color-code-ink);

  --color-ink: var(--color-ink);
  --color-ink-strong: var(--color-ink-strong);
  --color-ink-muted: var(--color-ink-muted);
  --color-ink-subtle: var(--color-ink-subtle);
  --color-ink-inverse: var(--color-ink-inverse);

  --color-line: var(--color-border);
  --color-line-strong: var(--color-border-strong);
  --color-line-subtle: var(--color-border-subtle);

  --color-accent: var(--color-accent);
  --color-accent-hover: var(--color-accent-hover);
  --color-accent-active: var(--color-accent-active);
  --color-accent-ink: var(--color-accent-ink);
  --color-accent-soft: var(--color-accent-soft);
  --color-accent-soft-ink: var(--color-accent-soft-ink);
  --color-accent-line: var(--color-accent-line);

  --color-success: var(--color-success);
  --color-success-soft: var(--color-success-soft);
  --color-success-line: var(--color-success-line);
  --color-warning: var(--color-warning);
  --color-warning-soft: var(--color-warning-soft);
  --color-warning-line: var(--color-warning-line);
  --color-danger: var(--color-danger);
  --color-danger-soft: var(--color-danger-soft);
  --color-danger-line: var(--color-danger-line);
  --color-info: var(--color-info);
  --color-info-soft: var(--color-info-soft);
  --color-info-line: var(--color-info-line);

  --color-scrim: var(--color-scrim);
  --color-grid-line: var(--color-grid-line);

  --font-display: var(--font-display);
  --font-sans: var(--font-sans);
  --font-mono: var(--font-mono);

  --text-2xs: var(--text-2xs);
  --text-xs: var(--text-xs);
  --text-sm: var(--text-sm);
  --text-base: var(--text-base);
  --text-md: var(--text-md);
  --text-lg: var(--text-lg);
  --text-xl: var(--text-xl);
  --text-2xl: var(--text-2xl);
  --text-3xl: var(--text-3xl);
  --text-4xl: var(--text-4xl);
  --text-5xl: var(--text-5xl);
  --text-6xl: var(--text-6xl);

  --radius-xs: var(--radius-xs);
  --radius-sm: var(--radius-sm);
  --radius-md: var(--radius-md);
  --radius-lg: var(--radius-lg);
  --radius-xl: var(--radius-xl);
  --radius-full: var(--radius-full);

  --shadow-xs: var(--shadow-xs);
  --shadow-sm: var(--shadow-sm);
  --shadow-md: var(--shadow-md);
  --shadow-lg: var(--shadow-lg);
  --shadow-overlay: var(--shadow-overlay);

  --spacing-control: var(--density-control-height);
  --spacing-control-sm: var(--density-control-height-sm);
  --spacing-control-lg: var(--density-control-height-lg);
  --spacing-gap: var(--density-gap);
  --spacing-stack: var(--density-stack);
  --spacing-section: var(--density-section-y);
  --spacing-card: var(--density-card-padding);
  --spacing-row: var(--density-row-height);

  --breakpoint-sm: 40rem;
  --breakpoint-md: 48rem;
  --breakpoint-lg: 64rem;
  --breakpoint-xl: 80rem;
  --breakpoint-2xl: 96rem;

  --ease-standard: var(--ease-standard);
  --ease-entrance: var(--ease-entrance);
  --ease-exit: var(--ease-exit);
}

/* --------------------------------------------------------------------------
   6. BASE LAYER
   -------------------------------------------------------------------------- */
@layer base {
  *,
  *::before,
  *::after {
    border-color: var(--color-border);
  }

  html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: calc(var(--layout-header-height) + 1.5rem);
  }

  body {
    background-color: var(--color-canvas);
    color: var(--color-ink);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
  }

  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    color: var(--color-ink-strong);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-tight);
    text-wrap: balance;
  }

  p {
    text-wrap: pretty;
  }

  code,
  kbd,
  samp,
  pre {
    font-family: var(--font-mono);
    font-size: 0.92em;
  }

  :focus-visible {
    outline: var(--focus-ring-width) solid var(--focus-ring-color);
    outline-offset: var(--focus-ring-offset);
    border-radius: var(--radius-xs);
  }

  ::selection {
    background: var(--color-accent-soft);
    color: var(--color-accent-soft-ink);
  }

  /* Long identifiers (component slugs, tokens) must never blow out a layout. */
  .break-token {
    overflow-wrap: anywhere;
    word-break: break-word;
  }
}

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

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --------------------------------------------------------------------------
   7. UTILITIES
   -------------------------------------------------------------------------- */
@utility label-caps {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-weight: var(--weight-medium);
}

@utility display-type {
  font-family: var(--font-display);
  letter-spacing: var(--tracking-display, -0.02em);
}

@utility grid-paper {
  background-image:
    linear-gradient(to right, var(--color-grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--color-grid-line) 1px, transparent 1px);
  background-size: 2rem 2rem;
}

@utility hide-scrollbar {
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}

@utility thin-scrollbar {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border-strong) transparent;
}

/* Animation primitives used by overlays, toasts and skeletons. */
@keyframes foundry-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes foundry-scale-in {
  from {
    opacity: 0;
    transform: translateY(4px) scale(0.985);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes foundry-slide-right {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes foundry-slide-left {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes foundry-slide-up {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

@keyframes foundry-spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes foundry-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}

@keyframes foundry-marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

@utility animate-fade-in {
  animation: foundry-fade-in var(--duration-fast) var(--ease-entrance);
}

@utility animate-scale-in {
  animation: foundry-scale-in var(--duration-normal) var(--ease-entrance);
}

@utility animate-slide-right {
  animation: foundry-slide-right var(--duration-normal) var(--ease-entrance);
}

@utility animate-slide-left {
  animation: foundry-slide-left var(--duration-normal) var(--ease-entrance);
}

@utility animate-slide-up {
  animation: foundry-slide-up var(--duration-normal) var(--ease-entrance);
}

@utility animate-spin-token {
  animation: foundry-spin 0.7s linear infinite;
}

@utility animate-pulse-token {
  animation: foundry-pulse 1.6s var(--ease-standard) infinite;
}

@utility animate-marquee {
  animation: foundry-marquee 40s linear infinite;
}

/* --------------------------------------------------------------------------
   8. PROSE — documentation typography, token driven
   -------------------------------------------------------------------------- */
.prose-foundry {
  color: var(--color-ink);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  max-width: var(--layout-prose-max);
}

.prose-foundry > * + * {
  margin-top: var(--space-4);
}

.prose-foundry h2 {
  font-size: var(--text-xl);
  margin-top: var(--space-10);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border-subtle);
}

.prose-foundry h3 {
  font-size: var(--text-lg);
  margin-top: var(--space-8);
  margin-bottom: var(--space-2);
}

.prose-foundry h4 {
  font-size: var(--text-md);
  margin-top: var(--space-6);
  margin-bottom: var(--space-2);
}

.prose-foundry ul,
.prose-foundry ol {
  padding-left: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.prose-foundry ul {
  list-style: disc;
}

.prose-foundry ol {
  list-style: decimal;
}

.prose-foundry li::marker {
  color: var(--color-ink-subtle);
}

.prose-foundry a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

.prose-foundry a:hover {
  color: var(--color-accent-hover);
}

.prose-foundry strong {
  font-weight: var(--weight-semibold);
  color: var(--color-ink-strong);
}

.prose-foundry :not(pre) > code {
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-xs);
  padding: 0.1em 0.35em;
  color: var(--color-ink);
}

.prose-foundry blockquote {
  border-left: 2px solid var(--color-accent-line);
  padding-left: var(--space-4);
  color: var(--color-ink-muted);
  font-style: italic;
}

/* --------------------------------------------------------------------------
   9. RANGE INPUT
   Vendor pseudo-elements cannot be expressed as utilities, so the slider's
   track and thumb are authored here and driven by --slider-fill.
   -------------------------------------------------------------------------- */
.foundry-slider {
  width: 100%;
  min-width: 0;
  height: 1.25rem;
  cursor: pointer;
  appearance: none;
  background: transparent;
}

.foundry-slider:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.foundry-slider::-webkit-slider-runnable-track {
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: linear-gradient(
    to right,
    var(--color-accent) var(--slider-fill, 0%),
    var(--color-surface-sunken) var(--slider-fill, 0%)
  );
}

.foundry-slider::-webkit-slider-thumb {
  appearance: none;
  margin-top: -0.3125rem;
  height: 1rem;
  width: 1rem;
  border-radius: var(--radius-full);
  border: 2px solid var(--color-accent);
  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

.foundry-slider::-moz-range-track {
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: var(--color-surface-sunken);
}

.foundry-slider::-moz-range-progress {
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: var(--color-accent);
}

.foundry-slider::-moz-range-thumb {
  height: 1rem;
  width: 1rem;
  border-radius: var(--radius-full);
  border: 2px solid var(--color-accent);
  background: var(--color-surface);
}

/* ---------------------------------------------------------------------------
   Progressive enhancement
   ---------------------------------------------------------------------------
   The blocking script in <head> stamps data-js="on" before first paint, so
   these swaps never flash. Controls that only work with scripting are hidden
   when there is none, and a link-based fallback takes their place — the shell
   must never present a control that cannot do anything.
--------------------------------------------------------------------------- */
[data-js='on'] [data-nojs] {
  display: none !important;
}

html:not([data-js='on']) [data-js-only] {
  display: none !important;
}

/* The code viewer's collapse is presentational: the full listing is always in
   the DOM, so with no scripting to expand it the clamp simply does not apply. */
[data-js='on'] [data-code-body][data-collapsed='true'] {
  max-height: calc(var(--code-collapsed-lines) * 1.21875rem + 1.75rem);
  overflow-y: hidden;
}

/* Same idea for the mobile documentation outline. */
[data-js='on'] [data-collapsed='true']#docs-outline {
  display: none;
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

The list is duplicated and translated by exactly minus fifty per cent, which is what makes the loop seamless. The duplicate is hidden from assistive tech so names are announced once.

  • Mask both edges, or the loop visibly restarts at the boundary.
  • The animation stops entirely under prefers-reduced-motion — that is not optional.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Duplicated track
  • Edge mask
  • Stops under reduced motion

Accessibility

Single announcement
The duplicated track is aria-hidden.
Reduced motion
The global reset disables the animation.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.