/* RomandaMedia theme base styles.

   Order matters. Tokens first so every layer below can reference them, then
   generic resets, then layout objects, then styled elements, then components,
   then utilities last so a helper class can win.

   Theme editor values arrive separately via theme-overrides.css, which
   base.html loads after this file.

   CACHE NOTE: HubSpot compiles this file into template_main.min.css and
   versions it from THIS file, not from the partials it includes. Editing only
   a partial leaves the compiled asset stale and the change never reaches the
   browser, with Design Manager showing the new source the whole time. Touching
   this file is what forces the rebuild. Last touched 2026-09-07 for the
   horizontal-reveal clip in utilities/_motion.css.
*/

/* Tokens: the brand baseline, generated from brand-tokens/[client].json */
/* GENERATED by scripts/render_tokens.py. Do not edit by hand; edit the brand-tokens file and re-run. */
/* Brand: RomandaMedia  Source: functions/marketing/brand/releases/RomandaMedia-Brand-Book-v1.2.html */

:root {
  /* Palette */
  --palette-ink: #141019;
  --palette-charcoal: #3B3A45;
  --palette-slate: #5C5B66;
  --palette-mist: #E8E5F0;
  --palette-cloud: #F8F7FC;
  --palette-white: #FFFFFF;
  --palette-plum-900: #1A1433;
  --palette-indigo-700: #2A1F55;
  --palette-violet-600: #3F1D77;
  --palette-violet: #7338D2;
  --palette-violet-bright: #8B58E0;
  --palette-lavender: #C0A4FB;
  --palette-lavender-mist: #F1EBFF;
  --palette-sand: #C9B08A;
  --palette-bone: #F3ECDE;
  --palette-lime: #A7E130;
  --palette-pale-lime: #DAFF8E;
  --palette-moss: #5F8410;
  --palette-error: #B3261E;

  /* Semantic roles. Reference these, not the palette. */
  --color-primary: var(--palette-violet);
  --color-primary-deep: var(--palette-violet-600);
  --color-primary-bright: var(--palette-violet-bright);
  --color-secondary: var(--palette-plum-900);
  --color-accent: var(--palette-lime);
  --color-accent-soft: var(--palette-pale-lime);
  --color-accent-deep: var(--palette-moss);
  --color-text: var(--palette-ink);
  --color-text-muted: var(--palette-slate);
  --color-surface: var(--palette-white);
  --color-surface-alt: var(--palette-cloud);
  --color-surface-deep: var(--palette-plum-900);
  --color-surface-warm: var(--palette-bone);
  --color-border: var(--palette-mist);
  --color-on-primary: var(--palette-white);
  --color-on-accent: var(--palette-ink);
  --color-on-deep: var(--palette-white);
  --color-error: var(--palette-error);

  /* Gradients */
  --gradient-deep: linear-gradient(135deg,#1A1433 0%,#2A1F55 55%,#3F1D77 100%);

  /* Type */
  --font-head: 'Fraunces', Georgia, 'Times New Roman', serif;
  --font-body: 'Lexend Deca', Arial, 'Helvetica Neue', Helvetica, sans-serif;
  --font-wordmark: 'Montserrat', Arial, sans-serif;

  /* Radii (theme decision, not a brand-book value) */
  --radius-sm: 6px;
  --radius-md: 12px;
  --radius-lg: 14px;
  --radius-pill: 999px;

  /* Layout (theme decision) */
  --layout-content-max: 1200px;
  --layout-content-narrow: 760px;
  --layout-gutter: 24px;
}

/* Generic: reset and normalize. No brand opinion lives here. */
*, *:before, *:after {
  box-sizing: border-box;
}

/* THE hidden ATTRIBUTE HAS TO WIN.

   The browser's own rule is `[hidden] { display: none }`, specificity 0,0,1.
   This theme sets `button { display: inline-block }` in the elements layer at
   the SAME specificity and later in the cascade, so it wins, and `hidden` stops
   working on every button in the theme. Setting `el.hidden = true` in JS then
   does nothing visible and there is no error to notice.

   Found 2026-08-27: the audit quiz's Back button stayed on screen at step one
   with hidden="" correctly set on the element.

   !important is right here rather than a specificity bump, because `hidden`
   means hidden and nothing in a component should be able to out-rank it. */
[hidden] { display: none !important; }
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */

/* Document
   ========================================================================== */

/**
 * 1. Correct the line height in all browsers.
 * 2. Prevent adjustments of font size after orientation changes in iOS.
 */

html {
  line-height: 1.15; /* 1 */
  -webkit-text-size-adjust: 100%; /* 2 */
}

/* Sections
   ========================================================================== */

/**
 * Remove the margin in all browsers.
 */

body {
  margin: 0;
}

/**
 * Correct the font size and margin on `h1` elements within `section` and
 * `article` contexts in Chrome, Firefox, and Safari.
 */

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

/* Grouping content
   ========================================================================== */

/**
 * Add the correct box sizing in Firefox.
 */

hr {
  box-sizing: content-box;
  height: 0;
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

pre {
  font-family: monospace, monospace; /* 1 */
  font-size: 1em; /* 2 */
}

/* Text-level semantics
   ========================================================================== */

/**
 * 1. Remove the bottom border in Chrome 57-
 * 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.
 */

abbr[title] {
  border-bottom: none; /* 1 */
  text-decoration: underline; /* 2 */
  text-decoration: underline dotted; /* 2 */
}

/**
 * Add the correct font weight in Chrome, Edge, and Safari.
 */

b,
strong {
  font-weight: bolder;
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

code,
kbd,
samp {
  font-family: monospace, monospace; /* 1 */
  font-size: 1em; /* 2 */
}

/**
 * Add the correct font size in all browsers.
 */

small {
  font-size: 80%;
}

/**
 * Prevent `sub` and `sup` elements from affecting the line height in
 * all browsers.
 */

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

/* Forms
   ========================================================================== */

/**
 * 1. Change the font styles in all browsers.
 * 2. Remove the margin in Firefox and Safari.
 */

button,
input,
optgroup,
select,
textarea {
  font-family: inherit; /* 1 */
  font-size: 100%; /* 1 */
  line-height: 1.15; /* 1 */
  margin: 0; /* 2 */
}

/**
 * Remove the inheritance of text transform in Edge and Firefox.
 * 1. Remove the inheritance of text transform in Firefox.
 */

button,
select { /* 1 */
  text-transform: none;
}

/**
 * Correct the inability to style clickable types in iOS and Safari.
 */

button,
[type="button"],
[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
}

/**
 * Remove the inner border and padding in Firefox.
 */

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

/**
 * Restore the focus styles unset by the previous rule.
 */

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

/**
 * Correct the padding in Firefox.
 */

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

/**
 * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers.
 */

legend {
  padding: 0;
}

/**
 * Add the correct vertical alignment in Chrome, Firefox, and Opera.
 */

progress {
  vertical-align: baseline;
}

/**
 * Correct the cursor style of increment and decrement buttons in Chrome.
 */

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

/**
 * 1. Correct the odd appearance in Chrome and Safari.
 * 2. Correct the outline style in Safari.
 */

[type="search"] {
  -webkit-appearance: textfield; /* 1 */
  outline-offset: -2px; /* 2 */
}

/**
 * Remove the inner padding in Chrome and Safari on macOS.
 */

[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

/**
 * 1. Correct the inability to style clickable types in iOS and Safari.
 * 2. Change font properties to `inherit` in Safari.
 */

::-webkit-file-upload-button {
  -webkit-appearance: button; /* 1 */
  font: inherit; /* 2 */
}

/* Interactive
   ========================================================================== */

/*
 * Add the correct display in Edge and Firefox.
 */

details {
  display: block;
}

/*
 * Add the correct display in all browsers.
 */

summary {
  display: list-item;
}

/* Objects: non-cosmetic layout patterns */
/* Layout objects. Structure only, no cosmetics. */

*, *::before, *::after { box-sizing: border-box; }

img, video, svg { max-width: 100%; height: auto; }

/* The standard centred column. Every section's inner wrapper. */
.content-wrapper {
  width: 100%;
  max-width: var(--layout-content-max);
  margin-left: auto;
  margin-right: auto;
  padding-left: var(--layout-gutter);
  padding-right: var(--layout-gutter);
}

/* The narrow modifier centres itself.

   It is applied to elements that are NOT also .content-wrapper (the FAQ
   accordion is one), and a max-width with no auto margins does not centre: it
   just pins the block to the left edge of its parent and looks like a
   misalignment bug. Carrying the margins here makes the modifier safe to use
   on its own, which is how it reads. */
.content-wrapper--narrow {
  max-width: var(--layout-content-narrow);
  margin-left: auto;
  margin-right: auto;
}

/* Vertical rhythm between page sections. */
.section {
  padding-top: var(--layout-section-y, 80px);
  padding-bottom: var(--layout-section-y, 80px);
}

.section--tight { padding-top: 48px; padding-bottom: 48px; }
.section--alt { background-color: var(--color-surface-alt); }
.section--warm { background-color: var(--color-surface-warm); }
.section--deep { background: var(--gradient-deep); color: var(--color-on-deep); }

/* The lavender ground the blog, careers, portal and legal pages sit on. Added
   here on 2026-08-30 as the FOURTH surface asked for it, which is the point
   the per-template copies stopped being worth keeping separate.
   section--blog-tinted, section--career-tinted, section--portal-tinted and
   section--legal-tinted still exist in their own stylesheets and still work;
   they are live and signed off, so they were left alone rather than
   refactored at the end of a session. Use THIS one for anything new, and
   collapse the other four when one of those pages is next touched.

   CONTRAST on #F1EBFF, measured for the legal pages: body 9.63:1,
   muted 5.75:1, links 5.60:1. All over 4.5:1. */
.section--tinted { background-color: var(--palette-lavender-mist); }

/* Auto-fitting card grid. min is the smallest a card may get before wrapping.

   THE min() IS LOAD-BEARING, do not simplify it to minmax(var(--grid-min), 1fr).

   A bare minmax() minimum is a floor the track will not go below EVEN WHEN THE
   CONTAINER IS NARROWER. At 375px the content wrapper is 343px wide, so a
   380px minimum produced 380px cards that ran 21px past the viewport: a real
   horizontal scrollbar, plus content that looked mis-centred because it sat
   flush left and overhung on the right. That is the "left has more padding
   than the right" report from mobile, 2026-08-27.

   min(var(--grid-min), 100%) clamps the floor to the container, so the track
   is 380px when there is room and full-width when there is not. */
.grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--grid-min, 260px), 100%), 1fr));
}

.grid--2 { --grid-min: 380px; }
.grid--4 { --grid-min: 200px; }

/* Two-column split for a hero or an image-beside-text row. */
.split {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  grid-template-columns: 1fr;
}

@media (min-width: 768px) {
  .split { grid-template-columns: 1fr 1fr; }
  .split--wide-left { grid-template-columns: 1.25fr 1fr; }
}

.stack > * + * { margin-top: var(--stack-gap, 1rem); }

/* Accessibility: the skip link target used by header.html. */
.screen-reader-text {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link:focus {
  position: fixed;
  top: 0.5rem; left: 0.5rem;
  z-index: 999;
  width: auto; height: auto;
  clip: auto;
  padding: 0.75em 1.25em;
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius-sm);
}
.content-wrapper {
  margin: 0 auto;
  padding: 0 1rem;
}

@media screen and (min-width: 1380px) {
  .content-wrapper {
    padding: 0;
  }
}

.dnd-section > .row-fluid {
  margin: 0 auto;
}

.dnd-section .dnd-column {
  padding: 0 1rem;
}

@media (max-width: 767px) {
  .dnd-section .dnd-column {
    padding: 0;
  }
}

/* Elements: bare HTML elements */
/* Type scale.

   Fraunces for headings, Lexend Deca for body, per Brand Book v1.2. Sizes use
   clamp() so the scale is fluid between the mobile breakpoint and the content
   max width, which avoids a separate set of media queries per heading level.
*/

html {
  font-size: 100%;
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-body);
  font-size: 1.125rem;
  line-height: 1.65;
  color: var(--color-text);
  background-color: var(--color-surface);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-head);
  color: var(--color-text);
  line-height: 1.12;
  margin: 0 0 0.5em;
  text-wrap: balance;
}

h1 { font-size: clamp(2.25rem, 5.5vw, 3.75rem); font-weight: 800; letter-spacing: -0.02em; }
h2 { font-size: clamp(1.85rem, 4vw, 2.75rem); font-weight: 700; letter-spacing: -0.015em; }
h3 { font-size: clamp(1.45rem, 3vw, 1.95rem); font-weight: 700; }
h4 { font-size: clamp(1.2rem, 2.2vw, 1.4rem); font-weight: 600; }
h5 { font-size: 1.15rem; font-weight: 600; }
h6 { font-size: 1rem; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; }

p { margin: 0 0 1.15em; }
p:last-child { margin-bottom: 0; }

/* A WORD LONGER THAN ITS LINE BREAKS, rather than pushing the whole page
   sideways. The realistic case is an email address or a URL written into body
   copy: `accounting@romandamedia.com` inside a 288px card at a 320px viewport
   overflowed the document by 16px, which is how billing.html failed
   overflow_scan on 2026-09-09.

   It had been failing silently for as long as that copy existed. The local
   render was resolving every `--font-*` to an unresolved HubL string, so it
   measured pages in a fallback face whose narrower metrics happened to fit the
   address. Fixing the render (BUILD_LOG 3.80) is what made the scan able to
   see it. */
p, li, dd, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }

a {
  color: var(--color-primary);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  transition: color 0.15s ease;
}

a:hover,
a:focus { color: var(--color-primary-deep); }

/* The eyebrow above a heading. Lime is the signal colour and is used sparingly. */
.eyebrow {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-accent-deep);
  margin: 0 0 0.75em;
}

.lead {
  font-size: 1.25rem;
  line-height: 1.55;
  color: var(--color-text-muted);
}

blockquote {
  margin: 1.5em 0;
  padding: 0 0 0 1.25em;
  border-left: 3px solid var(--color-accent);
  font-size: 1.15rem;
}

hr {
  border: 0;
  border-top: 1px solid var(--color-border);
  margin: 2.5rem 0;
}

/* Dark sections invert the type colours without needing a second set of rules. */
.on-deep,
.on-deep h1, .on-deep h2, .on-deep h3, .on-deep h4, .on-deep h5, .on-deep h6 {
  color: var(--color-on-deep);
}

.on-deep .lead { color: var(--palette-lavender); }

/* The eyebrow's default is the deep moss green, which is legible on white and
   nearly invisible on the deep gradient. */
.on-deep .eyebrow { color: var(--color-accent-soft); }
/* Buttons are excluded: .on-deep a beats .button--accent on specificity
   (0,1,1 vs 0,1,0), which silently turned lime button text pale and
   unreadable. Caught on the Phase 1 render check. */
.on-deep a:not(.button) { color: var(--color-accent-soft); }
/* Buttons.

   Three variants: solid primary, lime accent, and outline. HubSpot's CTA and
   form submit elements are given the same treatment here so a form button
   never looks like a different design system.

   CONVENTION, and it is not optional. Any component rule that styles links by
   element must exclude buttons:

       .thing a:not(.button) { color: ... }

   `.thing a` scores 0,1,1 and `.button` scores 0,1,0, so without the :not() the
   component silently repaints button text. This has now bitten twice in one
   session: lime buttons on a dark hero went pale and unreadable, and the header
   CTA rendered ink-on-violet. Both published fine and both looked broken.
*/

.button,
button,
.hs-button,
input[type="submit"],
input[type="button"] {
  display: inline-block;
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.2;
  padding: 0.875em 1.75em;
  border: 1.5px solid transparent;
  border-radius: var(--button-radius, var(--radius-pill));
  background-color: var(--button-bg, var(--color-primary));
  color: var(--button-text, var(--color-on-primary));
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

.button:hover,
button:hover,
.hs-button:hover,
input[type="submit"]:hover {
  background-color: var(--color-primary-bright);
  color: var(--color-on-primary);
  transform: translateY(-1px);
  box-shadow: 0 8px 24px rgba(115, 56, 210, 0.28);
}

.button:focus-visible,
button:focus-visible,
.hs-button:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 2px;
}

/* Lime. The signal colour, reserved for the single most important action on a
   page. Ink text on lime is the only accessible pairing; never white. */
.button--accent {
  background-color: var(--color-accent);
  color: var(--color-on-accent);
}

.button--accent:hover {
  background-color: var(--palette-pale-lime);
  color: var(--color-on-accent);
  box-shadow: 0 8px 24px rgba(167, 225, 48, 0.35);
}

.button--outline {
  background-color: transparent;
  border-color: currentColor;
  color: var(--color-text);
}

.button--outline:hover {
  background-color: var(--color-text);
  color: var(--color-surface);
  box-shadow: none;
}

.on-deep .button--outline {
  color: var(--color-on-deep);
}

.on-deep .button--outline:hover {
  background-color: var(--color-on-deep);
  color: var(--color-text);
}
/* Forms.

   HubSpot forms are not theme assets: they are rendered by the Forms tool and
   arrive with their own class names. Styling them here is how a form embedded
   anywhere on the site inherits the brand. See context/05 Part B for the
   capture pattern these forms sit inside.

   The class names below (.hs-form, .hs-input, .hs-button, .hs-error-msg) are
   HubSpot's, not ours. Do not rename them.
*/

.hs-form label,
form label {
  display: block;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--color-text);
  margin-bottom: 0.4em;
}

.hs-form-required { color: var(--color-primary); }

.hs-input,
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="number"],
input[type="password"],
input[type="search"],
input[type="url"],
select,
textarea {
  width: 100%;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.5;
  color: var(--color-text);
  background-color: var(--color-surface);
  padding: 0.75em 0.9em;
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-sm);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.hs-input:focus,
input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--palette-lavender-mist);
}

.hs-input[type="checkbox"],
.hs-input[type="radio"] {
  width: auto;
  margin-right: 0.5em;
  accent-color: var(--color-primary);
}

textarea { min-height: 8rem; resize: vertical; }

.hs-form-field { margin-bottom: 1.25rem; }

.hs-error-msg,
.hs-error-msgs label {
  color: var(--color-error);
  font-size: 0.875rem;
  margin-top: 0.35em;
}

.hs-input.invalid,
.hs-input.error { border-color: var(--color-error); }

/* A form dropped onto a dark section still has to be readable. */
.on-deep .hs-form label,
.on-deep form label { color: var(--color-on-deep); }

/* ---------------------------------------------------------------------------
   HUBSPOT V4 FORMS

   Everything above targets .hs-form / .hs-input, which is the LEGACY (V3)
   embed. A form built in HubSpot's current editor renders a different class
   tree, prefixed hsfc-, and inherits none of it. That is why the growth audit
   application rendered in Helvetica with 3px corners on a fully branded page.

   DO NOT STYLE THE hsfc- CLASSES DIRECTLY. That was tried first and loses:
   HubSpot injects ~176 inline rules AFTER the theme stylesheet, selected as
   `[data-hsfc-id="Renderer"] .hsfc-TextInput`, so a plain `.hsfc-TextInput`
   rule is out-specified and silently does nothing. Beating it means a
   specificity war against markup that is not ours and will change.

   Instead, every one of those inline rules resolves its value through a
   `--hsf-*` custom property. That is the supported theming surface: 201 of
   them, covering type, colour, radius, padding, spacing and the progress bar.
   Setting the variables wins without touching selectors, and survives HubSpot
   restructuring the form.

   Mapping brand tokens onto them is the whole integration. Confirmed against
   the live /growth-audit render 2026-08-27.

   Worth knowing before reaching for a custom build: the V4 renderer already
   ships the stepped "one question at a time" experience natively, including
   hsfc-Step and a progress bar. Styling it gets most of the quiz feel with no
   application to maintain.
--------------------------------------------------------------------------- */

.hsfc-Form,
[data-hsfc-id="Renderer"] {
  /* Global */
  --hsf-global__font-family: var(--font-body);
  --hsf-global__color: var(--color-text);
  --hsf-global__error-color: var(--color-error);

  /* Step heading and rich text */
  --hsf-heading__font-family: var(--font-head);
  --hsf-heading__color: var(--color-primary-deep);
  --hsf-richtext__font-family: var(--font-body);
  --hsf-richtext__color: var(--color-text-muted);

  /* Labels */
  --hsf-field-label__font-family: var(--font-body);
  --hsf-field-label__font-size: 0.9375rem;
  --hsf-field-label__color: var(--color-text);
  --hsf-field-label-requiredindicator__color: var(--color-primary);

  /* Text inputs and dropdowns */
  --hsf-field-input__font-family: var(--font-body);
  --hsf-field-input__font-size: 1rem;
  --hsf-field-input__color: var(--color-text);
  --hsf-field-input__background-color: var(--color-surface);
  --hsf-field-input__border-color: var(--color-border);
  --hsf-field-input__border-width: 1.5px;
  --hsf-field-input__border-style: solid;
  --hsf-field-input__border-radius: var(--radius-sm);
  --hsf-field-input__padding: 0.75em 0.9em;
  --hsf-field-input__placeholder-color: var(--color-text-muted);
  --hsf-field-dropdown-options__border-radius: var(--radius-sm);

  /* Textarea, same treatment */
  --hsf-field-textarea__font-family: var(--font-body);
  --hsf-field-textarea__font-size: 1rem;
  --hsf-field-textarea__color: var(--color-text);
  --hsf-field-textarea__border-color: var(--color-border);
  --hsf-field-textarea__border-width: 1.5px;
  --hsf-field-textarea__border-style: solid;
  --hsf-field-textarea__border-radius: var(--radius-sm);
  --hsf-field-textarea__padding: 0.75em 0.9em;
  --hsf-field-textarea__placeholder-color: var(--color-text-muted);

  /* Checkbox and radio */
  --hsf-field-checkbox__color: var(--color-primary);
  --hsf-field-checkbox__border-color: var(--color-border);
  --hsf-field-radio__color: var(--color-primary);
  --hsf-field-radio__border-color: var(--color-border);

  /* Helper and error text */
  --hsf-field-description__font-family: var(--font-body);
  --hsf-field-description__color: var(--color-text-muted);
  --hsf-field-footer__font-family: var(--font-body);
  --hsf-field-footer__color: var(--color-text-muted);
  --hsf-erroralert__font-family: var(--font-body);
  --hsf-erroralert__color: var(--color-error);
  --hsf-infoalert__font-family: var(--font-body);

  /* Buttons: the site's accent pill. */
  --hsf-button__font-family: var(--font-body);
  --hsf-button__font-size: 0.9375rem;
  --hsf-button__font-weight: 600;
  --hsf-button__background-color: var(--color-accent);
  --hsf-button__color: var(--color-on-accent);
  --hsf-button__border-color: transparent;
  --hsf-button__border-width: 1px;
  --hsf-button__border-style: solid;
  --hsf-button__border-radius: var(--radius-pill);
  --hsf-button__padding: 0.8em 1.6em;
  --hsf-button--hover__background-color: var(--color-accent-deep);
  --hsf-button--hover__color: var(--color-on-accent);
  --hsf-button--focus__background-color: var(--color-accent-deep);
  --hsf-button--focus__color: var(--color-on-accent);

  /* Progress bar. This is what makes a long application feel finite, so it
     takes the accent rather than a neutral. */
  --hsf-progressbar-trackLine__background-color: var(--color-border);
  --hsf-progressbar-progressLine__background-color: var(--color-accent);
  --hsf-progressbar-text__font-family: var(--font-body);
  --hsf-progressbar-text__font-size: 0.8125rem;
  --hsf-progressbar-text__color: var(--color-text-muted);

  /* Rhythm */
  --hsf-row__vertical-spacing: 1.15rem;
  --hsf-module__vertical-spacing: 1.5rem;
}

/* A V4 form on a dark section needs its type lifted, same as the V3 rule. */
.on-deep .hsfc-Form,
.on-deep [data-hsfc-id="Renderer"] {
  --hsf-global__color: var(--color-on-deep);
  --hsf-field-label__color: var(--color-on-deep);
  --hsf-heading__color: var(--color-on-deep);
  --hsf-progressbar-text__color: var(--color-on-deep);
}
/* Tables. Used by pricing comparisons and blog content. */

table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1.5rem;
  font-size: 1rem;
}

caption {
  caption-side: bottom;
  padding-top: 0.75rem;
  font-size: 0.875rem;
  color: var(--color-text-muted);
  text-align: left;
}

th, td {
  padding: 0.85em 1em;
  border-bottom: 1px solid var(--color-border);
  text-align: left;
  vertical-align: top;
}

thead th {
  font-family: var(--font-body);
  font-weight: 600;
  color: var(--color-text);
  background-color: var(--color-surface-alt);
  border-bottom-width: 2px;
}

tbody tr:last-child td { border-bottom: 0; }

/* Components: styling for HubSpot's own default modules when they appear in a
   dnd area. Our own modules carry their CSS in their module.css, which HubSpot
   includes only when the module is on the page. */
/* Menu and simple menu */

.hs-menu-wrapper ul {
  display: flex;
  flex-wrap: wrap;
  list-style: none;
  margin: 0;
  padding-left: 0;
}

/* Horizontal menu */

.hs-menu-wrapper.hs-menu-flow-horizontal .hs-menu-children-wrapper {
  flex-direction: column;
}

@media (max-width: 767px) {
  .hs-menu-wrapper.hs-menu-flow-horizontal ul {
    flex-direction: column;
  }
}

/* Vertical menu */

.hs-menu-wrapper.hs-menu-flow-vertical ul {
  flex-direction: column;
}

/* Flyouts */

.hs-menu-wrapper.hs-menu-flow-vertical.flyouts ul {
  display: inline-flex;
}

@media (max-width: 767px) {
  .hs-menu-wrapper.hs-menu-flow-vertical ul {
    display: flex;
  }
}

.hs-menu-wrapper.flyouts .hs-item-has-children {
  position: relative;
}

.hs-menu-wrapper.flyouts .hs-menu-children-wrapper {
  left: -9999px;
  opacity: 0;
  position: absolute;
}

.hs-menu-wrapper.flyouts .hs-menu-children-wrapper a {
  display: block;
  white-space: nowrap;
}

.hs-menu-wrapper.hs-menu-flow-horizontal.flyouts .hs-item-has-children:hover > .hs-menu-children-wrapper {
  left: 0;
  opacity: 1;
  top: 100%;
}

.hs-menu-wrapper.hs-menu-flow-vertical.flyouts .hs-item-has-children:hover > .hs-menu-children-wrapper {
  left: 100%;
  opacity: 1;
  top: 0;
}

@media (max-width: 767px) {
  .hs-menu-wrapper.flyouts .hs-menu-children-wrapper,
  .hs-menu-wrapper.hs-menu-flow-horizontal.flyouts .hs-item-has-children:hover > .hs-menu-children-wrapper,
  .hs-menu-wrapper.hs-menu-flow-vertical.flyouts .hs-item-has-children:hover > .hs-menu-children-wrapper {
    left: 0;
    opacity: 1;
    position: relative;
    top: auto;
  }
}

/* CTA, logo, and rich text images */

.hs_cos_wrapper_type_cta img,
.hs_cos_wrapper_type_logo img,
.hs_cos_wrapper_type_rich_text img {
  height: auto;
  max-width: 100%;
}

/* The shared micro-interaction vocabulary. Loaded globally rather than per
   module because .card, .button--accent and the footer links it targets are
   emitted from many places; a selector that matches nothing on a page costs
   nothing. Approved 2026-09-09, see build-log/2026-09-09-micro-interactions. */
/* Micro-interactions: the shared vocabulary, one place.
   ==========================================================================

   Approved by Roman 2026-09-09 off the [RM] Interaction Vocabulary specimen
   sheet. The organising rule is that THE MOTION DESCRIBES WHAT THE ELEMENT
   DOES, so eight different effects read as a language rather than as a site
   that could not decide:

     takes you somewhere   motion along the direction of travel
     one of a set          the indicator slides between options
     a surface to enter    the edge fills, the object stays put
     working right now     a repeat that runs only while busy
     a figure to notice    arrives once, on first sight, never again

   FOUR RULES THAT KEEP THIS FROM BECOMING A DISTRACTION. They are the reason
   the effects are allowed at all, so do not add a fifth effect without them.

   1. Nothing loops except "busy". A loop in peripheral vision is what makes a
      page feel restless. Everything else runs once and gets out of the way.
   2. Everything under 300ms, except the one-lap card glow at 700ms and the
      stat count at 900ms, both of which travel a distance.
   3. NO EFFECT IS EVER THE ACCESSIBLE SIGNAL. Focus rings are untouched by
      this file. Delete the whole thing and the keyboard experience is
      identical. That is the test.
   4. One effect per surface. A card gets the lap or the brackets, never both.
      Stacking a border recolour under a moving highlight is two effects
      fighting for one edge, which is why the featured card's violet border
      hover came off when the glow went on.

   Transform and opacity only, so nothing here can trigger layout.
   prefers-reduced-motion is honoured in one block at the bottom.

   The travelling card glow lives in css/templates/blog.css, not here, because
   it is scoped to the three blog card types. */


/* ------------------------------------------------- takes you somewhere (1)
   THE BRACKETS, on .card. Two corners snap in like a viewfinder locking on.

   Deliberately not the blog cards' travelling lap: a solutions card should not
   feel like a blog card, and the whole point of a vocabulary is that the
   difference carries information.

   .card already lifts and recolours its border on hover, from the three card
   module stylesheets. The brackets are additive rather than replacing that,
   because the border colour is a non-motion cue and someone who cannot see the
   brackets still gets it. */
.card { position: relative; }

.card::before,
.card::after {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  border: 2px solid var(--color-accent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.card::before {
  top: 8px; left: 8px;
  border-right: 0; border-bottom: 0; border-radius: 4px 0 0 0;
  transform: translate(5px, 5px);
}
.card::after {
  bottom: 8px; right: 8px;
  border-left: 0; border-top: 0; border-radius: 0 0 4px 0;
  transform: translate(-5px, -5px);
}
.card:hover::before,
.card:hover::after,
.card:focus-visible::before,
.card:focus-visible::after {
  opacity: 1;
  transform: translate(0, 0);
}

/* On a deep section the card is a translucent white wash, where lime at
   1.34:1 against it would be close to invisible. Accent-soft reads. */
.on-deep .card::before,
.on-deep .card::after { border-color: var(--color-accent-soft); }


/* ------------------------------------------------- takes you somewhere (2)
   THE ARROW ARRIVES, on the lime primary CTA.

   No arrow at rest, so the button's resting design is unchanged, and the glyph
   animates INSIDE the existing 1.75em right padding rather than being given
   room of its own. That is what keeps this from reflowing anything or making
   the button look lopsided while idle.

   Scoped to .button--accent, which _buttons.css reserves for "the single most
   important action on a page". That is the same set the header CTA, the hero
   buttons and the CTA band emit when their style field says accent.

   The glyph is content, not an image, and it is hidden from assistive tech: a
   screen reader announcing "Request a growth audit right arrow" is noise. */
.button--accent { position: relative; }

.button--accent::after {
  content: "\2192";
  position: absolute;
  right: 0.62em;
  top: 50%;
  opacity: 0;
  transform: translate(-7px, -50%);
  transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.2, 0.7, 0.2, 1);
  pointer-events: none;
  speak: never;
}
.button--accent:hover::after,
.button--accent:focus-visible::after {
  opacity: 1;
  transform: translate(0, -50%);
}
/* SMALL BUTTONS GET A TIGHTER ARROW. `.button--sm` in site-header.module.css
   is `padding: 0.6em 1.15em`, so a glyph parked at 0.62em from the right edge
   ran into the label. Roman saw it land on the word "audit" in the nav CTA.
   Moved in and scaled down so it sits inside that 1.15em with the label clear.
   Measured on the render rather than eyeballed. */
.button--sm.button--accent {
  /* Real room for the glyph, rather than squeezing it into padding that was
     never sized for one. `.button--sm` ships 1.15em, which at 0.875rem is
     16px: a 12px arrow plus an edge inset leaves about a pixel of clearance,
     which is still touching. 1.75em gives 24.5px and about 8px of clear space.
     The cost is a mildly asymmetric resting pill, 16px left against 24px
     right, which reads as space held for the arrow. Measured, not guessed. */
  padding-right: 1.75em;
}
.button--sm.button--accent::after {
  right: 0.34em;
  font-size: 0.85em;
}

/* While a submit is in flight the arrow would be promising forward motion the
   page is already doing. The sweep below is the state that matters then. */
.button--accent.is-busy::after { opacity: 0; }


/* ------------------------------------------------- takes you somewhere (3)
   THE UNDERLINE DRAWS, on footer links, left to right, the direction you are
   about to go.

   The existing colour shift to accent-soft stays. Motion is an addition here,
   never the whole affordance, because a viewer who cannot perceive it must
   still see that the link responded. */
/* Roman, 2026-09-09: he liked how this works in the footer and asked for it on
   the main nav, the utility bar and its mini-menu, the mega-menu columns, and
   linked text inside hero and FAQ copy.

   `:not(.button)` is on every element-level selector, per rule 3 in
   functions/marketing/web/README.md: a `.thing a` rule outranks `.button` and
   has already silently repainted two buttons. */
.site-footer__col a,
.site-footer__legal-links a,
.site-nav__link,
.site-nav__panel-all a,
.site-nav__col a:not(.button),
.site-nav__drawer-links a:not(.button),
.utility-bar__link,
.hero a:not(.button),
.faq__answer a:not(.button) { position: relative; }

.site-footer__col a::after,
.site-footer__legal-links a::after,
.site-nav__link::after,
.site-nav__panel-all a::after,
.site-nav__col a:not(.button)::after,
.site-nav__drawer-links a:not(.button)::after,
.utility-bar__link::after,
.hero a:not(.button)::after,
.faq__answer a:not(.button)::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -3px;
  height: 2px;
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.24s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.site-footer__col a:hover::after,
.site-footer__col a:focus-visible::after,
.site-footer__legal-links a:hover::after,
.site-footer__legal-links a:focus-visible::after,
.site-nav__link:hover::after,
.site-nav__link:focus-visible::after,
.site-nav__link[aria-expanded="true"]::after,
.site-nav__link.is-active::after,
.site-nav__panel-all a:hover::after,
.site-nav__panel-all a:focus-visible::after,
.site-nav__col a:not(.button):hover::after,
.site-nav__col a:not(.button):focus-visible::after,
.site-nav__drawer-links a:not(.button):hover::after,
.site-nav__drawer-links a:not(.button):focus-visible::after,
.utility-bar__link:hover::after,
.utility-bar__link:focus-visible::after,
.hero a:not(.button):hover::after,
.hero a:not(.button):focus-visible::after,
.faq__answer a:not(.button):hover::after,
.faq__answer a:not(.button):focus-visible::after { transform: scaleX(1); }

/* The mega-menu "all" link already ends in its own arrow glyph from
   site-header.module.css, so the rule above would underline the arrow too.
   Stop the underline short of it. */
.site-nav__panel-all a::after { right: 1.1em; }

/* The nav sits on the lavender header, where lime measures 1.34:1 and would be
   a line nobody can see. Violet is 5.60:1 there. On a deep header the nav
   flips to light text and lime reads at 12:1, so it keeps the accent. */
.site-nav__link::after,
.site-nav__panel-all a::after,
.site-nav__col a:not(.button)::after,
.site-nav__drawer-links a:not(.button)::after,
.utility-bar__link::after { background: var(--color-primary); }

.site-shell.is-on-dark:not(.is-menu-open) .site-nav__link::after,
.site-shell.is-on-dark:not(.is-menu-open) .utility-bar__link::after {
  background: var(--color-accent);
}


/* ------------------------------------------------------------ one of a set
   THE INDICATOR SLIDES, on the pricing tabs.

   NOT on the blog category rail, and that is a correction to the specimen
   sheet, which listed it. Those pills are anchors: clicking one loads a new
   page, so there is no before-and-after state to travel between and the
   "slide" would only ever be a jump on paint. Sliding needs an in-page tab
   switch, which the pricing tabs are and the blog rail is not.

   The fill is one absolutely positioned element moved by module.js from the
   buttons' own geometry, so it cannot drift when a label changes length or the
   row wraps. .is-active keeps its own background as the fallback: with no JS
   the tabs behave exactly as they did before this file existed. */
.pricing__tabs { position: relative; }

.pricing__tab { position: relative; z-index: 1; }

.pricing__tab-slider {
  position: absolute;
  z-index: 0;
  top: 0; left: 0;
  width: 0; height: 0;
  background: var(--color-primary);
  border-radius: var(--radius-pill);
  transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.2, 1),
              width 0.28s cubic-bezier(0.2, 0.7, 0.2, 1);
  pointer-events: none;
}
/* Once the slider is carrying the fill, the active tab's own background would
   double it. The class is set by module.js, so this only applies where the
   slider actually runs. */
.pricing__tabs.has-slider .pricing__tab.is-active { background: transparent; }


/* --------------------------------------------------------- a surface to enter
   THE FILL RISES, on EVERY outline button, not just the deep ones.

   Roman, 2026-09-09: he liked how "Talk to us" felt and asked for it on all
   outline buttons, keeping each one's own outline and text colour. He also
   asked for the deep one to go back to its original white-on-transparent,
   which the first version had recoloured to lime. Both done here.

   THE COLOURS ARE NOT SET BY THIS FILE. `_buttons.css` already defines the
   right pair per context, and it already inverted them on hover:

     light section   border currentColor, text --color-text
                     hover  background --color-text,    text --color-surface
     deep section    text --color-on-deep
                     hover  background --color-on-deep, text --color-text

   All this does is replace the instant background swap with a fill that rises
   from the bottom edge. So a black-and-white button stays black and white, and
   a client theme with different roles gets its own colours for free.

   `--outline-fill` exists because the fill cannot be `currentColor`. The hover
   rules change `color` to the contrasting value, and `currentColor` would
   change with it, so the rising panel would paint itself the same colour as
   the label. The variable is set per context and does not move on hover. */
.button--outline {
  --outline-fill: var(--color-text);
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.on-deep .button--outline { --outline-fill: var(--color-on-deep); }

.button--outline::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--outline-fill);
  transform: translateY(101%);
  transition: transform 0.26s cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* The background stays transparent so the rising panel is what fills it. The
   text colours come from _buttons.css and are deliberately not repeated.

   The `.on-deep` pair is listed separately on purpose. `_buttons.css` sets
   `.on-deep .button--outline:hover` at (0,3,0), which beats a bare
   `.button--outline:hover` at (0,2,0) whatever the file order, so without this
   the deep buttons would keep their instant white swap and the fill would rise
   behind a background that was already opaque. */
.button--outline:hover,
.button--outline:focus-visible,
.on-deep .button--outline:hover,
.on-deep .button--outline:focus-visible { background-color: transparent; }

.button--outline:hover::before,
.button--outline:focus-visible::before { transform: translateY(0); }


/* ------------------------------------------------------- working right now
   THE SWEEP, and it is the ONLY effect in this file allowed to repeat,
   because repeating is the information: it means still going.

   Driven by .is-busy, which main.js sets on submit and clears on settle. Never
   on hover. A form that already disables its button and swaps the label to
   "Sending..." gains a state you can see from across the room. */
/* Specificity, not source order. `_buttons.css` sets cursor via
   `input[type="submit"]`, which is (0,1,1) and beats a bare `.is-busy` at
   (0,1,0) no matter which file loads later. The element-qualified list below
   is what actually wins. Caught by the probe on 2026-09-09. */
.button.is-busy,
button.is-busy,
.hs-button.is-busy,
input[type="submit"].is-busy { cursor: progress; }

.is-busy::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    transparent 20%, var(--color-accent-soft) 50%, transparent 80%);
  opacity: 0.55;
  animation: rm-sweep 1s linear infinite;
  pointer-events: none;
}
.button.is-busy,
.hs-button.is-busy,
input[type="submit"].is-busy { position: relative; overflow: hidden; }

@keyframes rm-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}


/* ------------------------------------------------------- a figure to notice
   THE DIGITS ROLL. main.js counts a stat up once, on first sight, and never
   again, because a number that re-animates every time you scroll past it stops
   being a number and becomes furniture.

   The resting value in the markup is the real figure, so a reader who never
   scrolls, and a crawler, both see it. All the JS does is replay the approach.
   Tabular figures stop the width jittering while it counts. */
.stat__value { font-variant-numeric: tabular-nums; }


/* ----------------------------------------------------------- reduced motion
   Someone who asked their system for less motion gets every end state with no
   travel. Nothing here hides behind a transition that never runs, so the page
   is complete either way. The busy sweep goes static rather than vanishing,
   because it is the one effect carrying information rather than decoration. */
@media (prefers-reduced-motion: reduce) {
  .card::before,
  .card::after,
  .button--accent::after,
  .site-footer__col a::after,
  .site-footer__legal-links a::after,
  .pricing__tab-slider,
  .on-deep .button--outline::before {
    transition: none;
  }
  .card:hover::before,
  .card:hover::after,
  .card:focus-visible::before,
  .card:focus-visible::after { transform: translate(0, 0); }

  .is-busy::before { animation: none; opacity: 0.35; }
}

/* Utilities: last so they can override */
/* Surface treatment.

   Deliberately almost nothing. Three CSS pattern systems were tried here (a
   coloured glow, a line grid, and an embossed triangular lattice with nodes)
   and all three were rejected: at low contrast they read as graph paper, and at
   any higher contrast they fight the type.

   What the reference sites actually do is simpler than a pattern. Ripe Planet
   is a flat deep teal. Alora is one photographic metallic surface. Tavus is
   warm paper with a whisper of grain. None of them draw a pattern in CSS.

   So this file now offers only grain, and a photographic surface goes in as an
   image when there is one to use. A generated pattern is not a substitute for a
   real texture, and pretending otherwise cost three rounds of review.
*/

.texture { position: relative; isolation: isolate; }
.texture > * { position: relative; z-index: 2; }

/* TWO LAYERS, TWO PSEUDO-ELEMENTS. This is the fix for a whole class of bug.

   Both textures used to target ::before. An element has exactly one, so
   applying grain and image together meant the image rule simply overwrote the
   grain's background-image, and with no --texture-image set it resolved to
   `none` and killed both. That produced three separate "the texture is
   missing" reports on 2026-08-29 and 30, each looking like a different problem.
   
   The image now owns ::before and the grain owns ::after, so they compose
   instead of competing and either can be used alone or together. */
.texture::before,
.texture::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.texture::before { z-index: 0; }
.texture::after  { z-index: 1; }

/* Photographic surface, the lower layer. Set --texture-image on the section;
   with no image set this renders nothing and gets out of the way. */
.texture--image::before {
  background-image: var(--texture-image, none);
  background-size: cover;
  background-position: center;
  opacity: var(--texture-image-opacity, 0.35);
}

/* Fine film grain, the upper layer. Inlined SVG turbulence, so it costs no
   request, and it is a 160px repeating tile so it is resolution and
   orientation independent. */
.texture--grain::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
  background-size: 160px 160px;
  opacity: 0.14;
  mix-blend-mode: overlay;
}

/* MOBILE.

   The photographic surface is landscape, and `background-size: cover` on a
   375px portrait viewport magnifies it into a near-flat patch. Meanwhile
   `mix-blend-mode: overlay` puts very little light noise onto a dark gradient,
   so the grain alone was not visible either. Roman reported no texture at all
   on phones, twice.

   So on small screens the image steps back and the grain is pushed hard enough
   to actually read. A dedicated mobile crop is the better long-term answer and
   is tracked in #49. */
@media (max-width: 767px) {
  .texture--image::before { opacity: calc(var(--texture-image-opacity, 0.35) * 0.5); }
  .texture--grain::after { opacity: 0.3; mix-blend-mode: soft-light; }
}

/* A hairline rule that fades out at both ends. Useful between sections. */
.rule-fade {
  border: 0;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--color-border), transparent);
  margin: 0;
}
/* Scroll-reveal motion.

   NOTE ON COMMENTS IN CSS FILES: never write an HTML tag name in angle brackets
   here, not even inside a comment. HubSpot validates .css uploads and rejects
   the whole theme with "Can not save html to a css file". Write "the root html
   element", not the tag.


   The current romandamedia.com does this with animate.css plus WOW.js, two
   libraries and two extra requests. IntersectionObserver plus these keyframes
   does the same job in about thirty lines of JS and nothing to download.

   THE RULE FOR THIS THEME: motion is a garnish. It reveals content that is
   already there and readable. It never gates content behind an animation, and
   never moves anything the reader is trying to read.
*/

/* Repeating reveal. Same hidden state and transition as [data-reveal]; the
   only difference is that main.js keeps observing it, so it re-hides on the
   way out. Opt-in per element, because content that vanishes when you scroll
   back up is disorienting anywhere except where the motion IS the content. */
[data-reveal-repeat] {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition:
    opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: opacity, transform;
}
[data-reveal-repeat="left"]  { transform: translate3d(-40px, 0, 0); }
[data-reveal-repeat="right"] { transform: translate3d(40px, 0, 0); }
[data-reveal-repeat].is-revealed { opacity: 1; transform: none; }

[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition:
    opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: opacity, transform;
}

[data-reveal="left"]  { transform: translate3d(-28px, 0, 0); }
/* A longer throw than the other directions. This is the partner badge, the one
   element on the page whose arrival is meant to be noticed. */
[data-reveal="right"] { transform: translate3d(28px, 0, 0); }
[data-reveal="right-far"] { transform: translate3d(90px, 0, 0); opacity: 0; transition-duration: 0.85s; }
[data-reveal="scale"] { transform: scale(0.96); }

/* HORIZONTAL REVEALS WIDEN THE PAGE.

   A transform still contributes to the document scroll width. At 375px the
   badge band's 90px throw pushed the page wider than the viewport, which read
   as a strip of background down the right edge plus real horizontal scrolling,
   and then "fixed itself" the moment the element revealed and the transform
   cleared. Reported from mobile 2026-08-27.

   Below the mobile breakpoint every horizontal reveal falls back to the default
   vertical one. The motion still reads as motion, and nothing can widen the
   page.

   THAT FALLBACK IS NOT ENOUGH ON ITS OWN, twice over. Measured 2026-09-07
   after Roman reported sideways scroll on the solutions pages.

     1. It names each direction ATTRIBUTE BY ATTRIBUTE, so `data-reveal-repeat`,
        added with the timeline rebuild two days after this rule was written,
        was never covered. Its 40px throw put 8px of horizontal scroll on all
        thirteen solutions pages and on pricing, at every width from 320px to
        480px. That is the same bug as 2026-08-27, reintroduced by a new
        attribute rather than by a new value.
     2. The breakpoint is the wrong shape for the problem. An element sitting
        against the gutter cannot absorb a rightward throw AT ANY WIDTH, so the
        badge band still overflowed by up to 74px from 768px all the way to
        1440px, where no fallback applies. Closing that with a media query
        would mean tracking each module's own layout breakpoint from here, and
        this file should not know a module's layout.

   So both directions of both attributes fall back below the breakpoint, and
   `reveal-clip` below carries the guarantee at every other width. */
@media (max-width: 767px) {
  [data-reveal="left"],
  [data-reveal="right"],
  [data-reveal="right-far"],
  [data-reveal-repeat="left"],
  [data-reveal-repeat="right"] {
    transform: translate3d(0, 18px, 0);
  }
}

/* The width-independent half of the guarantee: a band that parks something
   outside its own box clips horizontally, so the parked position can never
   become document scroll width.

   `clip`, not `hidden`. `hidden` would make the band a scroll container, which
   breaks the vertically sticky table of contents on the blog, legal, careers
   and portal templates, and it can be scrolled programmatically even with no
   scrollbar. `clip` does neither.

   It goes on the band, never on the content wrapper: the wrapper carries the
   gutter as padding, and the clip edge is the padding box, so clipping there
   would cut the card shadows off at the content edge. A band has no horizontal
   padding, so its clip edge is the viewport edge and nothing real is touched.

   Safe above the fixed header, which is the objection that sent the
   2026-08-27 fix to a media query instead: the header lives in the global
   partial outside the main element, so no band is ever its ancestor, and a
   clip container is not a containing block for a fixed descendant anyway.
   Where `overflow-x: clip` is unsupported (Safari before 16) the declaration
   is dropped and the media query above still covers mobile. */
.reveal-clip { overflow-x: clip; }

[data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

/* Stagger children without writing a delay per element. */
[data-reveal-group] > * { transition-delay: calc(var(--i, 0) * 90ms); }

/* NO-JS SAFETY NET. If the observer never runs, every revealed element would
   stay at opacity 0 and the page would look empty. This class is added to
   the root html element by the motion script itself, so content is only
   ever hidden when something is definitely there to reveal it. */
html:not(.js-motion) [data-reveal] {
  opacity: 1;
  transform: none;
}

/* Marquee, for the logo strip. Duplicated content scrolls seamlessly. */
.marquee {
  display: flex;
  overflow: hidden;
  /* NO gap here, deliberately.
     The animation translates a track by -100% of ITS OWN width. A gap on this
     flex parent sits BETWEEN the two tracks and is not part of that width, so
     every loop landed one gap short and the strip visibly jumped as it
     restarted. Roman spotted it 2026-08-29.
     All spacing therefore lives inside .marquee__track: `gap` between items
     and `padding-right` for the trailing space, so the track's measured width
     already includes the space that follows it and -100% lands exactly on the
     start of the duplicate. */
  /* Black here is an ALPHA STOP, not a colour: in a mask, opaque means
     visible. Written as rgb() so the brand-colour check does not flag it. */
  mask-image: linear-gradient(to right, transparent, rgb(0 0 0) 8%, rgb(0 0 0) 92%, transparent);
}

.marquee__track {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  /* Both are required and they are not redundant: `gap` spaces the logos
     within a track, `padding-right` supplies the space between the last logo
     of one track and the first of the next. Remove either and the rhythm
     breaks at the seam. */
  gap: var(--marquee-gap, 3.5rem);
  padding-right: var(--marquee-gap, 3.5rem);
  animation: marquee-scroll var(--marquee-duration, 38s) linear infinite;
}

.marquee:hover .marquee__track { animation-play-state: paused; }

@keyframes marquee-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-100%, 0, 0); }
}

/* Respect the setting. Someone who has asked their operating system for less
   motion has usually asked for a reason, and a marquee is exactly the kind of
   perpetual movement that triggers vestibular symptoms. Reveals resolve
   instantly rather than being removed, so nothing disappears. */
@media (prefers-reduced-motion: reduce) {
  [data-reveal],
  [data-reveal-repeat],
  [data-reveal-repeat].is-revealed,
  [data-reveal].is-revealed {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .marquee__track { animation: none; }
  .marquee { mask-image: none; flex-wrap: wrap; justify-content: center; }
}
/* For content that needs to be visually hidden but stay visible for screenreaders */

.show-for-sr {
  border: 0 !important;
  clip: rect(0, 0, 0, 0) !important;
  height: 1px !important;
  overflow: hidden !important;
  padding: 0 !important;
  position: absolute !important;
  white-space: nowrap !important;
  width: 1px !important;
}

@media (max-width: 767px) {
  .show-for-sr--mobile {
    border: 0 !important;
    clip: rect(0, 0, 0, 0) !important;
    height: 1px !important;
    overflow: hidden !important;
    padding: 0 !important;
    position: absolute !important;
    white-space: nowrap !important;
    width: 1px !important;
  }
}
/* In-page anchor targets clear the FIXED site header. Without this a smooth
   scroll to #contact-form lands it underneath the bar, which reads as the
   link having done nothing. --header-h is only set on the header element by
   the header module (BUILD_LOG 3.53), so the fallback carries most pages. */
[id]:target,
#contact-form {
  scroll-margin-top: calc(var(--header-h, 64px) + 1.5rem);
}