/* Postage.
   One sheet of paper under one lamp, up and to the left. The paper and the
   masthead are drawn by js/stock.js (the poem app's letterpress shader); every
   value below is set to agree with that lamp — a well is dark on its upper-left
   wall and catches light on its lower-right, and nothing on the page contradicts
   it. That agreement is the whole look; a shadow pointing the other way is the
   one thing that would break it.

   There is exactly one exception, and it is the app's subject: a STAMP is a
   separate piece of paper lying on the sheet, so a stamp — and nothing else —
   casts a shadow. It is always a `drop-shadow` filter, never a `box-shadow`,
   because a box-shadow is the shape of the box and would put a hard rectangle
   back under the edge js/perf.js just finished notching. */

@font-face { font-family: 'FNMono'; src: url('../fonts/PlexMono-400.woff2') format('woff2'); font-weight: 400; font-display: block; }
@font-face { font-family: 'FNMono'; src: url('../fonts/PlexMono-500.woff2') format('woff2'); font-weight: 500; font-display: block; }

:root {
  --stock: #EFEADE;          /* must match stock.js's uStock */
  --ink: #221F1A;
  --ink-2: #5F594E;
  --ink-3: #9C9484;
  --brick: #8A452C;          /* the one accent, and it appears only as ink */
  --key: #4A4339;            /* a button: warm graphite, not black */
  --hair: rgba(34,31,26,.13);
  /* both are replaced at boot with the paper's own colour, read off the sheet */
  --bar-bg: #EDE8DC;
  --sheet-bg: #F1ECE0;
  /* One typeface. A field note is typed, and once the display line came off the
     masthead there was nothing left for a second face to set — a loaded font
     that renders nowhere is just weight. */
  --mono: 'FNMono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* the lamp, expressed for CSS: dark up-left, light down-right */
  --lo: rgba(34,31,26,.20);
  --hi: rgba(255,253,247,.62);
  --deboss: -0.5px -0.5px 0.6px var(--lo), 0.7px 0.8px 0.7px var(--hi);
  --well: inset 1.5px 1.5px 2.5px rgba(34,31,26,.17), inset -1px -1.5px 1.5px rgba(255,253,247,.55);
  --lift: 1px 2px 2px -1px rgba(34,31,26,.12), 6px 14px 22px -14px rgba(34,31,26,.34);
}

/* ── the stack, declared once ────────────────────────────────────────────────
   Two of these ranges are written by JS (the fan and the pile lay their own
   stamps out), so the numbers have to live somewhere they can be read together.
   Left to grow on their own the fan reached 30 while the overlays sat at 20,
   and six stamps painted over the top of the lightbox scrim — a positioned
   element with an explicit z-index beats every `auto` sibling however late in
   the DOM, and beats a smaller explicit one wherever it is.

     the fan's stamps          2 – 10   (js: layFan)
     the pile's stamps         1 – 40   (js: layPile)
     the print strip              50
     the tabs                     52
     shuffle                      54
     a stamp held up to the light 70
     its catalogue entry          71
     the overlays                100
     the desktop/mobile chip     120
*/
* { box-sizing: border-box; }
/* An author `display` beats the hidden attribute — without this the bar and both
   overlays are permanently on screen. */
[hidden] { display: none !important; }

html { scroll-behavior: smooth; }
/* The UA focus ring is a blue rounded rectangle, which is the one shape this
   app does not have. */
:focus-visible { outline: 1px solid rgba(138,69,44,.5); outline-offset: 4px; border-radius: 1px; }
:focus:not(:focus-visible) { outline: none; }
* { -webkit-tap-highlight-color: transparent; }
/* a dark scrollbar down the edge of a sheet of paper is a window frame */
* { scrollbar-color: rgba(34,31,26,.22) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(34,31,26,.18); border-radius: 9px; }
::-webkit-scrollbar-thumb:hover { background: rgba(34,31,26,.3); }
body {
  margin: 0;
  background: var(--desk, var(--stock));
  color: var(--ink);
  font-family: var(--mono);
  font-size: 16px;
  line-height: 1.55;
  letter-spacing: -.004em;
  -webkit-font-smoothing: antialiased;
}
/* ── the stage ────────────────────────────────────────────────────────────────
   #device is the app's world: the whole window, or an iPhone standing on a desk.
   Nothing inside reads the viewport or the document scroll — the sheet is sized
   to this box and the scroll offset comes from #scroll — which is what lets the
   framed view and a real phone be the same app rather than two layouts. */
#device {
  position: relative;
  width: 100vw;
  height: 100dvh;
  overflow: hidden;
  background: var(--stock);
  /* The app measures ITSELF, not the window. Inside the phone frame the viewport
     is still 1240 wide, so a `@media (max-width: 680px)` rule never fires and the
     framed view quietly lays itself out as a desktop — six marks at desktop size
     in a 362px column, one per row, with the wordmark pushed off the screen. */
  container-type: inline-size;
  container-name: app;
}
#scroll {
  position: absolute;
  inset: 0;
  /* a column, so the tabs are pushed to the bottom of a short screen rather than
     sitting wherever the content happens to end */
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  /* No rubber band. The wordmark is drawn into the paper canvas and positioned
     by `scrollTop`, and during an overscroll bounce the browser moves the
     CONTENT past the scroll limit without changing `scrollTop` — there is no
     number to read — so the drawn type stays put while everything else bounces,
     and it reads as the headline coming loose from the page. */
  overscroll-behavior: none;
  -webkit-overflow-scrolling: touch;
}
#stock { position: absolute; inset: 0; display: block; }

/* ⚠️ No z-index here. `mix-blend-mode` blends with the backdrop of its own
   stacking context, and a `z-index` on this wrapper makes one — which cut the
   marks off from the paper canvas and left each of them standing in a white
   rectangle. The sheet still paints above the canvas without it: both are
   positioned, and #scroll comes second in the DOM. */
.sheet-wrap {
  position: relative;
  flex: 1 0 auto;
  width: 100%;
  max-width: 968px;
  margin: 0 auto;
  padding: 0 28px 0;
}

/* ── the masthead ─────────────────────────────────────────────────────────────
   Drawn INTO the paper by the shader. The DOM keeps the words for anyone
   reading with something other than eyes, and holds the space open; the moment
   the press is up, `pressed` hands the pixels over to it. */
/* The home screen is two parts: the hero, which is most of it, and the bed. Both
   are set from the DEVICE's height rather than the window's — inside a phone
   frame `vh` is the wrong box. */
.mast {
  min-height: calc(var(--dev-h, 900px) * 0.46);
  padding: calc(1vh + 8px) 0 1vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0;
  text-align: center;
}
body.has-pages .mast { min-height: 0; padding: calc(4vh + 8px) 0 3vh; }
.mast h1 {
  margin: 0;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 20px;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--ink-2);
}
/* The row of marks — the one place in the app where ink sits ON the paper rather
   than in it, because that is what a stamp does. They are the real prints, cut
   out of the app's own pages with the paper divided out, so `multiply` puts them
   back on paper: the sheet's grain and the lamp's pool come through the ink
   instead of the mark reading as a sticker laid on top. */
/* The wordmark is drawn INTO the paper by the shader; the DOM keeps the words
   for anyone reading with something other than eyes, and holds the space open.
   The moment the press is up, `pressed` hands the pixels over to it. */
.mast .sub {
  /* tight under the wordmark: they are one lockup */
  margin: 7px 0 0;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .10em;
  color: var(--ink-2);
}
body.pressed .mast h1, body.pressed .mast .sub { visibility: hidden; }
body.has-pages .mast { padding: 7vh 0 6vh; }

/* ── the fan ──────────────────────────────────────────────────────────────────
   A few stamps laid over one another the way they come out of a glassine
   envelope: the middle one square on and forward, the rest leaning away and
   tucked behind it.

   These are the one thing in the app that is genuinely ABOVE the paper — a
   stamp is a separate piece of paper lying on the sheet — so they are the one
   thing allowed a shadow. It is a `drop-shadow` filter and never a `box-shadow`:
   a box-shadow is the shape of the box, which would put a hard rectangle back
   under an edge the perforator spent all that trouble notching. */
.fan {
  position: relative;
  /* An explicit share of the DEVICE's height, not "whatever is left": as a
     flex-grow box the fan swallowed the whole hero, sized its stamps against
     that, and printed them straight over the wordmark and off the top of the
     screen. The stamps are then fitted INSIDE this box by sizeFan(). */
  flex: 0 0 auto;
  height: clamp(150px, calc(var(--dev-h, 900px) * 0.30), 320px);
  margin: 0 0 18px;
  touch-action: pan-y;
  cursor: grab;
  user-select: none;
}
.fan:active { cursor: grabbing; }
.slide {
  position: absolute;
  top: 50%;
  left: 50%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  will-change: transform;
  transform-origin: 50% 62%;   /* they pivot about a point low in the fan, as a hand of cards does */
}
.fan.gliding .slide { transition: transform .46s cubic-bezier(.22,.7,.2,1), opacity .46s ease; }
.slide img {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
  /* one lamp, up and to the left, so the shadow falls down and to the right */
  filter: drop-shadow(1px 2px 2px rgba(34,31,26,.20)) drop-shadow(5px 11px 14px rgba(34,31,26,.22));
}
@media (prefers-reduced-motion: reduce) { .fan.gliding .slide { transition: none; } }
@container app (max-width: 680px) { .fan { height: clamp(130px, calc(var(--dev-h, 900px) * 0.26), 240px); margin-bottom: 16px; } }

/* ── the way in ───────────────────────────────────────────────────────────── */
.bench {
  margin: 30px 0 1vh;
  display: flex;
  justify-content: center;
  /* not `stretch`: a flex row stretches its items to the row's height, which
     silently beats `aspect-ratio` and makes the square bed as tall as whatever
     else is in the row */
  align-items: flex-start;
}
/* The way in is the page itself, pressed empty into the sheet: the bed where the
   photograph will lie on the left, the paper and the waiting stamp on the right.
   Nothing here is a flat fill — a blank rectangle of one colour cannot sit on
   paper that is graded by a lamp, and the mismatch is the first thing you see. */
.page-in {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  /* Square, not the page's own 3:4 — a bed the shape of the output is a tall
     empty box that pushes everything else off the screen, and what goes in it is
     a photograph of any shape anyway. */
  max-width: min(560px, calc(var(--dev-h, 900px) * 0.42));
  aspect-ratio: 4 / 3;
  border-radius: 14px;
}
/* A dashed rule cut into the sheet: the same stroke twice, once in the lamp's
   shade offset up and left, once lit and offset down and right — the deboss
   applied to a line instead of to type, so the outline belongs to the paper
   rather than being drawn on it. */
.bed { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.bed rect { fill: none; stroke-width: 2.2; stroke-dasharray: 13 9; stroke-linecap: round; transition: stroke .35s ease; }
.bed .lo { stroke: rgba(34,31,26,.46); transform: translate(-0.6px, -0.6px); }
.bed .hi { stroke: rgba(255,253,247,.9); transform: translate(0.6px, 0.6px); }
body.dragging .bed .lo { stroke: rgba(34,31,26,.66); }
body.dragging .bed .hi { stroke: rgba(255,253,247,1); }

/* once the notebook has pages, the empty one is not one of them */
body.has-pages .bench { display: none; }

.add { display: none; margin: 34px 0 0; justify-content: center; }
body.has-pages .add { display: flex; }

/* ── the pages ────────────────────────────────────────────────────────────── */
.pages { display: flex; flex-direction: column; align-items: center; gap: 12vh; }
.page { width: 100%; }
/* A tall page needs a narrower column than a wide one, or one page fills the
   whole window and you cannot see it. Height leads: the width follows from the
   ratio, so a short window shrinks the page instead of cropping it. Measured
   against the DEVICE, not the window — inside a phone `dvh` is the wrong box. */
.page[data-shape="portrait"] { max-width: min(560px, calc(var(--dev-h, 900px) * 0.78 * 3 / 4)); }
.page[data-shape="landscape"] { max-width: min(968px, calc(var(--dev-h, 900px) * 0.82 * 4 / 3)); }
.page { animation: lay .7s cubic-bezier(.2,.7,.2,1) both; }
@keyframes lay { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .page { animation: none; } html { scroll-behavior: auto; } }

/* ── the stamp ────────────────────────────────────────────────────────────────
   Everything else in this app is pressed INTO the paper — the wordmark is
   bitten into it, the empty bed is a well cut in it, a button is a key set into
   it. A stamp is the exception, and it is the exception for a real reason: it
   is a separate piece of paper lying on the sheet. So it is the one thing that
   gets a shadow, and the shadow is a `drop-shadow` filter, which follows the
   perforated alpha, rather than a `box-shadow`, which is the shape of the box
   and would put a hard rectangle back under a die-cut edge. */
.plate {
  position: relative;
  /* set per stamp from the shape it was printed at, so an album holding both
     shapes shows each one as it is */
  aspect-ratio: var(--ar, 3 / 4);
}
.plate .poster {
  width: 100%;
  height: 100%;
  display: block;
  /* contain, not cover: the plate is already the stamp's own aspect, and cover
     would crop a tooth off the edge if the press came back a pixel out */
  object-fit: contain;
  cursor: zoom-in;
  filter: drop-shadow(1px 2px 2px rgba(34,31,26,.20)) drop-shadow(6px 14px 18px rgba(34,31,26,.24));
}

/* ── the stamp before it is printed ───────────────────────────────────────────
   The photograph, already die-cut to the shape it is going to be, with the four
   pieces of lettering standing where they are going to stand. The perforation
   is an SVG mask built by js/perf.js from the SAME hole geometry that punches
   the finished stamp, so the wait is the stamp you are going to get rather than
   a spinner — and the two can never disagree about the gauge.

   The mask goes on inline, because the hole pattern depends on the aspect. */
.mock {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* the unprinted selvedge: 6% of the short side, the same margin the press is
     asked to leave */
  padding: 6%;
  background: var(--sheet-bg, #F1ECE0);
  mask-size: 100% 100%;
  -webkit-mask-size: 100% 100%;
  mask-repeat: no-repeat;
  -webkit-mask-repeat: no-repeat;
  filter: drop-shadow(1px 2px 2px rgba(34,31,26,.16)) drop-shadow(5px 11px 14px rgba(34,31,26,.18));
}
/* the photograph, drained most of the way to the paper: the design has not been
   drawn yet, and a full-colour photograph in a stamp-shaped hole is a sticker */
/* ⚠️ Explicit width and height, not `inset: 6%` with `width: auto`. For a
   REPLACED element — an <img> — `auto` means its intrinsic size, not "fill the
   insets", so an absolutely-positioned photograph laid out that way arrives at
   1400px wide, anchored at the top left inset and running off the other two
   edges. It looks exactly like a broken aspect ratio. */
.mock-photo {
  position: absolute;
  left: 6%;
  top: 6%;
  width: 88%;
  height: 88%;
  object-fit: cover;
  display: block;
  filter: saturate(.34) contrast(1.06) sepia(.16) brightness(1.06);
}
/* one band of paper at each end, so small type can be read over a photograph
   without a box round it */
.mock::after {
  content: '';
  position: absolute;
  inset: 6%;
  pointer-events: none;
  background:
    linear-gradient(rgba(241,236,224,.9) 0%, rgba(241,236,224,.4) 12%, rgba(241,236,224,0) 22%),
    linear-gradient(0deg, rgba(241,236,224,.9) 0%, rgba(241,236,224,.45) 14%, rgba(241,236,224,0) 26%);
}
/* the four things a stamp says, and nothing else */
.mock > span {
  position: absolute;
  z-index: 2;
  font-family: var(--mono);
  color: var(--ink-2);
  line-height: 1;
  white-space: nowrap;
  text-shadow: 0 1px 1.5px rgba(255,253,247,.75);
}
.s-country {
  top: 10%;
  left: 10%;
  right: 10%;
  text-align: center;
  font-size: clamp(6px, 3.1cqw, 12px);
  letter-spacing: .2em;
  overflow: hidden;
  text-overflow: ellipsis;
}
.s-value {
  left: 10%;
  bottom: 9%;
  font-size: clamp(9px, 5.2cqw, 22px);
  letter-spacing: .01em;
  color: var(--brick);
}
.s-year {
  right: 10%;
  bottom: 10%;
  font-size: clamp(6px, 2.6cqw, 10px);
  letter-spacing: .14em;
  color: var(--ink-3);
}
/* On the bottom band with the value and the year, not floating over the middle
   of the picture: small type over an engraving is small type nobody can read,
   and the paper at the foot is the only quiet place on a stamp. */
.s-title {
  left: 30%;
  right: 30%;
  bottom: 10%;
  text-align: center;
  font-size: clamp(6px, 2.7cqw, 10.5px);
  letter-spacing: .08em;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
}
/* the card is the container the lettering is sized against, so a stamp in the
   album and a stamp on the press are lettered at the same proportion */
.page { container-type: inline-size; }

/* ── on the press ─────────────────────────────────────────────────────────────
   What the press does to a photograph is take the colour out of it and put a
   little of it back as ink, so that is what the wait shows: the picture drains
   further toward the paper and comes back, with one band of light travelling
   down it like a cylinder coming round. It stops the moment the stamp lands. */
[data-state="printing"] .mock-photo { animation: drain 3s ease-in-out infinite; }
[data-state="printing"] .mock::before {
  content: '';
  position: absolute;
  left: -20%;
  right: -20%;
  height: 46%;
  top: -46%;
  z-index: 3;
  background: linear-gradient(
    rgba(255,253,247,0) 0%,
    rgba(255,253,247,.34) 45%,
    rgba(138,69,44,.10) 62%,
    rgba(255,253,247,0) 100%);
  animation: platen 3s cubic-bezier(.5,0,.5,1) infinite;
  pointer-events: none;
}
@keyframes drain {
  0%   { filter: saturate(.34) contrast(1.06) sepia(.16) brightness(1.06); }
  46%  { filter: saturate(.12) contrast(1.2) sepia(.3) brightness(1.1); }
  100% { filter: saturate(.34) contrast(1.06) sepia(.16) brightness(1.06); }
}
@keyframes platen {
  0%   { top: -46%; opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { top: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  [data-state="printing"] .mock-photo { animation: none; }
  [data-state="printing"] .mock::before { animation: none; opacity: 0; }
}
/* the lettering fades in and out with it, like ink deciding to take */
[data-state="printing"] .mock > span { animation: settling 3s ease-in-out infinite; }
@keyframes settling {
  0%, 100% { opacity: .55; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { [data-state="printing"] .mock > span { animation: none; } }

/* What the press is doing, said across the middle of the stamp — where there is
   room, and where the eye already is. Placed rather than left in flow: every
   other child of .mock is positioned, so a block one would sit alone in the top
   left corner on top of the country. */
.status {
  position: absolute;
  left: 12%;
  right: 12%;
  top: 50%;
  transform: translateY(-50%);
  z-index: 4;
  text-align: center;
  font-family: var(--mono);
  font-size: clamp(7px, 2.6cqw, 10px);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--brick);
  text-shadow: 0 1px 2px rgba(255,253,247,.85);
}
.status.bad { text-transform: none; letter-spacing: 0; color: #7d3030; line-height: 1.5; }

/* ── the line under a page ────────────────────────────────────────────────── */
.meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  flex-wrap: wrap;
  padding: 20px 2px 0;
}
/* the details, only when asked for */
.details {
  display: grid;
  gap: 12px;
  max-width: 420px;
  margin: 22px auto 0;
  padding: 20px 22px;
  border-radius: 10px;
  box-shadow: var(--well);
  animation: fade .25s ease both;
}
.details .row { display: flex; align-items: baseline; gap: 14px; }
.details .row.end { justify-content: flex-end; gap: 20px; padding-top: 4px; }
.details .row-l {
  flex: 0 0 auto;
  width: 5.5em;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-3);
  text-shadow: var(--deboss);
}
.details .row-v { font-family: var(--mono); font-size: 11px; color: var(--ink-2); }
.details .field { flex: 1 1 auto; }

.field {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--ink);
  background: transparent;
  border: 0;
  padding: 3px 0 4px;
  box-shadow: 0 1px 0 var(--hair);
  min-width: 0;
}
.field:hover { box-shadow: 0 1px 0 rgba(34,31,26,.28); }
.field:focus { outline: none; box-shadow: 0 1px 0 var(--brick); }
.field.place { width: auto; }
.field.year { width: 6ch; flex: 0 0 auto; }
.field::placeholder { color: var(--ink-3); }

/* ── things you can press ───────────────────────────────────────────────────
   Two, and only two. A PRIMARY is a well cut into the paper with its label
   inked in it — big enough to read across the room and to hit with a thumb,
   and still part of the sheet rather than a filled slab lying on it. Everything
   else is a word inked on the paper with a rule under it. */
.primary {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: #F6F1E5;
  background: var(--key);
  border: 0;
  padding: 12px 20px;
  border-radius: 999px;
  cursor: pointer;
  /* A soft black key set INTO the sheet: the paper's own edge in shade above it
     and lit below it (the lamp is up and to the left), a dark rim inside the
     top of the key where the paper overhangs, and one lit hair inside the
     bottom. No cast shadow — nothing in this app floats. */
  box-shadow:
    0 -1px 0 rgba(34,31,26,.22),
    0 1.5px 0 rgba(255,253,247,.68),
    inset 1px 1.5px 2.5px rgba(0,0,0,.4),
    inset -1px -1.5px 1.5px rgba(255,255,255,.13);
  transition: background .18s ease, box-shadow .18s ease, transform .08s ease;
}
.primary:hover { background: #564E42; }
.primary:active {
  transform: translateY(.5px);
  box-shadow:
    0 -1px 0 rgba(34,31,26,.26),
    0 1px 0 rgba(255,253,247,.5),
    inset 1.5px 2.5px 4px rgba(0,0,0,.5),
    inset -1px -1.5px 1.5px rgba(255,255,255,.1);
}
.primary:disabled { background: #6C665A; color: #E6E0D2; cursor: default; box-shadow: none; }
.primary.small { font-size: 10.5px; padding: 10px 17px; }
/* never wider than the bed it stands in */
#choose { max-width: calc(100% - 28px); }

/* Ink, not a box: a word inked onto the sheet, which goes down into it when you
   press it. No fills, no borders, no rounded rectangles. */
/* close to the lockup above it, and a clear gap to the bed below */
#showExamples { margin: 16px auto 0; }

.ink {
  /* A rule under a word is as wide as the word. As a block in a flex column an
     `.ink` stretches, and the underline runs the width of the screen. */
  align-self: center;
  width: fit-content;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--brick);
  background: none;
  border: 0;
  padding: 4px 0 5px;
  cursor: pointer;
  box-shadow: 0 1px 0 rgba(138,69,44,.3);
  transition: color .15s ease, box-shadow .15s ease, transform .08s ease;
}
.ink:hover { box-shadow: 0 1px 0 var(--brick); }
.ink:active { transform: translateY(.5px); color: #6f3722; }
.ink:disabled { color: var(--ink-3); box-shadow: none; cursor: default; }
/* quieter than ink: for the words that open something rather than do something.
   After `.ink`, or the cascade hands it back its colour and its rule. */
.ink.quiet-ink { color: var(--ink-3); box-shadow: none; }
.ink.quiet-ink:hover { color: var(--ink-2); box-shadow: 0 1px 0 rgba(34,31,26,.18); }

.chip {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: none;
  border: 0;
  padding: 6px 9px;
  border-radius: 2px;
  cursor: pointer;
  text-shadow: var(--deboss);
  transition: color .15s ease, box-shadow .2s ease;
}
.chip:hover { color: var(--ink); }
.chip[aria-pressed="true"] { color: var(--ink); box-shadow: var(--well); }
.chip.quiet { color: var(--ink-3); }
.chip.quiet:hover { color: var(--ink-2); }
.seg { display: inline-flex; gap: 2px; }

/* ── the two screens ──────────────────────────────────────────────────────────
   Generate is where pages are made; Library is where they are kept. The tabs sit
   under the strip, which belongs to Generate. */
/* The nav floats: a small strip of card lying on the sheet rather than a bar
   welded to its foot. It is the one thing in the app allowed a shadow, because
   it is the one thing that is genuinely above the paper. */
.tabs {
  position: sticky;
  bottom: calc(30px + env(safe-area-inset-bottom));
  z-index: 52;
  align-self: center;
  display: flex;
  gap: 4px;
  margin: 18px auto 0;
  padding: 6px;
  border-radius: 999px;
  background: var(--bar-bg);
  box-shadow:
    0 1px 0 rgba(255,253,247,.6) inset,
    0 -1px 0 rgba(34,31,26,.10) inset,
    0 2px 3px -1px rgba(34,31,26,.10),
    0 10px 22px -12px rgba(34,31,26,.34);
}
.tab {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: none;
  border: 0;
  padding: 11px 22px 10px;
  border-radius: 999px;
  cursor: pointer;
  text-shadow: var(--deboss);
  transition: color .15s ease, box-shadow .2s ease;
}
.tab:hover { color: var(--ink-2); }
.tab[aria-pressed="true"] { color: var(--ink); box-shadow: var(--well); }
/* the strip keeps clear of the bar above it */
.bar { margin-bottom: 0; }

/* the album */
/* room at the foot for Shuffle and the tabs, which both float over the pile */
.library { padding: 2vh 0 calc(150px + env(safe-area-inset-bottom)); }
/* An empty shelf is the library's own hero: one stamp, a heading, a line and
   the way back to the press — the same shape as the front screen, so the two
   read as one app rather than a screen and its apology. */
.lib-empty {
  min-height: calc(var(--dev-h, 900px) - 168px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: 0;
}
.lib-mark { height: clamp(90px, 16dvh, 150px); margin: 0 0 26px; }
.lib-mark img {
  height: 100%; width: auto; max-width: 100%; opacity: .8;
  filter: drop-shadow(1px 2px 2px rgba(34,31,26,.16)) drop-shadow(5px 12px 15px rgba(34,31,26,.2));
}
.lib-empty h1 {
  margin: 0;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 20px;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.lib-empty .sub {
  margin: 7px 0 24px;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .10em;
  color: var(--ink-2);
}
body.pressed .lib-empty h1, body.pressed .lib-empty .sub { visibility: hidden; }
/* ── the pile ─────────────────────────────────────────────────────────────────
   The album is not a grid. Stamps come loose, and this is how loose stamps
   really sit before anybody mounts them: tipped out down the page, overlapping,
   no two the same way up. The layout is computed in js (`layPile`) from a SEED
   rather than stored, so the pile survives a reload, a rerender lays it out
   identically, and Shuffle is one increment of a number.

   Every stamp is positioned from its own custom properties, and the transform
   is written in one place — here — so the lift can override the whole thing
   with a single declaration and CSS carries it on one curve. */
.pile { position: relative; margin: 0 0 12px; }
.loose {
  position: absolute;
  left: 0;
  top: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  transform: translate(var(--x, 0), var(--y, 0)) rotate(var(--rot, 0deg));
  transition: transform .52s cubic-bezier(.22,.7,.2,1), filter .34s ease, opacity .34s ease;
  will-change: transform;
}
/* A slower, looser curve while shuffling: the whole pile is being tipped out
   again, and it should land rather than snap. */
.pile.shuffling .loose { transition: transform .66s cubic-bezier(.24,.9,.24,1); }
.loose img {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
  /* one lamp, up and to the left. The shadow follows the perforated alpha
     because it is a filter and not a box-shadow. */
  filter: drop-shadow(1px 1.5px 1.5px rgba(34,31,26,.22)) drop-shadow(4px 9px 12px rgba(34,31,26,.20));
}
.loose:hover { filter: brightness(1.03); }
/* Held up to the light: the same node, moved and grown to the middle of what is
   on screen. Not a second screen and not a copy — that is what says the thing in
   the middle is the thing you put your finger on. */
.loose.up {
  z-index: 70 !important;
  cursor: zoom-out;
}
.loose.up img {
  filter: drop-shadow(2px 4px 4px rgba(34,31,26,.24)) drop-shadow(10px 26px 34px rgba(34,31,26,.34));
}
/* everything else goes soft behind it. The blur is on the SIBLINGS and never on
   the pile, because a filter on the parent blurs the lifted one with them. */
.pile.lifting .loose:not(.up) { filter: blur(5px); opacity: .5; }
@media (prefers-reduced-motion: reduce) {
  .loose, .pile.shuffling .loose { transition: none; }
}

/* the catalogue entry, under whichever stamp is being held up */
.lifted {
  position: absolute;
  left: 0;
  right: 0;
  /* `top` is written by lift(), from the bottom edge of the stamp it belongs to */
  z-index: 71;
  display: grid;
  justify-items: center;
  gap: 3px;
  padding: 0 20px;
  text-align: center;
  pointer-events: none;
  animation: fade .3s ease both;
}
.lifted > * {
  font-family: var(--mono);
  text-shadow: var(--deboss);
}
.lifted .lift-l { font-size: 11.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink); }
.lifted .lift-r { font-size: 10px; letter-spacing: .12em; color: var(--ink-2); }
.lifted .lift-perf { font-size: 9.5px; letter-spacing: .1em; color: var(--ink-3); }
.lifted #liftDownload { pointer-events: auto; margin-top: 6px; }

/* Shuffle. It belongs to the album and sits above the tabs — the one control on
   a screen whose whole content is one gesture. */
.pilebar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(96px + env(safe-area-inset-bottom));
  z-index: 54;
  display: flex;
  justify-content: center;
  pointer-events: none;
}
/* A small pill of the same stock, like the tabs below it. Left as bare ink it
   was perfectly clickable and completely unreadable: a quiet word floats over
   whatever stamp happens to be under it, and in a pile there is always one. */
.pilebar .ink {
  pointer-events: auto;
  color: var(--ink-2);
  padding: 9px 20px 8px;
  border-radius: 999px;
  background: var(--bar-bg);
  box-shadow:
    0 1px 0 rgba(255,253,247,.6) inset,
    0 -1px 0 rgba(34,31,26,.10) inset,
    0 2px 3px -1px rgba(34,31,26,.10),
    0 10px 22px -12px rgba(34,31,26,.34);
}
.pilebar .ink:hover { color: var(--ink); box-shadow:
    0 1px 0 rgba(255,253,247,.6) inset,
    0 -1px 0 rgba(34,31,26,.10) inset,
    0 2px 3px -1px rgba(34,31,26,.14),
    0 12px 26px -12px rgba(34,31,26,.4); }
/* nothing but the stamp, while a stamp is up */
body:has(.pile.lifting) .pilebar { display: none; }
/* on the library screen the sheet's own furniture steps back */
body.on-library .mast, body.on-library .bench, body.on-library .pages,
body.on-library .add, body.on-library .foot, body.on-library .bar { display: none; }

/* ── the strip along the bottom ───────────────────────────────────────────────
   Paper, not glass: a torn strip of the same stock lying over the sheet, lit by
   the same lamp. A blurred translucent bar is the one thing that would announce
   this as an app. */
.bar {
  position: sticky;
  bottom: 0;
  z-index: 50;
  margin-top: 12vh;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
  text-align: center;
  background: var(--bar-bg);
  box-shadow: 0 -1px 0 rgba(255,253,247,.45), 0 -14px 30px -20px rgba(34,31,26,.55);
}
.bar .count {
  text-align: center;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-3);
  text-shadow: var(--deboss);
}
/* Clear on the left, the thing you came for on the right. */
.bar-acts {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
}
.ink.press { font-size: 11.5px; }

/* ── the foot ─────────────────────────────────────────────────────────────── */
.foot { margin: 1.5vh 0 0.5vh; text-align: center; }
.foot-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; justify-content: center; }
/* the settings panel: the same paper card the prompt uses */
/* a few rows do not need the prompt's measure */
#panel .card { max-width: 460px; }
.rows { padding: 18px 22px 4px; display: grid; gap: 16px; }
.prow { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.prow.end { justify-content: flex-end; padding-top: 2px; }
.prow-l {
  flex: 0 0 auto;
  width: 4.5em;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-3);
  text-shadow: var(--deboss);
}
.foot-note {
  margin: 18px 0 0;
  font-family: var(--mono);
  font-size: 10px;
  line-height: 2;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-3);
  text-shadow: var(--deboss);
}
.foot-note a, .credit a { color: var(--brick); text-decoration: none; box-shadow: 0 1px 0 rgba(138,69,44,.3); }
.foot-note em { font-style: normal; color: var(--ink-2); }

/* ── overlays ─────────────────────────────────────────────────────────────── */
.overlay {
  position: absolute; inset: 0; z-index: 100;
  background: rgba(26,24,20,.62);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  animation: fade .25s ease both;
}
@keyframes fade { from { opacity: 0 } to { opacity: 1 } }
.overlay .card {
  background: var(--sheet-bg);
  max-width: 720px; width: 100%; max-height: 84vh;
  display: flex; flex-direction: column;
  box-shadow: 0 40px 80px -30px rgba(0,0,0,.6);
}
.card-head { display: flex; align-items: center; gap: 12px; padding: 16px 20px; box-shadow: 0 1px 0 var(--hair); }
.card-head .which { font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); }
.card-head .close { margin-left: auto; }
.overlay pre {
  margin: 0; padding: 22px 24px; overflow: auto;
  font-family: var(--mono); font-size: 11.5px; line-height: 1.95;
  white-space: pre-wrap; color: var(--ink-2);
}
.credit { margin: 0; padding: 14px 24px 20px; box-shadow: 0 -1px 0 var(--hair); font-family: var(--mono); font-size: 10px; line-height: 1.9; color: var(--ink-3); }

/* ── the light ────────────────────────────────────────────────────────────────
   A page at a time over the room, with the next one just off the edge and the
   set counted underneath — so it is plain that it turns. The scrim closes; the
   pages, the arrows and the dots do not. */
.light { display: block; padding: 0; cursor: zoom-out; overflow: hidden; }
.ltrack {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 88%;
  transform: translateX(0);
  translate: -50% -50%;
  touch-action: pan-y;
}
.ltrack.gliding { transition: transform .34s cubic-bezier(.22,.7,.2,1); }
.lpage {
  position: absolute;
  top: 50%;
  left: 50%;
  max-width: 84%;
  max-height: 100%;
  cursor: grab;
  user-select: none;
  -webkit-user-drag: none;
}
/* the neighbours are dimmed: the one in the middle is the one you are reading,
   and the other two are there to say which way it turns */
.lpage[data-at="-1"], .lpage[data-at="1"] { opacity: .38; }
.lpage[data-at="-1"] { transform: translate(-150%, -50%); }
.lpage[data-at="0"] { transform: translate(-50%, -50%); }
.lpage[data-at="1"] { transform: translate(50%, -50%); }
.light .cap {
  position: absolute;
  left: 0; right: 0; bottom: 14px;
  margin: 0;
  text-align: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #B6AE9E;
}
.dots {
  position: absolute;
  left: 0; right: 0; bottom: 40px;
  display: flex;
  justify-content: center;
  gap: 9px;
}
.dots .dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(239,234,222,.32);
  cursor: pointer;
  transition: background .2s ease, transform .2s ease;
}
.dots .dot[aria-current="true"] { background: #EFEADE; transform: scale(1.25); }
.light .nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0 0 3px;
  font-family: var(--mono);
  font-size: 26px;
  line-height: 1;
  color: #EFEADE;
  background: rgba(239,234,222,.12);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  transition: background .15s ease;
}
.light .nav:hover { background: rgba(239,234,222,.24); }
.light .prev { left: max(6px, 2%); }
.light .next { right: max(6px, 2%); }
@container app (max-width: 640px) {
  .light .nav { width: 38px; height: 38px; font-size: 22px; }
  .dots { bottom: 36px; }
}

/* ── the phone ────────────────────────────────────────────────────────────────
   iPhone 17 Pro, 402 × 874 logical points. Offered only where there is room and
   a real pointer — on an actual phone the app is already the phone.

   Height drives width through the aspect so a short window scales both axes
   together instead of going squat, and every measurement on the device is
   written `length × ratio`: CSS cannot divide a length by a length to get a
   number, and a `--ph-h / 874` factor silently collapses to zero. */
/* Top right, opposite the masthead, and outside #device — in the phone view it
   is on the desk beside the phone rather than on its screen. */
#view { display: none; }
@media (min-width: 780px) and (min-height: 640px) and (pointer: fine) {
  #view {
    display: inline-flex;
    position: fixed;
    top: 16px;
    right: 18px;
    z-index: 120;
  }
}
#view .chip { text-shadow: var(--deboss); }
/* nothing but the picture, while the picture is up */
body:has(.overlay:not([hidden])) #view { display: none; }
#view .chip[aria-pressed="true"] { color: var(--ink); box-shadow: var(--well); }
body.phone {
  --desk: #2A2723;
  display: grid;
  place-items: center;
  min-height: 100dvh;
}
body.phone #device {
  --ph-h: min(874px, 92dvh);
  width: calc(var(--ph-h) * 402 / 874);
  height: var(--ph-h);
  border-radius: calc(var(--ph-h) * 56 / 874);
  /* Two casts, not one: a tight contact shadow that says the phone is resting on
     something, and a wide ambient one that gives it height. One alone reads as a
     sticker on a dark page. */
  box-shadow:
    0 26px 50px -26px rgba(0,0,0,.6),
    0 62px 130px -30px rgba(0,0,0,.7),
    0 0 0 calc(var(--ph-h) * 11 / 874) #1b1e22,
    0 0 0 calc(var(--ph-h) * 13 / 874) #3d434b,
    0 0 0 calc(var(--ph-h) * 14 / 874) #14171a;
}
/* on the desk the chips are not on paper any more, so nothing is pressed into
   anything: light type, and the chosen one simply brighter */
body.phone #view .chip { color: #8A8377; text-shadow: none; box-shadow: none; }
body.phone #view .chip:hover { color: #C9C1B2; }
body.phone #view .chip[aria-pressed="true"] { color: #EFEADE; box-shadow: 0 1px 0 rgba(239,234,222,.35); }
/* The sheet runs under the phone's top edge, as it would on a real one — but a
   headline sliced in half by an opaque black pill reads as a hole in the paper,
   not as a notch. So the paper's own colour fades in over the last centimetre
   and the type dissolves into it before it gets there. */
.topfade, .island, .homebar { display: none; }
body.phone .topfade {
  display: block;
  position: absolute;
  top: 0; left: 0; right: 0;
  height: calc(var(--ph-h) * 62 / 874);
  background: linear-gradient(var(--top-bg, #EFEADE) 42%, transparent);
  z-index: 20;
  pointer-events: none;
}
body.phone .island {
  display: block;
  position: absolute;
  top: calc(var(--ph-h) * 11 / 874);
  left: 50%;
  transform: translateX(-50%);
  width: calc(var(--ph-h) * 124 / 874);
  height: calc(var(--ph-h) * 35 / 874);
  border-radius: 999px;
  background: #000;
  z-index: 25;
  pointer-events: none;
}
body.phone .homebar {
  display: block;
  position: absolute;
  bottom: calc(var(--ph-h) * 9 / 874);
  left: 50%;
  transform: translateX(-50%);
  width: calc(var(--ph-h) * 140 / 874);
  height: calc(var(--ph-h) * 5 / 874);
  border-radius: 999px;
  background: rgba(34,31,26,.35);
  z-index: 25;
  pointer-events: none;
}
/* the app keeps clear of the island and the home indicator */
body.phone .mast { padding-top: calc(var(--ph-h) * 74 / 874); }
body.phone .bar { padding-bottom: calc(var(--ph-h) * 26 / 874); }
/* a phone has no scrollbar down the side of the paper */
body.phone #scroll { scrollbar-width: none; }
body.phone #scroll::-webkit-scrollbar { display: none; }

/* ── narrow ───────────────────────────────────────────────────────────────── */
@container app (max-width: 680px) {
  .sheet-wrap { padding-inline: 20px; }
  /* The home screen is two parts: the hero, which is most of it, and the bed. Both
   are set from the DEVICE's height rather than the window's — inside a phone
   frame `vh` is the wrong box. */
.mast {
  min-height: calc(var(--dev-h, 900px) * 0.46);
  padding: calc(1vh + 8px) 0 1vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0;
  text-align: center;
}
body.has-pages .mast { min-height: 0; padding: calc(4vh + 8px) 0 3vh; }
  .pages { gap: 8vh; }
  /* the full column: the sheet's own 20px gutter is the margin, and the bed
     runs to it */
  /* the full column, or 40% of the height, whichever is smaller — it stays
     square either way */
  /* the whole column — the sheet's 20px gutter is the margin — and square */
  .page-in { max-width: 100%; }
  /* the bed is the column here, so the hero takes what is left rather than a
     share of the height */
  .mast { min-height: calc(var(--dev-h, 900px) * 0.44); padding: calc(1vh + 6px) 0 1vh; }
  .bench { margin-bottom: 3vh; }
  .foot { margin: 3vh 0 2vh; }
  .meta { gap: 10px; }
  .meta .acts { margin-left: 0; width: 100%; padding-top: 4px; }
  .field.place { flex: 1 1 14ch; width: auto; }
  .bar { padding-inline: 16px; }
}
