/* Reset + typography + a11y primitives */
*, *::before, *::after { box-sizing: border-box; }

* { margin: 0; }

html {
	font-size: 16px;
	-webkit-text-size-adjust: 100%;
}

@media (prefers-reduced-motion: no-preference) {
	html { scroll-behavior: smooth; }

	/* Cross-document view transitions: the browser cross-fades one page into the
	   next instead of flashing white between them. This is a classic multi-page
	   site, so it is the single largest change available to how *moving between
	   pages* feels, and it costs one at-rule — browsers without support ignore it
	   entirely, and there is no script, no dependency and no build step.

	   Gated on no-preference like every other motion in the theme. It must not be
	   moved out of this block: the transition is a full-page animation, and it is
	   exactly what someone asking for reduced motion is asking not to see.

	   animations.js pairs with this — it listens for `pagereveal` and, when the
	   page was entered through a transition, reveals the above-fold batch
	   instantly rather than replaying the entrance on top of the cross-fade.

	   TO REVERT: delete this at-rule. The JS half then never sees a transition,
	   reads false, and behaves exactly as it did before. */
	@view-transition { navigation: auto; }
}

body {
	font-family: var(--font-body);
	font-size: var(--text-base);
	line-height: var(--leading-normal);
	color: var(--color-body);
	background: var(--color-white);
	-webkit-font-smoothing: antialiased;
}

img, picture, svg, video, iframe { display: block; max-width: 100%; }

img { height: auto; }

input, button, textarea, select { font: inherit; }

h1, h2, h3, h4 {
	font-family: var(--font-heading);
	line-height: var(--leading-tight);
	font-weight: 700;
	overflow-wrap: break-word;
}

p { overflow-wrap: break-word; }

a { color: var(--color-primary); }

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

ul[class], ol[class] { list-style: none; padding: 0; }

/* Focus — always visible, high contrast on both light and dark surfaces */
:focus-visible {
	outline: 2px solid var(--focus-ring-color);
	outline-offset: 2px;
	border-radius: 2px;
}

.on-dark :focus-visible,
.section--dark :focus-visible {
	outline-color: var(--color-white);
}

/* Skip link — first tabbable element, visible on focus above the fixed nav */
.skip-link {
	position: fixed;
	top: 0.5rem;
	left: 0.5rem;
	z-index: 1000;
	padding: 0.75rem 1.25rem;
	border: 2px solid var(--color-border-dark);
	border-radius: var(--radius-sm);
	background: var(--color-dark);
	color: var(--color-white);
	box-shadow: var(--shadow-stamp-btn-red);
	text-decoration: none;
	transform: translateY(-200%);
}

.skip-link:focus-visible {
	transform: none;
	color: var(--color-white);
}

.visually-hidden {
	position: absolute !important;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* Icons inherit text color/size context */
.icon {
	width: 1.25em;
	height: 1.25em;
	flex-shrink: 0;
	stroke-width: 2;
}

/* Rebel Sketch icons — decorative artwork, never CSS-tinted. Traced to SVG, so
   the old "never larger than the 112px master" cap no longer applies. Still not
   tintable: each file carries its own fixed ink and brand colours, which is why
   there are separate light/ and dark/ sets rather than one recoloured set. */
.sketch-icon {
	flex-shrink: 0;
	image-rendering: auto;
}

/* Buttons — V2 "Structured Stamp": flat fill, hairline border, hard under-shadow.
   Hover lifts the stamp, :active presses it flat (the shadow absorbs the travel). */
.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 0.65rem;
	padding: 0.875rem 1.75rem;
	border: 2px solid rgba(0, 0, 0, 0.10);
	border-radius: var(--radius-sm);
	font-weight: 600;
	font-size: var(--text-base);
	text-decoration: none;
	cursor: pointer;
	transition: transform var(--transition), box-shadow var(--transition), background-color var(--transition), color var(--transition), border-color var(--transition);
}

/* Keyboard parity: focused buttons get the same visual state as hovered. */
.btn:hover,
.btn:focus-visible { transform: translateY(-2px); }

/* Pressed: the stamp goes down into the page. */
.btn:active {
	transform: translateY(3px);
	transition-duration: var(--transition-press);
}

/* Primary-CTA emphasis: a step larger than neighboring buttons. */
.btn--lg {
	padding: 1.05rem 2.1rem;
	font-size: var(--text-lg);
}

/* Quiet secondary for dark surfaces: outlined ghost, no stamp shadow —
   deliberately lighter than the primary so hierarchy reads as intended.
   Border alpha is set by the worst case for a dark surface here: composited
   over #C41C1C (the lightest stop of every dark gradient) 0.65 holds 3.21:1,
   where the old 0.35 fell to 1.77:1 and read as a disabled twin. Quietness
   now comes from having no fill and no stamp shadow, not from a faint edge. */
.btn--quiet {
	background: transparent;
	color: var(--color-white);
	border-color: rgba(255, 255, 255, 0.65);
	box-shadow: none;
}

.btn--quiet:hover,
.btn--quiet:focus-visible {
	background: rgba(255, 255, 255, 0.08);
	border-color: var(--color-white);
	color: var(--color-white);
}

.btn__divider {
	width: 2px;
	align-self: stretch;
	background: currentColor;
	opacity: 0.35;
	border-radius: 1px;
}

.btn--gradient {
	background: var(--color-primary);
	color: var(--color-white);
	box-shadow: var(--shadow-stamp-btn);
}

.btn--gradient:hover,
.btn--gradient:focus-visible {
	color: var(--color-white);
	box-shadow: 0 7px 0 var(--color-dark);
}

.btn--gradient:active { box-shadow: 0 2px 0 var(--color-dark); }

/* Transparent fill, so the offset is the only thing giving it a body. Red ink:
   this variant only ever sits on light surfaces. */
.btn--outline {
	box-shadow: var(--shadow-stamp-btn-red);
	background: transparent;
	color: var(--color-primary);
	border-color: var(--color-primary);
}

.btn--outline:hover,
.btn--outline:focus-visible { background: var(--color-tint); color: var(--color-primary-deep); box-shadow: 0 7px 0 var(--color-primary); }
.btn--outline:active { box-shadow: 0 2px 0 var(--color-primary); }

/* White stamp with the red under-shadow (secondary CTA on any surface). */
.btn--light,
.btn--ghost-dark {
	background: var(--color-white);
	color: var(--color-primary);
	box-shadow: var(--shadow-stamp-btn-red);
}

.btn--light:hover,
.btn--light:focus-visible,
.btn--ghost-dark:hover,
.btn--ghost-dark:focus-visible {
	color: var(--color-primary-deep);
	box-shadow: 0 7px 0 var(--color-primary);
}

.btn--light:active,
.btn--ghost-dark:active { box-shadow: 0 2px 0 var(--color-primary); }

.btn .icon,
.btn .sketch-icon { transition: transform var(--transition); }

.btn:hover .icon--arrow-right,
.btn:hover .sketch-icon--arrow-right,
.btn:focus-visible .sketch-icon--arrow-right { transform: translateX(4px); }

/* ===== Entrance reveals =====
   The hidden start state exists ONLY under html.js-motion, which the motion
   layer adds when it has both a working IntersectionObserver and no
   reduced-motion preference. So with JS off, with JS broken, or with motion
   reduced, .reveal carries no opacity and no translate of its own: full
   visibility is the default state, not something JS has to restore. */
.js-motion .reveal {
	opacity: 0;
	translate: 0 var(--reveal-shift);
}

.js-motion .reveal.is-revealed {
	opacity: 1;
	translate: none;
}

/* :where() drops this to .reveal's own specificity on purpose: several reveal
   targets are cards that must keep their own 200ms hover transition list, so
   those lists spell these same two legs out and win here on source order.
   Anything without a transition of its own picks the reveal timing up from
   this rule.

   Everything is spelled out — there is deliberately no composed
   --reveal-transition token. A custom property substitutes its inner var()s
   where IT is defined, so a :root token would freeze var(--reveal-duration)
   at its base value for every role, and var(--reveal-delay) at 0s the same
   way. At a real use site the vars resolve
   per element, which is what lets the role durations below and the per-element
   stagger delay work at all.

   var(--reveal-delay, 0s) is the scroll-reveal stagger, set inline per element
   by the motion layer. It sits ONLY on these two legs on purpose: the stagger
   used to be an inline transition-delay, which applies to every leg of an
   element's transition list — so for up to 1.24s after a card scrolled into
   view, its hover lift would be postponed by up to 240ms — the card sits
   fully settled and looking interactive while ignoring hovers. A delay scoped
   to the reveal's own legs cannot reach a hover. */
:where(.js-motion) .reveal {
	transition: opacity var(--reveal-duration) ease-out var(--reveal-delay, 0s), translate var(--reveal-duration) var(--reveal-ease) var(--reveal-delay, 0s);
}

/* Three roles, not one. Every reveal on this site used to be the same 14px
   fade-rise, so a section header, a sixteen-card grid and a full-width slab all
   moved identically — roughly one every 150px of scroll on the dense pages, which
   reads as generic rather than deliberate. The variants below only override the
   two custom properties the shared rules already read, so they cost no extra
   declarations and inherit every guarantee above (reduced-motion bail, JS-off
   visibility, the transition-list precedence for cards that own a hover).

   --lead   the element that ANNOUNCES a topic: section headers and page heroes.
            Further and slower, because it is what a scrolling reader navigates
            by, and it usually arrives alone rather than in a batch.
   (base)   one card among peers. Unchanged, and still staggered per row.
   --settle a full-width slab, table or panel. Fade only: a 14px slide on a
            700px-wide block reads as the layout shifting rather than as an
            entrance, and it is the one place the motion looked cheap. */
.js-motion .reveal--lead {
	--reveal-shift: 22px;
	--reveal-duration: 560ms;
}

.js-motion .reveal--settle {
	--reveal-shift: 0px;
	--reveal-duration: 420ms;
}

/* The hero entrance is deliberately NOT gated on js-motion — driving it from
   the deferred script holds the LCP element back until that script runs — and
   it is deliberately TRANSLATE-ONLY. Starting the keyframe at opacity 0 takes
   the H1 out of LCP candidacy altogether, because Chrome does not re-register
   content whose first paint happens inside an opacity-0 frame, and LCP then
   falls through to a less meaningful element. A rise without a fade keeps the
   entrance and keeps the headline as the LCP element. */
@media (prefers-reduced-motion: no-preference) {
	.reveal--hero {
		animation: reveal-rise var(--reveal-duration, 550ms) var(--reveal-ease, ease-out) both;
	}

	.reveal--hero:nth-of-type(2) { animation-delay: 120ms; }
}

/* Named for what it does rather than for the hero, because the boot batch below
   shares it. Deliberately has no `opacity` — see both comments around it. */
@keyframes reveal-rise {
	from {
		translate: 0 var(--reveal-shift);
	}
}

/* The boot batch — reveals already inside the first viewport when the deferred
   motion script runs — animates by KEYFRAME, not by the class toggle above.
   It has to. js-motion and is-revealed land on those elements in the same
   frame, so the browser never observes a change in opacity or translate and
   starts no transition at all: they jump straight to their final values.
   A keyframe needs no previously committed start state, which is why the hero
   entrance never had this problem. Same reason it is gated on the
   reduced-motion query rather than on js-motion: the animation defines its own
   `from`, so a page that never gets js-motion still renders at rest.

   It shares the hero's keyframe, and therefore its no-opacity rule. That rule
   is not a hero quirk — it is a property of anything painting above the fold.
   Chrome withholds an element's LCP entry until the `animation` property is
   REMOVED, so any fade applied above the fold pays the whole animation plus
   its cleanup slack straight into LCP, and an element that merely looks
   settled is not yet measured. Do not assume which element Chrome will pick
   either: a base-role card at boot index 0 can hold the LCP image just as
   easily as the hero block.
   So: no fade above the fold, for anything. */
@media (prefers-reduced-motion: no-preference) {
	.js-motion .reveal--boot {
		animation: reveal-rise var(--reveal-duration) var(--reveal-ease) both;
	}

	/* --settle is fade-only by design: zero travel, because a slide on a
	   full-width slab mid-scroll reads as the layout shifting rather than as an
	   entrance. Above the fold it cannot have the fade, so without a little
	   travel it would be the one element on the site still snapping. Exactly one
	   --settle lands in a boot batch site-wide — the contact form card — and at
	   load, before anything has scrolled, there is no established position for a
	   short rise to look like it is shifting away from. */
	.js-motion .reveal--boot.reveal--settle {
		--reveal-shift: 10px;
	}
}

/* One frame, while the motion layer stamps the hidden start state on
   everything the observer still owns. Without it those ~50 below-fold elements
   each run a 480ms fade-out the instant js-motion lands — off-screen work, on
   the main thread, during the page's worst moment. The start state is supposed
   to be instant; only the reveal is supposed to ease.
   !important because the reveal targets that are cards carry their own hover
   transition list, which includes the reveal properties and would otherwise
   still animate the hide. Removed on the next animation frame. */
.js-motion-arming .reveal {
	transition: none !important;
}

/* V2: emphasized heading segments are flat brand red (no gradient clip). */
.text-gradient { color: var(--color-primary); }

/* On dark surfaces brand red is only ~2.4:1, so the accent switches to the
   on-dark red. Same value sections.css gives .hero/.solution/.demo-section
   heading spans, so the two never disagree where they overlap. */
.section--dark .text-gradient,
.on-dark .text-gradient { color: var(--color-accent-on-dark); }

/* Reduced motion: kill everything, including any looping pulse. */
@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;
	}

	/* The motion layer refuses to set js-motion when motion is reduced, but the
	   preference can also flip *after* load, with the class already on <html>.
	   The blanket rule above would then collapse every reveal transition to
	   0.01ms and strand un-revealed elements at opacity 0, so neutralise the
	   start state outright rather than relying on a transition to undo it. */
	.js-motion .reveal {
		opacity: 1;
		translate: none;
	}

	/* The arrow shift is a direction affordance, not decoration: keep a damped
	   version while the card/button lifts stay suppressed (components.css). */
	.btn:hover .icon--arrow-right,
	.btn:hover .sketch-icon--arrow-right,
	.btn:focus-visible .sketch-icon--arrow-right { transform: translateX(2px); }
}

/* ===== Forced colors (Windows High Contrast) =====
   The design language is red fills, hairline borders and hard shadows — forced
   colors strips the fills and shadows, which erases any state carried by
   colour alone:
   - the sub-nav's current chip computes identical to its neighbours, and its
     icon filter (brightness(0) invert(1) = white ink for the red fill that no
     longer exists) would render white-on-white in the light palette;
   - active filter pills have the same colour-only state.
   Selection markers below are shape, not colour — a double border plus an
   underline — so they survive any palette. System colours are left to the
   palette; nothing here uses forced-color-adjust: none. */
@media (forced-colors: active) {
	.sub-nav__link[aria-current="true"],
	.category-pill.is-active,
	.category-pill[aria-pressed="true"],
	.filter-pills [aria-pressed="true"] {
		border: 3px double CanvasText;
		text-decoration: underline;
	}

	/* The divider inside buttons was a background, which forced colors strips. */
	.btn__divider { background: CanvasText; }
}

/* ===== Forced colours (Windows High Contrast) =====
   These icons are <img> files with baked-in fills, and forced-colors does not
   recolour image pixels, so a filter is the only lever available. There are two
   ink sets and they need opposite treatment, which is what the earlier version of
   these rules got wrong: it inverted bare `.sketch-icon`, so it fixed the navy
   set and simultaneously broke the white one, on both palettes.

   The two sets do not overlap, which is what makes one rule enough. Since the
   generated set replaced the traced one the palette is locked rather than
   averaged, so this is now exact rather than measured: light/ inks #181111
   (relative luminance 0.006) and dark/ inks #FCFBFA (0.966). The rule is simply:
   invert whichever set would otherwise match its Canvas.

   The classes are named for the ink, not the directory, because the directories
   are named for the surface — dark/ holds white ink — and that inversion is what
   made the original rules read as correct. The name `ink-navy` is now a slight
   lie about the hue (#181111 is a warm near-black, not navy) but it is load
   bearing: it is the contract between rebelpsa_sketch_icon() and these rules,
   and renaming it buys nothing. See rebelpsa_sketch_icon(). */

/* Light Canvas: white ink would be white-on-white. Navy already reads. */
@media (forced-colors: active) and (prefers-color-scheme: light) {
	.sketch-icon--ink-white { filter: invert(1) hue-rotate(180deg); }

	/* The current sub-nav chip loses its dark fill in forced colours, so the
	   brightness(0) invert(1) that made its icon white must go too — the mark's
	   own ink reads on Canvas. One class deeper than the pages.css rule that sets
	   that filter: that sheet loads after this one, so equal specificity would
	   lose on source order. It also outranks the ink rule above, which is correct
	   — a chip icon needs no inversion once the fill behind it is gone. */
	.sub-nav .sub-nav__link[aria-current="true"] .sketch-icon { filter: none; }
}

/* Dark Canvas: navy ink would be black-on-black. On the comparison table the
   check/x glyph is the only visual verdict, so verdicts vanished entirely.
   hue-rotate keeps the red marker strokes red instead of cyan. White ink is
   deliberately untouched here — inverting it was the bug. */
@media (forced-colors: active) and (prefers-color-scheme: dark) {
	.sketch-icon--ink-navy { filter: invert(1) hue-rotate(180deg); }

	/* The chip's own brightness(0) invert(1) is higher-specificity and stays,
	   which is right on a dark Canvas: pure white. */
}

/* ===== Print =====
   Three things break on paper without the rules below: a below-fold reveal is
   still at opacity 0 during print layout because the observer never fires
   there, so its content is silently lost; dark gradient bands print
   white-on-white once backgrounds are stripped; and fixed chrome stamps itself
   over every page after the first. */
@media print {
	/* Content-loss guard first: no reveal may be hidden on paper. */
	.js-motion .reveal {
		opacity: 1 !important;
		translate: none !important;
		transition: none !important;
	}

	/* Chrome repeats fixed/sticky boxes on every page box; none of these are
	   usable on paper. Hiding the footer drops a near-blank final page from
	   every document. */
	.site-header,
	.skip-link,
	.site-footer,
	.sub-nav,
	.legal-layout__sidebar,
	.btn { display: none !important; }

	.site-main { padding-top: 0 !important; }

	/* Dark surfaces flatten to ink once their backgrounds are stripped.
	   Targeted at the cascade root rather than surface by surface: one rule,
	   layout.css's `.section--dark { color: var(--color-on-dark) }`, is what
	   whitens the ink on .hero, .solution, .demo-section, .benefits-hero,
	   .benefits-cta, .blog-cta, .cta-panel and .demo-page__form. Listing those
	   individually is a list that drifts every time a section is added; covering
	   .section--dark and .on-dark covers all of them and stays covered.
	   The three below it set their own white outside that cascade, so they still
	   need naming. */
	.section--dark,
	.section--dark *,
	.on-dark,
	.on-dark *,
	.page-hero--dark,
	.page-hero--dark *,
	.benefits-hero,
	.benefits-hero *,
	.blog-single__hero,
	.blog-single__hero *,
	.comparison-table thead,
	.comparison-table thead *,
	.pricing-includes,
	.pricing-includes *,
	.legal-document__updated,
	.legal-document__updated * {
		color: #1a1a1a !important;
		background: none !important;
		box-shadow: none !important;
	}

	/* Accent-on-dark resolves to a mid red that prints as grey mush on white, and
	   the gradient text is transparent-filled — both need the flat ink above to
	   actually win. */
	.section--dark a,
	.on-dark a,
	.section--dark .text-gradient,
	.on-dark .text-gradient {
		color: #1a1a1a !important;
		-webkit-text-fill-color: #1a1a1a !important;
		background: none !important;
	}

	/* Torn-seam strips, textures, dot fields and blur glows are all flat colour
	   bars without their masks/backgrounds — decorative on screen, noise on paper.
	   The glows in particular print as large grey rectangles. */
	.page-hero--dark::before,
	.page-hero--dark::after,
	.site-footer::before,
	.benefits-hero::after,
	.hero::before,
	.hero::after,
	.section--dots::before,
	.solution::before,
	.solution::after,
	.benefits-section::before,
	.blog-single__hero::before,
	.blog-single__hero::after,
	.blog-cta::before,
	.benefits-cta::before,
	.demo-page__form::after,
	.hero__glow,
	.demo-section__glow { display: none !important; }
}
