@charset "utf-8";

/* ─────────────────────────────────────────────────────────────
   Dusk, 19:00 — Our Love Story

   One screen, no scrolling, built on a single horizon line:
   above it the story (the letter, and the heart blooming on the
   skyline), below it this moment (the vow, and the machine still
   counting). The sun sets behind the heart, so the light is
   anchored to the heart and scales with it.
   ───────────────────────────────────────────────────────────── */

@font-face {
  font-family: "Instrument Serif";
  src: url("InstrumentSerif-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Instrument Serif";
  src: url("InstrumentSerif-Italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("IBMPlexMono-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("IBMPlexMono-Medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Digital 7";
  src: url("digital-7_mono.woff2") format("woff2"),
    url("digital-7_mono.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  /* block, not swap: the dead segments hold the width of the counter, and
     a fallback would paint a box ~10px wider and then jump */
  font-display: block;
}

:root {
  /* sky */
  --zenith: #0d0f2b;
  --violet: #241a48;
  /* ink */
  --paper: #f4e6d9;
  --haze: #a695bc;
  --gold: #e9ba80;
  --rule: rgba(244, 230, 217, 0.22);
  /* syntax, drawn from the same sky — dusty, not candied */
  --kw: #a793d4;
  --str: #dfa268;
  --fn: #d98692;

  --display: "Instrument Serif", "Iowan Old Style", Georgia, "Songti SC", serif;
  --sans: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --led: "Digital 7", var(--mono);

  --gutter: clamp(1rem, 4vw, 3.5rem);
  /* where the drawn horizon actually lands; love.js keeps it honest */
  --horizon: 62%;
}

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

body {
  margin: 0;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: var(--zenith);
  color: var(--paper);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 4px;
  border-radius: 2px;
}

/* ── the sky ───────────────────────────────────────────────── */

.sky {
  position: fixed;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    180deg,
    var(--zenith) 0%,
    #141539 30%,
    var(--violet) 58%,
    #33224c 78%,
    #3d2449 100%
  );
}

.sky__glow {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 7s ease-out;
  /* the ground first, so it stays dark under the light, then a low haze
     along the skyline — both hung off the line that is actually drawn */
  background:
    /* the ground catches a little of it back, so the land is land and
       not a black band */
    radial-gradient(
        var(--sun-w, 30vw) calc(var(--sun-h, 12vh) * 0.5) at var(--sun-vx, 50%) calc(var(--horizon) + 4%),
        rgba(255, 196, 148, 0.13) 0%,
        rgba(255, 196, 148, 0) 72%
      ),
    linear-gradient(
      180deg,
      rgba(8, 9, 28, 0) calc(var(--horizon) - 7%),
      rgba(9, 9, 28, 0.3) calc(var(--horizon) - 1%),
      rgba(8, 8, 26, 0.72) calc(var(--horizon) + 1.5%),
      rgba(6, 7, 22, 0.95) calc(var(--horizon) + 10%),
      rgba(6, 7, 22, 1) 100%
    ),
    /* the sun itself, sitting on the horizon behind the heart. It lives
       in the sky rather than on the heart so it cannot push the page. */
      radial-gradient(
        var(--sun-w, 30vw) var(--sun-h, 12vh) at var(--sun-vx, 50%) var(--horizon),
        rgba(255, 216, 170, 0.85) 0%,
        rgba(255, 184, 132, 0.34) 38%,
        rgba(198, 98, 86, 0) 72%
      ),
    linear-gradient(
      180deg,
      rgba(198, 98, 86, 0) calc(var(--horizon) - 28%),
      rgba(198, 98, 86, 0.26) calc(var(--horizon) - 8%),
      rgba(255, 196, 148, 0.2) calc(var(--horizon) - 2%),
      rgba(198, 98, 86, 0) calc(var(--horizon) + 10%)
    );
}

.is-lit .sky__glow {
  opacity: 1;
}

/* A dark gradient this wide bands badly on most screens. A little grain
   dithers it away and gives the sky the tooth of printed paper. */
.sky::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.045;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}

/* the first stars, only in the upper sky, only just visible — at 19:00
   in September they are the ones you can already see */
.sky::after {
  content: "";
  position: absolute;
  /* never below the line, and never down among the letter on wide screens */
  inset: 0 0 calc(100% - min(var(--horizon), 55%)) 0;
  opacity: 0;
  transition: opacity 9s ease-out 1.2s;
  background-image:
    radial-gradient(2px 2px at 12% 18%, rgba(255, 246, 230, 0.85), transparent 62%),
    radial-gradient(1.4px 1.4px at 27% 9%, rgba(230, 236, 255, 0.6), transparent 62%),
    radial-gradient(1.6px 1.6px at 39% 26%, rgba(255, 246, 230, 0.5), transparent 62%),
    radial-gradient(1.4px 1.4px at 52% 13%, rgba(226, 232, 255, 0.68), transparent 62%),
    radial-gradient(2.2px 2.2px at 63% 31%, rgba(255, 242, 224, 0.72), transparent 62%),
    radial-gradient(1.3px 1.3px at 71% 7%, rgba(255, 246, 230, 0.45), transparent 62%),
    radial-gradient(1.7px 1.7px at 84% 21%, rgba(230, 236, 255, 0.62), transparent 62%),
    radial-gradient(1.4px 1.4px at 93% 12%, rgba(255, 246, 230, 0.5), transparent 62%),
    radial-gradient(1.3px 1.3px at 6% 34%, rgba(226, 232, 255, 0.42), transparent 62%),
    radial-gradient(1.8px 1.8px at 45% 40%, rgba(255, 246, 230, 0.36), transparent 62%),
    radial-gradient(1.3px 1.3px at 77% 43%, rgba(230, 236, 255, 0.32), transparent 62%),
    radial-gradient(1.2px 1.2px at 19% 47%, rgba(255, 246, 230, 0.26), transparent 62%),
    radial-gradient(1.5px 1.5px at 33% 4%, rgba(255, 246, 230, 0.4), transparent 62%),
    radial-gradient(1.2px 1.2px at 58% 22%, rgba(226, 232, 255, 0.3), transparent 62%);
  pointer-events: none;
}

.is-lit .sky::after {
  opacity: 1;
}

/* ── one screen ────────────────────────────────────────────── */

.screen {
  flex: 0 0 auto;
  width: 100%;
  min-height: 100svh;
  max-width: 96rem;
  margin-inline: auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "stamp"
    "heart"
    "rule"
    "meter"
    "letter"
    "vow";
  align-content: center;
  --row: clamp(0.4rem, 1.6svh, 1.2rem);
  gap: var(--row);
  padding: clamp(0.8rem, 2.4svh, 2rem) var(--gutter);
}


.screen__stamp {
  grid-area: stamp;
  text-align: center;
}

.heart {
  grid-area: heart;
  justify-self: center;
  /* the canvas reaches down into the row gap so the point can sit on the
     line rather than hover a gap above it */
  margin-bottom: calc(var(--row) * -1);
}

.horizon {
  grid-area: rule;
}

.meter {
  grid-area: meter;
}

.letter {
  grid-area: letter;
}

.vow {
  grid-area: vow;
}

.eyebrow {
  margin: 0;
  font-size: clamp(0.58rem, 1.3svh, 0.7rem);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--haze);
}

/* the pen: on whatever is being written, and gone once the letter is
   finished — a signed letter has no cursor sitting on it */
.is-typing::after {
  content: "";
  display: inline-block;
  width: 0.5ch;
  height: 0.78em;
  /* net zero width: the pen must not push a line into wrapping early,
     which would change the height of a block while it is being written */
  margin-left: 0.4ch;
  margin-right: -0.9ch;
  vertical-align: -0.04em;
  background: var(--gold);
  animation: blink 1.15s steps(1) infinite;
}

/* ── the heart, and the sun behind it ──────────────────────── */

.heart {
  position: relative;
  z-index: 1;
  width: min(74vw, calc(21svh * 1.09));
  aspect-ratio: 1 / 0.92;
}

.heart canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* the line everything is measured from: the heart rests on it, the code
   ends on it, and love.js reads its position into --horizon so the sky
   meets it exactly */
.horizon {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 1px;
  border: 0;
  margin: 0;
  transform: scaleX(0);
  transform-origin: 50% 50%;
  transition: transform 1.6s cubic-bezier(0.16, 1, 0.3, 1);
  /* A horizon is not a drawn line: it is only crisp where the light is,
     and dissolves into haze well before the edges of the picture. */
  background:
    radial-gradient(34% 100% at var(--sun-x, 50%) 50%, rgba(255, 220, 176, 0.95) 0%, rgba(255, 220, 176, 0) 100%),
    linear-gradient(
      90deg,
      rgba(244, 230, 217, 0) 2%,
      rgba(244, 230, 217, 0.1) 24%,
      rgba(244, 230, 217, 0.17) 50%,
      rgba(244, 230, 217, 0.1) 76%,
      rgba(244, 230, 217, 0) 98%
    );
}

.horizon.is-drawn {
  transform: scaleX(1);
}

/* ── the instrument ────────────────────────────────────────── */

.meter {
  position: relative;
  z-index: 1;
  text-align: center;
}

.meter__lede {
  margin: 0 0 0.4rem;
  text-wrap: balance;
  font-family: var(--display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.05rem, 2.9svh, 1.75rem);
  line-height: 1.25;
}

.clock {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.1rem 0.6rem;
}

.clock__unit {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
}

/* a real LED shows its unlit segments too. The dead segments hold the
   box, so switching the display on moves nothing. */
.led {
  position: relative;
  display: inline-block;
  font-family: var(--led);
  color: var(--gold);
  line-height: 1;
  text-shadow: 0 0 18px rgba(233, 186, 128, 0.45);
}

.led__ghost {
  opacity: 0.11;
  text-shadow: none;
  pointer-events: none;
}

.led__value {
  position: absolute;
  left: 0;
  top: 0;
}

.led--days {
  font-size: clamp(2.1rem, 6.4svh, 4rem);
}

.led--time {
  font-size: clamp(1.35rem, 4svh, 2.4rem);
}

.clock__label {
  font-size: clamp(0.52rem, 1.2svh, 0.62rem);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: rgba(244, 230, 217, 0.65);
}

/* ── the letter ────────────────────────────────────────────── */

.letter__title {
  margin: 0;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.7rem, 5svh, 3.2rem);
  line-height: 1;
  letter-spacing: -0.01em;
}

.letter__title em {
  font-style: italic;
  color: var(--gold);
}

.letter__intro {
  margin: 0.55rem 0 0;
  /* no orphans: never leave one word alone on the last line */
  text-wrap: pretty;
  max-width: 34rem;
  font-size: clamp(0.82rem, 1.9svh, 1.05rem);
  line-height: 1.6;
  color: rgba(244, 230, 217, 0.62);
}

/* code as typesetting: the machine on the right,
   the human in the margin on the left */
.lines {
  margin: clamp(0.8rem, 2.6svh, 2rem) 0 0;
  padding: 0;
  list-style: none;
}

.line {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.1rem;
  padding: 0.18rem 0;
}

.line__note {
  margin: 0;
  text-wrap: pretty;
  font-family: var(--display);
  font-style: italic;
  font-size: clamp(0.84rem, 1.95svh, 1.15rem);
  line-height: 1.3;
  color: var(--haze);
}

.line__date {
  display: inline;
  margin-right: 0.5rem;
  font-family: var(--sans);
  font-style: normal;
  font-size: clamp(0.5rem, 1.1svh, 0.6rem);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--gold);
}

.line__src {
  font-family: var(--mono);
  font-size: clamp(0.8rem, 2svh, 1.15rem);
  line-height: 1.55;
  color: rgba(244, 230, 217, 0.92);
  white-space: nowrap; /* a line of code is one line */
  padding-left: 0.9rem;
  border-left: 1px solid var(--rule);
}

.line--nested .line__src {
  text-indent: 2ch;
}

/* while the page is being written, a line's rule arrives with its text
   instead of hanging in the empty margin */
.writing .line__src {
  border-left-color: transparent;
}

.writing .line__src.is-typing,
.writing .line__src.is-written {
  border-left-color: var(--rule);
}

.kw {
  color: var(--kw);
  font-weight: 500;
}

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

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

/* ── below the line: the vow ───────────────────────────────── */

.vow {
  max-width: 30rem;
}

.vow__line {
  margin: 0;
  text-wrap: pretty;
  font-family: var(--display);
  font-size: clamp(1.15rem, 3.2svh, 2rem);
  line-height: 1.2;
}

/* the same stamp as the top of the page, at the other end of it —
   the same colour too, which is also what keeps it legible */
.vow__stamp {
  margin: 0.7rem 0 0;
  text-align: right;
}

.vow__sign {
  margin: 0.1rem 0 0;
  font-family: var(--display);
  font-style: italic;
  font-size: clamp(0.9rem, 2.2svh, 1.3rem);
  color: var(--gold);
  text-align: right;
}

/* ── phones: same screen, tighter ──────────────────────────
   Only the two dated milestones keep their margin note; the rest
   of the voice would cost a screenful. */

@media (max-width: 59.99rem) and (min-height: 32.01rem) {
  .screen {
    --row: clamp(0.28rem, 1.1svh, 0.9rem);
    padding-block: clamp(0.5rem, 1.7svh, 1.4rem);
  }

  .vow__stamp {
    margin-top: 0.3rem;
  }

  .led--days {
    font-size: clamp(1.7rem, 5.4svh, 3rem);
  }

  .led--time {
    font-size: clamp(1.1rem, 3.4svh, 1.9rem);
  }

  .meter__lede {
    font-size: clamp(0.88rem, 2.2svh, 1.2rem);
  }

  .letter__title {
    font-size: clamp(1.5rem, 4.2svh, 2.4rem);
  }

  .letter__intro {
    margin-top: 0.35rem;
    font-size: clamp(0.7rem, 1.5svh, 0.85rem);
    line-height: 1.32;
  }

  .lines {
    margin-top: clamp(0.4rem, 1.3svh, 0.9rem);
  }

  .line {
    padding: 0.06rem 0;
  }

  .line__src {
    font-size: clamp(0.78rem, 1.85svh, 1rem);
    line-height: 1.42;
  }

  .line__note {
    font-size: clamp(0.8rem, 1.9svh, 1.05rem);
    line-height: 1.25;
  }

  .line:not(.line--dated) .line__note {
    display: none;
  }

  /* a milestone gets a breath before it */
  .line--dated {
    padding-top: 0.3rem;
  }

  .vow {
    max-width: none;
  }

  .vow__line {
    font-size: clamp(1.05rem, 2.6svh, 1.5rem);
  }

  .vow__sign {
    font-size: clamp(0.85rem, 1.9svh, 1.1rem);
  }

}

/* a tablet in portrait has room to spare: the drawing takes it */
@media (min-width: 40rem) and (max-width: 59.99rem) and (min-height: 45rem) {
  .heart {
    width: min(58vw, calc(30svh * 1.09));
  }
}

/* short phones: the code and the vow, nothing else */
@media (max-width: 59.99rem) and (max-height: 45rem) {
  .letter__intro {
    display: none;
  }
}

/* anything short — a phone held sideways, a small laptop in a short
   window — drops the prose and keeps only the dated notes */
@media (max-height: 34rem) {
  .letter__intro {
    display: none;
  }

  .line:not(.line--dated) .line__note {
    display: none;
  }

  .line {
    padding: 0.04rem 0;
  }

  .vow__stamp {
    margin-top: 0.25rem;
  }
}

/* ── two columns, split by the horizon ─────────────────────── */

@media (min-width: 60rem), (min-width: 40rem) and (max-height: 32rem) {
  .screen {
    /* the heart column is wide enough that the drawing fills its height
       and its top lands with the first line of code */
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto auto;
    grid-template-areas:
      "stamp    stamp"
      "letter   heart"
      "rule     rule"
      "vow      meter";
    align-content: stretch;
    --row: clamp(0.5rem, 1.8svh, 1.4rem);
    column-gap: clamp(2rem, 6vw, 6rem);
    row-gap: var(--row);
  }

  .screen__stamp {
    text-align: left;
  }

  /* the story fills the sky above the line: the salutation at the top,
     the code resting on the horizon, the heart drawn across the rest */
  .letter {
    align-self: stretch;
    min-height: 0; /* let the row shrink to the screen, not to the text */
    display: flex;
    flex-direction: column;
  }

  /* the slack in the column is spread through the code's leading
     instead of collecting as one hole above it */
  .letter .lines {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }

  .heart {
    align-self: stretch;
    width: 100%;
    aspect-ratio: auto; /* the row decides its height */
  }

  /* the vow and the counter's first line share one baseline */
  .vow,
  .meter {
    align-self: baseline;
  }


  /* the margin is a column of its own, so the notes are sized by that
     column rather than by the viewport — that is what keeps every note
     on one line and the code block on an even rhythm */
  .lines {
    container-type: inline-size;
  }

  .line {
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    gap: 0 clamp(1rem, 2.5vw, 2rem);
    align-items: baseline;
  }

  /* the margin is the quieter voice: smaller than the code it annotates,
     and never touching the rule that separates them */
  .line__note {
    font-size: clamp(0.72rem, 2.15cqi, 1rem);
    text-align: right;
    padding-right: 0.7em;
  }

  .line__src {
    /* the same idea for the code: never wider than its own column */
    font-size: min(clamp(0.8rem, 2.3svh, 1.3rem), 2.6cqi);
    padding-left: clamp(1rem, 2.5vw, 2rem);
    margin-left: calc(clamp(1rem, 2.5vw, 2rem) * -1);
  }
}

/* a tall desktop screen: the composition keeps a sane height and the
   sky frames it, instead of stretching the heart column past the page */
@media (min-width: 60rem) and (min-height: 56rem) {
  .screen {
    min-height: min(100svh, 62rem);
  }
}

/* a phone held sideways: two columns, but everything has to give */
@media (max-height: 34rem) {
  .screen {
    --row: clamp(0.25rem, 1.5svh, 0.7rem);
    padding-block: clamp(0.4rem, 2svh, 1rem);
  }

  .letter__title {
    font-size: clamp(1.1rem, 5.5svh, 1.6rem);
  }

  /* there is no room for a margin: the code takes the whole column,
     which is the difference between 7px type and readable type */
  .line__note {
    display: none;
  }

  .line {
    grid-template-columns: minmax(0, 1fr);
  }

  .line__src {
    font-size: min(clamp(0.68rem, 2.4svh, 1rem), 4.8cqi);
    line-height: 1.3;
    padding-left: 0.8rem;
    margin-left: 0;
  }

  .meter__lede {
    font-size: clamp(0.8rem, 3.6svh, 1.1rem);
  }

  .led--days {
    font-size: clamp(1.5rem, 8svh, 2.4rem);
  }

  .led--time {
    font-size: clamp(1rem, 5svh, 1.6rem);
  }

  .vow__line {
    font-size: clamp(0.95rem, 4.6svh, 1.4rem);
  }

  .vow__sign {
    font-size: clamp(0.8rem, 3.4svh, 1rem);
  }
}

/* ── motion ────────────────────────────────────────────────── */

@keyframes blink {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}

/* ── on paper ──────────────────────────────────────────────
   Browsers do not print backgrounds, so the page as designed would
   print as light ink on white — that is, blank. This is the same
   letter set for paper: dark ink, no sky, the drawing inverted. */

@media print {
  body {
    background: #fff;
    color: #1b1b2e;
  }

  .sky {
    display: none;
  }

  .screen {
    min-height: auto;
    max-width: none;
  }

  /* the ink was drawn light on nothing; on paper it has to be dark */
  .heart canvas {
    filter: invert(1) hue-rotate(180deg);
  }

  .horizon {
    transform: none;
    background: #b9b3c4;
  }

  .letter__title,
  .meter__lede,
  .vow__line,
  .line__src {
    color: #1b1b2e;
  }

  .letter__intro {
    color: #4a4658;
  }

  .eyebrow,
  .line__note {
    color: #5c5670;
  }

  .letter__title em,
  .vow__sign,
  .line__date,
  .clock__label {
    color: #8a5a1e;
  }

  .kw {
    color: #5b3fa0;
  }

  .str {
    color: #8a5a1e;
  }

  .fn {
    color: #a03f52;
  }

  .led {
    color: #1b1b2e;
    text-shadow: none;
  }

  .led__ghost {
    opacity: 0.1;
  }
}

/* Nothing is written out under "reduce motion": love.js puts the whole
   letter on the page at once and these transitions are switched off.
   ?motion=full opts back in; see love.js */
@media (prefers-reduced-motion: reduce) {
  html:not(.motion) .sky__glow,
  html:not(.motion) .horizon {
    transition: none;
  }

  html:not(.motion) .is-typing::after {
    animation: none;
  }
}
