/* The terminal. Third act of the hero: alley -> house -> prompt.
   Told no, you're welcome, now show me.

   Palette lifted from her own Dxpamine front end rather than invented,
   so the three sites share a hand: #00ff88 primary, #00ff00 hot,
   #9bffad soft, on near-black.

   ★ THE WHOLE THING IS A SKIN OVER REAL FORM CONTROLS. Every question
   is an actual <input> or <select> that happens to be transparent and
   sat after a prompt. That is not pedantry -- it is the difference
   between this converting and this being a clever thing people give up
   on. A real input gets the phone's own keyboard, autofill, and paste,
   and three of the fourteen questions are URLs that nobody is going to
   type by hand. It also keeps the keyboard navigation and screen-reader
   support the brief asks for, which a div pretending to be a terminal
   destroys completely. */

:root {
  --t-bg: #040605;
  --t-green: #00ff88;      /* primary */
  --t-hot: #00ff00;        /* emphasis, the cursor */
  --t-soft: #9bffad;       /* secondary text */
  --t-dim: #2f6b4d;        /* chrome, rules, inactive */
  --t-warn: #ffd166;
}

.term {
  position: relative;
  background: var(--t-bg);
  color: var(--t-soft);
  /* ⚠️ A SYSTEM MONO STACK, not a webfont. JetBrains Mono would be more
     faithful to Dxpamine but costs 30-50 KB against a brief that caps
     total JS at 150 KB -- and every platform already ships an excellent
     mono. If she has JetBrains or Fira installed it gets used anyway. */
  font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular,
               Menlo, Consolas, monospace;
  font-size: clamp(0.82rem, 1.05vw, 0.98rem);
  line-height: 1.65;
  padding: clamp(28px, 6vh, 72px) 0 clamp(40px, 8vh, 96px);
  overflow: hidden;
  /* ⚠️ Load-bearing for the smoke, not just layout. This element is the
     smoke canvas's positioning context, so without a floor the canvas
     is only as tall as the shell and the effect ends in a hard
     horizontal line partway down the viewport with black beneath it.
     The shader's vignette hides the left, right and top edges; only
     this hides the bottom one. */
  min-height: 100svh;
  /* ⚠️⚠️ AND IT HAS TO BE border-box, OR THE FLOOR ABOVE IS A LIE. This
     sheet is the only one of the five without the `* { box-sizing:
     border-box }` reset the other four open with, so `min-height: 100svh`
     was the CONTENT height and the 118px of padding on the line above
     was added on top of it. Measured: 962 tall against an 844 viewport at
     390x844, 1026 against 900 at 1440x900, and the same mismatch at every
     size in between -- a scroll that revealed nothing except the padding
     it was made of, and on iOS a rubber-band on a page that is supposed
     to sit still.
     ⚠️ Deliberately NOT the blanket reset. Adding `*` here would also pull
     `.term__out`'s 32px of vertical padding INSIDE its min-height and
     max-height, shrinking the visible scrollback by 32px at every
     viewport including the desktop one -- a real visual change made to
     fix a bug that lives on exactly one element. The reset belongs in
     this sheet on the day someone audits every box in it; it does not
     belong in a mobile fix. */
  box-sizing: border-box;
}

/* CRT scanlines. A static repeating gradient at low opacity -- no
   filter, no blend mode. On krystallisjr a blend mode cost 44% of the
   frame budget; this costs nothing because it never changes and the
   compositor holds one texture. */
.term::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 255, 136, 0.035) 0px,
    rgba(0, 255, 136, 0.035) 1px,
    transparent 1px,
    transparent 3px);
  z-index: 2;
}

/* ── The green smoke ──────────────────────────────────────────────────
   Sits under everything: the scanlines, the phosphor bloom and the shell
   all paint over it. Scaled up from a 360px-wide buffer by the
   compositor, which is why it is `width:100%` here and tiny in the
   backing store.

   ⚠️ `image-rendering` stays default (smooth). Upscaling a low-res
   noise field is only invisible because the GPU interpolates it; force
   pixelated and the whole trick falls apart into visible blocks. */
.term__smoke {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  z-index: 0;
}

/* a soft phosphor bloom from the centre, so the panel has depth */
.term::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 42%,
              rgba(0, 255, 136, 0.055) 0%,
              rgba(0, 255, 136, 0.012) 45%,
              transparent 72%);
  z-index: 1;
}

.term__shell {
  position: relative;
  z-index: 3;
  width: min(880px, 92vw);
  margin: 0 auto;
  border: 1px solid var(--t-dim);
  border-radius: 4px;
  background: rgba(0, 20, 10, 0.35);
  box-shadow: 0 0 0 1px rgba(0, 255, 136, 0.06),
              0 24px 70px rgba(0, 0, 0, 0.8);
}

/* the title bar, so it reads as a window rather than a text block */
.term__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 14px;
  border-bottom: 1px solid var(--t-dim);
  color: var(--t-dim);
  font-size: 0.78em;
  letter-spacing: 0.04em;
}

.term__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.55;
  flex: none;
}

.term__title { margin-left: 6px; }

/* the scrollback */
.term__out {
  padding: 16px clamp(14px, 2.4vw, 26px);
  /* ⚠️ svh, NOT vh, and the min is capped by the max. Two bugs in one
     pair of lines, both only visible on a phone:

     1. `vh` resolves against the LARGE viewport on iOS -- the one you get
        with the toolbars hidden -- so 62vh is taller than what is
        actually on screen for as long as the toolbar is showing. Both
        sister sites already carry this lesson in their own sheets;
        this one predates it.
     2. min-height could EXCEED max-height, and when it does, min wins
        silently. On a 390px-tall landscape phone the floor was 280 and
        the ceiling 242, so the scrollback was 38px taller than its own
        maximum and the shell pushed past the screen with it.

     Wrapping the floor in min(..., 62svh) makes the ceiling genuinely a
     ceiling. Checked against the sizes this was tuned at: 844 tall gives
     388 and 900 gives 414, which are the values 46vh produced before. */
  min-height: min(clamp(280px, 46svh, 460px), 62svh);
  max-height: 62svh;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--t-dim) transparent;
}

.term__out::-webkit-scrollbar { width: 6px; }
.term__out::-webkit-scrollbar-thumb { background: var(--t-dim); }
.term__out::-webkit-scrollbar-track { background: transparent; }

.term__line { white-space: pre-wrap; word-break: break-word; }
.term__line--sys { color: var(--t-dim); }
.term__line--say { color: var(--t-soft); }
.term__line--hd  { color: var(--t-green); text-shadow: 0 0 9px rgba(0,255,136,0.45); }
.term__line--warn { color: var(--t-warn); }
.term__line--echo { color: var(--t-green); }
.term__line--sp  { height: 0.7em; }

/* ★ An option is a BUTTON, and has to look like one. As plain green
   text in a monospace list it read as output, not as something to
   press -- and a visitor who does not realise these are clickable is
   left staring at a command line waiting for them to know a command.
   Boxed, full width, obvious hit area, obvious hover. */
.term__opt {
  display: grid;
  grid-template-columns: 2.2em minmax(7em, auto) 1fr 1.2em;
  align-items: baseline;
  gap: 0 0.9em;
  width: 100%;
  margin: 3px 0;
  padding: 9px 12px;
  border: 1px solid rgba(47, 107, 77, 0.55);
  border-radius: 3px;
  background: rgba(0, 255, 136, 0.03);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color 130ms ease, border-color 130ms ease;
}

.term__opt b:first-child {
  color: var(--t-dim);
  border: 1px solid rgba(47, 107, 77, 0.6);
  border-radius: 2px;
  text-align: center;
  font-weight: 400;
}

.term__opt b { color: var(--t-green); font-weight: 400; }
.term__opt i { color: var(--t-dim); font-style: normal; }
.term__opt u {
  text-decoration: none;
  color: var(--t-dim);
  justify-self: end;
  transition: transform 130ms ease, color 130ms ease;
}

/* one shared state for hover, keyboard arrow and focus, so the thing
   under the cursor and the thing the keyboard will take are never two
   different rows */
.term__opt:hover,
.term__opt:focus-visible,
.term__opt.is-on {
  background: rgba(0, 255, 136, 0.12);
  border-color: var(--t-green);
  outline: none;
}
.term__opt:hover u,
.term__opt:focus-visible u,
.term__opt.is-on u { color: var(--t-hot); transform: translateX(3px); }
.term__opt:hover b:first-child,
.term__opt.is-on b:first-child { color: var(--t-hot); border-color: var(--t-green); }

.term__opt:active { background: rgba(0, 255, 136, 0.2); }

@media (prefers-reduced-motion: reduce) {
  .term__opt, .term__opt u { transition: none; }
}

/* the live prompt row */
/* ★ Her note: "make it obvious we type here somehow. because for now
   everything is kinda monochrome which is good." So the affordance is
   not allowed to be a colour nothing else on the page uses -- it has to
   come from MOTION, SHAPE and a state change. Three things do it, in
   descending order of how much they carry: the caret blinks (below),
   this row visibly wakes up when it has focus, and a dim ghost sits
   where the first character will land. */
.term__row {
  display: flex;
  align-items: baseline;
  gap: 0.6ch;
  padding: 6px clamp(14px, 2.4vw, 26px) 16px;
  border-top: 1px solid rgba(47, 107, 77, 0.45);
  transition: border-top-color 160ms ease, background-color 160ms ease;
}

/* The live line should look live. Same green, more of it -- the rule
   above the row brightens and the strip lifts off the shell, so the one
   part of the page that takes typing is the one part that is not flat. */
.term__row:focus-within {
  border-top-color: var(--t-green);
  background: rgba(0, 255, 136, 0.045);
}

@media (prefers-reduced-motion: reduce) {
  .term__row { transition: none; }
}

.term__ps { color: var(--t-green); white-space: nowrap; }
.term__ps b { color: var(--t-hot); font-weight: 400; }

/* ★ The real control. Transparent, unstyled, inheriting the mono face,
   so it looks like the caret is simply sitting after the prompt. */
/* ⚠️ Sized to its CONTENT, not flexed to fill. The block cursor is the
   next element in the row, so if the input stretches the cursor ends up
   pinned to the right-hand edge instead of sitting after what you typed.
   Width is written in `ch` on every keystroke -- see term.js. */
.term__in {
  flex: 0 1 auto;
  width: 1ch;
  max-width: 100%;
  min-width: 0;
  background: none;
  border: 0;
  outline: none;
  padding: 0;
  color: var(--t-hot);
  font: inherit;
  caret-color: transparent;   /* the block cursor below replaces it */
}
.term__in::placeholder { color: var(--t-dim); }

/* belt and braces: strip the spinners even if a number type ever
   returns, and kill the datalist/autofill arrow some engines draw */
.term__in::-webkit-outer-spin-button,
.term__in::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.term__in[type='number'] { -moz-appearance: textfield; appearance: textfield; }
.term__in::-webkit-calendar-picker-indicator { display: none !important; }

/* block cursor, sat right after the typed text */
/* ⚠️⚠️ THE BLINK BELONGS TO THE FOCUSED STATE. This was inverted --
   it blinked while idle and went SOLID AND STATIC on focus -- which
   meant that in the state a visitor is actually in while filling the
   form (focused, box empty) there was no movement anywhere on the
   screen, and the caret read as a piece of punctuation after the
   prompt. Measured on the real page: opacity 1, animation none.
   Every real terminal does it the other way round, and for the reason
   that matters here: a blinking caret is the one signal every person
   alive already reads as "this is waiting for you to type."
   Unfocused is a hollow block -- the other half of the same
   convention, and it makes the two states genuinely different rather
   than identical-but-for-a-blink. */
.term__cur {
  width: 0.62em;
  height: 1.05em;
  background: none;
  box-shadow: inset 0 0 0 1.5px var(--t-hot);
  translate: 0 0.16em;
  flex: none;
}

.term__row:focus-within .term__cur {
  background: var(--t-hot);
  /* a little presence, still the same single colour -- a filled block
     at this size is only 9x16px and the shell around it is busy */
  box-shadow: 0 0 7px rgba(0, 255, 0, 0.5);
  animation: term-blink 1.05s step-end infinite;
}

@keyframes term-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

/* ★ The ghost, sat exactly where the first character will appear.
   ⚠️ NOT a placeholder -- read the note in js/term.js about why the
   input's own placeholder cannot be used here (the box is sized to its
   content, so an empty one is 1ch and clips the text to a single
   letter). This is a sibling span, so it is unconstrained by that, and
   it is removed from the flow entirely the moment there is anything to
   type over -- no width held, no reflow as you type. */
/* ★ Neon, her call: "make the 'type here' neon so people see it fast."
   It was --t-dim (#2f6b4d), which is the colour this sheet uses for
   chrome and inactive rules -- so the one label whose entire job is to
   be noticed was painted in the palette's "ignore me" tone.

   ⚠️ NEON IS BLOOM, NOT BRIGHTNESS. Raising the core alone just makes a
   slightly lighter green word. What reads as a lit tube is a saturated
   core plus halation around it, so this is three stacked shadows at
   widening radii and falling alpha. Same lesson as the lights on the
   hero: richer is not brighter, and core-down-bloom-up is the axis that
   actually changes what you see.

   No animation on purpose. The caret two millimetres to its left is
   already blinking, and two moving things side by side read as a broken
   page rather than as one clear invitation. */
.term__ghost {
  color: var(--t-green);
  text-shadow:
    0 0 4px  rgba(0, 255, 136, 0.90),
    0 0 12px rgba(0, 255, 136, 0.55),
    0 0 26px rgba(0, 255, 136, 0.30);
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
}
.term__row:not(.is-empty) .term__ghost { display: none; }

/* ★ The question you are answering right now, lit so it cannot be
   missed. Same bloom construction as the ghost after the caret: it is
   the same job, at the other end of the line.

   ⚠️ ONE at a time. `.is-live` moves to the newest question and the
   previous one falls back to ordinary body colour -- fourteen glowing
   lines stacked up the scrollback would mean nothing at all. */
.term__line--q {
  color: var(--t-soft);
  transition: color 240ms ease, text-shadow 240ms ease;
}
.term__line--q.is-live {
  color: var(--t-green);
  text-shadow:
    0 0 4px  rgba(0, 255, 136, 0.90),
    0 0 12px rgba(0, 255, 136, 0.55),
    0 0 26px rgba(0, 255, 136, 0.30);
}

@media (prefers-reduced-motion: reduce) {
  .term__line--q { transition: none; }
}

/* progress through a form, e.g. [4/14] */
.term__prog { color: var(--t-dim); }

/* the review block before sending */
.term__rev {
  border-left: 2px solid var(--t-dim);
  padding-left: 1em;
  margin: 0.3em 0 0.6em;
}
.term__rev span { color: var(--t-dim); }

@media (prefers-reduced-motion: reduce) {
  /* ⚠️ Must match the focus-within selector's specificity or it does not
     win -- the blink now lives on `.term__row:focus-within .term__cur`
     (0,2,0) and a bare `.term__cur { animation: none }` (0,1,0) would be
     silently overridden. The focused caret stays filled and glowing and
     the idle one stays hollow, so the two states are still told apart
     without any movement at all. */
  .term__row:focus-within .term__cur { animation: none; }
  .term__cur { animation: none; }
}

/* ── The hint line ────────────────────────────────────────────────────
   Her note: "i understand those. but the average wont." Every command
   the page expects is printed here, at the moment it applies, so
   nothing has to be carried in the visitor's head. A terminal is only
   hostile when it assumes you already know its vocabulary. */
.term__hint {
  padding: 0 clamp(14px, 2.4vw, 26px) 13px;
  color: var(--t-dim);
  font-size: 0.82em;
  letter-spacing: 0.02em;
  min-height: 1.2em;
}

/* ⚠️ This has to LOOK like a control, not like another system line.
   It was dim green text with no border and her call was immediate:
   "that does not look like a link. be a bit more flexible with this."
   She is right, and the fix is to stop being precious about the
   terminal idiom -- a visitor's ability to leave matters more than
   whether an outlined button is strictly shell-like. Underline, border,
   arrow, hover fill, real focus ring. */
.term__backwrap {
  width: min(880px, 92vw);
  margin: 20px auto 0;
}

.term__back {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 9px 16px;
  border: 1px solid var(--t-dim);
  border-radius: 3px;
  color: var(--t-soft);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--t-dim);
  font-size: 0.86em;
  transition: color 160ms ease, border-color 160ms ease, background-color 160ms ease;
}

.term__back span { transition: transform 160ms ease; }

.term__back:hover,
.term__back:focus-visible {
  color: var(--t-hot);
  border-color: var(--t-green);
  background: rgba(0, 255, 136, 0.08);
  text-decoration-color: var(--t-green);
  outline: none;
}
.term__back:hover span,
.term__back:focus-visible span { transform: translateX(-3px); }

.term__back:focus-visible {
  box-shadow: 0 0 0 2px rgba(0, 255, 136, 0.35);
}

@media (prefers-reduced-motion: reduce) {
  .term__back, .term__back span { transition: none; }
}

/* ── Hit area ───────────────────────────────────────────────────────────
   Hit-tested at 37px tall against the 44px floor a finger needs, and it
   is the way out of the form. A transparent ::after rather than
   min-height, because this one has a drawn border and a focus ring, so
   any height added to it is visible at every viewport -- including the
   desktop one, which is not what is being fixed here. */
.term__back { position: relative; }
.term__back::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 46px;
  translate: 0 -50%;
}

/* ── Narrow ─────────────────────────────────────────────────────────────
   ⚠️ THE OPTION ROWS WRAPPED THEIR OWN HINT INTO A RAGGED COLUMN. The row
   is a four-column grid -- number, name, hint, chevron -- and the hint
   lives in the `1fr`, which at 390px of screen is 83px wide. Measured
   there, "about five minutes" broke over two lines and "angels,
   operators, investors" over three, each one hanging off the right edge
   of the name beside it. A hint that is harder to read than the thing it
   is explaining is not a hint.

   On a phone it goes UNDER the name instead, which is the ordinary
   mobile answer and also the truthful one: the name is the choice and
   the hint is a note on it, so stacking them says what the side-by-side
   arrangement was only implying while there was room for it.

   ⚠️ `align-items: start`, not the inherited `baseline`. With two rows in
   the second column, baseline alignment lines the number badge up with
   the FIRST line and leaves it floating a long way above the row's
   centre. 560px rather than 720: at 600-700 the hint still fits on one
   line beside the name, and stacking it there would be solving a problem
   that does not exist yet. */
@media (max-width: 560px) {
  .term__opt {
    grid-template-columns: 2.2em 1fr 1.2em;
    align-items: start;
    row-gap: 0.1em;
  }
  .term__opt b:first-child { grid-area: 1 / 1; }
  .term__opt i { grid-area: 2 / 2; }
  .term__opt u { grid-area: 1 / 3 / span 2 / auto; align-self: center; }

  /* ⚠️⚠️ AND THE PROMPT TAKES ITS OWN LINE, BECAUSE IT WAS EATING THE ONE
     YOU TYPE INTO. `guest@daedalus:~/apply founder$` is 245px of a 329px
     line at 390, and the input is `flex: 0 1 auto; min-width: 0` -- sized
     to its content so the block cursor can sit after what you typed, and
     therefore the first thing in the row with any give. So it gave.
     Measured on question one: the box was 0px wide while empty, and 41px
     -- FOUR CHARACTERS -- at 360 once a name was in it, with the rest
     scrolled out of sight inside the box. Someone entering an email
     address on a phone could see the last four letters of it.
     This is the fourteen-question form the whole site funnels into, so it
     is not a cosmetic tier.
     ✓ The prompt wraps onto its own line and the typing line gets the full
     width: 303px at 360 instead of 41. Which is also what a real terminal
     does with a long PS1, so nothing about the idiom is given up -- and it
     is better than the alternative of shortening the prompt string, which
     would mean the page quietly saying something different about where you
     are depending on the width of the screen.
     ⚠️ The input stays content-sized. It is NOT flexed to fill here: the
     block cursor is the next element in the row and it has to sit directly
     after the last character, not out at the right-hand edge. */
  .term__row { flex-wrap: wrap; }
  .term__ps { flex: 1 0 100%; }
  /* ⚠️ And the typing line keeps room for the cursor. Once the row is
     allowed to wrap, an input at the full `max-width: 100%` -- which is
     what a long answer like a repository URL gives -- pushes the block
     cursor onto a THIRD line, on its own, with the text above it. The
     caret has to sit against the last character; that is the entire reason
     the input is content-sized rather than flexed. Reserving the cursor's
     own width plus its gap keeps the two together at any answer length. */
  .term__in { max-width: calc(100% - 1.5em); }
}

/* ── Short ──────────────────────────────────────────────────────────────
   ⚠️ THIS PAGE IS ALLOWED TO SCROLL AND THAT IS NOT THE BUG. Fourteen
   questions with a live scrollback cannot be a single screen on a 390px
   landscape phone, and with the keyboard open there is less again --
   js/term.js already has keepVisible() for exactly that. What IS worth
   fixing is how much of that 390px went to nothing: `.term`'s own padding
   is 68px here, 17% of the viewport, and it was sized for a window five
   times as tall. Taking it to 36 gives the shell three more lines of
   scrollback, which is the only thing on this page anyone is reading.
   Measured: 514 -> ~474 against 390, so it still scrolls -- the page is
   shorter, not pretending to fit. */
@media (max-height: 560px) {
  .term { padding: 16px 0 20px; }
  .term__backwrap { margin-top: 12px; }

  /* ⚠️⚠️ AND THEN THE SCROLLBACK TAKES WHATEVER IS LEFT, rather than a
     fraction of the screen. Trimming the padding above got 514 down to
     474 against 390 and stopped there, which left the hint line cut in
     half and the way out below the fold -- on arrival, before a single
     question had been answered. On every other viewport this page is
     exactly one screen and the LOG scrolls inside it; landscape was the
     one size where the page itself scrolled, and the two things it hid
     were the line that tells you what to type and the link that lets you
     leave.
     The cause is that `62svh` is a guess at what is left over. At 844x390
     it asks for 242px of a 390px screen and the bar, the prompt row, the
     hint and the back link need 157 of the remaining 148. So the shell
     becomes a flex column and the log gets the slack by subtraction --
     measured 197px here, and correct at any height without another tier.
     ⚠️ The chrome is `flex: none` on purpose. Everything in a flex column
     shrinks by default, so without this the title bar, the prompt row and
     the hint all give up pixels too, and the one element that is SUPPOSED
     to absorb the squeeze is the only one with somewhere to put it.
     ⚠️ The 9em floor is a real floor, not a magic number: about five lines
     of log. Below roughly 310px of viewport the page scrolls again rather
     than the log shrinking to nothing -- which is the right way round. */
  .term { display: flex; flex-direction: column; }
  .term__shell {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  .term__bar, .term__row, .term__hint { flex: none; }
  .term__out { flex: 1 1 0; min-height: 9em; max-height: none; }
  .term__backwrap { flex: none; }
}

/* ── Arriving and leaving ─────────────────────────────────────────────
   The room hands off to this page with a fade through black, and the
   page fades up out of it. Opacity on one fixed layer, nothing else --
   the cheapest possible transition and the only kind this project can
   afford, given a full-viewport blur measured at 40% of the frame
   budget on krystallisjr. */
body { opacity: 0; transition: opacity 420ms ease; }
body.is-in { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  body { opacity: 1; transition: none; }
}
