/* The swipe page: a full-height layout, the card stack, and the bottom sheets. Colours and
   buttons come from app.css. */

.swipe-body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

.swipe-main {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
  /* A card dragged, or flying, off the side never widens the page under the sheets. */
  overflow-x: hidden;
  overflow-x: clip;
  padding: 12px calc(16px + env(safe-area-inset-right)) calc(16px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
}

/* ----- the home ----- */

/* Start a chat's first screen on the page itself (see swipePage in views.ts), as the chat lays its
   new chat out: the photos still open at the top, the mark and the greeting in the middle of the
   spare height (their auto margins), what they buy again over the composer, and the composer at the
   foot, by the thumb. */
.swipe-camera {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 12px;
  padding: 8px 0 0;
  text-align: left;
}

/* Unseen until swipe.js greets them by the phone's clock ("Afternoon, Alexander"), so the
   placeholder never shows first. */
.home-hello h1:not([data-greeted]) {
  visibility: hidden;
}

/* The composer, and what's said over it (who reads a photo, a photo that didn't go up), at the foot
   of the screen however long the page: on a short phone the lines over it scroll under it, so it
   stands on the page's own colour, out to the page's edges and down past the home indicator, which
   it keeps clear of. */
.home-foot {
  position: sticky;
  bottom: 0;
  z-index: 1;
  display: grid;
  gap: 10px;
  margin: 0 calc(-16px - env(safe-area-inset-right)) calc(-16px - env(safe-area-inset-bottom)) calc(-16px - env(safe-area-inset-left));
  padding: 4px calc(16px + env(safe-area-inset-right)) calc(12px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
  background: var(--bg);
}

/* What scrolls under it fades out over its top edge, rather than being cut off there. */
.home-foot::before {
  content: "";
  position: absolute;
  inset: -12px 0 auto;
  height: 12px;
  background: linear-gradient(to bottom, transparent, var(--bg));
  pointer-events: none;
}

/* While the field has focus the keyboard is up, over the home indicator: the composer sits on the
   keyboard without the strip kept for the indicator, as the chat's does. */
.home-foot:has(.chat-input:focus) {
  padding-bottom: 8px;
}

/* The line on who reads the photo, over the composer: spaced by the foot's gap alone, not a
   paragraph's margin on top of it. */
.home-foot > p.muted.small {
  margin: 0;
}

.home-chat {
  display: grid;
  gap: 10px;
}

.home-chat > .form-error {
  margin: 0;
}

/* A tonal button with its hint on a second line inside it (a card's Add your address and card,
   "Ready to buy when your price is in"; the correction sheet's Add a photo): no taller than a button
   of one line. */
.btn-hinted {
  padding-block: 0.375rem;
}

.btn-words {
  display: grid;
  justify-items: start;
  text-align: left;
  line-height: 1.15;
}

.btn-hint {
  color: var(--text-2);
  font-family: var(--font);
  font-size: 0.8125rem;
  font-weight: 500;
}

/* The camera between Pass and Buy is a label, which app.css's rule for buttons doesn't reach: a long
   press on it, or on Pass and Buy, opens no menu and selects nothing. */
.swipe-act {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* A laptop or tablet window: the greeting and the composer under it centre together, as a chat
   app's new chat does on a big screen; the composer still stays in sight over what they buy again. */
@media (min-width: 700px), (min-height: 960px) {
  .swipe-camera {
    justify-content: center;
  }

  .swipe-camera .home-hello {
    margin: 0;
  }
}

/* A short screen (375x667, an iPhone SE): the column starts under the bar and its gaps close to
   8px, so more of the home is in sight over the composer. */
@media (max-height: 700px) {
  .swipe-camera {
    gap: 8px;
    padding-top: 0;
  }

  .swipe-camera .home-hello {
    gap: 8px;
    padding-block: 8px;
  }
}

/* A screen up to 620px tall (320x568, the smallest iPhone; a 360x640 Android under Chrome's bar):
   the mark steps aside, as the chat's does when the keyboard leaves it no room (see fitHello in
   swipe.js), and the small lines step down, so the greeting and the composer share the first
   screen with the photos still open. */
@media (max-height: 620px) {
  .swipe-camera {
    gap: 6px;
  }

  .home-hello .chat-mark {
    display: none;
  }

  .home-foot > p.muted.small {
    font-size: 0.8125rem;
  }
}

/* ----- the bench ----- */

/* A fresh home (no photos open, nothing to buy again) stands on a packing bench: a clip of a roll
   of tape that ran out, snapped, looked over and packed (public/landing/bench.mp4, see assets.ts),
   across the top of the first screen, fading into the page where the greeting starts. It takes
   only the height the greeting and the composer leave over, so a tall phone shows more bench and a
   short one a strip of it, and the composer stays at the foot. */
.camera-bench {
  padding-top: 0;
}

.bench-scene {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  margin: -12px calc(-16px - env(safe-area-inset-right)) 0 calc(-16px - env(safe-area-inset-left));
  overflow: hidden;
  container-type: size;
}

.bench-clip {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
  /* The bench's wood, until the still arrives. */
  background: #4A3A2A;
}

/* Under 96px the band would be a sliver of wood: the clip waits for a taller screen, and the band
   is only the spare height, as it was. */
@container (max-height: 96px) {
  .bench-clip {
    display: none;
  }
}

/* The page's own colour rises from the band's foot, so the words under it stand on the counter. */
.bench-scene::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, transparent 42%, var(--bg));
  pointer-events: none;
}

/* A laptop or tablet window: a card of the bench over the centred group, with the corners of the
   cards after it and no fade, as tall as a 16:10 card of the column at most and still only the
   height the greeting and the composer leave over. */
@media (min-width: 700px), (min-height: 960px) {
  .bench-scene {
    max-height: min(36vh, 18.75rem);
    margin: 0 0 8px;
    border-radius: var(--radius-card);
  }

  .bench-scene::after {
    display: none;
  }
}

/* A screen up to 620px tall: no room over the greeting; the home is as it is without the bench. */
@media (max-height: 620px) {
  .bench-scene {
    display: none;
  }
}

/* ----- the strip of today's photos ----- */

/* Photos are priced side by side; the strip shows each one's state and switches between them. */
.swipe-tray {
  display: flex;
  gap: 10px;
  margin: 0 -16px;
  padding: 2px 16px 4px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.swipe-tray::-webkit-scrollbar {
  display: none;
}

.tray-tile {
  flex: none;
  display: grid;
  /* Rows packed at the top, so the pictures and names of tiles of different heights (one with a
     part number or size under its name, one without) sit level. */
  align-content: start;
  justify-items: center;
  gap: 4px;
  width: 88px;
  padding: 2px 0 0;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
}

/* A photo whose card starts on pickup says so under its price ("$20.24 · pickup"): wider than the
   others, so the words fit on the tile's one line rather than taking a second. */
.tray-tile.tray-pickup {
  width: 112px;
}

.tray-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 62px;
  height: 62px;
  overflow: hidden;
  border-radius: 14px;
  background: var(--surface-muted);
  color: var(--muted);
  box-shadow: 0 0 0 2px #00000000;
  transition: box-shadow 0.15s ease;
}

.tray-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.tray-thumb .icon {
  width: 24px;
  height: 24px;
  opacity: 0.4;
}

.tray-state {
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.tray-tile[aria-current="true"] {
  color: var(--text);
}

.tray-tile[aria-current="true"] .tray-thumb {
  box-shadow: 0 0 0 2.5px var(--text);
}

.tray-tile:focus-visible {
  outline: none;
}

.tray-tile:focus-visible .tray-thumb {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.tray-pricing .tray-state {
  color: var(--accent-ink);
}

/* A photo still being priced gets a pulse, so the strip says which ones are moving. */
.tray-pricing .tray-state::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 4px;
  border-radius: 50%;
  background: var(--accent);
  vertical-align: 1px;
  animation: tray-pulse 1.2s ease-in-out infinite;
}

@keyframes tray-pulse {
  50% {
    opacity: 0.25;
  }
}

/* "Which one?" and "Needs you", in the colour the tags on Orders say them in. */
.tray-choice .tray-state,
.tray-question .tray-state {
  color: var(--warning);
}

.tray-ordered .tray-state {
  color: var(--success);
}

/* ----- stage and cards ----- */

/* On the card screen, a note above the stage (a dead invite link's, say) keeps the page's 12px gap
   rather than adding its own margin: the card's lines need the height more. */
.swipe-main:has(> .swipe-stage:not([hidden])) > .flash {
  margin-bottom: 0;
}

/* The priced cards share the stage's one grid cell, the top one last in the markup and so on top.
   The stage takes the room the page has, and more when a card needs it: on a short phone, or under
   a notice, the page scrolls (to the switch and Buy), never the card, so no line on a card is ever
   cut through at its foot. */
.swipe-stage {
  position: relative;
  flex: 1 0 auto;
  display: grid;
  min-height: 440px;
}

.card-swipe,
.stage-card {
  width: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

/* A card waiting for its price, or a notice: the stage's height, at most 680px, centred. */
.stage-card {
  position: absolute;
  inset: 0;
  margin: auto;
  max-height: min(100%, 680px);
}

/* A priced card: as tall as what's on it, and at least the stage's height up to 680px, centred in a
   taller stage; the picture takes the spare height. */
.card-swipe {
  position: relative;
  grid-area: 1 / 1;
  align-self: center;
  min-height: min(100%, 680px);
}

/* A card scrolled to (a pin tapped, the next piece once one's bought) stops below the sticky bar,
   not under it, the status bar's inset in the iPhone app included. */
.swipe-stage,
.swipe-stage > .card-swipe {
  scroll-margin-top: calc(4.5rem + env(safe-area-inset-top));
}

/* The Ordered card too: what it says, its buttons, Cancel and the invite run taller than a 320px
   phone's stage (or a 393px one's under the strip of photos), and a card the stage's height cut
   Cancel through and hid the invite. Now the page scrolls to them instead. */
.stage-card-done {
  position: relative;
  inset: auto;
  grid-area: 1 / 1;
  align-self: center;
  max-height: none;
  min-height: min(100%, 680px);
}

.stage-card-done .card-body {
  flex: none;
}

.stage-card-done .card-photo {
  min-height: 96px;
  contain: size;
}

@media (max-height: 800px) {
  .stage-card-done .card-photo {
    min-height: 64px;
  }
}

/* And the waiting card, as the Ordered card: with Add your address and card over Snap your next
   item, its words and buttons run taller than a short phone's stage, so the picture gives up its
   height first, down to a strip, and then the card grows and the page scrolls to them, rather than
   the words scrolling inside a card the stage's height cut through. The cards waiting on a person
   (a photo we couldn't make out, a price a person is getting) too: Snap your next item, Cancel
   request and a guest's Email me when it's priced under their words did the same. */
.stage-card:is([data-working], [data-person]) {
  position: relative;
  inset: auto;
  grid-area: 1 / 1;
  align-self: center;
  max-height: none;
  min-height: min(100%, 680px);
}

.stage-card:is([data-working], [data-person]) .card-body {
  flex: none;
}

.stage-card:is([data-working], [data-person]) .card-photo {
  min-height: 96px;
  contain: size;
}

/* A card whose height isn't set gives its picture's box no height to take 100% of: laid over the
   box, the photo fills it, rather than sitting in it at its own shape with a band above and below. */
.stage-card:is([data-working], [data-person]) .card-photo img {
  position: absolute;
  inset: 0;
}

@media (max-height: 800px) {
  .stage-card:is([data-working], [data-person]) .card-photo {
    min-height: 64px;
  }
}

/* Up and down stays the page's: a scroll that starts on the card scrolls (and never buys); only a
   sideways drag is the card's. */
.card-swipe {
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  will-change: transform;
  transition: transform 0.3s ease;
  cursor: grab;
}

/* A finger held still on the picture before the drag gets no menu for it (Save to Photos, Copy) and
   doesn't lift it to drag elsewhere: the drag is the card's. */
.card-swipe img {
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
}

.card-variant {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text-2);
}

.card-swipe.pos-1 {
  transform: translateY(12px) scale(0.96);
}

.card-swipe.pos-2 {
  transform: translateY(24px) scale(0.92);
}

.card-swipe.dragging {
  transition: none;
  cursor: grabbing;
}

.card-swipe.settle {
  transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.card-swipe.flying {
  transition: transform 0.32s ease-in, opacity 0.32s ease-in;
  opacity: 0;
  z-index: 10;
  pointer-events: none;
}

.card-photo {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-muted);
  color: var(--muted);
  margin: 8px 8px 0;
  overflow: hidden;
  border-radius: 12px;
}

.card-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.card-photo .icon {
  width: 72px;
  height: 72px;
  opacity: 0.4;
}

/* Sent as words, not a photo: what they asked for, in the photo's place. */
.card-words {
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  text-align: center;
}

.card-words .icon {
  width: 32px;
  height: 32px;
}

.card-words p {
  display: -webkit-box;
  max-width: 22ch;
  margin: 0;
  overflow: hidden;
  color: var(--text);
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 750;
  line-height: 1.2;
  overflow-wrap: anywhere;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
}

/* On a priced card the strip above the title can be short: the words alone, on two lines at most. */
.card-swipe .card-words {
  padding: 10px 16px;
}

.card-swipe .card-words .icon {
  display: none;
}

.card-swipe .card-words p {
  font-size: 1.25rem;
  -webkit-line-clamp: 2;
}

/* Sent as words, on the waiting card, a card waiting on a person or the Ordered card: their picture
   shrinks to a strip (96px, 64px under 800px tall) once their words and buttons need the height, and
   its 32px icon, the gap and 1.5rem words don't fit there: the words were cut through mid-line, or
   not shown at all. There, as on a priced card, the words alone on the lines that fit, whole or
   ending in an ellipsis; a tall picture keeps the icon and the larger words. swipe.js measures the
   picture (data-fit: "strip", or "line" for one line), since a container query sees only the height
   a card's flex starts its picture at, not the one it ends with. */
.card-words:is([data-fit="strip"], [data-fit="line"]) {
  padding: 10px 16px;
}

.card-words:is([data-fit="strip"], [data-fit="line"]) .icon {
  display: none;
}

.card-words:is([data-fit="strip"], [data-fit="line"]) p {
  font-size: 1.25rem;
  -webkit-line-clamp: 2;
}

.card-words[data-fit="line"] p {
  -webkit-line-clamp: 1;
}

/* A seller's catalogue shot: shown whole, as a shop shows it, not cropped like a photo, and multiplied
   into a light well so its white box disappears; in dark mode the well is dimmed, not a white glare. */
.card-listing {
  padding: 16px;
  background: var(--well-listing);
}

.card-listing img {
  object-fit: contain;
  mix-blend-mode: multiply;
}

.shimmer::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(110deg, #FFFFFF00 30%, var(--shimmer) 50%, #FFFFFF00 70%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
}

@keyframes shimmer {
  from {
    background-position: 200% 0;
  }
  to {
    background-position: -200% 0;
  }
}

.card-body {
  display: grid;
  /* Never wider than the card: a long option or name shortens with an ellipsis instead. */
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
  padding: 12px 18px 14px;
}

/* The picture takes the card's spare height and gives it up first: a card that needs all its height
   for its words keeps a strip of it. Its own size doesn't count (contain), so a tall photo never
   makes the card taller. */
.card-swipe .card-photo {
  min-height: 96px;
  contain: size;
}

/* A phone under 800px tall (a 393px iPhone in Safari, or smaller) keeps a thinner strip: the price
   and its terms matter more than a taller slice of photo. */
@media (max-height: 800px) {
  .card-swipe .card-photo {
    min-height: 64px;
  }
}

/* A picture with the pills over it (In your photo and Imagine: 44px, 10px from its foot) keeps a
   slice of itself above them at every height, 64px, where the strip alone would leave it 10px:
   the photo that says this is the right thing. A card shown in their photo has its own shape. */
.card-swipe:not(.card-in-space) .card-photo:has(.card-space-go, .card-imagine) {
  min-height: 118px;
}

/* The words, whole: the card is as tall as they need, so they never scroll inside it. */
.card-swipe .card-body {
  flex: none;
  padding-bottom: 6px;
}

/* The total and what it holds, how long delivery takes (or where to pick it up), the quantity and
   how it gets to them: under the words, as one block. */
.card-foot {
  flex: none;
  display: grid;
  gap: 6px;
  padding: 0 18px 14px;
}

/* The card's last row: "Not quite right? Tell us more" at the left when the price can be put right,
   and Details at the right, on every card. Both are accent-ink words, never filled, each a 44px tap;
   on a phone too narrow for both on one line, Details wraps under it, still at the right. */
.card-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  column-gap: 12px;
  padding: 0 18px 14px;
}

.card-links > .link-button {
  margin: 0;
  min-height: 44px;
}

.card-links > .card-more {
  margin-left: auto;
}

/* Over the row, the foot keeps only the words' gap: the row's 44px taps hold their own room. */
.card-foot:has(+ .card-links) {
  padding-bottom: 6px;
}


.stage-card .card-body {
  text-align: center;
  justify-items: stretch;
  gap: 10px;
}

/* "Best match" / "Closest match": a sentence-case tag, not an all-caps eyebrow. */
.card-label {
  justify-self: start;
  padding: 3px 10px;
  border-radius: var(--radius-tag);
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-family: var(--font);
  font-size: 0.8125rem;
  font-weight: 650;
  letter-spacing: 0;
  text-transform: none;
}

.stage-card .card-label {
  justify-self: center;
}

.card-title {
  margin: 0;
  font-size: 1.25rem;
  line-height: 1.16;
  display: -webkit-box;
  overflow: hidden;
  font-weight: 750;
  font-stretch: 88%;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

.stage-card .card-title {
  font-size: 1.375rem;
}

/* A card's or a sheet's heading takes focus only so a screen reader starts there (see refocus and
   openSheet in swipe.js). It isn't a control, so it gets no focus ring. */
.card-title[tabindex="-1"]:focus,
.sheet h2[tabindex="-1"]:focus {
  outline: none;
}

.stage-card-done .card-title {
  color: var(--success);
}

.stage-card-done .card-title .icon {
  width: 1.2em;
  height: 1.2em;
  vertical-align: -0.2em;
}

/* The Ordered card's facts as rows (Arrives, Ships to, Total), then the order's number and what's
   next, both read from the left under the centred heading: a centred paragraph of 70 words put the
   day it arrives on its fourth line. The labels keep the muted grey, on the values' baseline; the
   values are in ink at 500, as Check and buy's are, since the muted grey on a dark card is 6.90:1,
   under the 7:1 the total and the day want (section 7 of the design brief). */
.stage-card-done .card-body > :is(.details, .card-desc) {
  text-align: start;
}

.stage-card-done .details {
  margin: 0;
  grid-template-columns: minmax(5rem, max-content) 1fr;
  align-items: baseline;
}

/* Under 360px wide the labels' column is as wide as its longest label, so "by Friday, October 30"
   keeps to one line. */
@media (max-width: 359px) {
  .stage-card-done .details {
    grid-template-columns: max-content 1fr;
    column-gap: 12px;
  }
}

.stage-card-done .details dd {
  color: var(--text);
  font-weight: 500;
  overflow-wrap: anywhere;
}

.card-desc {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Words shown whole, on the waiting, notice and Ordered cards. (A card's description is the details
   sheet's alone, whole there.) */
.card-desc-full {
  display: block;
  -webkit-line-clamp: unset;
}

/* Whole lines can carry an email address (the waiting card's): a long one wraps rather than
   widening the card. */
.card-desc-full {
  overflow-wrap: break-word;
}

/* Centred on the waiting and notice cards, so their lines split evenly rather than leaving a word
   on its own ("priced.", "ready."), as the clock's do. The Ordered card's reads from the left:
   there only its last line is kept from being a lone word. */
.card-desc-full {
  text-wrap: balance;
}

.stage-card-done .card-desc-full {
  text-wrap: pretty;
}

/* The waiting card's live step: a small spinner and what the search is doing. */
.card-progress {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text);
}

.card-progress .spinner {
  width: 16px;
  height: 16px;
  border-width: 2px;
}

/* The waiting card's bar (see fillBar in swipe.js): how far along the budget the answer is, under
   the title. Its fill eases to each new width, and a tick a second keeps it moving between the
   server's steps. */
.card-bar {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-tonal);
  overflow: hidden;
}

.card-bar-fill {
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: var(--accent);
  transition: width 1s linear;
}

@media (prefers-reduced-motion: reduce) {
  .card-bar-fill {
    transition: none;
  }
}

/* Where the search has looked, under the step (see updateWorking in swipe.js): its newest steps,
   newest last, muted and small as the clock is, with no marks. Each new one comes in from just
   below its place. */
.card-trail {
  display: grid;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.35;
  overflow-wrap: anywhere;
  text-wrap: balance;
}

.card-trail li,
.chat-trail li {
  animation: trail-in 0.3s ease-out;
}

@keyframes trail-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .card-trail li,
  .chat-trail li {
    animation: none;
  }
}

/* A button at work (the checkout sheet's "Placing order…", Buy all's "Ordering 4 pieces…"): the
   step line's 16px spinner before its words, 8px apart (.btn's gap). On petrol the ring is the
   fill and the arc the button's own text colour, since the soft ring would be lost on it. */
.btn-primary .spinner,
.room-buy-all .spinner {
  width: 16px;
  height: 16px;
  border-width: 2px;
}

.btn-primary .spinner {
  border-color: var(--accent);
  border-top-color: var(--on-accent);
}

.stage-card[data-working] .card-title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* A second look: the panel isn't line-clamped, so the whole message shows. */
.card-closer {
  margin: 0;
  font-size: 0.9rem;
}

.card-closer strong {
  display: block;
  color: var(--text);
}

.card-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 10px;
  margin-top: 2px;
}

.card-price strong {
  font-family: var(--font-display);
  font-size: min(2.6rem, 46px);
  font-weight: 800;
  letter-spacing: -0.01em;
  font-stretch: 85%;
  line-height: 1;
  font-variant-numeric: lining-nums proportional-nums;
}

.card-price span {
  font-size: 0.9375rem;
  color: var(--muted);
}

.card-delivery {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--text-2);
  font-weight: 500;
}

/* The total wraps under the buttons, right-aligned, when the row is too narrow for both (a 320px
   phone, a long unit), so neither "2 × roll" nor the total is cut. */
.stepper {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 12px;
  margin-top: 4px;
}

.stepper button {
  width: 44px;
  height: 44px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 50%;
  background: var(--surface-tonal);
  color: var(--text);
  font: inherit;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}

.stepper button:active {
  background: var(--border);
}

/* − at one, + at the most: dimmed, like any control that can't act. */
.stepper button[aria-disabled="true"] {
  opacity: 0.4;
  cursor: default;
}

.stepper button[aria-disabled="true"]:active {
  background: var(--surface-tonal);
}

.stepper button:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.stepper [data-qty] {
  min-width: 2ch;
  text-align: center;
  font-weight: 700;
  font-family: var(--font-display);
  font-size: 1.1875rem;
  font-variant-numeric: tabular-nums;
}

/* "× roll" after the count: the quantity is of the unit the price is per. A long unit ("× pack of
   6 rolls") wraps whole, on the rest of the row or a line of its own, never cut to "× pack of 6 r…". */
.stepper-unit {
  flex: 1 1 6rem;
  min-width: 0;
  margin-left: -4px;
  color: var(--muted);
  font-size: 0.9375rem;
}

/* Deliver it, or pick it up in a store: one switch, drawn like the request page's (.segmented in
   app.css): a tonal track with a steel edge, the chosen side raised inside a ring. */
.delivery-switch {
  display: flex;
  padding: 3px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-tonal);
}

.delivery-switch button {
  flex: 1 1 0;
  min-height: 2.75rem;
  padding: 4px 12px;
  border: 0;
  border-radius: 999px;
  background: #00000000;
  color: var(--text-2);
  font: inherit;
  font-family: var(--font-display);
  font-size: 0.9375rem;
  font-weight: 700;
  line-height: 1.15;
  cursor: pointer;
}

.delivery-switch button[aria-checked="true"] {
  background: var(--segment-on);
  color: var(--text);
  box-shadow: 0 0 0 var(--edge) var(--text-2), var(--shadow);
}

.delivery-switch button:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

/* "Not quite right?": link-sized, with room above and below to tap it. On the card it's in the last
   row, with Details (.card-links, which clears its margins); on the details sheet it keeps them, so
   its 44px take little more room than a line of words. */
.card-correct {
  justify-self: start;
  margin: -2px 0 -10px;
  padding: 10px 0;
  font-size: 0.9375rem;
  font-weight: 600;
  min-height: 44px;
  color: var(--accent-ink);
}

/* Cheaper from other brands?: under a branded item's options, link-sized as Not quite right? is;
   while its search goes, and once it's done, a quiet line in its place (see otherBrandsLine). */
.card-brands {
  justify-self: start;
  margin: -4px 0 -8px;
  padding: 10px 0;
  font-size: 0.9375rem;
  font-weight: 600;
  min-height: 44px;
  color: var(--accent-ink);
}

.card-brands-note {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 0.875rem;
  color: var(--text-2);
}

.card-brands-note .spinner {
  width: 16px;
  height: 16px;
  border-width: 2px;
}

.card-stamp {
  position: absolute;
  top: 22px;
  padding: 4px 12px;
  border: 3px solid currentColor;
  border-radius: 10px;
  background: var(--stamp-bg);
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  opacity: 0;
  pointer-events: none;
  font-stretch: 85%;
}

.stamp-like {
  left: 22px;
  color: var(--accent);
  transform: rotate(-10deg);
}

.stamp-nope {
  right: 22px;
  color: var(--text);
  transform: rotate(10deg);
}

/* In a deck of more than one option, a drag goes between them (NEXT, BACK) and buys nothing: neither
   stamp is in Buy's colour. */
.card-stamp.stamp-browse {
  color: var(--text);
}

/* ----- actions ----- */

.swipe-note {
  margin: -4px 0 0;
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted);
}

.swipe-note::before {
  content: "";
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 7px;
  border-radius: 50%;
  background: var(--accent);
  vertical-align: 1px;
  animation: swipe-note-pulse 1.6s ease-in-out infinite;
}

@keyframes swipe-note-pulse {
  0%,
  100% {
    opacity: 0.35;
  }
  50% {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .swipe-note::before {
    animation: none;
  }
}

/* Pass and Buy: labelled, the same size, flat, with the camera between them. They stay in sight:
   on a phone too short for the card (393x759 under the demo banner, 375x667 without it) the row
   sticks to the foot of the screen while the card scrolls under it, on the page's own colour, so no
   line shows between the pills. It keeps its place in the page below the card, so scrolled to the
   end the card's foot (the switch, Not quite right?) sits above it, never under it. Its margins
   take back .swipe-main's side and bottom padding, which the row's own padding puts back, so its
   colour runs to the edges of the column and its pills sit where they did, 16px over the home
   indicator's inset. */
.swipe-actions {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 3.5rem minmax(0, 1fr);
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin: 0 calc(-16px - env(safe-area-inset-right)) calc(-16px - env(safe-area-inset-bottom)) calc(-16px - env(safe-area-inset-left));
  padding: 8px calc(16px + env(safe-area-inset-right)) calc(16px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
  background: var(--bg);
}

/* Stuck over a card that runs on below it (swipe.js's markStuck): a hairline at its top edge, like
   the bar's, drawn inside it so nothing moves when it comes and goes. */
.swipe-actions.stuck {
  box-shadow: inset 0 1px 0 var(--border);
}

/* On a room, the piece Pass and Buy are for, named over them while its card's name is out of sight
   under the board (swipe.js's nameOverBar): its pin's number and name, a tap bringing the card up.
   A line across the row's three columns, in the muted ink, never a second filled control. */
.swipe-act-name {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 24px;
  margin: -2px 0 0;
  padding: 0 8px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-2);
  font: 500 0.875rem/1.2 var(--font);
  cursor: pointer;
}

.swipe-act-name:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A control on the card reached by Tab or VoiceOver (the delivery switch, Not quite right?) is
   scrolled to above the row, never left under it. */
html:has([data-actions]:not([hidden])) {
  scroll-padding-bottom: calc(4rem + 24px + env(safe-area-inset-bottom));
}

.swipe-act {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 4rem;
  padding: 0 14px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-tonal);
  color: var(--text);
  font-family: var(--font-display);
  font-size: 1.1875rem;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  transition: transform 0.12s ease, background-color 0.12s ease;
}

/* The words come from the title the markup already has ("Pass", "Buy", then "Buy $46.75" once
   swipe.js knows the total); the aria-label stays the accessible name. */
.swipe-act::after {
  content: attr(title);
}

.swipe-act .icon {
  width: 22px;
  height: 22px;
}

.swipe-act:not(:disabled):active {
  transform: scale(0.97);
}

/* Keyboard focus only: :focus-within also matched Buy whenever it had focus, so a sheet closed with
   a tap, handing focus back to Buy, left it ringed as if chosen. Closed with Escape, it's ringed. */
.swipe-act:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

/* The camera between Pass and Buy is a label, focused through its photo picker. A rule of its own,
   so a browser without :has (iOS 15) keeps Pass and Buy's. */
.swipe-act:has(input:focus-visible) {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.swipe-act:disabled {
  opacity: 0.4;
  cursor: default;
}

/* Buy: the screen's one petrol control. */
.swipe-act-yes {
  border-color: #00000000;
  background: var(--accent);
  color: var(--on-accent);
}

.swipe-act-yes:not(:disabled):active {
  background: var(--accent-pressed);
}

/* The heart means "save" in shopping apps; Buy gets a check. */
.swipe-act-yes .icon {
  display: none;
}

.swipe-act-yes::before {
  content: "";
  flex: none;
  width: 22px;
  height: 22px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%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 24 24' fill='none' stroke='%23000000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* Buy with the delivered total in it ("Buy $46.75"): the words carry the meaning, so the check
   steps aside and the letters narrow; a four-figure total (it has a comma) narrows further.
   Measured: "Buy $999.99" is 94px at 75% and "Buy $12,840.00" 100px at 62%, inside the 108px
   a 375px-wide phone leaves inside the pill. */
.swipe-act-yes:not([title="Buy"]) {
  padding: 0 12px;
}

.swipe-act-yes:not([title="Buy"])::before {
  display: none;
}

.swipe-act-yes:not([title="Buy"])::after {
  font-stretch: 75%;
}

.swipe-act-yes[title*=","]::after {
  font-stretch: 62%;
}

/* The camera between them: round, tonal, no words. */
.swipe-act-small {
  justify-self: center;
  width: 3.5rem;
  height: 3.5rem;
  padding: 0;
  color: var(--text-2);
}

.swipe-act-small::after {
  content: none;
}

.swipe-act-small .icon {
  width: 24px;
  height: 24px;
}

.card-swipe.settle .card-stamp {
  transition: opacity 0.3s ease;
}

.photo-buttons {
  display: grid;
  gap: 8px;
}

/* The waiting card's way to the next photo, under the wait line; and over it, for an account with
   nothing saved to buy with yet, Add your address and card. */
.stage-card .card-snap,
.stage-card .card-checkout {
  margin-top: 2px;
}

/* Its two lines centred, as everything on the waiting card is (the camera's sit by an icon). */
.card-checkout .btn-words {
  justify-items: center;
  text-align: center;
}

/* Under 360px the hint would take a third line, and the card's picture is already down to its strip
   at 320x568: the button says what it does on its own there. */
@media (max-width: 359px) {
  .card-checkout .btn-hint {
    display: none;
  }
}

/* A photo picked on the swipe screen, with the note it can go with. */
.note-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.note-head h2 {
  margin: 0;
}

.note-photo {
  flex: none;
  width: 64px;
  height: 64px;
  border-radius: 12px;
  background: var(--surface-muted);
  object-fit: cover;
}

/* Several photos of one item, side by side and smaller; the title goes under them on a narrow phone. */
.note-head:has(.note-photo-more) {
  flex-wrap: wrap;
}

.note-photos {
  flex: none;
  display: flex;
  gap: 6px;
}

.note-photos:has(.note-photo-more) .note-photo {
  width: 48px;
  height: 48px;
}

/* Or the next item typed, in Start a chat's sheet. */
.stage-card .card-type {
  display: inline-flex;
  align-items: center;
  justify-self: center;
  min-height: 44px;
  text-decoration: none;
}

/* Imagine's composer: Add a photo, the field and Send in a row, as a reply is typed. The field takes
   the row; Send keeps its size beside it, in reach with the keyboard up. Its placeholder ("Describe
   it…") fits at 320px; what still doesn't, with larger text, ends in an ellipsis, never cut mid-letter. */
.ask-row {
  display: flex;
  gap: 8px;
}

.ask-row .ask-input {
  flex: 1;
  min-width: 0;
  text-overflow: ellipsis;
}

.ask-row .ask-input::placeholder {
  text-overflow: ellipsis;
}

/* Add a photo and Send: squares as tall as the field, so it keeps the row on a 320px phone. */
.ask-photo,
.ask-send {
  flex: none;
  width: 3.25rem;
  min-height: 3.25rem;
  padding: 0;
}

.ask-photo .icon,
.ask-send .icon {
  width: 22px;
  height: 22px;
}

/* The photos picked to go with the words, small, each with its own way off. */
.composer-photos {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.composer-photo {
  position: relative;
  display: block;
}

.composer-photo img {
  display: block;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  object-fit: cover;
}

/* A 24px dot on the photo's corner, with a 44px place to tap around it. */
.composer-photo-remove {
  position: absolute;
  top: -8px;
  right: -8px;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 2px solid var(--surface);
  border-radius: 999px;
  background: var(--text);
  color: var(--surface);
  cursor: pointer;
}

.composer-photo-remove::after {
  content: "";
  position: absolute;
  inset: -10px;
}

.composer-photo-remove .icon {
  width: 14px;
  height: 14px;
  stroke-width: 3;
}

.composer-photo-remove:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

/* ----- toast and sheets ----- */

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(16px + env(safe-area-inset-bottom));
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 16px;
  width: max-content;
  max-width: calc(100% - 32px);
  padding: 8px 8px 8px 18px;
  border-radius: 16px;
  background: var(--toast-bg);
  color: var(--toast-text);
  box-shadow: var(--shadow-toast);
  font-size: 0.9375rem;
  font-weight: 500;
  transform: translateX(-50%);
}

/* While Pass and Buy show, the toast sits above them, never over them (iOS 15.4+; older iOS
   keeps it at the bottom). */
body:has([data-actions]:not([hidden])) .toast {
  bottom: calc(4rem + 28px + env(safe-area-inset-bottom));
}

/* The Undo after an order sits at the top of the Ordered card, over its picture, not at the foot of
   the screen, where for its 15 seconds it covered the invite (and on a short phone Cancel order and
   Track it). swipe.js sets --toast-top to 12px into the card, below the bar and clear of the strip of
   photos above the card; without it, 12px under the bar (8px + 44px links + 8px + its 1px line). */
body[data-ordered] .toast {
  top: var(--toast-top, calc(73px + env(safe-area-inset-top)));
  bottom: auto;
}

.toast-action {
  flex: none;
  border: 0;
  background: var(--toast-action);
  color: inherit;
  font: inherit;
  font-weight: 700;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  min-height: 44px;
  min-width: 64px;
  padding: 0 16px;
  border-radius: 999px;
}

.sheet-scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: var(--scrim);
}

.sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 41;
  max-width: 520px;
  max-height: 90vh;
  max-height: 90dvh;
  margin: 0 auto;
  padding: 0 20px calc(24px + env(safe-area-inset-bottom));
  overflow: auto;
  /* A field reached by Tab, or the cursor going into one, is scrolled to below the bar. */
  scroll-padding-top: 56px;
  border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
  background: var(--surface);
  box-shadow: var(--shadow-sheet);
}

/* Every bottom sheet's bar: the grab handle in the middle and Close at the left, as the chat's is,
   staying at the top while the sheet scrolls under it, so there's always a way out in sight. The
   bar, and only the bar, drags the sheet down to put it away (swipe.js), so the drag never fights
   a scroll, a field or the card's Stripe frame. Close is the 44px tonal round with the steel edge,
   never petrol: each sheet's own submit is its one petrol control. 56px tall, so Close sits 6px
   clear of the sheet's top edge rather than against it. Its margins take back the sheet's side
   padding and its padding puts it back, so Close lines up with the sheet's 20px gutter, as the
   chat's Close lines up with its composer. */
.sheet-bar {
  position: sticky;
  top: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  height: 56px;
  margin: 0 -20px;
  padding: 0 20px;
  background: var(--surface);
  touch-action: none;
}

/* While the sheet's lines go on under the bar, it draws a 1px hairline inside its foot, as the row at
   the sheet's foot does when it's stuck, so a line cut at its edge reads as scrolled, not broken. At
   the sheet's top there is none. */
.sheet-bar.stuck {
  box-shadow: inset 0 -1px 0 var(--border);
}

.sheet-handle {
  position: absolute;
  top: 8px;
  left: 50%;
  width: 36px;
  height: 5px;
  margin-left: -18px;
  border-radius: 999px;
  background: var(--border-strong);
}

.sheet h2 {
  margin: 0;
  font-size: 1.375rem;
}

/* A sheet's lines are spaced by its stack's gap alone, as the sign-in wall's are: a paragraph's or
   a heading's own bottom margin on top of the gap read as an empty block, between the checkout
   sheet's lead and the fee's rule and between Card and its line. */
.sheet .stack > p,
.sheet .stack > h3 {
  margin: 0;
}

.sheet .link-button {
  justify-self: center;
  min-height: 44px;
  text-align: center;
}

/* A sheet's own buttons in a row at its foot that stays in sight while the sheet scrolls under it,
   as Pass and Buy's row stays at the foot of the card's page: on the sheet's surface, with the
   hairline the card's row draws when it's stuck. It's last in its form, so scrolled to the end the
   sheet's lines sit above it, never under it. Its margins take back the sheet's side and bottom
   padding and its own padding puts them back, so its colour runs to the sheet's edges and its
   buttons sit 16px over the home indicator's inset. The sheet's padding is out of the sticky
   rectangle, so the row sticks that far below it to reach the sheet's foot. */
.sheet-actions {
  position: sticky;
  bottom: calc(-24px - env(safe-area-inset-bottom));
  z-index: 2;
  display: grid;
  gap: 10px;
  margin: 0 -20px calc(-24px - env(safe-area-inset-bottom));
  padding: 10px 20px calc(16px + env(safe-area-inset-bottom));
  background: var(--surface);
  box-shadow: inset 0 1px 0 var(--border);
}

/* Two of them side by side, the same size (Pass and Buy are never different sizes). */
.sheet-actions-pair {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

.sheet-actions-pair .btn {
  min-width: 0;
  padding-inline: 12px;
}

/* A field reached by Tab, or the cursor going into one (the worn item's typed size), is scrolled
   to above the row, never left under it. Check and buy's row and Buy all's are taller, the hold, a
   first order's agree line and the error line over Buy and Don't buy, and as tall as those lines
   wrap: swipe.js sets --sheet-row to the row's height as the sheet opens, scrolls or resizes. */
.sheet:has(form:not([hidden]) > .sheet-actions) {
  scroll-padding-bottom: var(--sheet-row, calc(3.5rem + 26px + env(safe-area-inset-bottom)));
}

/* What's said over Buy is spaced by the row's gap alone, as a sheet's lines are by its stack's. */
.sheet-actions > p,
.sheet-actions > div > p {
  margin: 0;
}

/* Check and buy's row and Buy all's draw their hairline only while the sheet's lines go on under
   them (swipe.js marks them stuck, as markStuck does the card's row); scrolled to the end, or with
   everything in sight, they sit under the last line unmarked. */
[data-sheet-form="confirm"] > .sheet-actions,
[data-sheet-form="room-buy"] > .sheet-actions,
[data-sheet-form="claim"] > .sheet-actions {
  box-shadow: none;
}

.sheet-actions.stuck {
  box-shadow: inset 0 1px 0 var(--border);
}

/* Answers to tap instead of typing, like "Just one, not a pack". */
.quick-answers {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.quick-answer {
  padding: 8px 14px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-tonal);
  color: var(--text);
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  min-height: 44px;
}

.quick-answer:hover {
  border-color: var(--text-2);
  color: var(--text);
}

@media (prefers-reduced-motion: reduce) {
  .card-swipe,
  .card-swipe.settle,
  .card-swipe.flying,
  .swipe-act {
    transition: none;
  }

  .swipe-act:not(:disabled):active {
    transform: none;
  }

  .shimmer::after,
  .tray-pricing .tray-state::before {
    animation: none;
  }

  .tray-thumb {
    transition: none;
  }
}

/* ----- invites and credit ----- */

/* The credit a visitor has, under the home's greeting. */
.camera-credit {
  margin: 0;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  background: var(--success-soft);
  color: var(--success);
  font-size: 0.9375rem;
  font-weight: 500;
}

.camera-credit strong {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* What credit takes off this card's order, and what's left after it. */
.card-credit {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--success);
}

/* The invite under an order just placed: a 44px row to tap, however short its line. */
.card-invite {
  justify-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 6px 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--accent-ink);
}


/* ----- a regular's home: photos still open, and Buy again ----- */

.camera-section-title {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 750;
  letter-spacing: 0;
}

.camera-open {
  display: grid;
  gap: 6px;
}

.camera-open-tiles,
.buy-again-list {
  display: flex;
  gap: 10px;
  margin: 0 -16px;
  padding: 2px 16px 6px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.camera-open-tiles::-webkit-scrollbar,
.buy-again-list::-webkit-scrollbar {
  display: none;
}

.photo-tile {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 15rem;
  min-height: 56px;
  padding: 6px 14px 6px 6px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 16px;
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
}

.photo-tile .tray-thumb {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 10px;
}

.photo-tile-text {
  display: grid;
  min-width: 0;
}

.photo-tile-title {
  overflow: hidden;
  font-size: 0.9rem;
  font-weight: 650;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* The part number or size: never cut, unlike the name above it. */
.photo-tile-specs {
  color: var(--text-2);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.25;
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}

.photo-tile-state {
  color: var(--muted);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

.photo-tile.tray-stack .photo-tile-state {
  color: var(--accent-ink);
  font-weight: 650;
}

/* "Which one?" and "Needs you", in the colour the strip over the cards says them in. Searching,
   Getting price, Cancelled (tray-closed) and Passed (tray-passed) keep the muted colour above. */
.photo-tile.tray-choice .photo-tile-state,
.photo-tile.tray-question .photo-tile-state {
  color: var(--warning);
  font-weight: 650;
}

.photo-tile:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.buy-again {
  display: grid;
  gap: 4px;
  margin-top: 8px;
}

.buy-again > .muted {
  margin: 0;
}

.buy-again-list {
  list-style: none;
}

.again-card {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 9.5rem;
  padding: 10px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 16px;
  background: var(--surface);
}

.again-picture {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 84px;
  overflow: hidden;
  border-radius: 10px;
  background: var(--surface-muted);
  color: var(--muted);
}

.again-picture img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.again-picture.thumb-listing {
  padding: 6px;
  background: var(--well-listing);
}

.again-picture.thumb-listing img {
  object-fit: contain;
  mix-blend-mode: multiply;
}

.again-title {
  display: -webkit-box;
  overflow: hidden;
  font-size: 0.875rem;
  font-weight: 650;
  line-height: 1.2;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* The part number or size, under a name cut at two lines: never cut itself. */
.again-specs {
  color: var(--text-2);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.25;
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}

.again-price {
  color: var(--muted);
  font-size: 0.8125rem;
}

/* A saving worth acting on: the amount, tagged on the picture. */
.again-drop {
  position: absolute;
  top: 6px;
  left: 6px;
  box-shadow: var(--shadow);
}

/* What the same order costs now, from a newer search: in ink, up or down, and in green when it's
   the saving tagged on the picture. What was paid stays muted under it. */
.again-now {
  color: var(--text);
  font-size: 0.8125rem;
  font-weight: 650;
}

.again-less {
  color: var(--success);
}

/* At the foot of the card, level with the next card's however many lines are above it. */
.again-card form {
  margin: auto 0 0;
}

.again-card .btn {
  width: 100%;
  min-height: 44px;
}

/* A photo that didn't go up, with the button that sends it again. */
.camera-error {
  display: grid;
  gap: 8px;
}

.camera-error .form-error {
  margin: 0;
}

/* The strip over the cards names each photo, and prices it once it can. */
.tray-name {
  max-width: 100%;
  overflow: hidden;
  color: var(--text);
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Under the name, which the tile's width cuts: the part number or size, values alone. A crown
   width fits; a look's style and colour, judged from the photo, would run to six lines in 88px,
   so the line stops at two (the tile's label says it whole). */
.tray-specs {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  max-width: 100%;
  overflow: hidden;
  color: var(--text-2);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.2;
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}

.tray-waiting {
  cursor: default;
}

.tray-waiting .tray-thumb {
  opacity: 0.6;
}

/* ----- what the card says besides the price ----- */

/* "Part 3850 · Width 1.88"": what a pro checks before buying. */
.card-specs {
  margin: 0;
  color: var(--text-2);
  font-size: 0.875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The label, what earned it beside it ("Part number read off your label"), wrapping under it on a
   narrow phone rather than taking a line of its own where it fits. Details is on the card's last row
   (.card-links). */
.card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
}

.card-reason {
  color: var(--text-2);
  font-size: 0.8125rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The options under a card's words once the customer asks for them (Show other options): scrolling
   sideways to the card's edge, as they do in the box of what to check. */
.card-body > .card-options {
  margin: 0 -18px 0 0;
  padding-right: 18px;
}

/* "Same as your order on Oct 12". */
/* "Also in your photo:", then the other things in it, each a tap from being priced instead. */
.card-also {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0;
}

.card-also-label {
  color: var(--text-2);
  font-size: 0.875rem;
  font-weight: 600;
}

.card-also-thing {
  max-width: 100%;
  min-height: 44px;
  padding: 6px 14px;
  overflow: hidden;
  border: var(--edge) solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-tonal);
  color: var(--accent-ink);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 650;
  white-space: nowrap;
  text-overflow: ellipsis;
  cursor: pointer;
}

.card-also-thing:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.card-same {
  margin: 0;
  color: var(--success);
  font-size: 0.875rem;
  font-weight: 650;
}

/* A match we're not sure of: the tag says so in the warning's colours, not the accent's. */
.card-label-check {
  background: var(--warning-soft);
  color: var(--warning);
}

/* What to check, in the description's place: a sentence, the judged details, the other options. */
.card-check {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 4px;
  padding: 8px 10px;
  border-left: 3px solid var(--warning);
  border-radius: 4px 10px 10px 4px;
  background: var(--surface-tonal);
}

.card-check-note,
.card-check-detail {
  margin: 0;
  color: var(--text-2);
  font-size: 0.875rem;
  line-height: 1.3;
}

.card-check-detail {
  color: var(--text);
  font-weight: 600;
}

/* The options, price first: a tap switches the card to it. At most three, so they wrap rather
   than scroll, since a row cut off at the edge looks broken. A long name wraps inside its chip; the
   price keeps its width and never breaks ("$22.89", not "$2", "2.8" and "9" down three lines). */
.card-options {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 4px 0 0;
}

.card-option {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: 6px;
  max-width: 100%;
  min-height: 44px;
  padding: 10px 14px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 22px;
  background: var(--surface);
  color: var(--text-2);
  font: inherit;
  font-size: 0.875rem;
  line-height: 1.3;
  text-align: left;
  cursor: pointer;
}

.card-option strong {
  flex: none;
  color: var(--text);
  font-weight: 750;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.card-option span {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  overflow-wrap: anywhere;
  color: var(--text-2);
  font-weight: 600;
}

.card-option[aria-checked="true"] {
  background: var(--segment-on);
  box-shadow: 0 0 0 var(--edge) var(--text-2);
}

.card-option:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

/* In the details sheet, whose picture is above its chips and out of sight, each option shows its own
   picture at the chip's start, so the options are told apart by their look: the seller's whole on
   the listing well, 4px in and multiplied in, as the checkout chip's, its corners in step with the
   chip's. An option whose page gave no picture has none. */
.card-option-pictured {
  align-items: center;
  padding: 4px 14px 4px 4px;
}

.card-option .card-option-picture {
  flex: none;
  width: 48px;
  height: 48px;
  padding: 4px;
  border-radius: 16.5px;
  background: var(--well-listing);
}

.card-option-picture img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  mix-blend-mode: multiply;
}

/* A re-priced order, said before anything else on its card. */
.card-repriced {
  margin: 0;
  padding: 8px 10px;
  border-radius: 10px;
  background: var(--warning-soft);
  color: var(--text);
  font-size: 0.875rem;
  font-weight: 600;
}

/* "Delivery and our service fee included", right under the price: what the one price has in it. */
.card-fee {
  margin: -2px 0 0;
  color: var(--muted);
  font-size: 0.875rem;
}

.card-store-price {
  color: var(--muted);
  font-size: 0.875rem;
}

/* Why a pickup asked for today is tomorrow, under the delivery line that says so: muted, a reason
   rather than news. */
.card-too-late {
  color: var(--muted);
  font-size: 0.875rem;
}

.card-map {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--accent-ink);
  font-size: 0.9375rem;
  font-weight: 600;
}

/* The waiting card's clock: how long so far, and about how long is left. Its longest lines take two
   on a phone, split evenly rather than leaving a word on its own. */
.card-clock {
  margin: 0;
  color: var(--muted);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
  text-wrap: balance;
}

.stage-card .card-notify,
.stage-card .card-cancel {
  justify-self: center;
  min-height: 44px;
}

/* A notice taller than the card (a long item name on a 320px phone, say) scrolls its words the way
   a swipe card's do, so its last button (Cancel request) is never cut off at the bottom. */
.stage-card .card-body {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  grid-auto-rows: max-content;
}

/* ----- the sheets on the way to buying ----- */

/* What's being bought, in sight at the top of each sheet: the item's picture beside the item and the total. */
.sheet-summary {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 14px;
  padding: 10px 12px;
  border-radius: 12px;
  background: var(--surface-tonal);
  font-size: 0.9375rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

/* The picture, 48px, beside the words and never under them: their own photo cropped to fill it, or
   the seller's whole on the listing well, multiplied in as on the card. */
.sheet-summary-picture {
  flex: none;
  width: 48px;
  height: 48px;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
}

.sheet-summary-picture img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.sheet-summary-picture.thumb-listing {
  padding: 4px;
  background: var(--well-listing);
}

.sheet-summary-picture.thumb-listing img {
  object-fit: contain;
  mix-blend-mode: multiply;
}

.sheet-summary-text {
  min-width: 0;
}

/* Under the total: what it has in it, in the card's words ("Delivery and our service fee included"). */
.sheet-summary-included {
  display: block;
  margin-top: 2px;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--muted);
}

.confirm-details {
  margin: 0;
}

/* Where it goes, once there's more than one address: on the card under the delivery switch, the
   address orders go to and Change, a 44px tap like "Not quite right?". */
.card-destination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
}

.card-destination-text {
  min-width: 0;
  margin: 0;
  color: var(--text-2);
  font-size: 0.9375rem;
  font-weight: 500;
  overflow-wrap: anywhere;
}

.card-destination-change {
  flex: none;
  min-height: 44px;
  padding: 0 2px;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--accent-ink);
}

/* The addresses to choose from, on Buy this? and on the sheet Change opens: a tonal pill each with
   the steel edge, the one orders go to raised inside a ring like the card's options. Never petrol:
   Buy is the one petrol control. */
.confirm-addresses {
  display: grid;
  gap: 8px;
}

.confirm-addresses-title {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 600;
}

.address-options {
  display: grid;
  gap: 8px;
}

.address-option {
  display: grid;
  gap: 2px;
  justify-items: start;
  min-width: 0;
  min-height: 44px;
  padding: 8px 16px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 22px;
  background: var(--surface-tonal);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.address-option strong {
  font-weight: 650;
  overflow-wrap: anywhere;
}

.address-option span {
  color: var(--text-2);
  font-size: 0.875rem;
  overflow-wrap: anywhere;
}

.address-option[aria-checked="true"] {
  border-color: var(--text-2);
  background: var(--segment-on);
  box-shadow: 0 0 0 var(--edge) var(--text-2);
}

.address-option:disabled {
  opacity: 0.55;
  cursor: wait;
}

.address-option:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

/* On Buy this?, the names alone, side by side while they fit, so the sheet stays short enough for
   Buy to be in sight: the Delivery line above it says where the chosen one is. */
.address-options-compact {
  display: flex;
  flex-wrap: wrap;
}

.address-options-compact .address-option {
  align-items: center;
  max-width: 100%;
  border-radius: 999px;
}

.wallet {
  display: grid;
  gap: 8px;
}

.wallet-or {
  margin: 0;
  text-align: center;
}

/* The iPhone app's Apple Pay button (see walletBox in views.ts): WebKit's own, black, where WebKit
   draws one; else ours, black with Apple's logo, as Apple's guidelines draw the button. */
.apple-pay-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 52px;
  border: 0;
  border-radius: 12px;
  background: #000;
  color: #fff;
  font: 600 1.0625rem/1 -apple-system, system-ui, sans-serif;
}
.apple-pay-drawn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.apple-pay-drawn .sign-in-logo {
  width: 18px;
  height: 18px;
  margin-top: -3px;
}
@supports (-webkit-appearance: -apple-pay-button) {
  .apple-pay-button {
    -webkit-appearance: -apple-pay-button;
    -apple-pay-button-type: buy;
    -apple-pay-button-style: black;
  }
  .apple-pay-drawn {
    display: none;
  }
}

/* ----- Details: the way into what we know about the item, at the card's foot (.card-links) ----- */

.card-more {
  min-height: 44px;
  padding: 0 2px;
  color: var(--accent-ink);
  font-size: 0.9375rem;
  font-weight: 650;
}

/* ----- the size, for something worn: chips to tap, or a field when the listing didn't say ----- */

.size-picker {
  display: grid;
  gap: 6px;
}

.size-picker-label {
  margin: 0;
  color: var(--text-2);
  font-size: 0.875rem;
  font-weight: 650;
}

.size-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.size-chip {
  min-width: 44px;
  min-height: 44px;
  padding: 6px 14px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-family: var(--font-display);
  font-size: 0.9375rem;
  font-weight: 700;
  cursor: pointer;
}

.size-chip[aria-checked="true"] {
  background: var(--segment-on);
  box-shadow: 0 0 0 var(--edge) var(--text-2);
}

.size-chip:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.size-other {
  justify-self: start;
  min-height: 44px;
  margin: -6px 0 -8px;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--accent-ink);
}

.size-input {
  width: 100%;
  min-height: 44px;
  padding: 8px 12px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

/* ----- the details sheet: what we know about the item, over its card ----- */

.details-body {
  gap: 10px;
}

/* The sheet stacks its lines full width; the label stays a tag. */
.details-body > .card-label {
  justify-self: start;
  align-self: start;
  width: fit-content;
}

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

/* One picture alone (no seller's, or no photo of theirs) takes the width, at a height that leaves
   the price on the screen. */
.details-pictures > :only-child {
  grid-column: 1 / -1;
}

.details-pictures > :only-child img {
  aspect-ratio: 4 / 3;
  max-height: 38vh;
}

.details-picture {
  display: grid;
  gap: 4px;
  margin: 0;
}

.details-picture img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 12px;
  background: var(--surface-muted);
}

.details-picture.card-listing {
  padding: 0;
  background: none;
}

.details-picture.card-listing img {
  object-fit: contain;
  padding: 10px;
  background: var(--well-listing);
  mix-blend-mode: normal;
}

.details-picture figcaption {
  color: var(--muted);
  font-size: 0.8125rem;
}

.details-asked {
  margin: 0;
  padding: 12px;
  border-radius: 12px;
  background: var(--surface-tonal);
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 700;
}

.sheet .details-title {
  font-size: 1.375rem;
  line-height: 1.16;
}

.details-note,
.details-desc {
  margin: 0;
  color: var(--text-2);
  font-size: 0.9375rem;
  line-height: 1.4;
}

.details-note {
  padding: 8px 10px;
  border-left: 3px solid var(--warning);
  border-radius: 4px 10px 10px 4px;
  background: var(--surface-tonal);
}

.details-heading {
  margin: 4px 0 0;
  font-size: 1rem;
}

.details-facts {
  display: grid;
  gap: 4px;
  margin: 0;
  padding-left: 1.2em;
  color: var(--text);
  font-size: 0.9375rem;
}

.details-specs {
  margin: 0;
}

/* While Imagine is on: whether to see it first, by Buy, with Imagine it tonal, since Buy is the
   sheet's petrol control. */
.details-imagine-ask {
  margin: 6px 0 0;
  color: var(--text);
  font-size: 0.9375rem;
  line-height: 1.4;
}

.details-imagine .icon {
  width: 20px;
  height: 20px;
}

/* Buy with the total in it, in its half of the sheet's row: the letters narrow as the card's Buy
   does, and further for a four-figure total (swipe.js marks it data-long). */
[data-details-buy] {
  font-stretch: 75%;
}

[data-details-buy][data-long] {
  font-stretch: 62%;
}

/* ----- rooms (see renderRoom in swipe.js and rooms.ts) ----- */

/* The room over its cards: its photo, or its drawing, with a numbered pin where each piece goes.
   The picture keeps its own shape (never cropped, so the pins land where the plan put them), and
   stays short enough that the top card's picture still shows on a phone's screen. */
.room-board {
  display: grid;
  gap: 8px;
}

.room-picture {
  position: relative;
  justify-self: center;
  width: fit-content;
  max-width: 100%;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  line-height: 0;
}

.room-base {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: 32vh;
  max-height: 32dvh;
}

/* The sketch: each piece's own picture fitted into the box the plan gave it, the white of a
   product shot multiplied away. */
.room-piece {
  position: absolute;
  z-index: 1;
  object-fit: contain;
  mix-blend-mode: multiply;
  pointer-events: none;
}

/* A pin sits on a picture, so it's drawn the same in both themes: its number in ink on white, with
   an ink edge, never petrol, which is Buy's alone. */
.room-pin {
  position: absolute;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  padding: 0;
  border: 1.5px solid #111719;
  border-radius: 999px;
  background: #FFFFFF;
  color: #111719;
  font: 700 0.8125rem/1 var(--font);
  box-shadow: 0 1px 4px #00000059;
  cursor: pointer;
}

/* A thumb's worth to tap, around a pin small enough not to hide the room. */
.room-pin::before {
  content: "";
  position: absolute;
  inset: -8px;
  border-radius: 999px;
}

/* Ordered in green, passed on or not found in grey, each with a white number and edge. A piece still
   being found, or getting a price, is drawn as one on its card is. */
.room-pin-ordered {
  border-color: #FFFFFF;
  background: #1B6E37;
  color: #FFFFFF;
}

.room-pin-passed,
.room-pin-left_out,
.room-pin-cancelled {
  border-color: #FFFFFF;
  background: #4F5A5F;
  color: #FFFFFF;
}

/* The piece whose card is on top, where Buy and Pass are: filled in ink (see markRoomPin). */
.room-pin-current {
  border-color: #FFFFFF;
  background: #111719;
  color: #FFFFFF;
}

.room-pin:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.room-caption {
  display: grid;
  gap: 2px;
}

.room-title {
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.35;
}

.room-title strong {
  font-family: var(--font-display);
  font-weight: 750;
}

.room-summary,
.room-status {
  margin: 0;
  color: var(--text-2);
  font-size: 0.875rem;
  line-height: 1.4;
}

/* The lines under the board name each piece after its pin's number, drawn as the pin is, so a name
   with a comma in it ("Round jute rug, 6 feet") reads as one piece. A name goes to the next line
   whole rather than leaving "lamp" under "Brass floor", unless it's longer than the line. */
.room-name {
  display: inline-block;
  max-width: 100%;
  margin-right: 10px;
}

.room-num {
  display: inline-grid;
  place-items: center;
  width: 20px;
  height: 20px;
  margin-right: 4px;
  border: 1.5px solid #111719;
  border-radius: 999px;
  background: #FFFFFF;
  color: #111719;
  font: 700 0.75rem/1 var(--font);
  font-variant-numeric: tabular-nums;
  vertical-align: 1px;
}

.room-note {
  margin: 0;
  color: var(--muted);
  font-size: 0.8125rem;
  line-height: 1.4;
}

/* The board's switch between its pictures (the drawing with the pieces, the photo), the card's
   delivery switch drawn the same, with room under it before the look's name. */
.room-views {
  margin-bottom: 8px;
}

/* Under 340px wide, "With pieces" and "Your photo" wrapped to two lines inside the pill. */
@media (max-width: 339px) {
  .room-views button {
    padding-inline: 6px;
    font-size: 0.875rem;
  }
}

/* Buy all, with room above it after the note and below it before what's still being found. */
.room-buy-all {
  margin: 10px 0 6px;
}

/* What Buy all ordered, in its place on the board (swipe.js's roomOrderedCard): a box like a card's,
   with the Ordered card's rows, its orders' numbers and See orders. */
.room-ordered {
  display: grid;
  gap: 10px;
  margin: 10px 0 6px;
  padding: 14px 16px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.room-ordered .card-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: 1.125rem;
}

.room-ordered .details {
  margin: 0;
}

.room-ordered .room-ordered-next {
  margin: 0;
}

/* Buy all's sheet: a row for each piece, a box to tick (all ticked to start with), its picture, its
   pin's number and name, and its total, so one they already have can be left out. The whole row is
   the box's label, at least 56px tall, and a long name wraps rather than pushing the total off. */
.room-buy-pieces {
  display: grid;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.room-buy-piece {
  display: grid;
  grid-template-columns: 20px 40px 1fr auto;
  gap: 12px;
  align-items: center;
  min-height: 56px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}

/* The box: the steel edge on the surface, filled in ink with a tick in the surface colour once
   ticked. Never petrol, which is Buy's alone; the focus ring is the usual petrol one. */
.room-buy-piece input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  display: grid;
  place-content: center;
  width: 20px;
  height: 20px;
  margin: 0;
  border: var(--edge) solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  cursor: pointer;
}

.room-buy-piece input[type="checkbox"]::before {
  content: "";
  width: 10px;
  height: 5px;
  margin-top: -3px;
  border: solid var(--surface);
  border-width: 0 0 2px 2px;
  transform: rotate(-45deg) scale(0);
}

.room-buy-piece input[type="checkbox"]:checked {
  border-color: var(--text);
  background: var(--text);
}

.room-buy-piece input[type="checkbox"]:checked::before {
  transform: rotate(-45deg);
}

.room-buy-piece input[type="checkbox"]:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

/* The piece's picture as its card shows it, as the checkout chip draws it: the seller's whole on the
   listing well, 4px in and multiplied in so the shot's white becomes the well (dimmed in dark mode by
   the dark well itself); else their photo, cropped to fill. */
.room-buy-picture {
  grid-column: 2;
  width: 40px;
  height: 40px;
  overflow: hidden;
  border-radius: 10px;
  background: var(--surface-muted);
}

.room-buy-picture img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.room-buy-picture.thumb-listing {
  padding: 4px;
  background: var(--well-listing);
}

.room-buy-picture.thumb-listing img {
  object-fit: contain;
  mix-blend-mode: multiply;
}

/* A name that wraps never leaves its last word alone ("Round jute rug, / 6 feet", not "6 / feet"). */
.room-buy-name {
  grid-column: 3;
  min-width: 0;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.3;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

.room-buy-amount {
  grid-column: 4;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--text);
}

/* Unticked: its total as its card says it, in the second ink, out of the Total. */
.room-buy-left-out .room-buy-amount {
  color: var(--text-2);
}

/* Which of the room's pins a piece's card is: "1 of 4", before its label. */
.card-piece {
  padding: 3px 8px;
  border-radius: var(--radius-tag);
  background: var(--surface-tonal);
  color: var(--text);
  font-size: 0.8125rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

[data-correct-photo] .icon {
  flex: none;
}

/* Under a room's board the stage keeps about the height a photo's card has without one, so the
   piece's picture isn't squeezed to its least: the page scrolls from the room to its cards. */
.room-board:not([hidden]) + .swipe-stage {
  min-height: min(620px, calc(100dvh - 140px));
}

/* And a piece's card keeps a picture of the piece worth the name: under 800px tall a card's picture
   gives way to a 64px strip, half of it under the pills, and a piece is chosen by its look. In
   their photo, a card keeps its own taller picture. */
.card-swipe:not(.card-in-space):has(.card-piece) .card-photo {
  min-height: min(220px, 32dvh);
}

/* ----- Start a chat (chatScreen in views.ts, openChat in swipe.js) ----- */

/* A screen of its own, as a chat app's new chat is: the sheet fills what's in sight, on the page's
   own colour, with no handle and no corners. swipe.js sets its top and height to what's in sight
   above the keyboard (see fitChat); without them it's the whole screen. */
.sheet.sheet-chat {
  top: var(--chat-top, 0px);
  bottom: auto;
  height: var(--chat-height, 100%);
  max-width: none;
  max-height: none;
  padding: 0;
  overflow: hidden;
  border-radius: 0;
  background: var(--bg);
  box-shadow: none;
  animation: chat-in 0.2s ease-out;
}

/* It fades in, as an order's photo does (no fade with Reduce Motion, below). */
@keyframes chat-in {
  from {
    opacity: 0;
  }
}

/* The chat has its own Close, at the top of its column, and nothing to drag. */
.sheet.sheet-chat .sheet-bar {
  display: none;
}

/* The page behind it holds still, as it does behind an order's photo, and the scrim under it is the
   page's own colour: as the keyboard slides away, before the chat has grown back, nothing of the
   page shows through under it. */
html.chat-open {
  overflow: hidden;
}

html.chat-open .sheet-scrim {
  background: var(--bg);
  animation: chat-in 0.2s ease-out;
}

/* Close at the top, the greeting (then the chat) in the middle and the composer at the foot, in a
   column as wide as the camera's, clear of the notch, the corners and the home indicator. */
.chat {
  display: flex;
  flex-direction: column;
  gap: 10px;
  height: 100%;
  max-width: 480px;
  margin: 0 auto;
  padding: calc(8px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right)) calc(12px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
}

/* While the field has focus the keyboard is up, and the home indicator is under it: the composer
   sits right on the keyboard, without the strip kept for the indicator. (Telling the keyboard by
   the height left over it didn't work in the iPhone app: the composer kept the strip over it.) */
.chat:focus-within {
  padding-bottom: 8px;
}

.chat-bar {
  display: flex;
  flex: none;
}

/* Close, + and Send: 44px rounds. Close and + are tonal with the steel edge, and Send is the
   screen's one petrol control. */
.chat-round {
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  min-height: 0;
  padding: 0;
}

.chat-round .icon {
  width: 22px;
  height: 22px;
}

/* The greeting, or the chat once something's been said, takes the height between them and
   scrolls on its own. */
.chat-scroll {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* The mark over the greeting, in the middle of what's left, as a chat app's new chat has them. */
.chat-hello {
  display: grid;
  justify-items: center;
  gap: 12px;
  margin: auto 0;
  padding: 12px 8px;
  text-align: center;
}

.chat-mark {
  width: 3.25rem;
  height: 3.25rem;
  background: var(--brand-mark) center / contain no-repeat;
}

/* Too little room over the composer for all three (the keyboard up on a small phone): the mark
   steps aside, so the greeting and its line are never cut off (see fitHello in swipe.js). */
.chat-hello-tight .chat-mark {
  display: none;
}

/* Calmer than a sheet's title, as a greeting is: Archivo at 500, full width. The home's is its h1. */
.sheet .chat-hello h2,
.home-hello h1 {
  margin: 0;
  font-size: clamp(1.75rem, 8.5vw, 2.25rem);
  font-weight: 500;
  font-stretch: 100%;
  letter-spacing: -0.015em;
  line-height: 1.1;
  text-wrap: balance;
}

.chat-lead {
  max-width: 30ch;
  margin: 0;
  color: var(--muted);
  font-size: 0.9375rem;
  line-height: 1.45;
}

/* The chat: theirs on the right in a tonal bubble, with the photos they sent over it; ours on the
   left in plain words, as a chat app sets them. */
.chat-thread {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 8px 0 12px;
}

.chat-msg {
  display: grid;
  gap: 6px;
  max-width: 85%;
}

.chat-msg p {
  margin: 0;
  color: var(--text);
  font-size: 1rem;
  line-height: 1.45;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.chat-mine {
  align-self: flex-end;
  justify-items: end;
}

.chat-mine p {
  padding: 10px 16px;
  border-radius: 20px;
  background: var(--surface-tonal);
}

.chat-ours {
  align-self: flex-start;
}

.chat-working {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* The waiting card's clock under one of ours while it's priced (see syncChatClock in swipe.js):
   muted and small, lined up with the words after the spinner, and split evenly over two lines as
   the card's is. */
.chat-msg p.chat-clock {
  margin: 2px 0 0 32px;
  color: var(--muted);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
  text-wrap: balance;
}

/* Where the search has looked, under the words and over the clock (see syncChatTrail in swipe.js),
   lined up with the words after the spinner, as the card's is under its step. */
.chat-msg ol.chat-trail {
  display: grid;
  gap: 4px;
  margin: 2px 0 0 32px;
  padding: 0;
  list-style: none;
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.4;
  overflow-wrap: anywhere;
}

/* The waiting card's wait line, said once a conversation under the first of ours to be priced
   (see tellWait in swipe.js), outside the message, lined up with its words. */
.chat-thread > p.chat-wait {
  margin: 0 0 0 32px;
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.4;
  overflow-wrap: anywhere;
}

/* What can be done from one of ours (See it, Imagine it, which thing in the photo): tonal pills
   with the steel edge, as the sheets' quick answers are, since Send is the screen's petrol one. */
.chat-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

.chat-actions .quick-answer {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.chat-actions .icon {
  width: 18px;
  height: 18px;
}

/* Who makes Imagine's pictures, over Show it in my photo: muted and small, as on Imagine's sheet. */
.chat-msg p[data-imagine-agree] {
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.4;
}

.chat-photos {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
}

.chat-photos img {
  display: block;
  width: 6.5rem;
  height: 6.5rem;
  border-radius: 16px;
  background: var(--surface-muted);
  object-fit: cover;
}

.chat > .form-error {
  flex: none;
  margin: 0;
}

/* Decorate a space (see setChatRoom in swipe.js): a chip over the composer, lit while it's on, and
   over it then what happens and who sees the photo and who draws it. */
.chat-ideas {
  display: flex;
  flex: none;
  flex-wrap: wrap;
  gap: 8px;
}

.chat-ideas .quick-answer {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.chat-ideas .icon {
  width: 18px;
  height: 18px;
}

.chat-ideas .quick-answer[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-ink);
}

.chat-room {
  display: grid;
  flex: none;
  gap: 6px;
}

.chat-room p {
  margin: 0;
}

/* The composer: a card at the foot, with the photos to go inside it over the field, then + and
   Send in its corners. The field's focus is the card's: its edge turns steel. */
.chat-composer {
  flex: none;
  display: grid;
  gap: 8px;
  padding: 10px;
  border: var(--edge) solid var(--border);
  border-radius: 28px;
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

.chat-composer:focus-within {
  border-color: var(--border-strong);
}

/* 17px on an iPhone, so iOS never zooms in on it; a line tall to start, and up to six as it grows
   (see growChatField), then it scrolls. */
.chat-input {
  display: block;
  width: 100%;
  max-height: 9.75rem;
  margin: 0;
  padding: 6px 8px 2px;
  border: 0;
  background: #00000000;
  color: var(--text);
  font: inherit;
  font-size: 1.0625rem;
  line-height: 1.4;
  resize: none;
}

.chat-input::placeholder {
  color: var(--muted);
  opacity: 1;
}

.chat-input:focus {
  outline: none;
}

.chat-tools {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* The photos to go with the words, inside the card, each with its way off on its own corner. */
.chat-composer .composer-photos {
  gap: 8px;
  padding: 2px 2px 0;
}

.chat-composer .composer-photo img {
  width: 4.75rem;
  height: 4.75rem;
  border: 1px solid var(--border);
  border-radius: 14px;
}

.chat-composer .composer-photo-remove {
  top: 5px;
  right: 5px;
}

@media (prefers-reduced-motion: reduce) {
  .sheet.sheet-chat,
  html.chat-open .sheet-scrim {
    animation: none;
  }
}

/* ----- Imagine (see imagine.ts, and Imagine in swipe.js) ----- */

/* On a priced card's picture: the way into Imagine, a frosted pill over the picture's corner, and
   across from it, with their photo behind the item, In your photo. Never petrol: Buy is the card's
   one petrol control. */
.card-imagine,
.card-space-go {
  position: absolute;
  right: 10px;
  bottom: 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 16px 0 12px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 999px;
  background: var(--stamp-bg);
  color: var(--text);
  box-shadow: var(--shadow);
  font-family: var(--font-display);
  font-size: 0.9375rem;
  font-weight: 700;
  cursor: pointer;
}

/* Sent as words, the card's picture is a short strip of them: the pill goes beside them, not over them. */
.card-swipe .card-words:has(.card-imagine) {
  flex-direction: row;
  justify-content: space-between;
  gap: 12px;
  text-align: left;
}

.card-swipe .card-words .card-imagine {
  position: static;
  flex: none;
}

.card-space-go {
  right: auto;
  left: 10px;
}

/* Outranks the picture's own icon rules: the placeholder's 72px, and words' none. */
.card-swipe .card-photo .card-imagine .icon,
.card-swipe .card-photo .card-space-go .icon {
  display: block;
  width: 20px;
  height: 20px;
  opacity: 1;
}

/* Under 360px wide, both pills don't fit over the picture with their icons: the icons go. */
@media (max-width: 359px) {
  .card-swipe .card-photo:has(.card-space-go) :is(.card-imagine, .card-space-go) {
    padding: 0 14px;
  }

  .card-swipe .card-photo:has(.card-space-go) :is(.card-imagine, .card-space-go) .icon {
    display: none;
  }
}

.card-imagine:focus-visible,
.card-space-go:focus-visible,
.card-space-again:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

/* ----- In your photo (showInSpace in swipe.js) ----- */

/* Shown in their photo, the picture is what the card's about: it takes more of the card (the page
   scrolls to the rest on a short phone, as ever), and it's shown whole, the room as they took it. */
.card-in-space .card-photo {
  min-height: min(360px, 48dvh);
}

/* Its own height doesn't count for a picture in the card (the card's is the content's), so the
   picture takes the box itself, to be shown whole in it; one taller than it's wide (a phone's photo
   held upright) has the height to be seen at the card's width. */
.card-space:not(.card-listing) > img:not(.card-space-item) {
  position: absolute;
  inset: 0;
  object-fit: contain;
}

.card-in-space .card-photo.card-space-tall {
  min-height: min(460px, 58dvh);
}

/* A wide one (a photo held sideways) takes its own shape instead, at least the height a piece's
   picture keeps, so no band of the well shows above and below it (swipe.js sets the shape once the
   picture is in). */
.card-in-space .card-photo.card-space-wide {
  min-height: min(220px, 32dvh);
  aspect-ratio: var(--picture-shape, 4 / 3);
}

/* While it's being made the picture shimmers, under the tag and Imagine, which stay in sight and
   tappable. */
.card-space.shimmer::after {
  pointer-events: none;
}

.card-space-tag,
.card-space .card-imagine {
  z-index: 1;
}

.card-swipe .card-space.card-words .card-imagine {
  position: relative;
}

/* Which of the options it is, or what's happening, over the picture's top corner. */
.card-space-tag {
  position: absolute;
  top: 10px;
  left: 10px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: calc(100% - 100px);
  min-height: 32px;
  margin: 0;
  padding: 0 12px;
  border-radius: 999px;
  background: var(--stamp-bg);
  color: var(--text);
  box-shadow: var(--shadow);
  font-size: 0.8125rem;
  font-weight: 700;
  line-height: 1.25;
}

.card-space-tag .spinner {
  width: 16px;
  height: 16px;
  border-width: 2px;
}

/* Try again, inside the tag of one that didn't come out: tall enough to tap. */
.card-space-again {
  min-height: 44px;
  padding: 0;
  border: 0;
  background: #00000000;
  color: var(--accent);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

/* The seller's own picture, small in the other corner: what they'd get, beside how it's drawn. */
.card-space .card-space-item {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 64px;
  height: 64px;
  padding: 4px;
  border: var(--edge) solid var(--border-strong);
  border-radius: 10px;
  background: var(--well-listing);
  box-shadow: var(--shadow);
}

/* The item at the sheet's top: its picture and name, as the card shows them. */
.imagine-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* The item's picture, as the checkout's summary shows it: their own photo filling the box, or the
   seller's whole on its well, blended into the well alone (isolated), so in dark mode the box
   isn't multiplied into the sheet and turned black. */
.imagine-item {
  flex: none;
  display: block;
  width: 56px;
  height: 56px;
  overflow: hidden;
  border-radius: var(--radius-sm);
  isolation: isolate;
}

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

.imagine-item.thumb-listing {
  padding: 4px;
  background: var(--well-listing);
}

.imagine-item.thumb-listing img {
  object-fit: contain;
  mix-blend-mode: multiply;
}

.imagine-heading {
  min-width: 0;
}

.sheet .imagine-heading h2 {
  margin: 0;
}

.sheet .imagine-heading p {
  margin: 2px 0 0;
}

/* The price under the item's name, as the card says it (one figure, all in), so the sheet that
   answers "want to see it before you buy?" says what buying it costs. */
.sheet .imagine-heading .imagine-price {
  font-size: 0.9375rem;
}

.imagine-price strong {
  font-weight: 650;
}

.imagine-price span {
  color: var(--muted);
  font-size: 0.8125rem;
}

/* Buy, under the newest picture: tonal, a pill as the chat's answers are. */
.imagine-buy {
  margin-top: 8px;
}

/* The chat: Imagine's messages on the left, theirs on the right, a picture it made full width. */
.imagine-thread {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.imagine-msg {
  display: grid;
  gap: 6px;
  max-width: 85%;
}

.sheet .imagine-msg p {
  margin: 0;
  padding: 10px 14px;
  border-radius: 18px;
  font-size: 0.9375rem;
  line-height: 1.4;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.imagine-theirs {
  align-self: flex-start;
}

.sheet .imagine-theirs p {
  border-bottom-left-radius: 6px;
  background: var(--surface-tonal);
  color: var(--text);
}

.imagine-mine {
  align-self: flex-end;
  justify-items: end;
}

.sheet .imagine-mine p {
  border-bottom-right-radius: 6px;
  background: var(--accent-soft);
  color: var(--text);
}

.imagine-msg:has(.imagine-picture) {
  max-width: 100%;
  width: 100%;
}

.imagine-picture {
  display: block;
  width: 100%;
  min-height: 120px;
  border-radius: 16px;
  background: var(--surface-muted);
}

.imagine-photo {
  display: block;
  width: 132px;
  max-width: 100%;
  border-radius: 14px;
  background: var(--surface-muted);
}

.imagine-working {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-radius: 18px 18px 18px 6px;
  background: var(--surface-tonal);
  color: var(--text-2);
  font-size: 0.9375rem;
}

/* Under an answer that sent something to be found (see foundLine in swipe.js): where it's at, with the spinner while it's priced; then its price and See it. */
.imagine-found {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  color: var(--text-2);
  font-size: 0.9375rem;
}

.sheet .imagine-failed p {
  background: var(--danger-soft);
  color: var(--danger);
}

/* ----- the chats on the left ----- */

/* The three lines that open the chats and put them away (chatsToggle in views.ts): a plain 44px
   round, as the bar's links are, at the left of the bar where a chat app keeps it, at the right of
   the chat's own bar, and in the panel. */
.chats-toggle {
  display: inline-grid;
  flex: none;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--text);
  cursor: pointer;
}

.chats-toggle:hover {
  background: var(--surface-muted);
}

.chats-toggle:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.chats-toggle .icon {
  width: 22px;
  height: 22px;
}

/* In the bar the lines sit close to the mark. */
.topbar > .chats-toggle {
  margin-right: -4px;
}

/* A phone's bar with the lines, the name and three links: the tile gives way, not the name, as it
   does for a fourth link (see app.css); on a 320px phone the lines close up to the name too. */
@media (max-width: 440px) {
  .topbar:has(> .chats-toggle) .brand-mark {
    display: none;
  }
}

@media (max-width: 359px) {
  .topbar > .chats-toggle {
    margin-right: -8px;
  }
}

/* The chat's bar: Close at the left, the lines at the right. */
.chat-bar {
  justify-content: space-between;
}

/* The panel: a column of the chats over the page's left edge, on a phone slid in from the left
   when open (html.chats-open) and out of sight, and out of reach, otherwise; the tinted surface a
   chat app's list has, with a hairline at its edge. Its padding is the bar's, so its lines sit
   where the bar's do. */
.chats {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  width: min(20rem, 85vw);
  padding: calc(8px + env(safe-area-inset-top)) 0 calc(12px + env(safe-area-inset-bottom)) env(safe-area-inset-left);
  border-right: 1px solid var(--border);
  background: var(--surface-muted);
  transform: translateX(-100%);
  visibility: hidden;
  transition: transform 0.2s ease-out, visibility 0s 0.2s;
}

html.chats-open .chats {
  transform: none;
  visibility: visible;
  box-shadow: 8px 0 32px -8px var(--scrim);
  transition: transform 0.2s ease-out;
}

/* The page behind the panel holds still under a scrim, as it does behind a sheet. */
html.chats-open {
  overflow: hidden;
}

.chats-scrim {
  position: fixed;
  inset: 0;
  z-index: 49;
  background: var(--scrim);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease-out, visibility 0s 0.2s;
}

html.chats-open .chats-scrim {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.2s ease-out;
}

.chats-bar {
  display: flex;
  flex: none;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
}

.chats-title {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 700;
  font-stretch: 80%;
}

/* New chat: the home, with a plus in a petrol round, as a chat app starts one. */
.chats-new {
  display: flex;
  flex: none;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  margin: 8px 12px 4px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-weight: 600;
  text-decoration: none;
}

.chats-new:hover {
  background: var(--surface-tonal);
}

.chats-new-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
}

.chats-new-mark .icon {
  width: 18px;
  height: 18px;
}

/* The rows scroll on their own under New chat, grouped under Today, Yesterday and so on. */
.chats-list {
  flex: 1;
  min-height: 0;
  padding: 0 8px 8px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.chats-group {
  margin: 12px 10px 4px;
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 600;
}

/* A row: the picture (or the words' bubble), the item or the words on one line, and where it's at
   under it. The one open now stands on the chosen surface, as a switch's chosen side does. */
.chats-item {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
}

.chats-item:hover {
  background: var(--surface-tonal);
}

.chats-item[aria-current="page"] {
  background: var(--segment-on);
  box-shadow: var(--shadow);
}

.chats-thumb {
  display: grid;
  flex: none;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  overflow: hidden;
  border-radius: 10px;
  background: var(--surface-tonal);
  color: var(--muted);
}

.chats-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.chats-thumb .icon {
  width: 18px;
  height: 18px;
}

.chats-text {
  display: grid;
  gap: 1px;
  min-width: 0;
}

.chats-name,
.chats-state {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chats-name {
  font-size: 0.9375rem;
  font-weight: 500;
}

.chats-state {
  color: var(--muted);
  font-size: 0.8125rem;
}

.chats-empty {
  margin: 12px 16px;
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.4;
}

@media (prefers-reduced-motion: reduce) {
  .chats,
  .chats-scrim,
  html.chats-open .chats,
  html.chats-open .chats-scrim,
  html.with-chats .swipe-body {
    transition: none;
  }
}

/* A laptop or tablet window: the panel stands beside the page, the page's bar and column starting
   after it, as a chat app's do, and the chat opens beside it rather than over it. Folded away
   (html.chats-shut, from the lines in the panel), the page takes the width back, and the lines in
   the bar bring it back. The strip of today's photos steps aside: the panel says the same of them. */
@media (min-width: 900px) {
  /* Under a sheet's scrim, which dims it with the page (the chat's scrim starts where it ends). */
  .chats {
    z-index: 39;
    width: 17rem;
    transform: none;
    visibility: visible;
    transition: transform 0.2s ease-out;
  }

  html.chats-shut .chats {
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform 0.2s ease-out, visibility 0s 0.2s;
  }

  html.chats-open .chats {
    box-shadow: none;
  }

  .chats-scrim {
    display: none;
  }

  html.with-chats .swipe-body {
    padding-left: 17rem;
    transition: padding-left 0.2s ease-out;
  }

  html.with-chats.chats-shut .swipe-body {
    padding-left: 0;
  }

  /* With the panel in sight, its own lines fold it away; the bar's and the chat's are for bringing it back. */
  html.with-chats:not(.chats-shut) .topbar > .chats-toggle,
  html.with-chats:not(.chats-shut) .chat-bar > .chats-toggle {
    display: none;
  }

  /* The sheets and the toast centre on the page's column beside the panel, and the chat fills it,
     its own scrim starting where the panel ends; every other sheet's scrim covers the panel too. */
  html.with-chats:not(.chats-shut) .sheet,
  html.with-chats.chat-open:not(.chats-shut) .sheet-scrim {
    left: 17rem;
  }

  html.with-chats:not(.chats-shut) .toast {
    left: calc(50% + 8.5rem);
    max-width: calc(100% - 17rem - 32px);
  }

  html.with-chats:not(.chats-shut) .camera-open {
    display: none;
  }
}
