/* THE APEX. Not a band's page, and it must not pretend to be one.
 *
 * This is the one surface on the platform that IS marketing, which in this
 * scene is a liability rather than an asset: a band arriving here is deciding
 * whether we are outsiders. So the page is built out of the product's own
 * materials — the plates we ship, the typefaces a band can choose, the
 * near-black every theme starts from — rather than out of the visual language
 * of a software company.
 *
 * No theme.css here, deliberately. That stylesheet is driven by a band's
 * ~10-field theme record and a per-band inline `:root` block; the apex has no
 * band and no record, so importing it would mean inventing one. These are our
 * own values, written once.
 *
 * No JavaScript. Nothing on this page needs it, and the same argument that
 * makes a band's page legible to an answer engine applies here — with more
 * force, because this is the page we actually want ranked. */

/* The wordmark face, declared here rather than in fonts.css because fonts.css
   serves the band sites and its families are the ones a band's `type_set` can
   name. This one is ours, it is used on one element, and no band can choose it.
   SIL Open Font License — see app/assets/fonts/README.md for what the download
   did and did not contain. */
/* THE RUNIC ACCENT — Ny Stormning, and it is DECORATION with a hard rule
   attached to it.
   Elder and Younger Futhark forms drawn on Latin codepoints: A-Z and a-z all
   map, so a browser renders ordinary text in rune-like letterforms rather than
   in actual runes. That is exactly what makes it dangerous — a visitor who
   cannot read it gets no fallback signal at all, because to the machine it is
   still the word.
   So it is allowed on ORNAMENT ONLY: the marks and rules that sit alongside
   real text, never on a word somebody has to read, and never on anything a
   crawler is meant to index. The same illegibility budget `--accent` is spent
   from, one axis over. Anything set in it carries its plain text in the
   document and is `aria-hidden` where it is purely a mark.
   SIL Open Font License 1.1, Reserved Font Name "Ny Stormning", KineticPlasma /
   Cannot Into Space Fonts. The archive carried its own OFL text, which is
   committed beside it as `ny-stormning-OFL.txt` — see
   app/assets/fonts/README.md for why that sentence is not boilerplate here. */
@font-face {
  font-family: 'Ny Stormning';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/ny-stormning-normal-400-09ddba97.woff2") format("woff2");
}

@font-face {
  font-family: 'Kun Denta';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/kun-denta-normal-400-98e8d0c6.woff2") format("woff2");
}

:root {
  --ground:   #07080a;
  --raise:    #0d0f12;
  --ink:      #efeae2;
  --ink-dim:  #918b83;
  --ink-faint:#605b56;

  /* Dried blood on black. It is 2.11:1 against the ground and that is the
     point — the design docs call this the illegibility budget, and it is spent
     on DECORATION only. Anything a reader has to read takes --ink or
     --accent-ui, never this. */
  --accent:    #8c1616;
  --accent-ui: #c07a5e;

  --rule:        rgba(239, 234, 226, .10);
  --rule-strong: rgba(239, 234, 226, .26);

  --display: 'Kun Denta', 'Grenze Gotisch', 'Times New Roman', serif;
  /* Ornament only — see the @font-face note above. Never a --display fallback:
     a face that renders readable text as unreadable text must not be reachable
     by inheritance. */
  --rune:    'Ny Stormning', 'Kun Denta', serif;
  --label:   'Oswald', 'Arial Narrow', system-ui, sans-serif;
  --body:    'EB Garamond', Georgia, 'Times New Roman', serif;
  --mono:    'IBM Plex Mono', ui-monospace, Menlo, monospace;

  --measure: 36rem;
  --wide:    64rem;
  --pad:     clamp(1.25rem, 5vw, 3rem);
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--body);
  font-size: 1.125rem;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
}

/* One of the plates the product ships, doing the job it was cut for. `ashfall`
   is a monochrome crush of the torch-lit birches. Fixed and screen-blended,
   exactly as a band's own ambient layer is, so this page shows the feature
   rather than describing it.

   image-set() with the JPEG named as a plain url() first: that is the CSS-level
   fallback for anything without image-set support, and every browser that has
   it takes the WebP, which is a third of the bytes for the same picture. */
.ambient {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  /* DARKER THAN IT WAS (.78), and the reason is the source rather than taste.
     The plate is cut from a 1179px phone frame, so `ashfall-1280` is already at
     native resolution — there is no larger file to ship and one cannot be
     invented. At .78 on a 2560px display the upscale read as a rough, blurry
     photograph. Taken down, it reads as the dark it always wanted to be, and the
     softness stops being the first thing the eye lands on. */
  opacity: .30; mix-blend-mode: screen;
  background-image: url("/assets/plates/ashfall-1280-b673077e.jpg");
  background-image: image-set(url("/assets/plates/ashfall-1280-3f6579f5.webp") type("image/webp"),
                              url("/assets/plates/ashfall-1280-b673077e.jpg") type("image/jpeg"));
  background-position: center; background-repeat: no-repeat; background-size: cover;
}
@media (max-width: 40rem) {
  .ambient {
    background-image: url("/assets/plates/ashfall-640-92c09c32.jpg");
    background-image: image-set(url("/assets/plates/ashfall-640-1e510b1b.webp") type("image/webp"),
                                url("/assets/plates/ashfall-640-92c09c32.jpg") type("image/jpeg"));
  }
}

/* PAST THE POINT WHERE THE FILE RUNS OUT, BLUR IT ON PURPOSE.
   Above ~1300px the 1280 plate is being stretched, and a stretched photograph
   looks like a mistake while a soft one looks like depth of field. This is not
   hiding the limitation so much as choosing which limitation to have: the detail
   is not in the file and no CSS puts it back.
   `inset` goes negative because a blur samples past the element's edge and would
   otherwise feather the layer into transparency at the top and bottom of the
   viewport, drawing a visible seam exactly where the vignette is trying to close
   the frame. */
@media (min-width: 81rem) {
  .ambient {
    inset: -2vmax;
    filter: blur(2px);
  }
}

/* Closes the edges of the frame in. The plate is a photograph of a forest at
   night and a rectangle of it stops at a hard edge; this takes the corners back
   down to the ground so it reads as depth rather than as a pasted-in picture. */
.vignette, .page::before {
  content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(120% 90% at 50% 42%, transparent 38%, var(--ground) 100%);
}

.page { position: relative; z-index: 1; }

/* ---- shared type ---------------------------------------------------------- */

.eyebrow {
  font-family: var(--label);
  font-size: .72rem;
  font-weight: 400;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 1rem;
}

h2 {
  font-family: var(--label);
  font-weight: 400;
  font-size: clamp(1.45rem, 3.6vw, 2rem);
  line-height: 1.16;
  letter-spacing: .02em;
  text-wrap: balance;
  margin: 0 0 1.5rem;
}

h3 {
  font-family: var(--label);
  font-weight: 400;
  font-size: 1rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  margin: 0 0 .6rem;
}

p { margin: 0 0 1.15rem; max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

a { color: var(--accent-ui); text-decoration-thickness: 1px; text-underline-offset: .18em; }
a:hover { color: var(--ink); }
a:focus-visible { outline: 2px solid var(--accent-ui); outline-offset: 3px; }

/* ---- the stage ------------------------------------------------------------ */

/* One screen, one word. Everything that explains the product is below the fold,
   because a band deciding whether we are outsiders decides on the first screen,
   and a paragraph there would be arguing rather than showing. */
.stage {
  min-height: 100svh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  padding: 4rem var(--pad) 6rem;
  position: relative;
}

/* The mark, drawn rather than set. `mask` rather than `img` so the colour comes
   from --ink and the same file works on any ground; the alpha channel is the
   whole drawing.

   The two files are 560 and 1120, which are EXACT for a 1x and a 2x screen at
   this width — the browser resamples neither. Any other pair asks it to rescale
   a hero element for nothing, and on a drawing made of hairline drips that is
   the difference between crisp and soft.

   The 1x file is unsharp-masked after the reduction and the 2x file is not. At
   1120 the drawing is 0.76 of its native 1481px, close enough that resampling
   costs nothing; at 560 it is 0.38, where area-averaging drops a one-pixel drip
   below the alpha it needs to read as a line at all. Sharpening the reduction is
   what puts it back, and doing the same to the 2x file would only add halos to
   an image that never lost anything. */
.wordmark {
  width: min(58vw, 560px);
  aspect-ratio: 1120 / 724;
  margin: 0;
  background: var(--ink);
  -webkit-mask: var(--mark) center / contain no-repeat;
          mask: var(--mark) center / contain no-repeat;
  --mark: image-set(url("/assets/brand/mark-560-9f366d21.png") 1x, url("/assets/brand/mark-1120-eb234d6b.png") 2x);
}

.claim {
  font-family: var(--mono);
  font-size: clamp(.7rem, 2.2vw, .84rem);
  letter-spacing: .42em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 2.25rem 0 0;
  max-width: none;
  /* The wordmark is unreadable by design; this is the line that says what it is,
     so it takes the letterspacing and none of the drama. */
  text-indent: .42em;
}

.scroll {
  position: absolute; bottom: 2.5rem; left: 50%; translate: -50% 0;
  font-family: var(--mono); font-size: 1rem;
  color: var(--ink-faint);
  margin: 0;
  animation: drift 3.4s ease-in-out infinite;
}
@keyframes drift {
  0%, 100% { transform: translateY(0);      opacity: .55; }
  50%      { transform: translateY(.45rem); opacity: 1; }
}

/* ---- what little is said ---------------------------------------------------- */

/* Text only, one short line at a time, revealed as it arrives. The reveal is a
   scroll-driven CSS animation — no JavaScript and no library. Browsers without
   `animation-timeline` never run it and the text is simply already there, which
   is why the resting state is VISIBLE and the animation moves away from it.
   Backwards, and every unsupported browser gets a blank page. */
.say {
  max-width: var(--wide); margin: 0 auto;
  padding: clamp(3.5rem, 14vh, 8rem) var(--pad);
  text-align: center;
}
.say__line {
  font-family: var(--label);
  font-size: clamp(1.4rem, 5vw, 2.6rem);
  line-height: 1.12; letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink);
  margin: 0 auto .2rem; max-width: 24ch;
}
.say__line:last-child { margin-bottom: 0; }

/* The one line that is allowed to be in the wordmark's face. */
.say__oath {
  font-family: var(--display);
  font-size: clamp(1.6rem, 6vw, 3.2rem);
  line-height: 1.05; color: var(--ink);
  margin: 0 auto .9rem; max-width: 18ch;
}
.say--last { padding-bottom: clamp(4rem, 16vh, 9rem); }

.say__addr {
  font-family: var(--mono);
  font-size: clamp(.9rem, 3vw, 1.15rem); letter-spacing: .04em;
  color: var(--accent-ui);
  margin: 0 auto 1.25rem; max-width: none;
}
.say__note {
  color: var(--ink-faint); font-size: .95rem; line-height: 1.6;
  margin: 1.4rem auto 2.5rem; max-width: 40ch;
}
.say__note:last-child { margin-bottom: 0; }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* `entry 0% entry 92%`, and the range is the whole trick. A mixed range
       like `entry 5% cover 32%` reads fine and is DEGENERATE for any element
       taller than the viewport — cover 32% resolves before entry 5%, so the
       animation completes before it begins and every frame is the end state.
       Measured on the first version of this page: opacity 1 at every scroll
       position, an animation correctly attached and never once running. */
    .say { animation: rise linear both; animation-timeline: view(); animation-range: entry 0% entry 92%; }
    @keyframes rise {
      from { opacity: 0; transform: translateY(2rem); }
      to   { opacity: 1; transform: none; }
    }
  }
}

/* ---- inner pages ---------------------------------------------------------- */

.hero {
  max-width: var(--wide);
  margin: 0 auto;
  padding: clamp(4rem, 12vw, 9rem) var(--pad) clamp(3rem, 8vw, 5rem);
}
.actions { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; align-items: center; margin-top: 2.25rem; }

.btn {
  display: inline-block;
  font-family: var(--label);
  font-size: .82rem; letter-spacing: .18em; text-transform: uppercase;
  text-decoration: none;
  padding: .95rem 2rem;
  border: 1px solid var(--rule-strong);
  color: var(--ink); background: transparent;
}
.btn:hover { border-color: var(--ink); background: rgba(239, 234, 226, .04); }
.btn--quiet { border-color: transparent; padding-left: 0; padding-right: 0; color: var(--ink-dim); }
.btn--quiet:hover { background: none; color: var(--ink); }

.hero--inner { padding-bottom: clamp(1.5rem, 4vw, 2.5rem); }

/* The way back to the root, and the one target on this site that was under the
   floor. Measured at 390px: 82 x 17, on /about, /privacy and /contact — a
   standalone masthead link, `display: inline` but not inside a sentence, so it
   takes none of 2.5.8's inline exemption.

   The mechanism is the band sites' (`theme.css:1140-1152`) rather than a new
   one: an absolutely-positioned ::after inside a relatively-positioned anchor
   grows the box a browser hit-tests against and contributes nothing to layout.
   Padding or min-height would both work and both would move the masthead, and
   this type is set tight on purpose. `inset: -4px 0` takes 17px to 25px.

   A pseudo-element has no content and never reaches the accessibility tree, so
   there is nothing to hide from a screen reader. */
.hero--inner .eyebrow a {
  color: var(--ink-faint); text-decoration: none;
  position: relative;
}
.hero--inner .eyebrow a::after { content: ""; position: absolute; inset: -4px 0; }
.hero--inner .eyebrow a:hover { color: var(--ink); }

.title {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(2.75rem, 10vw, 5rem); line-height: .9;
  margin: 0; color: var(--ink);
}

.prose {
  max-width: var(--wide); margin: 0 auto;
  padding: clamp(2rem, 5vw, 3rem) var(--pad) clamp(3rem, 7vw, 4.5rem);
  border-top: 1px solid var(--rule);
}
.prose h2 { margin-top: 2.75rem; }
.prose .lede { font-size: 1.25rem; color: var(--ink); }
.prose .actions { margin-top: 2.5rem; }

/* ---- the contact form ----------------------------------------------------- */

.form { margin-top: 2rem; max-width: var(--measure); }
.field { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1.5rem; }
.field label {
  font-family: var(--label); font-size: .7rem; letter-spacing: .2em;
  text-transform: uppercase; color: var(--ink-faint);
}
.field input, .field textarea {
  font-family: var(--body); font-size: 1rem; line-height: 1.5;
  color: var(--ink); background: var(--raise);
  border: 1px solid var(--rule-strong); border-radius: 0;
  padding: .8rem .9rem; width: 100%;
}
.field input:focus-visible, .field textarea:focus-visible {
  outline: 2px solid var(--accent-ui); outline-offset: 2px; border-color: transparent;
}
.form input[type="submit"] { cursor: pointer; margin-top: .5rem; }

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

/* ---- the auth pages ------------------------------------------------------- */

/* A hint under a field, not a paragraph in its own right: it sits inside .field
   so the flex column keeps it against the input it describes, which is also
   where aria-describedby says it is. */
.hint {
  margin: 0; color: var(--ink-faint); font-size: .85rem; line-height: 1.5;
  max-width: 46ch;
}

/* The remember-me row. The native checkbox is a bright white square on this
   ground — the loudest thing on a page whose submit button is a thin outline —
   so it is drawn here instead: the same 1px rule and raised ground as the text
   inputs, at the 24px target floor, with the tick drawn by a clip-path on
   ::after rather than by a font glyph or an SVG file.
   
   IT IS STILL AN <input type="checkbox">. Only the paint is replaced, so the
   keyboard, the label's click target and a screen reader's "checked" state are
   the browser's. And `appearance` goes back to `auto` under forced colours,
   where the author's paint is discarded and a hand-drawn tick would leave an
   empty box that cannot say whether it is checked. */
.check { display: flex; align-items: center; gap: .7rem; margin: 0 0 1.75rem; }
.check input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 24px; height: 24px; margin: 0; flex: none;
  background: var(--raise); border: 1px solid var(--rule-strong);
  display: grid; place-content: center; cursor: pointer;
}
.check input[type="checkbox"]::after {
  content: ""; width: 12px; height: 12px; background: var(--accent-ui);
  clip-path: polygon(14% 45%, 0 60%, 39% 100%, 100% 22%, 85% 8%, 38% 70%);
  transform: scale(0);
}
.check input[type="checkbox"]:checked { border-color: var(--accent-ui); }
.check input[type="checkbox"]:checked::after { transform: scale(1); }
.check input[type="checkbox"]:hover { border-color: var(--ink-dim); }

@media (forced-colors: active) {
  .check input[type="checkbox"] { appearance: auto; -webkit-appearance: auto; }
  .check input[type="checkbox"]::after { content: none; }
}
.check label {
  font-family: var(--body); font-size: .95rem; color: var(--ink-dim);
  letter-spacing: 0; text-transform: none;
}
.check input:focus-visible { outline: 2px solid var(--accent-ui); outline-offset: 2px; }

/* Sign in / create an account / forgotten. A row rather than a bullet list: it
   is navigation between three short states, and a <ul> of underlined links is
   the browser default that made these pages look unmade. The nav element and
   its label carry the structure a list was standing in for. */
.auth__links {
  display: flex; flex-wrap: wrap; gap: .6rem 2rem;
  margin-top: 2.5rem; padding-top: 1.5rem; border-top: 1px solid var(--rule);
}
.auth__links a {
  font-family: var(--label); font-size: .74rem; letter-spacing: .16em;
  text-transform: uppercase; text-decoration: none; color: var(--ink-dim);
  padding: .5rem 0;
}
.auth__links a:hover { color: var(--ink); }

/* Devise talks to a person through the flash and nothing else — paranoid mode
   means the "check your email" line is the ONLY feedback a correct address
   gets, and it must not read as chrome. Same left rule as .errors, so a refusal
   and a confirmation are visibly the same kind of message rather than two
   inventions. */
.flash {
  border-left: 2px solid var(--accent-ui);
  padding: .1rem 0 .1rem 1.1rem; margin: 0 0 1.75rem;
  color: var(--ink); max-width: var(--measure);
}
.flash--alert { border-left-color: var(--ink-dim); }

.errors {
  border-left: 2px solid var(--accent-ui);
  padding: .25rem 0 .25rem 1.1rem; margin-bottom: 1.75rem; color: var(--ink);
}
.errors p { margin: 0 0 .5rem; font-family: var(--label); font-size: .8rem;
            letter-spacing: .12em; text-transform: uppercase; }
.errors ul { margin: 0; padding-left: 1.1rem; color: var(--ink-dim); font-size: .95rem; }

/* Visually hidden, still announced. The examples section needs a heading for
   structure and does not need one on screen. */
.vh {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

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

footer {
  max-width: var(--wide);
  margin: 0 auto;
  padding: 2.5rem var(--pad) 4rem;
  border-top: 1px solid var(--rule);
  color: var(--ink-faint);
  font-family: var(--mono);
  font-size: .78rem;
  line-height: 1.7;
}
footer nav {
  display: flex; flex-wrap: wrap; gap: .5rem 2rem; margin-bottom: 1.5rem;
}
footer nav a {
  font-family: var(--label); font-size: .74rem; letter-spacing: .16em;
  text-transform: uppercase; text-decoration: none; color: var(--ink-dim);
  padding: .5rem 0;
}
footer nav a:hover { color: var(--ink); }
footer a { color: var(--ink-dim); }
footer p { max-width: var(--measure); }

/* ---- the preferences the band site already honours ------------------------ */

@media (prefers-contrast: more) {
  /* The plate is decoration and it is the first thing to go. Declared on the
     class rather than on a variable: this file has no per-band inline block
     after it, but the class rules still have to win on order, so they sit at
     the end of the file. */
  .ambient { display: none; }
  :root { --ink-dim: #d8d3cc; --ink-faint: #b8b3ac; --rule: rgba(239, 234, 226, .3); }
}

@media (forced-colors: active) {
  .ambient { display: none; }
  .btn { border: 1px solid; }

  /* The mark is a mask over a background colour, and forced colours replace
     that colour with Canvas — the same paint as the page behind it. Measured:
     the whole first screen rendered with no product name on it at all, because
     the drawing had gone the same white as the ground and the <h1> text under
     it is clipped to a 1px box. A drawn logo cannot survive a mode that throws
     away the author's colours, so here the word is set rather than drawn. */
  .wordmark {
    width: auto; aspect-ratio: auto; background: none;
    -webkit-mask: none; mask: none;
    font-family: var(--display);
    font-size: clamp(2rem, 7vw, 4rem);
    line-height: 1.05; letter-spacing: .02em;
  }
  .wordmark .vh {
    position: static; width: auto; height: auto; margin: 0;
    overflow: visible; clip-path: none; white-space: normal;
  }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* ==========================================================================
   THE RUNIC ORNAMENT — Ny Stormning, used on marks and nowhere else
   ==========================================================================

   One rule, stated as a rule rather than as a convention because the failure is
   invisible: a face that draws Latin letters as rune-forms turns readable text
   into unreadable text WITHOUT changing the document. The word is still there
   for a crawler, a screen reader and a copy-paste — only the human looking at it
   is locked out. Nothing that carries meaning may be set in it.

   So the only elements below are ones whose entire content is decorative, and
   each of them is `aria-hidden` in the markup or carries its plain text beside
   it. A `.rune` on a heading would pass every spec in this repository and be
   read by nobody. */
.rune {
  font-family: var(--rune);
  /* The forms are angular and narrow; without a little tracking they knit
     together into one shape at the sizes ornament is used at. */
  letter-spacing: .18em;
  /* Decoration, so it is spent from the illegibility budget `--accent` is spent
     from, and never from `--ink`. */
  color: var(--accent-ui);
}

/* The divider between sections — a line, with a rune-set mark sitting in it.
   Purely a mark: the element that carries it is aria-hidden and the sections it
   separates are already headed by real text. */
.rune-rule {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 3.5rem 0;
  color: var(--ink-faint);
}
.rune-rule::before,
.rune-rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule);
}
.rune-rule span {
  font-family: var(--rune);
  font-size: 1.1rem;
  letter-spacing: .3em;
  color: var(--accent-ui);
}
