/* DADCARE Mother App shell and authentication pages. */

body.shell { padding-bottom: 0; }

.hero { background: linear-gradient(160deg, var(--brand-soft), var(--bg)); }
.hero-in { max-width: 1100px; margin: 0 auto; padding: 14px 16px 26px; }
.hero-top { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; }
.logo { font-weight: 800; letter-spacing: .02em; font-size: 18px; }
.hero-top .spacer { flex: 1; }
.hero h1 { font-size: 26px; line-height: 1.2; margin: 0 0 8px; }
.hero p { color: var(--muted); margin: 0 0 16px; max-width: 46ch; }
.hero-cta { display: flex; gap: 10px; flex-wrap: wrap; }
@media (min-width: 720px) { .hero h1 { font-size: 34px; } }

.btn.small { min-height: 38px; padding: 8px 12px; font-size: 14px; }

.welcome { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.welcome > div:first-child { flex: 1; min-width: 0; }
.hint { color: var(--muted); font-size: 12px; }

.section { font-size: 14px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); margin: 18px 0 10px; }

.apps { display: grid; gap: 12px; grid-template-columns: repeat(2, 1fr); }
@media (min-width: 640px) { .apps { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 960px) { .apps { grid-template-columns: repeat(4, 1fr); } }

.app {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 18px 14px 16px; text-align: center;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text); min-height: 168px;
  justify-content: flex-start;
  transition: border-color .15s ease, transform .15s ease;
}
.app .nm { font-weight: 650; font-size: 15px; line-height: 1.25; }
/* Descriptive only. No tile ever carries a count, rating or statistic. */
.app .ds { font-size: 12px; line-height: 1.35; color: var(--muted);
  flex: 1; display: block; }
.app .badge { margin-top: auto; }
@media (hover: hover) {
  a.app:hover { border-color: var(--brand); transform: translateY(-1px); }
}
/* A coming-soon tile must not look like something you can use: it is not a
   link, it does not lift on hover, and its state is stated plainly. */
.app.disabled { cursor: not-allowed; background: var(--surface); border-style: dashed; }
.app.disabled .nm, .app.disabled .ds { color: var(--muted); }
/* The identity is kept — only its intensity drops — so a coming-soon app is
   still recognisably itself without looking usable. */
.app.disabled .app-ico { opacity: .55; }
.app.disabled .badge { background: var(--surface-2); color: var(--muted); }

.shell-foot {
  margin-top: 26px; padding: 18px 16px calc(22px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border); color: var(--muted); font-size: 12px;
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  justify-content: space-between;
}

/* Authentication pages */
.auth-wrap { max-width: 420px; margin: 0 auto; padding: 26px 16px 40px; }
.auth-brand { display: block; text-align: center; font-weight: 800; font-size: 20px;
  color: var(--text); margin-bottom: 18px; }
.auth-card h1 { font-size: 20px; margin: 0 0 14px; }
.auth-alt { text-align: center; color: var(--muted); font-size: 14px; margin-top: 14px; }
.auth-lang { display: flex; gap: 8px; justify-content: center; margin-top: 18px; }
.hint { color: var(--muted); font-size: 12px; margin: 6px 0 0; }

/* Context-aware shell */
.eyebrow { text-transform: uppercase; letter-spacing: .06em; font-size: 12px;
  color: var(--brand-dark); font-weight: 700; margin: 0 0 6px; }
.btn-lead { font-size: 16px; padding: 13px 22px; }
.app-active { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand) inset; }
.switch-form { margin: 0; }
.switch-form button.row { cursor: pointer; text-align: left; font: inherit; color: inherit; }

/* ── Way out of a mini-app ─────────────────────────────────────────────────
   My Business is a room inside DADCARE, and every room needs a door. This is
   it: one labelled link in the shared My Business header, present on every
   screen, going to the Mother App's Marketplace Home. It is deliberately named
   rather than a bare glyph - an unexplained icon is not an exit. It reads no
   permission and changes no state, so it is offered to everybody the shell is
   already showing. */
.mother-return {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  min-height: 36px; padding: 6px 10px; border-radius: 999px;
  background: var(--surface-2); color: var(--text);
  font-size: 13px; font-weight: 700; white-space: nowrap;
}
.mother-return .ic { width: 18px; height: 18px; }
@media (hover: hover) { .mother-return:hover { background: var(--brand-soft);
  color: var(--brand-dark); } }
/* With the door in the header, the heading is what gives up width first, and
   on a narrow phone the row wraps rather than pushing the page sideways - the
   same answer the Marketplace header already gives to the same problem. */
.topbar { flex-wrap: wrap; row-gap: 8px; }
.topbar strong { min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }

/* ── Shell header ──────────────────────────────────────────────────────────
   A shop's header, not a dashboard's: the DADCARE name, a little breathing
   room, and whatever one action the page needs. It is deliberately short —
   on a phone the header is height taken away from the products. */
.shell .mk-top { display: flex; align-items: center; gap: 10px;
  padding: 10px 16px; min-height: 52px; }
.shell .mk-top .mk-brand { font-size: 17px; font-weight: 800;
  letter-spacing: .01em; color: var(--text); }
.shell .mk-top .spacer { flex: 1; }

/* ── Global navigation: Home | Apps | Active | Inbox | Me ─────────────────── */
body.has-nav { padding-bottom: 78px; }

.globalnav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; justify-content: space-around;
  background: var(--surface); border-top: 1px solid var(--border);
  padding: 6px 4px calc(6px + env(safe-area-inset-bottom));
}
.globalnav a {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 4px; min-height: var(--tap);
  color: var(--muted); font-size: 11px; border-radius: 12px; padding: 7px 2px;
  /* Weight is reserved on both states so the active tab cannot shift layout. */
  font-weight: 600; text-decoration: none;
}
.globalnav a .lb { line-height: 1; letter-spacing: .01em; }

/* Chat attention on the Inbox tab: A DOT, NEVER A NUMBER, and ACCOUNT-LEVEL
   ONLY. It answers for Marketplace and Personal Direct — the conversations a
   person has wherever they are — and never for Team Chat, which belongs to one
   business and is reported inside My Business. A dot lit by a message in a
   workspace that is not open would send the reader to a tab the message is not
   in.

   A dot rather than a figure because Marketplace and Personal are different
   contexts too: a number summing a customer's question and a friend's message
   answers no question anybody asked. The dot says only that there is unread
   Chat, and the link's own accessible name says the same thing in words —
   nothing here is carried by colour alone.

   Absolutely positioned inside the mark, so the tab is exactly the same size
   and the label sits in exactly the same place whether the dot is there or
   not. The ring is the navigation's own surface, so the dot reads as separate
   from the icon in both themes and in both orientations. */
.globalnav a .navmark { position: relative; display: inline-flex; }
.globalnav a .navdot {
  position: absolute; top: -2px; right: -3px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--brand); border: 2px solid var(--surface);
  box-sizing: content-box;
}
.globalnav a.active { color: var(--brand-dark); background: var(--brand-soft); }

/* Same information architecture rendered as a side rail on wider screens.
   Identical destinations and meanings — no separate desktop product logic. */
@media (min-width: 900px) {
  body.has-nav { padding-bottom: 0; padding-left: 210px; }
  .globalnav {
    top: 0; right: auto; width: 210px; bottom: 0;
    flex-direction: column; justify-content: flex-start;
    border-top: 0; border-right: 1px solid var(--border);
    padding: 74px 12px 12px; gap: 4px;
  }
  .globalnav a { flex: none; flex-direction: row; justify-content: flex-start;
    gap: 12px; font-size: 15px; padding: 11px 12px; }
  .mk-top, .shell .mk-top { padding-left: 226px; }
}

.continue-card { border-color: var(--brand); }
.app-group { margin-top: 6px; }
.me-list .row { text-decoration: none; }


/* ── DADCARE icon system ──────────────────────────────────────────────────
   One inline SVG sprite drawn on currentColor. No emoji: emoji are another
   vendor's artwork and differ on every device. No image files and no icon
   library, so navigation costs no extra request. */
.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.ic { width: 24px; height: 24px; display: block; flex: none; }

.app-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; border-radius: 14px; flex: none;
  background: var(--a-app-soft); color: var(--a-app);
}
/* Mini App identities. Visual only — a colour never means "you may open this";
   availability comes from the registry and shows as the card's state badge. */
.a-business    { background: var(--a-business-soft);    color: var(--a-business); }
.a-marketplace { background: var(--a-marketplace-soft); color: var(--a-marketplace); }
.a-school      { background: var(--a-school-soft);      color: var(--a-school); }
.a-pharmacy    { background: var(--a-pharmacy-soft);    color: var(--a-pharmacy); }
.a-gym         { background: var(--a-gym-soft);         color: var(--a-gym); }
.app-ico .ic { width: 26px; height: 26px; }
.app-ico.lead { width: 44px; height: 44px; margin-right: 12px; }
.app-ico.sm { width: 38px; height: 38px; border-radius: 11px; margin-right: 10px; }
.app-ico.sm .ic { width: 22px; height: 22px; }
.mk-brand { display: inline-flex; align-items: center; gap: 8px; }
.topbar .app-ico.sm { margin-right: 0; }

/* The dashboard's identity banner: the cover the owner uploaded, with the
   logo and the business name sitting over it.

   The cover is the only asset shaped for this — a wide storefront header —
   so a business with only a logo gets a plain tinted band rather than its
   logo stretched into a cover it was never cropped to be. With neither
   uploaded the block is not rendered at all, so nothing reserves space for
   a picture that does not exist. */
.bizbanner {
  position: relative; overflow: hidden;
  border-radius: var(--radius); border: 1px solid var(--border);
  background: var(--surface-2); margin-bottom: 12px;
  min-height: 96px; display: flex; align-items: flex-end;
}
.bizbanner-cover {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
}
.bizbanner.nocover { min-height: 0; }
.bizbanner-id {
  position: relative; display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 10px 12px;
}
/* Legible over any photograph, in either theme, without tinting the picture
   itself: the scrim sits behind the words only. */
.bizbanner:not(.nocover) .bizbanner-id {
  background: var(--overlay); color: var(--on-overlay);
}
.bizbanner-name { font-weight: 700; }
.bizbanner-logo { border-radius: 10px; flex: none; }

/* A business's own uploaded logo, standing in for the generic app mark.
   It occupies exactly the box the icon it replaces occupies, so a business
   with a logo and one without lay the header out identically and nothing
   moves once the image arrives. `cover` crops a photo that is not square
   rather than squashing it; the element is decorative, so it carries an
   empty alt and the business is already named beside it. */
.bizlogo { object-fit: cover; background: var(--surface-2); }



/* ── Preference chips (Appearance, Language) ───────────────────────────── */
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
  border-radius: 999px; padding: 8px 16px; font-size: 14px; font-weight: 600;
  cursor: pointer; min-height: 40px;
}
.chip.on {
  background: var(--brand-soft); border-color: var(--brand); color: var(--brand-dark);
}

/* ── Language settings ─────────────────────────────────────────────────────
   A language is chosen by identity, not by country: the flag is a visual mark
   for the language itself, and the code that is stored is a language code. */
.langs { display: grid; gap: 8px; }
.lang-card {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 12px 14px; min-height: var(--tap); cursor: pointer;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  font: inherit;
}
.lang-card .grow { display: flex; flex-direction: column; gap: 1px; }
.lang-flag {
  width: 30px; height: 20px; flex: none; border-radius: 3px;
  border: 1px solid var(--border);
}
.lang-name { font-weight: 600; }
.lang-sub { color: var(--muted); font-size: 13px; }
.lang-tick { flex: none; color: var(--brand); opacity: 0; }
.lang-tick .ic { width: 20px; height: 20px; }
.lang-card.on { border-color: var(--brand); background: var(--brand-soft); }
.lang-card.on .lang-tick { opacity: 1; }
@media (hover: hover) { .lang-card:hover { border-color: var(--brand); } }

/* ── Marketplace Home: a public storefront ─────────────────────────────────
   A shop window, laid out the way a phone shopper reads one: search, then the
   categories that actually exist, then products in a grid where the picture
   is the biggest thing on the card.

   Everything is a normal block in normal document flow. Ordinary scrolling
   reaches every product, nothing depends on a gesture, and each card is a
   link and a button rather than a swipe target. */

/* Search. One field, because there is one thing to search. */
.mksearch { position: relative; margin: 4px 0 12px; }
.mksearch .ic {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  width: 20px; height: 20px; color: var(--muted); pointer-events: none;
}
.mksearch input {
  width: 100%; min-height: var(--tap); padding: 11px 14px 11px 40px;
  background: var(--surface); color: var(--text); font: inherit;
  border: 1px solid var(--border); border-radius: 999px;
}
.mksearch input:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* Category chips. Horizontally scrollable on a phone, which is the only way a
   row of real category names fits; the bar itself is hidden rather than
   invented when the catalogue has no categories in it yet. */
.chiprow {
  display: flex; gap: 8px; overflow-x: auto; margin: 0 -16px 14px;
  padding: 2px 16px 6px; scrollbar-width: none;
  -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity;
}
.chiprow::-webkit-scrollbar { display: none; }
.chiprow .chip { flex: none; white-space: nowrap; scroll-snap-align: start; }

/* Section heading. One line, sentence case, no management vocabulary. */
.mksection {
  display: flex; align-items: baseline; gap: 8px; margin: 0 0 10px;
  font-size: 17px; font-weight: 700; color: var(--text);
}
.mksection .count { font-size: 13px; font-weight: 600; color: var(--muted); }

/* The product grid, sized by the room it actually has.

   Column counts were fixed to viewport widths, and the viewport is not what
   this grid is laid out in: from 900px `body.has-nav` gives 210px of the
   screen to the navigation rail, so a 1040px window left the grid 782px and
   still asked it for four columns — cards came out narrower at 1040px than
   they had been at 1000px. Widening a window made the products smaller.

   `auto-fill` with a minimum removes the arithmetic instead of correcting it.
   The browser fits as many columns of at least `--pcard-min` as the grid's own
   content box holds, so the rail, the page gutters and the 1160px reading
   width are all already accounted for — there is nothing left to keep in step.
   A column is added only where there is room for one at the minimum, which is
   what "not narrower than it was" means as a rule rather than as a table.

   The minimum is a legibility budget, not a guess: a card carries a square
   photograph, two lines of title, a price that must not be cut, and a
   full-width action. It rises twice, because a card read at arm's length on a
   phone and a card read on a desktop are not the same card.

   `min(..., 100%)` is the narrow-screen guard: without it a container narrower
   than the minimum overflows sideways instead of dropping to one column. */
.pgrid {
  --pcard-min: 152px;
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(min(var(--pcard-min), 100%), 1fr));
}
@media (min-width: 700px) { .pgrid { --pcard-min: 208px; gap: 16px; } }
@media (min-width: 1040px) { .pgrid { --pcard-min: 232px; } }

.pcard {
  display: flex; flex-direction: column; min-width: 0; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 16px;
}
/* Media, then words. The picture is the card. */
.pcard-open { display: block; color: inherit; min-width: 0; }
.pcard-media {
  position: relative; display: block; width: 100%; aspect-ratio: 1 / 1;
  background: var(--surface-2);
}
.pcard-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Home autoplay: the one video the grid may be playing, over its own card's
   poster rather than instead of it. The picture stays underneath, so a browser
   that refuses to start the video leaves the shopper looking at the product
   instead of at a black square. Nothing is drawn for a card that is not the
   active one, because no element is created for one. */
.pcard-video-el {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
  background: var(--surface-2);
}
.noimage {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; padding: 12px; text-align: center;
  color: var(--muted); font-size: 13px; line-height: 1.3;
}
/* Sits on the picture, so the price stays the loudest thing in the words. */
.pcard-kind {
  position: absolute; left: 8px; top: 8px; padding: 3px 9px; border-radius: 999px;
  background: var(--surface); color: var(--brand-dark); border: 1px solid var(--border);
  font-size: 11px; font-weight: 700; letter-spacing: .02em;
}
.pcard-video {
  position: absolute; right: 8px; top: 8px; display: flex; align-items: center;
  gap: 4px; padding: 3px 8px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--text); font-size: 11px; font-weight: 700;
}
.pcard-video .ic { width: 13px; height: 13px; }

/* Four slots, reserved whether or not this listing fills them.

   Every fact under the photograph is optional except the name and the price:
   a seller may have left no display name and a listing may carry no city. With
   the rows placed by content, a card missing one of them pulled its price and
   its action up by a row, and two cards side by side in the same grid row read
   as two different components. The tracks are declared here and each element
   is assigned to one, so a missing fact leaves an empty track rather than
   closing the gap. Nothing is invented to fill it — the space is simply held.

   Anything added to this body later needs a `grid-row` of its own; without one
   it auto-places into row 1 and lands on the name. */
.pcard-body {
  --pcard-row-seller: 20px;   /* the identity mark's own height */
  --pcard-row-where: 17px;    /* one 12px line, with its pin */
  padding: 10px 12px 12px; min-width: 0;
  display: grid;
  grid-template-rows: auto var(--pcard-row-seller) auto var(--pcard-row-where);
  row-gap: 3px;
}
.pcard-name { grid-row: 1; }
.pcard-seller { grid-row: 2; }
.pcard-price { grid-row: 3; }
.pcard-where { grid-row: 4; }
/* Two lines of room, always. The clamp is a ceiling; this is the floor, so a
   one-word title reserves what a wrapped one occupies and the price below it
   sits at the same height on every card. In `em`, so the narrow-screen type
   step carries it without a second number to keep in step. */
.pcard-name {
  margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; color: var(--text);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; min-height: 2.6em;
}
/* One line, never two. A long amount with a currency and a unit used to wrap
   and push everything below it down; it now ends in an ellipsis, which loses
   the tail of a figure rather than the alignment of a row. */
.pcard-price {
  margin: 0; font-size: 16px; font-weight: 800; color: var(--text);
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pcard-price .cur { font-size: 12px; font-weight: 700; color: var(--muted);
  margin-right: 3px; }
.pcard-where {
  margin: 0; font-size: 12px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Whose product this is. Compact by design — the product is the subject of the
   tile and the seller is the attribution under it. The mark is an initial from
   the public display name, because there is no Marketplace avatar column and
   inventing one would be a schema change. */
.pcard-seller {
  margin: 0; display: flex; align-items: center; gap: 6px; min-width: 0;
}
.pcard-sellermark {
  display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 999px; flex: 0 0 auto;
  background: var(--brand-soft); color: var(--brand-dark);
  font-size: 11px; font-weight: 800; line-height: 1;
}
.pcard-sellername {
  font-size: 12px; font-weight: 600; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
/* The one buyer action DADCARE actually has, at the foot of the card.

   `margin-top: auto` is what puts it there. The grid stretches every card in a
   row to the same height, but the height it hands back used to pool underneath
   the button, so a card with a shorter body wore its action higher than its
   neighbour's. The slack now sits above the action instead, and every card in
   a row ends at the same line. */
.pcard-wa {
  margin: 10px 12px 12px; margin-top: auto;
  min-height: 38px; font-size: 13px; padding: 8px 10px;
  background: var(--surface); color: var(--brand-dark); border-color: var(--brand);
  font-weight: 700;
}
.pcard-wa:active { background: var(--brand-soft); }
/* One line at every card width, so the action's height is the same fact on
   every card. The label shortens; the button never grows a second line. */
.pcard-wa-t {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A seller who left no WhatsApp number gets no WhatsApp button — inventing one
   would offer a route that does not exist. What the card keeps is the room the
   button would have taken, so the card beside it does not wear its action at a
   different height. Empty, inert and hidden from a screen reader: it is a
   measurement, not a control. */
.pcard-wa-slot {
  margin: 10px 12px 12px; margin-top: auto;
  min-height: 38px; display: block; pointer-events: none;
}
@media (hover: hover) {
  .pcard:hover { border-color: var(--brand); }
}

/* One listing, at its own public address. A card crops to a square so a grid
   reads as a grid; this page does not, because here the photograph is the
   product and cropping it would hide part of what is being sold. */
.listing { display: grid; gap: 6px; max-width: 640px; }
.listing h1 { font-size: 22px; line-height: 1.25; margin: 8px 0 0; }
.listingmedia {
  width: 100%; border-radius: 16px; display: block; background: var(--surface-2);
}
/* The video is the same shape as the photograph it replaces: this page shows
   one product, and the media is the product. Controls stay native so playback
   works without JavaScript and with a keyboard. */
.listingvideo { background: var(--surface-2); }

/* The rest of the seller's photographs, in the order the seller put them in.
   A horizontal rail rather than a second full-width column: the first image is
   already shown above, and these are the ones a buyer swipes through. */
.pgallery {
  display: flex; gap: 8px; overflow-x: auto; margin: 8px 0 0; padding: 0;
  list-style: none; scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
}
.pgallery-item { flex: 0 0 auto; scroll-snap-align: start; }
.pgallery-item img {
  width: 128px; height: 128px; object-fit: cover; display: block;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface-2);
}
@media (min-width: 700px) { .pgallery-item img { width: 152px; height: 152px; } }
/* Each photograph is a control, so it is a button: pressable with a finger and
   with a keyboard, and announced as one. The button carries no look of its own
   — the photograph inside it is the whole affordance. */
.pgallery-pick {
  display: block; padding: 0; border: 0; background: none;
  border-radius: 12px; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.pgallery-pick:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Which photograph is on the sheet above, stated once. */
.pgallery-pick[aria-current="true"] img {
  border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand);
}
/* A photograph the browser could not load. Dimmed and inert rather than
   removed: the row keeps its shape and nothing shifts under a finger. */
.pgallery-pick.is-unavailable { cursor: default; }
.pgallery-pick.is-unavailable img { opacity: .4; }

/* Who is selling it. The initial is a fallback identity mark, not an avatar:
   there is no Marketplace avatar column, and inventing one would be a schema
   change nobody approved. */
.pseller { display: flex; align-items: center; gap: 8px; margin: 4px 0 0; }
.pseller-mark {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 999px; flex: 0 0 auto;
  background: var(--brand-soft); color: var(--brand-dark);
  font-size: 13px; font-weight: 800;
}
.pseller-name {
  font-size: 14px; font-weight: 600; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.postname { font-size: 17px; font-weight: 700; margin: 0; }
.postprice { font-size: 24px; font-weight: 800; color: var(--text); margin: 2px 0; }
.postprice .cur { font-size: 14px; font-weight: 700; color: var(--muted); }
.postdesc { color: var(--text); margin: 0; overflow-wrap: anywhere; }
.postwhere { color: var(--muted); font-size: 14px; margin: 0; }
.tag {
  justify-self: start; padding: 3px 10px; border-radius: 999px;
  background: var(--brand-soft); color: var(--brand-dark);
  font-size: 13px; font-weight: 600;
}

/* The central post action. Prominent, and still a labelled link. */
.globalnav a.post .postmark {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 999px;
  background: var(--brand); color: var(--on-brand);
  font-size: 22px; line-height: 1; font-weight: 600;
}

/* What the seller chose, shown back to them before they publish. */
.preview { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.previewitem { margin: 0; width: 84px; }
.previewitem img {
  width: 84px; height: 84px; object-fit: cover;
  border-radius: var(--radius); border: 1px solid var(--border);
}
.previewitem figcaption {
  font-size: 11px; color: var(--muted); overflow-wrap: anywhere;
}

/* The cover photograph is the one a shopper sees first, so it is the one
   marked here. Exactly one tile carries this at a time — the server keeps that
   invariant and the screen only draws it. */
.previewitem.cover img { border-color: var(--brand); border-width: 2px; }
.previewitem .noimage {
  display: flex; align-items: center; justify-content: center;
  width: 84px; height: 84px; text-align: center; padding: 4px;
  border-radius: var(--radius); border: 1px solid var(--border);
  background: var(--surface-2); color: var(--muted); font-size: 11px;
}
.previewitem figcaption { display: flex; flex-direction: column; gap: 2px; }
/* A file still on its way up. Dimmed and control-less, because there is
   nothing on the server yet to make a cover of or to remove. */
.previewitem.pending img, .previewitem.pending .noimage { opacity: .45; }
.coverbadge {
  display: inline-block; font-weight: 700; color: var(--brand);
  text-transform: uppercase; letter-spacing: .04em; font-size: 10px;
}
/* A control that reads as text: these sit under an 84px thumbnail, where a
   filled button would outweigh the picture it acts on. */
.linkbtn {
  background: none; border: 0; padding: 0; font: inherit; font-size: 11px;
  color: var(--brand); text-align: left; cursor: pointer; text-decoration: underline;
}
.linkbtn.danger { color: var(--danger); }
.linkbtn:disabled { color: var(--muted); cursor: default; text-decoration: none; }

/* Edit Product's own photo gallery. Tiles reuse .preview/.previewitem/
   .coverbadge above unchanged - same grid, same cover marker - so this is
   one gallery presentation, not a second one. Only the Remove control
   differs from the composer's text .linkbtn: Edit Product acts on photos
   already live to shoppers, so its Remove is a full var(--tap) touch
   target rather than a small link under the thumbnail. */
.sell-photo-remove {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: var(--tap); margin-top: 4px; padding: 0;
  border-radius: var(--radius); border: 1px solid var(--border);
  background: none; color: var(--danger); cursor: pointer;
}
.sell-photo-remove .ic { width: 16px; height: 16px; }
.sell-photo-remove:disabled { opacity: .5; cursor: default; }
.sell-photo-add-controls { display: flex; gap: 8px; }
.sell-photo-add {
  display: flex; align-items: center; justify-content: center;
  min-width: var(--tap); min-height: var(--tap); width: var(--tap); height: var(--tap);
  border-radius: var(--radius); border: 1px dashed var(--border);
  background: none; color: var(--brand); cursor: pointer; padding: 0;
}
.sell-photo-add .ic { width: 20px; height: 20px; }
.sell-photo-add:disabled { opacity: .5; cursor: default; }
.sell-photo-progress { font-size: 12px; color: var(--muted); margin-top: 6px; min-height: 15px; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── Marketplace immersive viewer ──────────────────────────────────────────
   Home is a grid; this opens over it. One product to a screen, scrolled
   vertically, with the media as the ground and the product information over
   it. Dark ground in both themes because the media is the subject — the same
   reasoning as the camera stage, and the same shared overlay token, so no
   colour is invented here either. */
.mkviewer {
  position: fixed; inset: 0; z-index: 60;
  background: var(--overlay); color: var(--on-overlay);
}
body.mkviewer-open { position: fixed; width: 100%; overflow: hidden; }

.mkviewer-track {
  height: 100vh; height: 100dvh;
  overflow-y: auto; overflow-x: hidden;
  scroll-snap-type: y mandatory; -webkit-overflow-scrolling: touch;
}
/* Snapping is an enhancement over ordinary scrolling, never a replacement:
   the track scrolls normally with a wheel, a keyboard or a scrollbar. */
.mkslide {
  height: 100vh; height: 100dvh;
  scroll-snap-align: start; scroll-snap-stop: always;
  position: relative; display: flex; flex-direction: column;
  justify-content: flex-end;
}
.mkslide-stage { position: absolute; inset: 0; overflow: hidden; }
.mkslide-poster, .mkslide-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: contain;
  background: var(--overlay);
}
/* The video is the pause control: a tap on it toggles playback, so it reads
   as pressable and takes a visible focus ring for the keyboard path that
   replaced the native control bar. */
.mkslide-video { z-index: 1; cursor: pointer; }
.mkslide-video:focus-visible { outline: 3px solid var(--brand); outline-offset: -3px; }

/* Shown only when the browser refused to start playback on its own. */
.mkslide-play {
  position: absolute; z-index: 2; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center;
  width: 68px; height: 68px; border-radius: 999px;
  border: 1px solid var(--on-overlay);
  background: var(--overlay); color: var(--on-overlay);
  cursor: pointer;
}
.mkslide-play .ic { width: 34px; height: 34px; }

.mkslide-info {
  position: relative; z-index: 3;
  display: grid; gap: 4px;
  padding: 16px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to top, var(--overlay), transparent);
  max-height: 62%; overflow-y: auto;
}
.mkslide-title {
  margin: 0; font-size: 19px; font-weight: 700; color: var(--on-overlay);
}
.mkslide-price {
  margin: 0; font-size: 22px; font-weight: 800; color: var(--on-overlay);
}
.mkslide-price .cur { font-size: 13px; font-weight: 700; opacity: .82; }
.mkslide-desc, .mkslide-where {
  margin: 0; font-size: 14px; color: var(--on-overlay); opacity: .86;
  overflow-wrap: anywhere;
}
.mkslide-kind {
  justify-self: start; margin: 2px 0; padding: 3px 10px; border-radius: 999px;
  background: var(--brand-soft); color: var(--brand-dark);
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em;
}
.mkslide-seller { margin: 0 0 2px; display: flex; align-items: center; gap: 8px; }
.mkslide-seller .pcard-sellername { color: var(--on-overlay); font-size: 13px; }
.mkslide-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.mkslide-actions .btn { min-height: 42px; flex: 1 1 auto; }

/* The seller's other photographs, in the seller's order. */
.mkslide-gallery {
  display: flex; gap: 8px; overflow-x: auto; margin-top: 10px;
  scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
}
.mkslide-thumb {
  padding: 0; margin: 0; flex: 0 0 auto; scroll-snap-align: start;
  border-radius: 10px; border: 2px solid transparent;
  background: var(--surface-2); line-height: 0; cursor: pointer;
}
.mkslide-thumb img {
  display: block; width: 70px; height: 70px; object-fit: cover;
  border-radius: 8px;
}
.mkslide-thumb[aria-current="true"] { border-color: var(--brand); }
.mkslide-thumb:focus-visible {
  outline: 3px solid var(--brand); outline-offset: 2px;
}

/* The horizontal axis, and why the stage claims only one of the two.
   `pan-y` leaves vertical panning to the browser, so the track scrolls and
   snaps between products exactly as it always has, while a gesture the
   viewer decides is horizontal is handled in script and prevented from
   also dragging the track. Suppressing touch handling outright is
   deliberately never done here: it would take ordinary scrolling away from
   this surface, and navigation must not become gesture-only. */
.mkslide-stage { touch-action: pan-y; }
.mkslide-poster[hidden], .mkslide-video[hidden] { display: none; }

/* One step along the current listing's own media. Present for a keyboard and
   a mouse, disabled at each end of the rail because the rail stops there -
   it never runs on into another product. */
.mkslide-step {
  position: absolute; z-index: 2; top: 50%;
  transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  /* 44x44 is the smallest target a finger hits reliably, and these two are
     pressed mid-scroll on a moving bus. */
  width: 44px; height: 44px; border-radius: 999px;
  border: 1px solid var(--on-overlay);
  background: var(--overlay); color: var(--on-overlay);
  cursor: pointer; opacity: .78;
}
.mkslide-step.prev { left: 8px; }
.mkslide-step.next { right: 8px; }
.mkslide-step.prev .ic { transform: rotate(180deg); }
.mkslide-step[disabled] { opacity: .3; cursor: default; }
.mkslide-step:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }

/* Where along this listing's media the reader is - never a count of anything
   else, and never a figure about another listing. */
.mkslide-at {
  position: absolute; z-index: 2; margin: 0;
  top: calc(14px + env(safe-area-inset-top, 0px)); left: 50%;
  transform: translateX(-50%);
  padding: 3px 10px; border-radius: 999px;
  background: var(--overlay); color: var(--on-overlay);
  font-size: 12px; font-weight: 650; opacity: .82;
}

/* The action rail. MKT-V1 draws Share and WhatsApp; the Owner's thumbs-up
   Like and Comments join the same column in MKT-V2, which is why it is a
   column rather than two loose buttons.
   z-index is 4, one above .mkslide-info's 3: the two are equal-height
   siblings and .mkslide-info is the later one in the DOM, so an equal
   z-index would let its own box - the reserved lane included, not only its
   visible text - paint over the rail and swallow every tap meant for Share
   and WhatsApp. A higher z-index here settles the stack unambiguously
   rather than depending on DOM order. */
.mkslide-rail {
  position: absolute; z-index: 4; right: 10px;
  bottom: calc(96px + env(safe-area-inset-bottom, 0px));
  /* Five controls now, so the column is tightened rather than shortened:
     the Owner's instruction is compact spacing, never hiding an approved
     action. 10px still leaves every 46px disc its own clear target. */
  display: flex; flex-direction: column; gap: 10px;
}
/* The information panel keeps a lane clear on its right so the rail never
   sits over the title, the price or the seller's own words. */
.mkslide-info { padding-right: 76px; }
/* On a short screen - a landscape phone, a small window, a device with the
   keyboard raised - the panel is capped and the rail moves above it rather
   than being pushed down onto it. */
@media (max-height: 640px) {
  .mkslide-info { max-height: 46%; padding-right: 76px; }
  /* A column of five 46px discs with 10px between them and a count hanging
     under the second is 296px tall. On a 390px-high landscape phone it
     cannot also start at 48% of the height, so on the shortest screens the
     rail is tightened further and anchored nearer the foot. Nothing is
     dropped: every one of the Owner's five is still drawn and still
     pressable. */
  .mkslide-rail {
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    gap: 6px;
  }
  .mkslide-rail .mkrail-act {
    min-width: 44px; min-height: 44px; width: 44px; height: 44px;
  }
  .mkslide-rail .mkrail-act .ic { width: 22px; height: 22px; }
  /* The column's gap narrowed to 6px, so the Like control carries more of
     the room its count needs rather than the count landing on Buy Now. */
  .mkslide-rail .mkrail-like { margin-bottom: 10px; }
  /* Scoped to the product page, where this rule was written. Unscoped, it
     also hid the Home viewer's strip on every 640px-tall phone — which was
     the surface the strip had just been reinstated for. Full mode decides its
     own strip below, and never leaves the axis without a tappable control. */
  .mkviewer:not(.mkviewer-full) .mkslide-gallery { display: none; }
}
.mkrail-act {
  display: flex; align-items: center; justify-content: center;
  /* Never below 44x44, for the same reason as the step controls. */
  min-width: 46px; min-height: 46px;
  width: 46px; height: 46px; border-radius: 999px;
  border: 1px solid var(--on-overlay);
  background: var(--overlay); color: var(--on-overlay);
  cursor: pointer; text-decoration: none;
}
.mkrail-act .ic { width: 24px; height: 24px; }
.mkrail-act:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }

/* ── The Owner's five, and what makes each one itself ─────────────────────
   Every control keeps the 46px disc above, so the column reads as one set
   and every target clears 44x44 whatever it carries. What changes per
   control is its colour and what hangs off it.

   NO COLOUR IS INVENTED HERE. Each rule below names a token that already
   existed: the partner green WhatsApp is the only thing allowed to use, the
   DADCARE brand blue and its on-brand ink, and the danger red the rest of
   DADCARE already draws refusals in. */

/* Like. The heart is white until it is pressed — the ordinary rail ink,
   which is what every other glyph in the column is — and the count sits
   directly beneath the disc rather than inside it, so the glyph keeps its
   full size and the figure never crowds it. */
.mkrail-like { position: relative; }
.mkrail-count {
  position: absolute; left: 50%; top: 100%;
  transform: translateX(-50%);
  margin-top: 2px;
  font-size: 11px; font-weight: 700; line-height: 1;
  color: var(--on-overlay);
  /* A photograph is behind this, so the figure carries its own contrast
     rather than relying on whatever pixels happen to be under it. */
  text-shadow: 0 1px 2px var(--overlay);
  pointer-events: none;
}
/* The count hangs below the disc, so the gap after Like is the one that has
   to make room for it. Given to the control rather than to the column, so
   the other four keep the spacing they had. 8px on top of the column's own
   10px clears the 13px the figure occupies; the compact rule below widens
   it again where the column's gap narrows. */
.mkslide-rail .mkrail-like { margin-bottom: 8px; }

/* LIKED: light DADCARE blue, and deliberately not red.

   Both values are existing tokens rather than a new blue: in the light
   theme `--brand-soft` is the DADCARE light blue, and in dark the same
   family's light end is `--brand-dark`. The rail sits on a photograph under
   a constant scrim in both themes, so the heart has to stay light in both —
   taking `--brand-soft` into dark mode would paint it the dark navy that
   token becomes there, which on this surface is very nearly invisible. */
.mkrail-like.on { color: var(--brand-soft); border-color: var(--brand-soft); }
html[data-theme="dark"] .mkrail-like.on {
  color: var(--brand-dark); border-color: var(--brand-dark);
}

/* Buy Now. WhatsApp's own green disc with their mark reversed out of it —
   the partner treatment the token block already reserves for this one
   action and nothing else in the shell. */
.mkrail-wa {
  background: var(--whatsapp);
  border-color: var(--whatsapp);
  color: var(--whatsapp-ink);
}

/* DADCARE Chat. A DADCARE-blue disc with a WHITE handset on it, in BOTH
   themes, and clearing contrast in both.

   THE INK. `--on-overlay` rather than `--on-brand`. `--on-brand` is the
   palette's pairing for ink on a THEMED surface, so it flips to dark ink in
   the Mother App's dark theme — which turned the Owner's white handset into
   a dark one. `--on-overlay` is the token this rail already uses for
   everything that sits on a photograph: white in both theme blocks, by
   design, for the same reason a photograph is not themed.

   THE DISC. `--brand` is the DADCARE blue, and in the light theme white
   sits on it at 6.6:1. The dark theme lifts `--brand` to a pale blue built
   to carry DARK ink, and a white glyph on that clears only 2.1:1 — under
   WCAG's 3.0 for a graphical object. The requirement was a DADCARE-BLUE
   circle, never that one shade, so the dark theme takes the palette's other
   DADCARE blue: `--brand-soft`, which on `body.shell` in dark is the deep
   navy end of the same family. Still DADCARE blue, still a white handset,
   and white on it clears 13.5:1.

   No new token and no hardcoded colour: `--brand`, `--brand-soft` and
   `--on-overlay` all already existed. The exact values live in the palette,
   which is the one place they belong. */
.mkrail-chat {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-overlay);
}
html[data-theme="dark"] .mkrail-chat {
  background: var(--brand-soft);
  border-color: var(--brand-soft);
}
/* Unread Chat, and only ever a real number.

   Drawn at the upper edge of the disc. `hidden` is what removes it at zero
   and for every guest — the element is not drawn empty, because an empty
   disc over the glyph would still be a claim that something is waiting. */
.mkrail-badge {
  position: absolute; top: -4px; right: -4px;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--danger); color: var(--on-brand);
  font-size: 11px; font-weight: 700; line-height: 1;
  border: 1px solid var(--on-overlay);
  pointer-events: none;
}
.mkrail-chat { position: relative; }
.mkrail-badge[hidden] { display: none; }
/* The seller, as a circle at the top of the rail. The same disc as its
   neighbours so the column reads as one set of controls, carrying an initial
   instead of a glyph because Marketplace stores no seller photograph and a
   stock face would be a picture of somebody else. */
.mkrail-seller {
  font-size: 18px; font-weight: 700; line-height: 1;
  text-transform: uppercase; letter-spacing: 0;
}

/* Share, in the header beside Close.
   Smaller than a rail control and off the media entirely: it is chrome, not
   one of the three things a shopper came to do. Same disc treatment as Close,
   sat immediately to its left so the two read as one pair. */
.mkviewer-share {
  position: absolute; z-index: 4;
  top: calc(10px + env(safe-area-inset-top, 0px)); right: 60px;
  width: 40px; height: 40px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--on-overlay);
  background: var(--overlay); color: var(--on-overlay);
  cursor: pointer;
}
.mkviewer-share .ic { width: 20px; height: 20px; }
.mkviewer-share:focus-visible {
  outline: 3px solid var(--brand); outline-offset: 2px;
}

/* The end of the grid.

   A position rather than a control: the sentinel carries no text, no border and
   no tab stop, and the browser tells the page when it comes into view. It has a
   height only so that it can be observed at all — a zero-height element is
   never "intersecting" on some engines. */
.feed-sentinel { height: 1px; margin: 0; padding: 0; }

/* What IS said out loud: one quiet line under the grid, never a panel over it,
   and nothing at all when there is nothing to say. */
.feed-status { margin: 12px 0 0; text-align: center; font-size: 13px; }
.feed-status:empty { display: none; }
.feed-retry {
  margin: 12px 0 0; display: flex; gap: 10px;
  align-items: center; justify-content: center; flex-wrap: wrap;
  font-size: 14px;
}

/* The public product page has no shell to put a message in, so it carries its
   own small one for "Link copied". */
.mkflash {
  position: fixed; z-index: 70; left: 50%; transform: translateX(-50%);
  top: calc(10px + env(safe-area-inset-top, 0px));
  max-width: min(92vw, 420px);
}
.mkflash:empty { display: none; }

/* The public product page's own way into the immersive viewer.
   Top-left, clear of the native control bar along the bottom of the video and
   clear of anything the browser draws in the top-right, so it never competes
   with playback controls the reader needs. */
.mkexpand {
  position: absolute; z-index: 2;
  top: 10px; left: 10px;
  display: flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; width: 44px; height: 44px;
  border-radius: 999px;
  border: 1px solid var(--on-overlay);
  background: var(--overlay); color: var(--on-overlay);
  cursor: pointer;
}
.mkexpand .ic { width: 22px; height: 22px; }
.mkexpand:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.listingstage { position: relative; display: block; }

.mkviewer-close {
  position: absolute; z-index: 4;
  top: calc(10px + env(safe-area-inset-top, 0px)); right: 12px;
  width: 40px; height: 40px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--on-overlay);
  background: var(--overlay); color: var(--on-overlay);
  font-size: 24px; line-height: 1; cursor: pointer;
}
.mkviewer-close:focus-visible, .mkslide-play:focus-visible {
  outline: 3px solid var(--brand); outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .mkviewer-track { scroll-behavior: auto; }
}

/* ── The Home/tap viewer ─────────────────────────────────────────────
   A customer who taps a product photograph gets the product, and nothing else
   on the screen at all. This is a mode of the viewer above, not a second one,
   so everything here is additive and reaches only `.mkviewer-full`; the public
   product page keeps every rule above exactly as it shipped.

   Two things make it a different surface.

   The ground is opaque, because the grid showing faintly through a product is
   the customer looking at two screens at once.

   And it takes the entire viewport. An earlier version of this mode reserved a
   lane for the DADCARE header and another for the bottom navigation, then
   shrank itself into what remained — on a tablet a 210px lane down the side as
   well. The reasoning had been that a viewer which does not overlap the shell
   never has to argue with it about a z-index. What it produced was the product
   sitting in a frame: a white strip above it, a navigation bar below it, and
   the Mother App visibly wrapped around the thing the customer asked to look
   at. The Owner rejected that, and rightly — the promise of this surface is
   the product filling the screen, not a picture mounted inside the app.

   Covering the shell needs nothing new. `.mkviewer` is already
   `position: fixed; inset: 0; z-index: 60`; `.mk-top` is `sticky` at
   z-index 20 and `.globalnav` is `fixed` at z-index 40, both children of the
   same root stacking context. The viewer has always been above them. It was
   simply handing the space back. It no longer does. */
.mkviewer-full {
  /* Every edge of the viewport, restated rather than merely inherited: this
     line is the Owner's contract, and a future edit that reintroduces a lane
     has to delete a rule that says in words why there is not one. Nothing is
     subtracted here for the header, the bottom navigation or the desktop side
     rail — the viewer paints over all three. */
  inset: 0;
  /* Opaque in both themes. `--overlay-solid` is the palette's own opaque
     counterpart to `--overlay`; no colour is invented here either. */
  background: var(--overlay-solid);
}
.mkviewer-full .mkviewer-track,
.mkviewer-full .mkslide {
  /* The whole viewport height, and only genuine browser behaviour may change
     it. `dvh` keeps that true while Android's browser chrome shows and hides;
     the Mother App's own header and navigation are covered, never subtracted.
     `vh` first, for a browser that does not know `dvh`. */
  height: 100vh;
  height: 100dvh;
}
.mkviewer-full .mkslide-poster,
.mkviewer-full .mkslide-video { object-fit: contain;
  background: var(--overlay-solid); }

/* One canvas, and layers over it.

   This replaces the two-band layout. That layout obeyed a rule — information
   must not sit over the image — by giving the information its own band, and
   the cost was the thing the bands existed to show: the panel was a flex item
   sized by its own content, the stage took what was left, and so the seller's
   words decided how big the product was. Measured at 360x640 the same
   photograph painted 226x302 under a short description and 170x226 under a
   long one, and opening "Read more" took it to 38x51 — five per cent of the
   area it had a moment earlier.

   So the rule is met the other way round. The stage is the whole viewer
   rectangle, `position: absolute; inset: 0`, exactly as the product page has
   always had it. Everything else — the caption, the sheet, the rail, the
   media controls — is absolutely positioned over it, out of flow, and
   therefore structurally incapable of entering any calculation the stage
   takes part in. The media rectangle stops being a consequence of the content
   and becomes a consequence of the device, which is the whole of the Owner's
   law and is now a property of the layout rather than a promise about it.

   `object-fit: contain` is unchanged and restated above: the photograph is
   fitted whole into a much larger box, never trimmed to fill one. */
.mkviewer-full .mkslide-stage { position: absolute; inset: 0; }
/* Out of flow, anchored to the foot of the canvas, and measured from the
   bottom up — so its height is its own business and can never be read by the
   stage. Capped and clipped rather than scrollable: a second scrolling
   surface here is what traps the vertical swipe between products. */
.mkviewer-full .mkslide-info {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  max-height: 50%; overflow: hidden;
  /* The rail overlays the same canvas, so the panel keeps a lane clear of it
     rather than running its text underneath. */
  padding-right: 76px;
  background: linear-gradient(to top, var(--overlay-solid), transparent);
}
/* Bottom-right of the media band, which is the rail's own parent in this mode. */
.mkviewer-full .mkslide-rail {
  bottom: 12px;
  right: 10px;
}

/* The caption: two rows, and no way to become three.

   Each row is a single line box whose children are `nowrap` with an ellipsis,
   so neither can wrap however long a seller's name or sentence is. The rows
   are the only children the caption has — the price, the location, the sale
   type and the product-page link are in the sheet, so there is no conditional
   row that could appear between them. */
.mkviewer-full .mkslide-info {
  display: flex; flex-direction: column; gap: 2px;
  /* The right lane is the action rail's. Without it a long description pushes
     "More" under Share on a 320px screen, where it is still a control and
     still looks pressable but belongs to something else. */
  padding: 12px 76px calc(12px + env(safe-area-inset-bottom, 0px)) 16px;
}
.mkviewer-full .mkcap-who,
.mkviewer-full .mkcap-line {
  margin: 0; min-width: 0;
  display: flex; align-items: baseline; gap: 6px;
  white-space: nowrap;
  color: var(--on-overlay);
}
.mkviewer-full .mkcap-who { font-size: 15px; font-weight: 700; }
.mkviewer-full .mkcap-line { font-size: 14px; }
/* Whichever part is long is the part that gives way. The seller's name is
   capped so a business with a very long one cannot push the product's own
   name out of the line entirely. */
.mkviewer-full .mkcap-seller {
  flex: 0 1 auto; min-width: 0; max-width: 45%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mkviewer-full .mkcap-dot { flex: 0 0 auto; opacity: .7; }
.mkviewer-full .mkcap-title,
.mkviewer-full .mkslide-desc {
  flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mkviewer-full .mkslide-desc { opacity: .92; }

/* "More", inline on the line it belongs to.

   `flex: 0 0 auto` so it is never the thing that shrinks, and a tap target of
   44px built from padding that an equal negative margin takes straight back
   out of the layout — so the control is thumb-sized while costing the caption
   no height at all. A block-level control here would be the third line this
   caption may not have. */
.mkviewer-full .mkslide-desc-toggle {
  flex: 0 0 auto; margin: -14px -8px; padding: 14px 8px;
  min-height: 44px; box-sizing: border-box;
  display: inline-flex; align-items: center;
  background: none; border: 0; cursor: pointer;
  color: var(--on-overlay); font-size: 14px; font-weight: 700;
  text-decoration: underline; text-underline-offset: 3px;
  white-space: nowrap;
}
.mkslide-desc-toggle:focus-visible {
  outline: 3px solid var(--brand); outline-offset: 2px;
}

/* The sheet: the product record, over the canvas rather than beside it.

   Absolutely positioned and out of flow, so neither its presence nor its
   height can reach the media behind it — opening it moves nothing. It scrolls
   inside itself and contains that scroll, which is what stops a drag meant for
   the sheet from becoming a swipe to the next product. */
.mkviewer-full .mkslide-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 6;
  max-height: 72%;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain; touch-action: pan-y;
  padding: 18px 16px calc(18px + env(safe-area-inset-bottom, 0px));
  background: var(--overlay-solid); color: var(--on-overlay);
  border-top-left-radius: 16px; border-top-right-radius: 16px;
  display: grid; gap: 8px;
}
.mkviewer-full .mkslide-sheet[hidden] { display: none; }
.mksheet-title { margin: 0; font-size: 18px; font-weight: 700; }
.mksheet-price { margin: 0; font-size: 22px; font-weight: 800; }
.mksheet-price .cur { font-size: 13px; font-weight: 700; opacity: .82; }
.mksheet-facts { margin: 0; font-size: 13px; opacity: .86; }
.mksheet-desc {
  margin: 0; font-size: 14px; line-height: 1.5; overflow-wrap: anywhere;
}
.mksheet-open { justify-self: start; min-height: 44px; }

/* Media navigation, over the canvas rather than beside it.

   DOTS, NOT ARROWS — the Owner's rule, and this mode now keeps it whole.

   An earlier reading of it kept the two circular step arrows on the mid-edges
   and argued that they were small. They are still arrows, and the Owner
   rejected them on sight. In this mode they are not built at all (see the
   `FULL` guard in the viewer's slide builder); this rule is the second lock,
   so a step control that reached the canvas by some other route still cannot
   be seen. `/p/<slug>/` is untouched and keeps both controls. */
.mkviewer-full .mkslide-step { display: none; }

/* The seller's photographs, just above the caption. The offset clears the
   two-line caption's own height; where the caption is one line the gap is a
   little larger, which is a gap rather than a collision. */
.mkviewer-full .mkslide-gallery {
  position: absolute; z-index: 4;
  bottom: calc(78px + env(safe-area-inset-bottom, 0px));
  /* Inset rather than padded. Padding would leave the element itself spanning
     the full width, and a full-width box at this z-index sitting later in the
     DOM than the rail would swallow the taps meant for Share and WhatsApp.
     Ending before the rail's lane means it never covers a control. */
  left: 76px; right: 76px;
  margin: 0; padding: 0;
  justify-content: center; gap: 6px;
  scrollbar-width: none;
}
.mkviewer-full .mkslide-gallery::-webkit-scrollbar { display: none; }
/* 40px of photograph inside a 44px target, so the strip stays compact without
   becoming something a thumb misses. */
.mkviewer-full .mkslide-thumb {
  min-width: 44px; min-height: 44px;
  display: flex; align-items: center; justify-content: center;
}
.mkviewer-full .mkslide-thumb img { width: 40px; height: 40px; }
/* A short screen drops the thumbnail strip, and nothing else.

   The rail used to lie down beside the close control here. That was written
   for a column of FOUR controls that spanned most of a 844x390 screen and
   landed on a mid-edge step chevron — and neither of those is true any more.
   The rail is the Owner's three, 162px tall, which clears the caption band on
   the shortest screen this mode supports; and full-media mode draws no step
   chevrons at all, because its horizontal axis is the dots.

   The row also put the rightmost control straight through Share, which sits at
   the same height 60px from the right edge: measured in Chromium at 844x390
   and at 768x480, Chat covered 1520 square pixels of it. A column cannot
   collide with the header, because the header is at the top and the rail is at
   the bottom. */
@media (max-height: 480px) {
  .mkviewer-full .mkslide-gallery { display: none; }
}

/* The Owner's compact dots, in place of the "2 / 5" reading the product page
   keeps. Hidden entirely when a listing has one photograph, because there is
   then no position to be in.

   Controls, not decoration. With the step arrows gone from this mode the dots
   are the only pointer route along the horizontal axis, and "dots instead of
   arrows" may not quietly mean "swipe only" — so each dot is a real button
   that goes straight to its own photograph, reachable by tap, by keyboard and
   by a screen reader. The circle is drawn by a pseudo-element so the ink stays
   7px while the control around it is a size a thumb can actually hit. */
.mkviewer-full .mkslide-at.dots {
  display: flex; gap: 2px; align-items: center;
  background: none; padding: 0 8px;
}
.mkdot {
  box-sizing: border-box;
  width: 22px; height: 44px; padding: 0; margin: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; -webkit-appearance: none; appearance: none;
  cursor: pointer; color: inherit;
}
.mkdot::before {
  content: ""; display: block;
  width: 7px; height: 7px; border-radius: 999px;
  background: var(--on-overlay); opacity: .45;
  transition: opacity .15s ease, transform .15s ease;
}
/* `.on` and `aria-current` say the same thing, and both are written, so the
   eye and the screen reader are told by one act rather than two. */
.mkdot.on::before,
.mkdot[aria-current="true"]::before { opacity: 1; transform: scale(1.25); }
.mkdot:focus-visible {
  outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 999px;
}
@media (hover: hover) { .mkdot:hover::before { opacity: .8; } }

/* Close is 40x40 on the product page, where it shipped. The Owner's rule for
   this viewer is that Close, Share and WhatsApp are all at least 44x44 — a
   real thumb target, pressed one-handed — so it grows here and only here. */
.mkviewer-full .mkviewer-close {
  width: 44px; height: 44px; min-width: 44px; min-height: 44px;
}
@media (prefers-reduced-motion: reduce) { .mkdot::before { transition: none; } }

/* ── Physical audit ────────────────────────────────────────────────────────
   A stock count is done standing in the store holding a phone, so the
   screen is a column of readable blocks rather than a spreadsheet: one
   product at a time while counting, one card per counted line afterwards.
   The three quantities that matter — what the system holds, what was
   counted, and the difference between them — always appear in that order
   and in the same three boxes, whether the line is being entered or
   reviewed. */
.auditsteps {
  margin: 0 0 14px; padding-left: 20px;
  color: var(--muted); font-size: 13px; line-height: 1.7;
}
.auditref { display: flex; align-items: flex-start; gap: 10px; }
.auditref .grow { flex: 1; min-width: 0; }
.auditref .name { font-weight: 650; overflow-wrap: anywhere; }
.auditref .meta { color: var(--muted); font-size: 13px; margin-top: 2px; }

.auditbadges { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }

.auditfacts {
  display: grid; grid-template-columns: auto 1fr; gap: 4px 12px;
  margin: 12px 0 0; font-size: 13px;
}
.auditfacts dt { color: var(--muted); }
.auditfacts dd { margin: 0; overflow-wrap: anywhere; }

.countgrid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
  margin-top: 12px;
}
.countcell {
  display: flex; flex-direction: column; gap: 4px;
  padding: 8px 10px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--surface-2);
}
.countcell .k {
  color: var(--muted); font-size: 11px; text-transform: uppercase;
  letter-spacing: .04em; margin: 0;
}
.countcell strong { font-size: 20px; font-weight: 700; line-height: 1.1; }
.countcell strong.up { color: var(--ok); }
.countcell strong.down { color: var(--danger); }
.countcell input[type="number"] {
  width: 100%; min-height: 40px; padding: 4px 8px;
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); color: var(--text);
  font-size: 20px; font-weight: 700;
  -moz-appearance: textfield; appearance: textfield;
}
/* The spinner is unusable at arm's length and invites a stray tap that
   silently changes a counted total. The keypad is the input. */
.countcell input[type="number"]::-webkit-outer-spin-button,
.countcell input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0;
}
/* The sentence under the boxes reads the variance back in words and, when
   there is one, says why a reason is now being asked for. */
.countword { margin: 8px 0 12px; }

.auditline {
  padding: 12px; border-radius: var(--radius);
  border: 1px solid var(--border); background: var(--surface);
}
/* Staff: the warning the Owner sees while choosing the Admin role, and the
   confirmation that must be ticked before it is saved. */
.adminwarn {
  margin: 8px 0 12px; padding: 12px; border-radius: var(--radius);
  border: 1px solid var(--warn); background: var(--warn-soft); color: var(--text);
}
.adminwarn[hidden] { display: none; }
.adminwarn p { margin: 0 0 8px; font-size: 14px; line-height: 1.45; }
.adminwarn-confirm { display: flex; align-items: center; gap: 8px; min-height: 44px; }

/* Stock history: the product name on a movement opens that product's own
   history. A full-height tap target that still reads as a name. */
.auditref a.prodlink {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--brand); text-decoration: underline;
  text-underline-offset: 3px; cursor: pointer;
}
/* Stock history export: the sheet on one product's history page, and the
   print-ready Stock History Statement it opens. Everything is stmt-scoped. */
.stmt-sheet > summary { list-style: none; display: inline-flex; }
.stmt-sheet > summary::-webkit-details-marker { display: none; }
.stmt-sheet form { display: grid; gap: 12px; margin-top: 12px; }
.stmt-choice { border: 0; margin: 0; padding: 0; display: grid; gap: 6px; }
.stmt-choice legend { font-weight: 650; margin-bottom: 4px; }
.stmt-choice label { display: flex; align-items: center; gap: 8px; min-height: 44px; }
.stmt-dates { display: grid; gap: 8px; }
.stmt-dates[hidden] { display: none; }
.stmt-dates label { display: grid; gap: 4px; }
.stmt-dates input { min-height: 44px; }
.stmt-page { background: var(--bg); color: var(--text); margin: 0; }
.stmt-toolbar { display: flex; gap: 8px; flex-wrap: wrap; padding: 12px 16px; }
.stmt-doc { padding: 0 16px 24px; max-width: 1100px; }
.stmt-title { font-size: 20px; margin: 8px 0 12px; }
.stmt-facts {
  display: grid; grid-template-columns: auto 1fr; gap: 4px 12px;
  margin: 0 0 12px; font-size: 14px;
}
.stmt-facts dt { color: var(--muted); }
.stmt-facts dd { margin: 0; overflow-wrap: anywhere; }
.stmt-note { color: var(--muted); font-size: 13px; }
.stmt-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.stmt-table th, .stmt-table td {
  border: 1px solid var(--border); padding: 6px 8px; text-align: left;
  vertical-align: top; overflow-wrap: anywhere;
}
.stmt-table th { background: var(--surface-2); }
@media screen and (max-width: 720px) {
  .stmt-table thead { display: none; }
  .stmt-table, .stmt-table tbody, .stmt-table tr, .stmt-table td { display: block; }
  .stmt-table tr {
    border: 1px solid var(--border); border-radius: 10px;
    margin-bottom: 8px; padding: 6px 0;
  }
  .stmt-table td { border: 0; padding: 2px 10px; display: grid;
                   grid-template-columns: 40% 1fr; gap: 8px; }
  .stmt-table td::before { content: attr(data-label); color: var(--muted); }
}
@media print {
  @page { size: A4 landscape; margin: 12mm; }
  .stmt-page { background: var(--surface); }
  .stmt-toolbar { display: none; }
  .stmt-doc { padding: 0; max-width: none; }
  .stmt-table { font-size: 10px; }
  .stmt-table tr { break-inside: avoid; }
}
.auditwhy { color: var(--muted); font-size: 13px; margin-top: 10px; }
.auditwhy:empty { display: none; }

.auditactions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.auditactions:empty { display: none; }
.auditactions .btn { flex: 1 1 auto; }

.auditsummary {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
  margin-bottom: 10px;
}
.auditsummary-cell {
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 10px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--surface-2);
}
.auditsummary-cell strong { font-size: 18px; font-weight: 700; line-height: 1.1; }
.auditsummary-cell .k {
  color: var(--muted); font-size: 11px; line-height: 1.3;
}

/* ── Workspace bar: who is here, where, when, and what is waiting ──────────
   A row under the header rather than inside it. The header is already at its
   width budget on a 320px phone — the door, the mark, the page name and the
   business switcher — and pushing four more things into it is what makes a
   header scroll sideways. This sits beneath, wraps the same way, and carries
   no colour of its own: every value below is a token, so the bar follows the
   theme instead of restating it. */
.workbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; row-gap: 8px;
  padding: 10px 16px;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
/* Installed app in landscape on a notched phone: keep clear of the notch. */
.workbar {
  padding-left: max(16px, env(safe-area-inset-left));
  padding-right: max(16px, env(safe-area-inset-right));
}
.workbar .spacer { flex: 1; }
.workbar-avatar {
  width: 38px; height: 38px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--brand-soft); color: var(--brand-dark);
  font-weight: 700; font-size: 16px;
}
/* The identity block gives up width before the actions do: a long business
   name truncates, a bell never disappears. */
.workbar-id { min-width: 0; flex: 1 1 12ch; }
.workbar-id p { margin: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.workbar-greet { font-weight: 650; font-size: 15px; color: var(--text); }
.workbar-biz { font-size: 13px; color: var(--brand-dark); }
.workbar-date { font-size: 12px; color: var(--muted); }

.workbar-actions { display: flex; align-items: center; gap: 6px; flex: none; }
.workbar-act {
  /* The shared tap target. These were 42px, just under it. */
  position: relative; width: var(--tap); height: var(--tap); border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: var(--text); text-decoration: none;
}
.workbar-act .ic { width: 22px; height: 22px; }
@media (hover: hover) {
  .workbar-act:hover { background: var(--brand-soft); color: var(--brand-dark); }
}
/* The badge is a count, not an alarm: it uses the brand, not the danger
   colour, because unread messages are ordinary and red means error here. */
.workbar-badge {
  position: absolute; top: 2px; right: 2px;
  min-width: 18px; height: 18px; padding: 0 4px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--brand); color: var(--on-brand);
  font-size: 11px; font-weight: 700; line-height: 1;
}

/* Paper carries none of this, the same way the header and tab bar do not. */
@media print { .workbar { display: none !important; } }

/* ── Touch targets for the shell's own controls ────────────────────────────
   The DADCARE return control and the chips are compact for a mouse and meet
   the 44px tap target on a touch screen. */
@media (pointer: coarse) {
  .mother-return, .chip { min-height: var(--tap); }
}

/* ── Short landscape screens ───────────────────────────────────────────────
   A phone on its side has ~360px of height, and the header, workspace bar
   and tab bar took nearly half of it. Everything stays — the same controls,
   names and order — but tighter: the header scrolls away with the page
   instead of pinning, the bars lose their spare padding, and tab labels sit
   beside their icons. Every control keeps the 44px tap target. */
@media screen and (orientation: landscape) and (max-height: 500px) {
  .topbar { position: static; padding-top: 6px; padding-bottom: 6px; }
  .workbar { padding-top: 4px; padding-bottom: 4px; }
  .workbar-avatar { width: 32px; height: 32px; font-size: 14px; }
  .tabbar { padding-top: 2px; padding-bottom: calc(2px + env(safe-area-inset-bottom)); }
  .tabbar a { flex-direction: row; gap: 6px; font-size: 12px; padding-top: 4px; padding-bottom: 4px; }
  body { padding-bottom: calc(52px + env(safe-area-inset-bottom)); }
}
