/* habit
   Split from the single v22 stylesheet. Everything here is scoped to
   .card.habit and this form's own class prefix (rule 14: a change made for one
   card form is scoped to that form, by class). */
/* Was `.card.habit{--panelh:488px}`. The Habit bought its picture down because
   it was the only form carrying a three-line headline AND a live caption in
   .sub, and 583px of panel did not leave room for both. The caption is gone,
   so neither is the reason: at 583 the action row still lands on its own
   bottom edge with 2.6px of the auto margin to spare, and the headline is
   capped by its 86px fit budget rather than by the panel, which is the same
   guarantee every other solo form runs on. One dial fewer. */
/* The .card.solo .hookline rule that used to sit here (fe-001,
   docs/reviews/frontend-2026-09-03.md) moved to shell.css: it was a
   system-wide solo-layout decision, not a habit-only one -- it set every
   solo card's headline size, not just this form's, contradicting the header
   comment above. The Habit keeps that size for free because it is itself a
   .card.solo. */

/* ------------------------------------------------------------------ *
   The Habit — twenty-six years, in twelve seconds.

   Everything here hangs off `.card.habit` or an `hb-` class, per AGENTS.md
   rule 14, and `pipeline/selftest.mjs`'s FORM_SECTIONS guard asserts that no
   `hb-` rule is set anywhere but between these two banners.

   Same shape of card as The Move on a different clock. The Move plays one
   session against yesterday's close and the payoff is where the line stops.
   This plays three hundred and twenty months against the money that went in,
   and the payoff is the gap that opens between two lines — so the fixed
   reference is not a horizontal rule but a second, rising line, and "green
   above it, red below it" becomes a geometric cut against a curve rather than
   against a straight edge.

   The restraint is the same. Two lines, four counters, a month, and a
   transport control. No grid, no second axis, no legend, no candlesticks.
 * ------------------------------------------------------------------ */

/* No headline rules. The subject is marked by `habitHook` with `.hooktick`,
   the same class and the same `--brand` every other form's ticker carries --
   see the note there for the two treatments that were drawn and dropped. A
   form that needs no rule of its own is the point, so there is nothing here.

   --- the readout -------------------------------------------------- */
/* Reuses `.rdout`'s column so the panel keeps one left edge; every row inside
   it is this form's own. Three short rows in reading order: when, what it is
   worth, what went in. */
/* The transport row. It used to open with "Investing $1,000 every month",
   which the headline above the picture already said — one plan, printed
   twice. With the label gone the month sits here instead of on a row of its
   own: beside the play button it reads as a player's timecode, which is what
   it is, and the readout loses a row it was not using. */
.hb-top{display:flex;align-items:center;gap:9px;min-height:30px}
/* The card's clock. It is rewritten on ~320 frames, so tabular figures and a
   min-width that initHabit measures from the widest month the payload
   actually contains — "September 2026" is 30px wider than "May 2011" and a
   proportional box would twitch for twelve seconds. */
.hb-clock{font-size:var(--fs-13);font-weight:600;color:var(--dim);
  font-variant-numeric:tabular-nums;line-height:1;min-height:13px}
.hb-ctl{margin-left:auto;appearance:none;-webkit-appearance:none;
  width:30px;height:30px;flex:none;border-radius:50%;
  border:1px solid var(--edge);background:transparent;color:var(--muted);
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  transition:color .16s ease-out,border-color .16s ease-out}
.hb-ctl:hover,.hb-ctl:focus-visible{color:var(--ink);border-color:var(--muted)}
.hb-ctl svg{width:13px;height:13px;fill:currentColor;stroke:none;display:block}
.hb-ctl .hb-i{display:none}
.card[data-hb="playing"] .hb-i-pause,
.card[data-hb="paused"] .hb-i-play,
.card[data-hb="armed"] .hb-i-play,
.card[data-hb="loading"] .hb-i-play,
.card[data-hb="invalid"] .hb-i-play,
.card[data-hb="done"] .hb-i-replay,
.card[data-hb="reduced"] .hb-i-replay{display:block}
/* Nothing to drive until the state machine is wired, and nothing to drive
   ever if the payload failed the model. Both read as disabled rather than as
   a control that does nothing. */
.card[data-hb="loading"] .hb-ctl,
.card[data-hb="invalid"] .hb-ctl{opacity:.35;pointer-events:none}

/* Fixed baselines and reserved widths. The portfolio figure walks from $959
   to $1,994,682 — four digits to nine — and every one of those steps is a
   chance for the row below it to move. Tabular figures stop the digits from
   changing width; the min-width initHabit measures from the *final* strings,
   before the first frame, stops the box from growing under them. Asserted
   nowhere: measured, because the Move's identical comment was wrong for a
   version and nobody could see it. */
.hb-value{font-size:var(--fs-29);font-weight:700;line-height:1;letter-spacing:var(--ls-n02);
  color:var(--ink);font-variant-numeric:tabular-nums;min-height:29px;display:block}
.hb-meta{font-size:var(--fs-14);font-weight:600;display:flex;gap:7px;align-items:baseline;
  font-variant-numeric:tabular-nums;line-height:1;min-height:14px}
.hb-dep{color:var(--dim);font-weight:400}
/* Both of these are reserved to their final width by initHabit, and min-width
   does nothing to a non-replaced inline box -- so "Deposited $1,000" grew 8.4px
   into "Deposited $320,000" and shoved the percentage beside it along with it.
   Measured, not assumed: the widths script found it in the frame, which is the
   only place it was ever going to show. */
.hb-depv,.hb-pct{display:inline-block}
/* The gain is the only figure that carries direction, because it is the only
   one that has one: deposits only ever rise, and the portfolio value is not
   up or down against anything until it is compared with them. */
.hb-pct{color:var(--stance)}

/* --- the plot ----------------------------------------------------- */
.hbviz{width:100%;height:100%;align-self:stretch;
  display:flex;flex-direction:column;gap:10px;justify-content:flex-start}
.hb-wrap{position:relative;flex:1;min-height:0;margin:0 15px}
.hb-wrap:focus-visible{outline:2px solid var(--muted);outline-offset:3px}
/* Inspection is a post-playback affordance. Offering the crosshair while the
   lines are still being drawn invites a drag that fights the animation, and
   there is nothing to the right of the leading edge to inspect. */
.hb-wrap{cursor:default;touch-action:none}
.card[data-hb="done"] .hb-wrap,
.card[data-hb="reduced"] .hb-wrap{cursor:crosshair}

/* One wipe, three consumers: the lines, the leading marker and the period
   labels are all revealed by the same custom property, which is what makes
   "the label arrives when the line reaches it" true by construction rather
   than by two timers that agree until one of them drifts. */
.hb-svg{width:100%;height:100%;overflow:visible;display:block;
  clip-path:inset(0 calc(100% - var(--hb-p,0%)) 0 0)}
.hb-line{fill:none;stroke-linecap:round;stroke-linejoin:round;
  vector-effect:non-scaling-stroke}
/* Cumulative deposits: muted grey, thinner, and drawn first so the portfolio
   line crosses over it rather than under. It is the reference, not a rival —
   it never carries direction and never carries the accent. */
.hb-dep-line{stroke:var(--edge);stroke-width:1.6}
/* Two copies of one portfolio path, clipped by the deposits curve itself:
   above it is green, below it is red, and the crossing is exact because it is
   a geometric cut rather than a colour chosen per segment. Fifty-six of the
   three hundred and twenty months are below, and the brief is explicit that
   they may not be smoothed away — a single-colour line would do exactly that
   while still plotting them. */
.hb-val{stroke-width:2.2}
.hb-val.up{stroke:var(--up)}
.hb-val.dn{stroke:var(--down)}

/* The leading edge, on the portfolio line. A position, not a price: the
   figures beside it snap to real stored months. */
.hb-dot{position:absolute;z-index:1;width:9px;height:9px;border-radius:50%;
  background:var(--stance);transform:translate(-50%,-50%);
  box-shadow:0 0 0 4px var(--stance-wash);opacity:0;
  transition:opacity .2s ease-out;pointer-events:none}
.card[data-hb="playing"] .hb-dot,
.card[data-hb="paused"] .hb-dot,
.card[data-hb="done"] .hb-dot,
.card[data-hb="reduced"] .hb-dot{opacity:1}
/* And gone while a finger is on the chart. The marker is the end of the line
   and it takes the card's live colour, so during inspection it sat at the
   all-time high painted red beside a readout saying the portfolio was down
   25% -- the leading edge of one month and the figures of another, on one
   frame. The scrub knob is the position while inspecting; there is only ever
   one of them. */
.hb-wrap.inspect .hb-dot{opacity:0}

/* --- the periods -------------------------------------------------- */
/* Context, not commentary. A hairline band and a 10px name, arriving as the
   line reaches the period and staying. They sit behind the plot and carry no
   number: the chart plots month ends, so the decline these windows measure is
   the month-end one, and printing "−12%" under "2020 crash" would be the
   smaller number wearing the larger name. Playback never pauses for them. */
.hb-marks{position:absolute;inset:0;pointer-events:none;z-index:0}
.hb-mark{position:absolute;top:0;bottom:0;display:block;
  background:rgba(255,255,255,.032);border-left:1px solid var(--line);
  opacity:0;transition:opacity .45s ease-out}
.hb-mark.on{opacity:1}
/* `--row` staggers a label that would otherwise sit on top of its neighbour.
   The bands are as narrow as three months and the names are sixty pixels
   wide, so "2020 crash" and "2022 selloff" overprinted each other into an
   unreadable smear -- the first thing visible in the frame and invisible in
   every payload. initHabit measures each name and assigns the lowest free row
   against the finished frame, which is the tightest frame there is because the
   window only ever widens; nothing here decides it.

   `left` is set per frame too, and the old `.flip` class is gone with it. A
   name is born at the leading edge -- every name is, because that is where its
   band starts -- and clamping it inside the plot slides it continuously
   leftward as the window widens, where flipping its anchor made it jump its
   own width on one frame. */
.hb-mark span{position:absolute;top:calc(2px + var(--row,0) * 14px);left:4px;
  font-size:var(--fs-10);line-height:1.1;
  color:var(--dim);letter-spacing:0;text-transform:none;white-space:nowrap}
.hb-svg{position:relative;z-index:1}

/* --- the value axis ----------------------------------------------- */
/* A consequence of the camera, not a grid bolted onto one. Each rung is a
   hairline the width of the plot with its value above the right-hand end, and
   it is on screen only while the camera can hold it: it fades in as the view
   grows past it and fades out when the zoom has closed the gap to the rung
   above into something too tight to read. initHabit decides both, per frame,
   off the same `camPct` the leading marker is placed with -- so the axis and
   the marker cannot disagree about what the view can see.

   Above the period bands and below the plot: the wash and the band borders
   are context, the axis is a reference, and the two lines the card is about
   cross over both. Nothing here is a colour decision the rest of the card has
   not already made -- `--line` is the band's own border, `--dim` the period
   names' own grey. */
.hb-axis{position:absolute;inset:0;pointer-events:none;z-index:0}
.hb-ax{position:absolute;left:0;right:0;height:0;display:block;
  border-top:1px solid var(--line);
  opacity:0;transition:opacity .5s ease-out}
.hb-ax.on{opacity:1}
/* At the LEFT end of its own rule, which is the one place on this chart a
   value can be printed without landing on the line. The plot is a window on a
   rising series, so the drawn line is at its lowest at the left edge and at
   its highest at the right -- and every rung the camera has carried into view
   is a value the line has already passed, so at the left edge the line is
   below it and at the right edge it is above it. Right-aligned was drawn first
   and the frame killed it: "$1,000,000" ran left out of the right edge into
   the exact months where the portfolio crosses a million, and the line struck
   the label through. It also keeps the axis clear of the leading marker and
   the skip control, both of which live on the right.

   Above its own rule rather than centred on it, because a hairline through
   10px type is a strikethrough. Tabular figures for the same reason the
   readout has them -- it is the same quantity, written the same way. */
.hb-ax span{position:absolute;left:0;bottom:3px;
  font-size:var(--fs-10);line-height:1.1;font-variant-numeric:tabular-nums;
  color:var(--dim);letter-spacing:0;text-transform:none;white-space:nowrap}

/* Click to skip, in the bottom-right of the plot — a player's fast-forward,
   not a labelled button competing with the transport control above. On screen
   only while there is playback left to jump past. */
.hb-skip{position:absolute;right:-6px;bottom:-6px;z-index:3;display:none;line-height:0;
  appearance:none;-webkit-appearance:none;border:0;background:transparent;
  padding:12px 8px 4px 16px;color:var(--dim);cursor:pointer;
  transition:color .16s ease-out}
.hb-skip svg{width:26px;height:10px;display:block;fill:none;stroke:currentColor;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.hb-skip:hover,.hb-skip:focus-visible{color:var(--ink)}
.hb-skip:focus-visible{outline:2px solid var(--muted);outline-offset:2px;border-radius:4px}
.card[data-hb="armed"] .hb-skip,
.card[data-hb="playing"] .hb-skip,
.card[data-hb="paused"] .hb-skip{display:block}

/* Drag inspection, post-playback. The same crosshair the static forms use,
   drawn against this form's own geometry. */
.hb-scrub{position:absolute;inset:0;pointer-events:none;opacity:0;z-index:2;
  transition:opacity .12s ease-out}
.hb-scrub.on{opacity:1}
.hb-scrub i{position:absolute;top:0;bottom:0;width:1px;background:var(--edge)}
.hb-scrub b{position:absolute;width:11px;height:11px;border-radius:50%;background:var(--stance);
  transform:translate(-50%,-50%);box-shadow:0 0 0 4px var(--stance-wash)}

/* --- the figures --------------------------------------------------
   The one fact the readout above the plot cannot carry, on its own row: the
   gain in dollars. The readout already prints the portfolio, the deposits and
   the percentage, and restating any of them here would be the third statement
   of one number that s09 took off the gauge and the streak.

   It counts the whole way through rather than appearing at the end. A counter
   that is invisible until it stops is not a counter, and a figure arriving in
   the same frame the chart finishes is the snap The Move was rebuilt to
   avoid. */
.hb-figs{display:flex;flex-direction:column;margin:6px 15px 0;flex:none}
.hb-bar{display:flex;justify-content:space-between;align-items:baseline;
  gap:12px;padding-top:12px;border-top:1px solid var(--line);
  font-size:var(--fs-14);font-weight:600;color:var(--muted)}
.hb-bar b{font-size:var(--fs-29);font-weight:700;color:var(--ink);line-height:1;
  font-variant-numeric:tabular-nums;letter-spacing:var(--ls-n02);text-align:right}
/* The treatment, stated from the first frame and never changed. It is a fact
   about the calculation rather than about the outcome, so there is nothing to
   withhold — and revealing it at the end would be one more thing moving on
   the frame the reader is watching the chart finish on. */
.hb-note{margin-top:9px;font-size:var(--fs-11);line-height:1.35;color:var(--dim);
  letter-spacing:0;text-transform:none}

/* No payoff caption. It read "$1,000 a month in the S&P 500 since 2000" while
   the years played and "$320,000 became $1,994,682" once they had landed, and
   both of those are already inside the panel -- the headline states the plan
   and the readout states the result, so the caption was the third printing of
   one pair of numbers. The same thing The Move does, for the same reason and
   by the same rule: the payload keeps `sub` and `initial`/`final.caption`, and
   this form no longer draws them. Removing the slot took the reserved `--band`
   and the swap-at-the-end with it; there is nothing left on this card that
   changes length.

   `.card.habit .hook .sub{display:none}` moved to shell.css, consolidated
   with The Move's and The Insider's copies of the same rule -- see shell.css
   for the declaration. This card's real deck ("$320,000 became $1,994,682")
   wraps to 2 lines and costs 73.17px over the pinned bottom if shown; a
   one-line deck costs 37.09px, the same floor every card in this group pays
   for the first line. Nothing here changed what this card renders. */

/* The screen-reader summary. The chart is the card, so a reader who cannot
   see it is handed the same result in a sentence rather than nothing. */
.hb-sum{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* --- the honest failure ------------------------------------------- */
/* A habit block the renderer will not vouch for says so, rather than
   animating a simulation it could not verify. There is no static fallback the
   way The Move has one: this form has never had a still version, and drawing
   an unchecked chart would be worse than drawing none. */
.hb-fail{display:flex;flex-direction:column;justify-content:center;width:100%;
  min-height:0;padding:16px;border:1px solid var(--edge);border-radius:8px;
  font-size:var(--fs-12);color:var(--dim);letter-spacing:0;text-transform:none}

/* Reduced motion gets the finished twenty-six years, immediately and without
   a wipe: the card's whole content is the simulation, so skipping the
   animation must not skip the chart. initHabit paints the completed state and
   never starts a frame loop; these rules stop the transitions that would
   still fire. */
@media (prefers-reduced-motion:reduce){
  .hb-svg{clip-path:none}
  .hb-dot,.hb-mark,.hb-ax{transition:none}
}
