/* ===========================================================================
   NEW FILE: wwwroot/css/chronos/43-streak-corner.css

   SECTION 1 of 3. Concatenate SECTION 1 (this), SECTION 2 (hearthCss) and
   SECTION 3 (flareCss) into this single file, in that order.

   Link it in Components/Shared/ChronosHead.razor immediately after the
   42-pet-suggest.css link (currently :163) and BEFORE the 24-theme-dark.css link
   (:168), which must stay last:

       <link rel="stylesheet" href="css/chronos/43-streak-corner.css?v=@cssV" />

   It has to live under css/chronos/: AssetVersion.CssToken (Services/AssetVersion.cs:47-73)
   computes @cssV by scanning that folder's mtimes, so editing this file busts its own
   cache with no hand-bumped token, and ChronosHead reaches /studio for free
   (StudioDocumentLayout.razor:38 renders <ChronosHead />). There is no url() in here,
   so the 2026-08-19 flame-sprite cache incident cannot repeat.

   NOTHING is appended to 01-topbar.css. A second `.streak-clock { }` block in that
   file would silently become the body WebFiles.RuleBody returns (tests/Chronos.Web.Tests/
   WebFiles.cs:45-56 returns the FIRST matching selector) to HomeArrangeLayoutTests and
   DesignArrangeMarkupTests. And there is no Corner Aurora block to remove: 01-topbar.css
   is 725 lines with zero "aurora" hits and no .streak-clock:: rule at all. Both pseudos
   were free before this file existed.

   ===========================================================================
   WHY ONE FILE FOR TWO EFFECTS
   ===========================================================================
   The two halves the owner picked contend for .streak-clock::after. Two stylesheets
   would have made the winner a function of link order, and link order is undecidable
   here: the ChronosHead cascade is shared by App.razor and StudioDocumentLayout while
   the two scripts are tagged in different lists. So the question is removed rather than
   answered. There is ONE sheet, each pseudo is declared exactly once, and the split is
   explicit:

     .streak-clock::before  THE BLOOM.  Resting cast (the hearth's half) plus the
                                        flare's swell. Phase-tinted. Declared here.
     .streak-clock::after   THE RIM.    Flare only, rests invisible. SECTION 3.
     canvas.streak-hearth   THE EMBERS. Hearth only, a <body> child. SECTION 2.

   The hearth's static bloom and the flare's bloom were the same layer twice - a tinted
   radial halo anchored on the pill, differing only in rest alpha - so they became one
   layer with a resting state, not two layers fighting for one mount.

   ===========================================================================
   FALLBACK, TWO DEEP
   ===========================================================================
   With no script at all this file still renders the finished corner: a static
   phase-tinted halo around the chronometer bezel, plus everything 01-topbar.css:193
   already ships (specular sheen, inner depth shadow, glowing digits, 72-frame flame,
   phase rim). Nothing is half-drawn, nothing waits for a partner, and no layout box
   exists to leave a gap. With no CSS the scripts attach nothing visible either: the
   hearth canvas would be a transparent box and the flare would animate pseudos with no
   content. Neither failure mode is a hole.

   ===========================================================================
   NOT A PULSE
   ===========================================================================
   01-topbar.css:191 reads "Static glows only - no new pulse (pulse-budgeted app)"
   because the Upgrade CTA owns the single pulse in this eyeline. This file declares NO
   animation, NO keyframes, NO transition and NO will-change. The halo below is static.
   The only moving things in the pair are discrete short-lived canvas particles, each
   with its own lifetime and sway, released in puffs whose spacing is re-randomised
   after every puff, and a one-shot flare that is dark between moments. Neither has a
   period to measure.
   =========================================================================== */

:root {
    /* MOMENT tints, used by the flare's rim only. Declared on :root on purpose:
       chronos-design.js rewrites a :root-only custom property block LAST in the
       cascade, so an admin retint reaches the effect for free, and no script ever
       samples a computed COLOUR (which would go stale on a theme flip and return
       mid-transition values inside theme.js's 380ms .theme-anim window). */
    --flare-advance:  #FBBF24;   /* warm gold - the day turned over */
    --flare-safe:     #4ADE80;   /* the .is-safe rim green */
    --flare-complete: var(--chronos-lavender);
    --flare-warn:     #F59E0B;   /* the .is-approaching rim amber */
    --flare-critical: var(--chronos-red);
    --flare-frozen:   #60A5FA;   /* the .is-frozen rim ice */
    --flare-broken:   #9AA1B2;   /* the .is-broken ink */
}

/* ---------------------------------------------------------------------------
   DESKTOP ONLY, and the gate is the one both scripts use.

   The width term is the repo's real breakpoint: below 981px 02-main-layout.css:55-67
   replaces the flex bar with a named-area grid that trades centring for the guarantee
   of never overflowing horizontally, and the pill is already shrunk three times down
   there. The pointer terms are what make this desktop rather than merely wide.

   `content: ''` is declared INSIDE this block, so on a phone the pseudo is never
   generated: no box in the tree, no paint, nothing to clean up, and the 375px pill is
   byte-identical to what ships today. Mobile is verification priority 1
   (CLAUDE.md:233-238). Both scripts hold the matching pair of braces - they return
   before creating a listener on a touch pointer and tear down on a drag below 981.
   --------------------------------------------------------------------------- */
@media (min-width: 981px) and (hover: hover) and (pointer: fine) {

    /* THE TWO TOKENS AND THE ONE STRUCTURAL DECLARATION.

       --corner-tint  the PHASE colour. Cascade only. NO SCRIPT EVER WRITES IT. It has
                      to stay a pure cascade value because ::before is visible at rest:
                      an inline per-moment override would outlive the flare and leave
                      the resting cast permanently the wrong colour, and removing it on
                      finish would snap the hue on the frame the alpha lands.
       --corner-cast  the RESTING opacity of that halo. Per-phase, and read back off
                      the element by streak-flare.ts with one getComputedStyle per
                      flare so the flare's envelope is floored on the real number
                      instead of on a constant duplicated in TypeScript. Changing a
                      value here needs no matching edit anywhere.
       --flare-tint   the MOMENT colour, defaulting to the phase colour. Overwritten
                      inline by the script for the duration of a flare, and only the
                      rim (SECTION 3) reads it.

       isolation makes .streak-clock its own stacking context, which is what keeps
       ::before's z-index -1 INSIDE the pill instead of escaping behind the page. CSS
       painting order inside a context puts negative-z children after the context
       root's background and before its content, so the halo lights the bezel face from
       within while the digits, the timer and the flame stay crisp on top. Anyone who
       later puts an absolutely positioned popover INSIDE the clock will be trapped by
       this - grep before adding one.

       The six rgb triples below MUST match the LADDER literal in Scripts/streak-hearth.ts.
       They are the one duplication this design did not manage to remove: embers are
       drawn on a canvas, so their colour cannot be a custom property. See
       verificationPlan for the source-text parity test that closes it.

       Order matters and mirrors the cascade in 01-topbar.css: .is-approaching and
       .is-critical only ever co-occur with .is-counting and all three are (0,2,0), so
       source order is what makes the deadline win. */
    .streak-clock {
        isolation: isolate;
        --corner-tint: #FB923C;
        --corner-cast: 0.17;
        --flare-tint: var(--corner-tint);
    }
    .streak-clock.is-safe        { --corner-tint: #86EFAC; --corner-cast: 0.13; }
    .streak-clock.is-counting    { --corner-tint: #FB923C; --corner-cast: 0.17; }
    .streak-clock.is-approaching { --corner-tint: #FBBF24; --corner-cast: 0.22; }
    .streak-clock.is-critical    { --corner-tint: #F87171; --corner-cast: 0.28; }
    /* Frozen: the 48-72h grace. Cold, and NOT dead - it matches the hue-rotated sprite
       at 01-topbar.css:302-306, and on the canvas the same ember engine runs with
       gravity inverted so the sparks become frost settling out of a cold fire. */
    .streak-clock.is-frozen      { --corner-tint: #93C5FD; --corner-cast: 0.16; }
    /* Broken, and Home's `_ =>` fallback, which renders is-broken for None as well.
       Cast 0 leaves the corner dark on purpose: the light going out IS the
       loss-aversion signal, and on the hearth side this phase never arms a timer at
       all. The flare still fires once here, floored on 0, so the death is announced
       and then the corner stays dark. */
    .streak-clock.is-broken      { --corner-tint: #94A3B8; --corner-cast: 0; }

    /* THE BLOOM. The one shared layer.

       A STATIC radial gradient that only ever moves by transform and opacity. Never an
       animated box-shadow and never a blur that re-rasterises per step: box-shadow is
       not compositable and repaints the box every frame (the pit-motion-on-compositor
       rule, born of the Pit x5 incident), while a gradient rasterises once and is then
       scaled by the compositor for free.

       Inset -14px / -16px rather than further out for two reasons. The onboarding tour
       spotlights this element by getBoundingClientRect plus a 6px pad (Home.razor.cs:56,
       OnboardingTour.razor.cs:14, :89-93), so a wider halo would fall outside the
       cutout; and the light should stay a halo on the instrument rather than a wash
       across the top bar. .streak-clock, .topbar and .chronos-root all declare no
       overflow, so it escapes the pill with nothing to clip it.

       will-change is deliberately absent: it is a promotion hint, and promoting a layer
       that is static 99% of the day is exactly the cost the flame's reduced-motion
       branch releases when it sets will-change:auto. WAAPI promotes for the duration of
       a flare and releases after.

       transform-origin is centre so the flare's swell grows symmetrically about the
       pill. The resting transform is `none`, and the flare's keyframes start and end at
       scale 1, so with fill:'none' there is no discontinuity at either edge. */
    .streak-clock::before {
        content: '';
        position: absolute;
        inset: -14px -16px;
        z-index: -1;
        pointer-events: none;
        border-radius: var(--radius-pill);
        transform-origin: 50% 50%;
        opacity: var(--corner-cast);
        background:
            radial-gradient(58% 130% at 50% 50%,
                color-mix(in srgb, var(--corner-tint) 55%, transparent) 0%,
                color-mix(in srgb, var(--corner-tint) 22%, transparent) 46%,
                transparent 74%);
    }

    /* THE TOUR GATE, in CSS so it cannot be late, and ONE rule for both effects rather
       than the duplicate pair the two proposals each shipped.

       Step 1 of the onboarding tour spotlights this exact element: .tour-backdrop is
       fixed at z 2000 and .tour-spotlight paints an 82% black mask with a 9999px spread
       box-shadow at 2001 (21-onboarding-tour.css:7-27), cut out to the clock's rect plus
       6px. Every layer in this file paints further out than that, so during the step
       that is explaining the streak they would all sit under the mask looking like a
       smudge. Raising them above 2001 would break the spotlight illusion, so they stand
       down.

       !important is load-bearing, not decoration: an author !important declaration is
       the one thing in the cascade that outranks a running WAAPI animation, so this
       also kills a flare that was already in flight when the tour opened. It takes the
       resting cast with it, which is correct - the corner should be a plain bezel
       inside the cutout. streak-flare.ts checks .tour-backdrop before starting a flare
       and streak-hearth.ts checks it before igniting a puff; this is the other half of
       both, and neither script polls. :has() is already used freely in this cascade
       (01-topbar.css:626-628, :699-703). The canvas half of this gate is in SECTION 2. */
    body:has(.tour-backdrop) .streak-clock::before,
    body:has(.tour-backdrop) .streak-clock::after {
        opacity: 0 !important;
    }
}

/* ===========================================================================
   SECTION 2 of wwwroot/css/chronos/43-streak-corner.css - THE HEARTH.
   Paste directly after SECTION 1 (sharedCss). This section declares NO pseudo
   element on .streak-clock; the hearth's only surface is its own canvas.
   =========================================================================== */

/* THE CANVAS. One per clock, a <body> child, created by Scripts/streak-hearth.ts.

   <body> because a node inside .streak-clock is forbidden (Home.razor:86-105 is an
   unkeyed @if chain that Blazor diffs by index) and a direct child of .topbar is
   forbidden too (topnav-collapse.ts:56-68 walks the bar's child rects and an extra
   nonzero box false-positives a wrap, collapsing every section label to an icon).
   <body> is not a safe harbour either - enhanced navigation morphs body-parented
   nodes away, which chronos-multiselect.ts:485-489 calls the common case - so the
   script staleness-checks its own canvas and re-attaches. This sheet only has to
   make sure the node costs no layout while it is there.

   THESE FOUR DECLARATIONS ARE DELIBERATELY OUTSIDE EVERY MEDIA QUERY. If they sat
   inside the desktop gate, a canvas that outlived a fast drag below 981px would
   fall back to display:block, in flow, and push the page. Out of flow always;
   only the display:none gates below are conditional. That is the difference
   between a stale node being invisible and a stale node being a layout shift.

   Parked by a transform written on sync, never per frame, so it can never feed the
   wrap measurement and never shifts a pixel. pointer-events:none so the clock's
   tooltip, the settings popover and the tour spotlight all still hit the real
   element underneath.

   z-index 30 puts it over the bar (whose content is z-index auto) and under
   everything that must stay on top: the language menu (50), modal backdrops and
   modals (200-1300), the genz dock (1350-1700), the tour (2000-2002), the spinner
   (2100). It is therefore ABOVE both flare pseudos, which is correct and
   deliberate: embers are in the air in FRONT of the device, and they are drawn
   with globalCompositeOperation 'lighter', so passing over the rim adds light to
   it rather than hiding it.

   `contain` states what is already true - nothing this canvas paints can affect
   anything outside its own box - so no engine has to work that out for itself.
   `position` is re-declared to fixed by the script only if an ancestor of the clock
   is ever found sticky or fixed; today the probe returns document coordinates. */
.streak-hearth {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 30;
    pointer-events: none;
    contain: layout paint;
}

/* THE TOUR GATE for the canvas half. The pseudo half is in SECTION 1, which is why
   that rule carries !important and this one does not: this element has no competing
   animation, only a script that also stands down. See SECTION 1 for the full
   argument about the 82% mask and the 6px cutout.

   A pure selector, evaluated by the engine, with no polling. The script asks
   document.querySelector('.tour-backdrop') once per ignition; this rule is what
   makes the answer instant when the tour opens MID-burst. */
body:has(.tour-backdrop) .streak-hearth { display: none; }

/* PHONE. Nothing renders and the script never arms below 981.
   02-main-layout.css:55-67 replaces the desktop flex bar with the two-row phone
   grid at max-width 980; the pill is shrunk three times on the way down and there
   is no air left around it. The real gate is in the TypeScript, which reads the
   query live and TEARS THE LAYER DOWN on a window drag rather than merely declining
   to build on the way up (topnav-collapse.ts:82-84). This is the belt, and it is
   what makes the 375px pill byte-identical to today even if the script is mid-sync.
   Mobile is verification priority 1 (CLAUDE.md:233-238). */
@media (max-width: 980px) {
    .streak-hearth { display: none; }
}

/* REDUCED MOTION. House convention is FREEZE ON A GOOD FRAME rather than delete -
   the flame's own branch at 01-topbar.css:77-85 pins frame 36 of 72. There is no
   good frozen frame for an ember: a stopped ember is a dot. So the motion layer
   stands down entirely and the static bloom on ::before (SECTION 1) carries the
   corner on its own, which is the same trade the flame makes. The real gate is in
   the TypeScript, which reads the query live at every decision and removes its
   canvases when the visitor flips the OS setting mid-session. */
@media (prefers-reduced-motion: reduce) {
    .streak-hearth { display: none; }
}

/* ===========================================================================
   SECTION 3 of wwwroot/css/chronos/43-streak-corner.css - THE FLARE'S RIM.
   Paste directly after SECTION 2 (hearthCss). SECTION 1 already declared the
   bloom on ::before, which the flare shares with the hearth's resting cast;
   this section declares the only surface the flare owns alone.

   Same desktop gate as SECTION 1, repeated rather than merged, so each section
   of this file can be deleted whole if one half of the pair is ever dropped.
   Repeating a media query costs nothing at parse time and is worth the
   separability: dropping the flare means deleting this block and setting
   USE_RING to false, and the corner still has its cast and its embers.
   =========================================================================== */

@media (min-width: 981px) and (hover: hover) and (pointer: fine) {

    /* THE RIM. A hairline ring sitting exactly on the pill outline, transparent
       inside, so it can travel over the digits without obscuring one of them.
       z-index 1 puts it above the clock's content; the bloom on ::before sits at
       z-index -1, below it and below the digits.

       IT RESTS AT OPACITY 0, and that is what makes it the layer allowed to carry
       a per-MOMENT colour. streak-flare.ts writes --flare-tint inline for the
       duration of a flare and the value is non-load-bearing: at rest nothing reads
       it, so an enhanced-nav morph stripping it costs nothing and the next flare
       rewrites it. The bloom on ::before deliberately does NOT work this way - it
       is visible at rest, so a leftover inline tint there would permanently
       recolour the resting cast. See SECTION 1.

       The fallback below the inline write is the phase tint, declared in SECTION 1,
       so a rim drawn without the script would still speak the bezel's colour.

       border-color is one of the five properties 24-theme-dark.css:126-138 forces a
       300ms !important transition on for every pseudo while html.theme-anim is set,
       so the rim cross-fades for a third of a second after a theme toggle. That is
       fine, and it is exactly why the tint is carried on `border` here and never on
       background-color or box-shadow: transform and opacity, the two properties the
       animation actually drives, are not in that list and cannot be hijacked by it.

       position:absolute plus pointer-events:none means ZERO layout box, so
       topnav-collapse.ts's wrap detector (it walks .topbar's children comparing
       rects) cannot see it and cannot false-positive the section labels into
       icon-only mode. `content` is declared INSIDE the desktop gate, so on a phone
       this pseudo is never generated at all and the 375px pill is byte-identical to
       what ships today. */
    .streak-clock::after {
        content: '';
        position: absolute;
        inset: 0;
        z-index: 1;
        pointer-events: none;
        opacity: 0;
        border-radius: var(--radius-pill);
        transform-origin: 50% 50%;
        border: 1px solid color-mix(in srgb, var(--flare-tint) 85%, transparent);
    }
}

/* prefers-reduced-motion has no block in this file on purpose. There is nothing here
   to reduce: this sheet declares no animation and no keyframes at all. The script
   reads the query LIVE at flare time (never cached at load, per chronos-motion.js:9-10)
   and substitutes a still profile - the bloom holds scale 1 and only its alpha moves,
   the rim holds its crest scale. That is the house convention the flame already
   follows when it pins to frame 36 of 72 (01-topbar.css:77-85): freeze on a good
   frame, never delete the effect. */
