/* ==========================================================================
   HowToSoftware Hosting - Base layer
   --------------------------------------------------------------------------
   Reset, typography, layout primitives and the handful of shared classes that
   need to cross component boundaries. Everything component-specific lives in
   the matching `.razor.css` file instead.
   ========================================================================== */

/* ── Reset ─────────────────────────────────────────────────────────────── */

*,
*::before,
*::after {
    box-sizing: border-box;
}

* {
    margin: 0;
}

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: calc(var(--ht-header-h) + var(--ht-space-6));
    -webkit-tap-highlight-color: transparent;
    background-color: var(--ht-ground);
}

/* Smooth scrolling is a preference, not a default. */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

body {
    position: relative;
    min-height: 100dvh;
    background-color: var(--ht-ground);
    color: var(--ht-ink-soft);
    font-family: var(--ht-font-sans);
    font-size: var(--ht-text-md);
    line-height: var(--ht-leading-normal);
    font-synthesis-weight: none;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/*
    The ground the whole site is drawn on, and the reason no section needs a backdrop of its
    own any more. Two layers, both fixed, both free:

      1. ONE light. A single soft wash from above left, white rather than tinted, standing in
         for the room the racks are in. The old design gave nine separate sections their own
         coloured radial blur, which is why the backgrounds never agreed with each other and
         why the page read as decorated rather than lit.

      2. ONE grid. Drafting paper at roughly two per cent, fading out over the first screen
         and a half. Continuous across the page rather than a rectangle parked behind a few
         cards - a grid you notice is a grid that has become an ornament.

    Both are anchored to the document, not to the viewport. Fixed layers would sit still while
    opaque sections scrolled over them, so the grid would appear, vanish and reappear on the
    way down. Anchored, it is simply where the page begins.
*/
body::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1400px;
    z-index: 0;
    pointer-events: none;
    background-image:
        radial-gradient(120% 62% at 22% -6%, rgba(255, 255, 255, 0.05) 0%, transparent 64%),
        repeating-linear-gradient(90deg, var(--ht-grid-ink) 0 1px, transparent 1px var(--ht-grid-cell)),
        repeating-linear-gradient(180deg, var(--ht-grid-ink) 0 1px, transparent 1px var(--ht-grid-cell));
    mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.5) 46%, transparent 100%);
}

/* Fine grain over the whole page. Large flat dark areas band badly on cheap
   panels; a little noise breaks the gradients up. One inline SVG, no request. */
body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 1;
    opacity: var(--ht-noise-opacity);
    pointer-events: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

img,
svg,
video {
    display: block;
    max-width: 100%;
}

button,
input,
textarea,
select {
    font: inherit;
    color: inherit;
}

button {
    background: none;
    border: none;
    cursor: pointer;
}

ul,
ol {
    list-style: none;
    padding: 0;
}

a {
    color: var(--hts-primary);
    text-decoration-color: color-mix(in srgb, var(--hts-primary) 40%, transparent);
    text-underline-offset: 0.2em;
    transition: color var(--hts-duration-fast) var(--hts-ease);
}

a:hover {
    color: var(--hts-primary-strong);
}

::selection {
    background-color: color-mix(in srgb, var(--ht-live-500) 34%, transparent);
    color: var(--ht-ink);
}

/*
    Machine voice. Anything that is a number, a unit, an identifier, a state or a designation
    is set in mono; prose is not. The distinction is load-bearing across the whole site, so it
    gets one class rather than being restated in every component.
*/
.hts-mono {
    font-family: var(--ht-font-mono);
    font-variant-numeric: tabular-nums;
    letter-spacing: var(--ht-track-mono);
}

/*
    A section number.

    This rule existed fourteen times, once per section, in four slightly different versions -
    some filled purple, some filled blue, one of them missing flex:none. That is what happens
    when a component owns a value the system should own, and it is why the numbers down the
    page never quite matched each other.

    Unfilled now. A solid brand-coloured square beside every heading spent the palette on
    counting, and the count is not the important thing on any of those screens.
*/
.kicker-index {
    display: grid;
    place-items: center;
    width: 1.75rem;
    height: 1.75rem;
    flex: none;
    border: 1px solid var(--ht-edge-strong);
    border-radius: var(--ht-radius-xs);
    color: var(--ht-ink-soft);
    font-family: var(--ht-font-mono);
    font-size: var(--ht-text-2xs);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
}

/* A label stamped on a panel: small, wide-tracked, upper case. */
.hts-label {
    font-family: var(--ht-font-mono);
    font-size: var(--ht-text-2xs);
    font-weight: 600;
    letter-spacing: var(--ht-track-label);
    text-transform: uppercase;
}

/* ── Typography ────────────────────────────────────────────────────────── */

h1,
h2,
h3,
h4 {
    color: var(--ht-ink);
    font-weight: 620;
    letter-spacing: var(--ht-track-tight);
    line-height: var(--ht-leading-tight);
    text-wrap: balance;
}

/*
    The display voice.

    Archivo carries a width axis, and this is where it earns its place: the headline is the
    body face pushed wider, not a second family bolted on. Equipment is labelled exactly that
    way - the same letterforms stamped broad across the panel and narrow in the manual - so
    the page reads as one object rather than as a pairing somebody chose.

    font-stretch rather than font-variation-settings, because font-stretch composes with
    font-weight; setting the axes directly would override every weight the components ask for.
    Only h1 and h2 take it. Stretching everything would flatten the distinction it exists to
    make.
*/
h1,
h2 {
    font-stretch: 116%;
    letter-spacing: var(--ht-track-display);
    line-height: var(--ht-leading-tight);
}

h1 {
    font-size: var(--ht-text-5xl);
    font-weight: 700;
}

h2 {
    font-size: var(--ht-text-4xl);
    font-weight: 680;
}

h3 {
    font-size: var(--hts-text-xl);
    line-height: var(--hts-leading-snug);
}

h4 {
    font-size: var(--hts-text-md);
    line-height: var(--hts-leading-snug);
}

p {
    text-wrap: pretty;
}

code,
kbd,
samp {
    font-family: var(--hts-font-mono);
    font-size: 0.92em;
}

/* ── Focus ─────────────────────────────────────────────────────────────── */

:focus-visible {
    outline: 2px solid var(--ht-live-400);
    outline-offset: 3px;
    border-radius: var(--hts-radius-xs);
}

/* FocusOnNavigate moves focus to the page heading for screen-reader users; it is not an
   interactive control, so it should not paint a focus ring. */
h1:focus,
h1:focus-visible {
    outline: none;
}

/* ── Skip link ─────────────────────────────────────────────────────────── */

.hts-skip-link {
    position: absolute;
    top: var(--hts-space-3);
    left: var(--hts-space-3);
    z-index: var(--hts-z-overlay);
    padding: var(--hts-space-3) var(--hts-space-5);
    border: 1px solid var(--hts-border-brand);
    border-radius: var(--hts-radius-pill);
    background: var(--hts-surface-raised);
    box-shadow: var(--hts-glow-purple);
    color: var(--hts-ink);
    font-size: var(--hts-text-base);
    font-weight: 600;
    text-decoration: none;
    transform: translateY(-180%);
    transition: transform var(--hts-duration-base) var(--hts-ease-out);
}

.hts-skip-link:focus-visible {
    transform: translateY(0);
}

/* ── Layout primitives ─────────────────────────────────────────────────── */

.hts-container {
    position: relative;
    z-index: 2;
    width: 100%;
    max-width: var(--hts-container);
    margin-inline: auto;
    padding-inline: var(--hts-gutter);
}

.hts-container--wide {
    max-width: var(--hts-container-wide);
}

.hts-container--narrow {
    max-width: var(--hts-container-narrow);
}

.hts-section {
    position: relative;
    padding-block: var(--hts-section-y);
}

/* A lifted plane, separated by a fading hairline rather than a hard border. */
.hts-section--soft {
    background: var(--hts-surface-soft);
}

.hts-section--inset {
    background: var(--hts-surface-inset);
}

.hts-section--soft::before,
.hts-section--inset::before,
.hts-section--soft::after,
.hts-section--inset::after {
    content: "";
    position: absolute;
    right: 0;
    left: 0;
    height: 1px;
    background: var(--hts-gradient-hairline);
    opacity: 0.6;
}

.hts-section--soft::before,
.hts-section--inset::before {
    top: 0;
}

.hts-section--soft::after,
.hts-section--inset::after {
    bottom: 0;
}

.hts-hairline {
    height: 1px;
    border: 0;
    background: var(--hts-gradient-hairline);
}

/* ── Shared text treatments ────────────────────────────────────────────── */

.hts-gradient-text {
    background: var(--hts-gradient-text);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.hts-lede {
    color: var(--hts-ink-soft);
    font-size: var(--hts-text-lg);
    line-height: var(--hts-leading-relaxed);
}

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ── Scroll reveal ─────────────────────────────────────────────────────── */

/* Elements marked `data-reveal` start slightly offset and settle into place the
   first time they enter the viewport. `wwwroot/js/site.js` adds `is-revealed`;
   if scripting is unavailable the `hts-js` class is never added and everything
   stays fully visible. */
[data-reveal] {
    opacity: 1;
}

html.hts-js [data-reveal] {
    opacity: 0;
    transform: translate3d(0, 18px, 0);
    transition:
        opacity var(--hts-duration-slow) var(--hts-ease-out),
        transform var(--hts-duration-slow) var(--hts-ease-out);
    transition-delay: var(--hts-reveal-delay, 0ms);
}

html.hts-js [data-reveal].is-revealed {
    opacity: 1;
    transform: none;
    will-change: auto;
}

/* Variants, so the page has a rhythm rather than one repeated entrance.
   Columns drift in from their own side; large panels settle in scale. */
html.hts-js [data-reveal="left"] {
    transform: translate3d(-26px, 0, 0);
}

html.hts-js [data-reveal="right"] {
    transform: translate3d(26px, 0, 0);
}

html.hts-js [data-reveal="scale"] {
    transform: scale(0.982);
}

html.hts-js [data-reveal="rise"] {
    transform: translate3d(0, 40px, 0);
}

/*
    A reveal that enters from the right sits 26px outside the page until it fires, and on a
    narrow screen that is 26px the visitor can scroll sideways into. `clip` rather than `hidden`
    because `hidden` would make the body a scroll container and break every `position: sticky`
    on the site.
*/
body {
    overflow-x: clip;
}

/*
    Mobile keeps the reveals but drops the horizontal ones.

    On a phone every section is one column, so content entering from the side is travelling
    across the whole screen to arrive where it would have been anyway - motion for its own
    sake, and the more expensive kind. The vertical reveals and the image wipes stay, because
    those are the ones that give the page its rhythm.
*/
@media (max-width: 720px) {
    html.hts-js [data-reveal="left"],
    html.hts-js [data-reveal="right"] {
        transform: translate3d(0, 18px, 0);
    }
}

/* ── Motion sections ─────────────────────────────────────────────────────
   A scene is not a larger fade-in. It is a small assembly sequence: the technical
   label establishes the coordinate, the rule is drawn, the title opens through a mask,
   then copy, visual and secondary details arrive. `site.js` only applies
   `.is-motion-active`; pages choose their roles with data attributes. That makes the
   vocabulary reusable without hiding content when scripting is unavailable. */

html.hts-js [data-motion]:not(.is-motion-active) [data-motion-step="label"] {
    opacity: 0;
    transform: translate3d(0, 8px, 0);
    letter-spacing: calc(var(--ht-track-label) + 0.12em);
}

html.hts-js [data-motion]:not(.is-motion-active) [data-motion-step="headline"] {
    clip-path: inset(0 0 105% 0);
    transform: translate3d(0, 0.55em, 0);
}

html.hts-js [data-motion]:not(.is-motion-active) [data-motion-step="copy"],
html.hts-js [data-motion]:not(.is-motion-active) [data-motion-step="details"],
html.hts-js [data-motion]:not(.is-motion-active) [data-motion-step="card"] {
    opacity: 0;
    transform: translate3d(0, 18px, 0);
}

html.hts-js [data-motion]:not(.is-motion-active) [data-motion-step="visual"] {
    opacity: 0;
    clip-path: inset(0 0 8% 0);
    transform: scale(0.97);
}

html.hts-js [data-motion] [data-motion-step="label"] {
    transition: opacity 320ms var(--hts-ease-out), transform 420ms var(--hts-ease-out), letter-spacing 460ms var(--hts-ease-out);
    transition-delay: var(--motion-delay, 0ms);
}

html.hts-js [data-motion] [data-motion-step="headline"] {
    transition: clip-path 660ms var(--hts-ease-out), transform 660ms var(--hts-ease-out);
    transition-delay: var(--motion-delay, 80ms);
}

html.hts-js [data-motion] [data-motion-step="copy"] {
    transition: opacity 440ms var(--hts-ease-out), transform 520ms var(--hts-ease-out);
    transition-delay: var(--motion-delay, 180ms);
}

html.hts-js [data-motion] [data-motion-step="visual"] {
    transition: opacity 500ms var(--hts-ease-out), transform 720ms var(--hts-ease-out), clip-path 720ms var(--hts-ease-out);
    transition-delay: var(--motion-delay, 250ms);
}

html.hts-js [data-motion] [data-motion-step="details"],
html.hts-js [data-motion] [data-motion-step="card"] {
    transition: opacity 400ms var(--hts-ease-out), transform 480ms var(--hts-ease-out);
    transition-delay: calc(var(--motion-delay, 340ms) + var(--hts-reveal-delay, 0ms));
}

html.hts-js [data-motion].is-motion-active [data-motion-step] {
    opacity: 1;
    clip-path: inset(0 0 0 0);
    transform: none;
    letter-spacing: inherit;
    will-change: auto;
}

/* Structural rules and topology connections draw after the label, before the detail. */
html.hts-js [data-motion]:not(.is-motion-active) [data-motion-draw] {
    transform: scaleX(0);
}

html.hts-js [data-motion] [data-motion-draw] {
    transform-origin: left center;
    transition: transform 620ms var(--hts-ease-out) 70ms;
}

html.hts-js [data-motion].is-motion-active [data-motion-draw] {
    transform: scaleX(1);
}

/* The scene observer continuously writes only this transform variable. It is reserved for
   selected media/diagrams, never for body copy or controls, so scrolling keeps its normal
   weight and text remains easy to read. */
html.hts-js [data-parallax] {
    --motion-parallax-y: 0px;
}

html.hts-js [data-parallax].is-revealed,
html.hts-js [data-motion].is-motion-active [data-parallax] {
    transform: translate3d(0, var(--motion-parallax-y), 0);
}

/* Promote only a parallax layer close enough to move. Keeping every off-screen layer promoted
   retains large textures in GPU memory on long pages. */
html.hts-js [data-parallax].is-motion-near {
    will-change: transform;
}

/* A departing scene does not vanish. Only explicitly marked technical garnish yields a little
   room to the next chapter, which creates continuity without taking readable content away. */
html.hts-js [data-scene].is-scene-leaving [data-scene-exit] {
    opacity: 0.42;
    transform: translate3d(0, -14px, 0);
}

html.hts-js [data-scene] [data-scene-exit] {
    transition: opacity 340ms var(--hts-ease-out), transform 460ms var(--hts-ease-out);
}

[data-count] {
    display: inline-block;
    font-variant-numeric: tabular-nums;
}

/* ── Photo plates ─────────────────────────────────────────────────────── */

/*
    A photograph used as a composition element rather than as an illustration in a box:
    full-bleed inside its frame, graded down into the page, with room for technical type on
    top of it.

    Shared here rather than in a component, because two very different families of image use
    it - photographs we took of our own rack, and game imagery belonging to a studio - and the
    difference between them belongs in the folders and the READMEs, not in two near-identical
    stylesheets.

    Everything below is CSS. The reveal is one transition on a class an IntersectionObserver
    adds; there is no frame loop, and nothing crosses to the server.
*/

.hts-plate {
    position: relative;
    margin: 0;
    min-width: 0;
}

/* The frame reserves its ratio before anything decodes, so nothing below it moves on arrival. */
.hts-plate-frame {
    position: relative;
    aspect-ratio: var(--plate-ratio, 16 / 9);
    border-radius: var(--hts-radius-lg);
    overflow: hidden;
    isolation: isolate;
    background: var(--hts-navy-950);
}

.hts-plate-image,
.hts-plate-standin {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--plate-position, center);
}

/* A lit photograph dropped into a near-black page glares. The grade settles it into the
   environment instead of leaving it sitting on top of it. */
.hts-plate-image {
    filter: saturate(0.9) contrast(1.05) brightness(0.86);
    transform: scale(1.045);
    transition: transform 1.5s var(--hts-ease-out);
}

/* Two gradients: down from the top so type has contrast wherever it lands, up from the bottom
   so the plate dissolves into the page rather than ending on a hard edge. */
.hts-plate-scrim {
    position: absolute;
    inset: 0;
    z-index: 1;
    background:
        linear-gradient(180deg, rgba(7, 10, 22, 0.74) 0%, rgba(7, 10, 22, 0.14) 40%, transparent 64%),
        linear-gradient(0deg, rgba(7, 10, 22, 0.92) 0%, rgba(7, 10, 22, 0.34) 28%, transparent 60%);
    pointer-events: none;
}

.hts-plate-overlay {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    align-items: flex-end;
    padding: clamp(1.1rem, 3vw, 2.5rem);
}

/* A rim, so a plate belongs to the same drawing set as the panels around it. */
.hts-plate-frame::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    border: 1px solid var(--hts-border-strong);
    border-radius: inherit;
    box-shadow: var(--hts-rim);
    pointer-events: none;
}

.hts-plate.is-pending .hts-plate-frame::after {
    border-style: dashed;
    border-color: var(--hts-rule);
}

/*
    The reveal is a wipe, not a fade: the frame opens from the bottom while the image settles
    out of a slight over-scale, which reads as a photograph arriving rather than an element
    switching on.
*/
html.hts-js [data-reveal="plate"] {
    opacity: 1;
    transform: none;
}

html.hts-js [data-reveal="plate"] .hts-plate-frame {
    clip-path: inset(0 0 100% 0);
    transition: clip-path 1.05s var(--hts-ease-out);
}

html.hts-js [data-reveal="plate"].is-revealed .hts-plate-frame {
    clip-path: inset(0 0 0 0);
}

html.hts-js [data-reveal="plate"].is-revealed .hts-plate-image {
    transform: scale(1);
}

/* Where to drop a file that has not been supplied. On the page, because a note in a README is
   a note nobody opens. */
.hts-plate-drop {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    margin-top: var(--hts-space-3);
}

.hts-plate-drop .drop-badge {
    padding: 0.16rem 0.55rem;
    border: 1px solid var(--hts-border-blue);
    border-radius: var(--hts-radius-pill);
    color: var(--hts-blue-300);
    font-family: var(--hts-font-mono);
    font-size: 0.5rem;
    font-weight: 700;
    letter-spacing: 0.2em;
    white-space: nowrap;
}

.hts-plate-drop .drop-path {
    color: var(--hts-console-accent);
    font-family: var(--hts-font-mono);
    font-size: 0.5625rem;
    line-height: 1.5;
    overflow-wrap: anywhere;
}

@media (prefers-reduced-motion: reduce) {
    html.hts-js [data-reveal="plate"] .hts-plate-frame {
        clip-path: none;
        transition: none;
    }

    .hts-plate-image {
        transform: none;
        transition: none;
    }
}

/* ── Text effects ────────────────────────────────────────────────────────── */

/*
    `wwwroot/js/site.js` wraps rise text by word and reserves per-character spans for the
    flicker/sweep effects that actually need them. Each animation unit carries its index.

    All of it is additive: without scripting there are no spans, the selectors match
    nothing, and the heading is simply a heading. The stagger runs off the character
    index, so a line settles left to right at a fixed rate rather than a fixed total
    duration - a long headline takes longer than a short one, which is what reading
    it does too.
*/

/* The wrapper keeps the split to a single item, so a flex parent lays it out as one run of
   text rather than as one item per word. */
[data-text-effect] .split {
    display: inline;
}

/* A word is the unit a line breaks on. Without this the characters, being inline-block,
   would let the browser break a heading in the middle of a word. */
[data-text-effect] .word {
    display: inline-block;
    white-space: nowrap;
}

[data-text-effect] .char {
    display: inline-block;
}

/* --- rise: words lift into focus, one after another --------------------- */

html.hts-js [data-text-effect="rise"] .char {
    opacity: 0;
    transform: translate3d(0, 0.55em, 0);
    filter: blur(6px);
}

html.hts-js [data-text-effect="rise"].is-revealed .char,
html.hts-js .is-revealed [data-text-effect="rise"] .char {
    animation: char-rise 0.62s var(--hts-ease-out) both;
    animation-delay: calc(var(--char-index) * 26ms);
}

@keyframes char-rise {
    from {
        opacity: 0;
        transform: translate3d(0, 0.55em, 0);
        filter: blur(6px);
    }

    to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
        filter: blur(0);
    }
}

/* --- flicker: characters come up like a display warming, some twice ------

   The offset on the second keyframe is deliberately not uniform: every character
   dips once on its way in, and because the dip sits at a different point in each
   character's own timeline the line reads as letters appearing and disappearing
   rather than as one block fading up. */

html.hts-js [data-text-effect="flicker"] .char {
    opacity: 0;
}

html.hts-js [data-text-effect="flicker"].is-revealed .char,
html.hts-js .is-revealed [data-text-effect="flicker"] .char {
    animation: char-flicker 0.9s var(--hts-ease) both;
    animation-delay: calc(var(--char-index) * 34ms);
}

@keyframes char-flicker {
    0% {
        opacity: 0;
        transform: translate3d(0, -0.18em, 0);
    }

    18% {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }

    26% {
        opacity: 0.06;
    }

    40% {
        opacity: 1;
    }

    52% {
        opacity: 0.35;
    }

    100% {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }
}

/* --- sweep: a light passes along the line as the characters arrive ------ */

html.hts-js [data-text-effect="sweep"] .char {
    opacity: 0;
}

html.hts-js [data-text-effect="sweep"].is-revealed .char,
html.hts-js .is-revealed [data-text-effect="sweep"] .char {
    animation: char-sweep 0.85s var(--hts-ease-out) both;
    animation-delay: calc(var(--char-index) * 30ms);
}

@keyframes char-sweep {
    0% {
        opacity: 0;
        color: var(--hts-blue-300);
        transform: translate3d(-0.3em, 0, 0);
    }

    45% {
        opacity: 1;
        color: var(--hts-blue-300);
        text-shadow: 0 0 14px rgba(131, 168, 243, 0.75);
    }

    100% {
        opacity: 1;
        color: inherit;
        text-shadow: none;
        transform: translate3d(0, 0, 0);
    }
}

/*
    Motion is a preference, not a detail. With it suppressed every effect collapses
    to "the text is there" - no animation, no residual transform, no blur.
*/
@media (prefers-reduced-motion: reduce) {
    html.hts-js [data-text-effect] .char {
        opacity: 1 !important;
        transform: none !important;
        filter: none !important;
        text-shadow: none !important;
        animation: none !important;
    }
}

/* ── Pointer spotlight ─────────────────────────────────────────────────── */

/* A panel marked `data-spotlight` lights from wherever the cursor is over it.
   wwwroot/js/site.js publishes the position as --spot-x / --spot-y and nothing
   else; everything visible is decided here.

   The overlay is an ::after at z-index -1 inside an isolated stacking context, so
   it sits above the panel's own background and below its content without any
   element needing to be wrapped. */
[data-spotlight] {
    position: relative;
    isolation: isolate;
}

html.hts-js [data-spotlight]::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: radial-gradient(
        16rem 16rem at var(--spot-x, 50%) var(--spot-y, 50%),
        var(--hts-spotlight),
        transparent 68%);
    opacity: 0;
    transition: opacity var(--hts-duration-slow) var(--hts-ease);
    pointer-events: none;
}

html.hts-js [data-spotlight].is-spotlit::after {
    opacity: 1;
}

/* ── Route entrance ────────────────────────────────────────────────────── */

/* Moving between the homepage, the infrastructure page and sign-in should feel
   like one surface changing rather than three separate loads. The whole main
   region lifts in; site.js restarts it after Blazor's enhanced navigation, which
   patches the DOM in place and would otherwise leave the animation spent.

   Only opacity and a small translate, and the final frame is `transform: none`
   so no containing block is left behind for the sticky sections further down. */
/*
    The page arrives in two movements rather than one.

    The whole document fades and lifts, and the first screen's own content follows a beat
    later - so a navigation reads as somewhere new opening rather than as a repaint. The
    second movement is what the single fade was missing: 12px on one element in 460ms is
    below the threshold at which a person registers that anything happened at all.

    Both are compositor properties on a `both`-filled animation, so nothing reflows and
    nothing is left half-applied if the animation is interrupted by another navigation.
*/
html.hts-js main[data-page-enter] {
    animation: page-enter 520ms var(--hts-ease-out) both;
}

@keyframes page-enter {
    from {
        opacity: 0;
        transform: translate3d(0, 16px, 0);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

/* The first section, a beat behind the page it is on. Only the first: past the fold the
   scroll reveals take over, and animating everything at once is the thing that makes a
   page feel like it is performing rather than arriving. */
html.hts-js main[data-page-enter] > *:first-child {
    animation: page-enter-lead 620ms var(--hts-ease-out) 90ms both;
}

@keyframes page-enter-lead {
    from {
        opacity: 0;
        transform: translate3d(0, 22px, 0);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

/* ── Navigation progress ───────────────────────────────────────────────── */

/*
    Enhanced navigation swaps the document in place, which is fast and completely silent.
    This is the only thing on the page that reports the wait while it is happening; the
    entrance animation plays afterwards, which is too late to answer "did my click land?".

    Fixed, above everything, and two pixels tall. It scales on a transform rather than
    animating width, so the creep costs one compositor property and no layout.
*/
.hts-progress {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 200;
    height: 2px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms linear;
}

.hts-progress::before {
    content: "";
    display: block;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg, var(--hts-blue-400), var(--hts-purple-400));
    box-shadow: 0 0 12px rgba(176, 105, 255, 0.7);
    transform: scaleX(calc(var(--progress, 0%) / 100%));
    transform-origin: left center;
    transition: transform 220ms var(--hts-ease-out);
}

.hts-progress.is-active {
    opacity: 1;
}

/* The last stretch is quicker than the creep: the request has landed, and a bar that
   crawled to 100 after the page was already there would read as the page being slow. */
.hts-progress.is-done::before {
    transition: transform 180ms var(--hts-ease-out);
}

/* ── Reduced motion ────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    html.hts-js [data-reveal] {
        opacity: 1;
        transform: none;
    }

    html.hts-js [data-motion] [data-motion-step] {
        opacity: 1 !important;
        clip-path: none !important;
        transform: none !important;
        letter-spacing: inherit !important;
        transition: none !important;
    }

    html.hts-js [data-motion] [data-motion-draw] {
        transform: scaleX(1) !important;
        transition: none !important;
    }

    html.hts-js [data-parallax] {
        transform: none !important;
        transition: none !important;
    }

    html.hts-js main[data-page-enter],
    html.hts-js main[data-page-enter] > *:first-child {
        animation: none;
    }

    /* The bar reports a wait, which is information, so it stays. What goes is the
       travelling fill - it appears at its position instead of sliding to it. */
    .hts-progress::before {
        transition: none;
    }

    /* The spotlight is a position readout, not motion, so it stays - but it stops
       fading, which is the part that moves. */
    html.hts-js [data-spotlight]::after {
        transition: none;
    }
}

/* ── Blazor error UI ───────────────────────────────────────────────────── */

#blazor-error-ui {
    position: fixed;
    bottom: 0;
    left: 0;
    z-index: var(--hts-z-overlay);
    display: none;
    width: 100%;
    padding: var(--hts-space-4) var(--hts-space-6);
    border-top: 1px solid var(--hts-border-brand);
    background: var(--hts-surface-raised);
    box-shadow: var(--hts-shadow-lg);
    color: var(--hts-ink);
    font-size: var(--hts-text-base);
}

#blazor-error-ui .dismiss {
    position: absolute;
    top: var(--hts-space-2);
    right: var(--hts-space-4);
    cursor: pointer;
}
