/* ==========================================================================
   datetime.css — site-local additions, loaded after the four shared sheets.

   shared/styles/ is the design system and is not forked. This file exists for
   rules that are true of site-datetime's page shapes only, and it is loaded
   from src/templates/partials/head-extra.html, i.e. inside base.html's
   `head_extra` block, which renders after tokens/base/layout/calculator.css.
   Every custom property used here is declared in shared/styles/tokens.css.

   IT MUST NOT CHANGE ANY RESERVED AD GEOMETRY. --slot-h / --slot-w and the
   .ad-slot box model belong to layout.css; measured CLS is 0 and that is a
   constraint, not an outcome (Design-System.md section 5). Nothing below
   touches a slot's width, height or padding — only the space above one.
   ========================================================================== */

/* --------------------------------------------------------------------------
   P3 — 96px minimum separation between a navigation control and an ad.
   Ad-Placement-Policy.md section 4; AdSense-Compliance.md finding R4.

   The 16 hand-built tool pages emit `ul.quick-links`, a centred row of
   accent-coloured pill links. Before this rule the result slot was its
   immediately following sibling and the gap from the bottom of a pill to the
   top of the "Advertisement" label was 24px (.quick-links has no bottom
   margin; .ad-slot--result has margin-block: var(--sp-6)). A user reaching
   for a link 24px above a 300x250 is the exact pattern Google's prohibition
   on ads "adjacent to navigational or other action items" is written about.

   calculator.html now emits the "Rebuilt ... conventions last checked" line
   between the two. This adds the hairline rule and the space:

       pill bottom
         + --sp-4  (16px)  margin above the meta line
         + 1px     hairline rule
         + --sp-4  (16px)  padding below the rule
         + ~19px            the meta line itself at --fs-xs
         + --sp-16 (64px)  margin above the slot
       = ~116px to the top of the Advertisement label.

   The selector is deliberately chained: on a generated page there are no
   pills, so the meta line stays a plain date line and the slot keeps its
   normal --sp-6 margin. Only the pages with the adjacency problem pay for it.
   -------------------------------------------------------------------------- */

.quick-links + .page-head__meta {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: var(--bw) solid var(--border);
}

.quick-links + .page-head__meta + .ad-slot--result {
  margin-block-start: var(--sp-16);
}

/* --------------------------------------------------------------------------
   FRESHNESS REFRESH — the reserved hero line box
   --------------------------------------------------------------------------
   src/calculators/lib/freshness.js recomputes this page for the visitor's
   date when the build date has rolled over. Rewriting the hero's text can
   change how many lines it wraps to: measured at 375px,
   "Saturday, 19 September 2026" occupies two lines at 60.72px while
   "Saturday, 3 October 2026" fits on one at 30.36px, and that 30px collapse
   moves everything below the panel — including the reserved result ad box.

   .result__value--text carries min-height: 0 in shared/styles/calculator.css,
   so nothing stops it. Two lines at line-height 1.15 is 2.3em; reserving that
   makes the box height independent of the answer's length.

   IT IS SCOPED TWICE, on purpose:

     * to this sheet, so site-fitness and site-grades — which share
       calculator.css and contain the build date zero times — are untouched;

     * to [data-fresh-refresh="pending"], which the inline pre-paint script in
       partials/head-extra.html sets ONLY when the browser's date differs from
       the build date. On the overwhelmingly common no-rollover path the
       attribute is never set, this rule never matches, and the page's layout
       is byte-identical to what it is today. The reservation therefore costs a
       blank line to nobody except the visitor who is about to see the hero
       change, and for them it exists from first paint, so the change shifts
       nothing.
   -------------------------------------------------------------------------- */

[data-fresh-refresh="pending"] .result__value--text {
  min-height: 2.3em;
}

/* --------------------------------------------------------------------------
   ...AND THE THREE OTHER BOXES A TEXT SWAP CAN RESIZE

   The hero reservation above was measured and it works — hero box height
   changed on 0 of 234 refreshing pages. It was also the ONLY reservation, and
   the hero is not the only thing that rewraps. Measured on a rolled-over
   build at 375px:

     .result__sub          sits between the reserved hero and the result ad
                           slot, inside the viewport, so a 1↔2 line flip here
                           is a layout shift Chrome actually counts;
     the two long-date <dd> rows of the working ("Wednesday, 19 August 2026"
                           → "Thursday, 20 August 2026") gained or lost a line
                           on 28 of 234 pages and moved the result ad slot by
                           ±21.68px.

   Both are single-value boxes with a known worst case of two lines, so both
   can be reserved the same way the hero is: fix the line-height so the
   arithmetic is ours, then reserve two of them. The line box then holds
   whichever wrapping the answer needs and the box height stops depending on
   the answer's length.

   Only the two <dd>s that carry a full weekday-and-date string are selected —
   the ISO, US and day-first rows are far too short to wrap at any width the
   site supports, and reserving a blank line under each of them would be
   whitespace bought for nothing.

   The cost is paid ONLY by a visitor whose date has rolled over, and only as
   at most one blank line under the hero and under two rows of the working.
   The alternative is those elements moving under an ad slot.

   NOT reserved: the prose paragraphs. Their line count is a function of the
   rendered text and no static value can stand in for it. Their reflow is
   handled by making the refresh land before first paint instead — see the
   blocking="render" note in partials/head-extra.html.
   -------------------------------------------------------------------------- */

[data-fresh-refresh="pending"] .result__sub,
[data-fresh-refresh="pending"] .calc__breakdown dd[data-fresh-fmt="full"] {
  line-height: 1.45;
  min-height: 2.9em;   /* two lines at the line-height set immediately above */
}

/* --------------------------------------------------------------------------
   AN ABORTED REFRESH MUST SAY SO, AND MUST NOT MOVE AN AD TO SAY IT

   A JS-enabled visitor whose refresh aborted is reading a stale page and is
   told nothing: shared/styles/calculator.css reveals `.no-js-note` only under
   `.no-js`, and base.html strips that class before first paint for everybody
   who has JavaScript. So the disclosure has to be revealed some other way.

   THE FIRST ATTEMPT REVEALED `.no-js-note` ITSELF, AND THAT WAS MEASURABLY
   WRONG. That paragraph sits inside `section.calc`, ABOVE the result ad slot
   — the one slot the file's own note two sections down calls "the one slot
   that can be in the viewport when the refresh lands". Measured in Chrome at
   427x925 with the state attribute set: the revealed note resolves to
   148.78px and the result ad slot moves by exactly that (top 1385.38 ->
   1534.16), the in-article slot moves the same, the document grows 149px, and
   CLS reads 0.0302 at scrollY 700 and 0.0968 at scrollY 1000. The rule
   immediately below this one exists to stop a 22px move of that same slot;
   this was 6.8 times further. It also revealed the note UNSTYLED — the
   border, padding and --fs-sm all live on `.no-js .no-js-note`, and `.no-js`
   is gone by then — so it came out as six full-size unstyled body lines.

   So the disclosure is a SEPARATE element, `.fresh-abort-note`, and
   calculator.html puts it at the very end of <main>: after the article, after
   both in-flow ad slots, after the FAQ and the related links. Nothing a
   reader is looking at sits below it except the footer.

   And even that is reserved rather than left to shift. Under
   [data-fresh-refresh="pending"] — which the pre-paint script in
   partials/head-extra.html sets only when the browser's date differs from the
   build date, so a same-day visitor pays nothing — the note is laid out from
   first paint and merely made invisible. Revealing it on abort is then a
   `visibility` change inside a box that already has its final size.

   MEASURED at 375x812 on a page whose module landed after first paint, with
   the state attribute flipped to "aborted": the result ad slot moved 0.00px,
   the in-article ad slot moved 0.00px and the document height did not change.
   The same flip on the old rule moved both slots 148.78px and grew the
   document by 149px.

   Not revealed for `url-pins-state:*`. That abort means the visitor followed
   a shared ?from= link and the engine is about to recompute the calculator
   from the URL against a base date the visitor chose; announcing the build
   date there would be a fourth date on a screen that already has three.
   -------------------------------------------------------------------------- */

.fresh-abort-note {
  display: none;
  margin-block: var(--sp-6) 0;
  padding: var(--sp-4);
  border: var(--bw) solid var(--warn);
  border-radius: var(--radius-sm, 6px);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

[data-fresh-refresh="pending"] .fresh-abort-note {
  display: block;
  visibility: hidden;
}

html[data-fresh-state="aborted"]:not([data-fresh-reason^="url-pins-state"]) .fresh-abort-note {
  display: block;
  visibility: visible;
}

/* --------------------------------------------------------------------------
   THE LABEL COLUMN OF THE WORKING, WHICH MOVES WITH THE ANSWER'S WIDTH

   The rule above reserves the two <dd>s that hold a full weekday-and-date
   string, on the reasoning that the ISO, US and day-first rows "are far too
   short to wrap at any width the site supports". That reasoning is sound and
   those rows still moved. Measured at 375px on /30-days-from-today/ refreshed
   three years on: the day-first row went from 22px to 43px while its own text
   stayed exactly ten characters long, and the result ad slot moved 22px with
   it.

   The <dl> is `display: grid` with content-sized columns. Refreshing the page
   made the longest VALUE longer — "Sunday, 27 September 2026" became
   "Thursday, 27 September 2029" — so the value column grew and the label
   column was squeezed from 108.25px to 96.86px. "Day-first format" then no
   longer fitted on one line, the <dt> wrapped, and the <dd> beside it was
   dragged to the same row height. Nothing about the row that moved had
   changed; the column split had.

   Pinning the label column stops the split moving. 7.25rem is 116px against a
   measured max-content of 108.25px for the longest label, and the labels are
   fixed strings that no refresh can touch, so the margin cannot be eaten.
   `minmax(0, 1fr)` on the value column makes a long answer WRAP rather than
   take width back — and the two rows where that can happen are reserved for
   two lines by the rule above.

   Like every rule in this section it only matches when the pre-paint script
   in partials/head-extra.html has found the browser's date differs from the
   build date, so the common path lays out exactly as it does today. These
   rows sit ABOVE the result ad slot — the one slot that can be in the
   viewport when the refresh lands — which is what makes this the reservation
   worth having.

   The article's prose paragraphs are reserved too, and by a different
   mechanism — see the section below.
   -------------------------------------------------------------------------- */

[data-fresh-refresh="pending"] .calc__breakdown dl {
  grid-template-columns: 7.25rem minmax(0, 1fr);
}

/* --------------------------------------------------------------------------
   THE ARTICLE'S PROSE, WHICH GROWS — AND THE CLAIM THAT SAID IT DID NOT

   THE CORRECTION FIRST, because a wrong measurement in a comment is worse
   than no measurement. This file used to state that prose growth "is bounded
   at one line because every list in the article is span-determined". Measured
   in Chrome at 375x812, module graph stalled, visitor scrolled into the
   article, /20-business-days-ago/ built 2026-08-28 and refreshed to
   2026-11-30: twelve blocks grew by a total of 144.45px — FIVE lines at the
   measured 28.9px line-height, not one — the document went 7371px to 7516px,
   and ASIDE.ad-slot--article moved 459.69 -> 604.14. CLS 0.2888 at scrollY
   3000, past Google's 0.25 "poor" threshold. The claim was wrong by 5x. The
   growth is not bounded by one list entry: a three-month roll moves the span
   by ninety days, and the paragraph that read "Federal holidays inside this
   span: none." comes back holding two.

   IT IS ALSO NOT BOUNDED BY A SLOW LINK. Sweeping the stall: 400ms and 800ms
   produce the identical 0.2888. Seven modulepreloads exceeding 400ms on a
   cold mobile cache is ordinary.

   THE FIX. reserveBlocks() in src/calculators/lib/freshness.js pins a
   min-height FLOOR before it writes, so a block that needs FEWER lines keeps
   its height; its own comment concedes that a block needing MORE lines still
   grows. Nothing the module does can help, because a layout shift is a
   comparison between painted frames and the module has already missed the
   first one. The reservation therefore has to be here, in CSS, from first
   paint — and freshness.js SPENDS it: absorbGrowth() measures how much each
   block actually grew and takes exactly that much back out of the padding
   below it, in the same task as the writes, so the browser lays out twice and
   paints once with the block at the height it already had.

   One line for a paragraph that carries any marker; two for the three that
   carry a LIST, because a list is the only marker whose zero case ("none")
   is the shortest text it can hold and whose full case is unbounded. That
   matches the measured shape of the growth exactly: 2 + 1 + 1 + 1 lines
   across four blocks.

   THE COST, AND WHY ALMOST NOBODY PAYS IT. Measured at 375x812, the whole
   reservation — this slack plus the hero, sub and working line boxes plus the
   abort disclosure's box — is 581px of document height, 396px of it as extra
   spacing inside the article. That would be paid on every visit after the
   build day, which is not a trade worth making. So freshness.js's
   releaseReservations() TAKES THE ATTRIBUTE OFF AGAIN whenever it finds it has
   run before first paint: there is no earlier frame to shift relative to, so
   the reservation is worth nothing and the visitor gets the page at its
   natural height. Only a visitor whose module lands after first paint keeps
   it, and for them commit() spends the slack against the growth it measures.

   MEASURED, in Chrome at 375x812, on /20-business-days-ago/ built 2026-05-30
   and loaded on 2026-08-28 with this module graph stalled 2,000ms and the
   visitor scrolled to y=3000 — the exact configuration in which this page
   previously recorded CLS 0.2888 with the in-article ad slot moving 144.45px:

     layout-shift entries          0
     CLS                           0.000000
     in-article ad slot            did not move
     document height               7888px before the refresh, 7888px after
     slack left on the two blocks
       that actually grew          1.71px of 30.60px — spent, to the pixel

   And on the pre-paint path, an eight-day-old build refreshed in the browser
   is pixel-identical in geometry to a native build at the visitor's date:
   result slot 1350.77px, article slot 5412.45px, document 8032px, on both.

   :has() is required and is not polyfilled. Where it is unsupported the rule
   simply does not match, absorbGrowth() finds no padding to spend, and the
   behaviour is exactly what it was before this section existed.
   -------------------------------------------------------------------------- */

[data-fresh-refresh="pending"] .prose p:has([data-fresh]),
[data-fresh-refresh="pending"] .prose li:has([data-fresh]) {
  padding-block-end: 1.8em;
}

[data-fresh-refresh="pending"] .prose p:has([data-fresh-fmt="list"]) {
  padding-block-end: 3.6em;
}
