/* Spatial roles: related copy < component groups < sections. See DESIGN.md. */
:root {
  --space-related: .75rem;
  --space-group: 1.5rem;
  --space-block: 2rem;
  --space-section: clamp(3rem, 6vw, 5rem);
  --space-gutter: clamp(1.25rem, 4vw, 2rem);
  --space-card: clamp(1.25rem, 3vw, 2rem);
  --measure-copy: 68ch;
}

/* ============================================================
   SAFFRON AUTOMATIONS — PREMIUM FOUNDATION (premium.css)

   One shared layer for the things that must be true on EVERY page and
   were true on only some of them. Audited 2026-09-06:
     · 13 shipping pages had no prefers-reduced-motion block
     · 13 had no :focus-visible treatment
     · 27 had no print styles
   Thirty-one one-line includes beat thirty-one divergent copies: a fix
   here lands everywhere, which is the whole reason this file exists
   rather than another paste into each <style>.

   Everything is written at :where()/low specificity so any page's own
   rule still wins. This layer raises the floor; it never overrides a
   deliberate design decision above it.
   ============================================================ */

/* ── 1. MOTION CONSENT ───────────────────────────────────────
   A visitor who has asked their OS to reduce motion has asked once,
   for everything. This is deliberately the blunt instrument: !important
   and the universal selector, because a single un-neutralised keyframe
   is the whole promise broken. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  /* Anything staged for a reveal must land visible, never stranded at
     opacity 0 because its transition was neutralised. */
  .sfa-reveal { opacity: 1 !important; transform: none !important; }
}

/* ── 2. KEYBOARD VISIBILITY ──────────────────────────────────
   :focus-visible only, so a mouse click does not draw a ring while a
   Tab press always does. Two rules, because a focus ring that sits
   under an adjacent element is a focus ring nobody can see. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--gold, #C9A96E);
  outline-offset: 3px;
  border-radius: 2px;
  /* Preserve fixed/sticky controls and skip links when they receive focus. */
}
/* Browsers that support :focus-visible should not also paint the legacy
   ring; those that do not keep :focus as their fallback. */
@supports selector(:focus-visible) {
  :where(a, button, input, select, textarea, summary, [tabindex]):focus:not(:focus-visible) {
    outline: none;
  }
}

/* ── 3. TOUCH ────────────────────────────────────────────────
   44x44 is the floor, met without forcing a height onto elements that
   should grow with their text. Inline links inside prose are exempt:
   padding there would break the line box, and the WCAG target-size
   rule excludes them for exactly that reason. */
/* min-height only. The min-width that used to sit here broke /schedule's
   calendar: seven day buttons in a 7-column grid resolved to 40px each, this
   rule forced them to 44, and 7x44 + six 6px gaps = 344px inside a 316px
   column — the Saturday column pushed 28px off the screen. Measured on the
   rendered page, and it is exactly the regression the guidance warns about:
   a blanket tap-target rule breaking a horizontal layout. Height never fights
   a horizontal grid; width always can. Width is handled by the ::after
   hit-area in §8, which grows the target without touching the box.

   THE REMAINING TRAP, and it caught me a second time on the same element:
   min-height is NOT safe on a button that carries an explicit aspect-ratio.
   /schedule's .day is aspect-ratio:1/1, so the 44px floor set the height and
   the ratio dragged the width up with it — the identical 28px overflow, from
   the property I had just called harmless. An element that is square by
   declaration has one dimension, not two. Such elements opt out locally with
   min-height:0; CSS cannot express "unless the author set an aspect-ratio". */
:where(button, [role="button"], input[type="submit"], input[type="button"], .btn) {
  min-height: 44px;
  touch-action: manipulation; /* removes the 300ms tap delay */
}
/* Tap feedback: a press should register instantly, before any network
   or route change. Kept to a 1px settle so it reads as physical rather
   than as a bounce. */
:where(a, button, .btn):active { transform: translateY(1px); }
@media (prefers-reduced-motion: reduce) {
  :where(a, button, .btn):active { transform: none; }
}
/* iOS paints its own grey flash over :active; ours is enough. */
:where(a, button, .btn, [role="button"]) { -webkit-tap-highlight-color: transparent; }

/* ── 4. SMALL-SCREEN CORRECTNESS ─────────────────────────────
   Safari on iOS inflates text in landscape unless told not to, and a
   rubber-band scroll past the end of a dark page shows the browser's
   own white ground unless the body claims it. */
html {
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
}
/* Anything pinned to an edge has to clear the notch and the home
   indicator. Applied to the roles that actually pin, not globally. */
:where(.sfa-header, .deck-masthead, [data-sticky]) {
  padding-left: max(0px, env(safe-area-inset-left));
  padding-right: max(0px, env(safe-area-inset-right));
}
:where(.sfa-safe-bottom) {
  padding-bottom: max(var(--s4, 1rem), env(safe-area-inset-bottom));
}
/* An image with no intrinsic ratio is the most common source of layout
   shift on this site's cards. */
:where(img, svg, video) { max-width: 100%; height: auto; }

/* ── 5. SCROLL CHOREOGRAPHY ──────────────────────────────────
   The staged state is applied by premium.js, never by this file alone.
   With JS off there is no .sfa-js on <html>, so .sfa-reveal never goes
   transparent and the page renders complete — a reveal system that can
   hide content permanently is worse than no reveal system. */
.sfa-js .sfa-reveal {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .5s cubic-bezier(.22,.61,.36,1),
              transform .5s cubic-bezier(.22,.61,.36,1);
  will-change: opacity, transform;
}
.sfa-js .sfa-reveal.sfa-in {
  opacity: 1;
  transform: none;
  will-change: auto; /* released once the element has landed */
}

/* ── 6. PRINT ────────────────────────────────────────────────
   A dark page printed as-is costs a cartridge and reads worse than the
   screen. Ink-on-paper, navigation dropped, link targets spelled out. */
@media print {
  :where(html, body) { background: #fff !important; color: #111 !important; }
  :where(.sfa-header, .deck-masthead, nav, .skip, footer nav, [data-sticky]) { display: none !important; }
  :where(a) { color: #111 !important; text-decoration: underline; }
  /* A printed page cannot be clicked, so the destination goes inline —
     but only for real off-page links, not fragments or javascript:. */
  :where(main, article) a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .85em; word-break: break-all; }
  :where(main, article) a[href^="/"]::after { content: " (saffronautomations.com" attr(href) ")"; font-size: .85em; }
  :where(.sfa-reveal) { opacity: 1 !important; transform: none !important; }
  :where(h1, h2, h3) { break-after: avoid; }
  :where(p, li, blockquote) { orphans: 3; widows: 3; }
}

/* ── 7. THE INTERACTION LAYER ────────────────────────────────
   Reusable primitives, not one-off hero decoration. Anything on the
   site can take .sfa-spot / .sfa-grain / .sfa-lift and behave the same
   way, which is the point: cohesion comes from shared parts, not from
   thirty pages each inventing their own hover. */

/* Pointer-tracked spotlight. premium.js writes --mx/--my; with no JS the
   fallback puts the light at the top-centre, which still reads as lit
   rather than broken. Pure paint — no layout, no reflow. */
.sfa-spot { position: relative; isolation: isolate; }
.sfa-spot::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  border-radius: inherit;
  background: radial-gradient(
    420px circle at var(--mx, 50%) var(--my, 0%),
    rgba(201,169,110,.13), rgba(201,169,110,.04) 38%, transparent 65%);
  opacity: 0; transition: opacity .4s var(--deck-ease, cubic-bezier(.2,.8,.2,1));
}
.sfa-spot:hover::after,
.sfa-spot:focus-within::after { opacity: 1; }

/* Film grain. Two facts make this cheap: feTurbulence renders once into a
   data URI, and the tile repeats. Kept at 3.5% — a texture you notice
   only when it is removed, which is the whole idea. Screens with a high
   DPR resolve the tile smaller, so the grain stays fine rather than
   turning into visible dots. */
.sfa-grain { position: relative; isolation: isolate; }
.sfa-grain::before {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  border-radius: inherit;
  opacity: .035; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
  background-size: 140px 140px;
}
@media (min-resolution: 2dppx) { .sfa-grain::before { background-size: 90px 90px; opacity: .028; } }

/* Lift. transform and box-shadow only — both composited, neither reflows.
   2px, because a card that jumps announces itself and a card that rises
   feels made. */
.sfa-lift {
  transition: transform .28s var(--deck-ease, cubic-bezier(.2,.8,.2,1)),
              border-color .28s var(--deck-ease, cubic-bezier(.2,.8,.2,1)),
              box-shadow .28s var(--deck-ease, cubic-bezier(.2,.8,.2,1));
}
.sfa-lift:hover, .sfa-lift:focus-visible {
  transform: translateY(-2px);
  border-color: rgba(201,169,110,.42);
  box-shadow: 0 14px 40px -18px rgba(201,169,110,.35), 0 2px 8px -4px rgba(0,0,0,.5);
}
.sfa-lift:active { transform: translateY(0); }

/* A hairline that draws from the centre outward as the card arrives.
   Implemented as a background LAYER, not a pseudo-element: .sfa-grain
   already owns ::before and .sfa-spot owns ::after, and an element has
   only one of each — the first version of this rule silently cost every
   card its grain. background-size on a 1px positioned layer is paint
   only, no reflow, so it stays as cheap as the transform it replaces. */
.sfa-hair {
  background-image: linear-gradient(90deg, transparent, var(--deck-gold, #C9A96E), transparent);
  background-repeat: no-repeat;
  background-position: 50% 0;
  background-size: 0% 1px;
  transition: background-size .7s var(--deck-ease, cubic-bezier(.2,.8,.2,1));
}
.sfa-in .sfa-hair, .sfa-hair.sfa-in { background-size: 100% 1px; }

@media (prefers-reduced-motion: reduce) {
  .sfa-spot::after { display: none; }
  .sfa-lift:hover, .sfa-lift:focus-visible { transform: none; }
  .sfa-hair { background-size: 100% 1px; transition: none; }
}

/* ── 8. TOUCH TARGETS THAT DO NOT MOVE THE LAYOUT ────────────
   Measured at 390px in a real browser (scripts/qa-viewport.mjs), not guessed:
   the masthead brand is 230x36 on fifteen pages, the tier mastheads 207x34,
   .mast-cta 108x42, .deck-why-verify 316x33, .back 198x24, and a run of footer
   links at 16-23px tall. All are wide enough and all are too short.

   The obvious fix — min-height on every link — is the one to avoid. It changes
   line boxes inside navs, pushes mastheads taller, and a blanket tap-target
   rule has regressed link widths on this site before. So the HIT AREA grows
   and the box does not: a transparent ::after, vertically centred, 44px tall.
   The thumb gets its target, the design keeps its proportions, and nothing
   reflows.

   Scoped to chrome — mastheads, navs, footers, back links. Links inside prose
   are deliberately untouched: WCAG exempts them, and an overlay there would
   sit over neighbouring words. */
:where(header, nav, footer, .sfa-header, .deck-masthead) a:not(:where(p a, li a, blockquote a)),
:where(.sfa-brand, .brand, .mast-cta, .back, .deck-why-verify) {
  position: relative;
}
:where(header, nav, footer, .sfa-header, .deck-masthead) a:not(:where(p a, li a, blockquote a))::after,
:where(.sfa-brand, .brand, .mast-cta, .back, .deck-why-verify)::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  transform: translateY(-50%);
  min-height: 44px;
  height: 100%;
  /* no paint, no pointer capture beyond the link itself */
  pointer-events: auto;
}
/* An element that already owns ::after for decoration keeps it — the overlay
   is additive convenience, never a reason to break an existing design. */
:where(.sfa-spot, .sfa-grain)::after { min-height: 0; }

/* Mobile reading layers: full native content remains available without JS. */
.deck-why-card.mobile-proof { display:block; }
.hero-ctas a { text-decoration:none; }
.mobile-proof > summary, .hero-explainer > summary {
  display:flex; align-items:center; justify-content:space-between; gap:1rem;
  min-height:48px; cursor:pointer; list-style:none;
}
.mobile-proof > summary::-webkit-details-marker, .hero-explainer > summary::-webkit-details-marker { display:none; }
.mobile-proof > summary svg, .hero-explainer > summary svg {
  flex:none; fill:none; stroke:currentColor; stroke-width:1.5;
}
.mobile-proof[open] > summary svg, .hero-explainer[open] > summary svg { transform:rotate(180deg); }
.mobile-proof > summary .deck-why-kicker { display:block; }
.mobile-proof[open] > summary { margin-bottom:var(--space-related); }
.hero-explainer { max-width:660px; margin:var(--space-group) auto 0; text-align:left; }
.hero-explainer > summary { color:var(--text-dim); justify-content:center; font-size:1rem; }
.hero-explainer > p { margin:.75rem 0 0; font-size:1rem; line-height:1.6; }
@media(max-width:768px) {
  .deck-why-card.mobile-proof { display:block; }
  .mobile-proof .deck-why-desc { font-size:1rem; }
  .deck-tiers-grid.mobile-selected-pricing { display:block; overflow:visible; }
  .mobile-selected-pricing .deck-tier { width:100%; }
  .mobile-selected-pricing .deck-tier[hidden] { display:none; }
  .deck-tier-overview [role="tab"][aria-selected="true"] { background:rgba(201,169,110,.12); box-shadow:inset 0 -3px var(--deck-gold); }
}
