/* ===========================================================
   OnFinance, Expressive Layer
   Loaded last. Everything here is additive and opt-in by class,
   so the underlying page still stands up if this file never
   arrives. Twelve techniques, one vocabulary.

   01  Bento grids
   02  Tactile brutalism and exposed wireframe
   03  Blueprint rulers and structural lines
   04  Asymmetric and organic layout
   05  Expressive minimalism / barely there UI
   06  Kinetic and variable typography
   07  Micro-interactions and tactile feedback
   08  Immersive 3D depth and glassmorphism
   09  Gamification
   10  Adaptive personalisation slots
   11  Eco-design performance rules
   12  Reduced motion, reduced data, print
=========================================================== */

/* ---- Registered custom properties -------------------------
   Registering these makes them animatable and type-checked, which
   is what lets the kinetic headings interpolate a real weight axis
   rather than snapping between values.
----------------------------------------------------------- */
@property --kin-wght{ syntax: "<number>"; inherits: true; initial-value: 700; }
@property --kin-track{ syntax: "<length-percentage>"; inherits: true; initial-value: -0.02em; }
@property --kin-prox{ syntax: "<number>"; inherits: false; initial-value: 0; }
@property --journey-pct{ syntax: "<percentage>"; inherits: false; initial-value: 0%; }
@property --ptr-dx{ syntax: "<number>"; inherits: true; initial-value: 0; }
@property --ptr-dy{ syntax: "<number>"; inherits: true; initial-value: 0; }
@property --tilt-x{ syntax: "<number>"; inherits: false; initial-value: 0; }
@property --tilt-y{ syntax: "<number>"; inherits: false; initial-value: 0; }


/* ===========================================================
   01  BENTO GRIDS
   A six column bed. Cells claim spans, so the rhythm is uneven
   by design and never degenerates into a plain card row.
=========================================================== */

.bento{
  display: grid;
  gap: var(--bento-gap);
  grid-template-columns: repeat(var(--bento-cols), minmax(0, 1fr));
  grid-auto-flow: dense;
  align-items: stretch;
}

.bento > *{ min-inline-size: 0; }

/* Span utilities. Mobile first, so these only bite from 640px up.
   A cell that does not ask for a width takes two of the six columns,
   which keeps a plain three-up row honest inside the same bed. */
@media (min-width: 640px){
  .bento > *{ grid-column: span 2; }
  .bento__cell--2{ grid-column: span 2; }
  .bento__cell--3{ grid-column: span 3; }
  .bento__cell--4{ grid-column: span 4; }
  .bento__cell--6{ grid-column: span 6; }
  .bento__cell--tall{ grid-row: span 2; }
}

/* Below 640px the bed collapses to two columns and cells that
   asked for width keep it, so the mosaic survives on a phone.

   grid-template-columns is declared directly here rather than by
   resetting --bento-cols, because most pages set that custom property
   inline on the container and an inline declaration outranks any
   stylesheet rule, media query or not. Resetting the custom property
   silently did nothing and left a six column bed on a phone, which is
   what crammed four service cards into one 390px row. */
@media (max-width: 639.98px){
  .bento{ grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Full width is the mobile default for every cell, whatever span it
     asked for at desk width. Half width is opt in, per cell, and only
     ever applied in pairs to compact tiles that read fine in half a
     phone. Deriving it from the desk span instead was what left odd
     cells stranded at a third of a row with a void beside them. */
  .bento > *{ grid-column: span 2; }
  .bento__cell--half-on-mobile{ grid-column: span 1; }
  .bento__cell--tall{ grid-row: auto; }
}

/* A very narrow phone cannot carry two of anything, so the opt in
   pairs fold back to single file below 380px. */
@media (max-width: 379.98px){
  .bento__cell--half-on-mobile{ grid-column: span 2; }
}

.bento__cell{
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: var(--space-sm);
  border-radius: var(--bento-radius);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  overflow: clip;
  transition: transform var(--tactile-duration) var(--tactile-ease),
              box-shadow var(--tactile-duration) var(--tactile-ease),
              border-color var(--tactile-duration) var(--tactile-ease);
}
.bento__cell > :last-child{ margin-block-end: 0; }

.bento__cell--accent{
  background: var(--ink-950);
  --color-text: var(--color-text-on-dark);
  --color-text-muted: var(--color-text-on-dark-muted);
  --color-border: rgba(245, 242, 237, 0.16);
  color: var(--color-text);
  border-color: transparent;
}
.bento__cell--accent h2,
.bento__cell--accent h3,
.bento__cell--accent h4{ color: var(--color-text); }

.bento__cell--red{
  background: linear-gradient(150deg, var(--brand-red-600), var(--brand-red-500) 60%, var(--brand-red-700));
  --color-text: #fff;
  --color-text-muted: rgba(255, 255, 255, 0.82);
  color: #fff;
  border-color: transparent;
}
.bento__cell--red h2,
.bento__cell--red h3,
.bento__cell--red h4{ color: #fff; }

.bento__cell--quiet{
  background: transparent;
  border-style: dashed;
  border-color: var(--wire-line);
}

.bento__cell--sand{
  background: var(--sand-050);
  border-color: var(--sand-100);
}

/* The oversized number or stat that anchors a bento cell. */
.bento__figure{
  font-size: clamp(2.6rem, 1.6rem + 4vw, 4.6rem);
  line-height: 0.92;
  font-weight: 800;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--brand-red-500);
  margin-block-end: var(--space-3xs);
}
.bento__cell--red .bento__figure,
.bento__cell--accent .bento__figure{ color: currentColor; }
.bento__cell--accent .bento__figure{ color: var(--brand-red-400); }

/* A phone number has too many characters for the stat-figure treatment
   above, sized for short numbers like "14" or "30+", at that size
   "0412 032 242" either wraps onto a second line or gets cut in two,
   with only "0412" reading as the oversized figure and the rest
   stranded in the small caption underneath. This variant keeps the
   same bold, prominent feel but at a size built to hold the whole
   number on one line, so it reads as a single phone number. */
.bento__figure--tel{
  font-size: clamp(1.7rem, 1.3rem + 1.8vw, 2.6rem);
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.bento__label{
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.bento__cell a:not(.btn):not(.btn-link){ font-weight: 600; }

/* A whole cell can be the link. */
a.bento__cell{ text-decoration: none; color: var(--color-text); }
a.bento__cell:hover{
  transform: translateY(var(--tactile-lift));
  box-shadow: var(--shadow-md);
  border-color: var(--brand-red-400);
}
a.bento__cell:active{ transform: scale(var(--tactile-press)); }


/* ===========================================================
   02  TACTILE BRUTALISM AND EXPOSED WIREFRAME
   Hard edges, zero radius, an offset solid shadow instead of a
   soft one, plus visible corner ticks and spec labels that make
   the underlying grid legible rather than hidden.
=========================================================== */

.wire{
  position: relative;
  border: 2px solid var(--wire-line-strong);
  border-radius: 0;
  background: var(--color-surface);
  box-shadow: var(--wire-shadow);
  transition: transform var(--tactile-duration) var(--tactile-ease),
              box-shadow var(--tactile-duration) var(--tactile-ease);
}
.wire--red{ box-shadow: var(--wire-shadow-red); border-color: var(--brand-red-500); }
.wire--flat{ box-shadow: none; }
.wire--thin{ border-width: 1px; }

a.wire:hover,
.wire--interactive:hover{
  transform: translate(calc(var(--wire-offset) * -0.5), calc(var(--wire-offset) * -0.5));
  box-shadow: calc(var(--wire-offset) * 1.6) calc(var(--wire-offset) * 1.6) 0 var(--ink-950);
}
a.wire:active,
.wire--interactive:active{
  transform: translate(var(--wire-offset), var(--wire-offset));
  box-shadow: 0 0 0 var(--ink-950);
}

/* Corner registration ticks. Purely decorative, drawn in CSS so
   they cost nothing to ship. */
.wire--ticks::before,
.wire--ticks::after{
  content: "";
  position: absolute;
  inline-size: 10px;
  block-size: 10px;
  border: 2px solid var(--wire-tick);
  pointer-events: none;
}
.wire--ticks::before{
  inset-block-start: -2px; inset-inline-start: -2px;
  border-inline-end: 0; border-block-end: 0;
}
.wire--ticks::after{
  inset-block-end: -2px; inset-inline-end: -2px;
  border-inline-start: 0; border-block-start: 0;
}

/* Machine-shop spec label. Sits on the border like a callout. */
.spec{
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}
.spec::before{
  content: "";
  inline-size: 6px;
  block-size: 6px;
  background: var(--brand-red-500);
  border-radius: 1px;
  flex: none;
}
/* A small tag pinned to a card's top edge, straddling the border.
   This class takes the element out of flow, so it belongs only on a
   short label such as an .idx numeral, never on a card or panel. Put
   it on a whole card and the card's own space in the layout vanishes,
   which is what left the contact and pre-approval forms sitting over
   a tall empty gap. The card that hosts one needs .spec--host so the
   tag has something to position against. */
.spec--corner{
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  transform: translateY(-50%);
  margin-inline-start: var(--space-xs);
  padding-inline: 0.5em;
  background: var(--color-surface);
  z-index: 2;
}
.spec--host{ position: relative; }
.on-dark .spec--corner,
.bento__cell--accent .spec--corner{ background: var(--ink-950); }

/* Index numerals. 01 / 04 style, for exposing sequence. */
.idx{
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--brand-red-500);
  display: block;
  margin-block-end: var(--space-3xs);
}
.idx__total{ color: var(--color-text-muted); font-weight: 600; }

/* Exposed-structure variant of the numbered steps list. The counter
   badge and the mobile red thread already own ::before and ::after,
   so the structure is drawn with borders instead of pseudo-elements. */
.steps--wire li{
  padding-block: var(--space-sm);
  border-block-start: 1px solid var(--wire-line);
}
.steps--wire li::before{ inset-block-start: var(--space-sm); }
.steps--wire .spec{ margin-block-end: 0.35em; }
@media (max-width: 720px){
  .steps--wire li:not(:last-child)::after{
    inset-block-start: calc(var(--space-sm) + 2.6rem);
    block-size: calc(100% + var(--space-md) - var(--space-sm) - 2.9rem);
  }
}
@media (min-width: 721px){
  .steps--wire li{
    padding-inline-end: var(--space-sm);
    border-inline-end: 1px solid var(--wire-line);
  }
  .steps--wire li:last-child{ border-inline-end: 0; }
}

/* Exposed-grid variant of the value grid. On desktop the cells are
   separated by drawing lines instead of white space, so the module
   reads as a schedule rather than a set of floating notes. On mobile
   the existing card treatment already carries it. */
@media (min-width: 721px){
  .value-grid--wire{
    gap: 0;
    border-block-start: 1px solid var(--wire-line);
  }
  .value-grid--wire .value-item{
    padding: var(--space-sm);
    border-block-end: 1px solid var(--wire-line);
    border-inline-end: 1px solid var(--wire-line);
    transition: background-color var(--tactile-duration) var(--tactile-ease);
  }
  .value-grid--wire .value-item:hover{
    background: color-mix(in srgb, var(--brand-red-500) 4%, transparent);
  }
}

/* A row of spec labels sitting under a hairline. Used at the foot of
   every page hero so the underlying grid is announced rather than
   hidden, and again wherever a block needs its metadata exposed. */
.spec-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-sm);
  margin-block-start: var(--space-sm);
  padding-block-start: var(--space-xs);
  border-block-start: 1px solid var(--wire-line);
}
.spec-row--tight{ margin-block-start: var(--space-xs); }

/* The spec row is a tight band of small caps with a rule above it. An
   availability note dropped straight underneath collided with it and read
   as one run-on block, so the note is given clear air when it follows. */
/* The notes travel in a stack of up to three (open-hours, closed-hours,
   and sometimes a returning-visitor line), and only one of the first
   two is ever shown. A single adjacent-sibling rule therefore misses
   whenever the first note in the stack is the hidden one, because +
   does not skip display: none. That is exactly what happened: during
   office hours the spacing worked, after hours the visible note fell
   through the selector and sat flush against the spec chips. The
   chains below give the gap to whichever note ends up first, and the
   is-active rules underneath tuck a second visible note back in so a
   pair still reads as one stack. */
.spec-row + .adaptive-note,
.spec-row + .adaptive-note + .adaptive-note,
.spec-row + .adaptive-note + .adaptive-note + .adaptive-note,
.cluster + .adaptive-note,
.cluster + .adaptive-note + .adaptive-note,
.cluster + .adaptive-note + .adaptive-note + .adaptive-note{
  margin-block-start: var(--space-md);
}
.adaptive-note.is-active + .adaptive-note.is-active,
.adaptive-note.is-active + .adaptive-note + .adaptive-note.is-active{
  margin-block-start: 0;
}
.adaptive-note + .eco-note{ margin-block-start: var(--space-sm); }

/* A raw rule that reads as a drawing line rather than a divider. */
.rule-wire{
  border: 0;
  border-block-start: 1px dashed var(--wire-line);
  margin: var(--space-md) 0;
  position: relative;
}
.rule-wire::after{
  content: "";
  position: absolute;
  inset-block-start: -3px;
  inset-inline-start: 0;
  inline-size: 3rem;
  block-size: 5px;
  background: var(--brand-red-500);
}


/* ===========================================================
   03  BLUEPRINT RULERS AND STRUCTURAL LINES
   The layout grid, made visible at very low contrast. Applied to
   sections so the eye reads structure underneath the content.
=========================================================== */

.section--blueprint{
  position: relative;
  isolation: isolate;
}
.section--blueprint::before{
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to right,
    var(--wire-line) 0 1px,
    transparent 1px var(--blueprint-gap)
  );
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
  opacity: 0.75;
}
@media (max-width: 720px){
  .section--blueprint{ --blueprint-gap: 2.6rem; }
  .section--blueprint::before{ opacity: 0.5; }
}

/* Horizontal hairline that runs the full bleed, used to separate
   scrollytelling chapters. */
.section--hairline{ border-block-start: 1px solid var(--wire-line); }

/* Vertical spine, drawn beside a narrow column of prose. */
.spine{
  position: relative;
  padding-inline-start: var(--space-md);
}
.spine::before{
  content: "";
  position: absolute;
  inset-block: 0.35em 0.35em;
  inset-inline-start: 0;
  inline-size: 2px;
  background: linear-gradient(to bottom, var(--brand-red-500), color-mix(in srgb, var(--brand-red-500) 8%, transparent));
  border-radius: 2px;
}


/* ===========================================================
   04  ASYMMETRIC AND ORGANIC LAYOUT
   Deliberately unequal columns, offset blocks and blob radii, so
   nothing sits on an obvious centre line.
=========================================================== */

@media (min-width: 900px){
  .split--asym{ grid-template-columns: minmax(0, 1.45fr) minmax(0, 0.85fr); }
  .split--asym-reverse{ grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.45fr); }
  .split--offset > :first-child{ transform: translateY(calc(var(--space-md) * -1)); }
  .split--offset > :last-child{ transform: translateY(var(--space-sm)); }
}

/* Pull a block out of the measure, into the gutter. The pull is one
   gutter, not one --space-xl. Those two are not the same number: at
   1280px --space-xl computes to about 87px while the gutter is only
   51px, so the old rule pulled the contact page artwork 36px clean off
   the right edge of the document. Measuring the pull in --gutter makes
   it self limiting, because a block can never bleed past the space
   that is actually there to bleed into. */
@media (min-width: 1100px){
  .bleed-start{ margin-inline-start: calc(var(--gutter) * -1); }
  .bleed-end{ margin-inline-end: calc(var(--gutter) * -1); }
}

/* Organic frames. Uneven border radii that feel drawn, not boxed. */
.organic{ border-radius: 62% 38% 46% 54% / 42% 48% 52% 58%; overflow: clip; }
.organic--soft{ border-radius: 44% 56% 52% 48% / 48% 44% 56% 52%; }
.organic--tile{ border-radius: var(--radius-xl) var(--radius-sm) var(--radius-xl) var(--radius-sm); }

/* A slightly rotated card. Small angles only, so text stays legible. */
.askew{ transform: rotate(-1.15deg); }
.askew--alt{ transform: rotate(0.9deg); }
.askew > *{ transform: rotate(0.001deg); } /* keeps text rasterising crisply */
@media (max-width: 720px){
  .askew, .askew--alt{ transform: none; }
}

/* Soft organic backdrop blob for sections that need warmth. */
.section--organic{ position: relative; overflow: clip; }
.section--organic::after{
  content: "";
  position: absolute;
  z-index: -1;
  inline-size: min(52vw, 620px);
  aspect-ratio: 1;
  inset-inline-end: -12%;
  inset-block-start: -18%;
  border-radius: 58% 42% 38% 62% / 46% 56% 44% 54%;
  background: radial-gradient(circle at 35% 35%,
              color-mix(in srgb, var(--brand-red-100) 90%, transparent),
              transparent 68%);
  pointer-events: none;
}
.section--organic.on-dark::after,
.band.section--organic::after{
  background: radial-gradient(circle at 35% 35%,
              color-mix(in srgb, var(--brand-red-600) 30%, transparent),
              transparent 68%);
}


/* ===========================================================
   05  EXPRESSIVE MINIMALISM / BARELY THERE UI
   Where the page needs to breathe. Chrome drops away, space
   opens up, and a single hairline carries the hierarchy.
=========================================================== */

.section--quiet{
  padding-block: var(--space-3xl);
}
.section--quiet .section-head{ margin-block-end: var(--space-xl); }

.quiet-list{
  list-style: none;
  margin: 0;
  padding: 0;
  border-block-start: 1px solid var(--color-border);
}
.quiet-list > li{
  padding-block: var(--space-sm);
  border-block-end: 1px solid var(--color-border);
  display: grid;
  gap: var(--space-2xs);
  transition: padding-inline-start var(--tactile-duration) var(--tactile-ease),
              background-color var(--tactile-duration) var(--tactile-ease);
}
@media (min-width: 780px){
  .quiet-list > li{ grid-template-columns: 9rem 1fr; gap: var(--space-md); align-items: baseline; }
  .quiet-list > li:hover{ padding-inline-start: var(--space-2xs); }
}
.quiet-list dt, .quiet-list__key{
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* Barely there button. Text and a rule, nothing more, until hover. */
.btn--barely{
  --btn-bg: transparent;
  --btn-fg: var(--color-text);
  box-shadow: none;
  border: 0;
  border-block-end: 2px solid var(--brand-red-500);
  border-radius: 0;
  padding-inline: 0.2em;
  padding-block: 0.6em;
}
.btn--barely:hover{
  transform: none;
  box-shadow: none;
  background: color-mix(in srgb, var(--brand-red-500) 8%, transparent);
}

/* Large quiet statement type on an empty field. */
.statement{
  font-size: clamp(1.6rem, 1.1rem + 2.4vw, 3.1rem);
  font-weight: 300;
  line-height: 1.18;
  letter-spacing: -0.025em;
  max-inline-size: 22ch;
  text-wrap: balance;
}
.statement strong,
.statement em{
  font-style: normal;
  font-weight: 800;
  color: var(--brand-red-500);
}
.on-dark .statement strong,
.on-dark .statement em{ color: var(--brand-red-400); }


/* ===========================================================
   06  VARIABLE TYPOGRAPHY

   This section used to animate real font axes from scroll
   position: headings arriving light and wide and settling heavy
   and tight, words rising word by word out of clipped slots.

   Both are gone, and the heading one is worth naming as a
   mistake. Interpolating a weight axis from 340 to 800 while the
   reader scrolls produces exactly the same picture as a webfont
   swapping in late. It does not read as typography responding to
   the page, it reads as the page still loading. It was on 64
   headings across the site.

   The axes are still used. They are simply set, not animated.
=========================================================== */

.kinetic{
  font-variation-settings: "wght" var(--kin-wght), "opsz" 32;
  letter-spacing: var(--kin-track);
}

/* The weight and tracking these headings used to travel towards.
   They now start there. */
.kinetic--scroll{
  --kin-wght: 800;
  --kin-track: -0.03em;
}

/* Slanted, oversized section marker that scales with the viewport. */
.kin-marker{
  font-size: clamp(3.4rem, 2rem + 7vw, 9rem);
  font-weight: 900;
  line-height: 0.82;
  letter-spacing: -0.06em;
  text-transform: uppercase;
  color: transparent;
  -webkit-text-stroke: 1.5px var(--wire-line);
  margin: 0;
  pointer-events: none;
  user-select: none;
}
@supports not (-webkit-text-stroke: 1px black){
  .kin-marker{ color: var(--wire-line); }
}

/* Fluid headline that tracks tighter as it grows, the way real
   display setting works. */
.h-fluid{
  letter-spacing: clamp(-0.045em, -0.02em - 0.4vw, -0.015em);
}


/* ===========================================================
   07  MICRO-INTERACTIONS AND TACTILE FEEDBACK
   Lift on hover, compress on press, 240ms, one easing curve for
   the whole site so every surface feels like the same material.
=========================================================== */

.tactile{
  transition: transform var(--tactile-duration) var(--tactile-ease),
              box-shadow var(--tactile-duration) var(--tactile-ease),
              border-color var(--tactile-duration) var(--tactile-ease),
              background-color var(--tactile-duration) var(--tactile-ease);
}
.tactile:hover{ transform: translateY(var(--tactile-lift)); }
.tactile:active{ transform: scale(var(--tactile-press)); }

/* Every button on the site inherits the same press physics. */
.btn{ transition-duration: var(--tactile-duration); transition-timing-function: var(--tactile-ease); }
.btn:active{ transform: scale(var(--tactile-press)); }

/* Pointer-following sheen on primary actions. Coordinates come
   from the experience script as element-local percentages. */
.btn--primary,
.btn--dark,
.btn--phone{
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.btn--primary::after,
.btn--dark::after,
.btn--phone::after{
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(
    16rem 16rem at var(--btn-x, 50%) var(--btn-y, 50%),
    rgba(255, 255, 255, 0.26),
    transparent 62%
  );
  opacity: 0;
  transition: opacity var(--tactile-duration) var(--tactile-ease);
  pointer-events: none;
}
.btn--primary:hover::after,
.btn--dark:hover::after,
.btn--phone:hover::after{ opacity: 1; }

/* Underline that draws in from the leading edge. */
.link-draw{
  position: relative;
  text-decoration: none;
  padding-block-end: 2px;
}
.link-draw::after{
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  opacity: 0;   /* see .nav-primary a::after, scaleX(0) can leave a 2px sliver */
  transition: transform var(--tactile-duration) var(--tactile-ease),
              opacity var(--tactile-duration) var(--tactile-ease);
}
.link-draw:hover::after,
.link-draw:focus-visible::after{ transform: scaleX(1); opacity: 1; }

/* Icon nudge, arrows and chevrons. */
.nudge svg{ transition: transform var(--tactile-duration) var(--tactile-ease); }
.nudge:hover svg{ transform: translateX(4px); }

/* Tap ripple for touch, driven by --tap-x / --tap-y from script. */
.ripple{ position: relative; overflow: hidden; }
.ripple::before{
  content: "";
  position: absolute;
  inset-block-start: var(--tap-y, 50%);
  inset-inline-start: var(--tap-x, 50%);
  inline-size: 0; block-size: 0;
  border-radius: 999px;
  background: currentColor;
  opacity: 0.16;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference){
  .ripple.is-rippling::before{ animation: ripple-out 520ms var(--ease-out-quart); }
  @keyframes ripple-out{
    from{ inline-size: 0; block-size: 0; opacity: 0.22; }
    to  { inline-size: 260%; block-size: 260%; opacity: 0; }
  }
}

/* Copy-to-clipboard and similar confirmations. */
.is-confirmed{ animation: pop-confirm 420ms var(--ease-spring); }
@keyframes pop-confirm{
  0%  { transform: scale(1); }
  38% { transform: scale(1.06); }
  100%{ transform: scale(1); }
}


/* ===========================================================
   08  IMMERSIVE 3D DEPTH AND GLASSMORPHISM
   Layers sit at different z depths inside a perspective field and
   parallax against the cursor. Translucent panels sample what is
   behind them. All of it collapses flat under reduced motion.
=========================================================== */

.depth-field{
  perspective: var(--depth-perspective);
  perspective-origin: calc(var(--ptr-x) * 100%) calc(var(--ptr-y) * 100%);
  transform-style: preserve-3d;
}

.depth-layer{
  transform: translate3d(
    calc(var(--ptr-dx) * var(--depth, 1) * 14px),
    calc(var(--ptr-dy) * var(--depth, 1) * 14px),
    0
  );
  transition: transform 480ms var(--ease-out-quart);
  will-change: transform;
}
.depth-layer--near{ --depth: 2.1; }
.depth-layer--far{ --depth: 0.45; }

/* Card that tilts toward the cursor. Script sets --tilt-x/--tilt-y
   as signed 0 to 1 values relative to the card centre. Interaction
   rather than animation: it happens under the visitor's own
   pointer, so it can never be caught looking half-done. */
.tilt{
  transform-style: preserve-3d;
  transform:
    rotateX(calc(var(--tilt-y, 0) * var(--tilt-max) * -1))
    rotateY(calc(var(--tilt-x, 0) * var(--tilt-max)));
  transition: transform 320ms var(--ease-out-quart);
  will-change: transform;
}
.tilt__lift{ transform: translateZ(28px); }
.tilt__lift--sm{ transform: translateZ(14px); }

/* Glass panel. backdrop-filter where supported, an honest opaque
   fallback where it is not. */
.glass{
  background: color-mix(in srgb, var(--color-surface) 88%, transparent);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius-lg);
}
@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))){
  .glass{
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%);
            backdrop-filter: blur(var(--glass-blur)) saturate(150%);
  }
}
.glass--strong{ --glass-blur: 22px; }

/* Floating geometry behind hero and band sections. Pure CSS shapes,
   no images, so the payload stays where it was. */
.float-field{
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: clip;
  pointer-events: none;
}
.float-shape{
  position: absolute;
  border-radius: var(--radius-xl);
  border: 1px solid rgba(245, 242, 237, 0.16);
  background: linear-gradient(140deg, rgba(245,242,237,0.07), rgba(245,242,237,0.01));
  transform: translate3d(
    calc(var(--ptr-dx) * var(--depth, 1) * 20px),
    calc(var(--ptr-dy) * var(--depth, 1) * 20px),
    0
  ) rotate(var(--rot, 0deg));
  transition: transform 620ms var(--ease-out-quart);
  will-change: transform;
}
.float-shape--a{ inline-size: 16rem; aspect-ratio: 1; inset-block-start: 8%; inset-inline-end: 6%; --rot: 18deg; --depth: 1.6; }
.float-shape--b{ inline-size: 9rem;  aspect-ratio: 1; inset-block-end: 12%; inset-inline-start: 4%; --rot: -12deg; --depth: 0.9; border-radius: 50%; }
.float-shape--c{ inline-size: 22rem; aspect-ratio: 2 / 1; inset-block-end: -6%; inset-inline-end: 18%; --rot: 8deg; --depth: 0.5; }
/* On a phone these shapes stop being background and start being
   obstacles. A 160px square pinned 6% in from the right edge of a
   390px screen sits on top of the heading, and its 1px light border
   reads as a stray box crossing the words rather than as depth.
   .float-field has overflow: clip, so pushing each shape past the
   corner leaves only an arc bleeding in from the edge, which keeps
   the depth and leaves the text alone. */
@media (max-width: 720px){
  .float-shape--c{ display: none; }
  .float-shape--a{ inline-size: 9rem; inset-block-start: -3rem; inset-inline-end: -3rem; }
  .float-shape--b{ inline-size: 7rem; inset-block-end: -2.5rem; inset-inline-start: -2.5rem; }
  .float-shape{ opacity: 0.55; }
}


/* ===========================================================
   09  GAMIFICATION
   A visible journey with four milestones, a progress meter and a
   small reward when a stage completes. State lives in localStorage
   and is written onto the document as data attributes.
=========================================================== */

.journey{
  --journey-pct: 0%;
  position: relative;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  padding: var(--space-sm);
  display: grid;
  gap: var(--space-xs);
}
.on-dark .journey,
.band .journey{ background: rgba(245, 242, 237, 0.05); }

.journey__head{
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs);
}
.journey__title{
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  margin: 0;
}
.journey__count{
  font-size: var(--text-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}

.journey__meter{
  block-size: 6px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-text) 10%, transparent);
  overflow: hidden;
}
.journey__meter::after{
  content: "";
  display: block;
  block-size: 100%;
  inline-size: var(--journey-pct);
  border-radius: inherit;
  background: linear-gradient(to right, var(--brand-red-600), var(--brand-red-400));
  transition: inline-size var(--duration-slow) var(--ease-out-quart);
}

.journey__steps{
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-3xs);
}

/* Four steps across a phone gives each label under 60px, so "Ran the
   numbers" stacks three deep and the rail stops reading as a row of
   progress. Two by two keeps the labels on one or two lines. */
@media (max-width: 439.98px){
  .journey__steps{
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2xs) var(--space-3xs);
  }
}
.journey__step{
  display: grid;
  gap: 0.35em;
  justify-items: center;
  text-align: center;
  font-size: 0.68rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--color-text-muted);
  transition: color var(--tactile-duration) var(--tactile-ease);
}
.journey__step::before{
  content: "";
  inline-size: 12px;
  block-size: 12px;
  border-radius: 999px;
  border: 2px solid var(--color-border-strong);
  background: transparent;
  transition: background-color var(--tactile-duration) var(--tactile-ease),
              border-color var(--tactile-duration) var(--tactile-ease),
              transform var(--tactile-duration) var(--ease-spring);
}
.journey__step[data-done="true"]{ color: var(--color-text); }
.journey__step[data-done="true"]::before{
  background: var(--brand-red-500);
  border-color: var(--brand-red-500);
  transform: scale(1.18);
}

/* Reward toast. Small, quiet, dismisses itself. */
.reward{
  position: fixed;
  inset-block-end: calc(env(safe-area-inset-bottom, 0px) + 5.5rem);
  inset-inline: var(--space-sm);
  z-index: 900;
  margin-inline: auto;
  max-inline-size: 24rem;
  display: flex;
  align-items: center;
  gap: 0.7em;
  padding: 0.85em 1.1em;
  border-radius: var(--radius-full);
  background: var(--ink-950);
  color: var(--color-text-on-dark);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-sm);
  font-weight: 600;
  opacity: 0;
  transform: translateY(0.8rem) scale(0.96);
  transition: opacity var(--duration-base) var(--ease-out-quart),
              transform var(--duration-base) var(--ease-spring);
  pointer-events: none;
}
.reward.is-visible{ opacity: 1; transform: none; }
.reward__mark{
  inline-size: 1.5em;
  block-size: 1.5em;
  border-radius: 999px;
  background: var(--brand-red-500);
  display: grid;
  place-items: center;
  flex: none;
  font-size: 0.8em;
}
@media (min-width: 780px){
  .reward{ inset-block-end: var(--space-sm); inset-inline-start: auto; inset-inline-end: var(--space-sm); margin-inline: 0; }
}

/* Streak and badge chips, used on the calculator suite. */
.badge-chip{
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.35em 0.75em;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  transition: transform var(--tactile-duration) var(--ease-spring),
              border-color var(--tactile-duration) var(--tactile-ease);
}
.badge-chip[data-earned="true"]{
  border-color: var(--brand-red-400);
  color: var(--brand-red-600);
  background: var(--brand-red-050);
}
.badge-chip[data-earned="true"]{ transform: scale(1); }
.badge-chip__dot{
  inline-size: 7px; block-size: 7px; border-radius: 999px;
  background: var(--color-border-strong);
}
.badge-chip[data-earned="true"] .badge-chip__dot{ background: var(--brand-red-500); }


/* ===========================================================
   10  ADAPTIVE PERSONALISATION SLOTS
   The interface reshapes around observed behaviour. Time of day,
   return visits, the pages already read and how far the visitor
   has scrolled. Never described as intelligence in the copy, it
   just quietly fits.
=========================================================== */

/* Hide only, and do it with enough weight to win wherever it sits.
   A plain [data-personal] rule is 0,1,0, exactly the same as
   .adaptive-note, so source order decided it and the later
   component rule turned every slot back on at once. :not() carries
   its argument's specificity, so this is 0,2,0 and settles it. The
   matching slot is left alone, which also means the component keeps
   its own display value rather than being forced to block. */
[data-personal]:not(.is-active){ display: none; }

/* The note carries its own colours rather than inheriting them,
   because it sits on paper, on tinted bands and on the near-black
   hero, and an inherited near-white foreground over a pale pink
   panel is unreadable. Every context sets the four tokens below,
   never the properties directly. */
.adaptive-note{
  --note-surface: #F9E3E3;
  --note-ink: var(--ink-900);
  --note-rule: var(--brand-red-600);
  --note-edge: color-mix(in srgb, var(--brand-red-500) 26%, transparent);

  display: flex;
  align-items: flex-start;
  gap: 0.7em;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--note-edge);
  border-inline-start: 4px solid var(--note-rule);
  background: var(--note-surface);
  color: var(--note-ink);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  margin-block-end: var(--space-xs);
}

/* A small marker so the note reads as a distinct aside rather than
   a stray tinted paragraph. It is decorative, so it is hidden from
   assistive technology by being a pseudo element. */
.adaptive-note::before{
  content: "";
  flex: none;
  inline-size: 0.5em;
  block-size: 0.5em;
  margin-block-start: 0.5em;
  border-radius: 999px;
  background: var(--note-rule);
}

/* Dark contexts.
   This list is every selector on the site that locally redefines
   --color-text to the on-dark value. Only .on-dark was listed
   before, so the two heroes, which set those tokens themselves
   without carrying the class, kept a pale pink panel underneath
   near-white text and washed out completely. If a new dark surface
   is ever added, add it here as well, or its notes will do the
   same thing. Grep for --color-text-on-dark to find them all. */
.hero .adaptive-note,
.page-hero .adaptive-note,
.band .adaptive-note,
.call-cta .adaptive-note,
.bento__cell--accent .adaptive-note,
.site-footer .adaptive-note,
.on-dark .adaptive-note{
  --note-surface: color-mix(in srgb, var(--brand-red-500) 38%, rgba(12, 15, 20, 0.80));
  --note-ink: #FFF1F0;
  --note-rule: #FF6B6F;
  --note-edge: rgba(255, 255, 255, 0.24);
}

.adaptive-note p{ margin: 0; }

/* ---- Availability states -------------------------------------
   The note that says the office is open used to wear the same red
   panel as the note that says it is closed, so the one piece of
   good news on the page read as a warning. Open is now green and
   closed keeps the red, which makes the dot at the start of the
   line an honest status light rather than decoration. */

.adaptive-note--open{
  --note-surface: var(--color-success-bg);
  --note-ink: var(--ink-900);
  --note-rule: var(--color-success);
  --note-edge: color-mix(in srgb, var(--color-success) 26%, transparent);
}

.hero .adaptive-note--open,
.page-hero .adaptive-note--open,
.band .adaptive-note--open,
.call-cta .adaptive-note--open,
.bento__cell--accent .adaptive-note--open,
.site-footer .adaptive-note--open,
.on-dark .adaptive-note--open{
  --note-surface: color-mix(in srgb, var(--color-success) 42%, rgba(12, 15, 20, 0.80));
  --note-ink: #EAF7F0;
  --note-rule: #52D69B;
  --note-edge: rgba(255, 255, 255, 0.24);
}

/* The status light breathes while the office is open. Purely
   decorative, so it is a pseudo element and it stops dead under a
   reduced motion preference. */
.adaptive-note--open::before{ animation: note-live 2.4s var(--ease-standard) infinite; }

@keyframes note-live{
  0%, 100%{ box-shadow: 0 0 0 0 color-mix(in srgb, var(--note-rule) 55%, transparent); }
  55%{ box-shadow: 0 0 0 0.32em color-mix(in srgb, var(--note-rule) 0%, transparent); }
}

@media (prefers-reduced-motion: reduce){
  .adaptive-note--open::before{ animation: none; }
}

/* Body wrapper. The text has to sit in its own element rather than
   as a bare node, because the closed note carries action links and
   a bare text node would become its own flex item beside them. */
.adaptive-note__body{ min-inline-size: 0; }

/* Out of hours, the note has to offer a way to leave a message
   instead of just reporting that nobody is there. */
.adaptive-note__acts{
  display: flex;
  flex-wrap: wrap;
  gap: 0.45em;
  margin-block-start: 0.6em;
}

.adaptive-note__act{
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  min-block-size: 2.5em;
  padding: 0.35em 0.8em;
  border: 1px solid color-mix(in srgb, currentColor 38%, transparent);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, currentColor 7%, transparent);
  color: inherit;
  font-weight: 650;
  font-size: 0.94em;
  line-height: 1.2;
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-standard),
              border-color var(--duration-fast) var(--ease-standard);
}
.adaptive-note__act svg{
  flex: none;
  inline-size: 1.05em;
  block-size: 1.05em;
}
.adaptive-note__act:hover,
.adaptive-note__act:focus-visible{
  background: color-mix(in srgb, currentColor 15%, transparent);
  border-color: color-mix(in srgb, currentColor 62%, transparent);
}

/* Returning visitors get a compressed hero. Class is set on <html>. */
[data-visit="returning"] .first-visit-only{ display: none; }
[data-visit="first"] .returning-only{ display: none; }

/* After business hours the call button steps back and the text
   button steps forward. Handled with order, not with hiding, so
   both are always reachable. */
[data-hours="closed"] .hours-swap{ flex-direction: row-reverse; }
[data-hours="closed"] .hours-note{ display: block; }
.hours-note{ display: none; }


/* ===========================================================
   11  ECO-DESIGN PERFORMANCE RULES
   Skip layout and paint for offscreen sections, keep images from
   forcing reflow, and never animate anything that costs a layout.
=========================================================== */

/* Opt in per section rather than blanket, because skipping render
   on a section that owns a scroll-driven animation would stall it.
   Applied to the deep-page sections that a visitor rarely reaches. */
.eco-defer{
  content-visibility: auto;
  contain-intrinsic-size: auto 480px;
}
.faq-item{
  content-visibility: auto;
  contain-intrinsic-size: auto 92px;
}

img[loading="lazy"]{ background: var(--surface-sunken); }

/* The dot is part of the sentence, not a sibling of it.
   As a flex item it sat at the leading edge of the box while the
   wrapped text centred itself in the middle, so on any centred
   eco-note the dot ended up stranded out to the left with nothing
   beside it. Setting it inline means it sits immediately before
   the first word and travels with the text, centred or not. */
.eco-note{
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  display: block;
  line-height: 1.5;
  /* Always trails a block of content and had no margin of its own, so it
     butted straight against whatever sat above it. On the about page that
     was a .wire card, whose 6px offset solid shadow is painted below its
     own border, and the shadow landed on top of this text. */
  margin-block-start: var(--space-sm);
}
.eco-note::before{
  content: "";
  display: inline-block;
  inline-size: 6px; block-size: 6px;
  margin-inline-end: 0.5em;
  vertical-align: 0.12em;
  border-radius: 999px;
  background: var(--color-success);
}


/* ===========================================================
   12  REDUCED MOTION, REDUCED DATA, PRINT
=========================================================== */

@media (prefers-reduced-motion: reduce){
  .depth-layer,
  .float-shape,
  .tilt{ transform: none !important; transition: none !important; }
  .kinetic{ transition: none; }
  .kin-word{ opacity: 1 !important; transform: none !important; }
  .askew, .askew--alt{ transform: none; }
  .reward{ transition: opacity 1ms linear; }
  .tactile:hover,
  a.bento__cell:hover,
  a.wire:hover,
  .wire--interactive:hover{ transform: none; }
}

@media (prefers-reduced-data: reduce){
  .float-field{ display: none; }
  .section--organic::after{ display: none; }
  .section--blueprint::before{ display: none; }
}

@media print{
  .float-field,
  .section--blueprint::before,
  .section--organic::after,
  .reward,
  .journey,
  .kin-marker{ display: none !important; }
  .wire{ box-shadow: none; border-width: 1px; }
  .bento{ display: block; }
}
