@charset "UTF-8";

/* Registers --spin-angle as a real, interpolatable <angle> custom
   property (rather than a plain string) so the google-summits-spinner
   asset's outer-ring gradient sweep (js/sequence-assets.js) animates
   smoothly instead of jumping. Declared here, in a normal stylesheet,
   rather than inside that asset's own inline SVG <style> block —
   Chromium silently fails to register @property when it's declared
   inside an SVG <style> element (confirmed directly: the same rule
   registers fine in an HTML stylesheet, but getComputedStyle() comes
   back empty for it when the identical text sits inside <svg><style>).
   @property registration is document-global once it succeeds, so
   having it here still reaches the custom property used inside that
   asset's foreignObject-embedded HTML divs. */
@property --spin-angle {
  syntax: '<angle>';
  inherits: true;
  initial-value: 0deg;
}

/* ============================================================
   FILM & ANIMATION
   Styles for scroll-driven animated media used as media fills
   within layout components. Mirrors the background-image fill
   pattern used by .split-layout-media. The actual playback
   engine is js/scroll-sequence.js — see the comment block at the
   top of that file for the full data-sequence-* / data-seq-anim-*
   markup contract. This file only carries the visual treatment.
   ============================================================ */

/* ── Scroll-driven sequence modes ─────────────────────────
   scroll-sequence.js wires up any .split-layout-media that
   carries a data-sequence-type attribute:

     frame-sequence  — numbered still frames (png/jpg/gif/svg/...)
                        preloaded and painted into a <canvas>
     svg-animation   — a single self-contained SVG, injected
                        inline and scrubbed via data-seq-anim
                        attributes on its own elements

   Both map the container's viewport position to progress 0→1
   (0 = entering viewport bottom, 1 = leaving viewport top), the
   same formula/easing the old <video> system used.
   ──────────────────────────────────────────────────────────── */

/* <canvas> injected by scroll-sequence.js for frame-sequence
   instances — fills the container exactly like the old <video>. */
.split-layout-media canvas {
   position: absolute;
   inset: 0;
   width: 100%;
   height: 100%;
   display: block;
   z-index: 0;
   pointer-events: none;
   transition: opacity 0.15s ease;
}

/* Wrapper <div> injected by scroll-sequence.js for svg-animation
   instances — markup from the js/sequence-assets.js manifest is
   injected inline inside it (see that file's header for why it's a
   manifest rather than a fetched/embedded .svg file). Same fill/
   position/z-index treatment as the canvas above so both sequence
   types behave identically inside .split-layout-media. */
.seq-svg-wrap {
   position: absolute;
   inset: 0;
   width: 100%;
   height: 100%;
   display: block;
   z-index: 0;
   opacity: 0;
   pointer-events: none;
   transition: opacity 0.15s ease;
}

/* .full-width-image's animated instance (the Full Width
   Image module on the Google Summits page) doesn't want the wrap
   stretched edge-to-edge like the 50/50 hero does — the module is a
   wide flat-colour band and the illustration should sit as a modest,
   centred mark within it, not fill it. inset:0 + an explicit
   width/height + margin:auto centres an absolutely positioned box
   inside its container, same trick as a fixed-size modal.

   Sizing math: the google-summits-key-visual asset (js/sequence-
   assets.js) is cropped to a viewBox="270 345 480 350" — tight around
   the mark's own getBBox() envelope (measured by sampling every ~50ms
   across its full animation, since the nodes travel: x stays pinned
   294–731 the whole time, so width is a constant 437; height only
   grows from 277 at rest to 306 at its widest swing) plus a ~22px
   margin on every side so a node never clips mid-travel. Because the
   crop is already tight, data-sequence-fit="contain" fitting that
   480x350 viewBox to this wrap means the wrap's own width (this wrap
   shares the viewBox's 480:350 aspect ratio, so "contain" scales
   uniformly with no letterboxing either way) maps the mark's 437-wide
   content to (wrap-width * 437/480) px — to get the mark itself to
   ~256px wide (not the viewBox's own edges) at the large end, the
   wrap needs to be sized to 256 / (437/480) ≈ 281px there.

   --seq-anim-width is a clamp with a deliberately shallow vw slope —
   the module itself spans nearly the full viewport width, so scaling
   the mark at anywhere near that rate would make it swing wildly;
   scaling it much slower than its container is what keeps it legible
   and means it reads a bit larger, relatively, on small screens. One
   continuous clamp covers both the 20:8 desktop band and the 1:1
   mobile container below 1080px — no separate breakpoint-specific
   size needed. Height is derived from width via the viewBox's own
   480:350 ratio rather than a second clamp, so the two can never
   drift out of sync. */
.full-width-image[data-sequence-type] .seq-svg-wrap {
   /* px parts are screen pixels (÷ page scale), so the mark fits its band
      exactly as it did before the page scale (js/ui-prefs.js) */
   --seq-anim-width: clamp(calc(189px / var(--ui-scale)), calc(10.2 * var(--vw)) + calc(151px / var(--ui-scale)), calc(281px / var(--ui-scale)));
   inset: 0;
   margin: auto;
   width: var(--seq-anim-width);
   height: calc(var(--seq-anim-width) * 350 / 480);
}

/* ============================================================
   ASSET REGISTER
   One rule per animated asset, keyed to the source file, for any
   asset that needs styling beyond what scroll-sequence.js already
   applies. Add new entries here as more animations are introduced.
   ============================================================ */

/* Clearance for KVs centred under a corner logo overlay: the overlay's
   top offset (--content-padding) + the tallest Google Cloud lockup
   (64px / sqrt(512/91) = 27px) + a 16px gap. Used at the top AND bottom
   of the centred art, so it stays centred. */
.case-study-teaser-media {
   --kv-logo-clear: calc(var(--content-padding) + 43px);
}

/* animated-assets/google-summits-logo-build/security-summits-logo-build-v6.svg
   Security Summits logo build loop (google-summits.html, "An idea taking
   shape"). NOT a scroll-sequence asset: a self-contained SVG whose own CSS
   @keyframes (plus one SMIL <animate> for the shield's turntable spin) loop
   on a timer (27.5s), shown with a plain <img>, so it works from file:// and
   needs no script. Under prefers-reduced-motion the
   <picture> swaps in the -still.svg (finished logo); the SVG also has its
   own reduced-motion guard, but browsers don't reliably pass that
   preference into images. Generated by the script in
   Desktop\davidflitcroft.com-tools\security-summits-logo-build (bump the
   -vN in the filename on regeneration: animated-assets/*.svg is cached as
   immutable in _headers).
   No logo overlay on this panel (review 2026-11-10). The SVG's viewBox
   is the construction grid exactly, so the whole grid + logo is centred
   in the panel and contained inside an inset of --content-padding: on
   the limiting axis the gap to the panel edge equals the text column's
   padding beside it; on the other axis it's larger, centred. */
.logo-build-media .logo-build-anim {
   position: absolute;
   inset: var(--content-padding);
   width: calc(100% - 2 * var(--content-padding));
   height: calc(100% - 2 * var(--content-padding));
   object-fit: contain;
   object-position: center;
   display: block;
   z-index: 0;
   pointer-events: none;
}

/* animated-assets/sequence-demo/black-square.svg */
/* No additional rules needed — sizing/position come from
   .seq-svg-wrap above; the animation itself lives in the SVG file
   via its data-seq-anim attributes. */

/* animated-assets/case-study-inspace/ — Inspace case study hero,
   48-frame webp camera orbit around a tower render. No additional
   rules needed — sizing/position come from the canvas rules above;
   the container's own inline background-color (set in index.html)
   matches the renders' #CCCCCC backdrop for a seamless edge while
   frames are loading or if a frame's edge anti-aliases to
   semi-transparent. */

/* google-summits-key-visual (js/sequence-assets.js), the Global KV in the
   full-width band on google-summits.html: sized and centred by the
   .full-width-image[data-sequence-type] .seq-svg-wrap rule above. */

/* svg-animation in a Case Study Teaser's narrow (30%) media column.
   Centred as a modest mark, capped to a share of the column's own width
   so it never crowds the narrower panel. top/left + translate (rather
   than inset:0 + margin:auto) lets aspect-ratio derive the height.
   Default ratio is the key visual's 480:350 viewBox. */
.case-study-teaser-media[data-sequence-type="svg-animation"] .seq-svg-wrap {
   inset: auto;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   width: min(75%, calc(281px / var(--ui-scale)));
   height: auto;
   aspect-ratio: 480 / 350;
}

/* google-summits-spinner (the Spinner KV) in the homepage Google Case
   Study Teaser: the square 1024 canvas has its own margin, so the wrap
   fills the panel (capped at 420px each way) and data-sequence-fit=
   "contain" keeps it square and centred. */
.case-study-teaser-media[data-sequence-src="google-summits-spinner"] .seq-svg-wrap {
   inset: 0;
   margin: auto;
   transform: none;
   width: min(100%, calc(420px / var(--ui-scale)));
   height: min(100%, calc(420px / var(--ui-scale)), calc(100% - 2 * var(--kv-logo-clear)));
   aspect-ratio: auto;
}

/* Homepage Inspace Case Study Teaser: the frame-sequence render sits
   30% smaller inside its #CCCCCC panel at desktop widths, where the
   30/70 column is tall and narrow. Below 1024px (stacked 16:9 panel)
   it stays full size. transform rather than width/height/inset
   because scroll-sequence.js sets those inline on the canvas; scaling
   also keeps the canvas's full-resolution bitmap, so nothing softens.

   Once inset, the render's own edges become visible, so: the panel's
   inline background is #D0D0D0 (sampled from the frames' backdrop,
   slightly lighter than the #CCCCCC used where the render fills the
   panel), and the canvas edges are feathered so the ground-plane
   streets that run off-frame fade out instead of ending on a hard
   line. */
/* breakpoint (min-width: 1025px): applied by class from js/ui-prefs.js (effective
   width = window width / UI scale); the @media copy is the no-JS fallback. */
html:not(.bp-max-1024) .case-study-teaser-media[data-sequence-type="frame-sequence"] canvas {
      transform: scale(0.7);
      -webkit-mask-image:
         linear-gradient(to right, transparent, #000 10%, #000 90%, transparent),
         linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
      -webkit-mask-composite: source-in;
      mask-image:
         linear-gradient(to right, transparent, #000 10%, #000 90%, transparent),
         linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
      mask-composite: intersect;
   }
@media (min-width: 1025px) {
	html:not(.dfui) .case-study-teaser-media[data-sequence-type="frame-sequence"] canvas {
      transform: scale(0.7);
      -webkit-mask-image:
         linear-gradient(to right, transparent, #000 10%, #000 90%, transparent),
         linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
      -webkit-mask-composite: source-in;
      mask-image:
         linear-gradient(to right, transparent, #000 10%, #000 90%, transparent),
         linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
      mask-composite: intersect;
   }
}

/* ── iso-pan (js/scroll-sequence.js → IsoPanInstance) ─────────
   Homepage Stan Case Study Teaser: isometric camera tracking shot
   under a photographic (tilt-shift style) depth-of-field blur.

   ANGLES
   --iso-angle  the pan track, set inline by the JS from data-iso-angle.
   --dof-angle  the focal plane, set inline from data-dof-angle (falls
                back to the track angle). Both are maths angles:
                counter-clockwise from horizontal. 30 = the iso axis
                rising to the right, -30 = the one falling to the right
                (the long boards in the Stan render).
   CSS gradients measure CLOCKWISE FROM "TO TOP", so the gradient that
   runs perpendicular to a line at angle a is 180deg - a. The JS does
   that conversion when it builds each layer's mask, so the focal band
   always lies exactly along the chosen iso axis.

   WHY A STACK OF LAYERS (progressive blur)
   A single blur sheet with a soft mask is really a cross-fade between
   "sharp" and "16px blurred" — mid-way you see a ghosted double image,
   which is the giveaway of a fake. A real lens doesn't do that: the
   circle of confusion (blur radius) GROWS CONTINUOUSLY with distance
   from the focal plane. On a flat iso "table top" (the whole point of
   tilt-shift) scene depth varies linearly across the frame, so blur
   radius grows ~linearly with distance from the focal line.

   The JS approximates that curve with --dof-layers stacked
   backdrop-blur sheets, radii doubling (… 3, 6, 12, 24px). Each sheet
   is masked to fade in exactly where the target blur reaches its own
   radius and stays on out to the frame edge. Because each sheet
   blurs what's already been blurred beneath it, the effective radius
   at any point is ~√(Σr²): smooth, monotonic, no ghosting.

   TUNING (all here, none in the JS)
   --dof-max-blur  blur radius reached at the far edges of the frame
   --dof-focus     half-width of the fully sharp band, as a fraction of
                   the distance from the focal line to the frame edge
   --dof-layers    number of sheets (more = smoother, heavier)
   --dof-tone      extra filters on the outermost sheet only. A real
                   lens spreads a highlight's light into a disc rather
                   than averaging it into grey the way a Gaussian blur
                   does, so far out-of-focus areas look slightly
                   brighter and richer, not muddier. A gentle
                   brightness/saturation lift mimics that "bokeh bloom".
   Values are read once on load (and re-read on resize). */
.split-layout-media[data-sequence-type="iso-pan"] {
   --dof-max-blur: 12px;   /* was 24px: halved strength */
   --dof-focus: 0.57;      /* was 0.14: blur now covers the outer ~43%
                              of the distance to the edge, half the
                              previous ~86% reach */
   --dof-layers: 4;        /* 1.5 / 3 / 6 / 12px. A 5th layer would only
                              add an invisible 0.75px sheet at this
                              strength, so drop it and save the cost */
   --dof-tone: brightness(1.025) saturate(1.075); /* halved with the blur */
}

/* The moving image. inset is overwritten in px by the JS to oversize
   the layer by exactly the pan travel on each side (plus a 2px safety
   margin), so its edges never enter the frame. Only transform changes
   per frame. */
.iso-pan-layer {
   position: absolute;
   inset: 0;
   z-index: 0;
   background-size: cover;
   background-position: center;
   background-repeat: no-repeat;
   opacity: 0;
   transition: opacity 0.15s ease;
   will-change: transform;
   pointer-events: none;
}

/* One blur sheet of the progressive stack. Static — never moves. The
   JS sets --b (this sheet's radius), --t (tone filters, outermost
   sheet only) and the mask-image. Deliberately direct children of the
   media container with no wrapper: a wrapper with opacity/filter/mask
   would become a "backdrop root" and cut the sheets off from the
   image behind them. Sit below .logo-overlay (z-index 2). */
.iso-pan-dof-layer {
   position: absolute;
   inset: 0;
   z-index: 1;
   pointer-events: none;
   -webkit-backdrop-filter: blur(var(--b, 0px)) var(--t, );
   backdrop-filter: blur(var(--b, 0px)) var(--t, );
}

/* ── Custom HTML loader overlay ───────────────────────────
   Place a .scroll-video-loader element inside your
   .split-layout-media container and design it freely in CSS.
   scroll-sequence.js doesn't currently toggle this (frame/SVG
   loading is fast enough not to need a per-instance spinner), but
   the class is kept available for a future sequence that does.

   z-index layering:
     0  canvas / svg wrap    (background)
     1  .scroll-video-loader (above canvas, below logo)
     2  .logo-overlay        (always on top)
   ──────────────────────────────────────────────────────── */
.scroll-video-loader {
   position: absolute;
   inset: 0;
   z-index: 1;
   display: flex;
   align-items: center;
   justify-content: center;
   transition: opacity 0.4s ease;
}

.scroll-video-loader--hidden {
   opacity: 0;
   pointer-events: none;
}

/* ── custom loading animation ───────────────────────────
everything is below
   ──────────────────────────────────────────────────────── */

/* Absolute Centering Box Layout */
.scroll-video-loader {
   width: 100%;
   height: 100%;
   min-height: 512px;
   background: transparent;
   display: flex;
   align-items: center;
   justify-content: center;
   box-sizing: border-box;
}

/* Scalable Vector Frame */
.cube-loader-svg {
   width: 100%;
   max-width: 512px;
   height: auto;
   display: block;
}

/* ── Cube loader SVG ──────────────────────────────────────
   The SVG frame and circles are animated entirely by
   cube-loader.js — no CSS keyframes needed.
   The SVG itself just needs to be centred and scaled.
   ──────────────────────────────────────────────────────── */
.cube-loader-svg {
   width: 100%;
   max-width: 512px;
   height: auto;
   display: block;
}

/* ============================================================
   PAGE LOADER OVERLAY
   Full-screen loading screen shown on page arrival.
   Fades out once all video assets are fetched.
   ============================================================ */

/* Lock scroll while the loader is present */
body:has(#page-loader) {
  overflow: hidden;
}

/* The overlay itself — fixed, full viewport, white */
.page-loader {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: var(--color-background-page);
  display: flex;
  align-items: center;
  justify-content: center;
  /* opacity transition handled inline by JS for precise timing */
}

/* Reuse .section-container + .printer-marks-container exactly as the
   rest of the page uses them — same max-width, padding, marks.
   We just need the inner content to fill the full viewport height. */
.page-loader > .section-container {
  width: 100%;
  height: calc(100 * var(--vh));
  height: calc(100 * var(--vh));
  display: flex;
  align-items: center;
  justify-content: center;
}

.page-loader__inner {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The centred column of: SVG → progress bar → text */
.page-loader__content {
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* SVG animation: half the original 256px design size */
.page-loader__svg {
  display: block;
  width: 128px;
  height: auto;
  margin-bottom: 24px;
}

/* Progress bar track — same width as the SVG */
.page-loader__progress-track {
  width: 128px;
  height: 1px;
  background-color: var(--color-background-muted);
  position: relative;
  overflow: hidden;
  margin-bottom: 16px;
}

/* Progress fill — JS sets width% */
.page-loader__progress-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  height: 100%;
  background-color: var(--color-background-inverse);
}

/* Loading text — body copy style.
   display:inline-block lets the flex parent centre it on the
   width of the static text alone; the dots are absolutely
   positioned so their animation never shifts that centre. */
.page-loader__text {
  font-family: "Nunito Sans", sans-serif;
  font-size: clamp(0.656rem, calc(0.31 * var(--vw)) + 0.594rem, 0.844rem);
  line-height: 140%;
  color: var(--color-text-secondary);
  margin: 0;
  padding: 0;
  white-space: nowrap;
  position: relative;
  display: inline-block;
}

.page-loader__dots {
  position: absolute;
  left: 100%;
  top: 0;
  width: 1.5em;
  text-align: left;
}

/* ============================================================
   ANIMATION SANDBOX (staging.html, bottom of the page)
   Uncropped preview panels for KVs (key visuals) in development, at the
   Figma KV frames' 2880:2048 proportions.

   KV NODE SCALE (sandbox): every KV is sized
   so its round node (the dots at the ends of the segments) has the same
   diameter, --kv-node (css/tokens.css), at every width: the Security
   KV's scale, 3.7% of a full-width module (~49px at a 1440px viewport,
   66.6px at 1920). A KV is drawn --kv-nodes-w node-widths wide, its
   artwork's width measured in node diameters (SVG width / node
   diameter in SVG units). Sandbox only: on the live pages each KV fits
   its own container (rules above), which can't share one node size:
     Security 13.175 (527 / 40)       Spinner 16.862 (1024 / 60.73)
     Global 6 (480 / 80)              Radial 10 (800 / 80)
     Diagonal 11.887 (951 / 80)       Horizontal 10.563 (845 / 80)
     Vertical 6.5 (520 / 80)
   Recalculate a KV's number if its viewBox or node size changes.
   The scroll sequences' wrap uses --kv-aspect for its height; their
   data-sequence-fit="contain" keeps their own ratio inside it.
   ============================================================ */
.full-width-image.anim-sandbox {
   aspect-ratio: 2880 / 2048;
}

.anim-sandbox .anim-sandbox-art {
   position: absolute;
   inset: 0;
   margin: auto;
   width: calc(var(--kv-nodes-w) * var(--kv-node));
   height: auto;
   max-width: none;
   display: block;
   pointer-events: none;
}

.full-width-image.anim-sandbox[data-sequence-type] .seq-svg-wrap {
   inset: 0;
   margin: auto;
   width: calc(var(--kv-nodes-w) * var(--kv-node));
   height: auto;
   aspect-ratio: var(--kv-aspect, 1 / 1);
}
