/* ════════════════════════════════════════════════════════════════════════════
   css/sell-tier2.css — MOTION AND DISCLOSURE FOR THE FOUR TECHNOLOGY
   TIER 2 SELLING PAGES
   service.tnaado.ca

     /web-design-toronto                 websites
     /ecommerce-website-design-toronto   online stores
     /shopify-development-toronto        Shopify
     /custom-software-toronto            custom software

   WHY A NEW FILE. css/sell.css draws all four of these pages and is shared
   with other work in flight, so it is not edited here. This file is additive
   only: it adds motion and disclosure behaviour to markup css/sell.css already
   lays out, and it introduces no colour, no spacing value and no type value of
   its own. Every value below is either a token from css/sell.css or a duration.

   ─── THERE IS NO JAVASCRIPT ───────────────────────────────────────────────
   Not "the JavaScript is optional" — there is none. Every reveal in this file
   is a CSS scroll-driven animation on a `view()` timeline, and every disclosure
   is a native <details>. That is a deliberate choice over the usual
   IntersectionObserver-plus-`.is-in` pattern, for three reasons:

     1. NO FLASH, AND NO STRANDED CONTENT. The observer pattern needs an
        opacity:0 start state, which must be applied before first paint and
        must be undone by a script that might never arrive. Get the ordering
        wrong, or let the script 404, and a page that takes PAID GOOGLE ADS
        TRAFFIC renders blank sections. A scroll-driven animation cannot fail
        that way: where `animation-timeline` is unsupported the declaration is
        dropped whole and the element simply renders finished.
     2. IT RUNS OFF THE MAIN THREAD, so it stays smooth while booking.js and
        the chrome are still settling.
     3. IT IS REVERSIBLE. Scrubbing back up the page unplays the reveal,
        because the animation is a function of position rather than an event
        that has already been spent.

   So the finished state is the DEFAULT STATE in every rule here, and the
   animation only ever moves an element away from it and back. Read every
   @keyframes below as "where it comes from", never "where it ends up".

   ─── REDUCED MOTION ──────────────────────────────────────────────────────
   Guarded twice, on purpose. css/sell.css already carries a global
   `prefers-reduced-motion: reduce` killswitch that sets animation:none on
   everything, which is sufficient. This file additionally wraps its own
   animations in `prefers-reduced-motion: no-preference`, so the intent is
   local and legible and a future edit to the other file cannot quietly
   reintroduce motion here. Belt and braces on an accessibility guard is not
   redundancy worth removing.

   ─── WHAT THE MOTION IS FOR ──────────────────────────────────────────────
   Not decoration. Each block below replaces a sentence that used to ASSERT
   something the reader can now watch happen:

     .wf (the three wireframes)  the layout being drawn at three widths, block
                                 by block. Replaced two paragraphs that
                                 described the reflow in words.
     .t2-cart / .sum rows        a quote adding itself up, line by line, so the
                                 arithmetic is witnessed rather than restated.
                                 Replaced the "restated outside the picture",
                                 "counting business days" and "adding the stages
                                 up" sentences. TRANSFORM ONLY — see §3.
     .t2-seq                     cards, tiers, stages, proof figures arriving in
                                 reading order instead of all at once.

   ─── DISCLOSURE ──────────────────────────────────────────────────────────
   Native <details>/<summary> only. Keyboard operable and findable by the
   browser's own find-in-page for free, and the content is never removed from
   the DOM, so it stays indexable — on these pages the text IS the proof.
   The panel does NOT animate its height. An attempt to do that with
   `::details-content` and `interpolate-size` broke every disclosure on all
   four pages and was reverted — the measurements are recorded at §4, because
   the feature reports as supported and the bug is invisible in source.
   What animates is the marker and a short fade on the body.
   ══════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════════
   §1 · TIMELINE VOCABULARY

   Two custom timelines rather than a bare `view()` everywhere, so the ranges
   read as intent instead of arithmetic. --t2-in is the ordinary "arriving"
   reveal; --t2-long is for tall blocks that should finish well before they
   leave the viewport.
   ══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* ── The generic arrival. Applied with class .t2-r ──────────────────── */
    .t2-r {
      animation: t2-rise both linear;
      animation-timeline: view();
      animation-range: entry 4% cover 20%;
    }
    /* A row of siblings that should arrive in sequence rather than together.
       The index is read off nth-child so no markup carries a counter. */
    .t2-seq > * {
      animation: t2-rise both linear;
      animation-timeline: view();
    }
    .t2-seq > *:nth-child(1) { animation-range: entry  4% cover 18%; }
    .t2-seq > *:nth-child(2) { animation-range: entry  8% cover 22%; }
    .t2-seq > *:nth-child(3) { animation-range: entry 12% cover 26%; }
    .t2-seq > *:nth-child(4) { animation-range: entry 16% cover 30%; }
    .t2-seq > *:nth-child(5) { animation-range: entry 20% cover 34%; }
    .t2-seq > *:nth-child(6) { animation-range: entry 24% cover 38%; }
    .t2-seq > *:nth-child(7) { animation-range: entry 28% cover 42%; }

    @keyframes t2-rise {
      from { opacity: 0; transform: translate3d(0, 14px, 0); }
      to   { opacity: 1; transform: none; }
    }

    /* ══════════════════════════════════════════════════════════════════════
       §2 · THE WIREFRAMES — A PAGE BEING LAID OUT, THREE TIMES

       This is the section that used to carry two paragraphs explaining that
       navigation folds into a button and a row of three cards becomes a
       stack. Both are deleted: the reader watches three widths of the same
       page draw themselves, and the widths are labelled.

       The stagger is nested — column, then block inside the column, then the
       type lines and tiles inside that — and every index comes from
       nth-child, so /web-design-toronto's existing .wf markup did not gain a
       single attribute to support this.

       Note the ranges are additive rather than delayed. A scroll-driven
       animation ignores animation-delay (there is no clock to delay against),
       so sequence is expressed by sliding the RANGE along the timeline. That
       is why these read as percentages rather than milliseconds.
       ═══════════════════════════════════════════════════════════════════════ */

    /* The three browser frames arrive left to right. */
    .wf__col {
      animation: t2-frame both linear;
      animation-timeline: view();
    }
    .wf__col:nth-child(1) { animation-range: entry  2% entry 55%; }
    .wf__col:nth-child(2) { animation-range: entry  8% entry 62%; }
    .wf__col:nth-child(3) { animation-range: entry 14% entry 70%; }

    @keyframes t2-frame {
      from { opacity: 0; transform: translate3d(0, 20px, 0) scale(.985); }
      to   { opacity: 1; transform: none; }
    }

    /* The blocks inside each frame draw in: bar, then hero, then the card
       row. `.wf__page > *` is exactly those three on every one of the three
       columns, so one rule covers nine elements. */
    .wf__page > * {
      animation: t2-draw both linear;
      animation-timeline: view();
      transform-origin: 0 0;
    }
    .wf__page > *:nth-child(1) { animation-range: entry 12% entry 48%; }
    .wf__page > *:nth-child(2) { animation-range: entry 20% entry 58%; }
    .wf__page > *:nth-child(3) { animation-range: entry 28% entry 68%; }

    @keyframes t2-draw {
      from { opacity: 0; clip-path: inset(0 100% 0 0); }
      to   { opacity: 1; clip-path: inset(0 0 0 0); }
    }

    /* The four type lines in the hero, and the cards in the row, land last —
       this is the beat that makes it read as a page being built rather than a
       panel fading in. */
    .wf__type i,
    .wf__tile {
      animation: t2-line both linear;
      animation-timeline: view();
      transform-origin: 0 50%;
    }
    .wf__type i:nth-child(1), .wf__tile:nth-child(1) { animation-range: entry 30% entry 60%; }
    .wf__type i:nth-child(2), .wf__tile:nth-child(2) { animation-range: entry 34% entry 64%; }
    .wf__type i:nth-child(3), .wf__tile:nth-child(3) { animation-range: entry 38% entry 68%; }
    .wf__type i:nth-child(4)                          { animation-range: entry 42% entry 72%; }

    @keyframes t2-line {
      from { opacity: 0; transform: scaleX(.2); }
      to   { opacity: 1; transform: none; }
    }

    /* The burger and the nav links are the reflow itself — the thing the
       deleted paragraph was describing. They arrive after their own bar so
       the fold reads as a decision rather than as part of the frame. */
    .wf__burger, .wf__links, .wf__cta {
      animation: t2-pop both linear;
      animation-timeline: view();
      animation-range: entry 34% entry 62%;
    }
    @keyframes t2-pop {
      from { opacity: 0; transform: scale(.7); }
      to   { opacity: 1; transform: none; }
    }

    /* ══════════════════════════════════════════════════════════════════════
       §3 · A QUOTE ADDING ITSELF UP

       Applied to the worked-sum blocks. The lines arrive in reading order and
       the total lands after them, so the sum is witnessed. Three pages used
       to follow their own sum with a sentence restating it in prose; all
       three sentences are deleted and this is what replaced them.
       ═══════════════════════════════════════════════════════════════════════ */
    .t2-cart > * {
      animation: t2-ledger both linear;
      animation-timeline: view();
    }
    .t2-cart > *:nth-child(1) { animation-range: entry  6% cover 16%; }
    .t2-cart > *:nth-child(2) { animation-range: entry 11% cover 21%; }
    .t2-cart > *:nth-child(3) { animation-range: entry 16% cover 26%; }
    .t2-cart > *:nth-child(4) { animation-range: entry 21% cover 31%; }
    .t2-cart > *:nth-child(5) { animation-range: entry 26% cover 36%; }
    .t2-cart > *:nth-child(6) { animation-range: entry 31% cover 41%; }
    .t2-cart > *:nth-child(7) { animation-range: entry 36% cover 46%; }
    .t2-cart > *:nth-child(8) { animation-range: entry 41% cover 51%; }

    /* TRANSFORM ONLY — NO OPACITY. This is the one reveal in this file that
       must never fade, and the reason is a rule the rest of the repo states
       twice. /shopify-development-toronto's file tree: "Nothing animates ...
       and no figure in it is gated behind motion." /ecommerce-website-design-
       toronto's bundle cart: "Nothing here animates and nothing here is
       scripted. It is a table."

       Both are about MONEY, and both are right: a published price that needs a
       scroll position to become legible is a price that can fail to be legible,
       and this firm's whole argument is that its figures are printed. So a
       quote line here arrives from a few pixels left at FULL OPACITY. It is
       readable on every frame, including the first one and including a landing
       straight onto #prices from an ad. The beat survives; the risk does not. */
    @keyframes t2-ledger {
      from { transform: translate3d(-10px, 0, 0); }
      to   { transform: none; }
    }

    /* §3 ONCE HAD a .t2-grow rule for growing a proportional money bar from
       zero. Its only markup was the .qbar strip on /custom-software-toronto,
       which moved to /ios-app-development-toronto with the $95,000 worked
       example, so the rule had nothing to select and is deleted rather than
       kept against a future need. If that strip ever comes back, it wants a
       transform-only entrance for the reason given directly above. */
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   §4 · DISCLOSURE — <details> THAT ANIMATES

   Outside the reduced-motion guard on purpose: the LAYOUT rules here (the
   marker, the summary's hit area, the focus ring) must apply to everybody.
   Only the transition itself is guarded, further down.

   Collapsing never removes content from the DOM — <details> hides it, it does
   not detach it — so every word stays indexable and stays findable by
   find-in-page, which auto-expands the panel it matched inside.
   ══════════════════════════════════════════════════════════════════════════ */

/* The native disclosure triangle is replaced by a drawn marker so it can be
   animated and so it sits on the type baseline. list-style:none on both
   selectors because Safari needs the ::-webkit- form. */
.t2-d > summary { list-style: none; }
.t2-d > summary::-webkit-details-marker { display: none; }

.t2-d {
  border-top: 1px solid var(--hair-bone);
}
.band:not(.band--bone) .t2-d { border-top-color: var(--hair-ink); }

.t2-d > summary {
  display: flex;
  align-items: baseline;
  gap: .75rem;
  padding: .95rem 0;
  cursor: pointer;
  font-family: var(--sans);
  font-size: .95rem;
  line-height: 1.45;
  transition: color 180ms var(--ease);
}
.t2-d > summary::before {
  content: "";
  flex: 0 0 auto;
  width: .52rem;
  height: .52rem;
  margin-top: .42rem;
  border-right: 1.5px solid var(--red-bone);
  border-bottom: 1.5px solid var(--red-bone);
  transform: rotate(-45deg);
  transition: transform 240ms var(--ease);
}
.band:not(.band--bone) .t2-d > summary::before {
  border-right-color: var(--red-ink);
  border-bottom-color: var(--red-ink);
}
.t2-d[open] > summary::before { transform: rotate(45deg); }

@media (hover: hover) and (pointer: fine) {
  .t2-d > summary:hover { color: var(--red-bone); }
  .band:not(.band--bone) .t2-d > summary:hover { color: var(--red-ink); }
}

.t2-d__body { padding: 0 0 1.1rem 1.27rem; }
.t2-d__body > :first-child { margin-top: 0; }
.t2-d__body > :last-child  { margin-bottom: 0; }

/* ── THE OPEN TRANSITION, AND THE ONE THAT WAS REVERTED ─────────────────
   WHAT IS HERE NOW: the body fades and lifts into place when the panel
   opens. It animates ON OPEN ONLY, it only ever runs forwards, and if it
   does not run the content is simply there. There is no way for this rule
   to leave a panel blank.

   WHAT WAS HERE FIRST, AND WHY IT IS GONE. The obvious modern way to do
   this is to animate the panel's height with `interpolate-size:
   allow-keywords` and a `::details-content` transition:

     .t2-d::details-content       { block-size: 0; opacity: 0; overflow: hidden;
                                    transition: block-size .3s, opacity .22s,
                                    content-visibility .3s allow-discrete; }
     .t2-d[open]::details-content { block-size: auto; opacity: 1; }

   That was written, shipped into all four pages, and TESTED IN THE BROWSER,
   where it turned out to BREAK EVERY DISCLOSURE ON THE PAGE. Measured on
   all four FAQ panels of /custom-software-toronto: after clicking the
   summary, `details.open` was true and the body had a laid-out height of
   118-204px, but the computed style of ::details-content stayed
   `block-size: 0px`, `opacity: 0`, `content-visibility: hidden` and the
   <details> element itself never grew — 58px closed, 58px "open". A reader
   clicking a question got the marker rotating and nothing else.

   Both features report as supported (CSS.supports('interpolate-size',
   'allow-keywords') and selector(::details-content) are both true here), so
   this is not a support gap and a @supports guard does not save it.

   The failure mode is the worst one available on these pages: they take PAID
   GOOGLE ADS TRAFFIC, the FAQ answers are where the prices are explained,
   and the content was still in the DOM for a crawler while being invisible
   to the person who clicked. It would not have shown up in any review that
   only read the source.

   So: no height animation. A disclosure that opens reliably beats a
   disclosure that opens beautifully, and native <details> opens reliably. */
@media (prefers-reduced-motion: no-preference) {
  .t2-d[open] > .t2-d__body,
  .t2-d[open] > .faq__a {
    animation: t2-open 260ms var(--ease) both;
  }
  /* TRANSFORM ONLY, for the same reason as §3's ledger keyframe. A fade was
     written here first and it does work — measured on /custom-software-toronto,
     the animation reaches playState "finished" and opacity settles at 1. But
     these panels are where the PRICES are explained, and a from-state of
     opacity:0 means the single worst outcome is "answer invisible" rather than
     "answer arrives without a fade". A 5px lift has no such worst case.
     Same judgement, same page, twice. */
  @keyframes t2-open {
    from { transform: translate3d(0, -5px, 0); }
    to   { transform: none; }
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   §5 · .tdx-row — THE SUMMARY CHROME FOR A PORTED DEMO STAGE

   css/components/demos.css deliberately does NOT style .tdx-row: the guide in
   PORTED-COMPONENTS.md says the summary chrome belongs to the host page's own
   stylesheet, so that a demo dropped into four different page designs does not
   drag a fifth one in with it. This is that chrome for these four pages, copied
   from the shape proved in __ported-demos.html and re-pointed at css/sell.css's
   tokens.

   js/demos.js only READS the <details> — it never opens or closes one — so the
   disclosure and the demo cannot fight over the same state. It mirrors the open
   state onto .tdx-row as data-open, which is what gates the will-change hints
   inside demos.css; that is why the class has to be on the <details> itself.

   The stages sit on INK bands only. demos.css is authored for the dark ground
   (its --dx-* family is bone-on-ink throughout) and has no bone override, which
   is the same constraint /shopify-development-toronto's file tree carries and
   for the same reason: on a light band it would render bone-on-bone.
   ══════════════════════════════════════════════════════════════════════════ */
.tdx-row { border-top: 1px solid var(--hair-ink); }
.tdx-row > summary {
  list-style: none;
  cursor: pointer;
  padding: 1.1rem 0;
  display: flex;
  gap: 1rem;
  align-items: baseline;
  font-family: var(--mono);
  font-size: .78rem;
  letter-spacing: .06em;
  color: var(--bone);
  transition: color 180ms var(--ease);
}
.tdx-row > summary::-webkit-details-marker { display: none; }
.tdx-row > summary b { color: var(--red-ink); font-weight: 500; }
.tdx-row > summary::after {
  content: "";
  margin-left: auto;
  width: .5rem; height: .5rem;
  border-right: 1.5px solid var(--red-ink);
  border-bottom: 1.5px solid var(--red-ink);
  transform: rotate(-45deg);
  transition: transform 240ms var(--ease);
}
.tdx-row[open] > summary::after { transform: rotate(45deg); }
@media (hover: hover) and (pointer: fine) {
  .tdx-row > summary:hover { color: var(--red-ink); }
}
.tdx-row__panel { padding: 0 0 2.4rem; }

/* Same judgement as §4: transform only, never opacity. A stage that has not
   faded in yet is a stage the reader cannot read. */
@media (prefers-reduced-motion: no-preference) {
  .tdx-row[open] > .tdx-row__panel { animation: t2-open 260ms var(--ease) both; }
}

/* ══════════════════════════════════════════════════════════════════════════
   §6 · MOUNT POINTS

   A [data-mount] element is a documented hole waiting for a ported component
   (see PORTED-COMPONENTS.md). Until one lands it must occupy NO SPACE and
   announce nothing — an empty box with padding would ship as a visible gap on
   a page paying for its own traffic. It is not display:none, because that
   would also hide a component mounted into it by a later pass that forgot to
   remove the attribute.
   ══════════════════════════════════════════════════════════════════════════ */
[data-mount]:empty {
  display: block;
  height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
}

/* ══════════════════════════════════════════════════════════════════════════
   §7 · THE TIER LADDER, TRIMMED TO TWO

   The repetition audit's R1 finding: the three- and four-card tier ladder was
   printed near-verbatim on four Tier-2 pages. Each page now shows only the
   tier it sells and the one immediately above it, which is two cards.

   css/sell.css:587 hard-codes `repeat(3, 1fr)` and is a SHARED stylesheet, so
   it is not edited here — this file loads after it and overrides for its own
   four pages only. With two cards in a three-column grid the ladder rendered
   as two cards and a hole.

   :has() rather than a flat 2-column rule so the override is a statement about
   the markup rather than about today's card count: a page that later needs a
   third card gets three columns back without anyone remembering this block.
   Where :has() is unsupported the grid falls back to sell.css's three columns,
   which is the pre-existing rendering and not a regression.
   ══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 56rem) {
  .tiers:has(> .tier:nth-child(2):last-child) {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   §8 · THE TIER RANGE — "THE CAP IS NOT THE SUM", DRAWN

   PRICES.md: "The ceiling is a cap, not a sum. Publish both numbers. Never
   print an equals sign between max contents and the ceiling — features are
   priced *from* $1,800 and rise with difficulty, so the gap is real headroom
   and most jobs land under the cap."

   All four pages published that as an ARGUMENT: a body sentence carrying the
   list-rate figure, followed by a red line reading "$18,000 is the cap, not
   the sum." Two sentences insisting on a relationship between three numbers.

   This draws the three numbers on one rail instead, so the relationship is
   visible and the insisting is deleted. The scale is absolute — zero at the
   left edge, the tier ceiling at the right — because that is the scale a
   reader would draw themselves, and because it makes the floor's position
   meaningful too. The headroom is literally the empty part of the rail.

   NO NUMBER IS INVENTED HERE. Every percentage authored into a --floor or
   --list is figure ÷ ceiling from PRICES.md, and every figure is also printed
   as text beside the bar. See §9's note on why that matters.

   OPEN-ENDED TIERS. Tier 04 has no published ceiling ("its scope has no
   published shape"), so .t2-range--open draws a rail that fades out at the
   right rather than a bar that stops. A tier with no cap must not be drawn
   with one.
   ══════════════════════════════════════════════════════════════════════════ */
.t2-range {
  --floor: 0%;
  --list: 100%;
  margin-top: 1rem;
}

/* The rail. A hairline the full width of the card, carrying the whole scale
   from zero to the ceiling. */
.t2-range__rail {
  position: relative;
  height: .5rem;
  border-bottom: 1px solid var(--hair-ink);
}
.band--bone .t2-range__rail { border-bottom-color: var(--hair-bone); }

/* WHAT THE JOB ACTUALLY COSTS: floor to the list-rate maximum contents. */
.t2-range__fill {
  position: absolute;
  left: var(--floor);
  right: calc(100% - var(--list));
  bottom: -1px;
  height: 3px;
  background: var(--red-ink);
  transform-origin: 0 50%;
}
.band--bone .t2-range__fill { background: var(--red-bone); }

/* THE HEADROOM: list-rate maximum to the ceiling. Drawn as a dotted
   continuation rather than left blank, so the gap reads as part of the same
   measurement instead of as the end of the bar. */
.t2-range__head {
  position: absolute;
  left: var(--list);
  right: 0;
  bottom: 0;
  height: 0;
  border-bottom: 1px dashed var(--red-ink);
  opacity: .55;
}
.band--bone .t2-range__head { border-bottom-color: var(--red-bone); }

/* The ceiling itself — a stop at the right-hand end. */
.t2-range__cap {
  position: absolute;
  right: 0;
  bottom: -2px;
  width: 1.5px;
  height: .6rem;
  background: var(--red-ink);
}
.band--bone .t2-range__cap { background: var(--red-bone); }

/* Open-ended: no stop, and the rail fades out rather than ending. */
.t2-range--open .t2-range__cap { display: none; }
.t2-range--open .t2-range__fill {
  right: 0;
  -webkit-mask-image: linear-gradient(90deg, #000 62%, transparent 100%);
          mask-image: linear-gradient(90deg, #000 62%, transparent 100%);
}
.t2-range--open .t2-range__head { display: none; }

/* THE LIST-RATE MARK. The figure has to sit where the bar says it sits, so
   the label is a block exactly --list wide with its text set flush right: its
   right edge IS the mark. No absolute positioning, which means a label that
   wraps to two lines pushes the rail down instead of landing on top of it.

   Two versions of this shipped wrong before this one, both caught at 1440 in
   Chrome and neither visible in source. First: three labels in one
   space-between row, which floated the middle figure halfway along a bar whose
   mark was at 86.7% — a label claiming to be a point on an axis and missing it
   by nine per cent. Second: `position:absolute; top:0` against a rail pushed
   down by `margin-top`, where the child margin COLLAPSED out of the parent, so
   the parent box started at the rail and top:0 put the label straight through
   the bar.

   Real text, always rendered, never gated behind motion — these are money
   figures and §3 settled that question for this file already. */
.t2-range__mark {
  width: var(--list);
  min-width: 12rem;
  padding-right: .4rem;
  margin-bottom: .45rem;
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  font-size: .625rem;
  line-height: 1.55;
  letter-spacing: .03em;
  color: var(--dim-ink);
}
.band--bone .t2-range__mark { color: var(--dim-bone); }
.t2-range__mark b { font-weight: 400; color: var(--red-ink); }
.band--bone .t2-range__mark b { color: var(--red-bone); }

/* The two ends of the rail, which genuinely are its ends. */
.t2-range__axis {
  display: flex;
  justify-content: space-between;
  gap: .75rem;
  margin-top: .5rem;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  font-size: .625rem;
  letter-spacing: .03em;
  color: var(--dim-ink);
}
.band--bone .t2-range__axis { color: var(--dim-bone); }
.t2-range__axis b { font-weight: 400; color: var(--red-ink); }
.band--bone .t2-range__axis b { color: var(--red-bone); }

/* Below 34rem the card is too narrow to set a label flush right against a mark
   at 85% without it wrapping to four lines, so it becomes an ordinary line.
   Same words, same figure, no false precision. */
@media (max-width: 34rem) {
  .t2-range__mark { width: 100%; padding-right: 0; text-align: left; }
}

/* The fill grows from the floor as the card arrives. TRANSFORM ONLY: the
   figures beside it are already legible on the first frame, so the worst case
   of this rule not running is a bar that is simply drawn. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .t2-range__fill {
      animation: t2-range-grow both linear;
      animation-timeline: view();
      animation-range: entry 12% cover 26%;
    }
    @keyframes t2-range-grow {
      from { transform: scaleX(0); }
      to   { transform: none; }
    }
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   §9 · THE STAGE DURATION LANE — A SCHEDULE WITH A SHAPE

   Every one of these pages listed its stages with the duration set right-
   aligned as text: "2 business days", "2 to 3 weeks", "1 day", "1 week". Four
   numbers in four different units, which the reader has to convert and hold in
   order to see the only thing that matters — that one stage is most of the
   job. So each stage now also carries a bar, all four on one scale, and the
   sentences that were describing the shape of the schedule are deleted.

   WHAT THE SCALE IS, EXACTLY, because a bar is a claim: every lane is drawn
   against the LONGEST STAGE ON THAT PAGE, in business days, at its maximum.
   --d is the stage's minimum as a percentage of that, --dmax its maximum. A
   stage quoted as a range draws solid to --d and dashed on to --dmax, so the
   uncertainty is part of the picture rather than hidden by picking one end.
   Nothing is summed and no total is drawn: the durations are published per
   stage and a total drawn from them would be a figure this firm has not
   published.

   The bar is aria-hidden and adds nothing a screen reader needs — the duration
   is already text in .step__dur, in the same row, in reading order.
   ══════════════════════════════════════════════════════════════════════════ */
.t2-lane {
  --d: 100%;
  --dmax: var(--d);
  position: relative;
  display: block;
  height: 2px;
  margin-top: .5rem;
  background: var(--hair-ink);
}
.band--bone .t2-lane { background: var(--hair-bone); }

/* Full-bleed across the .step grid rather than boxed into one of its columns.
   css/sell.css:625 sets `grid-template-columns: 3.5rem 1fr 11rem` at >=48rem;
   a fourth child would otherwise drop into the first column at 3.5rem wide. */
@media (min-width: 48rem) {
  .step .t2-lane { grid-column: 1 / -1; margin-top: .875rem; }
}

.t2-lane__d {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--d);
  height: 2px;
  background: var(--red-ink);
  transform-origin: 0 50%;
}
.band--bone .t2-lane__d { background: var(--red-bone); }

/* The "to" half of a range. Dashed, because it is the part of the estimate
   that is an estimate. */
.t2-lane__to {
  position: absolute;
  left: var(--d);
  top: 0;
  width: calc(var(--dmax) - var(--d));
  height: 0;
  border-top: 2px dashed var(--red-ink);
  opacity: .5;
}
.band--bone .t2-lane__to { border-top-color: var(--red-bone); }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .t2-lane__d {
      animation: t2-lane-grow both linear;
      animation-timeline: view();
      animation-range: entry 10% cover 24%;
    }
    @keyframes t2-lane-grow {
      from { transform: scaleX(0); }
      to   { transform: none; }
    }
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   §10 · THE FILE TREE'S PRICE TAGS — /shopify-development-toronto ONLY

   The tree is that page's signature: the real directory layout of an Online
   Store 2.0 theme with the money written beside the lines it pays for, so the
   picture IS the invoice. It was completely static, and it is the one thing on
   the page a reader scrolls to.

   THE CONSTRAINT, which is not negotiable and is stated twice elsewhere. The
   page's own header comment: "It is a static list. Nothing animates, nothing
   is scripted, and no figure in it is gated behind motion." §3 of this file
   says the same thing about quote lines. Both are right — a published price
   that needs a scroll position to become legible is a price that can fail to
   be legible.

   So this is the §3 treatment exactly: TRANSFORM ONLY, FULL OPACITY ON EVERY
   FRAME. Each tag arrives from six pixels right, in file order, and if the
   rule never runs the tag is simply sitting where it belongs. There is no
   frame of this animation on which $700 is unreadable, including the first
   one and including a landing straight onto #prices from an ad.

   Only the eight PRICED lines move. The directory names and the unpriced files
   do not, because the beat being drawn is "these are the lines with money
   against them", and moving all twenty-nine would say nothing.

   nth-child indices are the priced lines' positions in the authored list:
   templates 5-10, webhooks 28-29. If a file is added to the tree these must be
   recounted — which is why the tags are selected by position rather than by a
   class, so a mismatch shows up as a tag that does not move rather than as a
   silently wrong figure.
   ══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* ── A BUG THIS COMPONENT WALKED STRAIGHT INTO ────────────────────────
       A bare `animation-timeline: view()` HERE NEVER FINISHES. Measured, not
       guessed: sweeping the page from 900px above the tree to 1400px below it
       in 300px steps, every sample held at least one tag at a 4px offset. The
       tags were legible throughout — opacity stayed 1 at every sample, which
       is the rule this section exists to keep — but the price column sat
       permanently four pixels out of alignment, at every scroll position on
       the page, including the one a reader stops at.

       WHY. `view()` resolves against the subject's NEAREST SCROLLPORT
       ANCESTOR, and the tag's nearest scrollport is `.tree` itself: css/sell.css
       gives it `overflow-x: auto` so a monospaced tree can scroll sideways on a
       phone instead of widening the document, and per spec an `overflow-x` of
       `auto` computes `overflow-y` to `auto` as well. So the tree is a scroll
       container in the block axis too, it never scrolls in that axis, and the
       timeline's progress is pinned to one value forever. `view()` takes no
       scroller argument, so this cannot be overridden at the animation.

       THE FIX is to name the timeline on the SCROLLER rather than inside it.
       `.tree` measures against ITS nearest scrollport, which is the document,
       and a descendant may reference a named timeline on an ancestor. Scoped
       with :has() so it only applies to the instance that opted in, and the
       shared stylesheet that owns `.tree` is not edited.

       ⚠ AND THE ORDER OF THE TWO DECLARATIONS BELOW MATTERS. In Chrome the
       `animation` SHORTHAND resets `animation-timeline` to `auto`, so the
       longhand must come AFTER it or the named timeline is silently thrown
       away and the animation is left on an inactive timeline: it exists, its
       currentTime is null, its effect is never applied, and the element just
       renders its base style. Which looks exactly like "no animation", not
       like "broken". Writing the longhand first cost a round here. */
    .tree:has(.t2-tree) { view-timeline-name: --t2-tree; }

    .t2-tree > li:nth-child(5)  .tree__p,
    .t2-tree > li:nth-child(6)  .tree__p,
    .t2-tree > li:nth-child(7)  .tree__p,
    .t2-tree > li:nth-child(8)  .tree__p,
    .t2-tree > li:nth-child(9)  .tree__p,
    .t2-tree > li:nth-child(10) .tree__p,
    .t2-tree > li:nth-child(28) .tree__p,
    .t2-tree > li:nth-child(29) .tree__p {
      animation: t2-tag both linear;
      animation-timeline: --t2-tree;   /* AFTER the shorthand. See above. */
    }
    .t2-tree > li:nth-child(5)  .tree__p { animation-range: entry  6% cover 14%; }
    .t2-tree > li:nth-child(6)  .tree__p { animation-range: entry  9% cover 17%; }
    .t2-tree > li:nth-child(7)  .tree__p { animation-range: entry 12% cover 20%; }
    .t2-tree > li:nth-child(8)  .tree__p { animation-range: entry 15% cover 23%; }
    .t2-tree > li:nth-child(9)  .tree__p { animation-range: entry 18% cover 26%; }
    .t2-tree > li:nth-child(10) .tree__p { animation-range: entry 21% cover 29%; }
    .t2-tree > li:nth-child(28) .tree__p { animation-range: entry 30% cover 38%; }
    .t2-tree > li:nth-child(29) .tree__p { animation-range: entry 33% cover 41%; }

    @keyframes t2-tag {
      from { transform: translate3d(6px, 0, 0); }
      to   { transform: none; }
    }
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   §11 · THE CHECKOUT FLOW — /ecommerce-website-design-toronto ONLY

   That page's subject is what happens after the cart, and it opened by
   printing the five screens as a `ul.gate` — a plain bullet list. A SEQUENCE
   rendered as an unordered pile, at the top of the page it is the subject of.

   Drawn as a flow instead: numbered stops on one rule, arriving left to right
   in the order a customer walks them. The markup becomes an <ol>, because it
   was always ordered and the old element said it was not.

   No JavaScript, no timer, no observer. The arrival is a scroll-driven
   animation like everything else in this file, and the finished state is the
   authored default — with the rule dropped or motion reduced, all five stops
   are simply drawn.

   It carries no figure, so unlike §3, §8 and §10 it is allowed to fade.
   ══════════════════════════════════════════════════════════════════════════ */
.t2-flow {
  display: grid;
  gap: 1px;
  margin: 2rem 0 0;
  padding: 0;
  list-style: none;
  background: var(--hair-ink);
  border: 1px solid var(--hair-ink);
}
.band--bone .t2-flow { background: var(--hair-bone); border-color: var(--hair-bone); }
@media (min-width: 46rem) {
  .t2-flow { grid-template-columns: repeat(5, 1fr); }
}

.t2-flow__s {
  position: relative;
  padding: 1rem 1rem 1.1rem;
  background: var(--ink);
}
.band--bone .t2-flow__s { background: var(--bone); }

.t2-flow__s b {
  display: block;
  font-family: var(--mono);
  font-size: .6875rem;
  font-weight: 400;
  letter-spacing: .14em;
  color: var(--red-ink);
}
.band--bone .t2-flow__s b { color: var(--red-bone); }

.t2-flow__s span {
  display: block;
  margin-top: .5rem;
  font-size: .9375rem;
  line-height: 1.35;
}

/* The rule the stops sit on — one hairline crossing all five, so they read as
   one walk rather than five boxes. Horizontal only; stacked on a phone the
   grid gap already does that job. */
@media (min-width: 46rem) {
  .t2-flow__s::after {
    content: "";
    position: absolute;
    left: 1rem;
    right: -1px;
    top: 2.35rem;
    border-top: 1px solid var(--hair-ink);
  }
  .band--bone .t2-flow__s::after { border-top-color: var(--hair-bone); }
  .t2-flow__s:last-child::after { right: 1rem; }
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .t2-flow__s {
      animation: t2-flow-in both linear;
      animation-timeline: view();
    }
    .t2-flow__s:nth-child(1) { animation-range: entry  6% cover 16%; }
    .t2-flow__s:nth-child(2) { animation-range: entry 10% cover 20%; }
    .t2-flow__s:nth-child(3) { animation-range: entry 14% cover 24%; }
    .t2-flow__s:nth-child(4) { animation-range: entry 18% cover 28%; }
    .t2-flow__s:nth-child(5) { animation-range: entry 22% cover 32%; }
    @keyframes t2-flow-in {
      from { opacity: 0; transform: translate3d(-8px, 0, 0); }
      to   { opacity: 1; transform: none; }
    }
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   §12 · THE EXPOSURE PASS'S CLIPPING BOX, GIVEN ROOM FOR A FOCUS RING

   Mounting §11 of PORTED-COMPONENTS.md on these four proof grids turns every
   `.proof` figure into `overflow: hidden` — that is the component's own rule
   and it is correct, because the box has to clip the overscan the photograph
   settles out of.

   IT ALSO CLIPS THE FOCUS RING. Measured by tabbing, not by reading: the
   figures on these pages carry links and a button, and several sit flush with
   an edge of their own figure. On /ecommerce-website-design-toronto the "Open
   the store" button is inset 0px from the left and 0px from the bottom of the
   frame; on /shopify-development-toronto "the technology portfolio" is 0px
   left and 3px bottom; on /web-design-toronto "the office" is 0px left. The
   focus outline is 2px, so a keyboard reader tabbing to any of them got an
   indicator that was partly or entirely invisible. `overflow: visible` was
   correct on these figures before this mount and this is a regression I
   introduced.

   `overflow: clip` plus `overflow-clip-margin` is exactly the tool for it: the
   box still clips, but three pixels further out, which is enough for the ring
   and far short of the ~9px per side of overscan the component is there to
   hide. Scoped to `.proof[data-sheet-frame]` so it changes nothing for any
   other page that mounts the component, and the component's own stylesheet is
   not edited.
   ══════════════════════════════════════════════════════════════════════════ */
@supports (overflow-clip-margin: 3px) {
  .proof[data-sheet-frame] {
    overflow: clip;
    overflow-clip-margin: 3px;
  }
}


/* ── THE BUILD DEMO'S CEILING ─────────────────────────────────────────────
   MEASURED at 1440 on /web-design-toronto: .wb__view rendered 1185x740 — the
   tallest element on the page — and it was doing it by upscaling an 800px
   capture to 1185, so it was both too big and soft. .wb__view is aspect-ratio
   8/5, so the height follows the width and one max-width fixes both.

   52rem = 832px, which is just above the 800px assets' native size, so the
   picture is now at most 1:1 rather than 1.48x, and the view lands at 520px.

   SCOPED TO THIS SHEET, NOT THE COMPONENT. /technology mounts the same
   component as a plate inside the 3D storm rig, where css/storm-tier1.css
   records that box HEIGHT is a hit-testing constraint. This sheet is linked
   from the five Tier-1 landing pages only, so the cap cannot reach it. */
.wb { max-width: 52rem; }
