/* xpeto.kamjapowered.com
 *
 * shares a generator with kamjapowered.com and nothing else: not a font, not a
 * colour, not a component. design/README.md forbids it, so there is no shared
 * stylesheet and every value here is transcribed from the `xpeto rules` frame
 * (232:60) and measured off `xpeto landing` (232:3).
 *
 * the conceit: the page pretends to be its own markdown source, printed on a
 * beach. one font, four sizes, headings marked by their # prefix and colour. */

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/jetbrainsmono-400.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/jetbrainsmono-700.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/jetbrainsmono-400i.woff2") format("woff2");
}

:root {
  /* the page */
  --sand: #ede8d8;   /* the sky above the sea, and the page */
  --dune: #e4ddc8;   /* the beach, from the hero down */
  --ink: #33302b;    /* headings and strong. warmer than kamja's */
  --body: #5b5044;   /* every paragraph. 5.8:1 on the dune */
  --soft: #7c6f63;   /* the about paragraph, and every dashed border */

  /* the markdown colours */
  --clavel: #bb3b33;     /* carnation. the # title, once per page */
  --terracotta: #aa5c36; /* roof tile. ## headings */
  --jacaranda: #6b58a6;  /* the trees in may. ### headings */
  --azulejo: #1f5f72;    /* links and the prompt. 5.3:1 */
  --olive: #578a59;      /* emphasis, and strings in code */

  --code-fg: #2f2f2f;
  --code-call: #47899a;
  --code-bg: rgba(47, 47, 47, 0.04);

  --column: 860px;

  /* the measure, in monospace columns. the frame's paragraphs are 634-672px
   * wide because their line breaks are typed by hand and the box hugs them;
   * at 16px JetBrains Mono one column is exactly 9.6px, so 68ch is 652.8 and
   * reproduces the frame's breaks without hard-coding a single one of them.
   * the code block and the footer row use the full 860 column instead. */
  --measure: 68ch;
  /* the column, with a gutter that only engages once the window is narrower
   * than the column plus it. at 1440 `100% - 48px` is 1392, so the min picks
   * 860 and the desktop rendering is bit-for-bit what it was. */
  --col: min(var(--column), 100% - 48px);

  /* the hero art's scale: device pixels per art pixel. the frame is drawn at 6.
   *
   * it steps down on narrow windows rather than cropping, because a phone
   * should get the same composition at a smaller size and not the middle third
   * of it. only integer steps are used: the art is drawn on a 6px grid and a
   * fractional scale would render some art pixels wider than others, which is
   * exactly the "dirty" pixel art docs/claude/repo.md warns about.
   *
   * --k is the same number as a fraction of the frame's 6, so every measurement
   * taken off the figma frame can stay written as the frame's own value times
   * --k, and stays readable next to the node it came from.
   *
   * beach.js reads --s off the hero rather than keeping its own breakpoints, so
   * the canvas and the css cannot drift apart. */
  --s: 6;
  --k: 1;
}

@media (max-width: 1199px) { :root { --s: 3; --k: 0.5; } }
@media (max-width: 619px)  { :root { --s: 2; --k: 0.33333; } }

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

/* the frame's borders are 1px on a 4/4 dash. css `border-style: dashed` is
 * browser-defined and chrome draws roughly 2.5/2.5, which measured 67 dashes
 * across the install box where the frame has 42: visibly busier, and not a
 * thing a value can fix. four repeating gradients state the pattern exactly.
 *
 * there is no `border` property at all, deliberately. figma's stroke does not
 * grow its frame: the code block is exactly 392px, which is 16 + 15 lines of
 * 24 + 16, with nothing left over for a border. adding even a transparent 1px
 * pushed the content 1px right and the box 2px tall. */
.dashed {
  background-image:
    repeating-linear-gradient(to right,  var(--soft) 0 4px, transparent 4px 8px),
    repeating-linear-gradient(to right,  var(--soft) 0 4px, transparent 4px 8px),
    repeating-linear-gradient(to bottom, var(--soft) 0 4px, transparent 4px 8px),
    repeating-linear-gradient(to bottom, var(--soft) 0 4px, transparent 4px 8px);
  background-size: 100% 1px, 100% 1px, 1px 100%, 1px 100%;
  background-position: 0 0, 0 100%, 0 0, 100% 0;
  background-repeat: no-repeat;
}

body {
  margin: 0;
  background: var(--sand);
  color: var(--body);
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 16px;
  line-height: 26px;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

/* every pixel of the art is on a 6px grid and placed at native size. letting
 * the browser smooth it is the one thing that ruins it. */
.scene, .espeto, .barca, .ground, .beach { image-rendering: pixelated; }

a { color: var(--azulejo); text-decoration: none; }
a:hover { text-decoration: underline; }
a:focus-visible { outline: 2px solid var(--azulejo); outline-offset: 2px; }

/* the # ## ### prefixes are part of the conceit, not decoration, so they keep
 * the heading's own colour rather than dimming into punctuation */
.hash { font-weight: 700; }

/* ===== nav ===== */

.nav {
  width: var(--col);
  margin-inline: auto;
  padding: 48px 0 40px;
  display: flex;
  flex-wrap: wrap;
  gap: 12px 18px;
  justify-content: space-between;
  align-items: center;
}
.filename { font-weight: 700; color: var(--ink); line-height: 21px; }
.links { display: flex; flex-wrap: wrap; gap: 18px; align-items: center; line-height: 21px; }

/* ===== hero ===== */

.hero {
  position: relative;
  height: calc(880px * var(--k));
  overflow: hidden;
}
/* a fixed 1440 stage, centred, so every sprite keeps the exact offset it has
 * in the frame no matter how wide the window is */
.scene {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: calc(1440px * var(--k));
  height: calc(880px * var(--k));
  background: url("/static/img/beach.png") no-repeat 0 0;
  background-size: calc(1440px * var(--k)) calc(882px * var(--k));
}
/* the frame is 1440 wide and the beach is a 1440 still, so on a wider window
 * the scene was an island with the page background showing down both sides.
 *
 * the art is banded and drawn on a 6px grid, so a 144px slice of each edge
 * repeated outwards continues the sky, the sea and the sand, 24 grid cells at
 * a time so the foam reads as texture rather than as horizontal streaks. a
 * 6px slice smeared it into lines. scaling the still to fit would break the
 * 6px grid, which docs/claude/repo.md forbids. */
.scene::before,
.scene::after {
  content: "";
  position: absolute;
  top: 0;
  height: 882px;
  width: 50vw;
  z-index: 0;
  background-repeat: repeat-x;
  background-size: 144px 882px;
}
.scene::before {
  right: 100%;
  background-image: url("/static/img/beach-edge-left.png");
}
.scene::after {
  left: 100%;
  background-image: url("/static/img/beach-edge-right.png");
}

/* the generated beach, animated. it renders at viewport/6 art pixels, so it is
 * drawn to the window's width instead of being a 1440 still padded out to it,
 * and the edge slices above are only ever seen by a reader who does not get it:
 * no js, no canvas, or prefers-reduced-motion. beach.js adds `live` itself, so
 * the still is what shows until the canvas is genuinely running. */
.beach {
  display: none;
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  height: calc(882px * var(--k));
  z-index: 0;
}
.hero.live .beach { display: block; }
.hero.live .scene { background-image: none; }
.hero.live .scene::before,
.hero.live .scene::after { display: none; }

.espeto, .barca {
  position: absolute;
  display: block;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  /* above the edge fillers: the barca runs past the right edge of the stage on
   * purpose and must not be painted over by ::after */
  z-index: 1;
}
.espeto {
  left: calc(1158px * var(--k)); top: calc(413px * var(--k));
  width: calc(240px * var(--k)); height: calc(336px * var(--k));
  background-image: url("/static/img/espeto-planted.png");
}
/* mirrored in the frame: its relativeTransform carries -1 on the x scale, so
 * the 1638 figma reports is the RIGHT edge, and the left edge is 918 */
.barca {
  left: calc(918px * var(--k)); top: calc(689px * var(--k));
  width: calc(720px * var(--k)); height: calc(156px * var(--k));
  background-image: url("/static/img/barca.png");
  transform: scaleX(-1);
}

/* the same box, the same mirror, drawn rather than served. it replaces the
 * sprite above only once beach.js has found the coals inside that png, so a
 * failed decode or a tainted canvas leaves the still boat in place. */
.barca-live {
  display: none;
  position: absolute;
  left: calc(918px * var(--k)); top: calc(689px * var(--k));
  width: calc(720px * var(--k)); height: calc(156px * var(--k));
  transform: scaleX(-1);
  z-index: 1;
  image-rendering: pixelated;
}
.hero.fire .barca-live { display: block; }
.hero.fire .barca { background-image: none; }
.hero-copy {
  position: relative;
  width: var(--col);
  margin-inline: auto;
  padding-top: 96px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 24px;
}
.hero-copy h1 {
  margin: 0;
  font-size: 56px;
  line-height: 68px;
  font-weight: 700;
  color: var(--clavel);
}
.lede { margin: 0; max-width: var(--measure); }
.install {
  margin: 0;
  padding: 12px 16px;
  line-height: 21px;
  color: var(--ink);
  display: flex;
  gap: 10px;
}
.install code { font: inherit; }
.prompt { color: var(--azulejo); }
/* the separators are `·` flanked by two spaces in the frame, and html
 * collapses runs of whitespace, so the run has to be preserved explicitly */
.meta { margin: 0; font-size: 14px; line-height: 18px; white-space: pre-wrap; }

/* ===== the ground, and the bands on it ===== */

/* one continuous tile under everything below the hero. per docs/claude/repo.md
 * this art is placed at scalingFactor 1 in figma, so it renders at its natural
 * 1440x1200 and the css says so explicitly. */
.ground {
  background-color: var(--dune);
  background-image: url("/static/img/sand.png");
  background-repeat: repeat;
  background-size: calc(1440px * var(--k)) calc(1200px * var(--k));
  background-position: center top;
}

.band {
  width: var(--col);
  margin-inline: auto;
  padding: 40px 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
}
.band > * { margin: 0; }
.band p { max-width: var(--measure); }
/* the code block and the link rows are not prose and take the whole column */
.band pre, .band .cta { max-width: none; }
#contribute { padding-bottom: 56px; }

h2 { font-size: 20px; line-height: 30px; font-weight: 700; color: var(--terracotta); }
h3 { font-size: 20px; line-height: 30px; font-weight: 700; color: var(--jacaranda); }

/* one column wider than the rest: this paragraph's longest line is 69 columns
 * in the frame, and 68 folds "málaga" onto a line of its own.
 *
 * the selector is `.band p.rich` and not `.rich` on purpose. `.band p` above
 * is (0,1,1) and a bare `.rich` is (0,1,0), so the narrower measure would win
 * and the widening would silently do nothing. design/v0/design.md records the
 * same specificity trap breaking the kamja hero. */
.band p.rich { color: var(--soft); white-space: pre-line; max-width: 70ch; }
.t-wordmark { font-style: italic; color: var(--clavel); }
.t-em { font-style: italic; color: var(--olive); }

/* ===== code ===== */

/* font-size and line-height belong on the pre, not on the inline code inside
 * it. a block container contributes a strut of its own line-height to every
 * line box, and the box takes the taller of the two, so `pre code { line-height
 * }` alone leaves the body's 26px in charge and the block renders 2px per line
 * too tall. measured against the frame: 13 lines, 26px of accumulated drift. */
pre {
  width: 100%;
  margin: 0;
  padding: 16px 20px;
  background-color: var(--code-bg);
  overflow-x: auto;
  font-size: 14px;
  line-height: 24px;
  color: var(--code-fg);
}
pre code { font: inherit; color: inherit; }
.t-string { color: var(--olive); }
.t-call { color: var(--code-call); }

/* ===== link rows ===== */

.cta { display: flex; gap: 18px; padding-top: 8px; line-height: 21px; }
.cta .lead { font-weight: 700; }

/* ===== footer ===== */

.footer {
  width: var(--col);
  margin-inline: auto;
  padding: 32px 0 72px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.footer p { margin: 0; }
.footer-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 24px;
  justify-content: space-between;
  font-size: 14px;
  line-height: 18px;
}

/* ===== the hero below the frame's width =====
 *
 * LAST IN THE FILE ON PURPOSE. these rules override .hero, .scene and
 * .hero-copy, which are declared with the same specificity above, so a media
 * query placed before them loses and does nothing at all. it cost a round to
 * find: the mobile hero rendered with no copy in it.
 */
/* below the frame's own width the hero stops being an overlay and stacks.
 *
 * the copy sits over the sky on desktop because the sky is 78 of the art's 147
 * rows, which at scale 6 is 468px of clear space. the art scales and the text
 * does not, so at scale 3 that space is 234px and at scale 2 it is 156, and the
 * type starts landing in the sea. stacking is the only version of "the same
 * page, smaller" that stays readable. */
@media (max-width: 1199px) {
  .hero {
    height: auto;
    display: flex;
    flex-direction: column;
  }
  /* still the frame's stage, still centred, so every sprite keeps the offset it
   * has in figma. it just takes its own space now instead of being underlaid.
   *
   * `position: relative` and nothing else: the base rule's `left: 50%` plus
   * `translateX(-50%)` still does the centring, and it has to, because the
   * stage is WIDER than a phone. `margin-inline: auto` cannot centre an
   * overflowing box, since auto margins never go negative, so it pinned the
   * stage flush left and left a strip of page showing down that side. */
  .scene { position: relative; }
  .hero-copy { padding-top: 40px; padding-bottom: 16px; order: -1; }
}
