/* ================= Working / processing states =================

   One visual language for "the system is working", at two scales:
     .proc          inline, inside an existing status line
     .session-boot  full page, while the session is still unknown

   Its own file rather than part of components.css, which only classic_tasks.html and
   classic_team_board.html load: every app page has a session to check, and the alternative was
   shipping all of components.css to admin and mobile or giving them a second, slightly
   different set of dots.

   The inline indicator renders INSIDE the app's existing status elements (.cfg-status,
   .conn-status, .attach-status), so a working state sits where that area's success and
   error messages already appear. data-kind changes only the accent hue and the default
   wording; the motion is identical everywhere.

   No spinning wheel: a rotating disc reads as "loading, possibly forever" and, at this
   size, as a glitch. Three dots cresting in sequence read as active progress and stay
   legible at 12px next to text. */

.proc{
  display:inline-flex; align-items:center; gap:7px;
  font-size:12px; color:var(--text-dim);
  /* Non-zero min-height so showing and clearing the indicator cannot reflow the panel
     it sits in — a status area that changes height on every AI call makes the whole
     pane twitch. */
  min-height:1em;
}
.proc-dots{ display:inline-flex; align-items:center; gap:3px; flex:0 0 auto; }
.proc-dots i{
  width:5px; height:5px; border-radius:50%;
  background:var(--proc-accent, var(--accent));
  opacity:.35;
  animation:proc-pulse var(--dur-dots) ease-in-out infinite;
}
/* Staggered, not synchronised: three dots pulsing together is a throb, three in
   sequence is motion travelling left to right, which reads as progress. */
.proc-dots i:nth-child(2){ animation-delay:var(--dur-dots-step); }
/* Twice the step, expressed as a calc rather than a second number to keep in step. */
.proc-dots i:nth-child(3){ animation-delay:calc(var(--dur-dots-step) * 2); }

@keyframes proc-pulse{
  0%, 80%, 100%{ opacity:.28; transform:scale(.82); }
  40%          { opacity:1;   transform:scale(1);   }
}

.proc-msg{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* Kind = hue only. Every kind uses tokens that already exist and already flip with the
   theme, so none of this needs a light-mode rule of its own. */
.proc[data-kind="ai"]      { --proc-accent:var(--purple); }
.proc[data-kind="analyze"] { --proc-accent:var(--purple); }
.proc[data-kind="paste"]   { --proc-accent:var(--accent); }
.proc[data-kind="format"]  { --proc-accent:var(--accent); }
.proc[data-kind="save"]    { --proc-accent:var(--green); }
.proc[data-kind="load"]    { --proc-accent:var(--accent); }
.proc[data-kind="search"]  { --proc-accent:var(--accent); }

/* Under reduced motion the dots stop and settle at full opacity rather than disappearing:
   a user who asked for less animation still needs to see that something is in progress. */
@media (prefers-reduced-motion: reduce){
  .proc-dots i{ animation:none; opacity:.85; transform:none; }
}

/* ---------- THE BLOCK LOADER ----------

   The same statement as `.proc` above, filling a REGION rather than a status line: a panel
   that is fetching the thing it exists to show. It reuses `.proc-dots` exactly — one motion
   in this application means "working", and a spinner beside it would make two, which is the
   drift the whole motion section exists to prevent. Only the layout is new.

   IT FILLS ITS OWN CONTENT AREA AND NOTHING MORE. No overlay, no scrim, no blocking: the
   rest of the application stays usable while one panel waits.

   min-height reserves the space the answer will take, so the panel does not jump when the
   loader is replaced by a list. Everything is a token — there is no colour here that is not
   already the theme's, so it follows light and dark with no rule of its own. */
.loading-block{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:11px; padding:34px 20px; min-height:160px; text-align:center;
}
/* Larger than the inline dots, because this one is being looked AT rather than glanced at
   beside a field. Same animation, same stagger, same reduced-motion answer. */
.loading-block .proc-dots{ gap:5px; }
.loading-block .proc-dots i{ width:7px; height:7px; }
/* SUBORDINATE TO NOTHING HERE — it is the only text in the region — but still quiet: it is
   an interim state, not a headline. Wrapped at a comfortable measure so a longer line breaks
   in the middle of the panel rather than stretching to its edges. */
.loading-block-msg{
  max-width:min(36ch, 84vw);
  font-size:12.5px; line-height:1.5; color:var(--text-dim);
}
/* The stable announcement, for a screen reader only, when the visible line is rotating
   decoration. Both are present; only one is read, and only one is seen.

   DECLARED HERE AS WELL AS IN CSS/components.css, and that is the documented exception:
   mobile.html loads this stylesheet and not that one, so a shared rule cannot reach it. The
   two are identical on purpose and components.css names this class in its own selector list
   so the pair is findable from either end. */
.sr-only-loading{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* ---------- Session boot ----------

   The state the app is in before it knows who you are. Every page used to render its
   sign-in gate as the default visible state, which showed the login screen to somebody
   already signed in for as long as the session check took.

   This overlay is the default instead, and it is visible with NO JavaScript, so it cannot
   lose a race — it paints because the markup says so, not because something ran in time.

   The background matches the auth gate, so whichever way the check resolves the eye sees
   one continuous surface. Hardcoded rather than tokenised for the same reason the gate
   hardcodes it: this must be right before any stylesheet variable resolves.

   The light rule matches `:root.theme-light` as well as `body.theme-light`, and
   the FIRST is what actually fires here: the pre-paint script stamps the class on
   <html> only, before <body> is parsed, and applyTheme() does not add it to <body>
   until after the session check the curtain is covering. Scoped to <body> alone,
   light mode would paint its text ramp on the dark navy ground for the whole check
   — CSS/tokens.css:291 and CSS/components.css:1443 already name both selectors. */
.session-boot{
  position:fixed; inset:0; z-index:var(--z-boot);
  display:flex; align-items:center; justify-content:center;
  background:#0a1628;
  /* THE BRAND'S OWN RUN OF COLOUR — blue at one corner, green at the other, the same
     relationship the mark carries. It was a gold/blue pair, which belonged to the palette this
     product had before it had a name of its own. */
  background-image:
    radial-gradient(ellipse 900px 600px at 82% 12%, rgba(46,141,247,.20), transparent 60%),
    radial-gradient(ellipse 1100px 700px at 12% 82%, rgba(16,185,129,.16), transparent 60%);
}
:root.theme-light .session-boot, body.theme-light .session-boot{
  background:#e7ebf2;
  background-image:
    radial-gradient(ellipse 900px 600px at 82% 12%, rgba(46,141,247,.14), transparent 60%),
    radial-gradient(ellipse 1100px 700px at 12% 82%, rgba(16,185,129,.12), transparent 60%);
}

/* ---------- AND ON A TEAM'S OWN COLOUR ----------

   Without this, a team with a custom palette watches the app open on stock navy
   and repaint into their colour once the shell mounts — the flash of the wrong
   theme, on the surface that exists to prevent it.

   Costs nothing before first paint: the pre-paint scripts already stamp
   `data-theme-custom="1"` and the derived tokens above the first stylesheet for
   other reasons, so this is one declaration reading an already-resolved variable.

   `--bg`, not `--panel-2`, because it is both the ground the page is about to have
   and the safer surface — every contrast ratio the palette guarantees is met with
   margin here (measured in _docs/tests/alt-reveal-and-boot-curtain.test.js).

   `background-color`, never the `background` shorthand, so the two atmospheric
   washes above survive; they stay the brand's gold and blue on purpose
   (JS/core/theme-custom.js says why), not derived from the custom palette.

   The attribute is written only when all 32 tokens survived validation, so a
   corrupt or partial mirror leaves this selector unmatched and the two grounds
   above paint unchanged. Source order: both selectors are (0,3,0), so this one
   must stay below them. */
:root[data-theme-custom="1"] .session-boot{ background-color:var(--bg); }

/* No card, no border, no logo. This should read as the app opening rather than a screen
   of its own — anything more substantial makes a 200ms check feel like a page. */
.session-boot .proc{ font-size:13px; }

/* The overlay stacks now: the status line, then the quip beneath it. Column rather than a
   wrapper element so the existing .proc markup is untouched — it is still centred, it simply
   has something below it. */
.session-boot{ flex-direction:column; gap:10px; }

/* SUBORDINATE, and deliberately so. Smaller, dimmer, and never wider than a comfortable
   line: the primary answer to "what is happening" is the line above, and a joke that
   competes with it stops being an Easter egg and becomes the interface. */
.proc-quip{
  max-width:min(34ch, 82vw);
  text-align:center;
  font-size:12px;
  line-height:1.45;
  color:var(--text-faint);
  opacity:0;
  /* min-height reserves the line before there is text in it, so the status above does not
     shift downward when the quip arrives a frame later. */
  min-height:1.45em;
}
.proc-quip.is-in{
  opacity:1;
  transition:opacity var(--dur-quip) ease-out;
}
/* Both selectors, for the reason recorded on `.session-boot` above: <html> is the element the
   pre-paint script can reach. The declaration is the same token either way — the light ramp is
   already redefined by CSS/tokens.css — so this rule is here to state that the quip follows the
   theme at boot, not to give it a second colour. */
:root.theme-light .proc-quip, body.theme-light .proc-quip{ color:var(--text-faint); }

@media (prefers-reduced-motion: reduce){
  /* The text still changes — that is content, not motion — but it arrives rather than
     fades. Nothing here animates. */
  .proc-quip.is-in{ transition:none; }
}

/* Hidden by the state machine, not by a timer. */
.session-boot[hidden]{ display:none; }


/* ================= THE MARK ON THE BOOT CURTAIN =====================================
   The curtain is the one moment a reader has nothing else to look at, so it is
   where the product introduces itself.

   Its size is fixed in the tag as well as here — the <img> carries width/height
   so the box is reserved before the file arrives, on the one screen whose job is
   to look calm. Decorative, aria-hidden on the wrapper: `.session-boot` is already
   a live region, and a screen reader reading the logo too is the doubled-name
   defect sys_branding.md warns about. */
.boot-brand{
  display:flex; flex-direction:column; align-items:center; gap:10px;
  margin-bottom:6px;
}
.boot-brand img{
  display:block; width:82px; height:82px;
  /* A soft brand-coloured halo rather than a box: the mark has transparent edges and sits on a
     near-black ground, and a drop-shadow in its own colour is what keeps it from looking pasted
     on. Two shadows because one wide blur alone reads as fog. */
  filter:drop-shadow(0 0 22px rgba(56,189,248,.40)) drop-shadow(0 0 58px rgba(16,185,129,.26));
}
.boot-brand b{
  font-family:var(--app-brand-font, 'Space Grotesk', system-ui, sans-serif);
  font-size:22px; font-weight:700; letter-spacing:.20em; text-indent:.20em;
  color:#f4f6fb;
}
:root.theme-light .boot-brand b, body.theme-light .boot-brand b{ color:#16233a; }
:root.theme-light .boot-brand img, body.theme-light .boot-brand img{
  filter:drop-shadow(0 2px 10px rgba(46,141,247,.22));
}

/* IT BREATHES, ONCE, SLOWLY — and only if the reader has not asked it not to. The curtain can
   be up for a second or for three; a mark that is completely still reads as a frozen page, and
   anything faster than this reads as a spinner competing with the dots underneath it. */
@keyframes bootBrandIn{
  from{ opacity:0; transform:translateY(6px) scale(.97); }
  to  { opacity:1; transform:none; }
}
/* The duration is a token, not a number — `--dur-surface`, the same class of
   motion as a panel opening, so the product's sense of pace changes in one place. */
.boot-brand{ animation:bootBrandIn var(--dur-surface) var(--ease-out, cubic-bezier(.22,.61,.36,1)) both; }
@media (prefers-reduced-motion:reduce){
  .boot-brand{ animation:none; }
  .boot-brand img{ filter:none; }
}
