@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=EB+Garamond:wght@500&display=swap");

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

:root {
  --bg: #f4f4f4;
  --text: #1a1a18;
  --text-sec: #9a9a98;
  --brand: #3566be;
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  font-family:
    "Inter",
    system-ui,
    -apple-system,
    sans-serif;
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

/* ── Canvas ─────────────────────────────────────────── */

#canvas {
  display: block;
  position: fixed;
  inset: 0;
  cursor: grab;
  touch-action: none;

  transform-origin: center;
  transition:
    opacity 0.42s cubic-bezier(0.32, 0.72, 0, 1),
    transform 0.52s cubic-bezier(0.32, 0.72, 0, 1);
}

/* Handing over to the list: the shelf settles back and dissolves, and the
   names rise through it. */
body.list-open #canvas {
  opacity: 0;
  transform: scale(0.97);
}

#canvas.dragging {
  cursor: grabbing;
}

/* ── UI Overlay ─────────────────────────────────────── */

#ui {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  padding: 24px 28px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  pointer-events: none;
  z-index: 60;
}

/* The wordmark folds away so the open field has the whole width to itself. */
.brand {
  flex: none;
  max-width: 200px;
  overflow: hidden;
  transition:
    opacity 0.24s cubic-bezier(0.32, 0.72, 0, 1),
    max-width 0.42s cubic-bezier(0.32, 0.72, 0, 1);
}

body.search-open .brand {
  opacity: 0;
  max-width: 0;
}

/* With the list open, rows scroll underneath the header — fade them out
   behind it rather than letting them collide with the title. */
body.list-open #ui {
  background: linear-gradient(var(--bg) 62%, rgba(244, 244, 244, 0));
}

#ui h1 {
  line-height: 0;
}

#ui .logo {
  display: block;
  height: 28px;
  width: auto;
}

#ui .sub {
  font-size: 12px;
  opacity: 0.38;
  margin-top: 4px;
}

/* ── View switch (floating pill) ─────────────────────── */

.view-switch {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  z-index: 70;

  display: flex;
  gap: 2px;
  padding: 4px;

  border-radius: 999px;
  background: #e7e7e5;
  box-shadow:
    0 6px 22px rgba(0, 0, 0, 0.08),
    0 1px 2px rgba(0, 0, 0, 0.04);

  /* Unhurried on the way back; the fade out below is quicker. */
  transition:
    opacity 0.55s cubic-bezier(0.32, 0.72, 0, 1),
    transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}

/* Dragging the shelf or scrolling the list lets the switch recede. */
body.interacting .view-switch {
  opacity: 0.6;
  transition: opacity 0.2s ease-out;
}

/* Reach for it and it comes straight back. */
body.interacting .view-switch:hover {
  opacity: 1;
}

/* The pill steps out of the way while a book is open. */
body.modal-open .view-switch {
  opacity: 0;
  pointer-events: none;
  transform: translateX(-50%) translateY(10px);
}

/* The white well slides between the two views rather than blinking across. */
.view-switch-thumb {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 34px;
  height: 34px;
  border-radius: 999px;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1);
}

body.list-open .view-switch-thumb {
  /* One button (34px) plus the gap (2px). */
  transform: translateX(36px);
}

.view-switch-btn {
  position: relative;
  z-index: 1;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: #a9a9a7;
  cursor: pointer;
  transition:
    color 0.2s ease,
    background 0.2s ease;
}

.view-switch-btn svg {
  width: 15px;
  height: 15px;
  fill: currentColor;
}

.view-switch-btn:hover {
  color: #6a6a68;
}

.view-switch-btn.is-active {
  color: var(--brand);
}

/* ── Loader ─────────────────────────────────────────── */

#loader {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  z-index: 200;
  gap: 16px;
  transition: opacity 0.5s ease;
}

#loader.hidden {
  opacity: 0;
  pointer-events: none;
}

#loader .spinner {
  width: 24px;
  height: 24px;
  border: 2px solid rgba(26, 26, 24, 0.08);
  border-top-color: var(--text);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* ── List view ─────────────────────────────────────── */

.list-view {
  position: fixed;
  inset: 0;
  z-index: 50;

  background: var(--bg);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;

  padding: 92px 28px 132px;

  opacity: 0;
  pointer-events: none;
  transition: opacity 0.24s cubic-bezier(0.32, 0.72, 0, 1);
}

.list-view::-webkit-scrollbar {
  display: none;
}

.list-view.open {
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.38s cubic-bezier(0.32, 0.72, 0, 1);
}

.list-rows {
  max-width: 1100px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* Each row is a line of text flow, so the thumbnail can sit on the
   title's baseline the way an inline image does. */
.list-row {
  --row-size: clamp(26px, 4.4vw, 46px);

  display: block;
  max-width: 100%;
  border: none;
  background: transparent;
  padding: 2px 0;
  text-align: left;
  cursor: pointer;
  color: inherit;

  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity 0.3s cubic-bezier(0.32, 0.72, 0, 1),
    transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}

.list-view.open .list-row {
  opacity: 1;
  transform: none;
  transition:
    opacity 0.45s cubic-bezier(0.32, 0.72, 0, 1),
    transform 0.45s cubic-bezier(0.32, 0.72, 0, 1);
  transition-delay: var(--reveal-delay, 0ms);
}

.list-row-title {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500;
  font-size: var(--row-size);
  line-height: 1.2;
  color: var(--text);
}

.list-row-tail {
  white-space: nowrap;
  /* Sized like the title so the thumb's em units and the author's raise
     are both measured against the book name, not the row default. */
  font-size: var(--row-size);
}

/* The cover files are square mockups with the book floating in white space.
   The wrapper is the book's own 71:100 box; the image inside is blown up and
   offset so only the artwork shows. */
.list-row-thumb {
  display: inline-block;
  position: relative;
  overflow: hidden;

  /* Ascender to descender of the book name — measured at 0.983em for
     EB Garamond, so the cover stands as tall as the type itself. */
  height: 0.98em;
  aspect-ratio: 71 / 100;

  /* Centred on the book name. */
  vertical-align: middle;
  margin: 0 0.14em 0 0.26em;

  /* The cover grows on hover; keeping it out of hit-testing means the row
     under the cursor can never flip as it overlaps its neighbour. */
  pointer-events: none;

  transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
}

.list-row-thumb img {
  position: absolute;
  width: 204.75%;
  height: 145.28%;
  left: -55.96%;
  top: -23.97%;
}

.list-row-meta {
  font-size: 13px;
  color: var(--text-sec);
  transition: color 0.25s ease;
  /* Raised to sit on the book name's cap line. */
  vertical-align: calc(var(--row-size) * 0.44);
}

/* With a book open, hold the list in its receded state and keep only the
   chosen row lit — otherwise every name behind the modal flares up at once. */
body.modal-open .list-view .list-row {
  opacity: 0.32;
}

body.modal-open .list-view .list-row.is-selected {
  opacity: 1;
}

body.modal-open .list-view .list-row.is-selected .list-row-meta {
  color: var(--text);
}

/* Hovering the list recedes everything but the row under the cursor. */
@media (hover: hover) {
  .list-rows:has(.list-row:hover) .list-row {
    opacity: 0.32;
    transition: opacity 0.25s ease;
  }

  .list-rows:has(.list-row:hover) .list-row:hover {
    opacity: 1;
  }

  .list-row:hover .list-row-thumb {
    transform: scale(1.18);
  }

  /* The author comes up to the title's colour with the rest of the row. */
  .list-row:hover .list-row-meta {
    color: var(--text);
  }
}

/* ── Modal ─────────────────────────────────────────── */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 8, 0.38);
  backdrop-filter: blur(12px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s cubic-bezier(0.32, 0.72, 0, 1);
  z-index: 100;
}

.modal-backdrop.open {
  opacity: 1;
  pointer-events: auto;
}

.modal-panel {
  position: relative;
  background: #fcfcfc;
  border-radius: 32px;
  box-shadow:
    0 32px 80px rgba(0, 0, 0, 0.12),
    0 4px 16px rgba(0, 0, 0, 0.04);

  display: flex;
  flex-direction: column;

  padding: 32px;

  max-width: 420px;
  width: 100%;
  height: 640px;

  overflow: hidden;

  transform: translateY(20px) scale(0.97);
  opacity: 0;

  transition:
    transform 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    opacity 0.35s cubic-bezier(0.32, 0.72, 0, 1);

  --scroll-progress: 0;
}

.modal-backdrop.open .modal-panel {
  transform: translateY(0) scale(1);
  opacity: 1;
}

/* ── Close button ───────────────────────────────────── */

.modal-close {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  border-radius: 50%;
  cursor: pointer;
  font-size: 18px;
  color: #999;
  z-index: 2;
}

/* ── Sticky header ─────────────────────────────────── */

.modal-sticky {
  position: relative;
  flex-shrink: 0;

  /* Shrink more on scroll so the quotes sit closer to the book.
     At full scroll this now lands around the book height. */
  height: calc(282px - (146px * var(--scroll-progress)));

  display: flex;
  flex-direction: column;
  /* Center content horizontally by default; we translate the cover left via a
     computed CSS variable as the user scrolls. */
  align-items: center;

  overflow: hidden;
}

/* ── Cover ─────────────────────────────────────────── */

.modal-cover-wrap {
  width: calc(200px - (64px * var(--scroll-progress)));
  height: calc(200px - (64px * var(--scroll-progress)));

  /* X shift is computed in JS per panel width so the
     cover lands flush to the left padding on scroll end. */
  --cover-shift-x: -134px; /* 24px further left at scroll end */
  --cover-shift-y: -16px; /* slight nudge upwards on scroll */
  transform: translateX(calc(var(--cover-shift-x) * var(--scroll-progress)))
    translateY(calc(var(--cover-shift-y) * var(--scroll-progress)));

  transform-origin: center top;
}

.modal-cover {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── Title + author ────────────────────────────────── */

.modal-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;

  overflow: hidden;

  opacity: calc(1 - (var(--scroll-progress) * 1.6));

  transform: scale(calc(1 - (0.06 * var(--scroll-progress))))
    translateY(calc(-8px * var(--scroll-progress)));

  max-height: calc(120px * (1 - (var(--scroll-progress) * 1.4)));
}

.modal-title {
  font-family: "EB Garamond", Georgia, serif;
  font-size: 16px;
  font-weight: 500;
  color: #1a1a18;
  line-height: 22px;
  text-align: center;
  white-space: normal;
  word-break: break-word;
}

.modal-author {
  font-size: 13px;
  color: #9c9c9c;
  text-align: center;
}

/* ── Scroll area ───────────────────────────────────── */

.modal-scroll {
  flex: 1;
  overflow-y: auto;
  margin-top: 12px;
  scrollbar-width: none;
  display: flex;
  flex-direction: column;
}

.modal-scroll::-webkit-scrollbar {
  display: none;
}

/* Empty quotes: vertically center the message in the scroll area (not bottom-weighted). */
.modal-scroll:has(.modal-quotes:empty) {
  justify-content: center;
}

/* ── Quotes ────────────────────────────────────────── */

.modal-quotes {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.modal-quote {
  background: #f4f4f4;
  border-radius: 14px;
  padding: 20px;

  font-family: "EB Garamond", serif;
  font-style: normal;
  font-weight: 500;

  font-size: 15px;
  line-height: 24px;
  color: #1a1a18;
}

.modal-quote p {
  margin: 0;
}

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

.modal-quotes-empty {
  display: none;
  text-align: center;
  color: #9c9c9c;
  padding: 20px 24px;
  font-size: 12px;
  line-height: 1.5;
  flex-shrink: 0;
  /* Slight upward shift so it sits optically centered (geometric center reads low). */
  transform: translateY(-0.4em);
}

.modal-quotes:empty + .modal-quotes-empty {
  display: block;
}

/* ── Mobile ────────────────────────────────────────── */

@media (max-width: 640px) {
  .modal-panel {
    border-radius: 16px;
  }
}

@media (max-width: 540px) {
  .modal-panel {
    max-width: 340px;
    padding: 24px;
  }

  .modal-sticky {
    height: calc(262px - (130px * var(--scroll-progress)));
  }

  .modal-cover-wrap {
    /* On mobile, scale in place — no translate to avoid overflow/clipping */
    transform: none;
  }

  #ui {
    padding: 18px 20px;
  }

  #ui .logo {
    height: 24px;
  }

  .view-switch {
    bottom: 18px;
  }

  .list-view {
    padding: 72px 20px 108px;
  }

  .list-row-meta {
    font-size: 11px;
  }
}

/* ── Search ────────────────────────────────────────── */

/* The field is a passenger in the header row, so it only ever shows itself
   over the list — on the canvas there is nothing to filter. Open, it spans
   the width the rows below it occupy. */
.search {
  flex: 1;
  min-width: 0;

  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  pointer-events: none;

  opacity: 0;
  transform: translateY(-6px);
  transition:
    opacity 0.3s cubic-bezier(0.32, 0.72, 0, 1),
    transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}

body.list-open .search {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

body.modal-open .search,
body.posting .search {
  opacity: 0;
  pointer-events: none;
}

/* Behind the blurred sheet the search bar is invisible anyway, and fading it
   there costs the overlay a full-screen re-blur on every frame of the fade —
   right as the envelope is coming in. It goes at once instead. Coming back
   it still fades, since this rule no longer applies. */
body.posting .search {
  transition: none;
}

.search-toggle {
  flex: none;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: #a9a9a7;
  cursor: pointer;
  transition: color 0.2s ease;
}

.search-toggle svg {
  grid-area: 1 / 1;
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;

  transition:
    opacity 0.26s cubic-bezier(0.32, 0.72, 0, 1),
    transform 0.32s cubic-bezier(0.32, 0.72, 0, 1);
}

.search-toggle-close {
  opacity: 0;
  transform: rotate(-90deg) scale(0.6);
}

.search.is-open .search-toggle-find {
  opacity: 0;
  transform: rotate(90deg) scale(0.6);
}

.search.is-open .search-toggle-close {
  opacity: 1;
  transform: none;
}

.search-toggle:hover {
  color: #6a6a68;
}

.search.is-open .search-toggle {
  color: var(--brand);
}

/* Closed, the field is a zero-width sliver; opening sweeps it back across
   the header, stopping short of the magnifier. */
.search-input {
  width: 0;
  padding: 4px 0;
  border: none;
  border-bottom: 1px solid transparent;
  background: transparent;
  outline: none;

  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500;
  font-size: clamp(20px, 2.2vw, 26px);
  line-height: 1.2;
  color: var(--text);

  transition:
    width 0.42s cubic-bezier(0.32, 0.72, 0, 1),
    border-color 0.42s ease;
}

.search.is-open .search-input {
  width: calc(100% - 40px);
  border-bottom-color: rgba(26, 26, 24, 0.14);
}

.search-input::placeholder {
  color: var(--text-sec);
  opacity: 0.7;
}

/* ── Filtering ─────────────────────────────────────── */

.list-row.is-out {
  display: none;
}

/* Only rows that were absent a keystroke ago come back in — rows that
   survived the filter hold still rather than re-staging themselves. */
.list-row.is-in {
  animation: rowIn 0.4s cubic-bezier(0.32, 0.72, 0, 1) both;
  animation-delay: var(--in-delay, 0ms);
}

@keyframes rowIn {
  from {
    opacity: 0;
    transform: translateY(7px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.list-row-title mark,
.list-row-meta mark {
  background: none;
  color: var(--brand);
}

/* ── Nothing found ─────────────────────────────────── */

.list-empty {
  display: none;
  flex-direction: column;
  align-items: flex-start;
  max-width: 100%;
}

.list-view.is-empty .list-empty {
  display: flex;
  animation: rowIn 0.5s cubic-bezier(0.32, 0.72, 0, 1) both;
}

.list-empty-line {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500;
  font-size: clamp(24px, 3.6vw, 38px);
  line-height: 1.24;
  color: var(--text-sec);

  /* One line — a long query gives way before the sentence breaks. */
  max-width: 100%;
  white-space: nowrap;
}

.list-empty-line em {
  display: inline-block;
  max-width: 58vw;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: bottom;

  /* This padding is the whole gap — the spaces either side of the query are
     gone from the markup — and it keeps the clip box off the italic's lean. */
  padding: 0 6px;

  font-style: italic;
  color: var(--text);
}

.suggest-btn {
  margin-top: 20px;
  display: inline-flex;
  align-items: center;
  gap: 10px;

  padding: 16px 20px;
  border: 1px solid var(--brand);
  border-radius: 999px;
  background: transparent;
  color: var(--brand);
  cursor: pointer;

  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  line-height: 1;

  transition: background 0.74s cubic-bezier(0.25, 0.18, 0.15, 0.98);
}

/* It fills rather than lifting off the page — but stays in one palette. At
   the shelf's pace an inverting fill spends a beat with blue type on mid-blue
   ground, and the label drops out; a tint never crosses its own text. */
.suggest-btn:hover {
  background: rgba(53, 102, 190, 0.1);
}

.suggest-btn:active {
  background: rgba(53, 102, 190, 0.18);
}

.suggest-btn[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

.suggest-btn-mark svg {
  display: block;
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 0.74s cubic-bezier(0.25, 0.18, 0.15, 0.98);
}

.suggest-btn:hover .suggest-btn-mark svg {
  transform: translateX(3px);
}

/* ── Posting a suggestion ──────────────────────────── */

.post {
  position: fixed;
  inset: 0;
  z-index: 300;

  display: grid;
  grid-template-rows: 1fr auto;
  place-items: center;

  background: rgba(244, 244, 244, 0.9);
  backdrop-filter: blur(10px);

  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s cubic-bezier(0.32, 0.72, 0, 1);
}

.post.is-open {
  opacity: 1;
  pointer-events: auto;
}

.post-stage {
  position: relative;
  width: 360px;
  height: 440px;
}

/* ── The suggestion, as a book ─────────────────────── */

/* It belongs to the envelope, held just above the mouth — only a sibling of
   the pocket can slide down behind it. */
.post-book {
  position: absolute;
  left: 50%;
  top: -190px;
  z-index: 3;

  width: 120px;
  aspect-ratio: 71 / 100;

  opacity: 0;
  transform: translateX(-50%);
}

.post-book-face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 18px 14px;

  background:
    radial-gradient(
      120% 90% at 30% 12%,
      rgba(255, 255, 255, 0.9),
      rgba(255, 255, 255, 0) 60%
    ),
    linear-gradient(150deg, #f3efe6, #e6dfd1);
  border-radius: 2px 5px 5px 2px;
  box-shadow:
    0 14px 30px rgba(0, 0, 0, 0.16),
    0 2px 5px rgba(0, 0, 0, 0.08);
}

/* The page block, peeking out along the fore edge. */
.post-book-pages {
  position: absolute;
  top: 3px;
  bottom: 3px;
  right: -4px;
  width: 5px;
  border-radius: 0 3px 3px 0;
  background: repeating-linear-gradient(
    to bottom,
    #fbfaf7 0 2px,
    #ddd8cd 2px 3px
  );
  box-shadow: inset -1px 0 2px rgba(0, 0, 0, 0.08);
}

.post-book-spine {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 11px;
  border-radius: 2px 0 0 2px;
  background: linear-gradient(
    to right,
    rgba(60, 48, 32, 0.22),
    rgba(60, 48, 32, 0.05) 62%,
    rgba(255, 255, 255, 0.35)
  );
}

.post-book-rule {
  width: 26px;
  height: 1px;
  background: rgba(60, 48, 32, 0.28);
}

.post-book-title {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500;
  font-size: 16px;
  line-height: 1.22;
  text-align: center;
  color: #33302a;
  overflow-wrap: anywhere;
}

/* ── The envelope ──────────────────────────────────── */

.post-envelope {
  position: absolute;
  left: 50%;
  bottom: 40px;
  z-index: 4;

  width: 280px;
  height: 200px;

  opacity: 0;
  transform: translateX(-50%);

  /* Perspective for the flap, but a flat stacking context of its own, so the
     pieces inside still sort by z-index. */
  perspective: 800px;
}

.post-env-back {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: 5px;
  background: linear-gradient(160deg, #2f5aa8, #24467f);
  box-shadow:
    0 18px 38px rgba(0, 0, 0, 0.14),
    0 2px 6px rgba(0, 0, 0, 0.06);
}

/* The pocket. Its V-shaped top edge is what the book vanishes behind. */
.post-env-front {
  position: absolute;
  inset: 0;
  z-index: 4;
  border-radius: 5px;
  background: linear-gradient(165deg, #8ba5d9, #6b8aca);
  clip-path: polygon(0 0, 50% 26%, 100% 0, 100% 100%, 0 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

/* Two faces, so that folded back we see the darker inside of the flap. The
   back's triangle is mirrored — rotateX flips it, and the two must land on
   top of one another. */
.post-env-flap {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 48%;
  z-index: 2;

  transform-origin: top center;
  transform-style: preserve-3d;
}

.post-env-flap::before,
.post-env-flap::after {
  content: "";
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
}

.post-env-flap::before {
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  background: linear-gradient(180deg, #86a2d7, #6889c9);
  border-radius: 5px 5px 0 0;
}

.post-env-flap::after {
  clip-path: polygon(0 100%, 100% 100%, 50% 0);
  background: linear-gradient(0deg, #3f65b0, #5b7dc1);
  transform: rotateX(180deg);
}

.post-env-lines {
  position: absolute;
  left: 26px;
  bottom: 30px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.post-env-lines i {
  display: block;
  height: 2px;
  border-radius: 1px;
  background: rgba(255, 255, 255, 0.5);
  transform: scaleX(0);
  transform-origin: left center;
}

.post-env-lines i:nth-child(1) {
  width: 96px;
}
.post-env-lines i:nth-child(2) {
  width: 118px;
}
.post-env-lines i:nth-child(3) {
  width: 62px;
}

.post-stamp {
  position: absolute;
  top: 12px;
  right: 14px;
  z-index: 8;

  width: 42px;
  height: 48px;
  opacity: 0;

  --perf: #7b98d0;
  background:
    radial-gradient(circle 3px at 50% 0, var(--perf) 97%, transparent) 0 -1px /
      7px 7px repeat-x,
    radial-gradient(circle 3px at 50% 100%, var(--perf) 97%, transparent) 0
      calc(100% + 1px) / 7px 7px repeat-x,
    radial-gradient(circle 3px at 0 50%, var(--perf) 97%, transparent) -1px 0 /
      7px 7px repeat-y,
    radial-gradient(circle 3px at 100% 50%, var(--perf) 97%, transparent)
      calc(100% + 1px) 0 / 7px 7px repeat-y,
    #fdfdfb;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.12));
}

.post-stamp-ink {
  position: absolute;
  inset: 6px;
  background: linear-gradient(150deg, #1f3d72, var(--brand));
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}

.post-stamp-ink::after {
  content: "";
  position: absolute;
  inset: 7px 6px;
  border-radius: 1px 3px 3px 1px;
  background: rgba(255, 255, 255, 0.85);
}

/* The postmark: a ring thumped over the corner of the stamp, with the
   cancellation running off to the left the way a real one does. */
.post-mark {
  position: absolute;
  top: 24px;
  right: 26px;
  z-index: 9;

  width: 56px;
  height: 56px;
  border-radius: 50%;
  opacity: 0;

  border: 2px solid rgba(16, 30, 60, 0.42);
  background: linear-gradient(
    180deg,
    transparent 41%,
    rgba(16, 30, 60, 0.3) 41% 45%,
    transparent 45% 55%,
    rgba(16, 30, 60, 0.3) 55% 59%,
    transparent 59%
  );
}

.post-mark::before {
  content: "";
  position: absolute;
  top: 50%;
  right: 100%;
  width: 62px;
  height: 18px;
  margin-right: 4px;
  transform: translateY(-50%);
  background: repeating-linear-gradient(
    180deg,
    rgba(16, 30, 60, 0.32) 0 2px,
    transparent 2px 7px
  );
}

.post-trail {
  position: absolute;
  left: 0;
  top: -40px;
  width: 100%;
  height: 100%;
  overflow: visible;
  opacity: 0;
}

.post-trail path {
  stroke: rgba(53, 102, 190, 0.3);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-dasharray: 5 9;
  stroke-dashoffset: 300;
}

.post-note {
  margin-bottom: 14vh;
  font-family: "EB Garamond", Georgia, serif;
  font-size: 20px;
  color: var(--text);
  text-align: center;
  padding: 0 24px;
  opacity: 0;
}

.post-note.is-error {
  color: #b4522f;
}

/* ── The sequence ──────────────────────────────────── */

/* One 4.4s timeline, shared by every piece, so nothing drifts out of step:
   the book is set, the flap opens, it drops in, the flap seals, the stamp
   lands, and the whole thing is thrown into the air. */

.post.is-sending .post-book {
  animation: postBook 4400ms cubic-bezier(0.32, 0.72, 0, 1) both;
  /* Both movers are heavy to paint — gradients, clip-paths, a page block
     ruled every 3px. Promoted, each rasterises once and the flight is a
     compositor transform rather than a re-render per frame. */
  will-change: transform, opacity;
}

.post.is-sending .post-envelope {
  animation: postEnvelope 4400ms cubic-bezier(0.32, 0.72, 0, 1) both;
  will-change: transform, opacity;
}

.post.is-sending .post-env-flap {
  animation: postFlap 4400ms cubic-bezier(0.32, 0.72, 0, 1) both;
}

.post.is-sending .post-env-lines i {
  animation: postLine 4400ms cubic-bezier(0.32, 0.72, 0, 1) both;
}

.post.is-sending .post-env-lines i:nth-child(2) {
  animation-delay: 90ms;
}

.post.is-sending .post-env-lines i:nth-child(3) {
  animation-delay: 180ms;
}

.post.is-sending .post-stamp {
  animation: postStamp 4400ms cubic-bezier(0.32, 0.72, 0, 1) both;
}

.post.is-sending .post-mark {
  animation: postMark 4400ms cubic-bezier(0.32, 0.72, 0, 1) both;
}

.post.is-sending .post-trail {
  animation: postTrailFade 4400ms linear both;
}

.post.is-sending .post-trail path {
  animation: postTrail 4400ms linear both;
}

.post.is-sending .post-note {
  animation: postNote 4400ms cubic-bezier(0.32, 0.72, 0, 1) both;
}

@keyframes postBook {
  0%,
  3% {
    opacity: 0;
    transform: translateX(-50%) translateY(34px) scale(0.8) rotate(-8deg);
  }
  16% {
    opacity: 1;
    transform: translateX(-50%) translateY(0) scale(1) rotate(0deg);
  }
  /* A breath before it is given up. */
  22% {
    opacity: 1;
    transform: translateX(-50%) translateY(-7px) scale(1) rotate(1.6deg);
  }
  27% {
    opacity: 1;
    transform: translateX(-50%) translateY(0) scale(1) rotate(0deg);
  }
  /* Tipped towards the mouth of the envelope, then posted. */
  30% {
    opacity: 1;
    transform: translateX(-50%) translateY(12px) scale(0.99) rotate(-2.4deg);
    animation-timing-function: cubic-bezier(0.45, 0, 0.7, 0.6);
  }
  45% {
    opacity: 1;
    transform: translateX(-50%) translateY(228px) scale(0.72) rotate(0deg);
  }
  46%,
  100% {
    opacity: 0;
    transform: translateX(-50%) translateY(228px) scale(0.72) rotate(0deg);
  }
}

@keyframes postEnvelope {
  0% {
    opacity: 0;
    transform: translateX(-50%) translateY(48px) rotate(-2deg) scale(0.96);
  }
  14%,
  55.5% {
    opacity: 1;
    transform: translateX(-50%) translateY(0) rotate(0deg) scale(1);
    animation-timing-function: cubic-bezier(0.4, 0, 0.9, 0.5);
  }
  /* The flap thuds shut. */
  57.5% {
    transform: translateX(-50%) translateY(3px) rotate(0deg) scale(1.012, 0.986);
  }
  60%,
  64% {
    transform: translateX(-50%) translateY(0) rotate(0deg) scale(1);
    animation-timing-function: cubic-bezier(0.4, 0, 0.9, 0.5);
  }
  /* And the stamp lands. */
  66.5% {
    transform: translateX(-50%) translateY(5px) rotate(-0.7deg)
      scale(1.022, 0.972);
  }
  69% {
    transform: translateX(-50%) translateY(-2px) rotate(0.2deg) scale(1);
  }
  71%,
  74% {
    opacity: 1;
    transform: translateX(-50%) translateY(0) rotate(0deg) scale(1);
  }
  /* Wound up, and thrown. The flight is linear from waypoint to waypoint:
     the timeline's shared ease brings every segment to a halt at its end,
     and four halts in a row is what read as a stutter mid-throw. The
     spacing of the waypoints carries the acceleration instead. */
  77% {
    opacity: 1;
    transform: translateX(-50%) translateY(-18px) rotate(-5deg) scale(1.03);
    animation-timing-function: cubic-bezier(0.4, 0, 1, 1);
  }
  80% {
    opacity: 1;
    transform: translateX(-50%) translateY(-4px) rotate(7deg) scale(1);
    animation-timing-function: linear;
  }
  82.5% {
    opacity: 1;
    transform: translateX(-50%) translate(19px, -29px) rotate(10deg)
      scale(0.93);
    animation-timing-function: linear;
  }
  85% {
    opacity: 1;
    transform: translateX(-50%) translate(55px, -74px) rotate(14deg)
      scale(0.85);
    animation-timing-function: linear;
  }
  87.5% {
    opacity: 1;
    transform: translateX(-50%) translate(101px, -132px) rotate(18deg)
      scale(0.77);
    animation-timing-function: linear;
  }
  90% {
    opacity: 1;
    transform: translateX(-50%) translate(156px, -201px) rotate(23deg)
      scale(0.68);
    animation-timing-function: linear;
  }
  92.5% {
    opacity: 1;
    transform: translateX(-50%) translate(217px, -279px) rotate(27deg)
      scale(0.59);
    animation-timing-function: linear;
  }
  95% {
    opacity: 1;
    transform: translateX(-50%) translate(286px, -365px) rotate(32deg)
      scale(0.49);
    animation-timing-function: linear;
  }
  97.5% {
    opacity: 1;
    transform: translateX(-50%) translate(360px, -459px) rotate(36deg)
      scale(0.4);
    animation-timing-function: linear;
  }
  100% {
    opacity: 0;
    transform: translateX(-50%) translate(440px, -560px) rotate(41deg)
      scale(0.3);
  }
}

@keyframes postFlap {
  0%,
  16% {
    z-index: 2;
    transform: rotateX(0deg);
  }
  27%,
  46% {
    z-index: 2;
    transform: rotateX(-166deg);
  }
  /* Past upright it belongs in front of the pocket, not behind it. */
  51% {
    z-index: 7;
    transform: rotateX(-88deg);
  }
  56% {
    z-index: 7;
    transform: rotateX(0deg);
  }
  57.5% {
    z-index: 7;
    transform: rotateX(6deg);
  }
  60%,
  100% {
    z-index: 7;
    transform: rotateX(0deg);
  }
}

@keyframes postLine {
  0%,
  50% {
    transform: scaleX(0);
  }
  60%,
  100% {
    transform: scaleX(1);
  }
}

@keyframes postStamp {
  0%,
  60% {
    opacity: 0;
    transform: translate(10px, -14px) scale(2.7) rotate(-18deg);
  }
  65% {
    opacity: 1;
    transform: translate(0, 0) scale(1.08) rotate(-5deg);
  }
  66.5% {
    opacity: 1;
    transform: translate(0, 0) scale(0.97) rotate(-5deg);
  }
  68%,
  100% {
    opacity: 1;
    transform: translate(0, 0) scale(1) rotate(-5deg);
  }
}

@keyframes postMark {
  0%,
  67% {
    opacity: 0;
    transform: scale(1.7) rotate(-24deg);
  }
  70% {
    opacity: 0.9;
    transform: scale(0.96) rotate(-9deg);
  }
  72%,
  100% {
    opacity: 0.85;
    transform: scale(1) rotate(-9deg);
  }
}

@keyframes postTrailFade {
  0%,
  79% {
    opacity: 0;
  }
  84% {
    opacity: 1;
  }
  96% {
    opacity: 0.6;
  }
  100% {
    opacity: 0;
  }
}

@keyframes postTrail {
  0%,
  80% {
    stroke-dashoffset: 300;
  }
  97%,
  100% {
    stroke-dashoffset: 0;
  }
}

@keyframes postNote {
  0%,
  80% {
    opacity: 0;
    transform: translateY(8px);
  }
  88%,
  100% {
    opacity: 1;
    transform: none;
  }
}

/* Everything above is decoration; the message is the part that matters. */
@media (prefers-reduced-motion: reduce) {
  .post.is-sending .post-book,
  .post.is-sending .post-envelope,
  .post.is-sending .post-env-flap,
  .post.is-sending .post-env-lines i,
  .post.is-sending .post-stamp,
  .post.is-sending .post-mark,
  .post.is-sending .post-trail,
  .post.is-sending .post-trail path,
  .post.is-sending .post-note,
  .list-view.is-empty .list-empty,
  .list-row.is-in {
    animation: none;
  }

  .post.is-sending .post-stage {
    display: none;
  }

  .post.is-sending .post-note {
    opacity: 1;
  }
}

@media (max-width: 540px) {
  .search.is-open .search-input {
    width: calc(100% - 38px);
    font-size: 20px;
  }

  .post-stage {
    transform: scale(0.82);
  }

  .post-note {
    margin-bottom: 9vh;
    font-size: 18px;
  }
}
