/* ==========================================================================
   preview.css — PREVIEW-ONLY CHROME. Nothing in this file belongs to the
   business whose site it sits on, and nothing in it survives purchase.

   WHY THIS IS IN THE SKELETON AT ALL, given that the skeleton ships no
   components: because it is not one. It carries no business content, decides
   nothing about how the site looks, and is DELETED AT PURCHASE — so it can
   never appear in the artifact the DNA registry measures, which is the whole
   reason site.css ships no hero and no card grid. It is the second member of
   a category tweaks.js already established: house-wide machinery that serves
   the preview window and that the buyer never sees. (house-tech-spec §2, §5,
   §7; ad-ruling--bar-in-skeleton §2.4.)

   AT PURCHASE, delete: this file, its <link> in every page, the
   @preview:expiry block in every page, buy.html, and initExpiry() and its
   call in js/main.js. README.md carries the checklist; head-audit.mjs
   BLOCKERs a `status: sold` site that still carries any of it.

   THE 40 KB CSS CAP DOES NOT MEASURE THIS FILE. It is excluded by name in
   scripts/qa/lib/site.mjs, exactly as shippedJsFiles excludes tweaks.js, and
   for the same stated reason: deletability at purchase, not byte count
   (house-tech-spec §7, amended 2026-08-20).

   THE FENCE — read this before editing anything below.
   This file may reference ONLY tokens that tokens.css guarantees on every
   site, plus the one preview-only token declared below. It may NOT introduce
   a colour, a radius, a shadow, a motion value or a breakpoint. That fence is
   what keeps preview chrome from becoming a shipped design component.

   A BRIEF MAY OVERRIDE ANY OF IT, in this file, in the site's own copy —
   exactly as a brief extends site.css. Two overrides are common and both are
   the brief's, never the skeleton's:
     1. Colour, when the brief has a chrome ground of its own.
     2. STICKY STACKING. The house default is static flow (see below).
   ========================================================================== */

:root {
  /* The one preview-only token. It cannot live in tokens.css: that is the file
     the buyer inherits, and this value must not survive purchase. It resolves
     to --layout-tap-min rather than restating 44px, because a raw size literal
     outside tokens.css is a house blocker (spec §3) and because the bar's
     height IS the tap minimum — that is where the number comes from (spec §8). */
  --expiry-height: var(--layout-tap-min);
}

/* THE HOUSE DEFAULT IS STATIC FLOW, first in <body>, above the header.

   It is deliberately NOT sticky. A sticky bar puts a permanent offset into
   every first-screen calculation on the page, and the first screen is a brief
   commitment computed per site at every breakpoint (design-rules §5, §7.1). A
   skeleton default that a brief must UNDO is a design decision; a default a
   brief may EXTEND is a contract. This one can only be extended.

   To make it sticky, the BRIEF says so and the site's copy of this file adds:
     .expiry     { position: sticky; inset-block-start: 0; }
     .site-header{ position: sticky; inset-block-start: var(--expiry-height); }
     section[id] { scroll-margin-block-start:
                     calc(var(--expiry-height) + <the brief's header height>); }
   ...and the brief re-derives its first screen with the stack included. Both
   bars must be `sticky`, never `fixed`: sticky occupies flow height from first
   paint, so the stack costs no CLS.

   THE GROUND is the site's own --color-ink with --color-bg as its ink: the
   page's own pair, inverted. Three reasons, none of them a preference.
     1. It is the ONE pair every brief has already measured for AA (§4 of the
        brief, the body-text row), and inverting a pair does not change its
        ratio — so this bar passes contrast on every site without a new row.
     2. The page never uses that pair as a full-width field, so the strip reads
        as scaffolding rather than as one of the page's own sections.
     3. --color-accent is NOT available: it is the one colour that means "act"
        (tokens.css), and handing it to non-content demotes the page's own
        primary CTA 100px below it.                                          */
/* STICKY, PER BRIEF §5.1. The season rail is `position: sticky` above
   1024px, and its `top` offset is the measured height of the sticky chrome
   stack currently present (brief §5.1: "expiry bar + header, or header
   alone once sold"). That only holds if both bars are actually sticky, so
   this brief exercises the override css/preview.css's own header comment
   documents rather than the house static-flow default. Both are `sticky`,
   never `fixed`, so the stack costs no CLS (occupies flow height from first
   paint). */
.expiry {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-header);
  background-color: var(--color-ink);
  color: var(--color-bg);
}

.expiry__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  min-height: var(--expiry-height);
}

/* One line, never two. If a brief makes the bar sticky, the header's offset is
   a fixed token and a bar that wrapped would let the page slide under the
   header — there is no graceful degradation available. The copy is written to
   a measured width budget at 320px, and what yields is the button label first
   (design-rules §5: every repeated ornament states its minimum and what yields
   at that minimum). */
.expiry__text {
  font-size: var(--text-eyebrow);
  line-height: var(--leading-snug);
}

/* The Buy control. Geometry, tap target and transition are .btn's; only the
   colour pair and the side padding change. It is a chip of the page sitting on
   the chrome — the same ink/bg pair again, so the ratio is the site's own
   body-text ratio and is AA by construction. */
.btn--buy {
  flex: 0 0 auto;
  padding-inline: var(--space-sm);
  background-color: var(--color-bg);
  color: var(--color-ink);
}

/* THE ONLY CONTROL THAT TAKES MONEY HAD NO EDGE ON THE ONE PAGE THAT SELLS.

   `.btn--buy` is calibrated for the expiry bar, where a `--color-bg` fill on a
   dark ground measures 12.6:1 and reads unmistakably as a button. `buy.html`
   reuses the same class on the page body — and the page body IS `--color-bg`,
   while `.btn`'s border is `transparent` (site.css). Fill against ground:
   **1.00:1**. No border, no underline. The control was a paragraph of text.

   Measured on the rendered artifact by the Critic at
   `alex-landscaping-and-tree-trimming-service-duluth-mn` round 3, and present on
   SIX built sites because it came from here. `render-audit` reported buy.html
   clean at 6.26:1 throughout — it measures TEXT against background, which was
   fine; WCAG 1.4.11 non-text contrast, which is what a control's own boundary
   is, was checked by nothing.

   THE BORDER COLOUR IS NOT A NEW AESTHETIC DECISION. `--color-ink` against
   `--color-bg` is the pairing this exact control already uses to make its own
   edge visible — see `.btn--buy:focus-visible` immediately below, which was
   reasoned out and shipped for the same problem in the focus state. This
   extends that decision to the resting state rather than inventing a look.

   SCOPED AWAY FROM THE BAR ON PURPOSE. Inside `.expiry` the control is correct,
   Critic-passed on six sites, and must not move. */
:not(.expiry__inner) > .btn--buy {
  border-color: var(--color-ink);
}

/* THE FOCUS RING IS INSET, and this is a house default rather than a per-site
   fix for a measured reason. --color-focus is validated against --color-bg and
   --color-surface only (tokens.css) — it says nothing about a dark ground, and
   the first two briefs this pipeline produced BOTH failed WCAG 1.4.11 on
   exactly that. An outward ring would also land on the page below the bar,
   which is a second unvalidated pairing. Contained inside the control, the ring
   is the site's own ink/bg pair again and cannot fail on any site. */
.btn--buy:focus-visible {
  outline-color: var(--color-ink);
  outline-offset: calc(var(--space-3xs) * -1);
}

/* NO MOTION. Nothing in the bar reveals, transitions or animates beyond .btn's
   inherited hover. It never carries data-reveal. Chrome that stays still does
   not compete with the page's one hero moment (design-rules §0), and
   initExpiry()'s single textContent write happens inside a fixed-height,
   non-wrapping box, so it cannot reflow anything below it. Under
   prefers-reduced-motion nothing about the bar changes, because nothing about
   it was moving. */
