/* ==========================================================================
   Preheat: reusable pieces, in the vocabulary the demo uses
   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.
   ========================================================================== */

/* ---------- Type ---------- */

.serif {
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: 0;
}

.title {
  font-family: var(--display);
  font-weight: 400;
  font-size: 24px;
  line-height: 1.15;
}
.title--lg {
  font-size: 28px;
}
.title--sm {
  font-size: 20px;
}

/* The screen title in the baker app is the one place that stays sans and
   heavy, matching the demo's "Orders" and "June" headers. */
.screen-title {
  font-size: 28px;
  font-weight: 800;
  letter-spacing: -0.02em;
}

.eyebrow {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.lede {
  font-size: 14px;
  color: var(--muted);
}

.hint {
  font-size: 12px;
  color: var(--muted);
}

.strong {
  font-weight: 700;
}

.money {
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.text-sage {
  color: var(--sage);
}
.text-raspberry {
  color: var(--raspberry);
}
.text-muted {
  color: var(--muted);
}

/* ---------- Cards and rows ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
}

.card--pad {
  padding: 14px 16px;
}

.card--tap {
  display: block;
  width: 100%;
  text-align: left;
  color: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.card--tap:hover {
  border-color: var(--line-strong);
  color: inherit;
  transform: translateY(-1px);
}

/* A bordered list where each row is a label and a value: the order detail
   sheet, the quote breakdown, the receipt. */
.rows {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  overflow: hidden;
}

.rows__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line-soft);
  font-size: 14px;
}
.rows__row:last-child {
  border-bottom: 0;
}

/* rivet:context: Both sides shrink, and the VALUE stops at its longest word.
   That last clause is what keeps a price whole: .rows is `overflow: hidden`,
   "$9.00" has no break point, so a value that can never go below min-content
   can never be clipped mid-number. This rule has been wrong in both simpler
   forms. flex-shrink: 0 on the key pushed the price out of the card on a long
   line item ("Two dozen decorated cookies, lemon raspberry" beside "$9", cut
   off on the customer's own quote). flex-shrink: 0 on the value did the
   mirror image the day a value was a sentence: the shop screen's pickup
   windows crushed "Pickup times" to one letter per line, because the key's
   overflow-wrap: anywhere let it break where no word does.

   So: a long key wraps, breaking anywhere as the last resort; a long value
   wraps at spaces; a price survives both, because a price is one word.

   The 999 is the order of surrender, not a weight anyone tuned: flex hands
   out shrinkage in proportion to flex-shrink times basis, so the value gives
   up everything down to its longest word before the key gives up anything.
   With an even split the deficit of a paragraph-long value still crushed a
   three-letter key into a column of letters ("Ask", on the label screen). */
.rows__key {
  color: var(--muted);
  min-width: 0;
  overflow-wrap: anywhere;
}

.rows__value {
  font-weight: 700;
  text-align: right;
  flex-shrink: 999;
  min-width: min-content;
}

.rows__row--total {
  background: var(--cream-warm);
  font-size: 15px;
  font-weight: 800;
  border-bottom: 0;
}

/* A summary block that follows something else and wants a little separation
   from it: the receipt, the quote, the invoice, the payment instructions. */
.rows--summary {
  margin-top: 8px;
}

/* rivet:context: The cap belongs to the confirmation screen, not to the class.
   .confirmation centres and text-aligns its children, so a summary inside it
   has to opt back out for its rows to read as a list, and 380px is what stops
   the opt-out becoming a full-width slab on a centred page.

   It used to be unscoped, and the class was called --confirmation while three
   of its four callers were documents that centre nothing: the quote, the
   invoice and the payment instructions. They all inherited the cap. On a phone
   the column is narrower than 380px so it never showed; on a desktop quote it
   was a 108px right-edge mismatch against the rows directly above, which is a
   desktop-only bug that shipped unnoticed for exactly that reason. Widen the
   scope again and it comes back. */
.confirmation .rows--summary {
  width: 100%;
  max-width: 380px;
  text-align: left;
}
.rows__row--total .rows__key {
  color: var(--ink);
  font-weight: 800;
}

.note {
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  padding: 12px 15px;
  font-size: 13.5px;
  color: var(--ink-soft);
}
.note--sage {
  background: var(--sage-tint);
  border-color: var(--sage-line);
  color: var(--sage-text);
}
.note--raspberry {
  background: var(--raspberry-tint);
  border-color: var(--raspberry-line);
  color: var(--raspberry-deep);
}
.note--amber {
  background: var(--amber-tint);
  border-color: var(--amber-line);
  color: var(--amber-text);
}
.note--ink {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--cream);
}

/* rivet:context: A place where something will go, rather than a sentence
   adrift, and on a phone as well as at a desk. The dashed card was written in
   wide.css, so the only surface that got it was the one that needed it least: a
   baker signs up on her phone, and on her first day this is what almost every
   screen is. It is on twelve of them.

   Dashed rather than solid, because it is an absence. A solid card says
   something is here.

   The 300px cap stays, and it is what makes this work in both frames: the box
   is the width of its own text, so it reads as a card at 390px and does not
   become a slab at 1064. wide.css widens it to 34em there and adds nothing
   else. */
.empty {
  margin: auto;
  padding: 32px 24px;
  text-align: center;
  color: var(--muted);
  font-size: 14px;
  max-width: 300px;
  line-height: 1.5;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-card);
  background: var(--cream-warm);
}

/* rivet:context: The unread marks. Two of them, on purpose: a dot beside the
   name in the list, and a count on the DMs destination in both navigations.

   The dot is never the only signal. A coloured mark on its own fails anyone who
   cannot tell it from the background and fails everyone on a bright phone
   outdoors, so an unread thread also takes the darker, heavier name. Colour is
   the fast read; weight is the one that always works.

   A count rather than a dot on the navigation, because "three waiting" and "one
   waiting" are different decisions about whether to stop what you are doing. */
.unread-dot {
  /* inline-block, and not decoration: width and height do nothing at all on an
     inline span, so the first version of this rendered a zero-sized dot that
     looked exactly like a design that had chosen not to have one. */
  display: inline-block;
  vertical-align: middle;
  margin-right: 6px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--raspberry);
  flex: 0 0 auto;
}

.thread-card--unread .thread-card__name {
  color: var(--ink);
  font-weight: 900;
}

.thread-card--unread .thread-card__preview {
  color: var(--ink);
}

.nav-count {
  min-width: 18px;
  padding: 0 5px;
  border-radius: var(--pill);
  background: var(--raspberry);
  color: var(--on-accent);
  font-size: 11px;
  font-weight: 800;
  line-height: 18px;
  text-align: center;
}

/* ---------- Badges and pills ---------- */

.badge {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.05em;
  border-radius: var(--pill);
  padding: 3px 9px;
  white-space: nowrap;
  background: var(--cream-deep);
  color: var(--muted);
}
.badge--new {
  background: var(--sage-tint);
  color: var(--sage-deep);
}
.badge--waiting {
  background: var(--amber-tint);
  color: var(--amber-text);
}
.badge--paid {
  background: var(--sage-deep);
  color: var(--on-accent);
}
.badge--done {
  background: var(--cream-deep);
  color: var(--muted);
}
.badge--cancelled {
  background: var(--raspberry-tint);
  color: var(--raspberry-deep);
}

/* Stage tabs on the board and chapter-style nav elsewhere. */
.pills {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
  /* rivet:context: The strip scrolls sideways; the PAGE must not. Without this
     the overscroll chains: swipe the stage filter, it reaches its end, and the
     browser hands the leftover gesture to the document, which rubber-bands the
     whole screen left and right. On the board, which is the app's home screen
     and where a thumb naturally lands, that reads as the page being loose.

     contain and not none, and the difference matters on Android. `none` would
     also suppress the document's own horizontal overscroll, which is what
     Chrome uses for swipe-to-go-back; `contain` stops the chaining and leaves
     the navigation gesture alone. */
  overscroll-behavior-x: contain;
}
.pills::-webkit-scrollbar {
  display: none;
}

.pill {
  flex: 0 0 auto;
  padding: 8px 13px;
  border-radius: var(--pill);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}
.pill:hover {
  color: var(--ink);
}
.pill[aria-current='true'],
.pill.is-active {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--cream);
}
.pill__count {
  opacity: 0.65;
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-button);
  padding: 14px 18px;
  font-size: 15px;
  font-weight: 800;
  cursor: pointer;
  text-align: center;
  transition: background-color 0.15s ease, color 0.15s ease,
    border-color 0.15s ease;
}

.btn--block {
  width: 100%;
}

.btn--primary {
  background: var(--raspberry);
  color: var(--on-accent);
}
.btn--primary:hover {
  background: var(--raspberry-deep);
  color: var(--on-accent);
}

.btn--ink {
  background: var(--ink);
  color: var(--cream);
}
.btn--ink:hover {
  background: var(--ink-pressed);
  color: var(--cream);
}

.btn--sage {
  background: var(--sage);
  color: var(--on-accent);
}
.btn--sage:hover {
  background: var(--sage-deep);
  color: var(--on-accent);
}

.btn--outline {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink);
}
.btn--outline:hover {
  border-color: var(--ink);
  color: var(--ink);
}

.btn--quiet {
  background: transparent;
  color: var(--raspberry);
  font-size: 13px;
  font-weight: 700;
  padding: 8px 4px;
}

.btn--sm {
  padding: 9px 14px;
  font-size: 13px;
  border-radius: var(--pill);
}

.btn[disabled],
.btn--disabled {
  background: var(--disabled-bg);
  color: var(--disabled-fg);
  cursor: default;
  border-color: transparent;
}
.btn[disabled]:hover,
.btn--disabled:hover {
  background: var(--disabled-bg);
  color: var(--disabled-fg);
}

/* rivet:context: A quiet button stays quiet when it is disabled. The disabled
   fill is right on a solid button, where it replaces a solid colour, and wrong
   on this one, where it replaces nothing: it paints a filled shape where there
   was never a shape at all.

   It became visible when the 44px floor grew the form editor's reorder arrows
   from 24x33 to 44x44 and the up arrow on the first question turned into a
   large grey disc, drawing more attention than any control on the screen and
   pointing at the one thing you cannot do. */
.btn--quiet[disabled],
.btn--quiet.btn--disabled,
.btn--quiet[disabled]:hover {
  background: transparent;
  color: var(--disabled-fg);
}

.back-link {
  align-self: flex-start;
  background: none;
  border: none;
  color: var(--raspberry);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  padding: 6px 0;
}

/* rivet:context: Save follows the baker down a form too long to hold in view.
   It is the phone half of what the desk already does with a sticky rail, and it
   is opt-in per screen rather than a rule about buttons, because the cost is
   real: it spends about 70px of an 844px viewport for as long as the form is
   open. Shop settings is 2,427px with Save at 2,270, so changing the shop's
   name meant scrolling two and a half screens to commit it. That trade is worth
   it there and is not worth it on a 949px form where Save is already nearly in
   view.

   bottom is measured off the tab bar rather than the viewport. The bar is
   sticky at bottom: 0 and 81px tall, so a button at bottom: 0 spends the whole
   scroll sitting underneath it. z-index stays below the bar's 20 for the same
   reason: at rest, at the very end of the form, the button passes behind the
   bar rather than over it, which is what a bar that is always there should do.

   Sticky and not fixed, so it needs no media query to switch itself off. A
   sticky element only leaves its place when its place is off screen, so on a
   form that fits, this changes nothing at all. Above the desk breakpoint the
   same button is inside .split__sticky, which pins it beside the form instead;
   sticky on sticky is not a conflict, the inner one simply never has anywhere
   to travel. */
.btn--follow {
  position: sticky;
  bottom: calc(81px + 10px);
  z-index: 15;
}

/* rivet:context: A day the baker took off, told apart from a day her shop is
   simply shut on. Both are closed and the difference is whose decision it was:
   "I do not work Mondays" is a rule in settings, "I am away that week" is a row
   she can remove from the calendar. Rendering them identically would leave her
   no way to see which of the two she is looking at, and so no way to know
   whether the calendar can be changed from this screen.

   The reason is already on Availability::Day, which has listed :blackout since
   before anything could create one. */
.cal__day--off {
  background: repeating-linear-gradient(
    -45deg,
    var(--cream-deep),
    var(--cream-deep) 4px,
    var(--surface) 4px,
    var(--surface) 8px
  );
  color: var(--muted);
}

.cal__swatch--off {
  background: repeating-linear-gradient(
    -45deg,
    var(--cream-deep),
    var(--cream-deep) 3px,
    var(--surface) 3px,
    var(--surface) 6px
  );
  border: 1px solid var(--line-strong);
}

/* ---------- The 44px floor ---------- */

/* rivet:context: One block, deliberately, because a floor spread across twenty
   components is a preference and not a rule. Everything a thumb has to hit is
   listed here and spec/system/touch_targets_spec.rb measures the rendered boxes
   at 390px, so a control added later that lands under 44 turns something red.

   Before this the floor existed only as a sentence in wide.css calling 44px "a
   THUMB minimum" while .pill was 38px, the back link 33px tall and as narrow as
   37px on seven screens, and the form editor's reorder arrows 24x33. Ten of the
   thirteen phone screens had at least one.

   min-height rather than more padding, and inline-flex where the component was
   not already, so the ink stays where it was and only the box grows. The
   alternative was per-component padding arithmetic that has to be redone every
   time a font size moves.

   Width matters as much as height and is the half that gets forgotten: an
   arrow, a back link and a month stepper are all narrow, and a 44px-tall target
   33px wide is still a missed tap. .btn keeps min-width off, because a wide
   button does not need it and .btn--block would fight it. */
.pill,
.back-link,
.btn,
.btn--quiet,
.question__body,
.app-bar__brand,
a.hint,
.rows__value a {
  min-height: 44px;
}

.back-link,
.btn--quiet {
  min-width: 44px;
}

/* Not already flex, so min-height alone would leave the label at the top of a
   44px box. The gap replaces the whitespace between the label and its count,
   which a flex container strips. */
.pill,
.back-link,
.btn--quiet,
a.hint,
.rows__value a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
}


/* ---------- Forms ---------- */

.field {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.field__label {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
/* "optional" beside a label genuinely is secondary, and the way to say so is
   WEIGHT rather than contrast. It used to be --faint at 2.34:1, and there is no
   threshold below the legibility floor for words that matter less: hierarchy
   encoded in contrast stops being hierarchy the moment it stops being legible. */
.field__label .optional {
  font-weight: 400;
  color: var(--muted);
  letter-spacing: 0.04em;
}

/* The label for ONE input, as against the heading over a group of them. The
   uppercase tracked form is a section heading in disguise: it is right above
   "Your shop" with three controls under it, and it shouts over a single Email
   box.

   --ink-soft rather than --muted, and that is not a preference. --muted is
   4.07:1 on cream and 4.42:1 on white; AA wants 4.5. --ink-soft is 7.53 and
   8.16. A label naming the field you are typing into is not chrome. */
.field__label--plain {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-soft);
}

/* rivet:context: A label has to be nearer to its own control than to the one
   above it, and .field is a flex column with a single 9px gap, so it cannot say
   that on its own. Where one .field holds several labelled controls, and
   settings holds sixteen, every gap was the same 9px and the column read as an
   alternating list rather than as pairs.

   Negative margin rather than a smaller gap on .field, because the 9px is
   right BETWEEN pairs and only wrong inside one. */
.field__label--plain + .input,
.field__label--plain + .textarea,
.field__label--plain + .select {
  margin-top: -4px;
}

/* One labelled control inside a row of them. Without it the label and the input
   are peers of the row and lay out side by side, so a pair of money fields
   reads "Deposit percent [30] Flat deposit amount [$0.00]" on one line with
   both labels wrapping. min-width: 0 so a track can be narrower than the
   longest label rather than pushing the row wider than the card. */
.stack--pair {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* A group of controls that together answer one question, so that the country
   radios have a name a screen reader can read out and the login block is not
   three unrelated boxes.

   min-width: 0 because a fieldset ignores the flex or grid track it sits in
   and sizes to min-content, which overflows a 420px card the first time
   somebody puts a long legend in one. Same trap as .split and .label. */
.fieldset {
  display: flex;
  flex-direction: column;
  gap: 12px;
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

/* A legend has UA padding and does not participate in its parent's flex
   layout in every engine, so it is given the gap explicitly rather than
   relying on being a flex item. */
.fieldset > .field__label {
  padding: 0;
  margin-bottom: 0;
  float: left;
  width: 100%;
}

.field__help {
  font-size: 12.5px;
  color: var(--muted);
}

.input,
.textarea,
.select {
  width: 100%;
  font-size: 15px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-chip);
  padding: 12px 14px;
}
.input::placeholder,
.textarea::placeholder {
  color: var(--muted);
}
/* rivet:context: Two rules, and the order between them is the whole fix. Both
   are (0,1,1), so the focus-visible treatment wins only because it is written
   second; move it above and `outline: none` lands on keyboard focus as well.

   That was the bug. The single rule this pair replaces killed the outline for
   everybody and put a --raspberry-tint halo back in its place, measuring
   1.05:1 against cream and 1.14:1 against a white card. So the entire keyboard
   focus indicator on every form in the app was a 1px border swapping from
   --line-strong to --raspberry, and the sign-in email field carries autofocus,
   which means the app opened in that state.

   border-radius is repeated on purpose. base.css gives :focus-visible a 6px
   radius so the ring follows the shape, but that declaration changes the
   ELEMENT, not the outline, so an .input at --radius-chip snapped its corners
   from 12px to 6px the moment it took focus. */
.input:focus,
.textarea:focus,
.select:focus {
  border-color: var(--raspberry);
  outline: none;
  box-shadow: none;
}

.input:focus-visible,
.textarea:focus-visible,
.select:focus-visible {
  border-color: var(--raspberry);
  outline: 3px solid var(--raspberry);
  outline-offset: 2px;
  border-radius: var(--radius-chip);
}

.textarea {
  min-height: 92px;
  resize: vertical;
  line-height: 1.5;
}

/* Choice chips. The radio itself is hidden; the label is the control, which
   is what makes the demo's option rows feel like buttons rather than a form. */
.chips {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.chips--even .chip {
  flex: 1;
}

.chip {
  position: relative;
  display: block;
  padding: 11px 14px;
  border-radius: var(--radius-chip);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink);
  font-size: 14px;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}
.chip:hover {
  border-color: var(--faint);
}
.chip input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.chip__note {
  display: block;
  font-weight: 500;
  font-size: 12px;
  color: var(--muted);
  margin-top: 2px;
}
.chip:has(input:checked) {
  border: 2px solid var(--raspberry);
  background: var(--raspberry-tint);
  padding: 10px 13px;
}
.chip:has(input:focus-visible) {
  outline: 3px solid var(--raspberry);
  outline-offset: 2px;
}

/* Toggle row: a full-width label with a switch on the right. */
.toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-chip);
  background: var(--surface);
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}
.toggle input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.toggle__track {
  width: 44px;
  height: 26px;
  border-radius: var(--pill);
  background: var(--switch-off);
  display: inline-flex;
  align-items: center;
  padding: 3px;
  transition: background 0.2s;
  flex-shrink: 0;
}
.toggle__knob {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--on-accent);
  transition: transform 0.2s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.toggle:has(input:checked) .toggle__track {
  background: var(--sage);
}
.toggle:has(input:checked) .toggle__knob {
  transform: translateX(18px);
}

.file-drop {
  display: block;
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--radius-chip);
  background: var(--cream-warm);
  padding: 20px;
  font-size: 14px;
  font-weight: 600;
  color: var(--muted);
  text-align: center;
  cursor: pointer;
  width: 100%;
}
.file-drop:hover {
  border-color: var(--faint);
}

.errors {
  background: var(--raspberry-tint);
  border: 1px solid var(--raspberry-line);
  border-radius: var(--radius-panel);
  padding: 12px 15px;
  color: var(--raspberry-deep);
  font-size: 13.5px;
}
/* The messages are links to the fields they name, so they have to look like
   links without leaving the block's colour. Underlined rather than recoloured,
   because --raspberry-deep on --raspberry-tint is already the block's voice and
   a second red would say nothing. */
.errors a {
  color: inherit;
  text-decoration: underline;
}

/* rivet:context: The jump target is the input itself, and without this it lands
   flush against the top of the frame with its label scrolled off above it, so
   you arrive at a box with no idea which one it is. 64px clears the label and
   the group heading above it. */
.input:target,
.textarea:target,
.select:target {
  scroll-margin-top: 64px;
}

/* Focused after a failed submit, so it needs to look focused. */
.errors:focus-visible {
  outline: 3px solid var(--raspberry);
  outline-offset: 2px;
}

.errors ul {
  margin: 6px 0 0;
  padding-left: 18px;
}

.flash {
  border-radius: var(--radius-panel);
  padding: 11px 15px;
  font-size: 13.5px;
  font-weight: 600;
}
.flash--notice {
  background: var(--sage-tint);
  border: 1px solid var(--sage-line);
  color: var(--sage-text);
}
.flash--alert {
  background: var(--raspberry-tint);
  border: 1px solid var(--raspberry-line);
  color: var(--raspberry-deep);
}

/* ---------- Calendar ---------- */

.cal {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cal__weekdays,
.cal__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 5px;
}

.cal__weekdays {
  font-size: 11.5px;
  font-weight: 700;
  text-align: center;
  color: var(--muted);
}

/* One cell shape, four states. Closed days stay visible but inert so the
   customer can see the shape of the baker's week, not just its holes. */
.cal__day {
  min-height: 44px;
  border-radius: 10px;
  padding: 7px 0 6px;
  font-size: 13px;
  font-weight: 700;
  text-align: center;
  border: 1px solid transparent;
  background: transparent;
  color: var(--muted);
  cursor: default;
  font-variant-numeric: tabular-nums;
}

.cal__day--blank {
  visibility: hidden;
}

.cal__sub {
  display: block;
  font-size: 9.5px;
  font-weight: 700;
  margin-top: 1px;
  opacity: 0.85;
}

.cal__day--open {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink);
  cursor: pointer;
}
.cal__day--open:hover {
  border-color: var(--raspberry);
}

.cal__day--full {
  background: var(--cream-deep);
  color: var(--muted);
  text-decoration: line-through;
  cursor: not-allowed;
}

/* The picker's selected state is CSS-only. A customer taps a date and sees it
   fill immediately, on a page that ships no JavaScript to make that happen. */
.cal__day--selected,
.cal__day:has(input:checked) {
  background: var(--raspberry);
  border-color: var(--raspberry);
  color: var(--on-accent);
}
.cal__day--selected:hover,
.cal__day:has(input:checked):hover {
  border-color: var(--on-accent);
}
.cal__day:has(input:focus-visible) {
  outline: 3px solid var(--raspberry);
  outline-offset: 2px;
}

.cal__sub--picked {
  display: none;
}
.cal__day:has(input:checked) .cal__sub--idle {
  display: none;
}
.cal__day:has(input:checked) .cal__sub--picked {
  display: block;
}

/* The baker's calendar reads the same grid as counts rather than choices. */
.cal__day--has-room {
  background: var(--sage-tint);
  border-color: var(--sage-line);
  color: var(--sage-deep);
}
.cal__day--booked-out {
  background: var(--raspberry-tint);
  border-color: var(--raspberry-line);
  color: var(--raspberry-deep);
}

.cal__legend {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  font-size: 11.5px;
  color: var(--muted);
}
.cal__swatch {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 3px;
  margin-right: 5px;
  vertical-align: middle;
}
.cal__swatch--room {
  background: var(--sage);
}
.cal__swatch--full {
  background: var(--raspberry);
}
.cal__swatch--closed {
  background: var(--closed-swatch);
}

/* ---------- Motion ---------- */

@keyframes ph-fade-up {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes ph-pop {
  0% {
    transform: scale(0.94);
    opacity: 0;
  }
  60% {
    transform: scale(1.02);
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}
@keyframes ph-shake {
  0%,
  100% {
    transform: none;
  }
  20% {
    transform: translateX(-5px);
  }
  40% {
    transform: translateX(5px);
  }
  60% {
    transform: translateX(-3px);
  }
  80% {
    transform: translateX(3px);
  }
}

.fade-up {
  animation: ph-fade-up 0.3s ease both;
}
.pop {
  animation: ph-pop 0.35s ease both;
}
.shake {
  animation: ph-shake 0.45s ease;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* The revenue-cap meter (idea.md F13). One quiet bar: sage while there is
   room, amber past the shop's own warning threshold, raspberry once the cap
   is passed, when the bar is simply full and the words beside it carry the
   overage. */
.meter {
  height: 8px;
  border-radius: 999px;
  background: var(--line-soft);
  border: 1px solid var(--line);
  overflow: hidden;
}
.meter__fill {
  height: 100%;
  border-radius: inherit;
  background: var(--sage);
}
.meter--warn .meter__fill {
  background: var(--amber);
}
.meter--over .meter__fill {
  background: var(--raspberry);
}
