/* 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,
   and it is what a phone shows -- down there the line wraps to two and a box
   one line tall would cut it in half. Above 34rem 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. Same technique as the
   sign-up honeypot in application.css, for the opposite reason. */
.creed-reel,
.creed-tail { display: none; }

@media (min-width: 34rem) {
  .creed-reel { display: block; }
  .creed-tail { display: block; }

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

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

/* 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;
}

.creed { text-shadow: 0 0 14px rgba(197, 205, 217, 0.18); }

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

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

.links li { max-width: 100%; }
.links .prompt { color: var(--green); margin-right: 0.5rem; }
.links .sep { color: var(--faint); margin: 0 0.35rem; }
.links .what { color: var(--dim); }
.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;
}

/* --- 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;
    /* 0.4 measured 2.86:1 against the background -- below even the 3.0 floor
       for something that is not text, and visibly washed out on a second
       monitor. 0.6 is 5.04:1: clearly there, still clearly behind the
       wordmark, and it leaves room for the hover to lift to full. */
    opacity: 0.6;
    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; }
