/* ==========================================================================
   damdza.com — stylesheet
   --------------------------------------------------------------------------
   Layered top to bottom, most general first:

     1. Design tokens      the five colours, type, spacing, dither patterns
     2. Base               element defaults and resets
     3. Utilities          the hover inversion and the dim text roles
     4. Fixed overlays     starfield, cursor trail, scanlines
     5. Layout             page shell, grids
     6. Components         header, hero, nav boxes, sections, devlog, footer
     7. Responsive         the single breakpoint, collected in one place

   The whole palette is five values. Every fill is pure black, pure white, or
   a black/white dither pattern — the greys exist only as text colours. If you
   find yourself reaching for a sixth colour, reach for a dither instead.
   ========================================================================== */

/* ==========================================================================
   1. Design tokens
   ========================================================================== */

:root {
  /* — the entire palette — */
  --ink: #000000;
  /* page background, inverted text */
  --paper: #ffffff;
  /* all text, all borders, inverted backgrounds */
  --dim: #8a8a8a;
  /* tertiary metadata: boot log, dates, notes, footer */
  --dim2: #bdbdbd;
  /* secondary body copy: feature and link descriptions */
  --rule: #5a5a5a;
  /* dotted internal dividers only */

  /* — type — */
  --font-display: "Pixelify Sans", monospace;
  --font-body: "DotGothic16", "Courier New", monospace;

  /* — geometry — */
  --gutter: 28px;
  --border: 2px solid var(--paper);
  --hairline: 1px solid var(--paper);

  /* — dither patterns —
     These do the work colour normally would, so they are tokens rather than
     one-off backgrounds. Each needs its own background-size, kept alongside. */
  --dither-check: repeating-conic-gradient(var(--paper) 0 25%, var(--ink) 0 50%);
  --dither-dots: radial-gradient(var(--paper) 1px, transparent 1.15px);
  --dither-hatch: repeating-linear-gradient(135deg, rgba(255, 255, 255, .09) 0 2px, transparent 2px 7px);
  --dither-halftone: radial-gradient(var(--ink) 1px, transparent 1.2px);

  color-scheme: dark;
}

/* ==========================================================================
   2. Base
   ========================================================================== */

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.75;
  /* Antialiasing off on purpose: these are bitmap faces and should look it. */
  -webkit-font-smoothing: none;
  font-smooth: never;
  cursor: crosshair;
}

h1,
h2,
h3,
p,
dl,
dd,
pre {
  margin: 0;
}

h1,
h2,
h3 {
  font-weight: 400;
  line-height: 1.3;
}

a {
  color: inherit;
  text-decoration: none;
}

img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

::selection {
  background: var(--paper);
  color: var(--ink);
}

:focus-visible {
  outline: 2px solid var(--paper);
  outline-offset: 3px;
}

/* ==========================================================================
   3. Utilities
   --------------------------------------------------------------------------
   Invert-on-hover is the only interaction motif: hovering anything
   interactive swaps black and white, with no transition — it should snap.

   Every colour that takes part in the inversion has to come from a class,
   never from an inline style or an element-level default. An inline
   background:#000 would beat these rules and leave black text on a black
   box; grey text that survives the swap ends up at ~3.5:1 on white, which
   fails AA. Hence .dim / .dim2 rather than per-element colours.
   ========================================================================== */

.dim {
  color: var(--dim);
}

.dim2 {
  color: var(--dim2);
}

.inv:hover,
a:hover {
  background: var(--paper);
  color: var(--ink);
}

.inv:hover .dim,
.inv:hover .dim2,
a:hover .dim,
a:hover .dim2 {
  color: var(--ink);
}

/* Tags counter-invert so they stay legible inside an inverted row. */
.inv:hover .tag,
a:hover .tag {
  background: var(--ink);
  color: var(--paper);
}

/* Underlined text link, used in the footer. */
.ulink {
  border-bottom: var(--hairline);
}

/* ==========================================================================
   4. Fixed overlays
   --------------------------------------------------------------------------
   Three full-viewport layers, none of them interactive. They sit outside
   .page so they cover the viewport rather than the 1000px column.
   ========================================================================== */

.fx-stars,
.fx-trail,
.fx-scanlines {
  position: fixed;
  inset: 0;
  pointer-events: none;
}

/* Two dot grids at coprime-ish sizes, so the pattern never visibly tiles. */
.fx-stars {
  z-index: 0;
  opacity: .32;
  background-image:
    radial-gradient(var(--paper) .7px, transparent .8px),
    radial-gradient(var(--paper) .6px, transparent .7px);
  background-size: 47px 61px, 113px 89px;
  background-position: 0 0, 23px 37px;
}

/* Populated by app.js — empty in the markup. */
.fx-trail {
  z-index: 60;
}

.fx-trail>div {
  position: absolute;
  left: 0;
  top: 0;
  background: var(--paper);
  image-rendering: pixelated;
}

.fx-scanlines {
  z-index: 70;
  mix-blend-mode: screen;
  background: repeating-linear-gradient(180deg, rgba(255, 255, 255, .055) 0 1px, transparent 1px 4px);
}

/* ==========================================================================
   5. Layout
   ========================================================================== */

.page {
  position: relative;
  z-index: 1;
  max-width: 1000px;
  margin: 0 auto;
}

/* The horizontal gutter. One class so the breakpoint changes it in one
   place; every full-width strip carries it. */
.pad {
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

.section {
  padding-top: 44px;
  scroll-margin-top: 56px;
}

.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  align-items: start;
}

.grid-2--wide {
  grid-template-columns: 1.4fr 1fr;
}

.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

.grid-shots {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

/* ==========================================================================
   6. Components
   ========================================================================== */

/* — Header ---------------------------------------------------------------
   Sticky, and its background has to stay opaque: the page scrolls under it. */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 14px;
  padding-top: 8px;
  padding-bottom: 8px;
  background: var(--ink);
  border-bottom: var(--border);
  font-family: var(--font-display);
  font-size: 16px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
}

.brand:hover {
  background: none;
  color: inherit;
}

/* Inline SVG stroked in currentColor — see the comment on it in index.html.
   Nothing here sets a colour: it is white because the header is white, and it
   would go black with the header if that ever inverted. */
.brand__mark {
  display: block;
  width: 18px;
  height: 18px;
  flex: none;
}

.caret {
  width: 8px;
  height: 15px;
  background: var(--paper);
  animation: caret 1s steps(1) infinite;
}

@keyframes caret {

  0%,
  49% {
    opacity: 1;
  }

  50%,
  100% {
    opacity: 0;
  }
}

.filler {
  flex: 1;
  height: 2px;
  opacity: .6;
  background: repeating-linear-gradient(90deg, var(--paper) 0 2px, transparent 2px 5px);
}

.lang-toggle {
  margin-left: auto;
  display: flex;
  border: var(--border);
  cursor: pointer;
  user-select: none;
  line-height: 1.1;
}

.lang-toggle__opt {
  padding: 2px 8px;
  background: var(--ink);
  color: var(--dim);
}

.lang-toggle__opt--sr {
  border-left: var(--border);
}

/* The active half is chosen entirely by the attribute, so JS never writes a
   CSS string — it flips one dataset value. */
.lang-toggle[data-lang="en"] .lang-toggle__opt--en,
.lang-toggle[data-lang="sr"] .lang-toggle__opt--sr {
  background: var(--paper);
  color: var(--ink);
}

/* — Boot log ------------------------------------------------------------- */

.boot {
  display: flex;
  flex-direction: column;
  padding-top: 14px;
  font-size: 12px;
  color: var(--dim);
}

/* — Hero ----------------------------------------------------------------- */

.hero {
  padding-top: 8px;
}

.arch {
  position: relative;
  height: min(46vh, 340px);
  display: grid;
  place-items: end center;
  overflow: hidden;
  animation: drift 11s ease-in-out infinite;
}

@keyframes drift {

  0%,
  100% {
    transform: translateY(0) scale(1);
  }

  50% {
    transform: translateY(-8px) scale(1.015);
  }
}

.arch__fill,
.arch__outline {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  width: min(78%, 520px);
  height: 100%;
  /* The only radius on the page. */
  border-radius: min(38%, 200px) min(38%, 200px) 0 0;
}

/* A luminance ramp punched through a 3px dot grid, then contrast-crushed.
   That last step is what makes the dots read as varying density rather than
   varying grey — i.e. genuinely 1-bit rather than a blurred gradient. */
.arch__fill {
  background-image: radial-gradient(ellipse 70% 85% at 50% 88%,
      var(--paper) 0%, var(--dim2) 34%, var(--rule) 58%, var(--ink) 78%);
  -webkit-mask-image: radial-gradient(circle at 1px 1px, #000 1.05px, transparent 1.15px);
  mask-image: radial-gradient(circle at 1px 1px, #000 1.05px, transparent 1.15px);
  -webkit-mask-size: 3px 3px;
  mask-size: 3px 3px;
  filter: contrast(2.6);
}

.arch__outline {
  border: var(--border);
  border-bottom: 0;
}

.arch__door {
  position: absolute;
  left: 50%;
  bottom: 14%;
  transform: translateX(-50%);
  width: 26px;
  height: 26px;
  background: var(--ink);
  border: var(--border);
}

/* Arch + wordmark are one link into #cepachina. It must not pick up the
   sitewide a:hover inversion — that would flip 340px of decoration to white
   and the arch's own white borders would vanish into it. The far door lighting
   up is the affordance instead. */
.hero-link {
  display: block;
  text-align: center;
}

.hero-link:hover {
  background: none;
  color: inherit;
}

.hero-link:hover .arch__door {
  background: var(--paper);
}

.hero__body {
  text-align: center;
}

.wordmark {
  margin-top: -6px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(54px, 10vw, 124px);
  line-height: .86;
  letter-spacing: -.02em;
}

.kicker {
  margin-top: 6px;
  font-size: 13px;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--dim2);
}

.hero__tagline {
  max-width: 44ch;
  margin: 16px auto 0;
  line-height: 1.7;
}

/* — Boxes ---------------------------------------------------------------- */

.box {
  border: var(--border);
}

.box--padded {
  padding: 16px 18px;
}

.box--padded p+p {
  margin-top: 12px;
}

.box__title {
  padding: 2px 10px;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 15px;
  text-transform: lowercase;
}

/* Notched corner on the nav group titles. */
.box__title--notched {
  clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 100%, 0 100%);
}

/* — Nav ------------------------------------------------------------------ */

.nav {
  padding-top: 34px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.box__items {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px;
}

.nav-item {
  flex: 1 1 auto;
  min-width: 150px;
  border: var(--hairline);
  padding: 4px 10px;
  display: flex;
  gap: 8px;
  align-items: baseline;
  font-family: var(--font-display);
  font-size: 15px;
}

.nav-item__note {
  margin-left: auto;
  font-family: var(--font-body);
  font-size: 11px;
  text-transform: uppercase;
}

/* — Section headings ----------------------------------------------------- */

.section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}

.section-head__label {
  font-family: var(--font-display);
  font-size: 22px;
  text-transform: lowercase;
}

/* Pixelify Sans has no box-drawing glyphs at all, so the ▚ has to come from
   the body font — DotGothic16 does carry it. Left in the display stack it
   falls through to whatever monospace the OS picks, which is not the same
   shape. */
.section-head__label::before {
  content: "▚ ";
  font-family: var(--font-body);
}

.section-head__rule {
  flex: 1;
  height: 2px;
  background: var(--paper);
  opacity: .5;
}

.lede {
  max-width: 52ch;
  margin-bottom: 18px;
}

.lede--sm {
  font-size: 13px;
  margin-bottom: 16px;
}

.note {
  margin-bottom: 14px;
  font-size: 12px;
}

/* — readme --------------------------------------------------------------- */

.signoff {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 2px dotted var(--rule);
  font-size: 12px;
}

.now {
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 13px;
}

.now__row {
  display: flex;
  gap: 10px;
}

.now__label {
  width: 72px;
  flex: none;
}

.dither-band {
  height: 44px;
  border-top: var(--border);
  background-image: var(--dither-check);
  background-size: 4px 4px;
  opacity: .5;
}

/* — Screenshots ----------------------------------------------------------
   Every shot is an <img> under a halftone overlay. The treatment lives here
   rather than in the files, so a screenshot can be swapped without a
   rebuild. Contrast and brightness are pushed harder than a normal photo
   would need — these are very dark dungeon shots and go to mud otherwise. */

.shot {
  position: relative;
  border: var(--border);
  overflow: hidden;
}

/* Two exposures, because the screenshots are not one population.
   The base is the handoff's treatment and suits anything shot at normal
   brightness. Game captures are lit like a dungeon — that is the point — and
   go to a flat black rectangle under it, so they carry .shot--raw, which turns
   the whole treatment off: a dithered screenshot tells people the game looks
   1-bit, and it does not. Everything else — the devlog screenshot, anything
   that is about the site rather than the game — keeps the base filter. */
.shot img {
  filter: grayscale(1) contrast(1.55) brightness(1.05);
}

/* Shown as captured. Both halves have to go together — grayscale off but
   halftone on is dotted colour, which is worse than either. */
.shot--raw img {
  filter: none;
}

.shot--raw .shot__halftone {
  display: none;
}

/* Covers the whole figure and opens the lightbox. A real <button> so it is
   focusable and answers Enter and Space without any JS of its own. */
.shot__zoom {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: start end;
  padding: 6px;
  border: 0;
  background: none;
  cursor: crosshair;
  font: inherit;
  color: inherit;
}

/* A plus in a box, in the corner. Hidden until the shot is hovered or the
   button is tabbed to, so it never sits on top of the screenshot uninvited.
   A `+` rather than a zoom glyph because it exists in every font. */
.shot__zoom::after {
  content: "+";
  padding: 0 6px;
  border: var(--hairline);
  background: var(--ink);
  font-family: var(--font-display);
  font-size: 15px;
  line-height: 1.4;
  opacity: 0;
}

.shot:hover .shot__zoom::after,
.shot__zoom:focus-visible::after {
  opacity: 1;
}

.shot__halftone {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--dither-halftone);
  background-size: 3px 3px;
  opacity: .55;
  mix-blend-mode: multiply;
}

.shot--banner {
  aspect-ratio: 16 / 7;
  margin-bottom: 10px;
}

.shot--thumb {
  aspect-ratio: 4 / 3;
}

.shot--post {
  aspect-ratio: 4 / 3;
}

.grid-shots {
  margin-bottom: 22px;
}

/* — specs ---------------------------------------------------------------- */

.spec {
  padding: 6px 12px 10px;
  font-size: 13px;
}

.spec__row {
  display: flex;
  gap: 10px;
  padding: 3px 0;
  border-bottom: 1px dotted var(--rule);
}

.spec__row:last-child {
  border-bottom: 0;
}

.spec dt {
  flex: 1;
  text-transform: lowercase;
}

.spec dd {
  margin: 0;
  text-align: right;
}

/* — Features + CTA ------------------------------------------------------- */

/* Bottom spacing is opt-in — the readme grid butts straight up against
   the next section, the cepachina blocks stack inside one. */
.block {
  margin-bottom: 22px;
}

.feature h3 {
  font-family: var(--font-display);
  font-size: 19px;
  margin-bottom: 2px;
}

.feature p {
  font-size: 13px;
}

.cta {
  display: flex;
  gap: 18px;
  align-items: center;
  flex-wrap: wrap;
  padding: 16px 18px;
  background-image: var(--dither-hatch);
}

.cta__copy {
  flex: 1;
  min-width: 220px;
}

.cta__title {
  font-family: var(--font-display);
  font-size: 24px;
}

.cta__copy p {
  margin-top: 2px;
  font-size: 13px;
}

.btn {
  border: var(--border);
  padding: 7px 16px;
  font-family: var(--font-display);
  font-size: 17px;
}

/* — Devlog --------------------------------------------------------------- */

.posts {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.post__head {
  display: flex;
  gap: 12px;
  align-items: baseline;
  padding: 7px 12px;
  cursor: pointer;
}

.post__mark {
  font-family: var(--font-display);
  font-size: 17px;
  width: 20px;
  flex: none;
}

.post__date {
  font-size: 12px;
  width: 62px;
  flex: none;
}

.post__title {
  font-family: var(--font-display);
  font-size: 18px;
  flex: 1;
}

.tag {
  font-size: 11px;
  border: var(--hairline);
  padding: 0 7px;
  white-space: nowrap;
  text-transform: uppercase;
}

.post__body {
  border-top: var(--border);
  padding: 14px 16px;
  display: grid;
  grid-template-columns: 1fr 1.3fr;
  gap: 16px;
  align-items: start;
}

/* [hidden] loses to display:grid without this. */
.post__body[hidden] {
  display: none;
}

.post__body p+p {
  margin-top: 12px;
}

.post__foot {
  margin-top: 12px;
  font-size: 11px;
}

/* — Elsewhere ------------------------------------------------------------ */

.link-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.link-row {
  border: var(--border);
  padding: 10px 14px;
  display: flex;
  gap: 14px;
  align-items: baseline;
  flex-wrap: wrap;
}

.link-row__name {
  font-family: var(--font-display);
  font-size: 21px;
}

.link-row__desc {
  font-size: 13px;
  flex: 1;
  min-width: 180px;
}

/* — Lightbox -------------------------------------------------------------
   A modal <dialog>, so it renders in the top layer: it is above the cursor
   trail and the scanlines whatever their z-index says. */

.lightbox {
  padding: 0;
  border: 0;
  background: none;
  max-width: 100vw;
  max-height: 100dvh;
  /* dialog is display:none until open; this is the layout it gets when it is,
     and it fills the viewport so a click anywhere off the frame closes it. */
  width: 100vw;
  height: 100dvh;
}

.lightbox[open] {
  display: grid;
  place-items: center;
}

.lightbox::backdrop {
  background: rgba(0, 0, 0, .9);
}

.lightbox__frame {
  border: var(--border);
  background: var(--ink);
  max-width: min(1120px, 94vw);
}

.lightbox__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 2px 4px 2px 10px;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 15px;
}

.lightbox__count {
  margin-right: auto;
}

/* Sits on the white title bar, so its resting state is the inverse of every
   other control on the page and .inv:hover flips it back to white on black. */
.lightbox__x {
  border: 0;
  padding: 0 8px;
  background: none;
  color: inherit;
  font: inherit;
  cursor: crosshair;
}

.lightbox__stage {
  position: relative;
  display: grid;
  place-items: center;
}

.lightbox__img {
  display: block;
  width: auto;
  height: auto;
  max-width: min(1116px, 94vw);
  /* Leaves room for the bar and the caption inside the viewport. */
  max-height: calc(100dvh - 150px);
  object-fit: contain;
}

.lightbox__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 34px;
  padding: 10px 0;
  border: var(--border);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1;
  cursor: crosshair;
}

.lightbox__nav--prev {
  left: 10px;
}

.lightbox__nav--next {
  right: 10px;
}

.lightbox__caption {
  border-top: var(--border);
  padding: 6px 10px;
  font-size: 12px;
}

/* Scroll lock while the dialog is open — showModal() does not do this. */
.has-modal {
  overflow: hidden;
}

/* — Footer --------------------------------------------------------------- */

.site-footer {
  padding-top: 44px;
  padding-bottom: 40px;
}

.dither-rule {
  height: 30px;
  margin-bottom: 16px;
  background-image: var(--dither-dots);
  background-size: 4px 4px;
  opacity: .45;
}

.site-footer__row {
  display: flex;
  gap: 16px;
  align-items: baseline;
  flex-wrap: wrap;
  font-size: 12px;
}

.site-footer__links {
  margin-left: auto;
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}

/* — The square in the footer ---------------------------------------------
   Sized so the cursor trail's largest node (11px) sits inside it with a
   couple of pixels to spare: park the pointer here and the trail lands
   squarely in the box, which is the whole invitation. app.js does the rest.
   Revealed by app.js only when the trail is running, so it never appears on
   touch or under reduced motion where there would be nothing to catch. */

.egg {
  position: relative;
  align-self: center;
  flex: none;
  width: 19px;
  height: 19px;
  border: var(--border);
  display: grid;
  place-items: center;
}

.egg[hidden] {
  display: none;
}

/* The hint — a 3px dot dead centre, only while hovered. Aiming at it puts
   the trail in the middle of the box. */
.egg::after {
  content: "";
  width: 3px;
  height: 3px;
  background: var(--paper);
  opacity: 0;
}

.egg:hover::after {
  opacity: .5;
}

/* One per swallowed node. steps() rather than a smooth ease — everything
   else on this page moves in whole pixels. */
@keyframes egg-bite {
  from {
    opacity: 1;
    transform: scale(2.4);
  }

  to {
    opacity: 0;
    transform: scale(1);
  }
}

.egg--bite::after {
  animation: egg-bite 200ms steps(4);
}

/* The gulp, once the last one is down. */
.egg--full {
  background: var(--paper);
}

/* ==========================================================================
   7. Responsive — one breakpoint, and everything that changes at it
   ========================================================================== */

@media (max-width: 760px) {
  :root {
    --gutter: 16px;
  }

  .grid-2,
  .grid-2--wide,
  .grid-3,
  .grid-shots,
  .post__body {
    grid-template-columns: 1fr;
  }

  .wordmark {
    font-size: clamp(44px, 17vw, 90px);
  }

  /* Scanlines and the header's filler rule are noise at this width. */
  .fx-scanlines,
  .filler {
    display: none;
  }
}

/* ==========================================================================
   Reduced motion — the caret, the hero drift and the scramble all stop.
   app.js checks the same query and skips the cursor trail and the scramble
   entirely; this only covers the CSS animations.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {

  html {
    scroll-behavior: auto;
  }

  .caret,
  .arch {
    animation: none;
  }
}
