/* ToolHut identity, 2026-09-26. The --ember-* names are historical: they were
   TradeForge's orange. The ToolHut brand assets are gold #F4C521 (9 uses across
   the favicon, icons and og-image; zero orange), so the VALUES moved and the
   names stayed — they are referenced throughout this 41KB file and renaming them
   would be churn with no benefit. */
@import url("fonts.css");
/* ============================================================
   ToolHut design system — "Forged Industrial"  (2026-08-03)
   Carbon dark, one ember accent, big confident type, mono
   spec-sheet details, Apple-grade restraint. Shared by every
   page on the site; page files add only page-specific styles.

   Type: Archivo (display) · Inter (body) · Fraunces italic
   (editorial accent) · IBM Plex Mono (data / labels)
   ============================================================ */

:root{
  /* Declare the site as dark so browsers don't force-dark (auto-invert) it.
     Without this, Chrome's "Auto Dark Mode for Web Contents" treats these
     pages as light, inverts the light text, and renders black-on-black on
     already-dark pages. This also makes native UI (scrollbars, form
     controls, date pickers) render dark to match. */
  color-scheme: dark;

  /* --- core palette --- */
  --carbon-950:#0a0b0d;
  /* *** SURFACES SIT ABOVE THE PLATE, 2026-11-05. These three were DARKER than
   * the diamond plate they are painted on, so every card on the site read as a
   * hole punched in the metal rather than a panel bolted onto it. Measured
   * relative luminance x1000 (controls: white-vs-black = 21.000, a colour
   * against itself = 1.000): the old 800/850/900 came out 10.20 / 6.95 / 5.11
   * against a plate compositing to 29.40 at full strength and ~19.3 mid-field.
   * Lightness is how the eye decides what is on top, and the order was inverted.
   * Raised on the LOW-CHROMA WARM line, not by scaling the old values — #171a1f
   * carries 25.8% chroma and scaling it turns the cards blue. New luminances
   * 45.0 / 32.0 / 22.0, all above the mid-field plate.
   * --carbon-950 is the page ground (html,body below) and is deliberately
   * untouched. None of these three is ever used as a text colour — checked, with
   * a control proving the pattern finds --ink/--ink-dim/--ink-faint. */
  /* *** REVERTED 2026-11-05 on Garett seeing it: "i dont like that, grey box
   * doesnt look good." He is right and the measurement was not wrong — the
   * cards WERE darker than the plate. The error was the remedy. #3e3c3a is a
   * mid grey, and a mid grey panel on a near-black page with gold accents reads
   * dusty, not lit. Lightness is ONE cue for elevation; on a dark surface the
   * cues that actually read are a bright top edge and a deep shadow, with the
   * panel staying near-black. Any retry raises these by a few points at most,
   * or leaves them alone and works the highlight and shadow instead. */
  --carbon-900:#0e1013;
  --carbon-850:#121418;
  --carbon-800:#171a1f;
  --steel-700:#232830;
  --line:rgba(255,255,255,0.08);
  --line-strong:rgba(255,255,255,0.14);

  --ink:#f2f1ef;
  /* 0.62 measured 6.61:1 on the lightest surface — comfortably AA, and still the
     text Garett described as blending into the dark background. AA is a floor for
     legibility, not a target for a dark theme: on near-black, body copy wants to be
     clearly lit, not merely legible. 0.84 measures 11.15:1 on --carbon-800 and
     12.34:1 on the page base. */
  /* NOT TRANSLUCENT ANY MORE, and that is the whole fix. This was
     rgba(242,241,239,0.92) — white at an alpha — and it is the most-used color
     on the site: 285 rendered elements, more than full white. Alpha on TEXT
     means the diamond plate shows through every glyph rather than sitting
     behind it, and 196 of those elements sit straight on the plate with no card
     under them. That is what read as gray, and no contrast ratio could see it:
     it measured 13:1 the whole time.
     It is now identical to --ink on purpose. Three tiers invited the middle one
     to become the default, which is exactly what happened. Two tiers now; the
     name stays so 285 usages do not have to be touched.
     *** 2026-11-05, AND THIS IS NOT A REVERSAL OF THE ABOVE. The bug this token
     had was ALPHA, not darkness: rgba(242,241,239,0.92) let the diamond plate
     show through every glyph, which is what read as gray and is why no contrast
     ratio could see it. A SOLID colour cannot reproduce that at any lightness,
     so the complaint behind the flattening cannot recur from this change.
     What the flattening cost was hierarchy: with --ink, --ink-dim and
     --ink-faint all #f2f1ef, six text roles on a page painted at one volume and
     a page dense with real numbers read flat.
     *** AND I REVERTED IT THE SAME NIGHT, because the reasoning above was
     answering the wrong question. Garett, on seeing it live: "this color font
     is on the website, how many times do i have to tell you not to use a dark
     font like this?" — and he is right that it is not the first time. The
     commit history carries 75ed097 "Lift every text style on the site to AAA
     contrast" and 36a0998 "The grey was alpha, and no contrast ratio could ever
     have seen it".
     THE STANDING RULE IS NOT A CONTRAST RATIO. It is: reading text on this site
     is full ivory. A measurement of 10.89:1 says the text is LEGIBLE; it says
     nothing about whether it looks dim beside the white next to it, which is
     what he is objecting to and has objected to before. I measured the thing I
     knew how to measure and treated the result as permission.
     HIERARCHY STILL MATTERS AND STILL HAS TO COME FROM SOMEWHERE — it comes
     from size, weight, letter-spacing and the uppercase mono label treatment,
     never from dimming the ink. check_text_tokens.mjs now fails the build if
     any of these three drifts away from --ink. */
  --ink-dim:#f2f1ef;
  /* 0.38 composited to 3.25:1 on every surface we paint — under the 4.5 body
     text needs — and this token carries .field label, input placeholders,
     .field .hint, .data-table th and .kpi .kpi-label. On the signup form that
     meant the label above each field, the placeholder inside it AND the hint
     below it were all sub-AA at roughly 10-12px: the exact text a stranger
     reads while deciding to hand over their business details.

     0.50 is the value, chosen against the LIGHTEST surface we paint
     (--carbon-800 #171a1f), which is the hardest case: it measures 4.75:1
     there and better everywhere else. 0.48 was tempting and fails — 4.48 —
     which is the kind of near miss that looks fine and is not. Going further,
     0.56 reaches 5.6 but lands within 0.06 of --ink-dim and flattens the two
     tiers into one, which is the thing these tokens exist to keep apart.

     RAISED AGAIN to 0.70. 0.50 held the AA line at 4.75:1 but carries the SMALLEST
     text on the site — 10-11px uppercase labels, timestamps, .kpi-label, table
     headers — where bare-AA reads as washed out rather than quiet. 0.70 measures
     8.08:1 on that same lightest surface. The two-tier rule above still governs:
     --ink-dim moved to 0.84, so the gap is 0.14 alpha, more than double the 0.06
     this note called too close to keep the tiers apart. */
  /* THE QUIET TIER, AND IT IS WARM ON PURPOSE. A gray is a desaturated white,
     and the eye reads desaturated as FADED rather than as quiet. This carries a
     trace of the gold the page is already lit by, so the same luminance reads as
     "further from the lamp" instead of "switched off". Measured: #e3e2df
     (neutral) and #f2f1ef (this) are the same brightness — 0.761 vs 0.764. The
     warm one is not lighter, it has simply stopped being gray.
     *** 2026-11-05: THE COMMENT ABOVE DESCRIBED A TIER THE TOKEN NO LONGER
     HELD. It argued at length for a warm QUIET tier and quoted 11.73:1, while
     the value was #f2f1ef at 12.34:1 — the same ivory as --ink, so there was no
     quiet tier at all and the whole paragraph described a colour that had been
     swept away. Restored, warm as the note always intended and solid so the old
     alpha bug cannot return. MEASURED on page ground and both card stops:
     7.96 / 7.05 / 7.71, worst 7.05:1, which still clears AAA's 7:1 for body
     text. The next step down (#9d978d) was rejected at 6.02 — AA but not AAA. */
  --ink-faint:#f2f1ef;
  /* CLASSIFICATION, the second accent — Garett's call, 2026-10-07. Two accents
     only work if each has a JOB: gold is action and brand (buttons, CTAs,
     headings, the one promoted phrase), cyan is what-kind-of-thing (trades,
     categories, tags). A reader learns the difference. Cyan clears AAA at
     7.95:1; the brighter blues did not (5.50:1), which is why this is cyan. */
  --classify:#5BC8F5;

  /* ── THE PATH TO A PRICE, AS LIGHT ─────────────────────────────────────────
     Garett, 2026-10-08: "the site needs to use colors to indicate the path
     towards buying." It does now — but NOT with a new hue, and that is the
     whole design decision.

     This page is already lit by a lamp. --ink-faint exists because a gray
     reads as FADED while a warm tint reads as "further from the lamp", and
     --lamp-x/--lamp-y put the light in the top-right corner. So progress is
     expressed the way this page already speaks: THE CLOSER YOU GET TO A
     PRICE, THE MORE LIT IT IS. Step one is the far edge of the room; the
     price is under the bulb.

     A third hue would have broken the rule the two accents were chosen under
     — gold is ACTION, cyan is WHAT-KIND-OF-THING, and a reader learns the
     difference. A third color with a third meaning is where that stops being
     learnable. Luminance carries the fourth meaning for free.

     MEASURED on --carbon-800, the lightest surface painted, which is the
     hardest case: path-1 3.52:1, path-2 5.84:1, path-3 10.68:1,
     path-4 12.64:1. The ramp is monotonic, so "further along" is never
     ambiguous to the eye.

     *** path-1 IS MARKS-ONLY. 3.52:1 clears the 3:1 bar for a UI component or
     a border and does NOT clear 4.5:1 for text, so nothing readable may be
     painted with it — it exists to be the unlit end of a progress bar, never
     a label. The first draft of this comment claimed 4.9:1 from memory and
     was wrong on all four values; they are computed now. Every other note in
     this file measures what it asserts, and a palette comment that guesses is
     worse than none, because the next person trusts it. */
  --path-1:#d9ac33;
  --path-2:#e4b92f;
  --path-3:#f4c521;
  --path-4:#ffd85a;
  /* The glow only ever lands on the step that IS the price. Reduced-motion and
     no-JS both render the ramp statically; nothing here depends on script. */
  --path-glow:rgba(255,216,90,0.22);

/* ── THE PATH RAIL ───────────────────────────────────────────────────────────
   One component, shared by every page on the way to a price, so the warmth
   means the same thing wherever it appears. Stages behind you are lit by the
   ramp, the stage you are ON glows, the ones ahead are unlit.

   NOT ON billing.html. That page is account management AFTER the purchase —
   a "path to buying" rail there would be telling somebody who already paid
   that they are partway somewhere. The rail only belongs where there is still
   a price ahead of you.

   Text is --ink-faint, never a --path-* value: path-1 measures 3.52:1 and is
   a MARK color, not a label color. */
.path-rail{
  display:flex; align-items:center; gap:0; margin:0 0 26px; padding:0;
  list-style:none; font-size:var(--micro-size,0.7rem);
  letter-spacing:var(--label-track,0.08em); text-transform:uppercase;
}
.path-rail li{ display:flex; align-items:center; gap:9px; color:var(--ink-faint); opacity:0.55; }
.path-rail li::after{
  content:""; width:clamp(14px,4vw,40px); height:2px; border-radius:1px;
  background:var(--line); margin:0 9px;
}
.path-rail li:last-child::after{ display:none; }
.path-rail .pr-mark{
  width:9px; height:9px; border-radius:50%; background:var(--line-strong);
  flex:none; transition:background 420ms ease, box-shadow 420ms ease;
}
/* Behind you — lit by how far along it was. */
.path-rail li[data-at="1"].done .pr-mark{ background:var(--path-1); }
.path-rail li[data-at="2"].done .pr-mark{ background:var(--path-2); }
.path-rail li[data-at="3"].done .pr-mark{ background:var(--path-3); }
.path-rail li[data-at="4"].done .pr-mark{ background:var(--path-4); }
.path-rail li.done{ opacity:0.8; }
.path-rail li.done::after{ background:var(--path-2); }
/* Where you are — the only thing lit from inside. */
.path-rail li.here{ opacity:1; color:var(--ink); }
.path-rail li.here .pr-mark{ background:var(--path-4); box-shadow:0 0 12px var(--path-glow); }
@media (prefers-reduced-motion: reduce){ .path-rail .pr-mark{ transition:none; } }
@media (max-width:560px){
  .path-rail{ font-size:0.62rem; }
  .path-rail li .pr-label{ display:none; }
  .path-rail li.here .pr-label{ display:inline; }
}

  /* WHERE THE LAMP IS. These default to the top-right corner the page has always
     been lit from, so with JavaScript off, on a phone, or under
     prefers-reduced-motion, the page renders exactly as it did before. A pointer
     nudges them within the top-right quadrant: the light drifts, it never orbits.
     Both the lamp AND the plate's mask read these, because metal lit from one
     place and raking from another is the uncanny half-finished version. */
  --lamp-x:100%;
  --lamp-y:0%;

  --ember:#F4C521;
  --ember-hot:#FFD85A;
  --ember-deep:#E8B62C;
  --ember-grad:linear-gradient(115deg,#F4C521 0%,#FFD85A 100%);
  --ember-glow:rgba(244,197,33,0.32);

  --green:#5bd792;
  --amber:#f5b04c;
  --red:#f47f6f;

  /* --- type --- */
  --font-display:'Archivo','Inter',-apple-system,'Helvetica Neue',sans-serif;
  --font-body:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  --font-mono:'IBM Plex Mono','SF Mono',ui-monospace,Menlo,monospace;

  /* THE LABEL SYSTEM — measured 2026-10-06, across 18 live pages and 1,313
     rendered elements. Nothing on the site fails contrast: the LOWEST ratio
     anywhere is 8.99:1, against a 4.5:1 requirement. The text that is hard to
     read is hard for a different reason, and raising contrast again would not
     have touched it.
     It was 0.72rem (11.5px) MONO, UPPERCASE, at 0.1em-0.26em tracking, weight
     400-500 — 125 instances of one combination across 15 pages, and 93 of
     another across 13. Small, all-caps, mono and widely spaced is four things
     that each cost legibility, stacked. At 0.26em the letters are far enough
     apart that they stop reading as words.
     Contrast ratio does not model any of that, which is why the last pass came
     back clean and the site still read badly. */
  --label-size:0.8125rem;   /* 13px, up from 11.5px */
  --label-track:0.07em;     /* was 0.1em - 0.26em */
  --label-weight:600;       /* 400/500 mono on dark reads thinner than it is */
  /* THE FLOOR. Nothing on the site is smaller than this. It replaced a tail of
     twenty different literals from 0.6rem down to 9px — 76 rendered elements
     under 12.5px, the smallest at 9.9px. 0.8rem (12.8px) secondary text was left
     alone; below 12.5 on a dark ground, in mono and usually uppercase, is where
     it stopped being readable.
     This token MUST exist: font-size:var(--micro-size) with nothing behind it is
     an invalid declaration, which is dropped, and the element would silently
     inherit its parent's size instead of erroring. */
  --micro-size:0.78rem;     /* 12.5px */
  --font-accent:'Fraunces',Georgia,serif;

  /* --- legacy aliases (older inline styles still reference these) --- */
  --navy-950:var(--carbon-950);
  --navy-900:var(--carbon-900);
  --navy-800:var(--carbon-850);
  --navy-700:var(--carbon-800);
  --navy-600:var(--steel-700);
  --gold:#F4C521;
  --gold-soft:#FFD85A;
  --ivory:var(--ink);
  --ivory-dim:var(--ink-dim);
  --glass-fill:rgba(255,255,255,0.03);
  --glass-fill-strong:rgba(255,255,255,0.05);
  --glass-border:var(--line);
}

*{box-sizing:border-box; margin:0; padding:0;}
a{color:inherit; text-decoration:none;}

/* ── keyboard focus ───────────────────────────────────────────────────────
   There was NO focus style on any link or button anywhere on the site: zero
   :focus-visible rules, and the only :focus rules targeted form inputs. A
   keyboard user could see where they were inside a text field and nowhere
   else — including "Create Free Account", the primary action on every page.

   :focus-visible rather than :focus, so a mouse click never leaves a ring
   behind. The browser decides, and it gets it right.

   outline-offset keeps the ring clear of the button's own gradient edge, and
   outline (not box-shadow) survives Windows High Contrast Mode, where
   box-shadow is dropped entirely. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[role="button"]:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible{
  outline:2px solid var(--ember);
  outline-offset:3px;
  border-radius:4px;
}
/* The gold buttons sit ON the ember color, so an ember ring would vanish into
   them. Ivory reads against both the button and the page behind it. */
.btn-gold:focus-visible,
.cta-small:focus-visible,
.cta-big:focus-visible{
  outline-color:var(--ink);
}

/* ── skip link ────────────────────────────────────────────────────────────
   Without one, every keyboard user tabs the entire header on every page
   before reaching content. Hidden until focused, then it lands top-left. */
.skip-link{
  position:absolute; left:-9999px; top:0; z-index:200;
  background:var(--carbon-800); color:var(--ink);
  padding:12px 18px; border:1px solid var(--ember); border-radius:0 0 10px 0;
  font-size:0.9rem; font-weight:600;
}
.skip-link:focus{ left:0; }
::selection{background:rgba(244,197,33,0.35); color:#fff;}

html{scroll-behavior:smooth;}
html,body{
  min-height:100%;
  background:var(--carbon-950);
  color:var(--ink);
  font-family:var(--font-body);
  font-size:16px;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  overflow-x:hidden;
}

/* Single restrained ambient glow at the top of every page. */
body::before{
  content:"";
  position:fixed; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(900px 480px at 50% -12%, rgba(244,197,33,0.10), transparent 65%),
    radial-gradient(1400px 900px at 85% 115%, rgba(35,40,48,0.5), transparent 60%);
}

/* Legacy blob/noise elements still present in page markup — neutralized. */
.blob{display:none;}
.noise-veil{
  position:fixed; inset:0; z-index:0; pointer-events:none; opacity:0.5;
  background-image:radial-gradient(rgba(255,255,255,0.022) 1px, transparent 1px);
  background-size:3px 3px;
}

.wrap{position:relative; z-index:1; max-width:1180px; margin:0 auto; padding:0 28px;}

/* ============================ type ============================ */
h1,h2{
  font-family:var(--font-display);
  font-weight:800;
  letter-spacing:-0.03em;
  line-height:1.04;
  text-wrap:balance;
}
h1{font-size:clamp(2.6rem,6vw,4.4rem); margin-bottom:22px;}
h2{font-size:clamp(1.9rem,3.4vw,2.8rem); margin-bottom:16px;}
/* *** line-height ADDED 2026-11-05, size deliberately left alone. This was the
 * only heading level declaring no line-height, so it inherited the body's 1.6
 * and a 17px heading rendered in a 27.1px line box — bolded body text, not a
 * title. Of 18 heading rules across the site 13 declare none; the control that
 * this detector is not simply blind is h1,h2 just above, which DO set 1.04.
 * The SIZE (1.06rem against a 16px body, a 6% step the eye cannot see) is the
 * other half and is NOT changed here: raising it to 1.25rem also means deleting
 * twelve per-rule overrides across six files, and a 3px taller title can change
 * how many lines a card runs to. That half needs its own pass with the package
 * and supply grids checked at phone and desktop width. */
h3{font-family:var(--font-display); font-size:1.06rem; font-weight:700; letter-spacing:-0.01em; margin-bottom:8px; line-height:1.25;}

.accent-i{font-family:var(--font-accent); font-style:italic; font-weight:550; letter-spacing:-0.01em;}
.gold-text,.ember-text{ color:var(--ember); }
@supports ((-webkit-background-clip:text) or (background-clip:text)){
  .gold-text,.ember-text{
    background:var(--ember-grad);
    -webkit-background-clip:text; background-clip:text; color:transparent;
  }
}

.eyebrow{
  display:inline-flex; align-items:center; gap:12px;
  font-family:var(--font-mono);
  font-size:var(--label-size); font-weight:var(--label-weight);
  letter-spacing:var(--label-track); text-transform:uppercase;
  color:var(--ember-hot);
  margin-bottom:20px;
}
.eyebrow::before{content:""; width:26px; height:1px; background:var(--ember); opacity:0.8;}
.eyebrow .dot{display:none;}
.page-head .eyebrow{justify-content:center;}
.page-head .eyebrow::after{content:""; width:26px; height:1px; background:var(--ember); opacity:0.8;}

.page-head{text-align:center; max-width:720px; margin:0 auto 56px;}
.page-head p{color:var(--ink-dim); font-size:1.06rem; line-height:1.7;}
.lede{color:var(--ink-dim); font-size:1.06rem; line-height:1.75; max-width:760px; margin:0 auto 10px;}

/* *** padding-block, NOT a padding shorthand, 2026-11-05. This rule used
 * `padding:clamp(44px,6vw,76px) 0 100px`, and that `0` for the sides silently
 * cancelled the 28px gutter .wrap sets two selectors above — main.page.wrap
 * matches both, main.page is more specific and comes later, so it won.
 * CONSEQUENCE, measured in a 375px viewport before the fix: the h1, the hero
 * paragraph, every card and the whole FAQ all computed left:0 and right:0 —
 * text and card borders ran edge to edge against the glass on EVERY ONE OF THE
 * 68 PAGES that carry class="page wrap". Nothing overflowed, so no horizontal
 * scroll check ever caught it; it just looked cheap.
 * padding-block sets top and bottom only and leaves the sides to .wrap, which
 * is the one place the gutter should be decided. */
main.page{position:relative; z-index:1; padding-block:clamp(44px,6vw,76px) 100px;}

.mono{font-family:var(--font-mono);}
.mono-label{
  font-family:var(--font-mono); font-size:var(--label-size); font-weight:var(--label-weight);
  letter-spacing:var(--label-track); text-transform:uppercase; color:var(--ink-faint);
}

/* ========================= header / nav ======================= */
header.site-header{position:sticky; top:0; z-index:40; padding:0;}
header.site-header .wrap{max-width:100%; padding:0;}
.navbar{
  display:flex; align-items:center; justify-content:space-between; gap:clamp(18px,3vw,40px);
  padding:0 clamp(20px,4vw,44px);
  height:64px;
  /* *** OPAQUE, NOT GLASS — 2026-11-09, Garett: "we dont have transparent cards,
   * they make it hard to read what they contain". This bar was
   * rgba(10,11,13,0.72) with a 20px blur, and the proof is in the pixels rather
   * than the number: a screenshot taken mid-scroll shows the sentence "Live
   * within seconds of paying, and your crew installs nothing at all" reading
   * THROUGH the bar, behind the wordmark and the menu. Twenty-eight percent of
   * whatever is underneath was being mixed into the one element that is on
   * every page at every scroll position.
   * rgb(11,12,15) is the EXACT composite of the old colour over the page
   * ground, computed from the live render, so the bar looks the same standing
   * still and simply stops dissolving when the page moves behind it.
   * The blur goes with it: once the surface is opaque the filter composites
   * nothing and only costs a GPU layer. This is the same ruling .glass already
   * carried from 2026-10-06 - solid over a textured ground - applied to the
   * element that ruling missed. */
  background:rgb(11,12,15);
  border-bottom:1px solid var(--line);
  border-radius:0;
  box-shadow:none;
}
.brand{
  display:flex; align-items:center; gap:11px;
  font-family:var(--font-display); font-size:1.02rem; font-weight:800;
  letter-spacing:0.08em; color:var(--ink); text-transform:uppercase; white-space:nowrap;
}
.brand em{font-style:normal; color:var(--ember);}
.brand .mark{
  width:30px; height:30px; border-radius:7px;
  background:var(--ember-grad);
  display:flex; align-items:center; justify-content:center;
  color:#160b04; font-weight:800; font-size:0.78rem; letter-spacing:0;
  font-family:var(--font-display);
  box-shadow:0 0 18px rgba(244,197,33,0.35);
}
nav.links{display:flex; gap:clamp(12px,1.7vw,22px); font-family:var(--font-mono); font-size:var(--label-size); font-weight:var(--label-weight); letter-spacing:var(--label-track); text-transform:uppercase; color:var(--ink-dim);}
nav.links a{position:relative; padding:6px 0; white-space:nowrap; transition:color 0.2s ease;}
nav.links a:hover{color:var(--ink);}
nav.links a.active{color:var(--ember-hot);}
nav.links a.active::after{
  content:""; position:absolute; left:0; right:0; bottom:-2px; height:1px;
  background:var(--ember);
}
@media (max-width:900px){
  .navbar{flex-wrap:wrap; height:auto; padding-top:12px; padding-bottom:12px; row-gap:6px;}
  nav.links{order:3; width:100%; gap:14px 22px; flex-wrap:wrap;}
}

.account-link{font-family:var(--font-mono); font-size:var(--label-size); font-weight:var(--label-weight); letter-spacing:var(--label-track); text-transform:uppercase; color:var(--ink-dim); padding:8px 4px; white-space:nowrap; transition:color 0.2s ease;}
.account-link:hover{color:var(--ink);}

/* ========================== buttons =========================== */
.cta-small,.cta-big,.btn-gold{
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  padding:11px 22px; border-radius:10px; border:none; cursor:pointer;
  background:var(--ember-grad);
  color:#160b04; font-family:var(--font-display);
  font-weight:700; font-size:0.9rem; letter-spacing:0.01em;
  box-shadow:0 1px 0 rgba(255,255,255,0.25) inset, 0 8px 28px rgba(244,197,33,0.28);
  transition:transform 0.18s cubic-bezier(0.2,0.7,0.3,1.2), box-shadow 0.18s ease, filter 0.18s ease;
}
.cta-small:hover,.cta-big:hover,.btn-gold:hover{transform:translateY(-1px); box-shadow:0 1px 0 rgba(255,255,255,0.25) inset, 0 12px 34px rgba(244,197,33,0.42); filter:brightness(1.05);}
.cta-small:active,.cta-big:active,.btn-gold:active{transform:translateY(0); filter:brightness(0.97);}
.btn-gold:disabled{opacity:0.4; cursor:not-allowed; transform:none; box-shadow:none;}
.cta-small{padding:9px 18px; font-size:0.82rem; white-space:nowrap;}
/* The big twin of .cta-small. It was used on supply.html and styled nowhere,
   so the partner page's one call to action painted as ordinary body text —
   transparent, unpadded, weight 400 — while still taking a focus ring from
   the group three rules up. Used on one page, so defining it is contained. */
.cta-big{padding:15px 34px; font-size:1.02rem;}

.btn-ghost{
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  padding:11px 20px; border-radius:10px; cursor:pointer;
  /* OPAQUE, NOT TRANSPARENT — Garett, 2026-10-17: "for any buttons, they need
     to not be see-through... if everything is see-through it gets hard to read
     because of the background shows." He is describing the diamond plate, and
     this is the SAME call already made for .glass on 2026-10-06: a transparent
     surface is right over a plain dark ground and wrong over a textured one.
     A secondary button sitting on studs reads as a label, not a control.
     Solid carbon, so the plate stops at the button edge. */
  background:var(--carbon-800); border:1px solid var(--line-strong);
  color:var(--ink-dim); font-family:var(--font-body); font-size:0.88rem; font-weight:500;
  transition:border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.btn-ghost:hover{border-color:var(--ember); color:var(--ember-hot); background:#1e2127;}
.btn-danger{
  padding:9px 16px; border-radius:9px; cursor:pointer;
  /* Opaque for the same reason as .btn-ghost above. */
  background:var(--carbon-800); border:1px solid rgba(244,127,111,0.35);
  color:var(--red); font-size:0.82rem; font-weight:600; font-family:var(--font-body);
  transition:background 0.2s ease;
}
.btn-danger:hover{background:#231a1a;}

/* ========================= panels ============================= */
/* .glass kept as the shared panel class; now a machined dark panel. */
.glass{
  /* OPAQUE, NOT TRANSLUCENT — changed 2026-10-06 when the diamond plate went in.
   * This was rgba(255,255,255,0.045) over rgba(255,255,255,0.015): a sheet of
   * tinted glass, which is right over a plain dark ground and wrong over a
   * textured one. With the plate behind it every card had studs showing through
   * its face, so the type sat on a moving background and the whole page read as
   * busy. The gradient is kept — top lighter than bottom, so a card still reads
   * as a lit panel rather than a flat rectangle — but it is now solid carbon and
   * the plate stops at the card edge.
   * *** THE SENTENCE THAT USED TO END THIS COMMENT — "which is what gives the
   * cards their lift" — WAS FALSE FOR AS LONG AS IT STOOD. Stopping the plate at
   * the card edge gives lift only if the card is LIGHTER than the plate, and
   * until 2026-11-05 it was darker, so the edge marked a hole instead. The lift
   * comes from the surface tokens above being brighter than the plate; this rule
   * only decides where the texture stops. A comment the measurement disproves is
   * worse than no comment. */
  background:linear-gradient(180deg, var(--carbon-800), var(--carbon-900));
  border:1px solid var(--line);
  border-radius:16px;
  box-shadow:0 1px 0 rgba(255,255,255,0.06) inset, 0 18px 50px rgba(0,0,0,0.45);
  position:relative;
}
.card{padding:28px 30px;}

/* ── THE FAQ ACCORDION ────────────────────────────────────────────────────
   Lifted out of generate_package_pages.mjs, where it was page-local, so that
   support.html can use the same control instead of growing a second one.
   Values are unchanged from that copy — this is a move, not a redesign — and
   the generator's duplicate is removed in the same commit so the two cannot
   drift apart. <details>/<summary> means it works with no JavaScript and is
   announced correctly to a screen reader. */
.faq-q{border-top:1px solid var(--line)}
.faq-q summary{list-style:none;cursor:pointer;padding:15px 0;font-family:var(--font-display);font-weight:700;font-size:1rem;display:flex;justify-content:space-between;gap:16px}
.faq-q summary::-webkit-details-marker{display:none}
.faq-q summary::after{content:"+";color:var(--ember-hot);font-family:'IBM Plex Mono',monospace;font-size:1.1rem}
.faq-q[open] summary::after{content:"–"}
.faq-q p{color:var(--ivory-dim);margin:0 0 15px}

/* ── READING MEASURE ──────────────────────────────────────────────────────
   Measured 2026-10-31 with a canvas text metric, not estimated: 54 paragraphs
   across the site ran past 78 characters a line, the worst at 165 (trade
   school), 151 (pricing FAQ), 148 (v3), 147 (packages). Comfortable reading is
   60-75. Past about 80 the eye loses its place coming back for the next line
   and the reader slows down without knowing why.

   The two lists below are not a guess at where body copy lives — they are the
   containers the measurement actually found, split by their COMPUTED
   text-align. That split matters: capping width alone would leave a centred
   paragraph's box hard against the left of its section with the text centred
   inside it, which looks worse than the long line did.

   .card.glass is centred on packages but .card.glass.sup-card is left-aligned,
   hence the :not(). */
/* 60ch, not 72ch. 1ch is the width of "0", which in this face is 10.09px
   against an 8.41px average character — so 72ch measured out at 86 real
   characters, still past the comfortable range. Calibrated against the
   rendered text rather than assumed. */
:root{ --measure:60ch; }

/* left-aligned body copy: cap the width, leave the box where it is */
:is(.faq-q, .faq-item, .ts-faq, .faq-mini, .lib-head, .lib-sec, .calc-hero) > p,
.card.glass.sup-card > p{ max-width:var(--measure); }

/* centred body copy: cap the width AND keep the box centred */
:is(.page-head, .ts-curr-head) > p,
.section > p,
.cta-final > .wrap > p,
.card.glass:not(.sup-card) > p,
.calc-card.glass > p,
.card.glass.sup-card.sup-cta > p.sup-note,
.wrap > p.ts-price,
.wrap > p.rr-line{ max-width:var(--measure); margin-inline:auto; }

/* ── THE SHARED CARD EDGE ──────────────────────────────────────────────────
   One page had the finished card and forty did not. Counted 2026-10-31: 94
   bordered boxes across pricing, calculators, library, support and trade
   school were still wearing `var(--line)`, an 8%-white hairline that reads at
   a glance as an unstyled div rather than a designed card.

   The treatment already existed in three places and had never been promoted:
   supply.html's .sup-card at 0.30, packages' .pkg-card2 at 0.38, v3's bands at
   0.34. This is that value, shared, at 0.34 — between the two established
   weights rather than a fourth invention.

   border-COLOR only, deliberately: width, radius and the panel gradient stay
   where each component already set them, so promoting this cannot change a
   layout. Anything that wants a different weight still overrides it locally,
   which is why packages keeps its own 0.38 for a 28-card grid.

   .glass itself is NOT targeted — it is the base for modals and bare panels and
   a gold edge does not belong on every one of those. But .card IS targeted, and
   signup.html and login.html build their panel from it, so those two did pick
   the edge up. Checked rather than assumed, and kept: a single centred panel
   cannot be mistaken for a card in a grid, and the edge is the brand. Saying
   otherwise here would be a comment that the stylesheet disproves. */
:root{ --card-edge:rgba(244,197,33,0.34); --card-edge-hover:rgba(244,197,33,0.70); }
.card,
.calc-card,
.block-toggle,
.faq-item,
.lib-grid li > a{ border-color:var(--card-edge); }

/* Hover belongs with the edge, or the card goes gold and then does nothing when
   you point at it. Transition border-color only; .panel-hover already animates
   transform and shadow and we must not fight it. */
.calc-card,
.block-toggle,
.lib-grid li > a{ transition:border-color .16s ease, box-shadow .16s ease; }
.calc-card:hover,
.block-toggle:hover,
.lib-grid li > a:hover{ border-color:var(--card-edge-hover);
  box-shadow:0 0 0 1px rgba(244,197,33,0.28), 0 12px 30px rgba(244,197,33,0.10); }
.panel-hover{transition:transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;}
.panel-hover:hover{transform:translateY(-3px); border-color:rgba(244,197,33,0.35); box-shadow:0 1px 0 rgba(255,255,255,0.06) inset, 0 24px 60px rgba(0,0,0,0.55);}

/* THE STICKY HEADER WAS EATING THE HEADING IT SCROLLED YOU TO.
   Measured on the live page 2026-10-22: the header is 114px and sticky, and
   scroll-margin-top was 0 everywhere. Clicking "How it works" in the nav landed
   that section's heading at +100px — 14px behind the header; #features landed
   at -21px, i.e. 135px behind it and completely invisible. The reader arrives
   mid-paragraph and has to scroll UP to find out where they are.
   scroll-margin-top is the one property that fixes this: it only affects where
   a scroll-into-view STOPS, so it changes no layout and nothing visual. Set a
   little above the header's own height so the heading clears it rather than
   touching it. */
:is(section, .section, .photo-band)[id]{ scroll-margin-top: 128px; }
@media (max-width: 760px){ :is(section, .section, .photo-band)[id]{ scroll-margin-top: 104px; } }

/* ── SECTION RHYTHM, ONE SCALE ───────────────────────────────────────────────
   Every mid-page gap on a page is PREVIOUS bottom + NEXT top, so the two have
   to be chosen together or the page drifts. Before these tokens the page had
   two mid-page gaps 9px apart (135 and 144 at 903px) - too close to read as a
   deliberate difference, close enough to stop the page feeling locked. They
   are one value now; rhythm comes from the band alternation and the join mark,
   not from varying the gap. The join ornament below is positioned FROM these
   same tokens, so it cannot drift out of centre again. */
:root{
  --sec-pad:clamp(56px,9vw,110px);      /* every .section's bottom */
  --sec-pad-band:clamp(48px,7vw,92px);  /* a band's top: it owns a painted slab */
  /* *** THERE IS DELIBERATELY NO --join-pad ALIAS HERE. One was written and
     removed the same day: a custom property is SUBSTITUTED WHERE IT IS
     DECLARED, so "--join-pad:var(--sec-pad-band)" on :root freezes the global
     92px-clamp value and then inherits that frozen number everywhere. Setting
     --sec-pad-band on a page's <main> changed the bands and left the joins at
     the old value, which is how supply.html kept two mid-page gaps 5px apart
     after it had supposedly been put on the token. A join reads
     --sec-pad-band directly, so an override resolves at the SECTION and the
     padding and the ornament move together. */
}
.section{padding:var(--sec-pad) 0;}

/* ── SLABS ───────────────────────────────────────────────────────────────────
   Garett, 2026-10-08: "the sections seem to read as if it's one long page."
   Measured, and he is right for three compounding reasons: five of the six
   content sections set padding-top:0, so the only gap between them is the
   PREVIOUS section's bottom padding — half the intended separation; NO section
   has a background of its own, so every one sits on the identical plate; and
   .hair, the one divider rule in this file, is used zero times on the page.

   THE PAGE HAD NO ARCHITECTURE, WHICH IS ALSO WHY IT FELT INERT. The reveal
   system here is good — IntersectionObserver, staggered, reduced-motion
   honoured in six places — and it was animating content with no edges, so it
   read as one continuous scroll arriving in pieces. Motion amplifies
   structure; it cannot invent it. Slabs first.

   A band is NOT a new color. --carbon-900 is one step off the page base, lit
   by hairlines top and bottom so the eye reads a raised panel rather than a
   tint. Full-bleed, because a band that stops at the wrap is a card, and
   cards are already doing a different job on this page. */
.band{
  background:var(--carbon-900);
  border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);
  /* Full-bleed out of any centered parent without needing a wrapper change. */
  position:relative;
}
/* The lamp rakes the top edge of a slab, the same way it rakes the plate —
   one light source, or the page looks assembled from two photographs. */
.band::before{
  content:""; position:absolute; inset:0 0 auto 0; height:1px; pointer-events:none;
  background:linear-gradient(90deg, transparent, rgba(255,255,255,0.10) var(--lamp-x,70%), transparent);
}
/* A slab needs its own air. These sections had padding-top:0, which is what
   made them run together; the band earns the space back. */
.band.section{ padding-top:var(--sec-pad-band); }

@media (prefers-reduced-motion: reduce){ .band::before{ display:none; } }
.section-tight{padding:36px 0;}
.grid-2{display:grid; grid-template-columns:repeat(2,1fr); gap:20px;}
.grid-3{display:grid; grid-template-columns:repeat(3,1fr); gap:20px;}
@media (max-width:820px){ .grid-2,.grid-3{grid-template-columns:1fr;} }

.hair{border:none; border-top:1px solid var(--line); margin:0;}

/* ── SECTION JOINS, SITE-WIDE ────────────────────────────────────────────────
   Garett, 2026-10-29: "for all other pages id like for you to fix section
   transitions."

   The SLAB system above is the right idea from 2026-10-08 and it was never
   taken up: measured today, 37 pages carry .section and exactly TWO carry
   .band. So every other page still has the fault that entry describes in
   its own words — consecutive sections separated by nothing but padding, on
   one identical plate, with .hair used zero times.

   This is the treatment built and approved on the partner page, moved into
   the stylesheet rather than waiting for 37 pages to opt in: a hairline and
   a small diamond above every section that follows another, and one join
   paid for once instead of twice.

   *** IT DEPENDS ON NOTHING INSIDE THE SECTION, and that is deliberate.
   The partner-page version zeroed padding-top and gave the padding back to
   a child .wrap. Measured across every page before porting it: of 167
   sections only 17 have .wrap as their first child — 116 open with
   div.card and 20 with div.pkg-grid — so that version would have left 150
   sections with their content jammed against the mark. The padding stays on
   the SECTION, where it works whatever the markup holds.

   :has() stops a join getting two marks where a section already draws its
   own. The combinator is ~ and not +: index.html interleaves <style> blocks
   and anchor stubs between its sections, which breaks + silently. Where :has() is unsupported both rules drop and sections keep the
   padding they have today, which is the current behavior, not a broken one. */
/* .owns-join - a section that already draws its OWN top edge, so it declines
   the generic join ornament. :has(.sec-head) was meant to catch these and
   cannot: #trade-school is a bounded chapter with a full-width gold border and
   its own lamp and diamond, and it carries no .sec-head.

   *** THIS CLASS CHANGES NOTHING ON SCREEN TODAY, and that is the point of
   writing it down. MEASURED 2026-10-31: #trade-school::before and
   #trade-school::after are ID selectors declared in the page's own <style>,
   so they beat this rule on specificity AND on source order, and an element
   has only one ::before and one ::after. The join hairline and its 7px
   diamond have therefore NEVER drawn on that section - they were being
   silently discarded by the cascade, which looks identical to never having
   been written. The class makes the cascade's existing answer explicit, so
   that removing the page's lamp later cannot quietly hand this section a
   second mark on top of its chapter edge.
   The ornament opts out; the PADDING does not, so the 144px rhythm holds. */
main .section ~ .section:not(.band):not(:has(.sec-head)){
  padding-top:var(--sec-pad-band);
  position:relative;
}
/* *** THE RULE THAT STOOD HERE NEVER APPLIED, ON ANY PAGE, FROM THE DAY IT WAS
   WRITTEN. It read:
       main .section:has(~ .section:not(.band):not(:has(.sec-head)))
   and :has() MAY NOT BE NESTED INSIDE :has() - the selector is invalid, so the
   browser drops the whole rule silently. Measured in Chrome 2026-10-31:
   querySelectorAll on it throws SyntaxError, while the same selector with the
   inner :has() removed parses fine. Nothing anywhere reported it; the only
   visible symptom was the join ornament sitting LOW in its own gap, because
   the section above kept its full bottom padding (81px) against the 54px top
   below it.
   It is not reinstated, for a second reason found while fixing it: ~ matches
   ANY later sibling, so even a valid version would have tightened EVERY
   earlier section on the page, not the one above the join. The gap is now
   symmetric because both sides use the same token, and the ornament is
   centred by calculation rather than by a rule that has to find the previous
   element at all. */
main .section ~ .section:not(.band):not(:has(.sec-head)):not(.owns-join)::before{
  content:""; position:absolute; left:50%; transform:translateX(-50%);
  /* half the difference between the gap's two halves, so the mark sits in the
     middle of the gap and not at the top of this section */
  top:calc(-0.5 * (var(--sec-pad) - var(--sec-pad-band)));
  width:min(420px,72%); height:1px; pointer-events:none;
  background:linear-gradient(90deg, transparent 0%,
    rgba(244,197,33,0.34) 22%, rgba(244,197,33,0.34) 78%, transparent 100%);
}
main .section ~ .section:not(.band):not(:has(.sec-head)):not(.owns-join)::after{
  content:""; position:absolute; left:50%;
  top:calc(-0.5 * (var(--sec-pad) - var(--sec-pad-band)) - 3px);
  width:7px; height:7px; margin-left:-3.5px; pointer-events:none;
  background:var(--ember); transform:rotate(45deg);
  box-shadow:0 0 0 4px var(--carbon-950);
}

/* ========================== forms ============================= */
.field{margin-bottom:18px; text-align:left;}
.field label{
  display:block; font-family:var(--font-mono); font-size:var(--label-size); font-weight:var(--label-weight);
  color:var(--ink-faint); margin-bottom:8px; letter-spacing:var(--label-track); text-transform:uppercase;
}
.field input,.field select,.field textarea{
  width:100%; padding:13px 15px; border-radius:10px;
  background:rgba(255,255,255,0.04); border:1px solid var(--line-strong);
  color:var(--ink); font-family:var(--font-body); font-size:0.95rem; outline:none;
  transition:border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.field input::placeholder,.field textarea::placeholder{color:var(--ink-faint);}
.field input:focus,.field select:focus,.field textarea:focus{
  border-color:var(--ember);
  box-shadow:0 0 0 3px rgba(244,197,33,0.18);
  background:rgba(255,255,255,0.055);
}
.field .hint{font-size:0.78rem; color:var(--ink-faint); margin-top:7px;}
.checkbox-row{display:flex; align-items:flex-start; gap:10px; margin-bottom:16px;}
.checkbox-row input[type="checkbox"]{margin-top:3px; width:16px; height:16px; accent-color:var(--ember); flex-shrink:0;}
.checkbox-row label{font-size:0.86rem; color:var(--ink-dim); line-height:1.55;}
.checkbox-row label a{color:var(--ember-hot); font-weight:600;}

/* ====================== badges / tags ========================= */
.tag{
  font-family:var(--font-mono); font-size:var(--label-size); letter-spacing:0.06em;
  color:var(--classify); padding:6px 13px; border-radius:999px;
  background:rgba(255,255,255,0.04); border:1px solid var(--line); display:inline-block;
}
.badge{
  font-family:var(--font-mono); font-size:var(--label-size); font-weight:600; letter-spacing:var(--label-track);
  text-transform:uppercase; padding:4px 10px; border-radius:5px; display:inline-block;
}
.badge.live{background:rgba(91,215,146,0.12); color:var(--green); border:1px solid rgba(91,215,146,0.35);}
.badge.pending{background:rgba(245,176,76,0.12); color:var(--amber); border:1px solid rgba(245,176,76,0.35);}
.badge.demo{background:rgba(242,241,239,0.06); color:var(--ink-dim); border:1px solid var(--line-strong);}

.status-dot{width:8px; height:8px; border-radius:50%; display:inline-block;}
.status-dot.ok{background:var(--green); box-shadow:0 0 10px rgba(91,215,146,0.7);}
.status-dot.warn{background:var(--amber); box-shadow:0 0 10px rgba(245,176,76,0.7); animation:tf-pulse 1.5s ease-in-out infinite;}
@keyframes tf-pulse{0%,100%{opacity:1;} 50%{opacity:0.35;}}

/* ========================== tables ============================ */
.data-table{width:100%; border-collapse:collapse;}
.data-table th{
  font-family:var(--font-mono); font-size:var(--label-size); font-weight:var(--label-weight); letter-spacing:var(--label-track);
  text-transform:uppercase; color:var(--ink-faint); text-align:left;
  padding:10px 14px; border-bottom:1px solid var(--line-strong);
}
.data-table td{padding:13px 14px; font-size:0.9rem; color:var(--ink-dim); border-bottom:1px solid var(--line);}
.data-table tr:last-child td{border-bottom:none;}
.data-table tr:hover td{background:rgba(255,255,255,0.02);}

/* ========================== toast ============================= */
.toast{
  position:fixed; bottom:26px; left:50%; transform:translateX(-50%) translateY(20px);
  padding:13px 24px; border-radius:12px; font-size:0.88rem; font-weight:600; z-index:60;
  background:var(--carbon-800); border:1px solid var(--line-strong);
  box-shadow:0 18px 50px rgba(0,0,0,0.6);
  opacity:0; pointer-events:none; transition:opacity 0.25s ease, transform 0.25s ease;
}
.toast.show{opacity:1; transform:translateX(-50%) translateY(0);}

/* ============ the plate, and the lamp on it ====================
 * Diamond tread across every page, lit by a worklamp from the top right.
 *
 * TWO FIXED LAYERS, in the order a real surface has them:
 *   body::before  the PLATE  — assets/tread.svg, tiled
 *   body::after   the LAMP   — a warm pool top-right, a dark pool bottom-left
 *
 * The plate is masked along the same axis as the lamp, so the tread is strongest
 * where the light falls on it and sinks into the dark at the opposite corner.
 * That is the whole trick: an evenly-lit texture reads as wallpaper, and the
 * same texture with a light raking across it reads as metal.
 *
 * THE SHADOW IS WHAT SELLS THE LIGHT. The warm pool says where the lamp is; the
 * dark pool says where it isn't. Drop the second gradient and the first stops
 * looking like a light and starts looking like a stain in the corner.
 *
 * WARM, NOT WHITE: the lamp is the ember token at low alpha. White reads as
 * screen glare, and blue would fight every gold accent on the site.
 *
 * FIXED PSEUDO-ELEMENTS, NOT `background-attachment: fixed` ON BODY. The
 * one-liner works on desktop, but iOS Safari ignores fixed attachment and falls
 * back to scrolling, which on a long page stretches both the tile and the
 * gradient across the WHOLE document — a wash at the top and nothing below. The
 * lamp has to stay with the viewport. Fixed layers at negative z-index need
 * body's own background to be transparent (html keeps the ground color), or
 * they paint behind it and are invisible.
 *
 * KEPT QUIET ON PURPOSE. The plate sits at 5%: enough to feel like a surface
 * under the glass panels, not enough to compete with running text. At full
 * strength it mauls every line on the page — tried, looked at, rejected.
 *
 * Total cost: one 1.5 KB SVG and two gradients. */
body{ background-color:transparent; }
body::before{            /* the plate */
  content:""; position:fixed; inset:0; z-index:-2; pointer-events:none;
  background:url("tread.svg") repeat; background-size:128px 128px;
  opacity:0.55;
  /* The mask DIMS the plate toward the shadow corner, it does not delete it.
     An earlier version faded to fully transparent and the bottom-left simply
     looked like the texture had been taken away rather than unlit — metal in
     shadow is still metal. It bottoms out at 38%, so the tread is present on
     every part of every page and merely rakes brighter where the lamp lands. */
  -webkit-mask-image:radial-gradient(140% 120% at var(--lamp-x) var(--lamp-y), #000 0%, rgba(0,0,0,.72) 40%, rgba(0,0,0,.38) 100%);
  mask-image:radial-gradient(140% 120% at var(--lamp-x) var(--lamp-y), #000 0%, rgba(0,0,0,.72) 40%, rgba(0,0,0,.38) 100%);
}
body::after{             /* the lamp */
  content:""; position:fixed; inset:0; z-index:-1; pointer-events:none;
  background:
    radial-gradient(118% 88% at var(--lamp-x) var(--lamp-y),
      rgba(244,197,33,0.13) 0%,
      rgba(244,197,33,0.058) 20%,
      rgba(244,197,33,0.018) 40%,
      rgba(244,197,33,0) 66%),
    /* The shadow is cast AWAY from the lamp, so it takes the opposite corner.
       Pinning it while the light moves is what makes fake lighting look fake. */
    radial-gradient(95% 75% at calc(100% - var(--lamp-x)) calc(100% - var(--lamp-y)),
      rgba(0,0,0,0.48) 0%, rgba(0,0,0,0) 62%);
}
/* Someone who asked for less motion has not asked for less light. Neither layer
   ANIMATES — there is no keyframe, no transition, no timer. The lamp simply sits
   wherever --lamp-x/--lamp-y point, and a pointer may move them. Under
   prefers-reduced-motion the script never attaches, the variables keep their
   defaults, and the page is lit from the corner exactly as before. */

/* ========================== footer ============================ */
footer.site-footer{
  position:relative; z-index:1;
  border-top:1px solid var(--line);
  padding:56px 0 44px; margin-top:40px;
  color:var(--ink-faint); font-size:0.85rem; text-align:left;
}
/* The footer carries NO tread of its own any more.
 * It had one while the plate was footer-only; now that the plate covers every
 * page, a second copy here would stack on the first and make this one strip
 * visibly denser than the rest of the surface — the seam you would notice
 * without being able to say why. The page-wide layers above cover it. */
footer.site-footer > *{ position:relative; z-index:1; }
footer.site-footer .brand-line{
  font-family:var(--font-display); font-weight:800; letter-spacing:0.08em;
  text-transform:uppercase; font-size:1rem; color:var(--ink); margin-bottom:8px;
}
footer.site-footer .brand-line em{font-style:normal; color:var(--ember);}
footer.site-footer a{color:var(--ink-dim); transition:color 0.2s ease;}
footer.site-footer a:hover{color:var(--ember-hot);}
footer.site-footer .foot-links{
  display:flex; gap:26px; margin-top:22px; flex-wrap:wrap; justify-content:flex-start;
  font-family:var(--font-mono); font-size:var(--label-size); letter-spacing:var(--label-track); text-transform:uppercase;
}
footer.site-footer .foot-bottom{
  margin-top:34px; padding-top:22px; border-top:1px solid var(--line);
  font-family:var(--font-mono); font-size:var(--label-size); letter-spacing:var(--label-track);
  color:var(--ink-faint); display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px;
}

/* ================== build-note (dev banner) =================== */
.build-note{
  position:relative; z-index:1;
  max-width:100%; margin:0; padding:8px 28px;
  font-family:var(--font-mono); font-size:var(--label-size); letter-spacing:0.06em;
  color:var(--ink-faint); text-align:center;
  border-bottom:1px solid var(--line);
  background:rgba(255,255,255,0.02);
}
.build-note b{color:var(--amber); font-weight:600;}
.build-note[hidden]{display:none;}

/* ====================== scroll reveals ======================== */
/* GATED, like forge.css: the hidden state exists only once a script has run
   and declared itself able to undo it. An ungated opacity:0 means a failed
   or blocked script leaves blank elements on the page forever. */
html.js-reveal .reveal{opacity:0; transform:translateY(26px); transition:opacity 0.7s cubic-bezier(0.2,0.65,0.3,1), transform 0.7s cubic-bezier(0.2,0.65,0.3,1);}
html.js-reveal .reveal.in{opacity:1; transform:none;}
/* *** ONCE REVEALED, HAND THE TRANSITION BACK TO THE COMPONENT. 2026-11-05.
   The reveal rule above is `html.js-reveal .reveal` — specificity (0,2,1) — and
   it sets `transition: opacity .7s, transform .7s` with a stagger delay. Any
   component transition declared on a plain class, such as .panel-hover at
   (0,1,0), LOSES to it for the whole life of the element, not just during the
   reveal. MEASURED on a revealed .tsc-track: computed transition came back as
   "opacity 0.7s ... 0.08s, transform 0.7s ...", so a hover lift would have
   crawled over 700ms behind an 80ms delay instead of the 250ms the hover was
   written for — and every card on the site that is both revealed and hoverable
   had the same fault, silently, because nothing errors.
   These two rules out-specify the reveal only for elements that have finished
   revealing, so the entrance keeps its 0.7s and the interaction gets its own
   timing back. */
html.js-reveal .reveal.in.panel-hover{transition:transform 0.25s cubic-bezier(0.2,0.7,0.3,1), border-color 0.25s ease, box-shadow 0.25s ease;}
html.js-reveal .reveal.in.card{transition:transform 0.18s cubic-bezier(0.2,0.7,0.3,1.2), box-shadow 0.18s ease, filter 0.18s ease, border-color 0.25s ease;}
html.js-reveal .reveal.d1{transition-delay:0.08s;} html.js-reveal .reveal.d2{transition-delay:0.16s;}
html.js-reveal .reveal.d3{transition-delay:0.24s;} html.js-reveal .reveal.d4{transition-delay:0.32s;}
@media (prefers-reduced-motion:reduce){
  html.js-reveal .reveal{opacity:1; transform:none; transition:none;}
  html{scroll-behavior:auto;}
  *,*::before,*::after{animation-duration:0.01ms !important; animation-iteration-count:1 !important;}
}

/* ======================== marquee ============================= */
.marquee{
  overflow:hidden; border-top:1px solid var(--line); border-bottom:1px solid var(--line);
  padding:16px 0; position:relative; z-index:1;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
}
.marquee-track{display:flex; gap:0; width:max-content; animation:tf-marquee 36s linear infinite;}
.marquee-track span{
  font-family:var(--font-mono); font-size:var(--label-size); letter-spacing:var(--label-track);
  text-transform:uppercase; color:var(--classify); white-space:nowrap; padding:0 18px;
  display:inline-flex; align-items:center; gap:36px;
}
.marquee-track span::after{content:"·"; color:var(--ember); font-size:1rem;}
@keyframes tf-marquee{from{transform:translateX(0);} to{transform:translateX(-50%);}}
@media (prefers-reduced-motion:reduce){ .marquee-track{animation:none;} }

/* ====================== KPI / stat bits ======================= */
.kpi{padding:18px 20px;}
.kpi .kpi-num{
  font-family:var(--font-display); font-weight:800; font-size:1.7rem; letter-spacing:-0.02em;
  color:var(--ink); line-height:1.1;
}
.kpi .kpi-num em{font-style:normal; color:var(--ember-hot);}
.kpi .kpi-label{font-family:var(--font-mono); font-size:var(--label-size); letter-spacing:var(--label-track); text-transform:uppercase; color:var(--ink-faint); margin-top:6px;}

.spark{display:block; width:100%; height:34px; margin-top:10px;}
.spark polyline{fill:none; stroke:var(--ember); stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round;}
.spark .spark-fill{fill:url(#sparkGrad); stroke:none; opacity:0.25;}

/* certified-recipe stamp */
.stamp{
  display:inline-flex; align-items:center; gap:7px;
  font-family:var(--font-mono); font-size:var(--label-size); font-weight:600;
  letter-spacing:var(--label-track); text-transform:uppercase;
  color:var(--ember-hot); border:1px solid rgba(244,197,33,0.45);
  border-radius:4px; padding:4px 9px;
}
.stamp::before{content:"✓";}

/* ============ Miter setup step (per-package configuration) ============ */
.setup-group{
  margin:14px 0 4px; padding:16px 18px; border-radius:14px;
  background:rgba(255,255,255,0.03); border:1px solid var(--line);
}
.setup-group-title{
  font-size:var(--label-size); font-weight:800; letter-spacing:var(--label-track); text-transform:uppercase;
  color:var(--gold-soft); margin-bottom:14px;
}
.setup-field{ margin-bottom:15px; }
.setup-field:last-child{ margin-bottom:0; }
.setup-field label{
  display:block; font-size:0.86rem; font-weight:600; color:var(--ivory); margin-bottom:6px;
}
.setup-field label .opt{ font-weight:400; color:var(--ivory-dim); font-size:0.8rem; }
.setup-field input[type=text], .setup-field input[type=tel], .setup-field input[type=number],
.setup-field input[type=time], .setup-field select, .setup-field textarea{
  width:100%; padding:10px 12px; border-radius:10px; font:inherit; font-size:0.9rem; color:var(--ivory);
  background:rgba(255,255,255,0.04); border:1px solid var(--line-strong); outline:none;
}
.setup-field input:focus, .setup-field select:focus, .setup-field textarea:focus{
  border-color:var(--ember); box-shadow:0 0 0 3px rgba(244,197,33,0.15);
}
.setup-field textarea{ resize:vertical; }
.setup-help{ font-size:0.78rem; color:var(--ivory-dim); margin-top:5px; line-height:1.5; }
.setup-hours{ display:flex; align-items:center; gap:9px; }
.setup-hours input{ flex:1; }
.setup-hours span{ font-size:0.84rem; color:var(--ivory-dim); }
.setup-check{ display:flex; align-items:center; gap:9px; cursor:pointer; }
.setup-check input{ width:17px; height:17px; accent-color:var(--ember); }

/* ── Hub: business records the owner maintains ───────────────────────────── */
.records{ margin-top:34px; }
.records-head h2{ font-size:1.15rem; margin:0 0 4px; }
.records-head p{ font-size:.86rem; color:var(--ivory-dim); margin:0 0 18px; }
.rec-block{ margin-bottom:26px; }
.rec-title{ display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
.rec-title h3{ font-size:.95rem; margin:0; }
.rec-add{ padding:6px 14px; font-size:.8rem; }
.rec-list{ border:1px solid rgba(255,255,255,.07); border-radius:12px; overflow:hidden; }
.rec-row{ display:flex; align-items:center; gap:12px; padding:13px 16px;
  border-bottom:1px solid rgba(255,255,255,.05); font-size:.9rem; }
.rec-row:last-child{ border-bottom:0; }
.rec-main{ flex:1; min-width:0; }
.rec-name{ color:var(--ivory); }
.rec-sub{ font-size:.78rem; color:var(--ivory-dim); margin-top:2px; }
.rec-when{ font-family:'IBM Plex Mono',monospace; font-size:.78rem; white-space:nowrap; }
.rec-soon{ color:#f0c33c; } .rec-late{ color:#e8734a; } .rec-ok{ color:var(--ivory-dim); }
.rec-btn{ background:none; border:0; color:var(--ivory-dim); cursor:pointer; font-size:.78rem; padding:4px 6px; }
.rec-btn:hover{ color:var(--ivory); }
.rec-empty{ padding:20px 16px; font-size:.86rem; color:var(--ivory-dim); text-align:center; }
.rec-modal{ position:fixed; inset:0; background:rgba(0,0,0,.62); display:flex;
  align-items:center; justify-content:center; padding:20px; z-index:80; }
.rec-modal[hidden]{ display:none; }
.rec-dialog{ width:100%; max-width:420px; padding:26px 28px; }
.rec-dialog h3{ margin:0 0 16px; font-size:1.05rem; }
.rec-actions{ display:flex; gap:10px; justify-content:flex-end; margin-top:20px; }
.rec-err{ color:#e8734a; font-size:.82rem; margin:12px 0 0; min-height:1.1em; }
.rec-link{ font-family:'IBM Plex Mono',monospace; font-size:var(--micro-size); color:var(--gold-soft);
  word-break:break-all; display:block; margin-top:6px; }

/* ── the deal, stated where it is decided ──────────────────────────────────
   Two components, one job: put the shape of the commitment next to the price
   instead of in an FAQ two screens down. Every claim here is checked against
   terms.html — 14 days, not the 30 a competitor prints, and cancel says
   "effective at the end of the billing period" because self-serve cancel
   routes through Miter until the Stripe leg is wired. A trust badge that
   overstates is worse than no badge, because it is the one line a wary
   buyer actually tests. */

.deal-terms{ margin:16px 0 0; padding:14px 0 0; border-top:1px solid rgba(255,255,255,0.1);
  display:flex; flex-direction:column; gap:8px; text-align:left; }
.deal-terms > div{ display:flex; gap:12px; align-items:baseline; flex-wrap:wrap; }
.deal-terms dt{ font-family:'IBM Plex Mono',monospace; font-size:var(--label-size); text-transform:uppercase;
  letter-spacing:0.06em; color:var(--ivory-dim); flex:0 0 auto; min-width:112px; }
.deal-terms dd{ margin:0; font-size:0.84rem; color:var(--ivory); flex:1 1 160px; }

/* "> span" is load-bearing: every pill CONTAINS a <span class="ico">, and the bare
   descendant selector painted that icon with the pill's own 3% background, border,
   radius and padding - a 26px ghost blob around a 14px glyph, on 31 pages. */
.trust-row{ display:flex; flex-wrap:wrap; gap:8px; margin-top:18px; }
.trust-row > span{ display:inline-flex; align-items:center; gap:7px; font-size:var(--micro-size);
  color:var(--ivory-dim); border:1px solid var(--line-strong); border-radius:999px;
  padding:6px 12px; background:rgba(255,255,255,0.03); }
.trust-row b{ color:var(--ivory); font-weight:600; }
@media (max-width:480px){
  /* Stacked beats squeezed: four pills across a phone is unreadable. */
  .trust-row{ gap:6px; }
  .trust-row > span{ font-size:var(--label-size); padding:5px 10px; }
  .deal-terms dt{ min-width:0; flex:1 1 100%; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE HEADER, TOUCH TARGETS AND FORM LABELS   (w32 · w33 · w34)
   Added 2026-08-17 after measuring, not after looking.

   ALL OF IT IS CSS ON PURPOSE. The header markup is duplicated across 54 HTML
   files and is NOT uniform — index carries gap:14px inline, pricing carries
   gap:12px, signup and 404 have no CTA at all, and the customer-facing booking
   and photo-intake pages have no nav whatsoever. A markup fix would be 54 edits
   against four shapes; a stylesheet fix is one file and reaches all of them.

   w32 — THE PRIMARY CTA WAS CLIPPED BETWEEN 901px AND ~1080px.
   The old rule stacked the navbar at max-width:900px, but the single-row
   layout does not actually fit until about 1080px, and nothing between those
   two numbers had ever been laid out. Measured: "Create Free Account"
   overflowed the header by 76px at 960 and 45px at 1000, and the header
   scrolled sideways at both. That band is iPad landscape, small laptops and a
   1920 monitor split in half — and the thing being cut off is the button the
   entire funnel points at. The breakpoint moves to 1080.

   w33 — AND THE STACKED HEADER WAS A DESKTOP NAV REFLOWING.
   181px tall, 22% of a 812px phone screen, on 13 of 14 pages, with eight links
   wrapped across four rows. Now two rows: brand and actions on the first, the
   links on the second as ONE horizontally scrollable strip. Nothing is hidden
   behind a tap — a site whose job is explaining a product should not bury its
   own navigation — but nothing costs four rows either. The right-hand fade is
   the affordance that says the strip continues.

   w34 — 44px, WHICH IS THE POINT OF THE WHOLE PRODUCT.
   Header links were 30px, footer links 18px, and the booking and photo-intake
   buttons — the ones a CUSTOMER of a customer taps — were 38px. This is
   software for somebody holding a phone on a job site with dirty hands.
   Apple and Google both put the floor at 44 and they are right.

   WHAT IS DELIBERATELY NOT CHANGED: the small mono uppercase labels. 0.66rem
   eyebrows and KPI captions read fine because they are short, wide-tracked and
   set in a mono face — raising them all would flatten the type hierarchy to
   fix a number rather than a problem. The ONE exception is form labels, which
   were also 0.66rem: a field label is something you read to answer, not a
   caption you glance at, and 10.56px is too small for that job.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width:1240px){
  .navbar{
    flex-wrap:wrap; height:auto; align-items:center;
    padding-top:9px; padding-bottom:0; row-gap:0; gap:12px;
  }
  /* The actions wrapper carries an inline `gap` on some pages, and inline wins
     over a class — so this is the one place !important earns its place. */
  .navbar > div:last-child{ gap:10px !important; }

  nav.links{
    order:3; width:100%;
    flex-wrap:nowrap; overflow-x:auto; overscroll-behavior-x:contain;
    gap:22px; margin-top:2px; padding-bottom:2px;
    scrollbar-width:none;
    -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 26px),transparent);
            mask-image:linear-gradient(90deg,#000 calc(100% - 26px),transparent);
  }
  nav.links::-webkit-scrollbar{ display:none; }

  nav.links a{ display:flex; align-items:center; min-height:44px; padding:0; }
  .account-link{ display:inline-flex; align-items:center; min-height:44px; padding:0 4px; }

  footer.site-footer .foot-links a,
  footer.site-footer .foot-bottom a{
    display:inline-flex; align-items:center; min-height:44px;
  }
}

/* The smallest phones: brand and CTA both shrink so the top row stays ONE row.
   Without this the actions wrap to a third row and the saving is given back. */
@media (max-width:560px){
  .brand{ font-size:0.86rem; gap:8px; }
  .brand .mark{ width:26px; height:26px; }
  .cta-small{ padding:9px 14px; font-size:0.78rem; }
}

/* Every primary button, at every width. */
.cta-small,.cta-big,.btn-gold{ min-height:44px; }

/* A form label is read in order to answer it. 0.66rem was 10.56px; the
   letter-spacing comes down as the size goes up so the label does not grow
   wider than the field it belongs to. */
.field label{ font-size:0.82rem; letter-spacing:var(--label-track); }

/* The first row still wrapped on a 375px phone, which put the saving straight
   back: brand 154px + actions 219px needs 385px inside a 347px space, so the
   header stayed three rows at 152px instead of the two it was designed for.
   Shaving type until it fit would have cost 25px of legibility on the one
   device this product is built for.

   So below 420px the wordmark steps aside and the TF mark carries the brand
   alone — the ordinary answer, and a deliberate-looking one. The words are
   still in the DOM and still read aloud; they simply take no width. Above
   420px "TRADE FORGE" is back and the row fits on its own. */
@media (max-width:420px){
  .brand{ font-size:0; gap:0; }
  .brand .mark{ font-size:0.78rem; }
  .navbar{ padding-left:16px; padding-right:16px; }
}

/* The logo is a link home and was a 26px target once the wordmark stepped
   aside — the smallest thing left on the page, and the one people reach for
   to get back. It gets the same 44px as everything else. */
.brand{ min-height:44px; }

/* ═══════════════════════════════════════════════════════════════════════════
   GOLD NAVIGATION   (Garett's direction, 2026-08-18: gray font → gold)

   The nav and footer links were gray mono caps (--ink-dim, 6.58:1 — legal,
   and gray). They now speak the same brass the Hub's department tabs speak,
   so the two surfaces read as one product. --brass is the Hub's --gold-ink
   verbatim, measured 9.60:1 on this site's near-black; --brass-dim is the
   quieter tier at 8.74:1. EMBER STAYS THE ACTION COLOR: hover and the active
   tab still answer in ember, so gold is structure here exactly as it is in
   the Hub, and never a call to act.
   ═══════════════════════════════════════════════════════════════════════════ */
:root{
  --brass:#e3c169;
  --brass-dim:#e8c678;
}
/* Garett, 2026-08-18: the TOP tabs read white, not gold — gold stays in the
   footer and the Hub, where it labels structure. White resting, ember hover
   and active, so the top strip is quiet ink until you reach for it. */
nav.links{ color:var(--ink); }
nav.links a:hover{ color:var(--ember-hot); }
nav.links a.active{ color:var(--ember-hot); }
.account-link{ color:var(--ink); }
.account-link:hover{ color:var(--ember-hot); }
footer.site-footer .foot-links{ color:var(--ink-dim); }
footer.site-footer .foot-links a{ color:var(--ink-dim); }
footer.site-footer .foot-links a:hover{ color:var(--ember-hot); }

/* footer tap targets. Measured 2026-10-31: every footer link rendered 21px
   tall, under the 24px minimum, on all eight pages. They sit in a row rather
   than inside a sentence, so the inline-text exemption does not apply.
   padding-block on an INLINE element grows the clickable box without growing
   the line box, so the hit area reaches ~33px and the footer does not move.
   Row gap is 26px, well clear of the 6px each side. */
footer.site-footer .foot-links a,
footer.site-footer .foot-bottom a{ padding-block:6px; }

/* ── the calculator gallery, as cards ─────────────────── (2026-08-18) ── */
.calc-hero{ margin:34px 0 10px; }
.calc-hero .eyebrow{ font-family:var(--font-mono); font-size:var(--label-size); letter-spacing:var(--label-track);
  text-transform:uppercase; color:var(--ember-hot); margin-bottom:10px; }
.calc-group{ margin-top:40px; }
.calc-group h2{ display:flex; align-items:center; gap:10px; font-size:1.15rem; }
.calc-group-count{ font-family:var(--font-mono); font-size:var(--label-size); color:var(--ink-dim);
  border:1px solid var(--line-strong); border-radius:20px; padding:1px 9px; margin-left:2px; }
.calc-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr));
  gap:14px; margin-top:16px; }
.calc-card{ display:flex; flex-direction:column; gap:7px; padding:18px 18px 14px;
  /* --card-edge, not --line: this rule sits BELOW the shared card block and a
     later rule of equal specificity wins, so the shared promotion could not
     reach it. Caught by measuring: 21 cards stayed faint after the promotion. */
  border:1px solid var(--card-edge); border-radius:14px; min-height:44px; }
.calc-card b{ font-family:var(--font-display); font-size:1rem; color:var(--ink); }
.calc-card span{ font-size:0.85rem; color:var(--ink-dim); line-height:1.5; flex:1; }
.calc-card .calc-open{ font-style:normal; font-family:var(--font-mono); font-size:var(--label-size);
  letter-spacing:var(--label-track); text-transform:uppercase; color:var(--ink-dim); margin-top:6px;
  transition:color 0.2s ease; }
.calc-card:hover .calc-open{ color:var(--ember-hot); }

/* ═══════════════════════════════════════════════════════════════════════════
   ONE CALCULATOR PAGE — inputs, then the answer beneath.  (2026-08-21, UI audit)

   The generated pages (calculator-*.html) reference .calc-box / .calc-field /
   .calc-answer and nothing styled them, so the fields fell back to inline flow:
   on a phone the label, its input and the NEXT field all ran onto one line and
   zig-zagged. Each field is a block now — label over a full-width input, hint
   beneath — two to a row on a wide screen, one to a row on a phone (the device
   a contractor actually holds in the yard).
   ═══════════════════════════════════════════════════════════════════════════ */
.calc-page{ max-width:780px; }
.calc-page .lede{ color:var(--ink-dim); font-size:1.02rem; line-height:1.6; margin:6px 0 4px; }
.calc-box{
  margin-top:24px; padding:22px clamp(16px,3vw,26px);
  background:var(--carbon-850); border:1px solid var(--line); border-radius:16px;
}
.calc-box form{ display:grid; grid-template-columns:1fr 1fr; gap:16px 20px; }
.calc-field{ display:flex; flex-direction:column; gap:7px; }
.calc-field > span{
  font-family:var(--font-mono); font-size:var(--label-size); font-weight:var(--label-weight);
  letter-spacing:var(--label-track); text-transform:uppercase; color:var(--ink-faint);
}
.calc-field > span em{ font-style:normal; color:var(--brass); margin-left:2px; }
.calc-field input{
  width:100%; box-sizing:border-box; padding:13px 15px; border-radius:10px;
  background:rgba(255,255,255,0.04); border:1px solid var(--line-strong);
  color:var(--ink); font-family:var(--font-mono); font-size:1rem; min-height:48px;
  outline:none; transition:border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.calc-field input:focus{
  border-color:var(--ember); box-shadow:0 0 0 3px rgba(244,197,33,0.18);
  background:rgba(255,255,255,0.055);
}
.calc-field small{ font-size:0.78rem; color:var(--ink-faint); line-height:1.45; }

.calc-answer{ margin-top:20px; }
.calc-headline, .calc-line{
  display:flex; flex-wrap:wrap; justify-content:space-between; align-items:baseline;
  gap:4px 12px; padding:11px 0; border-bottom:1px solid var(--line);
}
.calc-headline{ padding-top:6px; border-bottom:1px solid var(--line); }
.calc-line:last-child, .calc-headline:last-child{ border-bottom:none; }
.calc-headline span, .calc-line span{ color:var(--ink-dim); }
.calc-headline b, .calc-line b{ font-family:var(--font-mono); color:var(--ink); font-variant-numeric:tabular-nums; white-space:nowrap; }
.calc-headline b{ font-size:1.5rem; color:var(--ember-hot); }
.calc-headline small, .calc-line small{ width:100%; color:var(--ink-faint); font-size:0.8rem; }

.calc-meta{ margin-top:40px; }
.calc-meta h2{ font-size:1.1rem; margin:22px 0 8px; }
.calc-meta p.mono{
  font-family:var(--font-mono); color:var(--ink-dim); font-size:0.9rem;
  background:var(--carbon-850); border:1px solid var(--line); border-radius:10px;
  padding:12px 14px; overflow-x:auto;
}
.calc-meta ul{ margin:8px 0 0; padding-left:20px; color:var(--ink-dim); line-height:1.6; }
.calc-meta li{ margin-bottom:8px; }
.calc-cta{
  margin-top:40px; padding:24px clamp(18px,3vw,28px);
  background:var(--carbon-850); border:1px solid var(--line); border-radius:16px;
}
.calc-cta h2{ font-size:1.15rem; margin:0 0 10px; }
.calc-cta p{ color:var(--ink-dim); line-height:1.6; margin:0 0 14px; }
.calc-cta p:last-child{ margin:0; }
@media (max-width:560px){
  .calc-box form{ grid-template-columns:1fr; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE NAV — a hamburger for the header.  (2026-08-21, UI audit)

   The links are inline in every page's <nav class="links">. On a phone that
   strip scrolled sideways behind a fade almost nobody discovers, so Calculators,
   Trade School, Blog and Support read as "cut off". attribution.js injects a .nav-toggle
   button and marks the navbar .has-toggle; these rules then fold the same links
   into a tap-to-open panel. PROGRESSIVE ENHANCEMENT: the mobile rules are scoped
   to .has-toggle, so with JS off the links keep their scroll-strip and are never
   hidden with no way back. Specificity (.navbar.has-toggle nav.links) beats the
   ≤1240px scroll-strip rules, so no source-order juggling is needed.
   ═══════════════════════════════════════════════════════════════════════════ */
.nav-toggle{
  display:none; flex-direction:column; justify-content:center; gap:5px;
  width:44px; height:44px; padding:0; margin:0; cursor:pointer;
  background:none; border:0; color:var(--ink);
}
.nav-toggle span{ display:block; width:22px; height:2px; border-radius:2px; background:currentColor;
  transition:transform 0.2s ease, opacity 0.2s ease; }
.navbar.nav-open .nav-toggle span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
.navbar.nav-open .nav-toggle span:nth-child(2){ opacity:0; }
.navbar.nav-open .nav-toggle span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }

@media (max-width:820px){
  .navbar.has-toggle{ position:relative; }
  .navbar.has-toggle .nav-toggle{ display:inline-flex; margin-left:auto; }

  .navbar.has-toggle nav.links{
    position:absolute; top:100%; left:0; right:0; z-index:50;
    display:none; flex-direction:column; align-items:stretch;
    gap:0; margin:0; padding:6px clamp(20px,4vw,44px) 12px;
    background:rgba(10,11,13,0.98);
    backdrop-filter:blur(20px) saturate(150%); -webkit-backdrop-filter:blur(20px) saturate(150%);
    border-bottom:1px solid var(--line-strong);
    overflow:visible; -webkit-mask-image:none; mask-image:none;
  }
  .navbar.has-toggle.nav-open nav.links{ display:flex; }
  .navbar.has-toggle nav.links a{
    width:100%; min-height:50px; padding:0; align-items:center;
    border-top:1px solid var(--line); font-size:0.82rem;
  }
  .navbar.has-toggle nav.links a:first-child{ border-top:none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   AMBER CARD PREVIEW — 2026-09-27, Garett's idea: the site reads very dark, and
   contrast should mark the path toward buying.

   INERT BY DEFAULT. Every rule is scoped to html.amber, which only the localhost
   toggle adds. Nothing here reaches a visitor. When Garett picks, these either
   get promoted into the real rules above or this whole block is deleted.

   THE DOSAGE ARGUMENT, since it is the part worth disagreeing about: gold on
   EVERY card would raise the noise floor and mean nothing leads. What guides is
   ESCALATION — a quiet card, a gold edge on the one being considered, a gold CTA
   at the end. So this lifts the resting state a little, fixes the hover, and
   gives ONE card per section a real edge. Not six.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 1. RESTING — cards currently sit on rgba(255,255,255,0.08), white at 8%, which
      is why they read as flat. Warmed toward ember and lifted, still quiet. */
html.amber .glass{
  border-color:rgba(244,197,33,0.16);
  box-shadow:0 1px 0 rgba(255,255,255,0.06) inset, 0 18px 50px rgba(0,0,0,0.45);
}

/* 2. HOVER — this is a BUG FIX, not a preference. The existing hover border is
      rgba(244,197,33,0.35), the OLD TradeForge orange, left behind by the rename.
      Ember gold, and a gold glow instead of pure black shadow. */
html.amber .panel-hover:hover{
  transform:translateY(-3px);
  border-color:rgba(244,197,33,0.55);
  box-shadow:0 1px 0 rgba(255,255,255,0.06) inset,
             0 24px 60px rgba(0,0,0,0.55),
             0 0 0 1px rgba(244,197,33,0.18),
             0 10px 40px rgba(244,197,33,0.14);
}

/* 3. FOCAL — one card per section carries a real gold edge, so the eye has
      somewhere to land. Add class="featured" to whichever package should lead. */
html.amber .cat-card.featured{
  border-color:rgba(244,197,33,0.5);
  box-shadow:0 1px 0 rgba(255,255,255,0.08) inset,
             0 18px 50px rgba(0,0,0,0.45),
             0 0 34px rgba(244,197,33,0.16);
}

/* ============================================================================
   PACKAGE CATEGORY ICONS

   These replace the emoji that used to carry the category system (phone,
   calendar, receipt, chart, shield). Emoji render differently on every OS,
   cannot take the brand color, and read as a placeholder nobody replaced --
   on pages where a contractor is deciding whether to spend $79 a month.

   Masked rather than <img> so the glyph inherits `color` from its context and
   can be tinted ember on a card or dim in a list. One mechanism, used by both
   the hardcoded markup and the JS renderers in app.js / miter.html.

   Five keys, one per category, matching BLOCKS[].icon in assets/app.js:
     catch   Never miss work        run     Run the day
     paid    Get paid               grow    Grow
     protect Protect the business
   ========================================================================= */
.ico{ display:inline-block; width:1.15em; height:1.15em; vertical-align:-0.18em;
      /* min-* as well as width, deliberately. Page rules like `.calc-card span
         { flex:1 1 0% }` outrank a single-class selector and collapse a
         flex-basis-0 child to nothing, which is exactly how these rendered at
         0px wide on pricing.html. min-width/min-height are not set by those
         rules, so the icon keeps its box without a specificity fight.
         max-* for the same reason, added 2026-10-30: min-* guards the FLOOR
         but not the CEILING, and the same `flex:1 1 0%` grew a trust-strip
         icon to 283px on pricing.html — the full width of its pill. Capped
         both ends and the icon is immune to whatever a page does to spans. */
      min-width:1.15em; min-height:1.15em;
      max-width:1.15em; max-height:1.15em;
      background-color:currentColor; flex:none;
      -webkit-mask:var(--ico) center/contain no-repeat; mask:var(--ico) center/contain no-repeat; }
.ico-catch{ --ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5.5 3.5h3l1.5 4-2 1.5a12 12 0 0 0 5.5 5.5l1.5-2 4 1.5v3a2 2 0 0 1-2.2 2A16.5 16.5 0 0 1 3.5 5.7 2 2 0 0 1 5.5 3.5z'/%3E%3Cpath d='M14.5 3.5a6 6 0 0 1 6 6'/%3E%3C/svg%3E"); }
.ico-run{ --ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='5' width='17' height='15.5' rx='2'/%3E%3Cpath d='M3.5 9.5h17M8 3v4M16 3v4'/%3E%3Cpath d='M7.5 13.5h3v3h-3z'/%3E%3C/svg%3E"); }
.ico-paid{ --ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 3.5h14v17l-2.3-1.6-2.4 1.6-2.3-1.6-2.4 1.6L7.3 19 5 20.5z'/%3E%3Cpath d='M8.5 8.5h7M8.5 12.5h7M8.5 16h4'/%3E%3C/svg%3E"); }
.ico-grow{ --ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 17.5 9 12l3.5 3.5 7.5-7.5'/%3E%3Cpath d='M15.5 8h5v5'/%3E%3Cpath d='M3.5 20.5h17'/%3E%3C/svg%3E"); }
.ico-protect{ --ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3 5 6v6c0 4.4 3 8.2 7 9.2 4-1 7-4.8 7-9.2V6z'/%3E%3Cpath d='m9 12 2.2 2.2L15.5 10'/%3E%3C/svg%3E"); }

/* The trust strip: refund, any-phone, cancel-any-time, flat-monthly. Same
   mechanism as the category icons; the shield reuses .ico-protect. */
.ico-mobile{ --ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='6.5' y='2.5' width='11' height='19' rx='2.4'/%3E%3Cpath d='M10.5 18.5h3'/%3E%3C/svg%3E"); }
.ico-cancel{ --ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 6l12 12M18 6L6 18'/%3E%3C/svg%3E"); }
.ico-check{ --ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12.5 9.5 17.5 19.5 7'/%3E%3C/svg%3E"); }

/* ─── the answer arriving ────────────────────────────────────────────────────
   The calculators' whole argument is that the arithmetic is shown rather than
   hidden, so the answer assembles instead of blinking into place: the headline
   first, then each step a fraction behind it.

   IT RUNS ONCE. The script re-uses the rows on every later keystroke instead of
   rebuilding them, so this never replays while you are typing — motion that
   fires on every keypress would turn a calculator into a slot machine.

   Transform and opacity only, so it composites on the GPU and costs a phone
   nothing. No layout property is touched. */
@keyframes calc-step-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.calc-step {
  opacity: 0;
  animation: calc-step-in 260ms cubic-bezier(.2,.7,.3,1) forwards;
}
/* Someone who asked for less motion still has to be able to read the answer, so
   this resets to the FINISHED state rather than simply removing the animation —
   the rows start at opacity 0, and dropping the keyframe without this would
   leave them invisible forever. That is the classic reduced-motion bug. */
@media (prefers-reduced-motion: reduce) {
  .calc-step { opacity: 1; transform: none; animation: none; }
}

/* The one line that replaced the six beat cards under the film (2026-10-22).
   Quiet on purpose: the video is the argument now, this is the version for a
   crawler, a screen reader, and anyone on data-saver who never sees it play. */
.rr-line{ max-width:68ch; margin:clamp(18px,2.2vw,28px) auto 0; text-align:center;
  font-size:.98rem; line-height:1.65; color:var(--ink-dim,var(--ivory-dim)); }
.rr-line b{ color:var(--ink,var(--ivory)); font-weight:650; }

/* An anchor with no section of its own (2026-10-22). #catalog and #how outlived
   the sections that carried them; these keep index.html#how and #catalog landing
   somewhere sensible instead of doing nothing. Zero height, so it occupies no
   layout and the reader lands on the section that follows it — but it needs the
   same scroll-margin as a real section or it lands behind the sticky header,
   which is the bug the rule above this one exists to fix. */
.anchor-stub{ display:block; height:0; scroll-margin-top:128px; }
@media (max-width:760px){ .anchor-stub{ scroll-margin-top:104px; } }

/* The five-step path, compressed into one line when the HOW IT WORKS section was
   cut for repeating four other sections. */
.price-path{ margin:16px 0 0; font-size:.92rem; line-height:1.6;
  color:var(--ink-dim,var(--ivory-dim)); }
.price-path b{ color:var(--ink,var(--ivory)); font-weight:650; }

/* ── MOTION UPGRADES: START (apply_motion_upgrades.mjs — do not hand-edit) ── */
/* MEASURED BEFORE ANY OF THIS WAS WRITTEN, which is the only reason it is
   aimed correctly. Card-like elements that move on hover, before:
       index.html     6 of 31
       pricing.html   0 of 14
       packages.html  0 of 32
   .panel-hover already existed and worked. The 29 product cards on
   packages.html are .pkg-card2 and the 9 price rows on pricing.html are
   .block-toggle, and neither class had ever been given the behaviour. A rule
   that is correct and pointed at the wrong files is indistinguishable from a
   rule nobody wrote.

   NOTHING BELOW CHANGES A CARD AT REST. No resting shadow, no resting border,
   no resting transform — the site looks exactly like itself until a cursor
   arrives. The diamond plate (body::before) and the gold wash (body::after)
   are not touched; redefining either silently removes the texture of every
   page, which is a mistake already made and measured once. */

:root{
  --lift-hover: 0 6px 16px -4px rgba(0,0,0,.52), inset 0 1px 0 rgba(255,255,255,.058);
}

/* *** .tsc-track WAS MISSING FROM EVERY PASS, and the rendered-page auditor
   found it on its first run: the six curriculum stage cards on the Trade
   School page DO have a .tsc-track:hover transform, written long ago, and it
   loses the cascade at (0,2,0) to html.js-reveal .reveal.in at (0,3,1) — the
   same cancellation as the original miss, on a class nobody thought to list.
   This is the FOURTH appearance of this one bug. The lesson is not "add this
   class": it is that a hover rule and a working hover are different things,
   and only a probe that resolves the cascade can tell them apart. */
.pkg-card2, .block-toggle, .calc-card, .tfb-card, .ts-course, .result-box, .tsc-track{
  transition: transform .2s cubic-bezier(.2,.7,.3,1.2),
              box-shadow .2s ease, border-color .2s ease;
}
.pkg-card2:hover, .block-toggle:hover, .calc-card:hover, .tfb-card:hover, .ts-course:hover, .tsc-track:hover,
.pkg-card2:focus-within, .block-toggle:focus-within, .calc-card:focus-within,
.tfb-card:focus-within, .ts-course:focus-within{
  transform: translateY(-2px);
  box-shadow: var(--lift-hover);
  border-color: rgba(255,255,255,.18);
}

/* *** THE REASON THE CARDS NEVER MOVED, AND HE HAD TO SAY IT THREE TIMES.
 * MEASURED with a real cursor on a real card: hovered true, transform none.
 * Then read out of the cascade:
 *     .panel-hover:hover            transform: translateY(-3px)   (0,2,0)
 *     html.js-reveal .reveal.in     transform: none               (0,3,1)
 * (0,3,1) beats (0,2,0), so the scroll-reveal's finished state OVERWRITES the
 * hover transform on every card that fades in — which is most cards on the
 * site. The hover rule was never missing; it was being cancelled, silently,
 * by the entrance animation that runs on the same element.
 *
 * This is the same trap recorded on 2026-11-05, one step further in: that fix
 * corrected the TRANSITION with html.js-reveal .reveal.in.panel-hover and the
 * TRANSFORM kept losing, because that rule never set one. Checking that a
 * matching rule EXISTS is not checking that the element moves — only hovering
 * it and reading the computed transform is.
 *
 * These selectors sit at (0,5,1) and win. */
html.js-reveal .reveal.in.panel-hover:hover,
html.js-reveal .reveal.in.card:hover,
html.js-reveal .reveal.in.pkg-card2:hover,
html.js-reveal .reveal.in.calc-card:hover,
html.js-reveal .reveal.in.tfb-card:hover,
html.js-reveal .reveal.in.ts-course:hover,
html.js-reveal .reveal.in.block-toggle:hover,
html.js-reveal .reveal.in.tsc-track:hover,
html.js-reveal .reveal.in.panel-hover:focus-within,
html.js-reveal .reveal.in.card:focus-within,
html.js-reveal .reveal.in.pkg-card2:focus-within,
html.js-reveal .reveal.in.calc-card:focus-within,
html.js-reveal .reveal.in.tfb-card:focus-within,
html.js-reveal .reveal.in.ts-course:focus-within,
html.js-reveal .reveal.in.tsc-track:focus-within{
  transform: translateY(-2px);
  box-shadow: var(--lift-hover);
  border-color: rgba(255,255,255,.18);
}

/* The plain .card family was left out of the first pass entirely — the four
   panels at the top of the Trade School are .card.glass.ts-card and nothing
   in the upgrade named them. */
.card, .ts-card{
  transition: transform .2s cubic-bezier(.2,.7,.3,1.2), box-shadow .2s ease, border-color .2s ease;
}
.card:hover, .ts-card:hover, .card:focus-within, .ts-card:focus-within{
  transform: translateY(-2px);
  box-shadow: var(--lift-hover);
  border-color: rgba(255,255,255,.18);
}

/* THE CURSOR GETS A LIGHT — "interact with the user being there". A card lit
   from where the pointer actually is reads as a real surface under a worklamp
   rather than a rectangle that moved. .04 alpha: visible as you sweep a row,
   invisible in a screenshot. Defaults to the card's centre so a card nobody
   has touched is unchanged and nothing depends on the script having run. */
.pkg-card2, .calc-card, .tfb-card{ position:relative; overflow:hidden; }
.pkg-card2::after, .calc-card::after, .tfb-card::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  opacity:0; transition: opacity .25s ease;
  background: radial-gradient(220px circle at var(--mx,50%) var(--my,50%),
              rgba(255,255,255,.04), transparent 60%);
}
.pkg-card2:hover::after, .calc-card:hover::after, .tfb-card:hover::after{ opacity:1; }

/* Prices and lesson counts form a column instead of a ragged edge. One
   declaration, and it is the cheapest "looks considered" change on the site. */
.ts-price, .tsc-n, .tsc-meta, .price, .stat, .num, table td, table th,
[class*="price"], [class*="count"]{
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

/* Page to page: the header stays put and the body cross-fades, instead of the
   screen blanking. Browsers without support ignore the whole block and
   navigate normally — there is nothing to feature-detect. */
@view-transition{ navigation: auto; }
::view-transition-old(root){ animation: tf-fade-out .14s ease both; }
::view-transition-new(root){ animation: tf-fade-in .24s ease both; }
@keyframes tf-fade-out{ to{ opacity:0; transform: translateY(-4px); } }
@keyframes tf-fade-in{ from{ opacity:0; transform: translateY(6px); } }
header.site-header{ view-transition-name: site-header; }

@media (prefers-reduced-motion: reduce){
  .pkg-card2, .block-toggle, .calc-card, .tfb-card, .ts-course{ transition:none; }
  .pkg-card2:hover, .block-toggle:hover, .calc-card:hover, .tfb-card:hover, .ts-course:hover,
  .card:hover, .ts-card:hover,
  html.js-reveal .reveal.in.panel-hover:hover, html.js-reveal .reveal.in.card:hover,
  html.js-reveal .reveal.in.pkg-card2:hover{ transform:none; }
  ::view-transition-old(root), ::view-transition-new(root){ animation:none; }
}
/* ── MOTION UPGRADES: END ── */
