/* ============================================================================
   NomNum, the design system's PRIMITIVES.
   tokens.css says what the values are. This file says what the material does:
   the type scale, the glass, the button depth model, the press physics, the
   focus ring and the two motion modes. Every page links tokens.css then this.

   Why one file rather than per-page blocks: the pages had each grown their own
   copy of "what a button looks like" and the copies had already drifted (the
   marketing CTA, the app's .btn and the legal pages' .btn-pill were three
   different objects wearing one brand). A primitive defined twice is a
   primitive that will disagree with itself.
   ========================================================================= */

/* ---- 1. TYPE -------------------------------------------------------------
   Two registers. The display classes are for STORYTELLING surfaces and are
   sized in vw so they behave like a poster; the UI sizes are fixed and calm.
   Nothing in a repeated workflow should ever use a display class: that is the
   65/35 rule expressed as CSS. */
.t-hero,
.t-statement,
.t-section,
.t-feature {
  margin: 0;
  font-family: var(--font);
  font-weight: 700;
  letter-spacing: var(--track-display);
  line-height: var(--leading-display);
  color: var(--foreground);
  text-wrap: balance;
}
.t-hero {
  font-size: var(--text-hero);
}
.t-statement {
  font-size: var(--text-statement);
}
.t-section {
  font-size: var(--text-section);
}
.t-feature {
  font-size: var(--text-feature);
  letter-spacing: var(--track-tight);
  line-height: 1.1;
}
.t-lead {
  margin: 0;
  font-size: var(--text-lead);
  line-height: 1.55;
  color: var(--foreground-muted);
  max-width: 34ch;
  text-wrap: pretty;
}
/* the one place the luminous ramp is allowed to touch type: a single word or
   line inside a display heading, never a whole paragraph and never UI text */
.t-lit {
  background: var(--accent);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: var(--accent-text);
}
.eyebrow {
  margin: 0;
  font-size: var(--text-caption);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--foreground-muted);
}

/* .glass, the class, survives; glass, the material, does not. Dozens of
   surfaces carry the class name, and renaming them all is churn the measured
   paper pass does not need: the class now MEANS an opaque instrument panel.
   The tint, the blur, the lit top edge and its masked ring are gone with the
   tokens that drew them. */
.glass {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  isolation: isolate;
}
.glass-floating {
  box-shadow: var(--shadow-sm);
}
/* Glass over an unknown background cannot be trusted to carry text: on a
   photograph it can land anywhere. This is the opaque relative that keeps the
   contrast promise, for panels that hold real content. */
.glass-opaque {
  background: var(--surface);
  border: 1px solid var(--border);
}

/* ---- 3. BUTTONS ----------------------------------------------------------
   One object, six variants, and the depth model from the brief: an ambient
   shadow beneath, a translucent surface, a highlight above. Pressing
   COMPRESSES those three together, which is why the press changes transform,
   shadow AND the highlight rather than only dimming opacity.

   Timing is deliberately asymmetric: compress in --motion-press (90ms, ease-in,
   the finger is already moving) and release in --motion-release (220ms) on a
   restrained spring. Symmetrical timing is what makes a press feel like a
   toggle rather than a physical object. */
.btn,
.nav-cta,
.plan-cta,
.hero-cta,
.btn-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 11px 20px;
  font-family: var(--font);
  font-size: var(--text-ui);
  font-weight: 650;
  letter-spacing: var(--track-tight);
  text-decoration: none;
  cursor: pointer;
  background: var(--surface);
  color: var(--foreground);
  /* RESTING DEPTH. Every control sits above the page, which is the state the
     press has to move away from: --shadow-sm alone made a pill look printed
     onto the paper, so pressing it could only change its size. */
  box-shadow: var(--shadow-raised);
  transform: translateZ(0);
  transition:
    transform var(--motion-release) var(--ease-spring),
    box-shadow var(--motion-release) var(--ease-out),
    background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}
.btn:hover,
.nav-cta:hover,
.plan-cta:hover,
.hero-cta:hover,
.btn-pill:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}
/* THE PRESS. Scale and lift compress, the shadow tightens to a contact
   shadow, and the internal highlight dims: three layers moving together. */
.btn:active,
.nav-cta:active,
.plan-cta:active,
.hero-cta:active,
.btn-pill:active {
  /* §2.2's order: sink first, then compress. Written scale-first this
     computed to ty=0 and the button shrank without sinking - the press-spec
     test measured it before any human did. */
  transform: translateY(1px) scale(0.972);
  box-shadow: var(--shadow-pressed);
  transition-duration: var(--motion-press);
  transition-timing-function: var(--ease-in);
}
.btn:disabled,
.btn[aria-disabled='true'] {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

/* THE PRIMARY, and the depth effect the owner asked to have back.
   Four layers move together, which is what separates a moulded object from a
   coloured rectangle that changes size:
     1. the SWEEP, warm blue into green (--grad-cta). White text is AA at both
        stops, 7.56:1 and 6.46:1, so the label holds all the way across.
     2. the BEVEL, a lit top edge and a shaded floor (--bevel-raised), which is
        what makes the pill look like it has a thickness.
     3. the CAST, the button's own colour thrown onto the page (--glow-accent)
        rather than a grey drop shadow underneath a coloured thing.
     4. the CONTACT shadow, tight and dark, holding it onto the surface.
   On press all four move together: the sweep deepens, the bevel flips to a
   floor-lit inset, the cast collapses and the contact tightens. Pressing only
   the transform is the flat-button tell this replaces. */
.btn.primary,
.nav-cta,
.plan-cta,
.hero-cta {
  background: var(--grad-cta);
  /* --cta-ink, not --on-accent: the peacock CTA is gold and carries navy
     while the white system's is blue and carries white (2.08:1 otherwise) */
  color: var(--cta-ink);
  border-color: transparent;
  font-weight: 700;
  box-shadow: var(--bevel-raised), var(--glow-accent-soft), var(--shadow-raised);
}
.btn.primary:hover,
.nav-cta:hover,
.plan-cta:hover,
.hero-cta:hover {
  box-shadow: var(--bevel-raised), var(--glow-accent), var(--shadow-md);
}
.btn.primary:active,
.nav-cta:active,
.plan-cta:active,
.hero-cta:active {
  /* the deepened sweep, so a held button is the same object under load rather
     than a different colour */
  background: var(--grad-deep);
  box-shadow: var(--bevel-pressed), var(--shadow-pressed);
}
/* §2.2's ring, on the primaries: Sea Glass at 3px so the ring is the
   palette's light doing its one legal job. Everything else keeps the
   universal 2px ring below. */
.btn.primary:focus-visible,
.nav-cta:focus-visible,
.plan-cta:focus-visible,
.hero-cta:focus-visible {
  outline: 3px solid var(--accent-cyan);
  outline-offset: 2px;
}
.btn.secondary {
  background: var(--background-secondary);
  color: var(--foreground);
  border-color: var(--border);
}
.btn.ghost {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  color: var(--accent-text);
}
.btn.ghost:hover {
  background: var(--background-secondary);
  box-shadow: none;
}
/* the other FILLED buttons take the bevel too. A solid coloured pill with no
   thickness sitting beside a primary that has one reads as the unfinished one,
   which is how the flat pass looked in review. */
.btn.danger {
  background: var(--danger);
  color: var(--on-accent);
  border-color: transparent;
  box-shadow: var(--bevel-raised), var(--shadow-raised);
}
.btn.icon {
  padding: 0;
  width: 42px;
  height: 42px;
  border-radius: 50%;
}
/* the glass variants, for controls that float over content or imagery */
.btn.glass {
  background: var(--surface);
  border-color: var(--border);
  color: var(--foreground);
}
.btn.glass-accent {
  background: var(--surface);
  border-color: var(--border);
  color: var(--accent-text);
  box-shadow: var(--shadow-sm);
}
/* legacy name kept until its last consumer is rewritten */
.btn.green {
  background: var(--success);
  color: var(--on-accent);
  border-color: transparent;
  box-shadow: var(--bevel-raised), var(--shadow-raised);
}

/* POINTER-AWARE LIGHT (§16). Opt-in with data-lit, desktop pointers only, and
   only on the few large glass controls where it reads as a lit material
   rather than a torch. --px/--py are set by the tiny helper in each page. */
@media (hover: hover) and (pointer: fine) {
  [data-lit] {
    position: relative;
    overflow: hidden;
  }
  [data-lit]::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(
      42% 120% at var(--px, 50%) var(--py, 50%),
      rgba(var(--white-rgb), 0.3) 0%,
      rgba(var(--white-rgb), 0) 70%
    );
    opacity: 0;
    transition: opacity var(--motion-medium) var(--ease-out);
    pointer-events: none;
  }
  [data-lit]:hover::after {
    opacity: 1;
  }
}

/* ---- 3b. EVERY CONTROL PRESSES -------------------------------------------
   The press physics above belonged to six named classes, so the product had
   two grades of control: a Build my plate that compressed under the finger,
   and a hundred chips, tabs, orbs, cards and menu items that did nothing at
   all. The instruction is "button depth and click effect for everything thats
   clickable and linkable", and a rule that has to be remembered per component
   is a rule that will be forgotten, so this is keyed on what an element IS.

   TWO DIFFERENT ANSWERS, because a control and a word in a sentence are not
   the same thing:

   - anything that behaves as a button - <button>, <summary>, and the ARIA
     roles that make a div into one - compresses. It is scoped with :where() at
     zero specificity for the TRANSITION, so a component's own transition still
     wins, and at plain specificity for :active, so the press lands even on
     components that never thought about it.
   - a LINK dims instead of moving. Transforming a[href] would shove inline
     links around inside running prose every time one is touched, and half the
     links on this site are words in a sentence rather than controls. Opacity
     is instant, costs no layout, and reads as "heard you" on both.

   Components that already define their own :active (the rail orbs at .96, the
   nav items, the stepper) are more specific and keep their own number. */
:where(button, summary, [role='button'], [role='tab'], [role='menuitem'], [role='option']) {
  transition:
    transform var(--motion-release) var(--ease-spring),
    box-shadow var(--motion-release) var(--ease-out);
}
/* :where() keeps this fallback UNDER the .btn rules: written with bare
   :not() chains its specificity (0,3,1) beat .btn:active and every button
   compressed without sinking - the press-spec test measured ty=0 before any
   human noticed the flat press. */
button:where(:not(:disabled)):where(:not([aria-disabled='true'])):active,
summary:active,
[role='button']:active,
[role='tab']:active,
[role='menuitem']:active,
[role='option']:active {
  transform: scale(0.972);
  transition-duration: var(--motion-press);
  transition-timing-function: var(--ease-in);
}
a[href]:active {
  opacity: 0.7;
  transition-duration: var(--motion-press);
}

/* ---- 4. FOCUS ------------------------------------------------------------
   One ring, everywhere, and it is never removed. Two-tone so it survives on
   both light surfaces and the luminous fills. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ---- 5. MOTION -----------------------------------------------------------
   Story motion. These are the only classes allowed a cinematic duration, and
   they are driven by the scroll position rather than by a timer, so nothing
   animates off-screen and nothing has to be waited for. */
@keyframes riseIn {
  from {
    opacity: 0;
    transform: translateY(28px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes lightSweep {
  from {
    background-position: 0% 50%;
  }
  to {
    background-position: 200% 50%;
  }
}
.reveal {
  animation: riseIn var(--motion-slow) var(--ease-out) both;
}
@supports (animation-timeline: view()) {
  .reveal {
    animation-timeline: view();
    animation-range: entry 5% cover 26%;
  }
}

/* ---- 6. THE INTELLIGENCE LIGHT (§26) -------------------------------------
   The product's one "thinking" signature. A slow band of the luminous ramp
   travels beneath a surface, which is why it is a background on a pseudo
   element and not a spinner: it belongs to the object being worked on rather
   than sitting on top of it as a widget. Original to us. */
.thinking {
  position: relative;
  overflow: hidden;
}
.thinking::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--accent-cyan);
  background-size: 200% 100%;
  opacity: 0.16;
  animation: lightSweep 2.6s linear infinite;
  pointer-events: none;
}

/* ---- 7. REDUCED MOTION ---------------------------------------------------
   The rule is not "turn everything off": it is that nothing MOVES while every
   state change stays legible. Presses keep their instant feedback, because a
   control that does not acknowledge a press feels broken, not calm. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .btn:hover,
  .nav-cta:hover,
  .plan-cta:hover,
  .hero-cta:hover,
  .btn-pill:hover,
  .btn:active,
  .nav-cta:active,
  .plan-cta:active,
  .hero-cta:active,
  .btn-pill:active {
    transform: none;
  }
  .btn:active {
    background-color: var(--background-secondary);
  }
  .btn.primary:active {
    filter: brightness(0.92);
  }
  /* the universal press, answered without movement. The acknowledgement is not
     the decoration here - a control that does nothing when touched reads as
     broken to everyone - so the compression becomes a dim, which is the same
     answer links already give. */
  button:active,
  summary:active,
  [role='button']:active,
  [role='tab']:active,
  [role='menuitem']:active,
  [role='option']:active {
    transform: none;
    opacity: 0.72;
  }
  .thinking::after {
    animation: none;
    opacity: 0.12;
  }
  [data-lit]::after {
    display: none;
  }
}

/* ---- 8. THE POINTER PHYSICS SURFACES ------------------------------------
   pointer.js writes custom properties; nothing in it writes a transform. This
   block is where those numbers become a look, which keeps the physics testable
   and the appearance readable in one place.

   Every rule here degrades to nothing when the properties are absent, so a
   browser without pointer events, or a person with reduced motion (where the
   script never sets the spatial values), gets the static design rather than a
   broken one. */

/* the local light. It is the SAME gradient at every tier, only the strength
   changes, so a button and a panel are recognisably one material. --lit is the
   spring, so the light fades in and out rather than switching. */
[data-tactile],
[data-surface],
[data-scene] {
  --px: 50%;
  --py: 50%;
  --lit: 0;
  --tx: 0px;
  --ty: 0px;
  --rx: 0deg;
  --ry: 0deg;
  --press: 0;
}
[data-tactile]::after,
[data-surface]::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(
    circle at var(--px) var(--py),
    rgba(var(--white-rgb), 0.42) 0%,
    rgba(var(--white-rgb), 0.12) 26%,
    rgba(var(--accent-rgb), 0.06) 48%,
    rgba(var(--white-rgb), 0) 72%
  );
  opacity: var(--lit);
}
[data-tactile],
[data-surface] {
  position: relative;
}

/* TACTILE: buttons. They lean toward the pointer by at most 3px and they do
   NOT tilt, because a tilting button is a button that is harder to hit. The
   translate is the spring's output; the scale is the press. */
[data-tactile] {
  transform: translate3d(var(--tx), var(--ty), 0) scale(calc(1 - var(--press) * 0.028));
  transition: none; /* the spring owns the motion; a transition would fight it */
}
/* the press compresses the light too, not just the box: the highlight tightens
   toward the contact point, which is what makes it read as a surface being
   pushed rather than a rectangle being scaled */
[data-tactile]::after {
  transform: scale(calc(1 - var(--press) * 0.06));
  transition: transform var(--motion-press) var(--ease-in);
}

/* SURFACE: panels and cards. A small tilt, no magnetic pull, so content stays
   where the eye left it. perspective on the element itself keeps the rotation
   local rather than requiring a parent to opt in. */
[data-surface] {
  transform: perspective(900px) rotateX(var(--rx)) rotateY(var(--ry));
  transform-style: preserve-3d;
}

/* SCENE: the hero only. The layers move at different rates, which is where the
   depth comes from; the numbers are small on purpose, because depth is a
   relationship between layers rather than a distance travelled. */
[data-scene] .layer-back {
  transform: translate3d(calc(var(--pnx, 0) * 6px), calc(var(--pny, 0) * 5px), 0);
}
[data-scene] .layer-mid {
  transform: perspective(1200px) rotateX(var(--rx)) rotateY(var(--ry))
    translate3d(calc(var(--pnx, 0) * 10px), calc(var(--pny, 0) * 8px), 0);
}
[data-scene] .layer-front {
  transform: translate3d(calc(var(--pnx, 0) * 3px), calc(var(--pny, 0) * 2px), 0);
}

@media (prefers-reduced-motion: reduce) {
  [data-tactile],
  [data-surface],
  [data-scene] .layer-back,
  [data-scene] .layer-mid,
  [data-scene] .layer-front {
    transform: none !important;
  }
  /* the press still answers, because "did it hear me" is not decoration */
  [data-tactile] {
    transform: scale(calc(1 - var(--press) * 0.02)) !important;
  }
  [data-tactile]::after,
  [data-surface]::after {
    display: none;
  }
}

/* ============================================================================
   THE FOOTER, ONCE.

   It was written THREE TIMES - site.css for the six marketing pages, an inline
   block in index.html, another in curryplatecompass.html - and the three had
   already drifted: two shapes of markup, two link lists, one with a logo
   reveal and one without, and .foot-fine carrying !important in two of them to
   win a fight with itself. Every page loads system.css, so this is the one
   copy, and a change to the footer is now a change in one place.

   THE LOOK. The navy band is gone with the token that fed it. What is left is
   the product's own light material with the composition doing the work rather
   than the colour: an oversized wordmark holding the bottom of the page, a
   short statement, three quiet link columns, a hairline, and the legal line.
   The ambient light drifts with the pointer, which is the only motion here.
   ========================================================================== */
.site-footer {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin: clamp(48px, 8vw, 120px) auto 20px;
  width: min(1400px, calc(100% - 24px));
  border-radius: var(--radius-lg);
  /* E6 §4.3: THE FOOTER JOINS THE SYSTEM. --sky-100 is the white palette's
     palest tint and on the field it read as a near-black slab sitting outside
     the layer stack. Section tier, with the same rim and cast every other panel
     carries, so it is a surface of this page rather than a band appended to it.
     EVERY GLASS NAME CARRIES ITS WHITE-PALETTE FALLBACK, because this rule is in
     system.css and system.css is the LANDING page's stylesheet too. The four
     glass tokens are declared only under html[data-theme='peacock'], so the first
     version left the landing footer referencing names that do not resolve there:
     the border and the background both became invalid at computed-value time and
     the footer lost its fill on the anchor page. E5 §0 says the landing page does
     not change, and scripts/_landing-guard.mjs caught this by hashing it. The
     fallbacks are the exact values the rule had before, so the landing page
     computes byte-identical styles and only the field gets the new surface. */
  border: 1px solid var(--glass-edge, var(--border));
  border-top-color: var(--glass-rim, var(--border));
  background-color: var(--surface-section, var(--sky-100));
  background-image: var(--glass-section, none);
  /* ONE token, not two, so the white palette can fall back to `none`: a
     box-shadow list cannot contain `none` as one of its entries. */
  box-shadow: var(--glass-footer-cast, none);
  color: var(--ink);
  /* the tilt every [data-surface] gets is right for a card and wrong for a
     1400px band: a half-degree rotation on something this wide reads as the
     page being crooked. The pointer position is kept, the rotation is not. */
  transform: none;
}
/* the ambient wash follows the pointer. --pnx/--pny come from pointer.js and
   are simply absent on touch and under reduced motion, where this is a static
   gradient rather than a broken one. */
.site-footer::before {
  content: '';
  position: absolute;
  /* inset: 0, NOT -20%. An absolutely positioned child that reaches past the
     padding box still counts toward scrollWidth even when overflow:hidden
     clips it, so a wash bled 20% wide reported the footer as 222px of
     horizontal overflow on every app screen. The gradient's own size gives it
     all the spread it needs. */
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    34% 42% at calc(50% + var(--pnx, 0) * 30%) calc(30% + var(--pny, 0) * 26%),
    rgba(var(--emerald-rgb), 0.1) 0%,
    rgba(var(--emerald-rgb), 0) 70%
  );
}
.foot-inner {
  position: relative;
  z-index: 1;
  padding: clamp(34px, 5vw, 72px) clamp(22px, 4vw, 60px) clamp(20px, 3vw, 34px);
}
.foot-top {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: clamp(28px, 5vw, 70px);
  align-items: start;
}
.foot-eyebrow {
  display: block;
  font-size: var(--text-caption);
  font-weight: 900;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin-bottom: 12px;
}
.foot-line {
  margin: 0;
  font-size: var(--text-feature);
  font-weight: 900;
  letter-spacing: var(--track-display);
  line-height: 1.06;
  color: var(--ink);
  max-width: 14ch;
}
.foot-say {
  margin: 14px 0 0;
  font-size: var(--text-ui);
  font-weight: 600;
  line-height: 1.6;
  color: var(--muted);
  max-width: 40ch;
}
.foot-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 130px), 1fr));
  gap: clamp(18px, 3vw, 34px);
}
.foot-col {
  display: flex;
  flex-direction: column;
  gap: 9px;
  min-width: 0;
}
.foot-lbl {
  font-size: var(--text-caption);
  font-weight: 900;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 2px;
}
.site-footer a {
  color: var(--ink);
  font-weight: 700;
  font-size: var(--text-ui);
  text-decoration: none;
  width: fit-content;
  max-width: 100%;
}
/* The rule wipes in from the left rather than appearing. The obvious version of
   this is a linear-gradient background at `background-size: 0 1px` growing to
   100%, and it is wrong for a reason worth recording: no-inverted-tiles reads
   an element's background-image to decide what colour it paints, and a gradient
   sized to zero still reports as a solid accent-blue ground. Every footer link
   on every page came back as dark-blue-on-dark-blue at 3.57:1. A pseudo-element
   paints only what it says it paints. */
.foot-col a {
  position: relative;
  transition: color var(--motion-fast) var(--ease-out);
}
.foot-col a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: var(--accent-blue);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--motion-medium) var(--ease-out);
}
.foot-col a:hover {
  color: var(--accent-text);
}
.foot-col a:hover::after {
  transform: scaleX(1);
}
.site-footer a:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}
/* THE WORDMARK. Decorative and aria-hidden: the name is already in the header
   of every page and in the legal line below, so a screen reader reading it a
   third time would be noise. It wraps rather than shrinking to nothing, which
   is what keeps it from pushing the page sideways at 375px. */
.foot-mark {
  margin: clamp(30px, 4.5vw, 62px) 0 clamp(16px, 2vw, 26px);
  font-size: clamp(2.3rem, 10.5vw, 8.5rem);
  font-weight: 900;
  letter-spacing: -0.045em;
  line-height: 0.92;
  color: var(--blue-500);
  user-select: none;
}
.foot-fine {
  margin: 0;
  padding-top: clamp(14px, 2vw, 22px);
  border-top: 1px solid var(--border);
  font-size: var(--text-caption);
  line-height: 1.6;
  color: var(--muted);
  /* NO max-width. The hairline above this line is the footer's baseline rule and
     it has to reach both edges; a 92ch cap stopped it two thirds of the way
     across at 1440 and left it looking like an underline on a paragraph. */
}
.foot-fine a {
  font-size: inherit;
}
@media (max-width: 760px) {
  .foot-top {
    grid-template-columns: minmax(0, 1fr);
    gap: 30px;
  }
  .foot-cols {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 120px), 1fr));
  }
}

/* THE ASSEMBLING MARK, once. Two identical copies of this lived in site.css and
   in curryplatecompass.html's <style>, and index.html carried a third that a
   comment-stripping accident had eaten the selectors out of, so the homepage
   shipped four rules that matched nothing. The thali-compass builds itself a
   quadrant at a time, clockwise from the top left, when the footer scrolls in.

   It is smaller and quieter than it was: at 220px on a navy band it was the
   loudest thing on the page, and the band is now white. */
.logo-reveal {
  position: relative;
  width: clamp(72px, 8vw, 104px);
  aspect-ratio: 1;
  margin: 0 0 18px;
  /* was a black drop shadow at .5, tuned to sit on navy; on white that reads as
     dirt under the artwork, so it is the accent at a tenth of the strength */
  filter: drop-shadow(0 14px 30px rgba(var(--accent-rgb), 0.18));
}
.logo-reveal.play {
  animation: lrBreath 8s ease-in-out 3.2s infinite;
}
.logo-reveal .lr-base {
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0.18;
}
.logo-reveal .lr-q {
  position: absolute;
  inset: 0;
  background: url('img/logo-320.png') center/cover no-repeat;
  opacity: 0;
}
.logo-reveal.play .lr-q {
  animation: lrQ 0.85s cubic-bezier(0.3, 0.7, 0.3, 1) both;
}
/* each quadrant grows out of the plate's centre (its origin is the corner that
   touches the hub), clockwise from top-left, with a tiny overshoot as it lands */
.logo-reveal .lr-q1 {
  clip-path: inset(0 50% 50% 0);
  transform-origin: 100% 100%;
}
.logo-reveal .lr-q2 {
  clip-path: inset(0 0 50% 50%);
  transform-origin: 0% 100%;
  animation-delay: 0.7s;
}
.logo-reveal .lr-q3 {
  clip-path: inset(50% 0 0 50%);
  transform-origin: 0% 0%;
  animation-delay: 1.4s;
}
.logo-reveal .lr-q4 {
  clip-path: inset(50% 50% 0 0);
  transform-origin: 100% 0%;
  animation-delay: 2.1s;
}
@keyframes lrQ {
  0% {
    opacity: 0;
    transform: scale(0.8);
  }
  62% {
    opacity: 1;
    transform: scale(1.04);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}
/* once whole, the assembled plate breathes gently */
@keyframes lrBreath {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.03);
  }
}
@media (prefers-reduced-motion: reduce) {
  .logo-reveal {
    animation: none !important;
  }
  .logo-reveal .lr-q {
    animation: none !important;
    opacity: 1;
    transform: none;
  }
}

/* ============================================================================
   THE TOLERANCE BAR (docs/DESIGN_PLAN.md §7)
   One reading, split by where its numbers came from. Every colour here is a
   measurement-ramp token; none of them is decoration.

   IT LIVES IN system.css, not planner.css, because the whole point is that one
   component answers "how much of this do we actually know" everywhere a total
   appears. A second copy in a second stylesheet is how the two drift into
   saying different things about the same number.
   ========================================================================= */
.tb {
  margin: 10px 0 0;
}
.tb-hd {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}
.tb-lbl {
  font-size: var(--text-caption);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 700;
}
/* the reading itself. tabular-nums is not a nicety: without it a total ticking
   from 999 to 1000 shifts every digit beside it, and 1 of 143 numbers in this
   product had it before this component (docs/DESIGN_PLAN.md §1b). */
.tb-val {
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  color: var(--foreground);
  font-size: var(--text-readout-sm);
  line-height: 1.1;
}
.tb-val small {
  font-size: var(--text-caption);
  font-weight: 700;
  color: var(--muted);
}
.tb-track {
  position: relative;
  display: flex;
  height: 12px;
  border-radius: var(--radius-sm);
  background: var(--band-track);
  overflow: hidden;
}
.tb-seg {
  display: block;
  height: 100%;
  min-width: 0;
}
.tb-measured {
  background: var(--m-measured);
}
.tb-modelled {
  background: var(--m-modelled);
}
/* NO FILL, ON PURPOSE. An estimate drawn as a solid block is exactly the thing
   this component exists to stop; the hatch says "we do not know this precisely"
   without needing a colour to be seen. */
.tb-estimated {
  background: var(--m-hatch);
}
/* the target range, painted behind the fill so an in-band reading sits visibly
   inside its own target instead of hiding it */
.tb-band {
  position: absolute;
  top: 0;
  bottom: 0;
  background: var(--surface);
  opacity: 0.55;
}
.tb-tick {
  position: absolute;
  top: -2px;
  bottom: -2px;
  width: 2px;
  margin-left: -1px;
  background: var(--foreground);
}
.tb-note {
  margin: 6px 0 0;
  font-size: var(--text-caption);
  color: var(--muted);
  line-height: 1.5;
}
/* Colour appears ONLY when the reading is outside a real range. A point target
   has no state (tolerance.js returns 'none') because the spread around a
   Mifflin-St Jeor estimate is not something this repo can source, and colouring
   "over" against a number whose own precision is unknown is the overclaim the
   whole pass exists to remove. */
.tb-state-under .tb-tick {
  background: var(--band-under);
}
.tb-state-in .tb-tick {
  background: var(--band-in);
}
.tb-state-over .tb-tick {
  background: var(--band-over);
}

/* ============================================================================
   TABULAR FIGURES, product-wide.
   The step-1 audit measured 1 of 143 number-bearing elements using tabular
   figures; everything else reflows as its digits change, because proportional
   Manrope draws "111" at 74.9px where "000" is 117.1px (measured at 64px).
   These are the numeric families the audit found; a new numeric surface should
   either use one of them or carry .num. Applied per family rather than to
   body * because prose digits ("225 dishes, from idli to biryani") are
   typography, not readings, and tabular prose reads typewritten.
   ========================================================================= */
.num,
.stat b,
.reco b,
.stepval,
.bp-kcal,
.bp-chip b,
.cart-row-n,
.cart-macro b,
.dsp-n,
.kc,
.lrn-n,
.cl-ic + *,
.tb-val {
  font-variant-numeric: tabular-nums;
}

/* ============================================================================
   THE LIVING THALI (two-flagship brief §2.2). Listening and Balancing.
   Rendered by thaliHTML in templates.js inside the one cart both paths share.
   The newest katori settles with a 300ms spring; fills are already sized in
   the SVG (share of the meal), so Balancing is just the next paint. Under
   reduced motion the settle is off and the thali is simply present.
   ========================================================================= */
/* tk-thali, NOT thali: the planner page's inline hero already owns `.thali`
   (an absolutely-positioned floating plate), and reusing the name put a
   728px-tall hero ellipse on top of the portion panel. Only a screenshot
   caught it - a class collision is invisible to every check here. */
.tk-thali {
  width: min(180px, 42vw);
  margin: 4px auto 10px;
}
.tk-thali svg {
  display: block;
  width: 100%;
  height: auto;
}
.th-rim {
  fill: var(--tk-steel);
  stroke: var(--tk-steel-deep);
  stroke-width: 1.5;
}
.th-well {
  fill: var(--tk-well);
}
.tk-bowl {
  fill: var(--tk-steel);
  stroke: var(--tk-steel-deep);
  stroke-width: 1;
}
.tk-food.tk-dal {
  fill: var(--tk-dal);
}
.tk-food.tk-palak {
  fill: var(--tk-palak);
}
.tk-food.tk-rice {
  fill: var(--tk-rice);
  stroke: var(--border-strong);
  stroke-width: 0.6;
}
.tk-food.tk-curd {
  fill: var(--tk-curd);
  stroke: var(--border-strong);
  stroke-width: 0.6;
}
.tk-food.tk-haldi {
  fill: var(--tk-haldi);
}
/* the 300ms spring settle on the item that just landed */
.tk-new {
  animation: tkLand var(--motion-medium) var(--ease-spring) both;
}
/* the over-budget offer: Sand ring, never an alarm token (no-shame owns
   that law); it slides in from below the rim and settles beside the meal */
.tk-alt-bowl {
  fill: var(--surface);
  stroke: var(--sand);
  stroke-width: 2;
}
.tk-alt {
  animation: tkSlide var(--motion-medium) var(--ease-spring) both;
}
@keyframes tkSlide {
  from {
    transform: translate(50px, 96px) scale(0.55);
    opacity: 0;
  }
}
/* the week of miniature plates */
.tk-week {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
  gap: 10px;
  margin: 10px 0 14px;
}
.tk-day {
  display: grid;
  justify-items: center;
  gap: 2px;
}
.tk-day-nm {
  font-size: var(--text-caption);
  font-weight: 700;
  color: var(--foreground-muted);
}
.tk-week .tk-thali svg {
  width: 64px;
  height: 64px;
  display: block;
}
@keyframes tkLand {
  from {
    transform: translate(var(--tx, 0), var(--ty, 0)) scale(0.4);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .tk-new,
  .tk-alt {
    animation: none;
  }
}
