/* nd10-theme.css — ND10 rebrand skin.
 *
 * ADDITIVE ONLY. This file changes two CUSTOM PROPERTY values and adds NEW
 * rules on top of EXISTING selectors (.tb-btn, .ds-ctab, .fs-enter-btn,
 * .ide-proj-add, .app-mode). It defines zero new HTML, removes zero
 * selectors, and deletes zero buttons — every control that exists today
 * keeps its exact markup and behavior; only its paint changes.
 *
 * Loaded via a single <link> after index.html's inline <style>, so cascade
 * order alone gives these rules priority without needing !important.
 *
 * Design source: nd10-shell-v8.html / nd10-open-v9.html mockups (approved
 * mockup round, 2026-08-06) — wireframe-corner-brackets × liquid-glass ×
 * dithered ambient corners. No sound in this pass (explicitly excluded).
 */

:root {
  /* ── Token remap ──────────────────────────────────────────────
     --blue is documented in index.html as "DevStudio accent — electric
     blue" — the mode this rebrand targets. --gold ("Folio accent") is a
     DELIBERATELY separate sub-brand and is left untouched on purpose: the
     ask was to skin DevStudio, not blur the two modes the app already
     distinguishes. */
  --blue:   #7cf5ff;
  --blue-d: rgba(124,245,255,.14);
  --blue-g: rgba(124,245,255,.30);

  /* DJ's ruling (Stage 2, 2026-08-12): cyan IS the Indie10 accent. Setting
     the channel here re-tints every translucent accent use in one move —
     every `rgba(var(--accent-rgb), …)` across the app follows this line.
     Revert = change these three numbers back to 59, 130, 246. */
  --accent-rgb: 124, 245, 255;

  /* New tokens this layer introduces, namespaced so they can never collide
     with the app's existing --bg/--rule/--text scale. */
  --nd-wire:      rgba(150,205,235,.20);
  --nd-wire-hot:  rgba(150,225,255,.55);
  --nd-glass-top: rgba(255,255,255,.10);
  --nd-glass-bot: rgba(0,0,0,.30);
  --nd-dither:    .40;   /* ambient corner opacity — one dial */
}

/* ── Corner brackets — a CONTAINER device, applied only to control-scale
   elements that are large enough to carry them (>=26px). Buttons under
   that height keep their existing look untouched; brackets on a control
   that small read as broken corners, which is the exact mistake an
   earlier mockup pass made and corrected. ── */
.nd-brk { position: relative; }
.nd-brk::before, .nd-brk::after {
  content: ''; position: absolute; width: 7px; height: 7px; pointer-events: none;
  background:
    linear-gradient(var(--nd-wire-hot), var(--nd-wire-hot)) top left / 7px 1px no-repeat,
    linear-gradient(var(--nd-wire-hot), var(--nd-wire-hot)) top left / 1px 7px no-repeat;
}
.nd-brk::before { top: -1px; left: -1px; }
.nd-brk::after  { bottom: -1px; right: -1px; transform: rotate(180deg); }

/* ── Topbar cluster buttons (.tb-btn) — glass background, brackets on the
   active state only, so idle chrome stays quiet. ── */
.tb-btn {
  background: linear-gradient(180deg, rgba(255,255,255,.03) 0%, rgba(255,255,255,.005) 60%, rgba(0,0,0,.08) 100%);
}
.tb-btn:hover {
  box-shadow: inset 0 1px 0 var(--nd-glass-top), inset 0 -1px 0 var(--nd-glass-bot);
}
.tb-btn.active,
.tb-btn.gold-active {
  position: relative;
  box-shadow: inset 0 1px 0 var(--nd-glass-top), inset 0 -1px 0 var(--nd-glass-bot),
              0 0 0 1px var(--nd-wire-hot);
}

/* ── Channels/Flows/Tools tab strip (.ds-ctab, from devstudio-chat-tabs.js)
   — same glass treatment, brackets on the active tab. ── */
.ds-ctab.active {
  position: relative;
  box-shadow: inset 0 1px 0 var(--nd-glass-top), 0 0 0 1px var(--nd-wire-hot);
}

/* ── Welcome-screen ENTER + mode buttons — the largest controls in the app,
   so these get the full bracket treatment. ── */
.fs-enter-btn,
.app-mode {
  position: relative;
}
.fs-enter-btn::before, .fs-enter-btn::after,
.app-mode::before, .app-mode::after {
  content: ''; position: absolute; width: 8px; height: 8px; pointer-events: none;
  background:
    linear-gradient(var(--nd-wire-hot), var(--nd-wire-hot)) top left / 8px 1px no-repeat,
    linear-gradient(var(--nd-wire-hot), var(--nd-wire-hot)) top left / 1px 8px no-repeat;
}
.fs-enter-btn::before, .app-mode::before { top: -1px; left: -1px; }
.fs-enter-btn::after,  .app-mode::after  { bottom: -1px; right: -1px; transform: rotate(180deg); }

/* ── Project-chip add button (.ide-proj-add) — small; glass only, no
   brackets, per the >=26px rule above. ── */
.ide-proj-add {
  background: linear-gradient(180deg, rgba(59,130,246,.10), rgba(59,130,246,.05));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10);
}

/* ── Ambient dither mount points ──────────────────────────────
   Canvases are inserted by nd10-ambient.js; this just positions and masks
   them so they never sit above interactive content (pointer-events:none)
   and never appear anywhere the script didn't explicitly place one. */
.nd-amb {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: var(--nd-dither);
}
.nd-amb canvas {
  display: block; width: 100%; height: 100%;
  image-rendering: pixelated; image-rendering: crisp-edges;
}
/* Containing block for the .nd-amb canvases above. #topbar is already
   position:relative in index.html — restated here only so this file is
   self-explanatory. #welcome is DELIBERATELY ABSENT: it is
   `position:fixed; inset:0` (index.html), which ALREADY establishes a
   containing block for absolute children, and this sheet loads last, so
   naming it here silently demoted it to `relative`. Since devstudio-dock.js
   makes <body> a flex column, a relative #welcome stops covering the
   viewport and collapses to content height — the splash then renders at
   ~430px in a 900px window with the rest black. Do not add #welcome back. */
#topbar { position: relative; }

/* ══════════════════════════════════════════════════════════════
   INTRO TIMELINE — the whole startup sequence, tunable from here.
   Change these six numbers and the entire intro retimes; nothing
   else needs editing. Read top to bottom, it IS the storyboard:

     0.00s  black
     0.30s  FINAL STAGE cuts in                 (--intro-mark-in)
     0.30s  glitch pass begins                  (--intro-glitch-a)
     1.90s  glitch ends on its own, clean
     1.95s  FINAL STAGE leaves — back to black  (--intro-studio-out)
     2.05s  iris opens onto the dither field    (--intro-iris-at)
     3.00s  INDIE10 lands on the field          (--intro-mark-app)
     3.35s  the interface follows               (--intro-ui-at)

   The glitch is an INTRO, not an idle loop: every glitch animation
   below is forced to a single iteration with fill-mode forwards.
   Their keyframes already land on a clean resting state (the tear
   layers and both ghosts end at opacity:0, the box ends #fff), so
   one pass leaves zero artifacts. This does NOT depend on JS — if
   nd10-ambient.js never runs, the glitch still stops and the iris
   still opens, because both are pure CSS. ── */
:root {
  --intro-mark-in:    .30s;
  --intro-glitch-dur: 1.6s;
  --intro-glitch-a:   .30s;   /* top tear + ghosts + box */
  --intro-glitch-b:   .55s;   /* bottom tear, offset for texture */
  --intro-studio-out: 1.95s;   /* FINAL STAGE leaves */
  --intro-iris-at:    2.05s;   /* dither field opens from centre */
  --intro-iris-dur:   1.15s;
  --intro-mark-app:   3.00s;   /* INDIE10 lands on the revealed field */
  --intro-ui-at:      3.35s;   /* the interface follows the mark */
}

/* ── PHASE HAND-OFF — studio mark to product mark ──────────────
   Both marks are in the DOM from the start, stacked, and cross-faded on
   pure CSS timing. Nothing swaps text at runtime, so there is no moment
   where a failed script leaves the wrong brand on screen — the worst case
   is both marks resting at their final opacity, which is still INDIE10.

     0.30s  FINAL STAGE cuts in on black
     0.30s  glitch pass runs (single iteration, ends clean)
     1.95s  FINAL STAGE drops out — back to black
     2.05s  iris opens: the 8-bit dither field breathes into view
     3.00s  INDIE10 lands ON the revealed field
     3.35s  the interface follows

   Order matters and is deliberate: the studio mark leaves BEFORE the field
   arrives, and the product mark lands on top of a field that is already
   breathing. The reveal is the transition between the two brands, not
   decoration behind either of them.
   ── */
.fs-wordmark-wrap { position: relative; display: inline-block; }

/* ── THE INTRO IS GATED, AND IT DOES NOT REPLAY ─────────────────
   Two problems, one cause and one policy.

   THE FLASH. Every timing below is a CSS delay measured from FIRST PAINT.
   If a stylesheet or a webfont lands after that moment — and the Google
   Fonts link in <head> is a blocking external request that can — the first
   thing on screen is the UNANIMATED state: the ENTER button sitting there
   with no mark, and then the whole intro playing afterwards as though it
   were starting fresh. That is exactly the "enter, then FINAL STAGE
   reflashes, then Indie10" sequence. Nothing below runs until
   html.intro-go is set, which happens once, after fonts settle or after a
   short cap if they never do (offline, or the weights simply never being
   requested — see the canvas-font note in the wiki).

   THE POLICY. A launch intro is a thing you enjoy once and resent on the
   twentieth open of the day. html.intro-skip jumps straight to the field
   with the mark already placed — no glitch, no iris, no hand-off — and the
   boot script sets it on every launch after the first of the day. ── */
/* The hold lives INLINE in index.html — a gate against late CSS cannot
   itself be late CSS. Only the skip rules remain here. */
html.intro-skip #welcome .fs-phase-studio { display: none; }
html.intro-skip #welcome .fs-phase-app,
html.intro-skip #welcome .i10-actions,
html.intro-skip #welcome .fs-enter-btn,
html.intro-skip #welcome .fs-wordmark-wrap { opacity: 1 !important; animation: none !important; }
html.intro-skip .i10-wordmark { clip-path: none !important; animation: none !important; }


/* superseded by the reworked hand-off below — kept adjacent so the two are
   read together rather than one being found alone. */
/* ── THE HAND-OFF, REWORKED ────────────────────────────────────
   The old version cross-faded two marks that shared the same bordered box,
   so it read as "same sign, new word" — a flicker — and the product mark
   popped in on a scale transform, which read as a second flicker. Now:

     · the STUDIO mark leaves completely — opacity AND the frame, easing
       out with a slight lift so the rectangle is unmistakably gone
     · nothing occupies the centre while the iris opens
     · the PRODUCT wordmark then MATERIALISES in pixel rows, stepped, to
       match the 8-bit field it is landing on — no scale, no pop

   `steps()` on the mask is what makes it read as pixels falling into place
   rather than a dissolve. Opacity is kept smooth and separate, because a
   stepped opacity is exactly the flicker we are removing. ── */
html.intro-go .fs-phase-studio {
  animation: nd-mark-out .55s cubic-bezier(.4,0,.6,1) var(--intro-studio-out) both;
}

.fs-phase-app {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  opacity: 0;
}
html.intro-go .fs-phase-app {
  animation: i10-wm-fade .55s ease-out var(--intro-mark-app) forwards;
}

/* The Indie10 wordmark — its own identity, deliberately NOT the studio's
   framed treatment. Condensed, heavily tracked, with the accent carrying
   the numerals so the brand reads at a glance. */
/* THE LOOK IS UNGATED. Only the ANIMATION belongs behind intro-go — gating
   the whole block meant that on every launch after the first, when intro-skip
   is set, the wordmark lost its font, size, weight and tracking and fell back
   to whatever the body inherits. Two rules, one for what it IS and one for how
   it ARRIVES. */
.i10-wordmark {
  font-family: 'Barlow Condensed', 'Syne', sans-serif;
  font-size: clamp(46px, 7.2vw, 88px);
  font-weight: 900;
  letter-spacing: .14em;
  text-transform: uppercase;
  line-height: 1;
  color: #fff;
  white-space: nowrap;
  padding-left: .14em;              /* optical: balance the trailing track */
  /* THE RESTING STATE IS OPEN, and that is the safety property the mask
     version did not have. Its failure mode was "permanently half-hidden";
     this one's is "fully visible", because both the base here and the
     animation's own `to` (inset(0)) mean nothing is clipped. Whether the fill
     holds or not, the mark ends up lit — which is the only outcome worth
     guaranteeing on a splash screen. */
  clip-path: none;
}
/* THE REVEAL IS A CLIP, NOT A MASK — and that is a bug fix, not a preference.
   The mask version could not finish. With mask-size 100% 260% the element only
   ever saw the top 38.5% of a gradient that does not turn solid until 46%, so
   the mark stayed permanently half-masked: the top of the numerals faded, and
   the glow on them clipped unevenly against that fade. That is exactly the
   reported "the glow on the ten isn't masked right" — not a glow problem, a
   mask that never completed.
   Worse, animating mask-position did not take at all: pinning the animation's
   currentTime to its end still computed the FROM value, so no end state was
   reachable by tuning it.
   clip-path has neither failure. inset(0) is unambiguously "nothing clipped",
   the steps() keep the stepped pixel-row character, and once it lands the glow
   is drawn in full because there is no longer anything clipping it. */
html.intro-go .i10-wordmark {
  animation: i10-pixel-fall 1.15s steps(13, end) var(--intro-mark-app) both;
}
.i10-wm-num {
  color: var(--blue);
  text-shadow: 0 0 22px rgba(var(--accent-rgb), .45);
}

@keyframes nd-mark-out {
  to { opacity: 0; transform: translateY(-6px); filter: blur(1px); }
}
/* the wordmark's own opacity — smooth, so nothing strobes */
@keyframes i10-wm-fade { from { opacity: 0; } to { opacity: 1; } }
/* the mask sweeping down in hard steps — this is the "pixels falling" */
@keyframes i10-pixel-fall {
  from { clip-path: inset(100% 0 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

@media (prefers-reduced-motion: reduce) {
  .fs-phase-studio { animation: none; opacity: 0; }
  .fs-phase-app    { animation: none; opacity: 1; }
}

/* ── SPLASH ACTIONS ────────────────────────────────────────────
   These three buttons used to carry ~10 hardcoded declarations each in
   inline style="" attributes, which beat every stylesheet and therefore
   could never be themed or overridden — not by a theme, not by the user's
   Custom CSS. They are classes now, built from tokens, which is what makes
   them customisable at all. Every value below is a var(), so re-skinning
   the splash is editing the token file, not this rule. ── */
.i10-actions {
  display: flex; gap: 8px; margin-top: 14px; justify-content: center; flex-wrap: wrap;
}
.i10-action {
  height: 32px; padding: 0 16px; border-radius: 7px;
  background: linear-gradient(180deg, var(--rule-08), var(--rule-04));
  border: 1px solid var(--rule-12);
  color: var(--text-soft);
  font: 700 10px var(--mono); letter-spacing: .8px; text-transform: uppercase;
  cursor: pointer; position: relative;
  transition: color .16s ease, border-color .16s ease, background .16s ease, transform .16s ease;
}
.i10-action:hover {
  color: var(--text-max);
  border-color: rgba(var(--accent-rgb), .55);
  background: linear-gradient(180deg, rgba(var(--accent-rgb), .16), rgba(var(--accent-rgb), .05));
  transform: translateY(-1px);
}
.i10-action:active { transform: translateY(0); }
.i10-action:focus-visible { outline: 2px solid rgba(var(--accent-rgb), .7); outline-offset: 2px; }

/* The primary action carries the accent at rest, so there is exactly one
   obvious way in and the other two read as alternates. */
.i10-action--primary {
  color: var(--blue);
  border-color: rgba(var(--accent-rgb), .40);
  background: linear-gradient(180deg, rgba(var(--accent-rgb), .14), rgba(var(--accent-rgb), .04));
}
.i10-action--primary:hover {
  color: var(--text-max);
  border-color: rgba(var(--accent-rgb), .75);
  background: linear-gradient(180deg, rgba(var(--accent-rgb), .26), rgba(var(--accent-rgb), .08));
}

/* The splash actions arrive after the product mark, not before it. */
html.intro-go .i10-actions,
html.intro-go .fs-enter-btn,
html.intro-go .fs-enter-sub,
html.intro-go #welcome-drop-hint,
html.intro-go #ds-home-mount {
  animation: nd-ui-in .55s ease-out var(--intro-ui-at) both;
}
@keyframes nd-ui-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ── CONTROLS MUST FAIL VISIBLE ────────────────────────────────
   MEASURED BUG, not a precaution: the rule above uses fill-mode `both`, so
   BEFORE its 3.35s delay elapses the element holds the from-keyframe —
   opacity 0. CSS animations do not advance while a document is hidden, and
   main.js opens the window with show:false until 'ready-to-show'. A launch
   that starts backgrounded or on another Space therefore reached 5s with
   .i10-actions still at opacity 0 and a 0px box: ENTER, New project, Open
   files and Open folder simply never appeared. Confirmed by reading the
   live DOM, not by reasoning about it.

   Decoration may fail hidden. CONTROLS MAY NOT. nd10-ambient.js stamps
   .nd-intro-complete once the intro's running time has elapsed — and again
   on visibilitychange for a window that booted hidden — which forces the
   whole splash to its finished state regardless of whether a single frame
   of the intro ever played. ── */
.nd-intro-complete .i10-actions,
.nd-intro-complete .fs-enter-btn,
.nd-intro-complete .fs-enter-sub,
.nd-intro-complete #welcome-drop-hint,
.nd-intro-complete #ds-home-mount,
.nd-intro-complete .fs-phase-app {
  animation: none !important;
  opacity: 1 !important;
  transform: none !important;
}
.nd-intro-complete .fs-phase-studio { animation: none !important; opacity: 0 !important; }
.nd-intro-complete > .nd-amb        { animation: none !important; clip-path: none !important; }

.wlc-logo-wrap { animation-delay: var(--intro-mark-in); }

/* ── No pre-animation flash. MEASURED, not theoretical: a frame-rate sample
   of the real boot showed .fs-ghost-r sitting at opacity 1 for the first
   ~286ms — the window paints before the animation engine applies the first
   keyframe, so the raw red/cyan ghost text and both tear layers flashed on
   every launch. Their keyframes all start at opacity:0, but a keyframe only
   governs once the animation is RUNNING; until then the element shows its
   own base value, and none of these declared one. Declaring it here means
   the resting state matches the first frame, so there is nothing to flash. */
.fs-ghost-r,
.fs-ghost-c,
.fs-wordmark::before,
.fs-wordmark::after { opacity: 0; }

.fs-wordmark::before,
.fs-wordmark::after,
.fs-ghost-r,
.fs-ghost-c,
.fs-box {
  animation-duration: var(--intro-glitch-dur);
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
}
.fs-wordmark::before,
.fs-ghost-r,
.fs-ghost-c,
.fs-box            { animation-delay: var(--intro-glitch-a); }
.fs-wordmark::after { animation-delay: var(--intro-glitch-b); }

/* ── The reveal: a centred circular iris opening onto the dither
   field. Scoped to the WELCOME mount only (`#welcome >`) — the
   topbar's ambient canvas is persistent chrome and must never
   animate. circle(100%) is measured to the box's farthest corner,
   so it always clears the viewport regardless of window size. ── */
html.intro-go #welcome > .nd-amb {
  clip-path: circle(0% at 50% 50%);
  animation: nd-iris var(--intro-iris-dur) cubic-bezier(.22,.61,.36,1)
             var(--intro-iris-at) forwards;
}
@keyframes nd-iris {
  from { clip-path: circle(0%   at 50% 50%); }
  to   { clip-path: circle(100% at 50% 50%); }
}

/* Reduced motion: no glitch, no iris — land on the finished frame
   immediately. The field is still revealed, just not performed. */
@media (prefers-reduced-motion: reduce) {
  .fs-wordmark::before, .fs-wordmark::after,
  .fs-ghost-r, .fs-ghost-c, .fs-box { animation: none !important; }
  .fs-ghost-r, .fs-ghost-c { opacity: 0 !important; }
  #welcome > .nd-amb { clip-path: none; animation: none; }
}

/* ── One-time boot resolve ────────────────────────────────────
   The FINAL STAGE wordmark's glitch (.fs-wordmark::before/::after,
   .fs-ghost-r/.fs-ghost-c, .fs-box — all defined earlier in this file,
   3s infinite loops) was designed as an ambient idle effect and never
   stops. nd10-ambient.js adds `.nd-intro-done` to #welcome once, after
   the FIRST glitch burst completes (3.6s — matches the existing
   animation-delay:.6s + one 3s cycle exactly, so it freezes right as a
   burst finishes instead of cutting it off mid-glitch). This class does
   not touch the base keyframes at all — it only freezes them and fades
   the channel-split layers out, landing on the plain white wordmark the
   app already had. No brand/mark swap here — that's a separate,
   still-undecided decision. */
.nd-intro-done .fs-wordmark::before,
.nd-intro-done .fs-wordmark::after,
.nd-intro-done .fs-ghost-r,
.nd-intro-done .fs-ghost-c {
  animation: none !important;
  opacity: 0;
  transition: opacity .5s ease-out;
}
.nd-intro-done .fs-box { animation: none !important; }

/* the resolve sweep — a single accent-colored line, added as a real DOM
   node by nd10-ambient.js (never CSS-generated content on an existing
   element), so it can never collide with the wordmark's own pseudo-elements */
.nd-resolve-sweep {
  position: absolute; left: 0; right: 0; top: 50%; height: 2px;
  background: var(--blue, #7cf5ff);
  box-shadow: 0 0 14px var(--blue, #7cf5ff);
  opacity: 0; pointer-events: none; z-index: 2;
}
.nd-resolve-sweep.run {
  animation: nd-sweep .5s ease-out both;
}
@keyframes nd-sweep {
  0%   { opacity: 1; transform: translateY(-46px); }
  100% { opacity: 0; transform: translateY(46px); }
}
@media (prefers-reduced-motion: reduce) {
  .nd-resolve-sweep { display: none; }
}
