/*
  Duka storefront.

  Every value here comes from the design canvas token panel (artboard 1j) and is FIXED. The only
  merchant input is --brand, set inline per shop by the layout, from which --on-brand and
  --brand-tint are derived server-side.

  Two rules that look like omissions and are not:
    * No shadows anywhere. Elevation is a 1px line on a white surface. Shadows blur on low-DPI
      panels and wash out in sunlight, which is the daylight-in-Kampala case, not an edge case.
    * Nothing below 12px, and product names never below 14px.
*/

:root {
    --ink: #1B1916;
    --text-2: #5C5750;
    --meta: #7A746B;
    --surface: #F7F5F1;
    --surface-2: #EFECE6;
    --surface-3: #F5F2ED;
    --border: #E6E1D9;
    --border-2: #DCD6CC;
    --muted: #C9C3B9;
    --white: #FFFFFF;

    --r-sm: 6px;
    --r-md: 10px;
    --r-lg: 12px;
    --r-xl: 14px;
    --r-pill: 999px;

    --gutter: 16px;
}

* { box-sizing: border-box; }

/*
   The hidden attribute has to beat a layout class. Any rule that sets display - .picker and .panel
   both do - outranks the user agent's [hidden] { display: none }, so markup that says hidden was
   rendered anyway. It showed up as the stock box and the empty combination panel both visible on a
   product with no options chosen, which is exactly the state they exist to be absent from.
*/
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    background: var(--surface);
    color: var(--ink);
    font-family: "Public Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    font-size: 14px;
    font-weight: 500;
    line-height: 1.5;
}

a { color: inherit; text-decoration: none; }

img { max-width: 100%; display: block; }

button { font: inherit; color: inherit; border: none; background: none; cursor: pointer; }

input, select, textarea { font: inherit; color: inherit; }

/* The phone frame. Above 420px the shop centres rather than stretching: this design is specified
   at 360px and a 1200px-wide product grid would be a different design, not a wider one. */
.shell {
    max-width: 420px;
    margin: 0 auto;
    background: var(--surface);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

/* ---------- type scale ---------- */

.t-price-xl { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; }
.t-title    { font-size: 20px; font-weight: 700; letter-spacing: -0.01em; line-height: 1.25; }
.t-heading  { font-size: 16px; font-weight: 700; }
.t-price    { font-size: 15px; font-weight: 700; }
.t-body     { font-size: 14px; font-weight: 500; }
.t-chip     { font-size: 13px; font-weight: 600; }
.t-meta     { font-size: 12px; font-weight: 400; color: var(--meta); }

.t-label {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--meta);
}

/* ---------- header ---------- */

.header {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px var(--gutter) 10px;
    background: var(--white);
}

/*
  The shop's mark, and the reason two storefronts no longer look like the same page with a different
  name typed into it. It spends one of the places brand is allowed, deliberately: a grey square of
  grey initials is the most generic thing on the screen, and it sits in the one spot a shopper looks
  to know whose shop they are in.

  A merchant's own logo replaces it when they have uploaded one. The initials are the designed
  fallback rather than a placeholder - most shops open without a logo and should still look like
  themselves.
*/
.header__logo {
    width: 48px;
    height: 48px;
    border-radius: 12px;
    background: var(--brand-tint);
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: 0.01em;
    color: var(--brand);
    overflow: hidden;
}

/* A real logo fills its corner-rounded tile; the tint behind it never shows through. */
.header__logo--image { background: var(--surface-2); }

.header__logo--image img { width: 100%; height: 100%; object-fit: cover; display: block; }

.header__names { flex: 1; min-width: 0; }

/* The shop's name is the headline of its own site, so it is set like one. */
.header__shop {
    font-size: 19px;
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.header__sub { font-size: 12.5px; color: var(--meta); line-height: 1.3; margin-top: 1px; }

/* Opening hours, on their own line below the subtitle. Smaller than the subtitle because it answers
   a question the shopper has not asked yet, and the shop's own name must stay the largest thing. */
.header__hours {
    font-size: 11.5px;
    color: var(--meta);
    line-height: 1.3;
    margin-top: 1px;
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

/* The one word a shopper is actually looking for, so it carries the only colour on the line. */
.hours__state {
    font-weight: 600;
    font-size: 10.5px;
    letter-spacing: 0.02em;
    padding: 1px 6px;
    border-radius: 999px;
    white-space: nowrap;
}

.hours__state--open { color: #12694A; background: #DFF3E9; }
.hours__state--shut { color: #7A3B3B; background: #F5E4E4; }

/* The 3px brand rule under the header. One of the five places brand is allowed. */
.brand-rule { height: 3px; background: var(--brand); }

.back {
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
}

.back__glyph {
    width: 10px;
    height: 10px;
    border-left: 2px solid var(--ink);
    border-bottom: 2px solid var(--ink);
    transform: rotate(45deg);
    margin-left: 4px;
}

/* ---------- cart button ---------- */

.cart-button {
    width: 44px;
    height: 44px;
    border-radius: var(--r-lg);
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    flex: none;
}

.cart-button__glyph {
    width: 20px;
    height: 16px;
    border: 2px solid var(--ink);
    border-top-width: 3px;
    border-radius: 3px 3px 6px 6px;
}

.cart-button__count {
    position: absolute;
    top: 6px;
    right: 6px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: 9px;
    background: var(--brand);
    color: var(--on-brand);
    font-size: 11px;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ---------- page body ---------- */

.page {
    padding: 14px var(--gutter) 0;
    display: flex;
    flex-direction: column;
    gap: 18px;
    flex: 1;
}

.section { display: flex; flex-direction: column; gap: 12px; }

.section__head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
}

.section__link {
    font-size: 14px;
    font-weight: 600;
    color: var(--brand);
    min-height: 44px;
    display: flex;
    align-items: center;
}

/* ---------- search ---------- */

.search {
    height: 44px;
    border-radius: var(--r-lg);
    background: var(--white);
    border: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 14px;
    color: var(--meta);
    font-size: 14px;
    width: 100%;
}

.search__glyph {
    width: 14px;
    height: 14px;
    border: 2px solid var(--meta);
    border-radius: 50%;
    flex: none;
}

.search input {
    border: none;
    outline: none;
    background: none;
    flex: 1;
    min-width: 0;
    color: var(--ink);
}

/* ---------- category rail ---------- */

.rail {
    display: flex;
    gap: 10px;
    overflow-x: auto;
    margin: 0 calc(var(--gutter) * -1);
    padding: 0 var(--gutter);
    scrollbar-width: none;
}

.rail::-webkit-scrollbar { display: none; }

.rail__item { flex: none; width: 96px; display: flex; flex-direction: column; gap: 6px; }

/*
  The "Most popular" row reuses .rail for its horizontal scroll and hidden scrollbar, but a product
  card reads at a grid cell's width, not a 96px category tile's. A modifier rather than changing
  .rail__item itself, which the category row above is still sized by.
*/
.rail__item--product { width: 150px; }

.rail__frame {
    aspect-ratio: 1;
    border-radius: var(--r-xl);
    background: var(--surface-2);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    position: relative;
}

.rail__frame img { width: 100%; height: 100%; object-fit: cover; object-position: center; }

/*
  A category tile from the handed-over design: a 1:1 warm tint with one ink line drawing. The tint
  comes from the shop's own category and is set inline, because it is data rather than a rule.

  radius 14 rather than --r-xl: the design names that number, and the tile is the one place on the
  storefront where the corner is specified rather than inherited.
*/
.rail__frame--tile { border-radius: 14px; }

/* 46% of the tile, which is the design's figure - the glyph is drawn on a 48-unit grid with an
   8-unit safe margin, so it already carries its own breathing room. */
.tile__glyph { width: 46%; height: 46%; display: block; }

/*
  What a category shows before anything in it has been photographed. A letter in the shop's own
  brand colour, not the hatched placeholder the product grid uses: a shortcut row of hatched squares reads
  as broken, where a row of monograms reads as a menu.
*/
.rail__mono {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 34px;
    font-weight: 700;
    line-height: 1;
    color: var(--brand);
    background: var(--brand-tint);
    letter-spacing: 0.02em;
}

.rail__label { font-size: 13px; font-weight: 600; text-align: center; }

/*
  The AI-is-working panel on the drafts screen. Deliberately the loudest thing on that page: the
  merchant has just handed over a pile of photographs and has nothing to look at, and the failure
  mode of a quiet progress bar is that they assume it has finished badly and start again.
*/
.working {
    margin: 12px 16px 4px;
    padding: 16px;
    border-radius: var(--r-lg);
    border: 1px solid var(--brand);
    background: var(--brand-tint);
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.working__head { display: flex; align-items: center; gap: 12px; }

.working__title { margin: 0; font-size: 17px; font-weight: 700; line-height: 1.2; }

.working__lede { margin: 2px 0 0; font-size: 14px; font-weight: 600; color: var(--brand); }

.working__note { margin: 0; font-size: 13px; line-height: 1.5; color: var(--text-2); }

.working__progress { margin: 0; }

/* A ring that turns, so the panel is visibly alive rather than merely present. */
.working__spinner {
    flex: none;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    border: 3px solid var(--brand-tint);
    border-top-color: var(--brand);
    animation: working-spin 900ms linear infinite;
}

@keyframes working-spin { to { transform: rotate(360deg); } }

/*
  Someone who has asked for less motion still needs to know this is running, so the ring stops
  turning and breathes instead - the information survives, the spinning does not.
*/
@media (prefers-reduced-motion: reduce) {
    .working__spinner {
        animation: working-pulse 1.6s ease-in-out infinite;
        border-top-color: var(--brand-tint);
        background: var(--brand);
    }

    @keyframes working-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
}

/*
  The logo picker in editor settings. The preview is the shop's actual mark at the size the header
  draws it, not a thumbnail of a file: a merchant deciding whether a logo works needs to see the
  thing shoppers will see.
*/
.logo-row { display: flex; gap: 14px; align-items: flex-start; padding: 14px; }

.logo-row__preview {
    flex: none;
    width: 64px;
    height: 64px;
    border-radius: 14px;
    overflow: hidden;
    background: var(--brand-tint);
    display: flex;
    align-items: center;
    justify-content: center;
}

.logo-row__preview img { width: 100%; height: 100%; object-fit: cover; display: block; }

.logo-row__mono { font-size: 22px; font-weight: 700; color: var(--brand); }

.logo-row__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }

.logo-row__actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* The picker sits inside a row rather than owning a screen, so it loses the tall empty middle. */
.dropzone--slim { padding: 10px 12px; gap: 2px; min-height: 0; }

.dropzone--slim .dropzone__label { font-size: 14px; }

/* ---------- product grid ---------- */

.grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px 12px;
}

.card { display: flex; flex-direction: column; gap: 8px; }

/*
  The 4:5 frame is what turns a merchant's mixed phone photos into a catalogue: portrait, landscape
  and square shots all read as one grid.

  `cover`, not `contain`. Contain never distorts, but it letterboxes - a wide landscape shot becomes
  a thin strip floating in a tall box, and a column of those reads as a broken grid even though every
  frame is the same size. Cover fills the frame and crops the overflow instead: uniform weight down
  the column, and still no stretching, because the aspect ratio is preserved either way. The inset
  goes with it; padding under cover would shrink the photo and then crop it anyway.

  The product page's hero keeps `contain` on purpose - there, seeing the whole item beats matching
  its neighbours.
*/
.frame {
    aspect-ratio: 4 / 5;
    border-radius: var(--r-lg);
    background: var(--surface-2);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

.frame img { width: 100%; height: 100%; object-fit: cover; object-position: center; }

/* Stands in for a photo the merchant has not uploaded yet, in the canvas's own hatch. */
.frame__placeholder {
    width: 70%;
    height: 85%;
    border-radius: 3px;
    background: repeating-linear-gradient(135deg, var(--border-2) 0 5px, var(--border) 5px 10px);
}

.frame__tag {
    position: absolute;
    top: 8px;
    left: 8px;
    background: var(--brand);
    color: var(--on-brand);
    font-size: 11px;
    font-weight: 700;
    padding: 3px 7px;
    border-radius: var(--r-sm);
}

.card__meta { display: flex; flex-direction: column; gap: 2px; }
.card__name { font-size: 14px; font-weight: 500; line-height: 1.3; }

/* ---------- chips ---------- */

.chips {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding: 12px var(--gutter);
    margin: 0 calc(var(--gutter) * -1);
    scrollbar-width: none;
}

.chips::-webkit-scrollbar { display: none; }

.chip {
    flex: none;
    height: 36px;
    padding: 0 14px;
    border-radius: var(--r-pill);
    border: 1.5px solid var(--border);
    background: var(--white);
    color: var(--text-2);
    font-size: 13px;
    font-weight: 600;
    display: flex;
    align-items: center;
    /* 36px tall but a 44px hit area, per the touch rules. */
    position: relative;
}

.chip::after {
    content: "";
    position: absolute;
    inset: -4px 0;
}

.chip--on {
    border-color: var(--brand);
    background: var(--brand-tint);
    color: var(--ink);
}

/* A colour filter chip carries its swatch first, with a little daylight before the name. */
.chip .swatch { margin-right: 6px; }

/* ---------- the filter bar ---------- */

/* A full-width bar, not a pill. A pill here sat on its own row under the category chips and read as
   a category that had wrapped - same shape, same size, same place. Hairlines above and below say
   "toolbar" instead, and give the panel below something to hang from. */
.filters {
    border-block: 1px solid var(--border-2);
    margin-bottom: 14px;
}

/* list-style covers Firefox's triangle; ::-webkit-details-marker covers Safari's. */
.filters__button {
    list-style: none;
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 46px;
    padding: 0 var(--gutter);
    font-size: 13.5px;
    font-weight: 600;
    cursor: pointer;
    user-select: none;
}

.filters__button::-webkit-details-marker { display: none; }

/* The caret follows the word rather than sitting at the far edge, so it reads as part of the label
   instead of as a second control. Drawn with borders: no font support, no image request. */
.filters__button > span:first-child::after {
    content: "";
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-left: 7px;
    vertical-align: 2px;
    border-right: 1.5px solid var(--muted);
    border-bottom: 1.5px solid var(--muted);
    transform: rotate(45deg);
}

.filters[open] .filters__button > span:first-child::after {
    transform: rotate(-135deg);
    vertical-align: 4px;
}

/* Removing the browser's triangle removes its focus affordance with it, and this is the one control
   here reachable only by tap or keyboard. */
.filters__button:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

.filters__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--brand);
    color: var(--on-brand);
    font-size: 11px;
    font-weight: 700;
}

/* Pushed to the far edge, and quiet: it is the answer to a question nobody asked yet. */
.filters__total {
    margin-left: auto;
    font-size: 12.5px;
    font-weight: 400;
    color: var(--meta);
}

/* No box of its own. The bar's own rules already frame this, and a second border inside them was
   two frames around one small thing. */
.filters__body { padding: 2px var(--gutter) 12px; }

.filters__body .t-label { display: block; }

/* The chips row brings its own vertical padding; the gutter comes from the body. */
.chips--filter { padding: 6px 0 2px; margin: 0; }

.filters__clear {
    display: inline-block;
    margin-top: 8px;
    font-size: 12.5px;
    color: var(--muted);
    text-decoration: underline;
}


/* ---------- the food menu ---------- */

/*
  A food shop's storefront, from the handed-over design. "Food is bought by name and appetite, not
  by browsing photos - so the menu is a list, not a grid." Only this shop category uses any of it.
*/

/* Tabs rather than chips: a menu is read top to bottom, so these jump to a heading instead of
   filtering the page. Sticky, so the section a shopper is in stays reachable while they scroll. */
.menutabs {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    gap: 6px;
    overflow-x: auto;
    scrollbar-width: none;
    padding: 10px var(--gutter);
    margin: 0 calc(var(--gutter) * -1);
    background: var(--surface);
    border-bottom: 1px solid var(--border-2);
}

.menutabs::-webkit-scrollbar { display: none; }

.menutab {
    flex: none;
    height: 36px;
    padding: 0 14px;
    border-radius: 18px;
    border: 1.5px solid var(--border-2);
    background: var(--white);
    color: var(--text-2);
    font-size: 13px;
    font-weight: 600;
    display: flex;
    align-items: center;
    text-decoration: none;
}

/* The heading a tab points at sits under a sticky strip, so it needs the strip's height of clearance
   or the first row lands behind it. */
.section[id^="menu-"] { scroll-margin-top: 60px; }

/* The one row that keeps big photographs: a plate is what sells a plate. */
.rail__item--plate { width: 200px; }

.plate__frame {
    aspect-ratio: 4 / 3;
    border-radius: 12px;
    background: var(--surface-2);
    overflow: hidden;
    position: relative;
    display: block;
}

.plate__frame img { width: 100%; height: 100%; object-fit: cover; display: block; }

.plate__tag {
    position: absolute;
    top: 8px;
    left: 8px;
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--ink);
    color: var(--white);
    font-size: 11px;
    font-weight: 700;
}

.plate__name { font-size: 14px; font-weight: 600; line-height: 1.3; }
.plate__price { font-size: 14px; font-weight: 700; }

/* The list itself: one card, rows divided by hairlines. */
.menu {
    background: var(--white);
    border: 1px solid var(--border-2);
    border-radius: var(--r-lg);
    overflow: hidden;
}

.menurow {
    display: flex;
    gap: 12px;
    padding: 12px;
    color: inherit;
    align-items: stretch;
    position: relative;
}

/*
  The whole row opens the dish. A stretched anchor rather than wrapping the row in one, because the
  row also holds the add button - and a button inside an anchor is invalid, which browsers resolve by
  dropping one of them.
*/
.menurow__open {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
    text-decoration: none;
    color: inherit;
}

.menurow__open::after { content: ""; position: absolute; inset: 0; }

.menurow + .menurow { border-top: 1px solid var(--border-2); }

.menurow__name { font-size: 15px; font-weight: 600; line-height: 1.3; }

/* Two lines at most: the menu's rhythm matters more than the whole sentence, and the item's own
   page carries the rest. */
.menurow__desc {
    font-size: 13px;
    color: var(--text-2);
    line-height: 1.4;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.menurow__foot { display: flex; gap: 8px; align-items: baseline; margin-top: auto; }
.menurow__price { font-size: 15px; font-weight: 700; }
.menurow__note { font-size: 12px; font-weight: 600; color: #B8433A; }

/* Not overflow:hidden - the add button hangs off its corner, the way the design draws it. */
.menurow__frame {
    flex: none;
    width: 88px;
    height: 88px;
    border-radius: 10px;
    background: var(--surface-2);
    position: relative;
}

.menurow__frame img { border-radius: 10px; }

/*
  The add control on a menu row, overlapping the photograph's corner the way the design draws it.
  One element in two states: a + on its own, or a count with a stepper either side once the dish is
  on the order. Above the stretched row link, so a thumb on it adds where a thumb anywhere else
  opens the dish.
*/
.menuadd {
    position: absolute;
    right: -6px;
    bottom: -6px;
    z-index: 1;
    display: flex;
    align-items: center;
    height: 32px;
    padding: 0 1px;
    border-radius: 16px;
    background: var(--white);
    border: 1.5px solid var(--border-2);
}

.menuadd button {
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    font-weight: 600;
    line-height: 1;
    color: var(--brand);
    background: none;
    cursor: pointer;
    position: relative;
}

/* The circle keeps the design's size; the target does not. */
.menuadd button::after { content: ""; position: absolute; inset: -9px; }

.menuadd__qty { min-width: 16px; text-align: center; font-size: 13px; font-weight: 700; }

/* While the shop is being told. Brief, and only enough that a second tap reads as ignored. */
.menuadd--busy { opacity: 0.55; pointer-events: none; }

/*
  The sheet that asks whether the dish is wanted as it comes. From the footer, because that is where
  a thumb already is on a menu - a dialog in the middle of the screen asks it to travel.
*/
.sheet {
    position: fixed;
    inset: 0;
    z-index: 10;
    background: rgba(27, 25, 22, 0.4);
    display: flex;
    align-items: flex-end;
}

.sheet__panel {
    width: 100%;
    background: var(--white);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    padding: 18px var(--gutter) max(18px, env(safe-area-inset-bottom));
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.sheet__title { font-size: 17px; font-weight: 700; line-height: 1.25; }
.sheet__actions { display: flex; flex-direction: column; gap: 8px; }

.sheet__close {
    min-height: 44px;
    font-size: 13px;
    color: var(--meta);
    background: none;
    cursor: pointer;
}

/* Slides up rather than appearing, which on a phone reads as part of the tap. */
@media (prefers-reduced-motion: no-preference) {
    .sheet:not([hidden]) .sheet__panel { animation: sheet-up 160ms ease-out; }
}

@keyframes sheet-up {
    from { transform: translateY(100%); }
    to { transform: translateY(0); }
}

/* A kitchen runs out of one dish, not of the dish: it stays on the menu, dimmed and labelled. */
.menurow--out { opacity: 0.45; }

/* ---------- product detail ---------- */

/*
  No padding, and the photograph fills the frame.
  
  It had padding: 6%, which fought the aspect-ratio above it: browsers disagree about whether the
  ratio applies to the content box or the border box, so the same page letterboxed the photo inside a
  band of grey on Chromium and pushed it off the right edge of the screen on the tester's iPhone.
  
  The padding was there to inset a photograph inside a frame, which is a job that moved: every stored
  image is already rendered to exactly this 4:5 with its own blurred fill, so it is meant to reach
  all four edges. Same defect as .line__frame carried on the cart, fixed there on 2026-09-11.
*/
.hero {
    aspect-ratio: 4 / 5;
    background: var(--surface-2);
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

/* cover rather than contain: every stored photograph is exactly 4:5 so the two agree, and cover is
   the one that still fills the frame if an older image ever turns up at another ratio. */
.hero img { width: 100%; height: 100%; object-fit: cover; display: block; }

.hero__back {
    position: absolute;
    top: 8px;
    left: 8px;
    width: 44px;
    height: 44px;
    border-radius: 22px;
    background: rgba(255, 255, 255, 0.92);
    display: flex;
    align-items: center;
    justify-content: center;
}

.thumbs { display: flex; gap: 8px; padding: 10px var(--gutter) 0; overflow-x: auto; scrollbar-width: none; }
.thumbs::-webkit-scrollbar { display: none; }

.thumb {
    width: 56px;
    height: 56px;
    border-radius: 8px;
    background: var(--surface-2);
    border: 2px solid transparent;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    overflow: hidden;
}

.thumb--on { border-color: var(--brand); }
.thumb img { width: 100%; height: 100%; object-fit: cover; }

.options { display: flex; gap: 8px; flex-wrap: wrap; }

.option {
    flex: 1 0 auto;
    min-width: 56px;
    height: 44px;
    border-radius: var(--r-md);
    border: 1.5px solid var(--border);
    background: var(--white);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 600;
}

/* A colour, shown as a colour. The ring is a hairline rather than a shadow, so a white swatch
   still reads as a swatch on a white card. */
.swatch {
    width: 18px;
    height: 18px;
    border-radius: 9px;
    border: 1px solid rgba(27, 25, 22, 0.18);
    flex: none;
    display: inline-block;
}

.option { gap: 6px; }

.option--on,
.option:has(input:checked) { border-color: var(--brand); background: var(--brand-tint); }

.option:focus-within { outline: 2px solid var(--brand); outline-offset: 2px; }

.option--out {
    color: var(--muted);
    border-style: dashed;
    text-decoration: line-through;
}

.accordion { display: flex; flex-direction: column; border-top: 1px solid var(--border); }

.accordion summary {
    display: flex;
    justify-content: space-between;
    align-items: center;
    min-height: 48px;
    border-bottom: 1px solid var(--border);
    font-size: 14px;
    font-weight: 500;
    list-style: none;
    cursor: pointer;
}

.accordion summary::-webkit-details-marker { display: none; }
.accordion summary::after { content: "+"; color: var(--meta); font-size: 18px; }
.accordion details[open] summary::after { content: "\2212"; }
.accordion .accordion__body { padding: 0 0 12px; font-size: 14px; color: var(--text-2); }

/* ---------- sticky action bar ---------- */

.sticky {
    position: sticky;
    bottom: 0;
    padding: 12px var(--gutter) 14px;
    background: var(--white);
    /* A line, not a shadow. */
    border-top: 1px solid var(--border);
    display: flex;
    gap: 10px;
    /* auto, not a fixed gap: on a short page the bar sits at the bottom of the screen where a
       thumb expects it, and on a long one it still sticks while scrolling. */
    margin-top: auto;
}

/*
  Every bar on the storefront and in the editor sits inside a .page, which already carries the
  gutter as padding - so the bar was inset by it, leaving a dead strip of background either side of
  a bar that is meant to look anchored to the bottom of the screen, and pushing its own button to
  twice the gutter. Measured at 375px: the bar ran 16 to 359 and the button began at 32.

  The negative inline margin cancels the page's padding so the bar's surface and its top rule reach
  both edges, while the bar's own padding above puts the button back at one gutter. Same trick .rail
  and .chips already use to bleed out of the same container.

  Scoped to .page rather than applied to .sticky itself, so a bar that is ever placed outside one
  does not get pulled off-screen.
*/
.page .sticky { margin-inline: calc(var(--gutter) * -1); }

.cta {
    flex: 1;
    min-height: 52px;
    border-radius: var(--r-xl);
    background: var(--brand);
    color: var(--on-brand);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    font-size: 16px;
    font-weight: 700;
    text-align: center;
    padding: 0 12px;
    width: 100%;
}

.cta--ghost {
    background: var(--white);
    color: var(--ink);
    border: 1.5px solid var(--border);
}

.cta[disabled] { background: var(--muted); color: var(--white); }

/* The mark itself is an inline SVG - see Shared/_WhatsAppIcon.cshtml. This only has to keep it from
   being squeezed when the button's label wraps. */
.cta__whatsapp { flex: none; }

.icon-button {
    width: 52px;
    height: 52px;
    border-radius: var(--r-xl);
    border: 1.5px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
}

/* ---------- cards, rows, lists ---------- */

.panel {
    background: var(--white);
    border-radius: var(--r-xl);
    border: 1px solid var(--border);
    overflow: hidden;
}

.row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 0 14px;
    min-height: 52px;
    border-bottom: 1px solid var(--border);
    width: 100%;
    text-align: left;
}

.row:last-child { border-bottom: none; }
.row--on,
.row:has(input:checked) { background: var(--brand-tint); }

.radio {
    width: 22px;
    height: 22px;
    border-radius: 11px;
    border: 2px solid var(--border-2);
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
}

.row--on .radio,
.row:has(input:checked) .radio { border-color: var(--brand); }
.radio__dot { width: 10px; height: 10px; border-radius: 5px; background: transparent; }
.row--on .radio__dot,
.row:has(input:checked) .radio__dot { background: var(--brand); }

.row__label { flex: 1; font-size: 14px; font-weight: 500; }
.row__value { font-size: 14px; font-weight: 600; color: var(--text-2); }

/* ---------- cart lines ---------- */

.line { display: flex; gap: 12px; padding: 12px; border-bottom: 1px solid var(--border); }
.line:last-child { border-bottom: none; }

.line__frame {
    width: 72px;
    height: 90px;
    border-radius: 8px;
    background: var(--surface-2);
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

/* No padding on the frame. It used to inset by 6%, which left a band of the frame's own background
   showing on all four sides of every photograph - in the cart, in the merchant's product list and
   in the drafts review. The placeholder below sizes itself and never needed it. */
.line__frame img { width: 100%; height: 100%; object-fit: cover; }

.line__body {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    min-width: 0;
    gap: 8px;
}

/* 44px, not 36. Everything else on this storefront that a thumb has to hit is 44 - the legal
   links, the sticky buttons - and these two were the smallest targets on the busiest screen. */
.stepper {
    display: flex;
    align-items: center;
    border: 1.5px solid var(--border);
    border-radius: var(--r-md);
    height: 44px;
}

.stepper button {
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    color: var(--text-2);
}

/*
  A disabled stepper button used to look exactly like a working one, so tapping it did nothing and
  said nothing. It now reads as unavailable, and the line prints why - see .stepper__limit.
*/
.stepper button:disabled { color: var(--border); cursor: not-allowed; }

.stepper__qty { min-width: 28px; text-align: center; font-size: 14px; font-weight: 600; }

/* What the shopper asked for on this line, quoted so it reads as their words rather than as a
   label the shop wrote. */
.linenote {
    font-size: 12.5px;
    font-style: italic;
    color: var(--text-2);
    line-height: 1.35;
}

/* While the shop is being told. Enough that a second tap reads as ignored, and no more. */
.lines--busy { opacity: 0.55; pointer-events: none; }

/*
  Takes a whole line off the order. Quiet and text-only: it sits beside a stepper on every row, and
  a second filled button per line would compete with "Send order on WhatsApp" for the eye.
*/
.linedrop button {
    min-height: 44px;
    padding: 0 4px;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--meta);
    background: none;
    text-decoration: underline;
    cursor: pointer;
}

/* The reason the + is dead: the shop has no more of this option. Its own line under the stepper
   rather than squeezed between it and the price, which on a narrow phone left neither room. */
.stepper__limit { flex-basis: 100%; font-size: 11.5px; color: var(--meta); }

/*
  "Added to your order", on the product page the shopper stayed on. Quiet: the cart count in the
  header is the real signal, and this only exists so the add is not silent.
*/
/* A slim call to action, for a row that is not the page's main one. */
.cta--slim { min-height: 44px; font-size: 15px; width: auto; }



/*
  The quantity stepper beside "Add to cart". Same shape and the same 44px targets as the cart's,
  so the gesture a shopper learns on one screen works on the other.
*/
.qty {
    display: flex;
    align-items: center;
    flex: none;
    height: 48px;
    border: 1.5px solid var(--border);
    border-radius: var(--r-md);
    background: var(--white);
    overflow: hidden;
}

.qty__step {
    width: 44px;
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    color: var(--text-2);
    background: none;
}

.qty__step:disabled { color: var(--border); cursor: not-allowed; }

/*
  The field is the control; the buttons only drive it. Narrow because it holds one or two digits,
  and centred so the number sits between the two buttons rather than against one of them.
*/
.qty__value {
    width: 40px;
    height: 100%;
    border: 0;
    padding: 0;
    text-align: center;
    font-size: 15px;
    font-weight: 600;
    background: none;
    -moz-appearance: textfield;
}

/* The native spinners are a second set of arrows next to ours, and far too small to hit. */
.qty__value::-webkit-outer-spin-button,
.qty__value::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Without the script the buttons stay hidden and the field is the whole control, so it needs room
   for the browser's own spinners. */
.qty:has(.qty__step[hidden]) .qty__value { width: 64px; -moz-appearance: auto; }

/* ---------- editor photo gallery ---------- */

.gallery { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }

.gallery__item { display: flex; flex-direction: column; gap: 4px; }

.gallery__badge {
    font-size: 11px;
    font-weight: 700;
    color: var(--on-brand);
    background: var(--brand);
    border-radius: var(--r-sm);
    padding: 3px 6px;
    text-align: center;
}

.gallery__action {
    font-size: 12px;
    font-weight: 600;
    min-height: 32px;
    border-radius: var(--r-sm);
    border: 1px solid var(--border);
    background: var(--white);
}

.gallery__action--remove { color: #B3261E; }

/* ---------- editor option rows ---------- */

.optrow {
    padding: 12px;
    border-bottom: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.optrow:last-child { border-bottom: none; }

/* ---------- the merchant's orders ---------- */

/* A state, always printed with its own word beside it - the colour is emphasis, never the
   meaning. Four tones for five states: Confirmed and Packed are both "underway" to a
   shopkeeper, and what has to stand out down a list is the one state that is a job to do. */
.ostate {
    font-size: 12px;
    font-weight: 700;
    padding: 2px 8px;
    border-radius: var(--r-pill);
    white-space: nowrap;
    flex: none;
}

.ostate--new  { background: var(--check-bg); color: var(--check); }
.ostate--on   { background: var(--surface-2); color: var(--ink); }
.ostate--done { background: var(--surface-2); color: var(--meta); }
.ostate--off  { background: var(--surface-2); color: var(--meta); text-decoration: line-through; }

/* An order row has no picture, so it does not want the products list's 72px frame. */
.line--order { align-items: stretch; }

.orow__top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.orow__who {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.orow__foot { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }

.olines, .oshort { margin: 0 0 8px; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }

/* Each action is its own form and its own press, so they stack rather than sit side by side:
   confirming moves stock and refusing does not, and they must not be a thumb's width apart. */
.page > form { margin-bottom: 8px; }

/* Removing an option is asked for rather than inferred from a cleared row - see the comment in
   Editor/Product.cshtml. Kept quiet: it is far rarer than pricing or counting an option, and must
   never be the easiest thing on the row to hit by accident. */
.optrow__remove {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    color: #B3261E;
    font-size: 14px;
}

.optrow__remove input { width: 22px; height: 22px; }

.optrow__line { display: flex; gap: 8px; align-items: center; }

.optrow__line select,
.optrow__line input {
    min-height: 44px;
    border: 1.5px solid var(--border);
    border-radius: var(--r-md);
    padding: 8px 10px;
    background: var(--white);
    min-width: 0;
}

.optrow__line select { flex: 1.4; }
.optrow__line input { flex: 1; width: 100%; }

/* The colour row scrolls rather than wrapping into four ragged lines on a 360px screen. */
.swatches {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding-bottom: 2px;
    scrollbar-width: none;
}

.swatches::-webkit-scrollbar { display: none; }

.swatchpick {
    flex: none;
    width: 32px;
    height: 32px;
    border-radius: 16px;
    border: 2px solid transparent;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

/* 32px drawn, 44px touchable — the swatches would otherwise be the smallest targets on the page. */
.swatchpick::after { content: ""; position: absolute; inset: -6px; }

.swatchpick--on,
.swatchpick:has(input:checked) { border-color: var(--ink); }

.swatchpick .swatch { width: 22px; height: 22px; border-radius: 11px; }

.swatchpick__none {
    width: 22px;
    height: 22px;
    border-radius: 11px;
    border: 1px solid var(--border-2);
    background:
        linear-gradient(to bottom right, transparent calc(50% - 1px),
        var(--muted) calc(50% - 1px), var(--muted) calc(50% + 1px), transparent calc(50% + 1px)),
        var(--white);
}

.field select {
    min-height: 48px;
    border-radius: var(--r-md);
    border: 1.5px solid var(--border);
    background: var(--white);
    padding: 12px 14px;
    width: 100%;
}

/* ---------- totals ---------- */

.totals { display: flex; flex-direction: column; gap: 8px; font-size: 14px; color: var(--text-2); }
.totals__row { display: flex; justify-content: space-between; gap: 12px; }

.totals__total {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding-top: 10px;
    border-top: 1px solid var(--border);
    color: var(--ink);
}

.totals__total b { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }

/* ---------- payment note ---------- */

.paynote {
    background: var(--white);
    border-radius: var(--r-xl);
    border: 1px solid var(--border);
    padding: 12px 14px;
    display: flex;
    gap: 12px;
    align-items: flex-start;
    font-size: 13px;
    line-height: 1.45;
    color: var(--text-2);
}

.paynote strong { color: var(--ink); }
.paynote__marks { display: flex; gap: 4px; flex: none; margin-top: 2px; }

.paymark {
    width: 22px;
    height: 22px;
    border-radius: var(--r-sm);
    font-size: 7px;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
}

.paymark--mtn { background: #FFCC00; color: #1B1916; }
.paymark--airtel { background: #E4002B; color: #FFFFFF; font-size: 6px; }
.paymark--cash { background: var(--surface-2); color: var(--text-2); }

/* ---------- forms ---------- */

.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: 14px; font-weight: 600; }

.field input, .field textarea {
    min-height: 48px;
    border-radius: var(--r-md);
    border: 1.5px solid var(--border);
    background: var(--white);
    padding: 12px 14px;
    outline: none;
    width: 100%;
}

.field input:focus, .field textarea:focus { border-color: var(--brand); }
.field textarea { min-height: 88px; resize: vertical; }

.field__error { font-size: 12px; color: #B3261E; font-weight: 600; }

/* ---------- empty and loading ---------- */

.empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    text-align: center;
    padding: 48px 24px;
}

.empty__glyph {
    width: 64px;
    height: 64px;
    border-radius: var(--r-xl);
    background: var(--surface-2);
    display: flex;
    align-items: center;
    justify-content: center;
}

.skeleton {
    background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 50%, var(--surface-2) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.4s linear infinite;
    border-radius: var(--r-lg);
}

@keyframes shimmer {
    from { background-position: 200% 0; }
    to { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .skeleton { animation: none; }
}

/* ---------- footer note ---------- */

.footnote {
    margin-top: 20px;
    padding: 12px var(--gutter);
    background: var(--white);
    border-top: 1px solid var(--border);
    display: flex;
    gap: 10px;
    align-items: center;
    font-size: 12px;
    color: var(--text-2);
    line-height: 1.35;
}

.footnote__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand); flex: none; }

.legal {
    padding: 16px var(--gutter) 24px;
    display: flex;
    gap: 16px;
    justify-content: center;
    font-size: 12px;
    color: var(--meta);
}

.legal a { min-height: 44px; display: flex; align-items: center; }

/* The two footer rows together. Scoped so the other layouts' .legal keeps its own spacing. */
.foot .legal { padding-bottom: 2px; }

/*
  "Powered by myduka.ai". The quietest thing on the page on purpose: a storefront belongs to the
  merchant, and their shop name is the only name that should carry weight on it.
*/
.poweredby {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    min-height: 44px;
    padding: 0 var(--gutter) 20px;
    font-size: 11.5px;
    color: var(--meta);
    text-decoration: none;
}

/* block, or the inline baseline leaves a gap under the logo that reads as a misalignment. */
.poweredby img { display: block; opacity: 0.8; }

@media (hover: hover) {
    .poweredby:hover img { opacity: 1; }
}

/* ---------- WhatsApp message preview (artboard 1e) ---------- */

.wa-preview { background: #E5DDD5; border-radius: var(--r-lg); padding: 16px; }

.wa-bubble {
    background: #DCF8C6;
    border-radius: 10px 0 10px 10px;
    padding: 10px 12px;
    font-size: 13px;
    line-height: 1.5;
    white-space: pre-line;
    color: #1B1916;
    margin-left: 24px;
}

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* ---------- product options: pick sizes, pick colours, price the combinations ----------

   The merchant thinks in three steps and the section is now built as three: which sizes, which
   colours, then what each combination costs. The combinations are written by the page as the first
   two are answered, because a shop stocking four sizes in three colours should not be twelve
   separate acts of data entry.

   Everything here reuses the chips and swatches already defined above. What is new is the affixed
   number field and the combination row, both of which exist to answer questions a bare number box
   left open: which currency, and how many of what. */

.picker { display: flex; flex-direction: column; gap: 8px; }

.picker__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
}

.picker__count { font-size: 13px; color: var(--text-2); }

/* An empty size list is a consequence of not having chosen a category, and silence there reads as a
   broken page. */
.picker__empty {
    font-size: 14px;
    color: var(--text-2);
    background: var(--surface-3);
    border: 1px dashed var(--border-2);
    border-radius: var(--r-md);
    padding: 12px;
}

/* A number that means money or quantity, with the unit attached to the field rather than floating
   in a placeholder that vanishes the moment you type. */
.affix {
    display: flex;
    align-items: stretch;
    border: 1.5px solid var(--border);
    border-radius: var(--r-md);
    background: var(--white);
    overflow: hidden;
    min-width: 0;
    flex: 1;
}

.affix:focus-within { outline: 2px solid var(--brand); outline-offset: 1px; }

.affix__unit {
    display: flex;
    align-items: center;
    padding: 0 10px;
    background: var(--surface-2);
    color: var(--text-2);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
}

.affix__unit--trail { border-left: 1px solid var(--border); }
.affix__unit:not(.affix__unit--trail) { border-right: 1px solid var(--border); }

/* Wins over .optrow__line input, which would otherwise draw a second border inside the frame. */
.optrow__line .affix input,
.combo__line .affix input,
.affix input {
    border: 0;
    background: transparent;
    min-height: 44px;
    padding: 8px 10px;
    width: 100%;
    min-width: 0;
}

.affix input:focus { outline: none; }

/* One stocked combination. The size and colour are settled by the pickers above, so they are shown
   rather than re-asked; only the price and the count are still open questions. */
.combo {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 12px;
    border-bottom: 1px solid var(--border);
}

.combo:last-child { border-bottom: none; }

.combo__head { display: flex; align-items: center; gap: 8px; }

.combo__name { font-weight: 600; font-size: 15px; flex: 1; min-width: 0; }

.combo__swatch {
    width: 20px;
    height: 20px;
    border-radius: 10px;
    flex: none;
    border: 1px solid rgba(27, 25, 22, .14);
}

.combo__drop {
    flex: none;
    min-height: 44px;
    min-width: 44px;
    border: 0;
    background: transparent;
    color: var(--text-2);
    font-size: 20px;
    line-height: 1;
    border-radius: var(--r-md);
    cursor: pointer;
}

.combo__drop:hover { color: var(--ink); background: var(--surface-2); }

.combo__line { display: flex; gap: 8px; align-items: stretch; }

/* Applies one price and one count to every row, so a merchant who charges the same for all of them
   answers once. */
.applyall {
    display: flex;
    gap: 8px;
    align-items: stretch;
    padding: 12px;
    border-bottom: 1px solid var(--border);
    background: var(--surface-3);
    flex-wrap: wrap;
}

.applyall .affix { flex: 1 1 120px; }

.applyall__go {
    flex: none;
    min-height: 44px;
    padding: 0 14px;
    border-radius: var(--r-md);
    border: 1.5px solid var(--border-2);
    background: var(--white);
    font-weight: 600;
    cursor: pointer;
}

.applyall__go:hover { border-color: var(--ink); }

/* ---------- work in progress ----------

   The editor swaps pages without reloading, which removed the browser's own loading indicator and
   replaced it with nothing: a merchant pressed "Add product", the photo uploaded for a few seconds,
   and the page sat still. This is the bar that says something is happening. The pressed button also
   changes its own label, which is the part people actually look at. */

body[aria-busy="true"]::before {
    content: "";
    position: fixed;
    inset: 0 0 auto 0;
    height: 3px;
    z-index: 50;
    background: linear-gradient(90deg, transparent, var(--brand), transparent);
    background-size: 40% 100%;
    background-repeat: no-repeat;
    animation: duka-progress 1.1s ease-in-out infinite;
}

@keyframes duka-progress {
    0%   { background-position: -40% 0; }
    100% { background-position: 140% 0; }
}

/* A moving bar is exactly what some people cannot tolerate; the bar stays, the movement goes. */
@media (prefers-reduced-motion: reduce) {
    body[aria-busy="true"]::before {
        animation: none;
        background: var(--brand);
        opacity: .6;
    }
}

button[disabled] { cursor: progress; }

/* Secondary next to the share actions: it is a step, not the headline. */
.cta--inline {
    /* Sized to its label, not to the space around it. .cta is flex: 1 for the row it usually sits
       in beside another button; in a page laid out as a column, that made a lone ghost button grow
       to fill the height of the screen - a 200px box with four words at the top of it. */
    flex: none;
    width: auto;
    align-self: flex-start;
    padding: 0 18px;
    min-height: 44px;
}

/* ---------- drafts: what a model proposed, waiting for a person ---------- */

/*
   The review screen is the add-product form with three things added: the photograph it was read
   from, a note when the reading needs explaining, and a mark on every field the merchant should
   look at twice. Everything else - the fields, the pickers, the swatches - is the same markup and
   the same CSS, because a merchant should not have to learn a second form.

   The mark is warm, not red. Nothing has gone wrong; the merchant is simply the one who decides,
   and a screen they see twenty times in a row should not look like twenty errors.
*/

:root {
    /* 5.75:1 on its own background, so the chip is readable rather than decorative. */
    --check: #7A5A12;
    --check-bg: #FAF3E2;
    /* 3.1:1 on white, against the ordinary field border's 1.3:1 - a flagged field has to be
       distinguishable from an unflagged one at arm's length in daylight, not just up close. */
    --check-line: #B08E3C;
}

/* A block of content in the page gutter that does not claim the remaining height, unlike .page.
   Used for the notices that sit above the form: an error, a confirmation, the reading progress. */
.strip {
    padding: 14px var(--gutter) 0;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* Prose inside a panel. .panel itself is deliberately unpadded - it holds .line rows that carry
   their own - so text put straight into one sits on the border, which is what it did. */
.panel--pad { padding: var(--gutter); }

.progress {
    height: 6px;
    border-radius: var(--r-pill);
    background: var(--surface-2);
    overflow: hidden;
}

.progress__bar {
    height: 100%;
    background: var(--brand);
    border-radius: var(--r-pill);
    transition: width 400ms ease;
}

@media (prefers-reduced-motion: reduce) {
    .progress__bar { transition: none; }
}

/* One tick per product in the batch, filled as the merchant works through it. A bar rather than
   "3 of 14" alone: the count is in the header, and this says how much is left without reading. */
.stepper-rail {
    display: flex;
    gap: 3px;
    padding: 10px var(--gutter) 0;
}

.stepper-rail__tick {
    flex: 1;
    height: 3px;
    border-radius: var(--r-pill);
    background: var(--border);
}

.stepper-rail__tick--done { background: var(--brand); }
.stepper-rail__tick--now { background: var(--ink); }

.draft__photo {
    border-radius: var(--r-lg);
    overflow: hidden;
    background: var(--surface-2);
    border: 1px solid var(--border);
    aspect-ratio: 4 / 5;
    max-height: 280px;
}

.draft__photo img { width: 100%; height: 100%; object-fit: contain; }

/* What the model could not do, in the merchant's terms. Sits above the fields it explains. */
.draft__note {
    font-size: 13px;
    color: var(--text-2);
    background: var(--surface-3);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: 10px 12px;
    margin: 0;
}

.draft__flag {
    font-size: 11px;
    font-weight: 600;
    color: var(--check);
    background: var(--check-bg);
    border: 1px solid var(--check-line);
    border-radius: var(--r-pill);
    padding: 1px 7px;
    margin-left: 6px;
    white-space: nowrap;
}

.field--check input,
.field--check select,
.field--check textarea { border-color: var(--check-line); background: #FFFDF8; }

/* Two buttons that mean opposite things, stacked so they are never close enough to mistap. */
.decide { display: flex; flex-direction: column; gap: 10px; }

/* ---------- choosing files ---------- */

/*
   A browser's own file input renders as "Choose Files / No file chosen" in the platform's button
   style, which on this screen was the only control that did not look like the rest of the shop and
   the only one that never said what had been picked. The real input is still there and still does
   the work; it is just behind a panel the whole width of the thumb.
*/

.dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 22px var(--gutter);
    border: 1.5px dashed var(--border-2);
    border-radius: var(--r-lg);
    background: var(--white);
    text-align: center;
    cursor: pointer;
}

.dropzone:focus-within {
    border-color: var(--brand);
    border-style: solid;
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

.dropzone__glyph {
    width: 26px;
    height: 22px;
    border: 1.5px solid var(--muted);
    border-radius: var(--r-sm);
    position: relative;
}

/* A photograph: a horizon and a sun, drawn rather than shipped as an image. */
.dropzone__glyph::before {
    content: "";
    position: absolute;
    inset: auto 3px 3px 3px;
    height: 7px;
    border-radius: 2px 2px var(--r-sm) var(--r-sm);
    background: var(--muted);
}

.dropzone__glyph::after {
    content: "";
    position: absolute;
    top: 4px;
    right: 5px;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--muted);
}

/* A file: one corner turned. */
.dropzone__glyph--file { width: 20px; height: 24px; }
.dropzone__glyph--file::before { inset: auto 4px 4px 4px; height: 2px; border-radius: 0; }
.dropzone__glyph--file::after {
    top: 0;
    right: 0;
    width: 0;
    height: 0;
    border-radius: 0;
    background: none;
    border-top: 7px solid var(--surface-2);
    border-left: 7px solid transparent;
}

.dropzone__label { font-weight: 600; }
.dropzone__hint { font-size: 12px; color: var(--meta); }

/*
    Something has been picked.

    The panel used to say so in words - "9 photos chosen" - under a border that never changed, and a
    merchant who was not already reading that line had no way to tell their tap had landed. So the
    panel takes the news itself: solid instead of dashed, brand-coloured, and the outline drawing
    becomes a tick. The words stay for a screen reader, where a colour says nothing.
*/
.dropzone--loaded {
    border-style: solid;
    border-color: var(--brand);
    background: var(--surface);
}

.dropzone--loaded .dropzone__glyph {
    width: 22px;
    height: 22px;
    border: none;
    border-radius: 50%;
    background: var(--brand);
}

/* The tick: two sides of a box, turned. */
.dropzone--loaded .dropzone__glyph::before {
    content: "";
    position: absolute;
    inset: 4px auto auto 7px;
    width: 5px;
    height: 9px;
    border: solid var(--white);
    border-width: 0 2px 2px 0;
    border-radius: 0;
    background: none;
    transform: rotate(45deg);
}

.dropzone--loaded .dropzone__glyph::after { content: none; }

/* The photographs themselves, which is the part that needs no explaining at all. */
.dropzone__previews {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 6px;
    margin-top: 10px;
}

.dropzone__previews img {
    width: 44px;
    height: 44px;
    object-fit: cover;
    border-radius: var(--r-sm);
    border: 1px solid var(--border-2);
    background: var(--surface-2);
}

.dropzone__more {
    min-width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 6px;
    border-radius: var(--r-sm);
    background: var(--surface-2);
    color: var(--meta);
    font-size: 13px;
    font-weight: 700;
}

/* A worked example of the file we are asking for, shown rather than described. Scrolls on its own
   rather than wrapping: a CSV line broken across three lines stops looking like a CSV line. */
.sample {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 10px 12px;
    border-radius: var(--r-md);
    background: var(--surface-2);
    overflow-x: auto;
}

.sample__row {
    font-family: ui-monospace, "Cascadia Mono", "Roboto Mono", monospace;
    font-size: 12px;
    color: var(--text-2);
    white-space: nowrap;
}

.sample__row--head { color: var(--ink); font-weight: 600; }

/* Separates two alternatives. Not a shadow and not a card - a line, like everything else here. */
.rule {
    border: none;
    border-top: 1px solid var(--border);
    margin: 2px 0;
}

/*
    The bar that offers checkout while a shopper is still browsing.

    Fixed to the foot of the screen rather than placed in the flow, because the moment it matters is
    the moment something was added - which on a menu happens without the page moving at all. It
    carries the count so a shopper can see what they are going to, and it is absent entirely while
    the cart is empty: a shop should not ask for payment from someone who has chosen nothing.
*/
.tocart {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: max(12px, env(safe-area-inset-bottom));
    z-index: 9;
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 48px;
    padding: 0 18px;
    border-radius: var(--r-xl);
    background: var(--brand);
    color: var(--on-brand);
    font-size: 15px;
    font-weight: 700;
    box-shadow: 0 6px 20px rgba(27, 25, 22, 0.22);
    max-width: calc(100% - var(--gutter) * 2);
}

.tocart__count {
    flex: none;
    min-width: 22px;
    height: 22px;
    padding: 0 6px;
    border-radius: 11px;
    background: var(--on-brand);
    color: var(--brand);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
}

.tocart__label { white-space: nowrap; }

/*
    Room for it. The bar floats, so without this the last row of a menu sits under it permanently -
    and the last row is a dish somebody might want. A following-sibling :has, because the bar is
    drawn after the page it belongs to and CSS cannot look backwards; a browser too old for :has
    simply keeps today's behaviour rather than getting a gap under an empty cart.
*/
.page:has(~ .tocart:not([hidden])) { padding-bottom: 76px; }

/* An arrow, drawn: two sides of a box, turned. */
.tocart__go {
    flex: none;
    width: 7px;
    height: 7px;
    border: solid var(--on-brand);
    border-width: 2px 2px 0 0;
    transform: rotate(45deg);
}

/* "from", before a shopper has picked which option they want. Smaller and quieter than the number
   it qualifies: it is a caveat on the price, not part of it. */
.t-price-from {
    font-size: 0.55em;
    font-weight: 600;
    color: var(--meta);
    margin-right: 4px;
    vertical-align: 0.28em;
}


/*
    Back one, inside the draft review. A text link with the same glyph the header uses, so it reads
    as "back" without being the back that leaves.
*/
.stepback {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    padding: 0 var(--gutter);
    font-size: 13px;
    font-weight: 600;
    color: var(--meta);
}

/*
    The go-live prompt on the products screen. A card, because it is the one thing on that screen
    that is not a product; bolder in the moment products have just arrived.
*/
.golive {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 14px;
    border: 1px solid var(--brand);
    border-radius: var(--r-md);
    background: var(--surface);
}

.golive--fresh { border-width: 2px; }

.golive__said { display: flex; flex-direction: column; gap: 4px; }

.golive__title { font-size: 16px; font-weight: 700; }

/* The pay-and-go-live screen. */
.golive-hero { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }

.golive-hero__mark {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: var(--brand);
    position: relative;
}

/* A tick: two sides of a box, turned. */
.golive-hero__mark::before {
    content: "";
    position: absolute;
    inset: 11px auto auto 16px;
    width: 9px;
    height: 17px;
    border: solid var(--on-brand);
    border-width: 0 3px 3px 0;
    transform: rotate(45deg);
}

.golive-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.golive-list li { display: flex; align-items: center; gap: 10px; font-size: 14px; }

.golive-list__tick {
    flex: none;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--surface-2);
    position: relative;
}

.golive-list__tick::before {
    content: "";
    position: absolute;
    inset: 5px auto auto 8px;
    width: 5px;
    height: 9px;
    border: solid var(--brand);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

.golive-fee {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 18px;
    border: 1px solid var(--border-2);
    border-radius: var(--r-lg);
    background: var(--white);
    text-align: center;
}

.golive-fee__label { font-size: 13px; font-weight: 600; color: var(--meta); }

.golive-fee__amount { font-size: 32px; font-weight: 800; line-height: 1.1; }

.golive-note { margin: 0; }

/*
    WhatsApp's own green, on the one button that opens WhatsApp. Fixed rather than a token: it is
    that company's colour and does not change with the shop's theme.
*/
.cta--whatsapp { background: #25D366; color: #fff; }

.share-row { display: flex; gap: 10px; }

.share-row .cta { flex: 1; }

.share__glyph { flex: none; }


/*
    The opening-hours picker, shared by signup and settings.

    Seven boxes drawn as chips, so a week is seen at a glance rather than read out of a dropdown;
    the presets under them are shortcuts. The per-day rows sit in a disclosure that opens itself
    when a day already has its own hours, and stays shut for the shop that has none.
*/
.dayset { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }

.dayset__legend { padding: 0; font-size: 14px; font-weight: 600; }

.daychips { display: flex; flex-wrap: wrap; gap: 6px; }

.daychip { position: relative; }

.daychip input { position: absolute; opacity: 0; width: 1px; height: 1px; }

.daychip span {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 40px;
    padding: 0 10px;
    border: 1.5px solid var(--border);
    border-radius: var(--r-xl);
    background: var(--white);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}

.daychip input:checked + span { background: var(--brand); color: var(--on-brand); border-color: var(--brand); }

.daychip input:focus-visible + span { outline: 2px solid var(--brand); outline-offset: 2px; }

.daypresets { display: flex; flex-wrap: wrap; gap: 6px; }

.daypreset {
    min-height: 32px;
    padding: 0 10px;
    border-radius: var(--r-xl);
    background: var(--surface-2);
    color: var(--text-2);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
}

.dayshared { display: flex; gap: 10px; }

.dayshared .field { flex: 1; }

/*
    The per-day hours, in a disclosure.

    It sat flush against the Opens/Closes row above it and gave no sign it could be opened at all:
    `display: flex` on a summary removes the browser's own disclosure triangle, and nothing replaced
    it. So there is room above it now, a tinted bar that reads as something to press, and a chevron
    that turns over when it opens.
*/
.dayown {
    margin-top: 14px;
    border: 1px solid var(--border-2);
    border-radius: var(--r-md);
    background: var(--white);
    overflow: hidden;
}

.dayown summary {
    min-height: 48px;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    background: var(--surface-2);
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

/* Safari draws its own triangle from this, on top of the chevron below. */
.dayown summary::-webkit-details-marker { display: none; }

.dayown summary:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* The chevron: two sides of a box, turned. Down when shut, up when open. */
.dayown summary::after {
    content: "";
    margin-left: auto;
    flex: none;
    width: 8px;
    height: 8px;
    margin-bottom: 3px;
    border: solid var(--meta);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
    transition: transform 150ms ease;
}

.dayown[open] summary::after { transform: rotate(-135deg); margin: 3px 0 0; }

.dayown[open] summary { border-bottom: 1px solid var(--border-2); }

.dayown__hint { margin: 12px 12px 8px; }

.dayown__row { display: flex; align-items: center; gap: 8px; padding: 6px 12px; }

/* Room under the last row, which otherwise sits on the container's own edge. */
.dayown__row:last-of-type { padding-bottom: 14px; }

.dayown__label { flex: none; width: 34px; font-size: 13px; font-weight: 600; }

.dayown__row select { flex: 1; min-width: 0; }


/* The photographs already added to a draft on the review screen, at thumbnail size. */
.draft__extras { display: flex; flex-wrap: wrap; gap: 6px; }

.draft__extras img {
    width: 56px;
    height: 56px;
    object-fit: cover;
    border-radius: var(--r-sm);
    border: 1px solid var(--border-2);
    background: var(--surface-2);
}


/*
    The editor home's links, on a row of their own under the header. They shared the header with the
    title and at phone width squeezed it to "Your…"; a row can hold three words and a count.
*/
.editor-nav {
    display: flex;
    gap: 4px;
    padding: 2px var(--gutter);
    background: var(--white);
    border-bottom: 1px solid var(--border-2);
    overflow-x: auto;
}

.editor-nav__link {
    flex: none;
    min-height: 44px;
    padding: 0 10px;
    display: flex;
    align-items: center;
    font-size: 14px;
    font-weight: 600;
    color: var(--brand);
    white-space: nowrap;
}


/*
    The shop's colour, as colours.

    Nine of these drawn as radio rows would be most of a phone screen spent on one question, and the
    swatch - the only part a merchant is actually reading - sat at the end of each row. A grid puts
    the answer first: the chosen one is ringed in its own colour and ticked in whatever the theme
    already works out is readable on top of it.
*/
.themeset { border: 0; margin: 0; padding: 0; }

.themeset__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    gap: 10px;
}

.themepick { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; }

.themepick input { position: absolute; opacity: 0; width: 1px; height: 1px; }

.themepick__dot {
    flex: none;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    position: relative;
    box-shadow: 0 0 0 1px rgba(27, 25, 22, 0.12) inset;
}

/* The ring, drawn outside the dot so it does not shrink the colour it is marking. */
.themepick input:checked + .themepick__dot {
    box-shadow: 0 0 0 2px var(--white), 0 0 0 4px var(--ring, var(--ink));
}

.themepick input:focus-visible + .themepick__dot {
    outline: 2px solid var(--ink);
    outline-offset: 4px;
}

/* The tick: two sides of a box, turned, in the theme's own on-brand colour. */
.themepick input:checked + .themepick__dot::after {
    content: "";
    position: absolute;
    inset: 15px auto auto 19px;
    width: 7px;
    height: 13px;
    border: solid var(--tick, #FFFFFF);
    border-width: 0 2.5px 2.5px 0;
    transform: rotate(45deg);
}

.themepick__name { font-size: 12px; font-weight: 600; color: var(--meta); }

.themepick input:checked ~ .themepick__name { color: var(--ink); }
