/* The front door, and the only page on this site that looks like this.
 *
 * It is loaded by app/views/layouts/landing.html.erb and by nothing else, so
 * none of it is parsed on the yoga or software pages. It defines no colours of
 * its own: the palette comes from application.css, which is where the site
 * keeps its names for things.
 *
 * One local exception to the site's "mono for chrome, sans for prose" rule.
 * Everything here is monospace, the creed line included. That rule exists so
 * paragraphs a stranger reads are comfortable; this page is not paragraphs,
 * it is a terminal, and a system sans in the middle of it would read as a
 * mistake rather than as a kindness.
 *
 * There were three treatments here for a day -- a plain one with no texture
 * and a still one with no motion -- so they could be compared in a browser
 * rather than argued about. Chad picked this one: glow, scanlines, curve, a
 * blinking caret, and lines that type themselves. The other two are gone, and
 * so is the /preview/:skin route that served them. */

/* --- the screen ---------------------------------------------------------- */

body.crt {
  font-family: var(--mono);
  color: var(--green);
}

/* application.css caps `main` at the reading measure, which is right for prose
   and wrong for a screen meant to fill the window. */
main.screen {
  max-width: none;
  padding: 2rem 1.25rem 3rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.terminal {
  width: 100%;
  max-width: 44rem;
}

/* --- the two overlays ---------------------------------------------------- */

/* pointer-events: none on both, and it is load-bearing rather than tidy.
   These are fixed and cover the whole viewport, which is exactly the shape of
   the bug CLAUDE.md records congo shipping -- every route answering 200 with
   correct markup behind a sheet of glass nothing could click. A system test
   asserts elementFromPoint over the congo link returns the link. */
.scan,
.glass {
  position: fixed;
  inset: 0;
  pointer-events: none;
}

.scan {
  z-index: 2;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0) 0 2px,
    rgba(0, 0, 0, 0.30) 2px 4px
  );
}

/* Not real barrel distortion -- that wants an svg filter, and the cost is not
   worth it. A generous corner radius and a dark rim read as a tube at the size
   anybody actually looks at this. */
.glass {
  z-index: 3;
  border-radius: 14px;
  background: radial-gradient(
    ellipse at center,
    rgba(0, 0, 0, 0) 52%,
    rgba(0, 0, 0, 0.55) 100%
  );
  box-shadow: inset 0 0 8rem rgba(0, 0, 0, 0.7);
}

/* --- the boot lines ------------------------------------------------------ */

.boot {
  list-style: none;
  margin: 0 0 2.25rem;
  padding: 0;
  /* Not var(--dim): at 0.8rem these are small text, and --dim against --bg
     measures 4.19:1, under the 4.5 small text wants. --quiet is the same
     grey-blue at 6.51:1 -- readable on a monitor that is not the one this was
     designed on, and still well under the creed's 12.06. */
  color: var(--quiet);
  font-size: 0.8rem;
  line-height: 1.75;
}

.boot li {
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  width: calc(var(--ch) * 1ch);
}

/* --- rotating words ------------------------------------------------------ */

/* A stack of words in a box exactly one line tall, wound past by a stepped
   translate. One technique serves the two mount paths and the creed.

   `steps(var(--n))` is why the word count never appears in this file: --n
   rides on the element, so a list can grow or shrink without a keyframe
   having to be recounted to match. The same trick already drives the typing
   animation above, which steps by var(--ch).

   Translating the stack by -100% moves it its own full height, which is --n
   lines. steps(--n, end) holds at 0, then -1, then -2 lines and so on up to
   -(n-1), so every word gets an equal turn and the wrap is invisible. There
   is no need to repeat the first word at the end.

   The resting state is the first word: with the animation off the stack sits
   at translateY(0). So reduced motion, and any browser that will not run
   this, both land on a complete page rather than an empty box -- the same
   rule the typing and the margin column follow. */

.reel,
.creed-reel {
  overflow: hidden;
}

.reel {
  display: inline-block;
  vertical-align: top;
  height: 1.75em;               /* .boot's line-height, so one word shows */
}

.reel .stack > span {
  display: block;
  height: 1.75em;
  line-height: 1.75em;
}

.creed-reel {
  display: block;
  height: 1.7em;                /* .creed's line-height */
}

.creed-reel .stack > span {
  display: block;
  height: 1.7em;
  line-height: 1.7em;
}

/* display: block, and it is not cosmetic. `.stack` is a span, and transform
   has no effect on a non-replaced inline box -- the animation runs, reports
   its name to getComputedStyle, and moves nothing at all. Found by checking
   which word was on screen rather than whether the animation existed, which
   is the only way that failure shows up. */
.reel .stack,
.creed-reel .stack {
  display: block;
}

@keyframes roll {
  to { transform: translateY(-100%); }
}

/* The mount paths start turning once the boot lines have finished typing,
   which is at about 1.2s; the creed waits a moment longer again so the two
   halves of the page do not move at the same instant. */
.reel .stack {
  animation: roll var(--dur) steps(var(--n), end) infinite;
  animation-delay: 2s;
}

.creed-reel .stack {
  animation: roll var(--dur) steps(var(--n), end) infinite;
  animation-delay: 2.6s;
}

/* The full sentence is the one that ships to a screen reader, at every width.
   It is moved off screen rather than hidden with display:none, which would
   take it out of the accessibility tree along with everything it says -- the
   same technique as the sign-up honeypot in application.css, for the opposite
   reason.

   **It used to be what a phone showed**, on the grounds that the line wraps
   down there and a box one line tall would cut it in half. The premise was
   wrong: the reel carries "a place for... shamans" and not the whole
   sentence, which is twenty-odd monospace characters and fits a 320px screen
   with room over. What it needed was `nowrap` so it cannot wrap however long
   a noun somebody adds -- a word too long is then clipped at the edge, which
   is visible, rather than silently eating the line below. */
.creed-reel,
.creed-tail { display: block; }

.creed-reel .stack > span { white-space: nowrap; }

.creed-full,
.offscreen {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- the wordmark -------------------------------------------------------- */

/* The mark beside it, on the screens where the drifting column is not drawn.
   The wordmark wraps to two lines on a phone and leaves a square of empty
   space to its right, which is exactly the shape of a mark. */
.beside { display: none; }

@media (max-width: 64rem) {
  .wordmark {
    display: flex;
    align-items: center;
    gap: 1rem;
  }

  /* The h1 carries the margin for the pair; on the row it would push the
     mark down by its own bottom margin. */
  .wordmark h1 { margin-bottom: 0; }
  .wordmark { margin: 0 0 1.5rem; }

  .beside {
    display: block;
    flex: 0 0 auto;
    width: clamp(96px, 30vw, 128px);
    height: auto;
    image-rendering: pixelated;
    /* The same dimming the column uses, so it reads as one of them rather
       than as a logo somebody put next to a title. */
    opacity: 0.8;
  }
}

/* clamp() rather than a media query: the wordmark is the widest thing on the
   page and the one most able to push a phone sideways. */
h1 {
  font-family: var(--mono);
  font-size: clamp(2rem, 11vw, 4.25rem);
  line-height: 1.1;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--green);
  margin: 0 0 1.5rem;
}

.cursor::before {
  content: "\258D";
  margin-left: 0.12em;
}

h1 {
  text-shadow: 0 0 2px rgba(126, 231, 135, 0.85),
               0 0 22px rgba(126, 231, 135, 0.40);
}

/* --- the sentence the page exists to say --------------------------------- */

.creed {
  font-size: clamp(0.98rem, 2.9vw, 1.3rem);
  line-height: 1.7;
  color: var(--fg);
  max-width: 38rem;
  margin: 0 0 2.5rem;
}

/* A tight bloom rather than a wide one. This was 14px, which is a halo wider
   than the strokes it surrounds -- on a panel that renders subpixels
   differently it smears the glyph edges and reads as soft rather than dim.
   The phosphor look survives at 5px; the smearing does not. */
.creed { text-shadow: 0 0 5px rgba(228, 233, 239, 0.22); }

/* --- the links ----------------------------------------------------------- */

/* Lifted clear of both overlays, the way the footer is and for the same
   reason. The scanlines and the vignette are atmosphere, and atmosphere over
   a name and a tagline is a tube you have to read through -- so the wordmark,
   the boot lines, the creed and the drifting marks keep the phosphor and the
   thing you actually came here to click does not.

   z-index 4 rather than 2 or 3: those are .scan and .glass. Adding
   `position: relative` is what makes the number mean anything. */
.links {
  position: relative;
  z-index: 4;
}

.links {
  list-style: none;
  padding: 0;
  margin: 0 0 2rem;
  font-size: 0.9rem;
  line-height: 1.9;
}

/* A row rather than a run of inline spans, so a tagline long enough to wrap
   wraps inside its own column instead of starting again under the prompt.
   tracker's is the one that found this: two sentences where the others are a
   clause, and the second line landed beneath the `>` and broke the list's
   left edge. `min-width: 0` on the tagline is what lets a flex item be
   narrower than its longest word wants. */
.links li {
  display: flex;
  align-items: baseline;
  max-width: 100%;
}

.links li .what { flex: 1 1 auto; min-width: 0; }
.links .prompt { color: var(--green); margin-right: 0.5rem; flex: 0 0 auto; }
.links .sep { color: var(--faint); margin: 0 0.35rem; }
/* --quiet, not --dim: these are the words telling a visitor what congo and the
   marks page are, which makes them content rather than decoration, and --dim
   measures 4.19:1 -- under what small text wants. The separator dots above
   stay faint on purpose; a dot is punctuation and does not have to be read. */
.links .what { color: var(--quiet); }
.links .name { color: var(--fg); }

/* --- the footer ---------------------------------------------------------- */

/* application.css gives footer.site a raised bar and a rule above it, which is
   right on a page with a header to match and wrong under a screen. */
/* Lifted above the two overlays rather than left under them. The vignette is
   heaviest at the edges, which is exactly where the footer sits, and the line
   about javascript and analytics is a promise the site makes -- it should be
   readable rather than atmospheric. */
footer.site.is-quiet {
  position: relative;
  z-index: 4;
  background: transparent;
  border-top: 0;
}

/* The one link down here, on the pages that carry it. --quiet rather than the
   footer's own colour so it reads as a link without shouting over the
   sign-off it sits beside. */
footer.site.is-quiet .sep { color: var(--faint); margin: 0 0.3rem; }
footer.site.is-quiet a { color: var(--quiet); }
footer.site.is-quiet a:hover { color: var(--fg); }

/* --- the pages that are not the front door ------------------------------- */

/* The mark bank and the contact form wear the CRT, and they are pages of
   prose and controls rather than a screen to look at. So everything on them
   comes up clear of the overlays -- except the heading, which is left down
   there in the phosphor to say what kind of site this is.

   Not applied to the container, which would take the heading up with it: a
   positioned child cannot sit below an overlay its positioned parent is above.
   `:not(h1)` on the direct children is what keeps the one exception possible. */
body.is-page .page > *:not(h1) {
  position: relative;
  z-index: 4;
}

/* --- motion -------------------------------------------------------------- */

@keyframes blink { 50% { opacity: 0; } }

/* Same fail-safe as the typing below: the rule does not set opacity, only the
   keyframe does, so a browser that will not run this shows the mark rather
   than hiding it. */
@keyframes arrive {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* `from` is spelled out, and the rule below does not set width:0 itself, so
   that a browser refusing steps(var(--ch)) leaves the line at its full width
   and merely unanimated. The other way round -- width:0 in the rule, the width
   restored only by the animation -- fails to a blank page, which is the worst
   failure a front door can have. */
@keyframes type {
  from { width: 0; }
  to   { width: calc(var(--ch) * 1ch); }
}

.cursor { animation: blink 1.06s steps(1, end) infinite; }

/* The column arrives once the boot lines have finished, which is the whole of
   why the boot lines are there. This delay belonged to the pinned mark before
   the column replaced it.

   The strip fades to full and the dimming sits on the images instead, so that
   hovering one can lift it on its own. */
.margin-marks { animation: arrive 0.9s ease-out both; animation-delay: 1.9s; }

/* `both`, not `forwards`: backwards holds the from-state through the delay, so
   a line waiting its turn is empty rather than briefly whole. */
body.crt .boot li {
  animation: type 0.42s steps(var(--ch), end) both;
  animation-delay: calc(var(--i) * 0.38s);
}

/* Nothing on this page is revealed by motion, so switching all of it off
   leaves the page complete rather than empty. */
@media (prefers-reduced-motion: reduce) {
  body.crt .cursor { animation: none; }
  body.crt .stack { animation: none; }
  body.crt .margin-marks { animation: none; }

  body.crt .boot li {
    width: calc(var(--ch) * 1ch);
    animation: none;
  }
}

/* A ch-measured line that wraps looks broken, and at this width they wrap. So
   narrow screens get the lines whole and unanimated. The specificity here
   matches the animation rule above deliberately -- equal weight, later in the
   file, so this wins without an !important. */
@media (max-width: 34rem) {
  body.crt .boot li {
    white-space: normal;
    width: auto;
    animation: none;
  }

  /* A fixed corner and vertically centred content are on a collision course
     once the viewport is short as well as narrow. Rather than shrink the mark
     until it is a smudge, the screen takes a top padding that reserves the
     corner, which pushes the centred block clear of it. */
  main.screen { padding-top: 5.5rem; }
}

/* --- the marks in the left margin ---------------------------------------- */

/* All fifteen marks drift up the far left, forever. Chad asked whether a
   carousel of them would work; this is one, turned ninety degrees and put in
   the margin, where it reads as an illuminated edge rather than as a sponsor
   bar sliding past.

   Four things here are load-bearing rather than decorative:

   1. pointer-events: none on the strip, and auto on the images only. This is
      a fixed strip running down the left of every link on the page -- the
      exact shape of the overlay bug CLAUDE.md records congo shipping. Putting
      the events back on the images alone means hovering one can pause and
      name it without the strip itself covering anything. A system test asks
      the browser what sits on top of the congo link.

   2. The column is hidden below 64rem rather than made smaller. The terminal
      is 44rem centred, so there is no room beside it until about 1020px, and
      shrinking the marks is not an option: the halftone gives out under
      112px, which is written down in "The front door".

   3. Each item carries margin-bottom and the column has no `gap`. The drift
      translates by exactly -50%, so the two halves of the list have to be the
      same height. With `gap`, thirty items have twenty-nine gaps and half the
      height is not one set, and the loop stutters once a cycle.

   4. The column is display: flex, and not for layout. A block container lets
      the last item's margin-bottom collapse through its bottom edge, which
      made the column 1652px instead of 1680 and landed the translate fourteen
      pixels short. Flex containers do not collapse their items' margins. */

.margin-marks { display: none; }

@media (min-width: 64rem) {
  .margin-marks {
    display: block;
    position: fixed;
    top: 0;
    bottom: 0;
    left: 1.4rem;
    width: 7rem;
    overflow: hidden;
    pointer-events: none;

    /* So marks dissolve at the top and bottom of the strip rather than
       appearing and vanishing at a hard edge. */
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
    mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
  }

  .margin-marks .column {
    display: flex;
    flex-direction: column;
    animation: drift 210s linear infinite;
  }

  /* Each mark links to its own entry on the marks page. The link is exactly
     the width of a drawing and takes the pointer events the strip refuses, so
     the clickable area is the mark and never the text beside it. */
  .margin-marks .item {
    position: relative;
    display: block;
    width: 7rem;
    margin-bottom: 1.75rem;
    pointer-events: auto;
  }

  /* The site underlines links on hover, and there is nothing here to
     underline but the name, which should not be. */
  .margin-marks .item:hover { text-decoration: none; }

  /* The dimming lives on the image rather than on the strip, so that one mark
     can come up to full strength on its own. On the strip it could only ever
     be all of them or none. */
  .margin-marks img {
    display: block;            /* inline images add line-height, which would
                                  break the exact halves the loop depends on */
    width: 7rem;
    height: 7rem;
    /* Third setting for this. 0.4 was 2.86:1, under even the 3.0 floor for
       non-text; 0.6 was 5.04:1 and still read as washed out on a second
       monitor. 0.8 is 8.2:1 -- present rather than suggested, and still short
       of the 12.57:1 the hover lifts it to, so pointing at one is still a
       visible change. */
    opacity: 0.8;
    image-rendering: pixelated;
    transition: opacity 0.25s ease-out;
  }

  /* Hovering any one of them stops the drift -- a moving target is a poor
     thing to ask anybody to point at -- and brings that mark up. :hover
     reaches the strip even though the strip takes no pointer events itself,
     because the image inside it does. */
  .margin-marks:hover .column { animation-play-state: paused; }
  .margin-marks .item:hover img { opacity: 1; }

  /* Hidden outright between 64rem and 80rem. Without this the name has no
     rules at all down here and renders as a stray word of unstyled body text
     in the margin, at full strength, all the time. */
  .margin-marks .name { display: none; }
}

/* The name only appears where there is room for it: at 64rem the margin is
   160px, which a caption would spill straight out of and over the text. By
   80rem there is 288px, so the name can sit beside the mark. Below that,
   hovering still pauses and brightens -- it just says nothing. */
@media (min-width: 80rem) {
  .margin-marks { width: 22rem; }

  /* pointer-events: none, and not for tidiness. The name is positioned
     outside the link's own box and is hit-testable even at opacity 0, so
     without this there is an invisible thirteen-rem strip over the text
     swallowing clicks. */
  .margin-marks .name {
    display: block;
    pointer-events: none;
    position: absolute;
    left: 8.25rem;
    top: 50%;
    transform: translateY(-50%);
    width: 13rem;
    font-family: var(--mono);
    font-size: 0.8rem;
    color: var(--green);
    opacity: 0;
    transition: opacity 0.2s ease-out;
  }

  .margin-marks .item:hover .name { opacity: 1; }
}

@keyframes drift {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .margin-marks .column { animation: none; }
  .margin-marks img { transition: none; }
}

/* --- a page wearing the front door's chrome ------------------------------ */

/* The marks page uses the landing layout, so it gets the scanlines, the
   vignette and no nav -- `application` carries yoga, software and about, and
   putting those one click from the front door would undo the splash-only
   decision sideways. What it does not want is the single centred screen, so a
   page opts out with a body class rather than by needing a second layout. */

body.is-page main.screen {
  align-items: flex-start;
  justify-content: center;
  padding-top: 3rem;
}

body.is-page .page {
  width: 100%;
  max-width: 52rem;
}

body.is-page .back {
  font-family: var(--mono);
  font-size: 0.85rem;
  margin: 0 0 2rem;
}

body.is-page h1 {
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  margin-bottom: 1.25rem;
}

body.is-page .lede { color: var(--fg); max-width: 38rem; }
body.is-page .note { max-width: 38rem; }
