/* The shell: the palette, the frame, the header's outer columns and its
   controls, the typing rules, the activity log and the footer. Everything
   here is true of the page whatever screen is on it — a screen's own
   stylesheet loads after this one and paints what it puts in the middle. */

/* One palette, phosphor-green on black. Every colour below is a variable
   so the "matrix" of it is one edit, not thirty. */
:root {
  --bg: #000603;
  --fg: #00ff41;
  --fg-mid: rgba(0, 255, 65, 0.62);
  --fg-dim: rgba(0, 255, 65, 0.34);
  --line: rgba(0, 255, 65, 0.22);
  --panel: rgba(0, 255, 65, 0.035);
  --warn: #ffd21f;
  --alert: #ff5f4d;
  --glow: 0 0 6px rgba(0, 255, 65, 0.5);
  color-scheme: dark;
  /* Motion, with its durations named here because this is where the
     keyframes that use them are. motion.js reads these back rather than
     keeping a second copy of either number. The generic power-on's stages
     are the four delays under `main.pwr-on` below: the header first, then
     the screen, then the log, then the floor.

     The power-off is an afterglow: the page drops to `--glow-from` of itself
     on the first frame and fades from there over `--off-ms`, quick at first
     and with a long tail, the way a phosphor lets go. */
  --off-ms: 1800ms;
  --off-ease: cubic-bezier(0.15, 0.6, 0.35, 1);
  --glow-from: 0.28;
  --on-ms: 340ms;
  --on-ease: cubic-bezier(0, 0, 0.2, 1);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 13px / 1.5 ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  letter-spacing: 0.02em;
  text-shadow: var(--glow);
}
/* The curtain. The page waits the way a terminal waits: a cursor, then the
   name types itself, and only then is the rest shown — and that pre-reveal
   state is also the state a screen that is switched off is in, which is what
   makes powering on and off one transition run in two directions.
   `visibility`, never `display`: all of it is laid out and reserved from the
   first frame, so nothing moves when it appears and nothing moves when it
   goes. If the script never runs none of this applies and the page is simply
   visible, which is the honest failure.

   Everything the curtain covers carries `.curtain` — the shell's own floor
   and header columns from the markup, and the current screen's regions from
   the shell as it makes them. The controls are the exception on purpose: a
   page with no way to switch itself back on is a dead page. */
/* The descendants too, and loudly. `visibility` is inherited, so an ancestor
   set to hidden is normally the whole of it — but a descendant that sets its
   own `visibility: visible` wins for itself, and two things on this page set
   exactly that inline: every line the typing engine finishes (the ghost stops
   being a ghost) and the weekly verdict, which is shown or hidden by the bar
   arithmetic. Before there were two directions this never showed, because the
   curtain was only ever down before anything had been said. `!important`
   beats an inline declaration; the `*` is what carries it to the descendant
   that made one. */
main.titling .curtain, main.titling .curtain * { visibility: hidden !important; }
/* Power off: everything behind the curtain goes at once and leaves an
   afterglow — the regions as a faint copy of themselves. The title line
   stays lit; it names the screen that is off. Played while the screen
   behind it is still live; the stop is on the far end. */
@keyframes afterglow { from { opacity: var(--glow-from); } to { opacity: 0; } }
main.pwr-off .curtain {
  animation: afterglow var(--off-ms) var(--off-ease) both;
  filter: blur(0.35px);
}
/* Power on, for a screen with no arrival of its own. Four stages down the
   page, so it reads as the page coming up rather than as one fade. */
@keyframes pwr-on { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
main.pwr-on .curtain { animation: pwr-on var(--on-ms) var(--on-ease) both; }
main.pwr-on .screen.curtain { animation-delay: 80ms; }
main.pwr-on h2.curtain, main.pwr-on #log.curtain { animation-delay: 140ms; }
main.pwr-on footer.curtain { animation-delay: 180ms; }

/* One screen is shown; the rest keep their DOM and are not laid out at all.
   The flex chain from <main> down to a screen's own grid has to survive the
   extra step, or a card grid that sized itself to the viewport goes back to
   sizing itself to its content. */
.screen { display: flex; flex-direction: column; min-height: 0; }
.controls .sgroup { display: contents; }

/* Scanlines — faint enough to read through. */
#scan {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.22) 0 1px, transparent 1px 3px);
  mix-blend-mode: multiply;
}
main { position: relative; z-index: 2; max-width: 1180px; margin: 0 auto; padding: 22px 20px 40px; }
/* Fullscreen changes one thing: the width cap comes off. Sides only — the
   shorthand would reset the bottom padding too, at a higher specificity
   than the desktop rule that trims it, and give the floor back its 40px. */
.fs main { max-width: none; padding-left: 28px; padding-right: 28px; }

header { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline; justify-content: space-between; border-bottom: 1px solid var(--line); padding-bottom: 7px; }
h1 { font-size: 15px; font-weight: 600; margin: 0; letter-spacing: 0.22em; }
h1 .sep { color: var(--fg-dim); margin: 0 8px; }
/* Every column of the header reserves its full height before it has
   anything in it: the page arrives empty and fills, and without the
   reservations the header grows and takes the whole grid down with it.
   The two lines here are always present and blanked rather than removed.
   The min-heights are measured, not guessed — 13px and 11px against the
   page's 1.5 line-height. */
.hleft { display: flex; flex-direction: column; gap: 7px; }
.hnote { display: flex; flex-direction: column; gap: 4px; font-size: 11px; letter-spacing: 0.14em;
  min-height: 40px; }

/* The header's middle column. The shell reserves it and a screen fills it;
   what goes in it is that screen's stylesheet's business. `.mid` is the
   current screen's share of it — one per screen, one shown. */
.middle { display: flex; flex-direction: column; gap: 3px; align-items: stretch; flex: 1 1 auto; min-width: 0; }
.middle .mid { display: flex; flex-direction: column; gap: 3px; flex: 1 1 auto; min-width: 0; }

/* #verdict is an id with a display of its own, which outranks the UA's
   rule for the attribute — so being hidden has to say so louder. */
[hidden] { display: none !important; }
@keyframes blink { 50% { opacity: 0; } }

/* The right of the header: the status is read, the buttons are pressed.
   Top-aligned rather than baseline-aligned — this column's first line is
   11px against a 15px title, and on the baseline it sat low enough to read
   as a margin. Fixed width, because the status is the one thing in the
   header whose text changes on its own and the bars must not move when it
   does. The 14px gap is wider than the gap inside either row: at 6px they
   scanned as one block and the clock read as a label over the buttons. */
.hmeta { position: relative; display: flex; flex-direction: column; align-items: flex-end; gap: 14px; font-size: 11px;
  color: var(--fg-mid); letter-spacing: 0.14em; margin-left: auto;
  align-self: flex-start; min-width: 172px; }
.hmeta .live { display: flex; gap: 12px; align-items: baseline; }
.hmeta .controls { display: flex; gap: 8px; align-items: center; min-height: 24px; }
/* Held back until the title has finished typing, and then up for good — on
   every screen, and in the off state, since the off state is where the button
   that ends it lives. `visibility`, so the row keeps its width from the first
   frame and nothing in the header moves as they arrive; and driven by a class
   the script adds, so a page whose script never runs still has its buttons. */
.controls.holding button { visibility: hidden; }
#link.down { color: var(--alert); }
/* Old, not wrong: the last frame is still true of when it was made. */
#link.stale { color: var(--warn); }
button {
  font: inherit; font-size: 11px; letter-spacing: 0.14em;
  background: transparent; color: var(--fg-mid); border: 1px solid var(--line);
  padding: 3px 9px; cursor: pointer; text-shadow: inherit;
}
button:hover { color: var(--fg); border-color: var(--fg); }
/* Square and wordless: the label is the glyph, the state is whether it is
   lit, and the sentence explaining it is in the tooltip. A control pressed
   twice a day should not be the size of one you read. */
button.icon { padding: 4px 8px; letter-spacing: 0; font-size: 16px; line-height: 1; }
button[aria-pressed="true"] { color: var(--bg); background: var(--fg); text-shadow: none; }
#menu { position: absolute; top: calc(100% + 4px); right: 0; z-index: 5; background: var(--bg); border: 1px solid var(--fg); min-width: 220px; }
#menu button { display: block; width: 100%; text-align: left; border: 0; border-bottom: 1px solid var(--line); padding: 7px 10px; letter-spacing: 0.08em; }
#menu button:last-child { border-bottom: 0; }
#menu button:hover { background: rgba(0, 255, 65, 0.12); color: var(--fg); }
#menu button[aria-current="true"] { color: var(--fg); }
#menu button[aria-current="true"]::before { content: "▸ "; }

/* A line occupies its finished size before a character of it lands. The
   full text sits in the box as a hidden ghost, which gives the box its
   width, height and line breaks; what has landed is drawn over the top,
   out of flow. So nothing moves as a line types — not the line, not the
   row under it, not the card, not the grid. `width: 100%` on the overlay
   is what makes it wrap identically to the ghost underneath. */
.typing { position: relative; display: inline-block; max-width: 100%; vertical-align: top;
  white-space: inherit; }
.typing .ghost { visibility: hidden; }
.typing .shown { position: absolute; left: 0; top: 0; width: 100%; }
/* On a row that does not wrap the ghost must not be capped either, or a
   row that was one line while typing becomes two the moment it is not. */
.oneline .typing { max-width: none; }
/* One line, however long, scrollable by hand. Wrapping is what made a
   card's height depend on its text — two lines here, one there, and every
   row below moving as the feed changed. The scrollbar is hidden: one under
   every row is more chrome than the rows are worth. */
.oneline { display: block; white-space: nowrap; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
.oneline::-webkit-scrollbar { display: none; }
/* The cursor a line carries while typing, and the one the title keeps for
   good. It rides inside the overlay, so it costs the layout nothing. */
.cur { animation: blink 1.1s step-end infinite; }
/* Everything still. One switch for the lot, because the thing being turned
   off is "this page moves", not any one of its movements. `!important`
   because much of it is inline or in a keyframe. */
html.noanim *, html.noanim *::before, html.noanim *::after {
  animation: none !important; transition: none !important;
}

/* Empty until it types itself, so it reserves the line it will occupy. */
h2 { font-size: 11px; letter-spacing: 0.22em; color: var(--fg-mid); margin: 0 0 8px; font-weight: 500;
  min-height: 16.5px; }
/* Height counted in rows, not pixels, so it survives a font-size change:
   an entry is 11.5px at 1.5 line-height plus 3px of padding either side
   and its separator. Three rows exactly — the log is the corner of the
   eye, and the room belongs to the cards. Older entries scroll. */
#log { --log-row: 24.25px; list-style: none; margin: 0; padding: 0; font-size: 11.5px;
  height: calc(3 * var(--log-row)); overflow-y: auto; border: 1px solid var(--line); }
#log li { display: flex; gap: 12px; padding: 3px 10px; border-bottom: 1px solid rgba(0, 255, 65, 0.08); }
#log li:first-child { background: rgba(0, 255, 65, 0.06); }
#log time { color: var(--fg-dim); }
#log .who { color: var(--fg-mid); min-width: 56px; }
#log .attn { color: var(--warn); }
/* The floor is pinned to the bottom of the window, so it reserves the line
   it will occupy or the log and the heading ride up when it fills. */
footer { margin-top: 18px; color: var(--fg-dim); font-size: 10.5px; letter-spacing: 0.08em;
  display: flex; gap: 18px; align-items: baseline; justify-content: space-between;
  min-height: 15.75px; }
footer #paths { word-break: break-all; }
footer .mark { flex: none; letter-spacing: 0.16em; }

/* Thin and in the palette, on every box that can scroll — the default
   chrome bar is a grey slab across a phosphor card. */
#log { scrollbar-width: thin; scrollbar-color: var(--fg-dim) transparent; }

/* A dashboard is looked at, not read down: on a desktop the page itself
   never scrolls and the regions that can outgrow their share scroll inside
   their own boxes. Below this width a single column of cards can only be
   read by scrolling, so the page goes back to being a page. dvh, not vh,
   because mobile Safari's vh is the address bar's problem. */
@media (min-width: 760px) {
  html, body { overflow: hidden; }
  main { height: 100vh; height: 100dvh; display: flex; flex-direction: column; padding-bottom: 14px; }
  header { flex: none; }
  .screen { flex: 1 1 auto; }
  h2, #log, footer { flex: none; }
  footer { margin-top: 12px; }
}

/* Drawn at 125% on a desktop, because that is the size it was tuned at —
   by eye, at a browser zoom of 125% nobody had noticed was on. The zoom
   scales `100dvh` along with everything else, which would push the log a
   quarter of a window below the fold, so `main` asks for 80% of the height
   and gets 100%. From 1200px only: the zoomed layout is then never narrower
   than 960px, clear of the 760px breakpoint above, and a phone is untouched.
   A browser's own zoom multiplies with this one — so a tab already at 125%
   wants resetting to 100%. */
@media (min-width: 1200px) {
  html { zoom: 1.25; }
  main { height: 80vh; height: 80dvh; }
}

/* Behind a password and without it: the access screen alone, and no log,
   since nothing has happened that a visitor is entitled to read about. The
   footer stays — its mark is the shell's and says nothing about anyone. */
main.locked #activity, main.locked #log { display: none; }

/* A screen with nothing behind it yet. Centred on the space the real one
   will fill, so the shape of the page is right before its contents are. */
.nosignal { font-size: 13px; letter-spacing: 0.22em; color: var(--warn); min-height: 19.5px; }
.blank { flex: 1 1 auto; display: flex; align-items: center; justify-content: center;
  color: var(--fg-dim); font-size: 11px; letter-spacing: 0.16em; }
.blank p { margin: 0; }

@media (prefers-reduced-motion: reduce) {
  .cur { animation: none; }
  /* motion.js checks the same preference and skips the wait, so nothing is
     left holding a promise that a suppressed animation will never settle. */
  main.pwr-off .curtain, main.pwr-on .curtain { animation: none; }
}
