/* ============================================================
   FEATURE ROWS for /sales/

   READ THIS BEFORE LOOKING FOR A SCROLL ENGINE. There is not one any
   more. These blocks were built as scroll driven stages: a tall
   section, a panel pinned with position sticky, and three points that
   lit up one at a time as the visitor scrolled through. That was
   removed on 24 August 2026 because the owner's clients read it as
   broken. The dimmed points looked like a page that had failed to load
   rather than a deliberate effect, and a visitor who thinks the page is
   broken does not stay to find out otherwise. Subtlety lost to clarity,
   correctly.

   stage.js is deleted. The class names still say "stage" because
   renaming every one of them across the markup would be a large diff
   for no change in behaviour, and a misleading name is cheaper to
   explain than a risky rename is to verify. It is explained here.

   What is left is a plain alternating two column row: copy on one side,
   a photograph or a built panel on the other, everything visible at
   once. The scroll version is in git history if it is ever wanted back.

   Loaded by /sales/ only, on top of landing.css, which all four landing
   pages share. Nothing in here can reach /admin/, /accounting/ or
   /seo/.
   ============================================================ */

/* ---------- the section lead-in ---------- */

/* Sits on the same green band as the row below it, with no padding
   between them, so the two sections read as one block rather than as a
   heading stranded on cream above a colour change. */
.what-you-get { padding-bottom: 0; }
.what-you-get + .stage.band-dark { padding-top: 2.2rem; }
.what-you-get h2 {
  font-size: clamp(2.2rem, 6vw, 4.2rem);
  /* 1.15, not 1. At line-height 1 the descender of the y sat below the
     line box and was clipped on the owner's screen. The small padding
     underneath is the same fix from the other side. */
  line-height: 1.15;
  padding-bottom: 0.1em;
  letter-spacing: -0.03em;
  text-align: center;
  margin: 0;
  /* Cream, like every other heading on the dark bands. It was briefly
     espresso at the owner's request on 24 August 2026 and reverted the
     same day: dark on olive did not suit, which is also what the
     contrast numbers said. */
}

/* ---------- the row ---------- */

.stage { position: relative; }
.stage-pin { display: block; }

.stage-grid {
  width: min(100% - 2.4rem, var(--container));
  margin-inline: auto;
  display: grid;
  gap: 1.6rem;
  align-items: center;
}

@media (min-width: 900px) {
  .stage-grid { grid-template-columns: 0.95fr 1.05fr; gap: 3.4rem; }
  /* Alternate which side the visual sits on down the page. Five rows
     with the picture always on the right reads as a template. */
  .stage.flip .stage-visual { order: -1; }
}

.stage-copy h2 {
  font-size: clamp(2rem, 5.6vw, 4.1rem);
  line-height: 1.02;
  letter-spacing: -0.025em;
  margin: 0 0 1.1rem;
}

/* ---------- the points ---------- */

.stage-steps {
  display: grid;
  gap: 1.05rem;
  margin: 1.5rem 0 0;
  padding: 0;
  list-style: none;
}

.stage-step {
  display: grid;
  grid-template-columns: 24px 1fr;
  gap: 0.85rem;
  align-items: center;
}

.stage-step p { margin: 0; font-size: 1.14rem; line-height: 1.35; }

/* Always filled. In the scroll version this ring started empty and
   filled as each point became active, and that is exactly the detail
   that read as a broken page. */
.stage-tick {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1.5px solid var(--terracotta);
  color: var(--terracotta);
  display: grid;
  place-items: center;
}
.stage-tick::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
}

/* Terracotta sits too close to the dark bands to read, so it goes sage
   there instead. */
.band-dark .stage-tick { border-color: var(--sage); color: var(--sage); }

/* The call to action that sits under the workstation panel. Centred to
   the panel above it, clear of the panel's shadow. */
.stage-cta { margin: 1.6rem 0 0; text-align: center; }

/* The founder portrait. landing.css gives .feature-photo a 3:2 landscape
   frame, tuned for the old team group shot, and object-fit: cover crops
   whatever does not fit. Sean's portrait is 3:4 the other way, so in
   that frame the crop took his head off. Overridden here, in the sales
   page's own file, because /admin/ and /accounting/ still use the 3:2
   frame for landscape photographs and must keep it. */
.feature-photo {
  aspect-ratio: 3 / 4;
  max-width: 440px;
  width: 100%;
  margin-inline: auto;
}

/* ---------- the visual side ---------- */

.stage-visual { position: relative; }

.stage-photo {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* A labelled hole where a photograph goes. Deliberately obvious rather
   than a tasteful grey box: a placeholder that looks like a design
   decision is a placeholder that ships. Nothing carrying this class may
   be deployed. */
.stage-photo-todo {
  border: 2px dashed var(--terracotta);
  border-radius: var(--radius);
  aspect-ratio: 4 / 3;
  display: grid;
  place-content: center;
  text-align: center;
  gap: 0.35rem;
  padding: 1.5rem;
  color: var(--terracotta);
  font-size: 0.84rem;
  line-height: 1.5;
}
.stage-photo-todo b { font-family: var(--font-display); font-size: 1rem; }
.band-dark .stage-photo-todo { color: var(--sage); border-color: var(--sage); }

/* ---------- panel: shared chrome for the built visuals ----------
   The coach scorecard and the lead list are built in DOM rather than
   screen recorded. Text stays crisp at any resolution, the whole thing
   weighs a few KB against megabytes of video, it is legible on a phone
   with the sound off, and nothing real is exposed: no API key, no
   login, no customer record. */

.panel {
  background: var(--cream);
  border: 1px solid rgba(35, 27, 24, 0.1);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.band-dark .panel { background: #17110f; border-color: rgba(253, 251, 247, 0.14); }

.panel-bar {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid rgba(35, 27, 24, 0.09);
  background: rgba(35, 27, 24, 0.03);
}
.band-dark .panel-bar {
  border-bottom-color: rgba(253, 251, 247, 0.12);
  background: rgba(253, 251, 247, 0.04);
}
.panel-bar i { width: 9px; height: 9px; border-radius: 50%; background: rgba(35, 27, 24, 0.18); }
.band-dark .panel-bar i { background: rgba(253, 251, 247, 0.22); }
.panel-bar span {
  margin-left: auto;
  font-size: 0.71rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.55;
}
.panel-body { padding: 1.05rem; }

/* ---------- device: the IP and connectivity row ---------- */

.device-row {
  display: grid;
  grid-template-columns: 20px 1fr auto;
  align-items: center;
  gap: 0.7rem;
  padding: 0.72rem 0;
  border-bottom: 1px dashed rgba(253, 251, 247, 0.12);
  font-size: 0.91rem;
}
.device-row:last-child { border-bottom: 0; }
.device-row .dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  margin-inline: auto;
  background: var(--sage);
}
.device-row .state {
  font-size: 0.74rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--sage);
}

/* ---------- coach scorecard ---------- */

.bubble {
  max-width: 88%;
  margin: 0 0 0.7rem;
  padding: 0.75rem 0.95rem;
  border-radius: 14px;
  font-size: 0.92rem;
  line-height: 1.5;
}
.bubble.them { background: rgba(35, 27, 24, 0.06); border-bottom-left-radius: 4px; }
.bubble.us { background: rgba(184, 90, 63, 0.13); margin-left: auto; border-bottom-right-radius: 4px; }
.bubble b {
  display: block;
  font-size: 0.69rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  opacity: 0.55;
  margin-bottom: 0.25rem;
}

.score {
  border-top: 1px solid rgba(35, 27, 24, 0.1);
  margin-top: 0.9rem;
  padding-top: 0.9rem;
}
.score-row {
  display: grid;
  grid-template-columns: 9.5rem 1fr 1.5rem;
  align-items: center;
  gap: 0.7rem;
  margin-bottom: 0.55rem;
  font-size: 0.86rem;
}
.score-row .bar {
  height: 7px;
  border-radius: 4px;
  background: rgba(35, 27, 24, 0.1);
  overflow: hidden;
}

/* Drawn at its value. In the scroll version this started at zero width
   and animated on arrival, which means that with the script gone every
   bar would otherwise have sat empty. */
.score-row .bar i {
  display: block;
  height: 100%;
  width: var(--v);
  border-radius: 4px;
  background: var(--olive);
}
.score-row b { text-align: right; font-variant-numeric: tabular-nums; }

.score-note {
  margin: 0.8rem 0 0;
  padding: 0.6rem 0.75rem;
  background: rgba(184, 90, 63, 0.1);
  border-left: 3px solid var(--terracotta);
  border-radius: 0 8px 8px 0;
  font-size: 0.85rem;
}

/* This caption is not decoration. The scorecard shows illustrative
   numbers, and without a label saying so they read as a client result,
   which would be a fabricated statistic. Do not remove it. */
.score-caption { margin: 0.7rem 0 0; font-size: 0.73rem; opacity: 0.6; }

/* ---------- lead list ---------- */

.lead-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.2rem 0.9rem;
  padding: 0.6rem 0;
  border-bottom: 1px dashed rgba(35, 27, 24, 0.12);
  font-size: 0.86rem;
}
.lead-row:last-child { border-bottom: 0; }
.lead-row .co { font-weight: 600; }
.lead-row .who { grid-column: 1; opacity: 0.65; font-size: 0.79rem; }

/* The blanked out contact details are the argument, not a limitation of
   the mock: it says we hold verified numbers and emails without putting
   a real person's details on a public page. */
.lead-row .redacted {
  grid-column: 2;
  grid-row: 1 / 3;
  align-self: center;
  background: rgba(35, 27, 24, 0.7);
  border-radius: 4px;
  width: 6.5rem;
  height: 0.85rem;
}

/* ---------- seat availability ----------
   Not a row, but sales page only like everything else in this file. The
   number itself is rendered from the SEATS constant at the foot of
   /sales/index.html. Read the comment there first. */

.seats {
  margin: 0.9rem 0 0;
  font-family: var(--font-body);
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--terracotta);
  letter-spacing: 0.01em;
}

.waitlist-note {
  max-width: 34rem;
  margin: 1.6rem auto 0;
  padding: 0.85rem 1rem;
  border: 1px solid var(--sage);
  border-radius: var(--radius);
  font-size: 0.95rem;
}

.sticky-seats {
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
  align-self: center;
}

/* ---------- narrow screens ---------- */

@media (max-width: 899px) {
  /* Photographs get capped so the copy under them is not pushed off a
     phone screen. The built panels are not capped: clipping one cuts
     the bottom off the scorecard, which is the part that sells. */
  .stage-photo, .stage-photo-todo { max-height: 34vh; object-fit: cover; }
  .stage-photo-todo { aspect-ratio: 16 / 9; }

  /* The founder's portrait shares the .stage-photo class with the team
     and office photos above, purely for the border-radius and shadow
     that class already carries, not for anything scroll-length related.
     The 34vh cap two lines up was written for those two, whose 3:2 shape
     means capping their height on a phone is the right call. Sean's
     photo is 3:4, tall on purpose, and the same cap squashed it from
     482px down to 289px, well short of what its own aspect-ratio asks
     for. object-fit: cover then had to crop a wide, short window out of
     a tall portrait to fill that squashed box, and centred cropping
     took the top of his head with it. Confirmed by measuring both
     numbers directly rather than guessing from the screenshot: the
     container was 482px, the image inside it 289px. Restoring its full
     height here, scoped to exactly this photo so the team and office
     photos keep the cap they were written for. */
  .feature-photo .stage-photo { max-height: none; }

  .stage-grid { gap: 1.1rem; }

  /* Centred on mobile only, and deliberately not on a wide screen. Once
     the grid stacks to one column here, .stage-copy sits directly under
     "What you get", which is centred, and left-aligned text under a
     centred heading reads as unfinished rather than as a considered
     choice. On the two-column desktop layout the same text sits beside
     a photograph instead, where left-aligned body copy is the normal,
     readable choice and centring it would be the wrong call, which is
     why this stays inside the mobile breakpoint rather than moving up
     to the base .stage-copy rule. */
  .stage-copy { text-align: center; }
  /* This used to shrink the gap under the heading on mobile, 0.8rem
     against the desktop rule's 1.1rem, to save vertical space on a
     phone. Owner's screenshot showed exactly the cost of that: the
     heading and the first bullet read as nearly touching. Reversed
     here to be MORE generous than desktop rather than less, 1.6rem,
     because a single narrow column stacking heading straight into a
     tight list needs more separation to read as intentional, not
     less, the opposite of the space-saving instinct that set 0.8rem
     in the first place. */
  .stage-copy h2 { margin-bottom: 1.6rem; }

  /* The list centres as ONE block, not row by row, and this is the
     second version of that fix, not the first.

     Attempt one gave .stage-steps justify-items: center, which let each
     .stage-step size to its own line and centre independently. "Never
     shared with another client" and "An Australian account manager on
     top of it" are not the same length, so every icon landed at a
     different horizontal position, zigzagging down the list, worse
     than the plain left alignment it was meant to fix.

     Attempt two gave .stage-steps width: max-content instead, sizing
     the block to its widest line and centring that as one unit, which
     did line the icons up. It also clipped text on the tech and IP row:
     "Company machine, locked down and encrypted" is wider than a phone
     screen at this font size, and max-content asked for the box to be
     exactly that wide regardless, so the line ran off the right edge
     rather than wrapping. max-width: 100% capped the OUTER box but did
     nothing for the grid column inside it, which had grid-template-
     columns: 22px auto, and an auto track does not shrink a line of
     text below its own natural width, only max-width on the box does.

     This version stops deriving the width from content at all. A fixed
     cap, min(26rem, 100%), gives every row the same width to stretch to
     regardless of its own text length, which is what actually keeps the
     icons aligned, and switching the text column from auto to 1fr lets
     it shrink and wrap normally inside that cap, which is what actually
     stops the overflow. Centred with margin-inline: auto same as
     before. Confirmed on the exact line that broke it: it wraps to two
     lines now with no clipping and no horizontal scroll. */
  /* gap bumped from the base 1.05rem to 1.5rem alongside the heading
     margin above: same complaint, same fix, more room between lines
     that were reading as too close together on a phone. */
  .stage-steps { max-width: min(26rem, 100%); margin-inline: auto; gap: 1.5rem; }
  .stage-step { grid-template-columns: 22px 1fr; text-align: left; }
  .stage-step p { font-size: 1.02rem; }
  .panel-bar { padding: 0.45rem 0.75rem; }
  .panel-body { padding: 0.75rem; }
  .bubble { font-size: 0.86rem; padding: 0.6rem 0.75rem; margin-bottom: 0.5rem; }
  .score { margin-top: 0.7rem; padding-top: 0.7rem; }
  .score-row { margin-bottom: 0.4rem; }
  .score-note { font-size: 0.8rem; padding: 0.5rem 0.6rem; margin-top: 0.6rem; }
  .device-row { padding: 0.58rem 0; font-size: 0.86rem; }
  .lead-row { padding: 0.5rem 0; font-size: 0.82rem; }
}

/* ---------- the sticky bar, on this page only ----------
   /sales/ is the only landing page with a seat count, so the bar here
   carries a line the other three do not. Tightened here rather than in
   landing.css so the other pages keep the bar they already have. */

@media (max-width: 799px) {
  .sticky-cta { padding: 0.45rem 4vw 0.55rem; }
  .sticky-cta .btn { padding: 0.72rem 1rem; }
  .sticky-seats {
    display: block;
    text-align: center;
    margin-bottom: 0.28rem;
    font-size: 0.76rem;
    opacity: 0.75;
  }
}

@media (max-width: 430px) {
  .score-row { grid-template-columns: 7.4rem 1fr 1.4rem; font-size: 0.8rem; }
  .lead-row .redacted { width: 4.4rem; }
}

/* ============================================================
   THE COST COMPARISON, on the pricing card
   Sales page only, like everything else in this file. Every figure
   in it is an external market rate and the sources are in a
   comment beside the markup. Read that before touching a number.
   ============================================================ */

/* The head of the card is centred, per the owner. The comparison
   rows below it stay left and right aligned, because a column of
   figures has to line up to be read. */
.price-panel .price-now,
.price-panel .price-was,
/* margin-inline: auto is not decorative here, it is the fix. landing.css
   sets a global "p { max-width: 62ch; }", which narrows this line to
   about 524px inside a 570px panel. A narrower block with no auto
   margins sits flush left by default, so text-align: center was only
   ever centring the text inside a box that itself was not centred.
   Caught from a screenshot, 24 August 2026: the line rendered left of
   centre. Same trap logged twice already on this page, for the section
   furniture and the homepage hero, both times for the same reason. */
.price-panel .seats { text-align: center; margin-inline: auto; }
.price-panel .price-now { margin-top: 0.9rem; }

/* Card chrome from the owner's mock of 24 August 2026: a maroon rule
   around the card, and the badge sitting astride the top edge rather
   than inside it. The badge needs the panel to be its positioning
   context, and the panel needs breathing room above so the overhanging
   half of the badge does not collide with the seat line of the section
   before it. */
.price-panel {
  position: relative;
  border: 1px solid var(--maroon);
  margin-top: 1.4rem;
}
.price-panel .promo-badge {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  white-space: nowrap;
}

.stack-lead {
  margin: 1.9rem 0 0.6rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--linen);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--espresso-soft);
  text-align: center;
  /* Same trap as the seat line above it, and the fourth time it has
     bitten this page: landing.css caps every <p> at 62ch, which
     narrows this line to less than the panel's full width, and a
     narrower block with no auto margins sits flush left regardless of
     text-align. margin-inline: auto is what actually centres the box.
     Caught 24 August 2026 alongside the same fix on .price-panel
     .seats. */
  margin-inline: auto;
}

.stack { list-style: none; margin: 0; padding: 0; }
.stack li {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  padding: 0.62rem 0;
  border-bottom: 1px solid var(--linen);
  font-size: 0.95rem;
}
.stack li span { flex: 1; }

.stack li b {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}
.stack li.priceless b { color: var(--olive); font-style: italic; }

/* The total is deliberately QUIET now, the opposite of the first
   version. It was a highlighted bar with bold black type, styled with
   near equal weight to the deal strip below it, and the owner watched
   real eyes land on $7,442 as if it were the price being sold rather
   than the number being argued against. The research behind this
   pattern is unambiguous on the fix: mute the anchor, do not compete
   with the offer for the same attention. So: no fill, no bold, muted
   ink, and the figure itself is not tabular-emphasised the way the
   real price is. It still has to be legible, it just cannot win. */
.stack-total {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  margin: 0.9rem 0 0;
  padding-top: 0.65rem;
  border-top: 1px solid var(--linen);
  font-size: 0.92rem;
  font-weight: 400;
  color: var(--espresso-soft);
}
.stack-total b { font-weight: 500; text-decoration: line-through; text-decoration-color: var(--terracotta); }

/* The bridge between the anchor and the offer, and the one line on the
   card written to be read rather than scanned. Olive, not terracotta or
   maroon: this is the one place on the card that says something good is
   happening rather than stating a fact, and it needed its own colour to
   read as a different kind of line, not a fourth price in a row of
   prices. */
.stack-save {
  margin: 0.7rem 0 0;
  text-align: center;
  font-size: 1.05rem;
  color: var(--olive);
}
.stack-save strong {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
}
.stack-total span { flex: 1; }
.stack-total b { font-variant-numeric: tabular-nums; font-size: 1.25rem; }

/* The deal strip is a LINK to the form, and it has to look like one:
   it replaced the button that used to sit under the card, so if this
   reads as a passive summary bar the card has no call to action at
   all. Maroon fill, cream text, and the same hover the site's buttons
   use. */
.stack-ours {
  display: block;
  margin: 0.9rem 0 0;
  padding: 1.1rem 1rem;
  background: var(--maroon);
  color: var(--cream);
  border-radius: var(--radius);
  text-align: center;
  font-size: 1.05rem;
  font-weight: 600;
  text-decoration: none;
  /* A visible outline, not just a colour fill, so this block reads as
     the one thing on the card built to be pressed, next to a muted
     anchor line and a bridge line that are both plain text. */
  box-shadow: 0 10px 26px -10px rgba(122, 28, 28, 0.55);
  transition: background-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
}
.stack-ours:hover { box-shadow: 0 12px 30px -8px rgba(184, 90, 63, 0.6); }
.stack-ours:active { transform: translateY(1px); }
/* background-color is set below, once the animation section defines it,
   because an animated property stays under the animation's control even
   while animation-play-state is paused: a plain background-color rule
   here would be silently ignored the moment the flash is running. See
   the comment there for the reasoning. */

/* The deal strip flashed between maroon and its own hover colour for a
   while on 24 August 2026, added at the owner's explicit request after
   he was shown a gentler pulse and asked for something louder instead.
   Removed the same day, once the light sweep below shipped and the
   owner decided the sweep alone was enough: two attention effects
   stacked on one button was more than the button needed. The keyframe
   and the reasoning behind how it was built to stay clear of the WCAG
   blink concern are in git history if a flash is ever wanted again.

   Ordinary hover feedback stays, on its own terms rather than as a
   leftover of stopping the flash: the deal strip is still the loudest
   thing on the pricing card and still needs a hover state. */
.stack-ours:hover, .stack-ours:focus-visible {
  background-color: var(--terracotta);
}

/* ============================================================
   THE SAME LIGHT SWEEP, ON THE DEAL STRIP TOO.

   Added 24 August 2026, the owner having liked it on the photographs
   and asked for it here as well. Runs alongside the flash rather than
   instead of it, since the flash was its own separate, explicit
   request and nothing has walked that back.

   Uses the same .sheen-mask element the photographs use, and for the
   identical reason: .stack-ours carries its own box-shadow, and
   putting overflow:hidden directly on it to contain the sweep would
   clip that shadow off, the exact mistake already made and fixed once
   on the photographs today. The mask is a plain empty span sitting
   inside the button in the markup, clipping only itself.

   position: relative goes on .stack-ours so the mask, which is
   absolutely positioned, anchors to the button rather than to
   whatever positioned ancestor happens to be further up the page. */

.stack-ours { position: relative; }

.stack-ours .sheen-mask::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    115deg,
    transparent 40%,
    rgba(253, 251, 247, 0.55) 49%,
    rgba(253, 251, 247, 0.75) 50%,
    rgba(253, 251, 247, 0.55) 51%,
    transparent 60%
  );
  transform: translateX(-140%);
  animation: photo-sheen 5s ease-in-out infinite;
}

/* On hover the button already stops flashing and settles on a fixed
   colour, per the fix above; the sweep stops with it, for the same
   reason, rather than continuing to move on an element someone is
   actively about to press. */
.stack-ours:hover .sheen-mask::after, .stack-ours:focus-visible .sheen-mask::after {
  animation: none;
  opacity: 0;
}

/* .stack-ours itself needs no reduced-motion rule of its own any more:
   the flash that once needed one is gone, and its own background-color
   is a plain, unanimated property now. */
@media (prefers-reduced-motion: reduce) {
  .stack-ours .sheen-mask::after { animation: none; display: none; }
}

.stack-ours strong {
  font-family: var(--font-display);
  /* Larger than the total's crossed-out figure and larger than its own
     surrounding text: this is the number the whole card exists to
     leave someone holding. */
  font-size: 1.55rem;
  color: var(--cream);
  white-space: nowrap;
}

/* Not fine print to be shrunk away. It is what makes the table above
   a comparison a prospect can check rather than a claim they have to
   take on trust, and under ACL it is what substantiates it. Set as a
   quiet aside rather than as a statement, per the owner: italic and
   centred, matching the treatment of the founder's note further down
   the page rather than reading as legal boilerplate stapled to the
   bottom of the card. margin-inline: auto for the same reason as the
   two rules above it: this is also a <p>, also capped at 62ch by
   landing.css, also narrower than the panel. */
.stack-note {
  margin: 0.9rem 0 0;
  margin-inline: auto;
  font-size: 0.76rem;
  line-height: 1.5;
  font-style: italic;
  text-align: center;
  color: var(--espresso-soft);
}

@media (max-width: 430px) {
  .stack li, .stack-total { font-size: 0.89rem; }
  .stack-save { font-size: 0.96rem; }
  .stack-save strong { font-size: 1.12rem; }
  .stack-ours { font-size: 0.98rem; }
  .stack-ours strong { font-size: 1.32rem; }
}

/* ============================================================
   THE FOUNDER'S NOTE, "Someone who cares"
   Owner's spec, 24 August 2026: the whole thing italic, in black,
   with a clear blank line between paragraphs, so it reads as a
   personal note rather than site copy.

   The italic is synthesised. The self-hosted Inter files carry no
   italic face, so the browser slants the roman. At body sizes that
   is indistinguishable from a true italic to anyone not looking
   for it; if a real italic subset is ever added to fonts.css, this
   picks it up with no change here.
   ============================================================ */

.founder p {
  font-style: italic;
  color: var(--espresso);
  margin-bottom: 1.35rem;
  max-width: 58ch;
}
.founder .lede { color: var(--espresso); }
.founder .terms-line { color: var(--espresso); }
/* The button is not part of the letter. */
.founder .btn { font-style: normal; }

/* ============================================================
   SCROLL REVEAL FOR THE FIVE FEATURE ROWS

   Added 24 August 2026, after the pinned scroll-stage effect was
   removed for reading as broken to real clients. This is a different
   pattern and worth being explicit about the difference: that effect
   pinned the section and held it on screen through a long scroll
   distance while three points sat in a dimmed, ambiguous "in between"
   state for as long as the visitor kept scrolling. This is a one-shot
   reveal on a normal scroll, the same mechanism already running
   everywhere else on this page. Nothing is pinned, nothing holds the
   scroll, and every element completes its transition in well under a
   second the first time it enters view, then stays put.

   IT REUSES LANDING.JS RATHER THAN ADDING NEW JAVASCRIPT. .rv/.in and
   the IntersectionObserver behind them are defined in landing.css and
   landing.js and already run on this page for the founder note, the
   pricing card, the FAQ and the final call to action. Adding the class
   "rv" to the copy and visual columns below is enough; the same
   observer picks them up, the same three second safety net still
   applies if it never fires, and the same reduced motion handling
   already skips all of it. .pop only adds a different CLOSED state and
   TRANSITION for the visual column, so the image or panel scales in
   with a slight overshoot rather than just sliding up like the text
   beside it: two different motions reading as a considered choice
   rather than one fade repeated ten times down the page.

   A SPECIFICITY TRAP TO KNOW ABOUT if this pattern is ever copied
   elsewhere. .rv.pop and landing.css's .rv.in both match two classes,
   so without the explicit .rv.pop.in rule below, source order alone
   decides which wins, and because this stylesheet loads after
   landing.css the pop rule would win and the element would stay
   scaled down forever, never reaching .in. The three-class rule is not
   defensive extra code, it is required. */

.rv.pop {
  transform: scale(0.92) translateY(14px);
  transition: opacity 0.65s ease,
              transform 0.65s cubic-bezier(0.34, 1.56, 0.64, 1);
  /* The text column leads, the image or panel follows by a beat. On a
     phone, where the two stack vertically, this happens naturally
     because the visitor scrolls past the copy first. On a wide screen,
     where they sit side by side and would otherwise cross the
     intersection threshold at the same instant, the delay is what
     creates the same lead-and-follow order deliberately. */
  transition-delay: 0.1s;
}
.rv.pop.in { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .rv.pop { transform: none; transition: none; }
}

/* ============================================================
   CTA ATTENTION PULSE

   Owner asked for the button to blink. A literal blink, the button
   flashing between visible and invisible, is the thing substituted
   here, on purpose, for two reasons that both matter: WCAG 2.2.2 treats
   text or controls that blink for more than three seconds as a failure
   unless the visitor can pause it, which nothing on this page offers a
   control for, and a hard on/off flash is also simply the wrong era of
   web design for a page that has spent all day being pulled back from
   an AI-generated look. A slow, continuous ring that expands and fades
   gets the same job done, drawing the eye without ever making the
   button itself hard to read or disappear.

   SCOPED TO TWO BUTTONS, NOT EVERY CALL TO ACTION ON THE PAGE. The
   header, hero, founder and final buttons stay exactly as they are.
   Pulsing all seven instances of "Get me a salesperson" on the page at
   once would compete with itself and read as spam, which defeats the
   point of an attention cue. The two chosen are the ones that need it
   most: the sticky bar, because it is the one button present on every
   screen for the whole scroll and the natural place for an ambient cue,
   and the mid-scroll button under the workstation panel, because it is
   the one inline call to action that also shows on a desktop screen,
   where the sticky bar does not exist at all above 800px. Between the
   two, something pulses in every viewport a visitor might be using. */

@keyframes cta-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(122, 28, 28, 0.5); }
  60% { box-shadow: 0 0 0 12px rgba(122, 28, 28, 0); }
}

.sticky-cta .btn,
.stage-cta .btn {
  animation: cta-pulse 2.4s ease-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .sticky-cta .btn, .stage-cta .btn { animation: none; }
}

/* ============================================================
   A LIGHT SWEEPS ACROSS THE PHOTOGRAPHS, ONCE SETTLED.

   Added 24 August 2026, replacing a slow zoom drift the owner tried
   first and did not like, offered as one of four genuinely different
   options rather than a second guess at the same idea. The zoom is
   gone entirely: the photographs themselves are completely static
   again, exactly as they were before any of this, and stay that way.

   WHAT MOVES INSTEAD is a soft diagonal band of light layered on top
   of the photo, not the photo itself. It sweeps across in well under a
   second, then the photo sits untouched for several seconds before it
   sweeps again. This is a different kind of effect from the zoom on
   purpose: a zoom asks "is the picture changing", inviting exactly the
   "is something wrong" question that led here, where a sheen reads
   unambiguously as a deliberate lighting flourish, the same visual
   language as light catching glass or foil on packaging, and it
   cannot be mistaken for the image itself doing something odd because
   the image never moves at all.

   IMPLEMENTED AS A PSEUDO-ELEMENT, NOT A SECOND IMG. ::after gives a
   layer positioned exactly over the photo with none of the weight of
   an actual image asset, and pointer-events: none keeps it from ever
   intercepting a tap or a drag on the photo beneath it.

   THE TIMING IS DELIBERATE, LEARNED FROM THE ZOOM'S OWN MISTAKE. Most
   of each five second cycle the band sits fully off to one side, off
   screen and clipped away, and the sweep itself takes about three
   quarters of a second. A slow, subtle sweep would repeat the exact
   failure the zoom just had: technically running, not actually seen.
   Both endpoints of the keyframe sit off screen, so the loop resets
   invisibly with no visible jump back to the start.

   TWO ATTEMPTS AT CONTAINING THE SWEEP WERE WRONG BEFORE THIS ONE, both
   caught by checking rather than assuming, both the same afternoon.

   Attempt one put overflow:hidden on .stage-visual, left over from when
   that same rule contained the zoom before the zoom was removed. It
   clipped .stage-photo's own drop shadow along with the sweep, since an
   ancestor's overflow:hidden clips everything painted outside a
   descendant's box, shadow included.

   Attempt two moved the containment onto the sweep's own pseudo-element
   instead, giving it a border-radius so it stayed rounded with no
   overflow property anywhere. That stopped clipping the photo's shadow,
   but broke something else: a transform-translated element still
   contributes to its ancestors' SCROLLABLE overflow even while it is
   visually clipped by its own border-radius, which is a paint-time
   effect only. With nothing anywhere clipping that geometry, the page
   gained almost 650px of horizontal scroll the moment the sweep was
   added, confirmed by hiding both new pseudo-elements and watching
   document.documentElement.scrollWidth drop back to exactly the
   viewport width.

   THE ACTUAL FIX NEEDED A SEPARATE ELEMENT, because the two
   requirements are genuinely incompatible on a single box: something
   has to clip the sweep's geometry with overflow:hidden, and that
   same overflow:hidden cannot sit on anything that is also casting a
   shadow, itself or through a child. .sheen-mask is an empty span
   added alongside the photo purely to be that clip: absolutely
   positioned over the photo, overflow:hidden and rounded to match, and
   nothing else on the page depends on it existing. The sweep lives on
   its own ::after, inside the mask, where its geometry is contained
   for real rather than merely disguised. */

.stage-visual.pop.in .stage-photo {
  position: relative;
}

.sheen-mask {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: var(--radius);
  pointer-events: none;
}

.stage-visual.pop.in .sheen-mask::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    115deg,
    transparent 40%,
    rgba(253, 251, 247, 0.5) 49%,
    rgba(253, 251, 247, 0.65) 50%,
    rgba(253, 251, 247, 0.5) 51%,
    transparent 60%
  );
  transform: translateX(-140%);
  animation: photo-sheen 5s ease-in-out infinite;
}

@keyframes photo-sheen {
  0% { transform: translateX(-140%); }
  15% { transform: translateX(140%); }
  100% { transform: translateX(140%); }
}

@media (prefers-reduced-motion: reduce) {
  .stage-visual.pop.in::after { animation: none; display: none; }
}

/* ============================================================
   THE HERO COLUMN, CENTRED, ON THIS PAGE ONLY.

   Added 24 August 2026. .hero-copy is shared CSS in landing.css, used
   by all four landing pages, and left-aligned there by default. This
   rule lives here instead, in the sales page's own stylesheet, so
   /admin/, /accounting/ and /seo/ keep the layout they already have
   rather than picking up a change nobody asked for on their pages.

   text-align: center on the wrapper handles the headline (a block
   element, so its text centres within its own full-width box) and the
   button (display: inline-block, so it follows text-align like a word
   would). It does not touch .lede or .terms-line on its own: both are
   <p> tags, and landing.css caps every <p> at 62ch, which is narrower
   than this column on anything but a phone. A narrower box with no
   auto margins sits flush left regardless of the text-align on its
   parent, the same trap this page has hit and logged three times
   already today on the pricing card. margin-inline: auto is what
   actually moves the box, not the text-align that looks like it
   should be doing that job. */

.hero-copy { text-align: center; }
.hero-copy .lede,
.hero-copy .terms-line {
  margin-inline: auto;
}
