/* ==========================================================================
   Preheat: rules that belong to one surface, not to a component
   Palette and component shapes come from the product demo at
   marketing/landing/public/demo.html. If a screen here and that demo
   disagree, the demo wins.
   ========================================================================== */

/* ---------- Layout ---------- */

/* The baker app is phone-shaped by default: one column, bottom bar. At 900px a
   sidebar appears beside it and the bar goes away. Inside a Hotwire Native
   shell the server renders neither, because the shell draws its own. */
.app {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--cream);
}

.app__frame {
  width: 100%;
  max-width: var(--app-width);
  margin: 0 auto;
  flex: 1;
  display: flex;
  flex-direction: column;
  background: var(--cream);
  border-inline: 1px solid var(--line);
}

@media (max-width: 500px) {
  .app__frame {
    border-inline: 0;
  }
}

/* ---------- Sidebar (wide screens only) ---------- */

.sidebar {
  display: none;
}


.sidebar__brand {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 10px 14px;
  font-family: var(--display);
  font-size: 21px;
  color: var(--ink);
}
.sidebar__brand:hover {
  color: var(--ink);
}

.sidebar__nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sidebar__item {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 12px;
  border-radius: var(--radius-chip);
  font-size: 14.5px;
  font-weight: 700;
  color: var(--ink-soft);
}
.sidebar__item svg {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}
.sidebar__item:hover {
  background: var(--cream-deep);
  color: var(--ink);
}
.sidebar__item[aria-current='page'] {
  background: var(--raspberry-tint);
  color: var(--raspberry-deep);
}

.sidebar__shop {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: auto;
  padding: 10px;
  border-top: 1px solid var(--glass-line);
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ink-soft);
  min-width: 0;
}
.sidebar__shop:hover {
  color: var(--ink);
}
.sidebar__shop-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px var(--gutter) 8px;
}

.app-bar__brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--display);
  font-size: 20px;
  color: var(--ink);
}
.app-bar__brand:hover {
  color: var(--ink);
}

/* rivet:context: 44px because it is a link, 32px of paint because that is what
   the app bar is drawn around. The account avatar is the anchor itself, so the
   44px floor would have grown the gradient circle with the target;
   content-box clipping keeps the circle at 32 inside a 44 box, and the bar looks
   identical.

   background-origin as well as background-clip. A background is sized to the
   padding box by default, so clipping alone paints the middle of a 44px
   gradient into a 32px hole and the circle comes out a flat pink instead of a
   gradient.

   Both AFTER the `background` shorthand, which is not a style preference: the
   shorthand resets every background-* longhand it does not name, so declaring
   them above it silently undoes them. That is how the first version of this
   shipped a 44px gradient circle into the app bar, looking exactly like a
   design decision.

   The floor itself is in components.css; this is the one control that could
   not simply take it. */
.avatar {
  width: 44px;
  height: 44px;
  padding: 6px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--avatar-from), var(--raspberry));
  background-origin: content-box;
  background-clip: content-box;
  color: var(--on-accent);
  font-size: 13px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.screen {
  flex: 1;
  padding: 4px var(--gutter) 28px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.screen__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.stack--tight {
  gap: 6px;
}
.stack--loose {
  gap: 18px;
}

.row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.row--between {
  justify-content: space-between;
}
.row--wrap {
  flex-wrap: wrap;
}

.spacer {
  margin-top: auto;
}

/* rivet:context: This looks like dead code and is the opposite. `display:
   contents` removes the wrapper's own box, so on a phone the children of a
   .split are laid out as if the wrapper were not there: they stay direct flex
   items of .screen and keep its gap, exactly as they did before anyone wrapped
   them. That is what lets wide.css turn the same markup into two columns
   without the phone ever seeing a container. Change this to `block` or delete
   it and every screen with a rail silently grows a nested box on mobile.

   Two rules follow from it. They are conventions, not checks: an earlier
   version of this note said a script enforced them and no such script exists.

   .split and .split__sticky go on plain <div>s with no role, no aria-* and no
   semantic tag name. `display: contents` removes the box but not the node, and
   on a <li> it takes the list marker with it.

   DOM order IS mobile order. A rail is a re-position at width, never a
   re-order, so rail markup must be written where it belongs in the phone's
   reading order. Nothing in CSS can check that, which is why the rule is
   instead: a screen may not gain a .split unless it is in the 390px baseline,
   where reordering it shows up as a changed checksum.

   Declared here rather than in wide.css because the storefront loads this file
   and not that one, so a .split on a customer page would otherwise be
   permanently boxless with no rule ever to make it a grid. There are none
   today, and a customer surface wanting one is a sign the split belongs in
   components.css instead. */
.split,
.split__sticky,
.split__stack {
  display: contents;
}

/* Chrome that only makes sense with a mouse and a wide window. Rendered
   always and hidden below the desk tier, rather than rendered only there,
   because the alternative is a server-side width guess and there is not one.
   Use it sparingly: anything a phone also wants belongs in the .split, which
   re-positions rather than hides. */
.desk-only {
  display: none;
}

/* Anchor targets inside a long form. Without this a jumped-to field sits flush
   against the top of the frame with its label above the fold. */
.field[id] {
  scroll-margin-top: 26px;
}

/* .split__stack is the non-rail half of a .split whose content is several
   peer sections rather than one form. Same display: contents mechanism as
   .split__sticky, so the phone never sees a box; wide.css gives it the flex
   column the sticky half already has, minus the sticking. It replaced
   .panels, a multicol layout that balanced sections by height: the balance
   was tuned for five sections and read as broken the moment two more grew
   under it, which is what a layout owned by the browser does over time. */

/* A set of peers you tap into: a DM thread, a customer, a recipe. On a phone
   this is exactly what a .stack was for these screens before it had a name, so
   a screen can gain one with no mobile consequence; wide.css tiles it once
   there is room. Declared here rather than in wide.css, which may only vary
   classes that already exist. */
.card-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
}

/* ---------- Tab bar ---------- */

/* Deliberately Material 3's navigation bar, because that is literally what the
   Android shell draws: a BottomNavigationView with labelVisibilityMode
   "labeled" and a raspberry-tint active indicator, in
   native/android/app/src/main/res/layout/activity_main.xml. A baker who uses
   the web and the app should see one navigation, not two, and the metrics are
   what do that rather than the colours: 80px tall, a 24px icon centred in a
   64x32 pill, a 12px label 4px under it. Changing any of those three numbers
   here without changing the shell is what makes them look like two products. */

/* rivet:context: the shell's bar is opaque and this one is not, and that is
   the one difference on purpose. Android constrains the web view to sit above
   the nav, so nothing ever passes behind it; here the bar is sticky over a
   scrolling page, and the blur is what stops a heading colliding with a label
   as it goes under. Match the shell here and the bar reads as a hole in the
   page. */
.tab-bar {
  position: sticky;
  bottom: 0;
  z-index: 20;
  display: grid;
  /* However many destinations there are, on one row. shop_nav_items returns
     four or five depending on whether the shop has Instagram connected, and a
     hardcoded repeat(4, 1fr) wrapped the fifth onto a second row: the bar was
     two items high for every shop with a connected account. Auto columns mean
     the next destination added cannot bring that back. */
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-top: 1px solid var(--glass-line);
  padding-bottom: env(safe-area-inset-bottom);
}

/* 12 + 32 + 4 + 16 + the label is Material's 80dp, and the safe-area inset
   rides on top of it the same way the shell's window insets do. */
.tab-bar__item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 12px 0 16px;
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--muted);
}

/* rivet:context: Out of flow, and that is not decoration. In the column with
   the icon and the label the count added 12px to every tab, so the whole bar
   grew and the sticky Save on the settings form, which clears the bar by a
   measured amount, ended up 12px underneath it. spec/system/sticky_save_spec.rb
   is what said so.

   On the corner of the icon is also where a badge belongs: it is a property of
   the destination, not a third line of it. */
.tab-bar__item .nav-count {
  position: absolute;
  top: 6px;
  left: 50%;
  margin-left: 6px;
}

/* In the sidebar there is room for it in flow, at the far end of the row, the
   way a mail client puts a folder's count. */
.sidebar__item .nav-count {
  margin-left: auto;
}

/* The pill IS the icon's own box: width to the column so five tabs still fit a
   320px screen, capped at Material's 64px, and 32px tall. The viewBox is 24x24
   and preserveAspectRatio defaults to xMidYMid meet, so the artwork renders at
   24x24 centred in whatever the box becomes rather than stretching with it.
   That is why there is no wrapper element around the svg. */
.tab-bar__item svg {
  box-sizing: border-box;
  width: 100%;
  max-width: 64px;
  height: 32px;
  padding: 4px 0;
  border-radius: 16px;
  transition: background-color 120ms ease;
}

/* rivet:context: gated on a real pointer, and it has to stay gated. This bar
   only exists below 900px, so almost everyone reaching it is touching it, and
   a touch browser leaves :hover stuck on the last thing tapped. Ungated, the
   tab you just came from keeps a grey pill while the one you are on has a
   pink one, which reads as two active tabs. The shell has no hover state at
   all for the same reason. */
@media (hover: hover) {
  .tab-bar__item:hover {
    color: var(--ink-soft);
  }
  .tab-bar__item:hover svg {
    background: var(--line-soft);
  }
}

.tab-bar__item[aria-current='page'] {
  color: var(--raspberry);
}
/* Beats the hover pill above rather than sitting beside it: hovering the
   current tab must not swap its indicator for the grey one. */
.tab-bar__item[aria-current='page'] svg,
.tab-bar__item[aria-current='page']:hover svg {
  background: var(--raspberry-tint);
}

/* ---------- The gate: sign in, register, forgot, reset ---------- */

/* Four screens, one shape, and the shape is an answer to a question the old
   ones did not ask: what is the wordmark for, and what is the form for.

   The wordmark is the sign on the door and stays on the cream. The card is the
   task and holds nothing but the task. The line under the card is the OTHER
   door. That split is why "Forgot your password?" now sits inside the card and
   "Create a shop" outside it, where the two of them used to be one line
   separated by a middot — which asked the reader to tell a recovery from a
   different destination by reading both. */

/* Centres the screen rather than the block inside it, so an alert ("that reset
   link has expired") rides down with the form it is about. Give .auth the auto
   margin instead and the flash stays pinned to the top of the viewport, a
   sentence on its own with nothing near it.

   rivet:context: `safe center`, not `center`, and it is what lets the
   registration screen join this family. Plain centring on a flex column pushes
   overflow out of the TOP of the scroll container, where it cannot be reached
   by scrolling; the registration form is about 640px tall and a laptop with
   devtools open is not. `safe` falls back to flex-start the moment the content
   stops fitting. Registration used to opt out of centring altogether and carry
   an inline `padding-top: 20px` for exactly this reason. */
/* `.app .screen:has(.auth)` rather than `.screen:has(.auth)`, and the leading
   class is not decoration. :has() takes the specificity of its most specific
   argument, so the bare selector is (0,2,0) — an exact tie with
   `.app--web .screen { padding-top: 22px }` further down this file, which means
   whichever is written second wins. This block sits ABOVE that one, so the
   padding here would lose at 900 and up and the rule would only look correct in
   whichever prototype loaded it last. (0,3,0) settles it wherever either moves. */
.app .screen:has(.auth) {
  justify-content: safe center;
  padding-block: 28px;
}

.auth {
  width: 100%;
  max-width: 380px;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* Signed out there is no sidebar and no app bar, so nothing on the screen is
   left-aligned for this to hang off. Centred reads as deliberate; ragged left
   reads as unfinished. The form inside the card is left-aligned, because a
   lockup is a mark and a form is read. */
.auth__brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
}

/* On registration the wordmark heads a block of prose rather than a card, so
   it lines up with the headline under it. A centred mark over a left-aligned
   sentence is the mismatch that reads as an accident. */
.auth--wide .auth__brand {
  justify-content: flex-start;
}

/* Every other screen in the baker app puts its content on a .card. The gate
   was the one place content sat straight on the background, and that is what
   made it read as a form floating in a room rather than as part of the
   product. It is also the cheapest possible fix: no new component. */
.auth__card {
  display: flex;
  flex-direction: column;
  gap: 18px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: 24px;
}

/* The gate is the only screen in the product with no data on it, which makes
   it the only place the display face gets to be the largest thing present. */
.auth__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: 26px;
  line-height: 1.1;
  letter-spacing: 0;
  color: var(--ink);
}

/* --ink-soft rather than --muted, for the same reason as .field__label--plain:
   on this screen the sentence under the title is the instruction, not chrome. */
.auth__lede {
  font-size: 14.5px;
  color: var(--ink-soft);
  max-width: 42ch;
}

/* Inside the card and under a hairline: still this task, no longer the form. */
.auth__recover {
  border-top: 1px solid var(--line-soft);
  padding-top: 16px;
  font-size: 13.5px;
}

/* The other door: outside the card, on the cream, where it cannot be mistaken
   for part of the form above it. */
.auth__intro {
  text-align: center;
  font-size: 13.5px;
  color: var(--ink-soft);
}

/* The gate used to carry a local colour override here, darkening its labels
   and help text to --ink-soft because --muted was 4.07:1 and --muted-2 was
   3.04:1 and neither passed AA. Its own note said to delete it the day the
   tokens moved. They have moved: --muted is 5.25:1 and --muted-2 is gone. */

/* "optional" beside a label is the one piece here that genuinely is secondary,
   and the fix is to say so with WEIGHT rather than with contrast. --faint is
   2.54:1 on white; there is no threshold below the floor for words that matter
   less. Hierarchy encoded in contrast is the thing that stops being hierarchy
   the moment it stops being legible. */
.auth .field__label .optional {
  color: var(--ink-soft);
  font-weight: 400;
  letter-spacing: 0;
}

/* The pitch. It exists on the registration screen only, because that is the
   only gate screen where the reader has not decided yet. On a phone it is
   simply what sits above the card. */
.auth__aside {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.auth__points {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 13px;
  font-weight: 700;
  color: var(--ink-soft);
}

.auth__points li {
  display: flex;
  align-items: center;
  gap: 7px;
}

/* A drawn dot rather than a ✓ glyph. The glyph is content, so a screen reader
   announces "check mark" before each of the three lines; a background has
   nothing to announce. */
.auth__points li::before {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--raspberry);
  flex-shrink: 0;
}

/* Same value the inline styles they replace carried. */
.thread-aside {
  margin-top: 20px;
}

/* Same value the inline style it replaces carried. */
.auto-reply__note {
  margin-top: 16px;
}

/* The two blocks beside the menu. Same values the inline styles carried. */
.menu-aside {
  margin-top: 10px;
}
.menu-aside__line {
  margin-top: 4px;
}

/* An order on a customer's history. Same values the inline styles carried. */
.history-card__meta {
  font-size: 13px;
  color: var(--muted);
  margin-top: 6px;
}
.history-card__money {
  color: var(--ink);
}

/* Same value the inline style it replaces carried, so the phone does not move. */
.recipe-card__line {
  margin-top: 4px;
}

/* The two people lists: a DM thread and a customer. Same values the inline
   styles they replace carried, so the phone does not move; they exist because
   an inline style beats every media query. */
.thread-card__name,
.person-card__name {
  font-size: 15px;
  font-weight: 800;
}
.thread-card__preview {
  color: var(--ink-soft);
  font-size: 14px;
  margin-top: 6px;
}
.person-card__meta {
  font-size: 13px;
  color: var(--muted);
  margin-top: 6px;
}

/* The order card on the board. These carry exactly the values that used to be
   inline on the same elements, so the phone rendering is unchanged; the point
   of moving them is that an inline style beats every media query, so the desk
   tier could not reach this card at all while they were there. */
.order-card__name {
  font-size: 15px;
  font-weight: 800;
}
.order-card__title {
  color: var(--ink-soft);
  font-size: 14px;
  margin-top: 6px;
}
.order-card__when {
  font-size: 13px;
  color: var(--muted);
  margin-top: 6px;
}
.order-card__money {
  color: var(--ink);
}
.order-card__age {
  margin-top: 4px;
}

/* The one line of a booked order: date, who, what. Its own class so the desk
   tier has something to hang a rule on, and so the size stops being an inline
   style no media query can reach. */
.booked__line {
  font-size: 13.5px;
}
.booked__deposit {
  font-size: 13px;
}

/* ---------- The calendar's week view ---------- */

/* A day row's head: the date on the left, its state on the right, ruled off
   from the orders beneath so seven days read as seven rows and not one list. */
.wk-day__head {
  padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
}
.wk-day__date {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
}

/* ---------- The form builder ---------- */

/* A question in the list: the whole row is a link to its editor, with the two
   reorder controls beside it. Reordering is buttons rather than dragging
   because this is a phone screen first, and a drag handle inside a scrolling
   column fights the scroll. */
.question {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 10px 8px 10px 14px;
}

.question__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
  color: var(--ink);
}
.question__body:hover {
  color: var(--ink);
}

.question__moves {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}
.question__moves form {
  margin: 0;
}
.question__moves .btn {
  padding: 2px 10px;
  font-size: 15px;
  line-height: 1.2;
}

/* Label, note and price on one line at width, stacked on a narrow phone where
   three inputs side by side leave no room to read what you typed. */
.option-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 6px;
  margin-bottom: 10px;
}

/* Above the phone-shaped column, not above some arbitrary phone width. The
   frame is --app-width until the sidebar appears, so a 430px handset would
   otherwise get three columns squeezed into 400px of gutter-to-gutter. */
@media (min-width: 520px) {
  .option-row {
    grid-template-columns: 2fr 2fr 1fr;
  }
}

/* ---------- The quote builder ---------- */

/* Description, quantity and unit price. Stacked on a phone, because a
   description squeezed into a third of 430px is unreadable while typing. */
.line-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 6px;
  margin-bottom: 10px;
}

/* rivet:context: The same min-width: auto trap that made the label sheet
   scroll sideways, and it bites hardest here. A grid item will not shrink
   below its content's intrinsic width, and a native date input has a wide one
   it refuses to give up, much wider on iOS Safari than in desktop Chrome.
   Inside a two-column pair on a phone that is what pushes the row out.
   
   Not reproducible in headless Chrome, which is why it is fixed by cause
   rather than by chasing a screenshot. */
.line-row > * {
  min-width: 0;
}

@media (min-width: 520px) {
  .line-row {
    grid-template-columns: 3fr 1fr 1.4fr;
  }
  .line-row--pair {
    grid-template-columns: 1fr 1fr;
  }
}

/* rivet:context: iOS sizes a date input from its own shadow content and will
   not go narrower, whatever width you declare, so it pushes its container out
   and the page scrolls sideways. It also gives itself a height and a vertical
   centring of its own, so beside a text input of the same declared size it
   sits a few pixels off.
   
   -webkit-appearance: none is what lets it accept a width, and an earlier
   version of this comment claimed it would cost the native picker. That was
   wrong: the picker is user-agent behaviour on the input type, not on its
   appearance, and tapping still opens it. Removing the declaration is what
   brings the overflow back. */
input[type="date"].input,
input[type="number"].input {
  -webkit-appearance: none;
  appearance: none;
  min-width: 0;
  max-width: 100%;
  width: 100%;
  height: 44px;
  line-height: normal;
  text-align: left;
}

/* Safari puts the clear and picker affordances in a shadow root that ignores
   the width above unless it is told to shrink too. */
input[type="date"].input::-webkit-date-and-time-value {
  text-align: left;
  min-width: 0;
}

/* An eyebrow used as a section break rather than as a caption directly above
   its content, so it needs air above it that the plain one does not want. */
.eyebrow--spaced {
  margin-top: 22px;
}

/* ---------- Customer pages ---------- */

/* A customer arrives once, cold, from a phone. Full-bleed white page, no app
   chrome, nothing to learn. */
.public {
  min-height: 100dvh;
  background: var(--surface);
  display: flex;
  flex-direction: column;
}

.public__frame {
  width: 100%;
  max-width: 540px;
  margin: 0 auto;
  flex: 1;
  display: flex;
  flex-direction: column;
  background: var(--surface);
}

.public__head {
  background: var(--cream);
  border-bottom: 1px solid var(--line);
  padding: 20px 26px;
}

.public__body {
  padding: 22px 26px 34px;
  display: flex;
  flex-direction: column;
  gap: 24px;
  flex: 1;
}

.public__foot {
  padding: 18px 26px 26px;
  text-align: center;
  font-size: 12px;
  color: var(--muted);
}

.confirmation {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 44px 30px;
  text-align: center;
}

.tick {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--sage);
  color: var(--on-accent);
  font-size: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.reference {
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: var(--radius-chip);
  padding: 12px 18px;
  font-size: 13.5px;
  color: var(--ink-soft);
}
.reference b {
  font-weight: 800;
  letter-spacing: 0.04em;
}

/* ---------- Menu page ---------- */

.menu-item {
  display: flex;
  gap: 14px;
  align-items: center;
  padding: 14px 16px;
}
.menu-item__thumb {
  width: 58px;
  height: 58px;
  border-radius: 12px;
  object-fit: cover;
  background: linear-gradient(135deg, var(--photo-from), var(--photo-mid) 55%, var(--photo-to));
  flex-shrink: 0;
}
.menu-item__body {
  flex: 1;
  min-width: 0;
}
.menu-item__name {
  font-size: 15px;
  font-weight: 800;
}
.menu-item__desc {
  font-size: 13.5px;
  color: var(--muted);
}

/* ---------- Glass fallbacks ---------- */

/* Translucency is a finish, never the thing that makes text legible. Where the
   blur cannot run, or where someone has asked for less of it, both surfaces go
   opaque rather than sitting half-transparent over scrolling content. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .tab-bar,
  .sidebar {
    background: var(--surface);
  }
  .tab-bar {
    border-top-color: var(--line);
  }
  .sidebar {
    border-right-color: var(--line);
  }
}

@media (prefers-reduced-transparency: reduce) {
  .tab-bar,
  .sidebar {
    background: var(--surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* ---------- Wide screens ---------- */

/* Not last in the file, and never was: 276 lines follow, including another
   media query. The real requirement is that this block comes AFTER .tab-bar
   and .app-bar, which it does. Put it before them and both navigations render
   at once.

   rivet:context: `screen and`, and it is not decoration. A media query in
   paged media evaluates against the page AREA, not the viewport, and the page
   area is margin-dependent: US Letter portrait is about 739px but LANDSCAPE is
   about 979px, so a bare `min-width: 900px` matches while printing. This block
   caps .app__frame and pads .screen, so on landscape paper it collapsed the
   invoice into a 560px column on the left half of the sheet and pushed
   everything down 22px. It survived only because @media print comes later and
   undoes both at equal specificity, which stopped being true the moment these
   selectors were scoped below and gained a class.

   rivet:context: .app--web, not .app. The class is emitted only outside a
   Hotwire Native shell. Ungated, every rule here reached the shell's web view
   at tablet width: a shell at 1280 was measured getting a 560px frame and 22px
   of top padding instead of 460px and 4px. The shells draw their own
   navigation, so nothing here is theirs. Scoping raises each selector by
   (0,1,0), which is why .app--solo below is compound rather than descendant. */
@media screen and (min-width: 900px) {
  .app--web {
    flex-direction: row;
    align-items: stretch;
  }

  .app--web .sidebar {
    position: sticky;
    top: 0;
    align-self: flex-start;
    height: 100dvh;
    width: var(--sidebar-width);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 18px 14px;
    background: var(--glass);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border-right: 1px solid var(--glass-line);
  }

  /* The sidebar carries the brand and the shop, so the in-column header that
     did that on a phone would only repeat itself. */
  .app--web .app-bar {
    display: none;
  }

  /* That header was also the screen's top margin. Without it the title sits
     against the viewport edge. */
  .app--web .screen {
    padding-top: 22px;
  }

  .app--web .app__frame {
    max-width: var(--app-width-wide);
    margin: 0;
    border-inline: 0;
  }

  /* Flush left is only right when the sidebar is what it is flush against.
     With no sidebar the column has the whole viewport, so it goes back to the
     middle.

     Compound, not descendant. `.app--web .app__frame` above is (0,2,0), so a
     descendant `.app--solo .app__frame` at (0,2,0) would tie it and leave the
     signed-out auth screens depending on line order. Compound is (0,3,0) and
     wins outright. The note that used to sit here claimed the two rules were
     the same specificity and that source order decided between them; they were
     (0,1,0) and (0,2,0), and writing them in the opposite order still yields a
     centred frame. Measured. */
  .app--web.app--solo .app__frame {
    margin-inline: auto;
  }

  /* The bottom bar and the sidebar are the same navigation. Only one shows. */
  .app--web .tab-bar {
    display: none;
  }
}

/* ---- Invoice ------------------------------------------------------------
   The one screen that exists to leave the screen. Everything here is chosen
   for how it reads on paper as much as in a browser, which is why the head is
   a two-column band rather than the stacked header the other screens use: a
   printed invoice puts the shop on the left and the number on the right, and
   that is a convention worth not being clever about. */
.invoice {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 20px 18px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
}

.invoice__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line-soft);
}

.invoice__logo {
  max-height: 56px;
  max-width: 160px;
  object-fit: contain;
}

/* The fallback when a shop has not uploaded a logo. Set in the display face so
   an invoice without one still looks issued rather than unfinished. */
.invoice__wordmark {
  margin: 0;
  font-family: var(--display);
  font-size: 20px;
  line-height: 1.2;
  color: var(--ink);
}

.invoice__meta {
  text-align: right;
  flex-shrink: 0;
}

.invoice__number {
  margin: 0;
  font-weight: 600;
  color: var(--ink);
}

.invoice__foot {
  padding-top: 12px;
  border-top: 1px solid var(--line-soft);
  font-size: 13px;
  color: var(--muted);
}

.invoice__foot p {
  margin: 0;
}

/* ---- Labels -------------------------------------------------------------
   A sheet of stickers. Sizes are in points because this layout really exists
   on paper and a printer works in physical units.

   Nothing scales text to fit. Several jurisdictions set a minimum type size,
   so shrinking a label to make a long ingredient list fit would quietly make
   it non-compliant. It grows instead, and the screen warns first. */
/* rivet:context: Cells are sized in inches on screen, not just on paper, so
   the preview is the sheet rather than an approximation of it. CSS maps 1in to
   96px, so a 4 x 2 inch label is 384 x 192 on screen: the same proportions and
   roughly the same size as the Avery 5163 the print rules target.
   
   The point is that a baker can see her ingredient list is too long BEFORE she
   spends a sheet finding out. A preview at a different shape to the output
   cannot tell her that. */
.label-sheet {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 4in));
  justify-content: center;
  gap: 10px;
  margin-top: 14px;
}

/* rivet:context: min-width: 0 is load-bearing rather than tidying. A grid item
   defaults to min-width: auto, so one unbroken string wider than its track, an
   email address or a long ingredient, pushes the track out and the whole page
   scrolls sideways on a phone. That is exactly what it was doing. */
.label {
  min-width: 0;
  width: 100%;
  max-width: 4in;
  min-height: 2in;
  overflow-wrap: anywhere;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  font-size: 9pt;
  line-height: 1.4;
  color: var(--ink);
  break-inside: avoid;
}

.label p {
  margin: 0 0 4px;
}

.label__name {
  font-family: var(--display);
  font-size: 13pt;
  line-height: 1.2;
  margin-bottom: 7px;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--line-soft);
}

/* A micro-label rather than a sentence, so the ingredients are what the eye
   lands on instead of the word "Ingredients". */
.label__key {
  display: block;
  font-size: 6.5pt;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 1px;
}

/* The line somebody with an allergy scans for, so it gets a rule of its own
   rather than being one more bold sentence. No fill: printers drop background
   colours by default, and a highlight that vanishes on paper is worse than
   none at all. */
.label__allergens {
  font-weight: 700;
  padding-left: 8px;
  border-left: 2px solid var(--raspberry);
  margin: 6px 0;
}

.label__statement {
  margin-top: 6px;
  color: var(--ink-soft);
}

.label__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 10px;
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px solid var(--line-soft);
}

.label__producer,
.label__date {
  font-size: 7.5pt;
  color: var(--muted);
  margin: 0;
}

/* rivet:context: From 600px the customer surface stops being a strip of white
   on white and becomes a sheet on a desk. On a phone .public and .public__frame
   are both --surface, which is right: the column IS the page. Give it a 1440px
   browser and the frame has no edge of any kind, so .public__head's cream fill
   reads as a rectangle floating in the middle of nothing and the page looks
   half-loaded. It is the loudest unstyled signal on the surface, and it is on
   every storefront screen.

   The page goes cream and the sheet stays white, which is the direction a warm
   palette wants. Lighter-outside would make the column look like a shadow.

   `screen and` because a US Letter page area is about 739px at default margins,
   so a bare min-width: 600px matches while printing, and an invoice with a
   border and a radius around it is not what anybody wants on paper. The print
   block later in this file resets the frame anyway; this is belt as well as
   braces, and it costs seven characters.

   Nothing here reaches a phone: below 600px not one declaration applies. */
@media screen and (min-width: 600px) {
  .public {
    background: var(--cream);
    padding-block: 32px;
  }

  .public__frame {
    border: 1px solid var(--line);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
    /* flex: 0 0 auto so a short page is a sheet rather than a column stretched
       to the height of the viewport. */
    flex: 0 0 auto;
    min-height: 0;
  }

  /* rivet:context: The head band is rounded here rather than clipped by the
     frame, and that is not a stylistic choice. `overflow: hidden` on the frame
     is the obvious way to stop a full-bleed fill squaring off the corners, and
     it would silently break the label sheet: that sheet deliberately escapes
     this column with left: 50% and a viewport-relative width, because a 540px
     column cannot preview a 8.5in sheet of stickers. A clipping ancestor cuts
     it back to the frame with nothing to say why. */
  .public__head {
    border-radius: var(--radius-card) var(--radius-card) 0 0;
  }
}

/* The customer surface is a 540px phone column, which is right for an order
   form and far too narrow to preview a sheet of 4 inch labels in. This breaks
   out of it, centred on the viewport, and stops at the width of a sheet of
   paper. Only where there is room: below 600px the column is already the whole
   screen. */
/* rivet:context: Scoped to .public, and the scope is the whole point. This
   breakout centres the sheet on the VIEWPORT, so it only lands correctly when
   the column it escapes from is itself centred on the viewport. The baker app's
   column is not: above 900px it sits to the right of a 232px sidebar. Unscoped,
   this put the sheet's left edge at x=104 on a 1440px screen and painted 128px
   of it underneath the navigation. Measured in Chromium, not theorised.

   `screen and` because a US Letter page box is about 816px, so a bare
   min-width: 600px matches while printing too. */
@media screen and (min-width: 600px) {
  .public .label-sheet {
    position: relative;
    left: 50%;
    transform: translateX(-50%);
    /* 52px, not 32, so the gutter matches .public__body's 26px padding either
       side. A sheet that breaks out of the column and then sits closer to the
       screen edge than everything above it reads as a mistake. */
    width: min(100vw - 52px, 8.5in);
  }
}

/* The caveat is deliberately not .no-print, so it survives printing: a sheet
   that leaves this page without it is the app asserting something it has no
   standing to assert. Small and muted because it belongs to the sheet rather
   than competing with it.

   This comment lost its opening three lines in a past edit, leaving an orphan
   `*/` at the top level. CSS error recovery then ate `.label-sheet__note` along
   with it, so the rule below was dropped entirely and the caveat rendered at
   15px ink, on screen and on paper, for as long as that was true. */
.label-sheet__note {
  margin-top: 10px;
  font-size: 8pt;
  line-height: 1.4;
  color: var(--muted);
}

/* ---- Print --------------------------------------------------------------
   idea.md F6 asks for a PDF, and this is where one comes from: the browser's
   own print-to-PDF, which every phone and desktop already has. The rejected
   alternative was headless Chrome in the production image, sharing one box
   with SQLite (idea.md 5.1), for a document a baker prints a few times a week.

   Last in the file, and for the same reason as the wide-screen block above it:
   these rules undo ones written earlier at equal specificity, so source order
   is what decides. Moving this block up silently stops it working. */
@media print {
  /* Chrome and Safari both drop background fills by default, and an invoice
     that prints as dark text on nothing is the correct outcome anyway: it is
     the ink a baker is paying for. */
  body {
    background: var(--surface);
  }

  /* Navigation, the app header, flash messages and every control. None of it
     is part of the document, and a printed "Save as PDF" button is a joke at
     the reader's expense. */
  .app-bar,
  .tab-bar,
  .sidebar,
  .flash,
  .back-link,
  .btn,
  .no-print,
  .public__foot {
    display: none !important;
  }

  /* The frame exists to hold a phone-width column inside a viewport. On paper
     the page is the frame. */
  .app__frame,
  .public__frame,
  .screen {
    max-width: none;
    margin: 0;
    padding: 0;
    border: 0;
    box-shadow: none;
  }

  .invoice {
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
    gap: 20px;
  }

  /* A page break through a total is how a reader ends up disputing a number
     that was only ever half in front of them. */
  .rows {
    break-inside: avoid;
  }

  /* Labels print two across whatever the screen was doing, and each one keeps
     its border because that border is the cut line. */
  /* Undo the on-screen breakout: on paper the page is the frame. */
  .label-sheet {
    position: static;
    left: auto;
    transform: none;
    width: auto;
    grid-template-columns: repeat(2, 1fr);
    justify-content: stretch;
    gap: 0;
    margin: 0;
  }

  /* rivet:context: min-height, not height. A fixed 2in cell CLIPS a long
     ingredient list, and clipping a label is the worst possible failure here:
     it produces a sticker that looks finished and is missing ingredients.
     Overflowing the cell is ugly and obvious, which is what you want. The
     screen warns before it happens. */
  .label {
    border: 1px dashed var(--line-strong);
    border-radius: 0;
    max-width: none;
    min-height: 2in;
    padding: 0.18in 0.2in;
    overflow: visible;
  }
}
