/* ==========================================================================
   上海情绪地图 · Components (v0.1)
   Each class below is shown live in design-system/index.html.
   Build new UI from these pieces; add new pieces here first, then use them.
   ========================================================================== */
@import url("./tokens.css");

/* ---- Page ground: watercolour paper ------------------------------------- */
.ds-paper {   /* paper surfaces: cards, postcards. Not for whole pages. */
  background-color: var(--paper);
  background-image: var(--tex-paper);
  background-size: 512px;
  color: var(--ink);
  font-family: var(--font-body);
}

/* ---- Page ground: tinted watercolour light + paper grain + floating washes ----
   Use one of .ds-ground--garden / --dusk / --river on <body>. The background
   scene (the window desk) sits on top of this as a line drawing.            */
.ds-ground {
  color: var(--ink); font-family: var(--font-body);
  background: var(--tex-paper) 0 0 / 512px, var(--ground, var(--ground-garden));
  background-blend-mode: multiply, normal;
  min-height: 100vh;
}
.ds-ground--garden { --ground: var(--ground-garden); }
.ds-ground--dusk   { --ground: var(--ground-dusk); }
.ds-ground--river  { --ground: var(--ground-river); }

/* Sketch frame: a hand-drawn double pen line around a box (no solid borders). */
.ds-sketch-frame { position: relative; }
.ds-sketch-frame::after {
  content: ""; position: absolute; inset: -3px; pointer-events: none;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M1.5 2.2 C 30 1.2, 70 2.6, 98.4 1.6 C 98.9 30, 97.8 70, 98.6 98.2 C 70 99.1, 30 97.9, 1.8 98.6 C 1 70, 2.3 30, 1.5 2.2' fill='none' stroke='%233B3226' stroke-opacity='.5' stroke-width='1.1' vector-effect='non-scaling-stroke'/%3E%3Cpath d='M2.4 1.4 C 35 2.4, 65 1.1, 97.6 2.6 C 98.4 35, 99 65, 97.7 97.4 C 65 98.3, 35 99, 2.6 97.6 C 1.6 65, 1 35, 2.4 1.4' fill='none' stroke='%233B3226' stroke-opacity='.18' stroke-width='.9' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
    center / 100% 100% no-repeat;
}

/* ---- Painted object sticker -------------------------------------------
   For every story object (PNG with transparent background).
   White die-cut edge + soft shadow; lifts and tilts on hover.            */
.ds-sticker {
  display: inline-block; cursor: pointer; border: 0; padding: 0; background: none;
  filter:
    drop-shadow(0 var(--sticker-edge) 0 #FFFDF7) drop-shadow(0 calc(var(--sticker-edge) * -1) 0 #FFFDF7)
    drop-shadow(var(--sticker-edge) 0 0 #FFFDF7) drop-shadow(calc(var(--sticker-edge) * -1) 0 0 #FFFDF7)
    drop-shadow(var(--shadow-rest));
  transform: rotate(var(--tilt, -4deg));
  transition: transform var(--dur-base) var(--ease-spring), filter var(--dur-base) var(--ease-paper);
}
.ds-sticker:hover, .ds-sticker:focus-visible {
  transform: rotate(calc(var(--tilt, -4deg) * -0.5)) translateY(-6px) scale(1.06);
  filter:
    drop-shadow(0 var(--sticker-edge) 0 #FFFDF7) drop-shadow(0 calc(var(--sticker-edge) * -1) 0 #FFFDF7)
    drop-shadow(var(--sticker-edge) 0 0 #FFFDF7) drop-shadow(calc(var(--sticker-edge) * -1) 0 0 #FFFDF7)
    drop-shadow(var(--shadow-lift));
  outline: none;
}
.ds-sticker img, .ds-sticker svg { display: block; width: 100%; height: auto; }
/* Small sticker: objects shown small, e.g. on the map preview before zooming in; a thinner die-cut edge. */
.ds-sticker--mini { --sticker-edge: 2.5px; }

/* ---- Dog-ear: a paper corner turned up, the handle for folding paper back ----
   Put it in a corner of a paper surface: <button class="ds-dogear"></button>. */
.ds-dogear {
  position: absolute; top: 0; right: 0; width: 46px; height: 46px; border: 0; padding: 0; cursor: grab;
  background:
    linear-gradient(225deg, transparent 50%, color-mix(in srgb, var(--ink) 22%, var(--paper-deep)) 50%, var(--paper-deep) 57%, var(--paper) 88%),
    var(--tex-paper) 0 0 / 512px;
  background-blend-mode: normal, multiply;
  clip-path: polygon(0 0, 100% 100%, 0 100%);   /* the turned-down flap; the corner it came from shows the desk */
  transform-origin: 0 100%;
  transition: scale var(--dur-base) var(--ease-spring);
}
.ds-dogear:hover, .ds-dogear:focus-visible { scale: 1.18; outline: none; }

/* ---- Handwritten note with an arrow ------------------------------------ */
.ds-note {
  font-family: var(--font-hand); font-size: var(--text-hand); line-height: 1; color: var(--ink);
  display: inline-flex; align-items: center; gap: var(--space-2); transform: rotate(-5deg);
}
.ds-note .zh { font-family: var(--font-title); font-size: calc(var(--text-hand) * .95); }
.ds-note svg { width: 54px; height: 26px; overflow: visible; animation: ds-nudge 1.4s var(--ease-paper) infinite; }
@keyframes ds-nudge { 50% { transform: translateX(7px) rotate(3deg); } }
/* Arrow on the left, pointing back at a target to the left of the note:
   <span class="ds-note ds-note--left"><svg>…arrow…</svg><span class="zh">拉一下</span> pull</span> */
.ds-note--left svg { scale: -1 1; }

/* ---- Hanging tag: used for 关闭 and small actions -------------------------
   Markup: <button class="ds-tag"><svg class="ds-tag__art">…</svg><span>× 关闭</span></button>
   The SVG (see index.html, symbol #tag-art) is a watercolour-washed paper tag
   with a sketchy outline and a loop of string.                               */
.ds-tag {
  position: relative; display: inline-grid; place-items: center; width: 124px; height: 66px;
  border: 0; background: none; padding: 0; cursor: pointer;
  font-family: var(--font-title); font-size: 23px; color: var(--ink);
  transform-origin: 50% -26px; transform: rotate(4deg);
  transition: transform var(--dur-base) var(--ease-spring);
}
.ds-tag__art { position: absolute; left: -8px; top: -30px; width: 140px; height: 104px; overflow: visible; filter: drop-shadow(2px 4px 4px rgba(110,70,40,.18)); }
.ds-tag span { position: relative; top: 4px; }
.ds-tag:hover, .ds-tag:focus-visible { transform: rotate(-4deg); outline: none; }
.ds-tag:focus-visible { outline: 1.5px dashed var(--line); outline-offset: 3px; border-radius: var(--radius-card); }   /* keyboard: the swing alone was too slight to see */

/* ---- Language switch: a painted teacup (中) that flips to a coffee mug (EN)
   Markup: <button class="ds-lang" data-lang="zh"><svg class="face zh">…teacup…</svg><svg class="face en">…mug…</svg></button> */
.ds-lang {
  position: relative; width: 84px; height: 84px; border: 0; padding: 0; background: none; cursor: pointer;
  perspective: 500px;
}
.ds-lang .flip { position: absolute; inset: 0; transform-style: preserve-3d; transition: transform var(--dur-slow) var(--ease-spring); }
.ds-lang .face { position: absolute; inset: 0; width: 100%; height: 100%; backface-visibility: hidden; overflow: visible; }
.ds-lang .face.en { transform: rotateY(180deg); }
.ds-lang[data-lang="en"] .flip { transform: rotateY(180deg); }
.ds-lang:hover .flip { translate: 0 -3px; }

/* ---- Speech bubble: a watercolour wash behind handwritten speech -------- */
.ds-bubble {
  position: relative; max-width: 20em; padding: var(--space-5) var(--space-6);
  font-family: var(--font-body); font-size: var(--text-body); line-height: 1.7; color: var(--ink);
  isolation: isolate;
}
.ds-bubble::before {
  content: ""; position: absolute; inset: -14px -18px; z-index: -1;
  background: var(--bubble-wash, url("./textures/wash-butter.webp")) center / 100% 100% no-repeat;
}
.ds-bubble::after {   /* the tail: a small trailing drop of the same wash */
  content: ""; position: absolute; left: 18%; bottom: -30px; width: 34px; height: 34px; z-index: -1;
  background: var(--bubble-wash, url("./textures/wash-butter.webp")) center / 100% 100% no-repeat;
  border-radius: 50%; transform: scale(.55) translate(-10px, 8px); opacity: .9;
}

/* ---- Emotion chip ------------------------------------------------------- */
.ds-chip {
  display: inline-flex; align-items: center; gap: var(--space-2); padding: 6px 18px 6px 14px;
  font-family: var(--font-title); font-size: 21px; color: var(--chip-ink, var(--ink));
  background: var(--chip-wash) center / 100% 100% no-repeat; border: 0; cursor: pointer;
  transition: transform var(--dur-quick) var(--ease-spring);
}
.ds-chip:hover { transform: translateY(-2px) rotate(-2deg); }
.ds-chip[aria-pressed="false"] { opacity: .4; filter: grayscale(.6); }

/* ---- Postcard: how every story opens --------------------------------------
   The object flies off the map and becomes the stamp in the corner of a postcard; the card comes to the
   middle of the window and settles straight. Closing, it flies back and the stamp becomes the object again.
   The story is real HTML text. Set the story's emotion on the card: --emo (the watercolour edge, the stamp's
   ground) and --emo-ink (the postmark, the stamp's word).
   Markup:
   <article class="ds-postcard" style="--emo: var(--emo-calm); --emo-ink: var(--emo-calm-ink)">
     <div class="ds-postcard__message"><h2 class="ds-postcard__name">…</h2><p class="ds-postcard__feel">…</p><p class="ds-postcard__body">…</p></div>
     <span class="ds-postcard__rule" aria-hidden="true"></span>
     <div class="ds-postcard__address">
       <span class="ds-postcard__stamp"><img src="…/<id>-sticker.webp" alt=""></span>
       <span class="ds-postcard__postmark" aria-hidden="true"><svg viewBox="0 0 200 100">…</svg><span><b>平静</b><i>上海 · 2026</i></span></span>
       <span class="ds-postcard__line ds-postcard__line--district">上海市 徐汇区</span><span class="ds-postcard__line">place</span><span class="ds-postcard__line">Story 6 of 31</span>
     </div>
   </article>
   The postmark's rings and wavy lines: see .ds-postcard__postmark in index.html (js/story.js writes the same).
   Add .is-still once the card has stopped moving (it shows the fade at the foot of a long story). */
.ds-postcard {
  position: relative; display: grid; grid-template-columns: minmax(0, 1.5fr) 8px minmax(0, 1fr); gap: var(--space-5);
  width: min(660px, 100%); aspect-ratio: 3 / 2; padding: calc(5.5% + var(--space-4)) calc(3.6% + var(--space-5));
  background: var(--paper) var(--tex-paper); background-size: 512px; border-radius: var(--radius-card); box-shadow: var(--shadow-lift);
  color: var(--ink); font-family: var(--font-body); text-align: left;
}
.ds-postcard::before {   /* the watercolour edge, in the story's colour, pigment pooled along its inner side */
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; opacity: .72;
  background: var(--tex-paper) 0 0 / 512px, var(--emo, var(--emo-moved)); background-blend-mode: multiply;
  -webkit-mask: url("./textures/postcard-edge.webp") center / 100% 100% no-repeat; mask: url("./textures/postcard-edge.webp") center / 100% 100% no-repeat;   /* tools/make_postcard_edge.py */
}
.ds-postcard > * { position: relative; min-height: 0; }
/* the story scrolls if it is long; its last line fades out instead of being cut, and can scroll clear of the fade */
.ds-postcard__message { overflow: auto; padding-right: var(--space-2); overscroll-behavior: contain; }
.ds-postcard__message::after {   /* a paper-coloured fade stuck to the bottom; only on a card at rest (.is-still) */
  content: ""; position: sticky; bottom: 0; display: none; height: var(--space-6); margin-top: calc(var(--space-6) * -.4);
  background: linear-gradient(to bottom, transparent, var(--paper)); pointer-events: none;
}
.ds-postcard.is-still .ds-postcard__message::after { display: block; }
.ds-postcard__message:focus-visible { outline: 1.5px dashed var(--line); outline-offset: 4px; }
.ds-postcard__name { margin: 0; font-family: var(--font-title); font-weight: 400; font-size: 36px; line-height: 1.1; }
.ds-postcard__feel { margin: var(--space-2) 0 var(--space-3); font-size: var(--text-small); color: var(--ink-soft); line-height: 1.6; }
.ds-postcard__body { margin: 0; font-size: 16px; line-height: 1.9; }
.ds-postcard__rule { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 200' preserveAspectRatio='none'%3E%3Cpath d='M3 2 C 2.2 50, 3.8 110, 2.8 198' fill='none' stroke='%233B3226' stroke-opacity='.45' stroke-width='1.1' vector-effect='non-scaling-stroke'/%3E%3Cpath d='M3.6 6 C 3 70, 4.2 130, 3.4 194' fill='none' stroke='%233B3226' stroke-opacity='.16' stroke-width='.9' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E") center / 100% 100% no-repeat; }   /* the hand-drawn line down the middle */
.ds-postcard__address { display: flex; flex-direction: column; justify-content: flex-end; gap: var(--space-3); }
.ds-postcard__stamp {   /* the object, as a stamp with a perforated edge */
  position: absolute; top: 0; right: 0; width: 46%; min-width: 92px; aspect-ratio: 5 / 6; rotate: 3deg;
}
.ds-postcard__stamp::before {
  content: ""; position: absolute; inset: 0; background: var(--paper); box-shadow: var(--shadow-rest);
  -webkit-mask: radial-gradient(circle at 50% 50%, transparent 2.4px, #000 2.7px) -5px -5px / 10px 10px; mask: radial-gradient(circle at 50% 50%, transparent 2.4px, %23000 2.7px) -5px -5px / 10px 10px;
}
.ds-postcard__stamp::after { content: ""; position: absolute; inset: 8%; background: color-mix(in srgb, var(--emo, var(--emo-moved)) 22%, var(--paper)); }
.ds-postcard__stamp img { position: absolute; z-index: 1; left: 3%; top: 3%; width: 94%; height: 94%; object-fit: contain; display: block; -webkit-user-drag: none; }
.ds-postcard__postmark {   /* the round postmark in the story's colour, struck across the stamp's corner */
  position: absolute; top: 24%; right: 30%; width: 62%; min-width: 118px; aspect-ratio: 2 / 1; z-index: 2;
  color: var(--emo-ink, var(--ink)); opacity: .78; rotate: -9deg; pointer-events: none;
}
.ds-postcard__postmark svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.ds-postcard__postmark > span {
  position: absolute; left: 3%; top: 6%; width: 44%; height: 88%; display: flex; flex-direction: column; align-items: center; justify-content: center;
  line-height: 1.05; text-align: center;
}
.ds-postcard__postmark b { font-family: var(--font-title); font-weight: 400; font-size: 21px; }
.ds-postcard__postmark i { font-style: normal; font-family: var(--font-hand); font-size: 13px; letter-spacing: .04em; }
:lang(en) .ds-postcard__postmark b { font-family: var(--font-hand); font-weight: 600; font-size: 19px; }
.ds-postcard__line { border-bottom: 1.2px dashed var(--line-ghost); padding-bottom: 4px; font-family: var(--font-hand); font-size: 20px; line-height: 1.3; }
/* Caveat has no Chinese: in Chinese the address lines are in the body font (站酷小薇), not a system fallback */
:lang(zh) .ds-postcard__line { font-family: var(--font-body); font-size: 18px; letter-spacing: .04em; }
/* the first address line, the same on every card: the story's district, in the brush font and the story's ink */
:lang(zh) .ds-postcard__line--district, .ds-postcard__line--district { font-family: var(--font-title); font-size: 21px; letter-spacing: .06em; color: var(--emo-ink, var(--ink)); }
:lang(en) .ds-postcard__line--district { font-family: var(--font-hand); font-weight: 600; font-size: 21px; letter-spacing: 0; }
:lang(zh) .ds-postcard__line--count, .ds-postcard__line--count { font-size: 16px; color: var(--ink-soft); }
@media (max-width: 640px) {   /* on phones the card stands upright: the story above, the address below */
  .ds-postcard { aspect-ratio: 3 / 4.6; grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 8px auto; gap: var(--space-4); padding: calc(4.5% + var(--space-4)) calc(5.5% + var(--space-3)); }
  .ds-postcard__rule { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 6' preserveAspectRatio='none'%3E%3Cpath d='M2 3 C 50 2.2, 110 3.8, 198 2.8' fill='none' stroke='%233B3226' stroke-opacity='.45' stroke-width='1.1' vector-effect='non-scaling-stroke'/%3E%3Cpath d='M6 3.6 C 70 3, 130 4.2, 194 3.4' fill='none' stroke='%233B3226' stroke-opacity='.16' stroke-width='.9' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E"); }
  .ds-postcard__address { padding-top: 116px; gap: var(--space-2); }   /* the stamp and postmark on top, the address under them */
  .ds-postcard__stamp { width: 88px; min-width: 0; }
  .ds-postcard__postmark { top: 26px; right: 62px; width: 116px; min-width: 0; }
  .ds-postcard__name { font-size: 30px; }
}

/* Scrim: the dim behind an open postcard, a light warm dusk over the map. */
.ds-scrim { position: fixed; inset: 0; background: var(--scrim); opacity: 0; transition: opacity var(--dur-base) var(--ease-paper); }
.ds-scrim.is-shown { opacity: 1; }

/* ---- Approximate place: a dashed watercolour ring under a sticker ---------
   For stories whose place is only roughly known. Put it under the sticker,
   centred on the place; --approx-color is the story's emotion colour.      */
.ds-approx {
  position: absolute; display: block; pointer-events: none; aspect-ratio: 5 / 2;
  background: var(--approx-color, var(--emo-mixed)); opacity: .8;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 60'%3E%3Cellipse cx='75' cy='30' rx='70' ry='25' fill='%23000' fill-opacity='.13'/%3E%3Cellipse cx='75' cy='30' rx='70' ry='25' fill='none' stroke='%23000' stroke-width='3.4' stroke-dasharray='10 8' stroke-linecap='round'/%3E%3Cellipse cx='75.8' cy='31' rx='68.5' ry='24' fill='none' stroke='%23000' stroke-opacity='.4' stroke-width='1.8' stroke-dasharray='4 14' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 60'%3E%3Cellipse cx='75' cy='30' rx='70' ry='25' fill='%23000' fill-opacity='.13'/%3E%3Cellipse cx='75' cy='30' rx='70' ry='25' fill='none' stroke='%23000' stroke-width='3.4' stroke-dasharray='10 8' stroke-linecap='round'/%3E%3Cellipse cx='75.8' cy='31' rx='68.5' ry='24' fill='none' stroke='%23000' stroke-opacity='.4' stroke-width='1.8' stroke-dasharray='4 14' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}

/* ---- Count tag: how many stories are in a pile of stickers ---------------
   A white parcel label with a rose border like the paint tin's, tied by its string to the top
   sticker of the pile. The picture is assets/fx/count-tag.webp; the number
   and the words are HTML. --counttag-w sets its width (about 1.5 stickers).
   It sways slowly about its knot and swings once when the pointer touches it.
   Markup:
   <button class="ds-counttag"><span class="ds-counttag__sway"><span class="ds-counttag__nudge">
     <img class="ds-counttag__art" src="…/count-tag.webp" alt="">
     <span class="ds-counttag__text"><b class="ds-counttag__n">12</b><span class="ds-counttag__words zh">个故事</span></span>
   </span></span></button>
   --knot-x / --knot-y and --text-* come from count-tag.json (js sets them).  */
.ds-counttag {
  --knot-x: 4.1%; --knot-y: 7.2%;
  --text-x: 34.4%; --text-y: 30.3%; --text-w: 58.9%; --text-h: 56.2%;
  position: relative; display: block; width: var(--counttag-w, 180px); aspect-ratio: 387 / 208;
  border: 0; padding: 0; background: none; cursor: pointer; color: var(--ink); font: inherit;
}
.ds-counttag__sway, .ds-counttag__nudge { position: absolute; inset: 0; transform-origin: var(--knot-x) var(--knot-y); }
.ds-counttag__sway { animation: ds-counttag-sway 5.6s var(--ease-ink) infinite alternate; animation-delay: var(--sway-delay, 0s); }
.ds-counttag:hover .ds-counttag__nudge, .ds-counttag:focus-visible .ds-counttag__nudge { animation: ds-counttag-nudge 1.5s var(--ease-paper); }
@keyframes ds-counttag-sway { from { rotate: -1.5deg; } to { rotate: 2.5deg; } }
@keyframes ds-counttag-nudge { 18% { rotate: 7deg; } 42% { rotate: -4deg; } 66% { rotate: 1.8deg; } }
.ds-counttag__art { position: absolute; inset: 0; width: 100%; height: 100%; display: block; -webkit-user-drag: none; }
.ds-counttag__text {
  position: absolute; left: var(--text-x); top: var(--text-y); width: var(--text-w); height: var(--text-h);
  display: flex; align-items: center; justify-content: center; white-space: nowrap;
  font-size: calc(var(--counttag-w, 180px) * .1); line-height: 1;
}
.ds-counttag__n { font-family: var(--font-title); font-weight: 400; font-size: 2.3em; color: var(--emo-mixed-ink); margin-right: .18em; }
.ds-counttag__words { font-family: var(--font-hand); font-size: 1.3em; color: var(--ink); }
.ds-counttag__words.zh { font-family: var(--font-title); font-size: 1.15em; }   /* Caveat has no Chinese: the brush font, as in .ds-note */
.ds-counttag:focus-visible { outline: none; }
.ds-counttag:focus-visible .ds-counttag__text { outline: 1.5px dashed var(--rouge); outline-offset: 3px; border-radius: var(--radius-card); }

/* ---- Folded map cover: the outside of the map folded up on the desk ------
   Like the cover of an old Shanghai pocket map, painted: cream watercolour paper, the whole city in the middle
   as a watercolour vignette (its edges dissolve into the paper), the title written down the empty sea in the
   top left in the brush font, and a tin-rose band along the bottom with two hand-drawn gold lines and the
   number of stories. A pen outline drawn twice, like every paper thing on the desk. Pigments: the map's own,
   tin rose, gilt. The words are HTML. --foldcover-w is the cover's width (every size follows it);
   --foldcover-art is the vignette (site/assets/map/cover-vignette.webp, made by tools/make_foldcover.py).
   Markup:
   <div class="ds-foldcover"><span class="ds-foldcover__art"></span><b class="ds-foldcover__title">上海情绪地图</b>
     <span class="ds-foldcover__band"><span class="ds-foldcover__meta">31 个故事</span><span class="ds-foldcover__year">2026</span></span></div> */
.ds-foldcover {
  --fw: var(--foldcover-w, 240px);
  position: relative; overflow: hidden; color: var(--ink); aspect-ratio: 4 / 5;
  background: var(--tex-paper) 0 0 / calc(var(--fw) * 1.6), var(--paper);
  background-blend-mode: multiply;
}
.ds-foldcover::after {   /* the pen outline round the paper's edge, two passes */
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M1.5 2.2 C 30 1.2, 70 2.6, 98.4 1.6 C 98.9 30, 97.8 70, 98.6 98.2 C 70 99.1, 30 97.9, 1.8 98.6 C 1 70, 2.3 30, 1.5 2.2' fill='none' stroke='%233B3226' stroke-opacity='.55' stroke-width='1.1' vector-effect='non-scaling-stroke'/%3E%3Cpath d='M2.4 1.4 C 35 2.4, 65 1.1, 97.6 2.6 C 98.4 35, 99 65, 97.7 97.4 C 65 98.3, 35 99, 2.6 97.6 C 1.6 65, 1 35, 2.4 1.4' fill='none' stroke='%233B3226' stroke-opacity='.2' stroke-width='.9' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.ds-foldcover__art {   /* the city, Chongming at the top, the bay at the bottom */
  position: absolute; left: 13%; right: 1%; top: 3%; height: 76%;
  background: var(--foldcover-art, none) center / contain no-repeat;
}
.ds-foldcover__title {   /* written down the sea in the top left, like a brush title on an old map */
  position: absolute; left: 7%; top: 6%;
  writing-mode: vertical-rl; font-family: var(--font-title); font-weight: 400;
  font-size: calc(var(--fw) * .102); line-height: 1; letter-spacing: .12em; color: var(--ink);
}
.ds-foldcover__band {   /* tin rose, with uneven painted ends and two thin gold lines */
  position: absolute; left: 0; right: 0; bottom: 6%; height: 15%;
  display: flex; align-items: center; justify-content: center; gap: calc(var(--fw) * .05);
  background: var(--tex-paper) 0 0 / calc(var(--fw) * 1.6), var(--tin-rose);
  background-blend-mode: multiply;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 20' preserveAspectRatio='none'%3E%3Cpath d='M0 1.6 C 22 .6, 48 2.2, 74 1 S 96 1.8, 100 1.2 L 100 18.8 C 80 19.6, 52 18.1, 30 19.2 S 6 18.4, 0 19 Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 20' preserveAspectRatio='none'%3E%3Cpath d='M0 1.6 C 22 .6, 48 2.2, 74 1 S 96 1.8, 100 1.2 L 100 18.8 C 80 19.6, 52 18.1, 30 19.2 S 6 18.4, 0 19 Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.ds-foldcover__band::before, .ds-foldcover__band::after {   /* the gold lines, drawn by hand (two passes) */
  content: ""; position: absolute; left: 0; right: 0; height: calc(var(--fw) * .03); background: var(--gilt);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 10' preserveAspectRatio='none'%3E%3Cpath d='M0 5.2 C 40 4.1, 90 5.9, 140 4.8 S 185 5.6, 200 4.9' fill='none' stroke='%23000' stroke-width='2.2'/%3E%3Cpath d='M0 6.4 C 50 5.6, 100 7, 150 6.1 S 190 6.6, 200 6.2' fill='none' stroke='%23000' stroke-opacity='.35' stroke-width='1.2'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 10' preserveAspectRatio='none'%3E%3Cpath d='M0 5.2 C 40 4.1, 90 5.9, 140 4.8 S 185 5.6, 200 4.9' fill='none' stroke='%23000' stroke-width='2.2'/%3E%3Cpath d='M0 6.4 C 50 5.6, 100 7, 150 6.1 S 190 6.6, 200 6.2' fill='none' stroke='%23000' stroke-opacity='.35' stroke-width='1.2'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.ds-foldcover__band::before { top: 12%; }
.ds-foldcover__band::after { bottom: 12%; }
.ds-foldcover__meta, .ds-foldcover__year { position: relative; color: var(--paper); line-height: 1; white-space: nowrap; }
.ds-foldcover__meta { font-family: var(--font-title); font-size: calc(var(--fw) * .072); letter-spacing: .06em; }
.ds-foldcover__year { font-family: var(--font-hand); font-weight: 600; font-size: calc(var(--fw) * .08); }
/* English: the title runs across the top in handwriting instead of down the side */
:lang(en) .ds-foldcover__title {
  writing-mode: horizontal-tb; left: 9%; right: 9%; top: 6%;
  font-family: var(--font-hand); font-weight: 600; font-size: calc(var(--fw) * .1); line-height: .95; letter-spacing: 0;
}
:lang(en) .ds-foldcover__meta { font-family: var(--font-hand); font-weight: 600; font-size: calc(var(--fw) * .085); letter-spacing: 0; }

/* ---- Place names over the close-up map ------------------------------------
   Districts in the brush font, waters (rivers, lakes, the sea) in the river's blue. Real HTML text that keeps
   one size on the screen while the map zooms (js scales it back), placed by js from site/data/places.json. */
.ds-place {
  position: absolute; left: 0; top: 0; white-space: nowrap; pointer-events: none; user-select: none;
  font-family: var(--font-title); font-size: 32px; line-height: 1; letter-spacing: .12em; color: var(--map-label);
  translate: -50% -50%;
}
.ds-place--water { color: var(--map-label-water); letter-spacing: .06em; }
:lang(en) .ds-place { font-family: var(--font-hand); font-weight: 600; letter-spacing: .02em; }

/* ---- Paint box: the emotion filter --------------------------------------
   An open tin of seven watercolour pans (喜悦 → 愤怒), a brush standing in a jar of rinse water, and the lid's
   cream panel, where the words are real text. js/paintbox.js builds it from assets/paintbox/paintbox.json and
   places every layer and pan by percentages of the whole picture (the tin, the jar, and the brush wherever it
   rests). Sizes inside are in drawing units: --paintbox-units is the picture's width in them.
   Point at a pan: it brightens and the lid names it. Pick one: the brush dips into it, the pan shines wet, the
   lid says "平静 · 5". Pick it again, or the jar: the brush rinses, the lid goes back to "上海 水彩".
   A soft paper-coloured glow under it keeps it apart from the map.                                          */
.ds-paintbox {
  position: relative; aspect-ratio: var(--paintbox-aspect, 900 / 520); isolation: isolate;
  container-type: inline-size; user-select: none; -webkit-user-select: none;
  --pu: calc(100cqw / var(--paintbox-units, 900));   /* one drawing unit */
}
.ds-paintbox::before {   /* the glow it sits on */
  content: ""; position: absolute; z-index: -1; inset: -10% -7% -14% -7%; pointer-events: none;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--paper) 90%, transparent), transparent);
}
.ds-paintbox__layer { position: absolute; display: block; pointer-events: none; -webkit-user-drag: none; }
/* bottom to top, never changing while the brush moves: the jar's back (water), its glass, the brush, and the strip
   of glass in front of the brush where it stands in the water (jarMask: jarFront cut along the brush) */
.ds-paintbox__jar-back { z-index: 1; }
.ds-paintbox__jar-glass { z-index: 2; }
.ds-paintbox__brush { position: absolute; z-index: 3; pointer-events: none; }
.ds-paintbox__jar-mask { z-index: 4; }
.ds-paintbox__tips { position: absolute; }
.ds-paintbox__brush .ds-paintbox__layer { position: absolute; }
.ds-paintbox__shine { opacity: 0; transition: opacity var(--dur-base) var(--ease-paper); }
.ds-paintbox__shine.is-wet { opacity: 1; }
.ds-paintbox__ring { opacity: 0; }   /* the pale ring of colour that spreads round a pan as the brush presses in (ring-<emotion>.webp) */
.ds-paintbox__pan, .ds-paintbox__jar {
  position: absolute; z-index: 5; border: 0; padding: 0; background: none; cursor: pointer; border-radius: var(--radius-card);
}
.ds-paintbox__pan::before {   /* the pan brightens a little under the pointer */
  content: ""; position: absolute; inset: 14% 10%; border-radius: var(--radius-card); pointer-events: none;
  background: radial-gradient(closest-side, var(--paper), transparent); opacity: 0;
  transition: opacity var(--dur-quick) var(--ease-paper);
}
.ds-paintbox__pan:hover::before, .ds-paintbox__pan:focus-visible::before { opacity: .55; }
.ds-paintbox__pan:focus-visible, .ds-paintbox__jar:focus-visible { outline: 1.5px dashed var(--line); outline-offset: 1px; }

/* the lid's panel: "上海 水彩 / Water Colours · 7", or the emotion pointed at or chosen */
.ds-paintbox__lid-text { position: absolute; pointer-events: none; }
.ds-paintbox__face {
  position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: calc(var(--pu) * 7); text-align: center; white-space: nowrap;
  opacity: 0; transition: opacity var(--dur-base) var(--ease-paper);
}
.ds-paintbox__face.is-shown { opacity: 1; }
.ds-paintbox__title { font-family: var(--font-title); font-size: calc(var(--pu) * 40); line-height: 1; color: var(--rouge); letter-spacing: .08em; }
.ds-paintbox__sub { font-family: var(--font-hand); font-size: calc(var(--pu) * 20); line-height: 1; color: var(--ink-soft); }
.ds-paintbox__name { font-family: var(--font-title); font-size: calc(var(--pu) * 40); line-height: 1; color: var(--emo-ink, var(--ink)); letter-spacing: .06em; }
.ds-paintbox__stroke {   /* one stroke of the colour under its name */
  display: block; width: calc(var(--pu) * 130); height: calc(var(--pu) * 13); background: var(--emo, var(--emo-calm)); opacity: .9;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M3 7.2 C 20 3.4, 48 4.6, 70 4.2 C 88 3.9, 104 4.4, 117 5.6 C 112 8.4, 96 8.8, 76 9 C 52 9.3, 26 9.6, 6 9.8 C 2.6 9.6, 1.6 8.2, 3 7.2 Z' fill='%23000'/%3E%3Cpath d='M10 5.6 C 30 3, 60 2.6, 88 3.2' fill='none' stroke='%23000' stroke-opacity='.45' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M3 7.2 C 20 3.4, 48 4.6, 70 4.2 C 88 3.9, 104 4.4, 117 5.6 C 112 8.4, 96 8.8, 76 9 C 52 9.3, 26 9.6, 6 9.8 C 2.6 9.6, 1.6 8.2, 3 7.2 Z' fill='%23000'/%3E%3Cpath d='M10 5.6 C 30 3, 60 2.6, 88 3.2' fill='none' stroke='%23000' stroke-opacity='.45' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.ds-paintbox__hint { font-family: var(--font-body); font-size: calc(var(--pu) * 19); line-height: 1; color: var(--ink-soft); }
:lang(en) .ds-paintbox__hint { font-family: var(--font-hand); font-size: calc(var(--pu) * 23); }
:lang(en) .ds-paintbox__name { font-family: var(--font-hand); font-size: calc(var(--pu) * 44); font-weight: 600; }

/* ---- About-me note on the cork board ---------------------------------------
   The lined note on the cork board (site/assets/scene/aboutNote.webp, cut out of the desk picture by
   tools/make_about_note.py, with aboutBoard.webp the cork painted in under it). js lays the picture out like the
   other props; .ds-pinnote is the HTML text written on its lines, and the whole thing turns about the pin
   (--pin-x, --pin-y: the pin's place in the note, as percentages).
   It shows it can be opened, quietly: every few seconds its free corner lifts in a breath of wind and drops back
   (ds-flutter); under the pointer it lifts off the board a little and the pin wobbles; once per visit a .ds-note
   says 关于我 beside it. Opened, it comes off its pin and becomes the letter (.ds-letter).
   <div class="ds-pinnote"><img src="aboutNote.webp" alt=""><span class="ds-pinnote__text"><b>关于我</b><i>about me</i></span></div> */
.ds-pinnote {
  position: absolute; display: block; cursor: pointer; border: 0; padding: 0; background: none;
  transform-origin: var(--pin-x, 49%) var(--pin-y, 13%);
  animation: ds-flutter 6.5s var(--ease-ink) infinite;
  transition: translate var(--dur-base) var(--ease-spring), scale var(--dur-base) var(--ease-spring);
}
.ds-pinnote img { display: block; width: 100%; height: auto; pointer-events: none; -webkit-user-drag: none; }
.ds-pinnote__text {   /* written on the note's top two lines */
  position: absolute; left: 22%; right: 12%; top: 31%; display: flex; flex-direction: column; gap: .15em;
  container-type: inline-size; pointer-events: none; rotate: -1.5deg;
}
.ds-pinnote__text b { font-family: var(--font-title); font-weight: 400; font-size: 26cqw; line-height: 1; color: var(--ink); letter-spacing: .06em; }
.ds-pinnote__text i { font-family: var(--font-hand); font-style: normal; font-size: 20cqw; line-height: 1; color: var(--ink-soft); }
:lang(en) .ds-pinnote__text b { font-family: var(--font-hand); font-weight: 600; font-size: 25cqw; letter-spacing: 0; }
.ds-pinnote:hover, .ds-pinnote:focus-visible { translate: 0 -3%; scale: 1.04; animation: ds-pinwobble .6s var(--ease-spring); }
.ds-pinnote:focus-visible { outline: 1.5px dashed var(--line); outline-offset: 3px; }
/* a breath of wind: the free corner lifts and drops; most of the time it is still */
@keyframes ds-flutter { 0%, 78%, 100% { rotate: 0deg; } 83% { rotate: -4deg; } 88% { rotate: 1.6deg; } 93% { rotate: -.8deg; } }
@keyframes ds-pinwobble { 30% { rotate: 3deg; } 60% { rotate: -2deg; } 100% { rotate: 0deg; } }

/* ---- The letter the about note opens into ----------------------------------
   A sheet of lined writing paper (.ds-paper with faint ruled lines), slightly turned, like a note taken off the
   board and unfolded. Brush-font greeting, the body in the reading font, a handwritten signature, and the email
   as an ink-underlined link. Real HTML text, from content/about.json.
   <article class="ds-letter ds-paper"><h2 class="ds-letter__title">你好，我是 Katherine</h2><p>…</p>
     <p class="ds-letter__next">…</p><p class="ds-letter__contact">…<a href="mailto:…">…</a></p>
     <p class="ds-letter__sign">Katherine Wu</p></article> */
.ds-letter {
  position: relative; width: min(34em, calc(100vw - 2 * var(--space-5))); padding: var(--space-7) var(--space-7) var(--space-6);
  box-shadow: var(--shadow-paper); rotate: -1deg; border-radius: 2px;
  /* the ruled lines over the paper's grain (the grain picture is opaque, so it goes underneath) */
  background-image:
    repeating-linear-gradient(180deg, transparent 0 calc(var(--leading-body) * var(--text-body) - 1px), color-mix(in srgb, var(--emo-sad) 22%, transparent) calc(var(--leading-body) * var(--text-body) - 1px) calc(var(--leading-body) * var(--text-body))),
    var(--tex-paper);
  background-size: 100% calc(var(--leading-body) * var(--text-body)), 512px;
  background-position: 0 calc(var(--space-7) + 2.95em + 14px), 0 0;   /* lines up with the body text's baselines */
  font-size: var(--text-body); line-height: var(--leading-body);
}
.ds-letter::before {   /* the pin hole the note hung from */
  content: ""; position: absolute; left: 50%; top: var(--space-3); width: 7px; height: 7px; translate: -50% 0; border-radius: 50%;
  background: radial-gradient(circle, var(--ink-faint), transparent 70%);
}
.ds-letter p { margin: 0; max-width: var(--measure); }   /* no gaps between paragraphs: the text keeps to the ruled lines */
.ds-letter__title { margin: 0 0 calc(var(--leading-body) * var(--text-body) * .5); font-family: var(--font-title); font-weight: 400; font-size: var(--text-h2); line-height: 1.3; }
:lang(en) .ds-letter__title { font-family: var(--font-hand); font-weight: 600; font-size: 34px; }
.ds-letter__next { color: var(--ink-soft); }
.ds-letter__contact { margin-top: calc(var(--leading-body) * var(--text-body)) !important; }
.ds-letter__contact a { color: var(--emo-sad-ink); text-decoration: none; background: linear-gradient(var(--emo-sad-ink), var(--emo-sad-ink)) 0 100% / 100% 1px no-repeat; white-space: nowrap; }
.ds-letter__contact a:hover, .ds-letter__contact a:focus-visible { background-size: 100% 2px; outline: none; }
.ds-letter__sign { margin-top: var(--space-4) !important; text-align: right; font-family: var(--font-hand); font-weight: 600; font-size: 30px; line-height: 1; rotate: -3deg; }

/* ---- Photo string: the story collection on the wall -------------------------
   A length of twine across the wallpaper between the cork board and the window, with little instant photos
   hanging from it on wooden pegs: the stories most recently found, each photo its object on a wash of its
   emotion's colour; a story not found yet shows the object's pencil draft (site/assets/objects/<id>-sketch.webp,
   tools/make_sketches.py) on plain paper. The photos sway gently, each in its own time, about their pegs. A tag
   at the end says 收集册 and how many are found. Clicking any of it opens the album (.ds-album).
   <div class="ds-photostring"><svg class="ds-photostring__twine">…</svg>
     <button class="ds-polaroid" style="--emo:var(--emo-joy)"><img src="5-sticker.webp" alt=""><span>托腮的猪</span></button>
     <button class="ds-polaroid is-missing"><img src="12-sketch.webp" alt=""><span>？</span></button> … </div>  */
.ds-photostring { position: absolute; container-type: inline-size; }
.ds-photostring__twine { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.ds-photostring__twine path { fill: none; stroke: var(--umber); stroke-linecap: round; }
.ds-polaroid {
  --emo: var(--paper-deep);
  --pw: var(--polaroid-w, 15cqw);
  position: absolute; width: var(--pw); aspect-ratio: 5 / 6; padding: calc(var(--pw) * .07) calc(var(--pw) * .07) 0; box-sizing: border-box; border: 0; cursor: pointer;
  display: flex; flex-direction: column; align-items: center;
  background: var(--tex-paper) 0 0 / 256px, var(--paper); background-blend-mode: multiply;
  box-shadow: var(--shadow-rest); translate: -50% 0;
  transform-origin: 50% -6%; animation: ds-sway 5.5s var(--ease-ink) var(--sway-delay, 0s) infinite alternate;
  transition: scale var(--dur-base) var(--ease-spring);
}
.ds-polaroid::before {   /* the wooden peg it hangs from */
  content: ""; position: absolute; left: 50%; top: -14%; width: 12%; height: 26%; translate: -50% 0; border-radius: 2px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--umber) 85%, var(--ink)), var(--umber) 40%, color-mix(in srgb, var(--umber) 70%, var(--paper)));
  box-shadow: 0 0 0 .6px var(--line-ghost);
}
.ds-polaroid img {   /* the object on a wash of its colour */
  display: block; flex: none; align-self: stretch; width: auto; aspect-ratio: 1; min-width: 0;
  object-fit: contain; padding: calc(var(--pw) * .06); box-sizing: border-box; pointer-events: none; -webkit-user-drag: none;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--emo) 38%, var(--paper)), color-mix(in srgb, var(--emo) 18%, var(--paper)));
}
.ds-polaroid > span {
  flex: 1; display: grid; place-items: center; width: 100%; font-family: var(--font-title); line-height: 1;
  color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: max(11px, calc(var(--pw) * .13));
}
.ds-polaroid.is-missing img { background: var(--paper-deep); }
.ds-polaroid.is-missing > span { color: var(--ink-soft); font-family: var(--font-hand); }
.ds-polaroid:hover, .ds-polaroid:focus-visible { scale: 1.08; animation-play-state: paused; outline: none; }
@keyframes ds-sway { from { rotate: -2.2deg; } to { rotate: 2.2deg; } }
.ds-photostring__tag { position: absolute; }   /* a .ds-tag hanging from the twine's end: 收集册 · 3/31 */

/* ---- Album: every story, collected ------------------------------------------
   An open scrapbook over the desk (behind it the light --scrim). Index tabs stand up from its top edge: 收集 /
   全部 (show what this visitor has found, or everything) and 图 / 文 (stamps, or every story as text to read
   through). In 图, each page holds a grid of stamps: the object on a wash of its emotion colour, with a
   perforated edge like the postcard's stamp, its pen name below in the brush font and its district in hand. A
   story not found yet is its pencil draft on plain paper with 还没找到. In 文, the stories run on as a list:
   a dot of the emotion colour, the name, 上海市 徐汇区 · the place, and the whole story. Any story opens its
   postcard. Pages turn with the hand-drawn arrows at the bottom corners.
   <section class="ds-album ds-paper">
     <nav class="ds-album__tabs"><button class="ds-album__tab" aria-pressed="true">收集</button>…</nav>
     <header class="ds-album__head"><h2 class="ds-album__title">收集册</h2><p class="ds-album__count">找到 12 / 31</p></header>
     <div class="ds-album__spread"><div class="ds-album__page">…slots…</div><div class="ds-album__page">…</div></div>
     <footer class="ds-album__foot"><button class="ds-album__turn">←</button><span>1 / 2</span><button class="ds-album__turn">→</button></footer>
   </section> */
.ds-album {
  position: relative; width: min(1040px, calc(100vw - 2 * var(--space-5))); padding: var(--space-6) var(--space-6) var(--space-5);
  border-radius: 4px; box-shadow: var(--shadow-paper), 0 0 0 1px var(--line-ghost);
}
.ds-album::after {   /* the spine down the middle: a soft fold shadow and the stitches */
  content: ""; position: absolute; left: 50%; top: var(--space-5); bottom: var(--space-5); width: 30px; translate: -50% 0; pointer-events: none;
  background:
    repeating-linear-gradient(180deg, transparent 0 14px, var(--line-ghost) 14px 22px) 50% 0 / 1.4px 100% no-repeat,
    linear-gradient(90deg, transparent, color-mix(in srgb, var(--umber) 16%, transparent) 45%, color-mix(in srgb, var(--umber) 16%, transparent) 55%, transparent);
}
.ds-album__tabs { position: absolute; right: var(--space-6); top: 0; translate: 0 -100%; display: flex; gap: var(--space-4); }
.ds-album__tabs > div { display: flex; gap: 2px; }
.ds-album__tab {
  border: 0; cursor: pointer; padding: 6px 14px 8px; border-radius: 6px 6px 0 0; font-family: var(--font-title); font-size: 17px; color: var(--ink-soft);
  background: var(--tex-paper) 0 0 / 256px, var(--paper-deep); background-blend-mode: multiply; box-shadow: 0 -1px 0 var(--line-ghost) inset;
  translate: 0 4px; transition: translate var(--dur-quick) var(--ease-paper);
}
.ds-album__tab[aria-pressed="true"] { color: var(--ink); translate: 0 0; background: var(--tex-paper) 0 0 / 256px, color-mix(in srgb, var(--tin-rose) 45%, var(--paper)); background-blend-mode: multiply; }
.ds-album__tab:focus-visible { outline: 1.5px dashed var(--line); outline-offset: 2px; }
:lang(en) .ds-album__tab { font-family: var(--font-hand); font-weight: 600; font-size: 19px; }
.ds-album__head { display: flex; align-items: baseline; gap: var(--space-4); margin-bottom: var(--space-4); }
.ds-album__title { margin: 0; font-family: var(--font-title); font-weight: 400; font-size: var(--text-h2); }
.ds-album__count { margin: 0; font-family: var(--font-hand); font-size: 22px; color: var(--ink-soft); }
.ds-album__count b { font-family: var(--font-title); font-weight: 400; font-size: 26px; color: var(--tin-rose); }
:lang(en) .ds-album__title { font-family: var(--font-hand); font-weight: 600; font-size: 34px; }
.ds-album__spread { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-8); }
.ds-album__page { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-5) var(--space-4); align-content: start; }   /* minmax(0, …): a long name (it doesn't wrap) is cut with … instead of widening its column and every stamp in it */
.ds-album__slot {
  --emo: var(--paper-deep); --emo-ink: var(--ink-soft);
  display: flex; flex-direction: column; align-items: center; gap: 6px; border: 0; padding: 0; background: none; cursor: pointer; color: var(--ink);
  rotate: var(--tilt, 0deg); transition: translate var(--dur-base) var(--ease-spring), rotate var(--dur-base) var(--ease-spring);
}
.ds-album__stamp {   /* perforated, like the postcard's stamp; the object on a wash of its colour */
  position: relative; width: 100%; aspect-ratio: 5 / 6; display: grid; place-items: center;
}
.ds-album__stamp::before {
  content: ""; position: absolute; inset: 0; box-shadow: var(--shadow-rest);
  background: radial-gradient(closest-side, color-mix(in srgb, var(--emo) 40%, var(--paper)), color-mix(in srgb, var(--emo) 16%, var(--paper)) 85%), var(--paper);
  /* holes only round the edge: the dots, and a solid piece over everything but a 5px rim */
  -webkit-mask: linear-gradient(#000 0 0) center / calc(100% - 10px) calc(100% - 10px) no-repeat, radial-gradient(circle at 50% 50%, transparent 2.4px, #000 2.7px) -5px -5px / 10px 10px;
          mask: linear-gradient(#000 0 0) center / calc(100% - 10px) calc(100% - 10px) no-repeat, radial-gradient(circle at 50% 50%, transparent 2.4px, #000 2.7px) -5px -5px / 10px 10px;
}
.ds-album__stamp img { position: relative; width: 82%; height: 82%; object-fit: contain; -webkit-user-drag: none; }
.ds-album__name { font-family: var(--font-title); font-size: 16px; line-height: 1.1; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-album__where { font-family: var(--font-body); font-size: 12px; line-height: 1; color: var(--emo-ink); letter-spacing: .04em; }
:lang(en) .ds-album__name { font-family: var(--font-hand-name); font-weight: 600; font-synthesis-weight: none; font-size: 18px; }   /* pen names stay as written: Chinese ones in the brush font */
:lang(en) .ds-album__where { font-family: var(--font-hand); font-size: 15px; }
.ds-album__slot:hover, .ds-album__slot:focus-visible { translate: 0 -4px; rotate: 0deg; outline: none; }
.ds-album__slot:focus-visible .ds-album__stamp { outline: 1.5px dashed var(--line); outline-offset: 4px; }
.ds-album__slot.is-missing { cursor: default; }
.ds-album__slot.is-missing .ds-album__stamp::before { background: var(--paper); box-shadow: 0 0 0 1px var(--line-ghost); }
.ds-album__slot.is-missing .ds-album__name { font-family: var(--font-body); font-size: 14px; color: var(--ink-soft); }
:lang(en) .ds-album__slot.is-missing .ds-album__name { font-family: var(--font-hand); font-size: 17px; }
.ds-album__slot.is-missing:hover { translate: none; }
/* 文: every story as text, set like a book: two columns of a fixed height, one on each page, flowing on to the next
   spread; js turns the page by scrolling the list sideways by one spread (its width plus the gap). In 收集, only
   the stories found so far, then a line saying how many still wait on the map. */
.ds-album__list {
  height: min(60vh, 540px); columns: 2; column-gap: var(--space-8); column-fill: auto; overflow: hidden;
}
.ds-album__entry { margin: 0 0 var(--space-5); padding: 0 0 var(--space-4); border-bottom: 1.2px dashed var(--line-ghost); cursor: pointer; }
.ds-album__entry h3 { display: flex; align-items: center; gap: 10px; margin: 0 0 4px; font-family: var(--font-title); font-weight: 400; font-size: 20px; break-after: avoid; }
.ds-album__entry h3::before {   /* a dot of the emotion's colour, like a drop of paint */
  content: ""; flex: none; width: 12px; height: 12px; border-radius: 50% 45% 55% 48%; background: var(--emo); box-shadow: 0 0 0 2px color-mix(in srgb, var(--emo) 30%, transparent);
}
.ds-album__entry .ds-album__where { display: block; margin: 0 0 6px 22px; font-size: 13px; break-after: avoid; }
.ds-album__entry p { margin: 0; font-size: 15px; line-height: 1.8; orphans: 2; widows: 2; }
.ds-album__entry:hover h3, .ds-album__entry:focus-visible h3 { color: var(--emo-ink); }
.ds-album__entry:focus-visible { outline: 1.5px dashed var(--line); outline-offset: 4px; }
.ds-album__waiting { margin: 0; font-family: var(--font-hand); font-size: 20px; color: var(--ink-soft); }
:lang(zh) .ds-album__waiting { font-family: var(--font-body); font-size: 15px; }
.ds-album__foot { display: flex; align-items: center; justify-content: center; gap: var(--space-5); margin-top: var(--space-5); font-family: var(--font-hand); font-size: 20px; color: var(--ink-soft); }
.ds-album__turn { border: 0; background: none; cursor: pointer; padding: 4px 8px; color: var(--ink); font-family: var(--font-hand); font-size: 28px; line-height: 1; }
.ds-album__turn:disabled { opacity: .25; cursor: default; }
@media (max-width: 720px) {
  .ds-album__spread { grid-template-columns: 1fr; gap: var(--space-5); }
  .ds-album::after { display: none; }
  .ds-album__page { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ds-album__list { columns: 1; height: 62vh; overflow: auto; }
}

/* ---- Envelope: the way into the ending ---------------------------------------
   A cream envelope lying on the stack of books at the front of the desk, after the visitor has come back from
   the map once. Its flap is closed with a tin-rose wax seal marked 尾; a stamp (the folded map as a drawing, on a
   wash of dusk lilac) in the corner, a round postmark 寄自上海 2026, and 给读到这里的你 written on the front.
   Sizes follow --env-w. Parts, back to front: __back (the inside, seen once the flap is open), __letter (the
   letter's top edge, inside), __front (the pocket), __flap (the triangle, two faces, turning on its top edge:
   js turns it with --flap from 0deg closed to 180deg open: it lifts off the envelope from its point, towards the viewer,
   and folds over upwards until it lies flat on the desk above the envelope, its fold along the envelope's top edge and
   its point away from it, as a real flap does; past upright .is-open puts it behind the letter), __seal on the flap's point.
   Opening: pull the seal up (or click), the seal pops, the flap lifts and folds over, the letter rises out and
   becomes .ds-endletter, and the P.S. follows it; closing, both go back into the pocket.
   <div class="ds-envelope" style="--env-w:220px">
     <span class="ds-envelope__back"></span><span class="ds-envelope__letter"></span>
     <span class="ds-envelope__front"><span class="ds-envelope__to">给读到这里的你</span>
       <span class="ds-envelope__stamp"><img src="…"></span><span class="ds-envelope__postmark">…</span></span>
     <span class="ds-envelope__flap"><span class="ds-envelope__flap-out"></span><span class="ds-envelope__flap-in"></span></span>
     <span class="ds-envelope__seal">尾</span></div> */
.ds-envelope {
  --ew: var(--env-w, 220px); --flap: 0deg;
  position: relative; width: var(--ew); aspect-ratio: 16 / 10; display: block; border: 0; padding: 0; background: none; cursor: pointer;
  perspective: calc(var(--ew) * 4); transform-style: preserve-3d; color: var(--ink);
}
.ds-envelope > span { position: absolute; display: block; }
.ds-envelope__back, .ds-envelope__front { inset: 0; border-radius: 3px; }
.ds-envelope__back { background: var(--tex-paper) 0 0 / 256px, var(--envelope-deep); background-blend-mode: multiply; }
.ds-envelope__letter {   /* the top of the letter, inside, a little whiter than the envelope */
  left: 7%; right: 7%; top: 6%; height: 60%; background: var(--tex-paper) 0 0 / 256px, var(--paper); background-blend-mode: multiply;
  box-shadow: 0 0 0 .6px var(--line-ghost);
}
.ds-envelope__front {
  background:
    /* the pocket's two side folds and bottom fold, pencil lines from the bottom corners to the middle */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 100' preserveAspectRatio='none'%3E%3Cpath d='M1 99 L 80 47 L 159 99' fill='none' stroke='%233B3226' stroke-opacity='.28' stroke-width='.8' vector-effect='non-scaling-stroke'/%3E%3Cpath d='M2 98 L 81 48.5 L 158 98.5' fill='none' stroke='%233B3226' stroke-opacity='.12' stroke-width='.7' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E") center / 100% 100% no-repeat,
    linear-gradient(170deg, transparent 52%, color-mix(in srgb, var(--envelope-deep) 60%, transparent) 100%),
    var(--tex-paper) 0 0 / 256px, var(--envelope);
  background-blend-mode: normal, normal, multiply, normal;
  box-shadow: var(--shadow-rest), 0 0 26px 8px color-mix(in srgb, var(--paper) 55%, transparent);   /* and a faint light round it on the desk */
  /* the pocket: the whole front except a V cut down from the top, where the flap folds over */
  clip-path: polygon(0 0, 50% 52%, 100% 0, 100% 100%, 0 100%);
}
.ds-envelope__front::before {   /* an airmail border: tin-rose and dusk-lilac stripes round the edge, painted on */
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .85;
  background: repeating-linear-gradient(135deg, var(--tin-rose) 0 calc(var(--ew) * .028), transparent calc(var(--ew) * .028) calc(var(--ew) * .045), var(--lilac) calc(var(--ew) * .045) calc(var(--ew) * .073), transparent calc(var(--ew) * .073) calc(var(--ew) * .09));
  -webkit-mask: linear-gradient(#000 0 0), linear-gradient(#000 0 0) content-box; -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0), linear-gradient(#000 0 0) content-box; mask-composite: exclude;
  padding: calc(var(--ew) * .028);
}
.ds-envelope__front::after {   /* the pen outline round the envelope, two passes */
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M1.5 2.2 C 30 1.2, 70 2.6, 98.4 1.6 C 98.9 30, 97.8 70, 98.6 98.2 C 70 99.1, 30 97.9, 1.8 98.6 C 1 70, 2.3 30, 1.5 2.2' fill='none' stroke='%233B3226' stroke-opacity='.55' stroke-width='1.1' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.ds-envelope__to {   /* the address, by hand, on the lower half */
  position: absolute; left: 9%; bottom: 11%; max-width: 58%; font-family: var(--font-title); font-size: calc(var(--ew) * .062); line-height: 1.3; letter-spacing: .06em;
  padding-bottom: 2px; background: linear-gradient(var(--line-ghost), var(--line-ghost)) 0 100% / 100% 1px no-repeat;
}
.ds-envelope__to i { display: block; font-family: var(--font-hand); font-style: normal; font-size: .9em; color: var(--ink-soft); letter-spacing: 0; }
:lang(en) .ds-envelope__to { font-family: var(--font-hand); font-weight: 600; font-size: calc(var(--ew) * .066); letter-spacing: 0; }   /* small enough that its two lines stay under the seal */
.ds-envelope__stamp {   /* a little perforated stamp in the top right corner, below the flap's edge */
  position: absolute; right: 6%; bottom: 9%; width: 15%; aspect-ratio: 5 / 6; rotate: 4deg; display: grid; place-items: center;
}
.ds-envelope__stamp::before {
  content: ""; position: absolute; inset: 0; box-shadow: var(--shadow-rest);
  background: radial-gradient(closest-side, color-mix(in srgb, var(--lilac) 30%, var(--paper)), color-mix(in srgb, var(--lilac) 55%, var(--paper)));
  -webkit-mask: linear-gradient(#000 0 0) center / calc(100% - 6px) calc(100% - 6px) no-repeat, radial-gradient(circle at 50% 50%, transparent 1.5px, #000 1.8px) -3px -3px / 6px 6px;
          mask: linear-gradient(#000 0 0) center / calc(100% - 6px) calc(100% - 6px) no-repeat, radial-gradient(circle at 50% 50%, transparent 1.5px, #000 1.8px) -3px -3px / 6px 6px;
}
.ds-envelope__stamp img { position: relative; width: 80%; height: 80%; object-fit: contain; }
.ds-envelope__postmark {   /* the round postmark over the stamp's corner, in dusk lilac ink */
  position: absolute; right: 16%; bottom: 13%; width: 17%; aspect-ratio: 1; rotate: -12deg; color: var(--emo-surprise-ink); opacity: .5;
  display: grid; place-items: center; text-align: center; font-family: var(--font-title); font-size: calc(var(--ew) * .028); line-height: 1.1;
  border-radius: 50%; box-shadow: 0 0 0 1px currentColor inset;
}
.ds-envelope__postmark i { display: block; font-family: var(--font-hand); font-style: normal; font-size: 1.1em; }
.ds-envelope__flap {   /* the triangle over the top, hinged on the envelope's top edge */
  left: 0; top: 0; width: 100%; height: 56%; transform-origin: 50% 0; transform: translateZ(var(--flap-z, 3px)) rotateX(var(--flap)); transform-style: preserve-3d;
}
.ds-envelope__flap > span { position: absolute; inset: 0; clip-path: polygon(0 0, 100% 0, 50% 100%); backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.ds-envelope__flap-out {
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0.5 0.5 L 50 98 L 99.5 0.5' fill='none' stroke='%233B3226' stroke-opacity='.5' stroke-width='1' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E") center / 100% 100% no-repeat,
    linear-gradient(180deg, transparent 40%, color-mix(in srgb, var(--envelope-deep) 70%, transparent)),
    var(--tex-paper) 0 0 / 256px, var(--envelope);
  background-blend-mode: normal, normal, multiply, normal;
  filter: drop-shadow(0 1px 1px rgba(110, 70, 40, .22));
}
/* the inside face: turned over on the flap, so its triangle is clipped the other way up; folded open, its wide side
   then lies along the fold and its point away from the envelope (clipped like the outside it came out upside down) */
.ds-envelope__flap > .ds-envelope__flap-in { clip-path: polygon(0 100%, 100% 100%, 50% 0); }
.ds-envelope__flap-in { transform: rotateX(180deg); background: var(--tex-paper) 0 0 / 256px, var(--envelope-deep); background-blend-mode: multiply; }
.ds-envelope__seal {   /* tin-rose wax, a little uneven, pressed with 尾 */
  left: 50%; top: 52%; width: 15%; aspect-ratio: 1; translate: -50% -50%; display: grid; place-items: center;
  border-radius: 48% 52% 46% 54% / 53% 47% 55% 45%;
  background: radial-gradient(circle at 38% 34%, color-mix(in srgb, var(--tin-rose) 55%, var(--paper)), var(--tin-rose) 45%, color-mix(in srgb, var(--tin-rose) 70%, var(--ink)) 100%);
  box-shadow: 0 0 0 calc(var(--ew) * .012) color-mix(in srgb, var(--tin-rose) 80%, var(--ink)) inset, 1px 2px 3px rgba(110, 70, 40, .3);
  font-family: var(--font-title); font-size: calc(var(--ew) * .07); line-height: 1; color: var(--paper); text-shadow: 0 -1px 0 rgba(0, 0, 0, .18);
}
:lang(en) .ds-envelope__seal { font-family: var(--font-hand); font-weight: 600; font-size: calc(var(--ew) * .08); }
.ds-envelope.is-open .ds-envelope__seal { opacity: 0; }
/* depth: closed, the flap lies on the pocket (z 3px) with the seal on it (4px); the letter (1px) is inside the
   pocket (2px). Opening, js sets --flap-z to -1px once the flap has turned past upright, so it ends behind the letter. */
.ds-envelope__seal { transform: translateZ(4px); }
.ds-envelope.is-open { --flap-z: -1px; }
.ds-envelope__letter { transform: translateZ(1px); }
.ds-envelope__front { transform: translateZ(2px); }
.ds-envelope__letter {   /* ruled lines on the letter, so it reads as a letter as it rises */
  background:
    repeating-linear-gradient(180deg, transparent 0 calc(var(--ew) * .05), color-mix(in srgb, var(--emo-sad) 20%, transparent) calc(var(--ew) * .05) calc(var(--ew) * .054)) 0 calc(var(--ew) * .06) / 100% calc(var(--ew) * .054),
    var(--tex-paper) 0 0 / 256px, var(--paper);
  background-blend-mode: normal, multiply, normal;
}
.ds-envelope:hover .ds-envelope__seal, .ds-envelope:focus-visible .ds-envelope__seal { animation: ds-pinwobble .6s var(--ease-spring); }
.ds-envelope:focus-visible { outline: 1.5px dashed var(--line); outline-offset: 6px; }
/* waiting on the desk: every few seconds the flap's point lifts a little off the seal, as if the letter wanted out */
.ds-envelope.is-waiting .ds-envelope__flap { animation: ds-flapbreath 7s var(--ease-ink) infinite; }
@keyframes ds-flapbreath { 0%, 80%, 100% { transform: translateZ(3px) rotateX(0deg); } 86% { transform: translateZ(3px) rotateX(16deg); } 92% { transform: translateZ(3px) rotateX(4deg); } }   /* the point lifts off the envelope, towards the viewer */

/* ---- The closing letter: what the map adds up to ---------------------------------
   Read at dusk. Two sheets side by side, as if taken out of the envelope and laid on the desk: the letter
   (cream, a deckled edge, slightly turned) and the P.S., a smaller sheet of butter-yellow notepaper held to it
   with a hand-drawn paperclip, its five notes numbered by hand. Real HTML text from content/ending.json. On a
   narrow screen the P.S. comes under the letter. Each sheet scrolls inside itself if its text is long.
   <div class="ds-ending">
     <article class="ds-endletter ds-paper"><p class="ds-endletter__greeting">亲爱的读者：</p><p>…</p>…
       <p class="ds-endletter__signoff">同在上海的</p><p class="ds-endletter__sign">Katherine</p></article>
     <aside class="ds-endnote"><span class="ds-endnote__clip"></span><h3 class="ds-endnote__title">附言：这张地图没有说出的</h3>
       <ol><li><b>情绪是谁命名的</b><p>…</p></li>…</ol></aside></div> */
.ds-ending { display: flex; align-items: flex-start; gap: var(--space-6); max-width: min(1120px, calc(100vw - 2 * var(--space-5))); padding-top: var(--space-6); }
.ds-endletter {
  position: relative; flex: 1.45; min-width: 0; max-height: min(84vh, 780px); overflow: auto; overscroll-behavior: contain;
  padding: var(--space-6) var(--space-7) var(--space-5); rotate: -1.2deg; box-shadow: var(--shadow-paper);
  font-size: 16px; line-height: 1.85;
  /* a deckled edge: the paper's border is a little torn */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0.6 0.9 L 9 0.3 L 18 1 L 27 0.4 L 37 0.9 L 46 0.2 L 56 0.8 L 66 0.3 L 75 1 L 85 0.4 L 94 0.8 L 99.4 0.5 L 99.7 12 L 99.2 24 L 99.8 37 L 99.3 50 L 99.8 63 L 99.2 76 L 99.7 88 L 99.4 99.5 L 90 99.1 L 80 99.7 L 70 99.2 L 60 99.8 L 50 99.3 L 40 99.8 L 30 99.2 L 20 99.7 L 10 99.2 L 0.5 99.6 L 0.2 88 L 0.8 75 L 0.3 62 L 0.8 50 L 0.2 37 L 0.7 25 L 0.3 12 Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0.6 0.9 L 9 0.3 L 18 1 L 27 0.4 L 37 0.9 L 46 0.2 L 56 0.8 L 66 0.3 L 75 1 L 85 0.4 L 94 0.8 L 99.4 0.5 L 99.7 12 L 99.2 24 L 99.8 37 L 99.3 50 L 99.8 63 L 99.2 76 L 99.7 88 L 99.4 99.5 L 90 99.1 L 80 99.7 L 70 99.2 L 60 99.8 L 50 99.3 L 40 99.8 L 30 99.2 L 20 99.7 L 10 99.2 L 0.5 99.6 L 0.2 88 L 0.8 75 L 0.3 62 L 0.8 50 L 0.2 37 L 0.7 25 L 0.3 12 Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.ds-endletter p { margin: 0 0 .6em; max-width: var(--measure); text-indent: 2em; }
:lang(en) .ds-endletter p { text-indent: 0; }
.ds-endletter__greeting { text-indent: 0 !important; font-family: var(--font-title); font-size: var(--text-h2); line-height: 1.3; margin-bottom: var(--space-4) !important; }
:lang(en) .ds-endletter__greeting { font-family: var(--font-hand); font-weight: 600; font-size: 34px; }
.ds-endletter__signoff { text-indent: 0 !important; text-align: right; margin: var(--space-3) 0 0 !important; color: var(--ink-soft); }
.ds-endletter__sign { text-indent: 0 !important; text-align: right; margin: 0 !important; font-family: var(--font-hand); font-weight: 600; font-size: 38px; line-height: 1.1; rotate: -4deg; }
.ds-endletter__sign::after {   /* a small heart in tin rose after the name */
  content: ""; display: inline-block; width: .5em; height: .45em; margin-left: .25em; vertical-align: .15em; background: var(--tin-rose);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 18'%3E%3Cpath d='M10 17 C 3 11, 0 8, 1.5 4.5 C 3 1, 7.5 1, 10 5 C 12.5 1, 17 1, 18.5 4.5 C 20 8, 17 11, 10 17 Z'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 18'%3E%3Cpath d='M10 17 C 3 11, 0 8, 1.5 4.5 C 3 1, 7.5 1, 10 5 C 12.5 1, 17 1, 18.5 4.5 C 20 8, 17 11, 10 17 Z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.ds-endnote {
  position: relative; flex: 1; min-width: 0; max-height: min(70vh, 680px); overflow: auto; overscroll-behavior: contain;
  margin-top: var(--space-7); padding: var(--space-6) var(--space-5) var(--space-5); rotate: 2deg; font-family: var(--font-body); color: var(--ink);
  background:
    repeating-linear-gradient(180deg, transparent 0 25px, color-mix(in srgb, var(--marigold) 30%, transparent) 25px 26px) 0 62px / 100% 26px,
    var(--tex-paper) 0 0 / 512px, color-mix(in srgb, var(--butter) 40%, var(--paper));
  background-blend-mode: normal, multiply, normal;
  box-shadow: var(--shadow-paper); font-size: 14px; line-height: 26px;
}
.ds-endnote__clip {   /* the paperclip, drawn in two passes of pencil, over the top edge */
  position: absolute; left: 26px; top: -16px; width: 22px; height: 58px; rotate: -8deg;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 58' fill='none' stroke-linecap='round'%3E%3Cpath d='M15 18 V 46 C 15 53, 5 53, 5 46 V 8 C 5 2, 18 2, 18 8 V 40 C 18 44, 11 44, 11 40 V 14' stroke='%237A6A55' stroke-width='2'/%3E%3Cpath d='M16 19 V 46 C 16 54, 6 54, 6 46 V 9' stroke='%23FBF6E9' stroke-opacity='.8' stroke-width='.8'/%3E%3C/svg%3E") center / contain no-repeat;
}
.ds-endnote__title { margin: 0 0 4px; font-family: var(--font-title); font-weight: 400; font-size: 20px; line-height: 26px; }
:lang(en) .ds-endnote__title { font-family: var(--font-hand); font-weight: 600; font-size: 24px; }
.ds-endnote ol { margin: 0; padding: 0; list-style: none; counter-reset: ps; }
.ds-endnote li { counter-increment: ps; position: relative; padding-left: 28px; margin-bottom: 8px; }
.ds-endnote li::before {   /* the number, written by hand */
  content: counter(ps); position: absolute; left: 2px; top: 0; font-family: var(--font-hand); font-weight: 600; font-size: 24px; line-height: 26px; color: var(--tin-rose);
}
.ds-endnote b { display: block; font-family: var(--font-title); font-weight: 400; font-size: 17px; }
:lang(en) .ds-endnote b { font-family: var(--font-hand); font-weight: 600; font-size: 20px; }
.ds-endnote p { margin: 0; color: var(--ink); }
@media (max-width: 760px) {
  .ds-ending { flex-direction: column; align-items: stretch; max-height: 86vh; overflow: auto; }
  .ds-endletter, .ds-endnote { max-height: none; overflow: visible; rotate: 0deg; }
  .ds-endnote { margin-top: 0; }
}

/* ---- Dusk: the room while the closing letter is read --------------------------
   Two flat layers over the whole desk scene, both faded in with opacity only (no blend modes): .ds-dusk is the
   evening sky's peach-to-lilac wash with a slight darkening under it, and .ds-dusk__lamp the desk lamp's warm pool,
   brighter than by day. The lamp itself switches on (lampGlow, as in step 1½). */
.ds-dusk { position: absolute; inset: 0; pointer-events: none; background: var(--dusk-wash), linear-gradient(var(--dusk-shade), var(--dusk-shade)); }
.ds-dusk__lamp { position: absolute; pointer-events: none; background: var(--dusk-lamp); }

/* ---- Signature: the maker's name in the corner of the page ------------------
   Like a painter signing a picture: Katherine Wu · 2026 in handwriting, small and faint, bottom right of every
   view, above everything but never in the way (no pointer events). */
.ds-signature {
  position: fixed; z-index: 40; right: var(--space-5); bottom: var(--space-3); pointer-events: none; user-select: none;
  font-family: var(--font-hand); font-weight: 600; font-size: 19px; line-height: 1; letter-spacing: .02em;
  color: var(--ink); opacity: .42; rotate: -3deg;
}

@media (prefers-reduced-motion: reduce) {
  .ds-envelope__flap { animation: none !important; }
  .ds-counttag__sway, .ds-counttag__nudge { animation: none !important; }
  .ds-note svg { animation: none; }
  .ds-pinnote, .ds-polaroid { animation: none !important; }
  .ds-lang .flip, .ds-sticker, .ds-tag, .ds-scrim { transition-duration: 1ms; }
}
