/* HIDDEN MEANS HIDDEN, EVERYWHERE, AND IT HAS TO BE SAID ONCE.
   ===========================================================================
   `hidden` is a UA `display: none`, which ANY `display` in this file
   overrides -- so every component that lays itself out with flex or grid has
   to remember to add its own `[hidden]` guard, and ten of them did. Four did
   not: `.tests-detail`, `.tests-groups`, `.tests-runs` and `.tests-failures`
   all set `display: flex`, so nothing in the Tests screen could ever hide.
   Tapping a category rendered its tests BELOW the still-visible list of
   categories and the still-visible runs table, off the bottom of the screen,
   which read as the category not opening at all.

   One rule instead of a rule per component. `!important` is correct here and
   is the standard idiom: it is what makes the attribute mean what the HTML
   spec says it means. The one rule that must still beat it --
   `.report[hidden] { display: block !important }` inside @media print, which
   is how the printed report is revealed -- is a class plus an attribute and
   out-specifies this on its own.

   Leaving the ten existing per-component guards alone: they are redundant
   now, not wrong, and deleting them is churn in a file this large. */
[hidden] { display: none !important; }

/* ===========================================================================
   THE TOKENS, from the design canvas's "שפה עיצובית" board.
   ===========================================================================
   Dark and warm-neutral. ONE text colour for every piece of content,
   interactive or not: hierarchy comes from size and weight, never from grey.
   Grey is reserved for `disabled` and means exactly that -- a control that
   will not respond. Colour means an entry TYPE, nothing else.

   The old palette was warm brown (#12100e / #d98b4a) with a separate cool
   blue for the app's own replies, and it encoded meaning in four more ways:
   a reply's background said which pass had priced it, the summary had its
   own surface, and `--muted` was used for any text thought less important.
   All of that is gone. A capture is priced or it is not (solid or dashed
   border), and it is food, a workout or a symptom (three colours).

   Names are kept where their MEANING is unchanged, so nothing downstream
   has to be rewritten to keep working: --bg, --surface, --line, --ink,
   --accent, --danger still mean what they meant. */
:root {
  /* THE COLUMN EVERY SCREEN IS READ IN, including the chat. One number, so
     the diary, the settings screens and the jump-to-newest button cannot
     drift apart on a desktop. Wider than any phone this runs on, which is
     what lets it apply everywhere without a breakpoint: on a 390px screen
     nothing is capped, on a 1280px one everything is. */
  --column: 26rem;
  /* The desktop stage the phone frame sits on, and the frame itself. Tokens
     rather than literals because they change with the theme like everything
     else -- a black bezel on a bright page reads as a hole in the screen. */
  --stage: #080706;
  --bezel: #16161a;
  --bezel-edge: #2a2a30;
  /* Native controls -- time and number inputs, their pickers, scrollbars --
     render from the platform's light palette unless this says otherwise.
     Without it the time fields had a light picker on a dark sheet. */
  color-scheme: dark;

  --bg: #0D0E10;
  --surface: #17191D;
  /* A capture of your own, one step up from the surface so it reads as a
     different kind of object without a second hue. */
  --own: #212429;
  --line: #2E3238;
  --ink: #F4F2ED;
  /* ONLY for a disabled control. Never for ordinary text: see the note at
     the top of this block. */
  --disabled: #6B6F76;
  /* A SECONDARY CONTROL -- the ••• on a bubble, Correct, Edit, Delete.
     This reverses the canvas's rule that content is one colour whether it
     is interactive or not, and the reversal came from using the app: at
     full ink those controls read as part of the sentence above them, so
     nothing said they could be pressed. The owner asked for grey.

     Its own name even though it is the same value as --disabled today,
     because they are different ideas and will not always agree: this one
     means "press me, I am not the point", the other means "you cannot
     press me". Where they meet -- a disabled secondary control -- opacity
     is what still separates them. */
  --affordance: #6B6F76;
  --danger: #FF5B4F;
  --ok: #7ED69A;

  /* THE THREE ENTRY TYPES, each a solid, a tint and a border. The solid is
     for a filled chip or an icon, the tint is the entry's own background,
     the border is its 1px edge -- and dashed when it is still waiting for a
     number. Food doubles as the app's primary action colour: the thing this
     app is for is logging a meal. */
  --food: #F2B544;
  --food-tint: #261F12;
  --food-line: #4D3D1C;
  /* The ink on a filled food chip. Dark enough on #F2B544 to clear 4.5:1. */
  --food-on: #17120A;

  --workout: #6EC4F2;
  --workout-tint: #10212B;
  --workout-line: #234A60;
  --workout-on: #08151C;

  --symptom: #C3B3FF;
  --symptom-tint: #1D1A2B;
  --symptom-line: #3D3560;
  --symptom-on: #15102B;
  /* Ink dark enough to read on the danger fill, for the one chip that
     takes it: a meal whose calories are high enough to be worth saying so. */
  --danger-on: #1A0906;
  /* Ink for text on the --ok fill, which a settled calorie figure now takes.
     The same job --food-on does on --food: a light chip needs dark ink, and
     near-white on #7ED69A is the unreadable combination that was reported
     on the yellow one. */
  --ok-on: #07180E;
  /* The settled green's own fill and edge, the same quartet the three entry
     types have. A reviewed meal's bubble takes these: the figure in it is no
     longer the app's working guess, and that is worth seeing from across the
     conversation rather than only on the chip. */
  --ok-tint: #11211A;
  --ok-line: #24503C;

  /* THE ONE HUE HERE THAT IS NOT AN ENTRY TYPE.
     The drawer colours items by the group they belong to, and it ran out: the
     four settings screens and the two "about this app" rows both wore the
     lilac, so a menu that colours groups showed two groups as one. Every other
     hue in this file was already spoken for -- food on Admin, workout on
     Reports, ok on Share, danger on Log out -- and grey is what those icons
     looked like before they were coloured at all, which read as not yet
     loaded.
     Teal because of where it sits: far from the lilac it is being told apart
     from, and far from the yellow above it and the red below it in the column,
     which are the only two colours it is ever seen next to. */
  --about: #5FD0C4;

  /* `--accent` is food, and stays a name of its own because a hundred rules
     mean "the app's one accent" rather than "the colour of a meal". */
  --accent: var(--food);
  --accent-on: var(--food-on);

  /* CORNERS. 14 fields and inner cards · 18 bubbles and cards, with the
     tail corner at 6 · 24 sheets · 999 for buttons and chips. */
  --r-field: 14px;
  --r-bubble: 18px;
  --r-tail: 6px;
  --r-sheet: 24px;
  --r-pill: 999px;

  /* The smallest thing a thumb may be asked to hit. */
  --tap: 44px;

  /* Height the sticky header occupies, so the date pill can park directly
     under it. Tied to .chat-header's padding plus its h1 line box -- if that
     changes, this must change with it. */
  --header-height: 3rem;

  /* ONE FAMILY, WORDS AND FIGURES ALIKE.
     The canvas specified IBM Plex Sans Hebrew. Its Latin is good and its
     HEBREW is the weak half of the family -- cramped counters and tight
     sidebearings at the 14-17px this app actually lives at -- which the
     owner said plainly on seeing it: "the Hebrew font is ugly". Five faces
     were set in the app's own strings at the app's own sizes and he picked
     Rubik, which has the most presence of them on a dark background and the
     strongest Latin of the five -- and the Latin matters here, because a
     single bubble routinely holds both scripts.

     NUMBERS ARE RUBIK TOO, not a separate monospace. What a figure needs in
     this app is to line up in a column and not to reflow as it counts, and
     `tabular-nums` gives both without a second family; a mono face was
     buying a contrast between a number and a word that nothing here needed.
     See `.num` below, which is where the tabular figures are switched on --
     the token alone does not do it. */
  --font-text: "Rubik", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-num: var(--font-text);
}

/* EVERY NUMBER, in one rule. Applied by class rather than by hunting for
   each figure: a number is a number wherever it lands, and the rule that
   makes it monospaced is also the rule that stops it reflowing. */
.num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}

/* WHAT A PERSON TYPED, IN WHATEVER LANGUAGE THEY TYPED IT.
   `dir="auto"` alone picks a direction from the first strong character,
   which is right for the box but wrong INSIDE it: a Hebrew sentence with an
   English dish name in the middle would drag the dish to the wrong end.
   `unicode-bidi: plaintext` resolves each paragraph on its own, so English
   stays LTR inside a Hebrew interface and Hebrew stays RTL inside an
   English one. Everything a user wrote gets this: descriptions, transcripts,
   place names, preference lists. */
.user-text {
  unicode-bidi: plaintext;
  text-align: start;
}

/* WHAT A FIELD SAYS WHEN IT IS EMPTY.
   This app had no ::placeholder rule at all, so every empty field showed
   whatever the browser chose. Chromium chooses something legible. iOS Safari
   -- the phone this is actually used on -- chooses a dark grey meant for a
   light background, and this app is dark only. The composer's placeholder
   was therefore invisible on the one device that matters: the field read as
   a bare grey bar with no words in it, which is exactly how it was reported.
   `color-scheme: dark` does not cover it, and no amount of desktop testing
   would have shown it.
   `opacity: 1` because Firefox applies its own on top of the colour, which
   would otherwise wash this out a second time. */
input::placeholder,
textarea::placeholder {
  color: var(--disabled);
  opacity: 1;
}
/* The prefixed form, in its OWN rule and not in the list above.
   A selector list containing one pseudo-element the browser does not know
   is dropped whole -- so pairing the two in one list would lose the
   standard rule on any browser that lacks the prefixed one, and vice versa.
   Same reasoning as the -webkit-mask-image pair further down: iOS Safari is
   the phone this app is used on, and this is the property that was
   invisible there. */
input::-webkit-input-placeholder,
textarea::-webkit-input-placeholder {
  color: var(--disabled);
  opacity: 1;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  /* No bottom padding for the safe area here — the composer owns that (see
     .composer's padding-bottom). A sticky element cannot be pushed outside
     its containing block, so padding on body put the chat screen's bottom
     edge ~34px above the viewport on a device with a home indicator, and the
     composer detached and scrolled away there. Invisible on desktop, where
     the inset is 0, which is why it read as working. */
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font: 17px/1.5 var(--font-text);
}

/* The top is padded by the status bar's height too. Added to the Home Screen
   this app draws under the clock and the notch (translucent status bar and
   viewport-fit=cover, see index.html), so 1rem alone put the title of the
   sign-in screens behind them. The inset is 0 in a browser tab and on a
   desktop, which is why this only ever showed on the phone. */
.screen { display: none; padding: 1rem; padding-top: calc(1rem + env(safe-area-inset-top)); }
.screen[data-active] { display: block; }
/* Sign in, reset and set-password are a column, not a stripe. Unbounded,
   every field ran the full width of a desktop window -- and the password
   field's eye, which sits at the field's inline end, ended up around 1400px
   from the word it belongs to. It read as missing on the web and present on
   the phone, where the same field is narrow enough that the eye lands
   beside the text. It was always there; it was stranded.

   `margin-inline`, not `margin-left`: the form has to stay centred in both
   directions. #screen-log is exempt on purpose -- the chat owns the whole
   width and its children set their own padding. */
/* BRIGHT.
   One block, read when the pre-paint script in index.html puts
   data-theme="light" on the root. Every value here answers a dark one above
   it, in the same order, so the two can be read side by side.

   It is not an inversion. Pure white is glare on a phone held at a table, so
   the page is a warm off-white and the surfaces above it are lighter rather
   than darker -- the opposite of the dark theme's stacking, which is how
   depth reads in daylight. The accent hues shift as well as flip: the amber
   that carries a meal on near-black is too pale to read as a figure on
   paper, so it darkens; the same for the blue and the violet. The *-on
   tokens, the ink that sits ON a coloured chip, stay dark in both themes,
   because the chips stay coloured.

   color-scheme flips with it, or the time pickers and scrollbars stay dark
   on a white sheet -- which is the exact bug the dark declaration was
   written for, in the other direction. */
:root[data-theme="light"] {
  color-scheme: light;

  --stage: #C9C4BA;
  --bezel: #E6E1D8;
  --bezel-edge: #CFC8BB;

  --bg: #F7F4EE;
  --surface: #FFFFFF;
  --own: #ECE6DA;
  --line: #DED7C9;
  --ink: #1A1C20;
  --disabled: #8C9099;
  --affordance: #6B6F76;
  --danger: #C62B1E;
  --ok: #2F7D4F;
  --food: #B4791A;
  --food-tint: #FBF1DA;
  --food-line: #E8D3A4;
  --food-on: #FFFFFF;
  --workout: #17688F;
  --workout-tint: #E4F1F9;
  --workout-line: #B4D7EA;
  --workout-on: #FFFFFF;
  --symptom: #5B41C4;
  --symptom-tint: #EDE9FC;
  --symptom-line: #CFC5F2;
  --symptom-on: #FFFFFF;
  --danger-on: #FFFFFF;
  --ok-on: #FFFFFF;
  --ok-tint: #E7F5EC;
  --ok-line: #BBE0C8;
  --about: #0F7F75;
}

/* THE SUN AND THE MOON, in Preferences. Two buttons rather than a checkbox:
   a checkbox says "dark mode: on", which makes dark a deviation from a light
   norm this app does not have. Two marks side by side with one lit says the
   same thing without ranking them. */
.theme-toggle { display: flex; gap: 0.5rem; margin: 0.25rem 0 1rem; }
.theme-button {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: var(--tap);
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.theme-mark { display: inline-flex; color: var(--affordance); }
/* The chosen one is filled, and its mark takes the accent: the same way the
   composer's own tabs say which of them is open. */
.theme-button[data-current="true"] {
  background: var(--own);
  border-color: var(--accent);
}
.theme-button[data-current="true"] .theme-mark { color: var(--accent); }

/* THE DEVICE FRAME, on anything that is not a phone.

   `display: contents` below phone width: the element is not in the layout at
   all, so a phone renders exactly what it rendered before this existed. The
   whole feature is a desktop one and costs mobile nothing.

   Above it, the app is put in a phone: a fixed-size box, centred on a darker
   stage, with a bezel. The sizes are CSS variables so the form-factor picker
   can change them, and the height is capped by the window as well, so a short
   laptop screen gets a shorter phone instead of a frame running off the page.

   `transform: translateZ(0)` is load-bearing and not a paint hint: a transform
   on an ancestor makes it the containing block for `position: fixed`
   descendants. Without it the menu drawer, the sheets, the walkthrough and the
   jump-to-newest button would all position against the viewport and sit
   outside the frame. */
#device { display: contents; }

/* THE PICKER IS A DESKTOP THING AND IS OFF EVERYWHERE ELSE. Declared outside
   the media query so the default is "gone": it turned up at the foot of a
   real iPhone because the only rule governing it lived INSIDE the desktop
   query, which left the question of whether it appeared to a JS read of a
   computed style at load time. A control that must never reach a phone does
   not get to depend on the timing of a stylesheet. */
.form-factor { display: none; }

@media (min-width: 48rem) {
  body {
    display: flex;
    justify-content: center;
    /* `safe` is load-bearing, not a nicety. A flex container centring an item
       TALLER than itself puts the item's top ABOVE the scroll origin, where
       no scrollbar can ever reach it -- the top of a 932pt phone on an 800pt
       window, gone. `safe center` centres while it fits and aligns to the
       start when it does not. The plain value first, for a browser that does
       not know the keyword: cropping the top is the old behaviour, not a new
       one. */
    align-items: center;
    align-items: safe center;
    min-height: 100dvh;
    padding: 1.25rem 0;
    background: var(--stage);
  }
  /* THE FRAME DOES NOT SCROLL; WHAT IS INSIDE IT DOES.
     It used to do both, and that is what put every panel 24,000px above
     the screen. `transform` makes this the containing block for
     `position: fixed` descendants -- which is why it is here -- but that
     containing block is the PADDING BOX, and on a scrolling element the
     padding box spans the whole scrollable content, not the part you can
     see. So the menu and every sheet anchored to the top of the diary's
     full height: `box=-46,-24361` on an 840px window, measured off a real
     press. Desktop only, because on a phone #device is `display: contents`
     and fixed resolves against the real viewport.
     Giving the scroll to the screen inside leaves this a fixed-size box,
     so the panels anchor to the frame you can actually see. */
  #device {
    display: block;
    position: relative;
    flex: 0 0 auto;
    width: var(--device-w, 390px);
    /* THE PHONE'S OWN HEIGHT, NOT THE WINDOW'S. This was
       `min(calc(100dvh - 2.5rem), var(--device-h, 844px))`, which CROPPED
       every phone taller than the window: an 844pt frame drawn 760pt tall on
       a laptop, with the app inside laying itself out against a height no
       handset has. Reported as "it squeezes the height and doesn't maintain
       the same height of the actual phone it is emulating". A short window is
       answered by SCALING below instead, which keeps the phone's measurements
       and only makes the picture of it smaller.
       The fallback is the window, which is what Fill is: form-factor.js
       clears both variables for it. */
    height: var(--device-h, calc(100dvh - 2.5rem));
    overflow: hidden;
    background: var(--bg);
    border: 12px solid var(--bezel);
    border-radius: 54px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35), 0 0 0 1px var(--bezel-edge);
    /* translateZ, and nothing else. It is not a paint hint: a transform makes
       this the containing block for every `position: fixed` descendant, which
       is what keeps the menu, the sheets and the tour inside the frame.
       THE SCALE IS GONE. It was `scale(var(--device-scale, 1))`, set from the
       window, and it made the two biggest phones draw the same size as each
       other and smaller than the SE: "make sure the emulators appear in the
       same size on the desktop. they look squeezed". A frame that is the
       phone's exact size has to be drawn at the phone's exact size; a window
       too short for it scrolls. */
    transform: translateZ(0);
  }
  /* The screens filled the WINDOW before; now they fill the frame.

     #device IN THE SELECTOR IS LOAD-BEARING, and not for scoping. The base
     rule -- #screen-log[data-active] { min-height: 100dvh } -- is declared
     several hundred lines BELOW this block, and at equal specificity the
     later rule wins whatever a media query says. So this lost, the diary
     stayed a full WINDOW tall inside an 844px frame, and the sticky composer
     with its tabs sat where 100dvh ended: under the bottom of the frame,
     outside the outline. The extra id wins on specificity instead of hoping
     about order. */
  /* The scroller now, and exactly the frame's height so it has somewhere
     to scroll within. `min-height` was right when #device scrolled; a
     scroller has to be BOUNDED, or it grows and the frame clips it. */
  /* WHICHEVER SCREEN IS SHOWING, not one id. This inherited `#screen-log
     [data-active]` from the `min-height` rule it replaced, which was fine
     for a height and fragile for a SCROLLER: app.js clears data-active
     from every screen and sets it on the new one, so anything that leans
     on that attribute stops being a scroll container the moment a screen
     changes -- and a frame with no scroller puts its content, header and
     all, wherever the overflow lands. The sign-in screen needs to scroll
     on a short window too. */
  #device .screen[data-active] {
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
  }

  /* AND vh MEANS THE WINDOW, which is taller than the frame. A sheet at
     85vh and the Suggest drawer at 60vh were measuring something the app is
     no longer drawn in, so both could run past the bezel. Percentages here
     are of the frame, which is what they always meant. */
  #device .overlay-sheet { max-height: 70%; }
  #device .suggest-drawer { max-height: 55%; }

  /* AND THE BODY LOCK IS OFF IN HERE. body[data-sheet-open] makes the body
     `position: fixed`, which is right when the DOCUMENT scrolls: it is what
     stops the page behind a sheet scrolling with it. Here the body is the
     stage that centres the frame, so taking it out of flow tore the app out
     of its own outline the moment any sheet or the menu opened. app.js
     freezes the frame's own overflow in this mode instead, which is the
     same guarantee applied to the thing that actually scrolls. */
  body[data-sheet-open] {
    position: static;
    inset-inline: auto;
    width: auto;
    overflow: visible;
  }

  /* The picker sits on the stage, not in the app: it is about the frame, not
     about the diary, and putting it inside would make it the only control on
     screen that is not part of the product. */
  .form-factor {
    position: fixed;
    top: 1rem;
    inset-inline-end: 1rem;
    display: flex;
    gap: 2px;
    padding: 3px;
    border-radius: 999px;
    background: var(--bezel);
    border: 1px solid var(--bezel-edge);
    z-index: 1;
  }
  .form-factor button {
    padding: 0.3rem 0.7rem;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: #8d8880;
    font-size: 0.78rem;
    font-weight: 600;
    cursor: pointer;
  }
  .form-factor button[data-current="true"] { background: var(--accent); color: #12100e; }
  /* The column cap is what the frame is now doing, and 26rem inside a 390px
     frame would do nothing anyway. Left alone deliberately: on a window too
     narrow for the frame the cap is still the thing keeping lines readable. */
}

.screen:not(#screen-log) { max-width: var(--column); margin-inline: auto; }

/* ...AND SO DOES THE CHAT, now that it is read on a desktop as well as a
   phone. The comment above used to say #screen-log was exempt because "the
   chat owns the whole width", which was true when the whole width was a
   phone. On a 1280px window it made a line of conversation run the width of
   the monitor, which is not a chat, and was reported as exactly that:
   "I am on desktop and it is stretched."

   Capping the SCREEN rather than the timeline inside it centres the header,
   the conversation, the composer and the Suggest drawer as one column, since
   all four are in its flow -- the header and the dock are sticky, not fixed.
   No media query: 26rem is wider than any phone this runs on, so the rule
   does nothing until the window is bigger than a phone. */
#screen-log[data-active] { max-width: var(--column); margin-inline: auto; }
/* Their headings sit over the middle of that column, not against its
   reading edge -- the same call as the chat header's title, and the reason
   is the same: a heading hard against one side of a centred form reads as
   a stray line rather than the form's name. */
.screen:not(#screen-log) h1 { text-align: center; }
/* The chat screen's children own their own padding — the header, the
   timeline and the composer each set theirs. The screen's 1rem left a strip
   below the composer, so at rest it sat 16px above the viewport bottom and
   then jumped down to meet it the moment sticky engaged. */
#screen-log { padding: 0; }
/* A column at least as tall as the viewport, with the timeline taking the
   slack. `position: sticky` can only pin an element that has somewhere to
   scroll, so on an empty chat — first launch, or before the week loads —
   the composer simply sat under the header, halfway up the screen. Stretching
   the timeline pushes it to the bottom edge where it belongs, with or without
   messages, and sticky still does its job once the conversation is long
   enough to scroll. Higher specificity than `.screen[data-active]` above,
   which sets `display: block`. */
#screen-log[data-active] { display: flex; flex-direction: column; min-height: 100dvh; }
.timeline { flex: 1 1 auto; }
/* Bottom-aligned: a short day sits above the composer rather than floating
   under the header with a gap beneath it. */
.timeline { justify-content: flex-end; }
/* The three sign-in screens used to be pinned `direction: ltr; text-align:
   left` here, because they were English chrome sitting in a hardcoded RTL
   page. Every word on them is translated now and `dir` on <html> follows the
   language, so the pin is gone: they read left-to-right in English and
   right-to-left in Hebrew, which is what the pin was approximating. */
/* Two fields now, not one. Without a column the inputs and the two buttons
   collapse against each other and the form reads as a single block. */
#login-form { display: flex; flex-direction: column; gap: 0.6rem; }
/* `.status` is end-aligned everywhere else (it trails a line of chat); under
   a form it belongs at the same edge the fields and labels start from. */
#login-form .status { text-align: start; margin: 0; }
#reset-form { display: flex; flex-direction: column; gap: 0.6rem; }
#reset-form .status { text-align: start; margin: 0; }
#set-password-form { display: flex; flex-direction: column; gap: 0.6rem; }
#set-password-form .status { text-align: start; margin: 0; }

h1 { font-size: 1.25rem; margin: 0 0 1rem; }

input[type="password"], input[type="email"], input[type="text"], textarea {
  width: 100%;
  padding: 0.75rem;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
}

/* The eye sits inside the field's trailing edge — the end of the line being
   typed, whichever side that is. Physical `right` until the sign-in screens
   stopped being pinned to LTR above; logical now, or it would sit on top of
   the first character in Hebrew. */
.password-field { position: relative; display: block; }
.password-field input { padding-inline-end: 3rem; }
.password-toggle {
  position: absolute;
  top: 50%;
  inset-inline-end: 0.25rem;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  padding: 0.5rem;
  background: transparent;
  color: var(--faint, var(--ink));
}

/* The 6-digit code, read off an email and typed back in -- large,
   spaced-out digits are the common pattern for exactly this kind of
   field, and a bare default-sized input (the bug this fixes: it wasn't
   matched by the shared input rule above at all, being type="text"
   rather than password/email) reads as broken, not just small. */
#reset-code {
  font-size: 1.5rem;
  letter-spacing: 0.5rem;
  text-align: center;
}

textarea { min-height: 5rem; resize: vertical; }

button {
  font: inherit;
  padding: 0.75rem 1rem;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: var(--accent-on);
  font-weight: 600;
}

button.secondary { background: var(--surface); color: var(--ink); border: 1px solid var(--line); }
/* Switching between signing in and signing up is a change of mind, not an
   action — drawn as a link so it does not compete with the button that
   actually submits the form. */
button.linkish {
  background: transparent;
  color: var(--ink);
  font-weight: 400;
  text-decoration: underline;
  padding: 0.5rem 0;
}
button.danger { background: transparent; color: var(--danger); padding: 0.25rem 0.5rem; }
/* Disabled here means "not available right now" (not standalone, or denied
   in Settings) rather than "working" -- the dimmed look says so at a glance,
   where the plain solid button otherwise reads as pressable. */
#notifications-toggle:disabled, #notifications-test:disabled { opacity: 0.5; }

/* Preferences.
   One sheet, two groups, one Save. Before this the sheet was a flat stack of
   headings, textareas and buttons with a Save in the middle and a second
   Save at the bottom -- so a changed checkbox and a pressed button could
   silently be about different things. The groups are what say which
   controls belong together; the single footer is what says one press saves
   all of it. */
/* `.overlay-sheet.prefs-sheet`, not `.prefs-sheet`, and the doubled class is
   load-bearing. `.overlay-sheet` is one class too, and it sets `padding: 1rem`
   700 lines further down -- equal specificity, later in the file, so it won.
   The bare `.prefs-sheet { padding-bottom: 0 }` that used to sit here had
   never applied: the sheet's computed padding-bottom was 16px, not 0. Found
   by measuring in the browser, not by reading the diff.

   `padding-top: 0` is what makes the sticky head below actually pin flush. A
   sticky child's stopping line is the scrollport inset by the scroll
   container's own padding, so that same `padding: 1rem` was holding the head
   17px down and content scrolled visibly through the band above it. The head
   carries the 1rem as its own `padding-block` start instead, so the spacing
   is unchanged and the band is gone. */
.overlay-sheet.prefs-sheet { padding-bottom: 0; padding-top: 0; }
.overlay-sheet.profile-sheet { padding-top: 0; }

/* SAVE IS STUCK TO THE BOTTOM, FULL WIDTH. On a sheet long enough to
   scroll -- Preferences is five cards deep -- the one button that commits
   the whole thing sat at the end of the scroll, so saving meant scrolling
   past everything you had just changed to find it. */
/* AND NOTHING LEFT UNDER THE PINNED FOOTER.
   .sheet-actions is `sticky; bottom: 0`, but the sheet carries
   `padding-bottom: calc(20px + safe-area)` inside the SAME scroll
   container -- and sticky stops at the padding edge, so a band of surface
   always sat below the button. Reported as "save is not below": on a phone
   with a home indicator it reads as a button stranded mid-sheet rather
   than planted on the edge.

   The inset is not lost, it moves: .sheet-actions already carries its own
   `calc(12px + env(safe-area-inset-bottom))`, which is the one that should
   own it, being the thing actually against the edge.

   :has() rather than a class, because .overlay-sheet.prefs-sheet above is
   this same bug patched for one sheet, and the next sheet to grow a footer
   would have inherited the gap again. */
.overlay-sheet:has(.sheet-actions) { padding-bottom: 0; }

.sheet-actions {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  gap: 10px;
  margin: 0 -20px;
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  background: var(--surface);
}
.sheet-actions button {
  flex: 1;
  min-height: 52px;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--accent-on);
  font-size: 16px;
  font-weight: 700;
}
/* A destructive action never grows to fill the row: it is square, outlined
   in the danger colour, and sits beside Save rather than competing with it
   for width. */
.sheet-actions button.danger {
  flex: 0 0 auto;
  width: 52px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--danger);
  color: var(--danger);
}

.pref-group {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  padding: 0.85rem;
  margin-bottom: 0.85rem;
}

/* The two language buttons are stacked, not a row. Left unstyled they were
   inline-block and sat flush against each other -- two full-width targets
   with no gap between them is a mis-tap waiting to happen, and every other
   list in this sheet already says so in the same three lines. */
#language-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 0.85rem;
}
/* A row, not a word: each button holds a flag and a name, and left inline
   the two sat flush against each other with no space between the picture
   and the thing it names. The gap is the one on .notify-row, so the two
   sheets space a mark and its label identically. */
#language-list button {
  width: 100%;
  min-height: 2.75rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
/* Bigger than the word beside it. At text size a flag is a smudge -- it is
   here to be recognised before the name is read, which it cannot do at the
   size of the name. `line-height: 1` keeps it from setting the row's
   height. */
.language-flag {
  flex: 0 0 auto;
  font-size: 1.25rem;
  line-height: 1;
}
.pref-group-title {
  margin: 0 0 0.15rem;
  font-size: 0.95rem;
  font-weight: 600;
}
/* Sentence-sized, not heading-sized: these name a field, and at the old
   0.78rem muted they read as legal small print under a bolder heading than
   the panel's own title. */
.pref-hint {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--ink);
  /* The notes used to overflow the sheet on a narrow phone: a long unbroken
     run — a URL, a pasted line with no spaces — has nowhere to wrap. Still
     true whichever way the page reads. */
  overflow-wrap: anywhere;
}
.pref-field { margin-top: 0.75rem; }
.pref-field > label {
  display: block;
  margin-bottom: 0.15rem;
  font-size: 0.9rem;
  font-weight: 600;
}
.pref-field textarea { margin: 0; }

/* The enable button and its test sit on one line, and the test is drawn as a
   link: it is a diagnostic, not a second setting, and at equal weight the
   two read as a pair of choices. */
/* ONE ROW, ONE CONTROL. The master switch shared this row with the test
   button, which took enough width that "Enable on this phone" wrapped to
   two lines and the switch ended up at a different inset from the four it
   governs. It is a .notify-row in everything but name now, so it lines up
   with them exactly. */
.pref-actions { display: flex; align-items: center; margin-top: 0.6rem; }
/* The test is an action, not a setting, so it sits under the group rather
   than in it -- and at the start edge, where an action belongs. */
.pref-test-row { display: flex; margin: 0.25rem 0 0.1rem; }
/* THE MASTER SWITCH, drawn as a switch like the four rules under it.
   It was a bordered button carrying a ☐ character; then, this morning, a
   drawn checkbox with a tick. Both were the wrong control: this is one
   thing that is on or off, not one of a set you are choosing from -- and
   the four rows below it are switches now, so a checkbox here would be the
   one row in the group wearing a different kind of control again.

   Still a <button>, because it carries the iOS permission gesture and that
   must run synchronously in the handler (see notifications.js). Only its
   clothes changed, and `aria-pressed` -- which a toggle button should carry
   anyway -- is what both the track and the knob are keyed on. */
.pref-toggle {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 52px;
  padding: 0.1rem 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  color: var(--ink);
  font-size: 16px;
  font-weight: 700;
  text-align: start;
  gap: 12px;
  /* Room for the words. It wrapped to two lines beside the test button,
     which also pushed the track off the line the switches below it sit on. */
  white-space: nowrap;
}
/* The track. */
/* THE TRACK LEADS THE ROW, because the four switches below it do: their
   markup is `<input><span>`, so the control sits on the START side -- the
   right in Hebrew, the left in English -- and the words follow it. This one
   was ordered to the END, which put the master switch on the opposite side
   from every switch it governs. `order` rather than a DOM change, because
   the button's text comes from the catalogue via textContent and cannot
   have an element in front of it. */
.pref-toggle::before {
  content: "";
  flex: 0 0 auto;
  order: -1;
  position: relative;
  width: 50px;
  height: 30px;
  border-radius: var(--r-pill);
  background: var(--line);
  transition: background 120ms linear;
}
.pref-toggle[aria-pressed="true"]::before { background: var(--accent); }
/* The knob, positioned against the row rather than the track -- a ::before
   cannot hold a child, so the two pseudo-elements are siblings and this one
   is placed from the row's end edge. `inset-inline-end` so it mirrors. */
.pref-toggle { position: relative; }
.pref-toggle::after {
  content: "";
  position: absolute;
  top: 50%;
  /* Measured from the START edge now that the track is there. Off is the
     near end of the track, on is the far end -- which is what a switch
     means, and it mirrors because the property is logical. */
  inset-inline-start: 4px;
  width: 22px;
  height: 22px;
  margin-top: -11px;
  border-radius: var(--r-pill);
  background: var(--ink);
  transition: inset-inline-start 120ms ease, background 120ms linear;
}
.pref-toggle[aria-pressed="true"]::after { inset-inline-start: 24px; background: var(--accent-on); }
/* A reader who asked for less motion keeps the colour and loses the blink:
   red already says it, and the pulse is the part that is motion. */
@media (prefers-reduced-motion: reduce) {
  .advice-kcal-value[data-risk="way-over"] { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .pref-toggle::before, .pref-toggle::after { transition: none; }
}
/* A secondary button, not a link. It sends a real notification -- an action,
   and the only other one in this card -- and `linkish` drew it as a change
   of mind. */
.pref-test {
  flex: 0 0 auto;
  min-height: 40px;
  padding: 0 16px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font-size: 15px;
  font-weight: 700;
  text-decoration: none;
}

.pref-subhead {
  margin: 0.9rem 0 0.35rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* The four mute toggles and the eating-window/check-hour fields, under the
   same Notifications heading as the two buttons above. Its own block, not
   folded into #avoid-text's rules, since these are checkboxes and native
   time/number inputs rather than a textarea. */
.notify-settings { margin-top: 0.25rem; }
/* Box first, text after -- the master control above these rows is a button
   reading "☐ Enable on this phone", indicator first, and four rows with the
   box on the other side made the group read as two unrelated things. This
   reverses the trailing-control choice made in the redesign the day before:
   matching the control these rows belong to beats the general convention.
   The whole row is still the tap target at thumb height, which is the part
   that worked. */
/* A ROW IS A LABEL AND A SWITCH. The checkbox was drawn by hand -- an
   appearance:none square with a tick built from two rotated borders -- and
   drawn well, but it is still a checkbox: a control that says "this is one
   of a set you are choosing from". These are not a set. Each one is a thing
   that is on or off, which is what a switch says. */
.notify-row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  padding: 0.1rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 16px;
  font-weight: 500;
}
.notify-row span { flex: 1; }
.notify-row:last-of-type { border-bottom: 0; }

/* THE SWITCH. A real <input type="checkbox"> still, so the label
   association, the keyboard and the accessibility tree are untouched --
   only its clothes changed. `appearance: none` and the track is the input
   itself; the knob is its ::after, moved by `inset-inline-start` so the
   whole thing mirrors with the page rather than sliding the wrong way in
   Hebrew. */
.notify-row input[type="checkbox"],
.switch {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: 0 0 auto;
  width: 50px;
  height: 30px;
  margin: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--line);
  transition: background 120ms linear;
}
.notify-row input[type="checkbox"]::after,
.switch::after {
  content: "";
  position: absolute;
  top: 4px;
  inset-inline-start: 4px;
  width: 22px;
  height: 22px;
  border-radius: var(--r-pill);
  background: var(--ink);
  transition: inset-inline-start 120ms ease, background 120ms linear;
}
.notify-row input[type="checkbox"]:checked,
.switch:checked { background: var(--accent); }
.notify-row input[type="checkbox"]:checked::after,
.switch:checked::after { inset-inline-start: 24px; background: var(--accent-on); }
.notify-row input[type="checkbox"]:focus-visible,
.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .notify-row input[type="checkbox"],
  .notify-row input[type="checkbox"]::after,
  .switch,
  .switch::after { transition: none; }
}
.notify-window { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center; font-size: 0.9rem; margin: 0.25rem 0 0.5rem; }
.notify-window label { display: flex; align-items: center; gap: 0.4rem; }
.notify-hour {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 2.75rem;
  font-size: 0.9rem;
}
.notify-window input[type="time"], .notify-hour input[type="number"] {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.4rem 0.5rem;
  font: inherit;
}
.notify-hour input[type="number"] { width: 4.5rem; text-align: center; }

/* Sticky, so the one Save is reachable without scrolling to the end of a
   sheet that is taller than the phone. Matches the sheet's own background so
   the content scrolls under it rather than appearing to end at it. */
.pref-foot {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 0;
  padding-bottom: calc(0.6rem + env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--line);
}
.pref-foot .status { flex: 1; text-align: start; margin: 0; }
.pref-foot button { flex: 0 0 auto; padding-inline: 1.5rem; }

.row { display: flex; gap: 0.5rem; align-items: center; }
.grow { flex: 1; }

.status { color: var(--ink); font-size: 0.85rem; min-height: 1.4em; }

.entry {
  display: flex;
  gap: 0.75rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--line);
}

.entry[data-dup] { opacity: 0.5; }
.entry[data-dup] .entry-text { text-decoration: line-through; }

.entry img { width: 64px; height: 64px; object-fit: cover; border-radius: 8px; }
.entry-time { color: var(--ink); font-variant-numeric: tabular-nums; font-size: 0.85rem; }

/* The recorder. Sized for a glance and a thumb: the timer says it is
   listening, and the send button is the whole width because the hand
   reaching for it may be coming off a steering wheel. */
/* Under Send, in the panel, for the same reason as .quick-log above:
   it was floating over the dock, furthest from the control that
   started it. A rule above it separates it from the Send button
   rather than leaving two stacked blocks reading as one. */
.voice {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.5rem;
  padding: 0.5rem 0.1rem 0.1rem;
  border-top: 1px solid var(--line);
}
.voice[hidden] { display: none; }
.voice-status { display: flex; align-items: center; gap: 0.5rem; }
.voice-dot { width: 0.7rem; height: 0.7rem; border-radius: 50%; background: var(--danger); animation: voice-pulse 1.2s ease-in-out infinite; }
@keyframes voice-pulse { 50% { opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) { .voice-dot { animation: none; } }
.voice-elapsed { font-variant-numeric: tabular-nums; font-weight: 600; }
/* The last three seconds. Colour rather than a new element, so nothing
   moves at the moment you are trying to finish a sentence. */
.voice-elapsed[data-low="true"] { color: var(--danger); }
.voice-hint { color: var(--ink); font-size: 0.85rem; }

/* THE SAME COUNTDOWN, SEEN RATHER THAN READ.
   A number has to be looked at; a bar draining is caught out of the
   corner of an eye while you are talking, which is the whole situation
   this is for. It drains rather than fills, because what matters is
   what is LEFT, not what has been used. */
.voice-meter {
  height: 3px;
  margin-top: 0.5rem;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}
.voice-meter > span {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: inherit;
  background: var(--accent);
  /* Matches the 250ms tick, so the bar slides instead of stepping.
     `linear`, because any easing makes a constant countdown look like
     it is speeding up and slowing down. */
  transition: width 250ms linear;
}
@media (prefers-reduced-motion: reduce) {
  .voice-meter > span { transition: none; }
}
#voice-send { padding-block: 0.9rem; font-size: 1.05rem; }
.voice-note { display: flex; flex-direction: column; gap: 0.25rem; }
.voice-note-label { font-size: 0.9rem; }
.voice-note audio { width: 100%; max-width: 240px; height: 2rem; }
/* .composer-field, its parent, stopped supplying any inset of its own (the
   composer-drawer work) -- without its own horizontal padding and a top
   margin to replace what the parent used to give it, a pending photo sat
   flush against the field's left border and only 3px under its top edge.
   Measured with a real thumbnail, not assumed. */
/* UNDER THE FIELD, WRAPPING. They were a flex item BESIDE the textarea,
   which paid for every thumbnail out of the field's width: at 30px plus a
   gap, one photo left a narrow box and two left a sliver, so the moment
   you attached a picture you could no longer type what it was. Below it,
   the field keeps its whole width and the row wraps instead of competing.
   The inline padding matches the textarea's own, so a thumbnail lines up
   with the first character above it rather than with the field's border.
   An empty photo row is not a row: it stays in the markup (the drawer's
   busy rule and reply-mode both depend on it existing) but claims no
   space and no gap until something is in it. */
#pending-photos:empty { display: none; }

#pending-photos {
  flex: 0 0 auto;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.4rem;
  padding: 0.1rem 0.75rem 0.5rem;
}
/* The positioning context for the ✕, and only that. */
.pending-photo { position: relative; display: block; flex: 0 0 auto; }
/* 56, not 30. At 30 a thumbnail was too small to tell two plates of the
   same colour apart, which is the only question it is on screen to answer
   -- and it had to be that small to fit inside the field's row. It is not
   in the row any more, so it can be the size it needed to be. */
#pending-photos img {
  width: 56px; height: 56px; object-fit: cover;
  border-radius: 8px; border: 1px solid var(--line);
  display: block;
}
/* Over the corner of its own picture, not in a row beneath it: a list of
   ✕s under a list of thumbnails makes you count to find out which one
   belongs to which. Filled with the panel's own surface so it reads on a
   photo of any brightness, which a transparent glyph does not. */
.pending-photo-drop {
  position: absolute;
  top: -7px;
  inset-inline-end: -7px;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 11px;
  line-height: 1;
  display: grid;
  place-content: center;
}
.pending-photo-drop:active { filter: brightness(1.4); }

/* Chat */
/* Sticky at the very top, so it has to clear the status bar itself: its own
   background then also fills the strip behind the clock while the chat scrolls
   under it. --header-height is measured from this element (app.js), so the
   date pills park below the inset without a second correction. */
/* BASELINE, NOT CENTRE, and that is the whole hamburger fix. Centring
   the ☰ box on the title's box is only right if the title's cap box
   is centred in its content box, and that is a property of the font,
   not of CSS. Measured: on SF Pro (iOS, macOS) the two centres differ
   by 0.06px, so centring looked correct on the phone; on Segoe UI
   (Windows) they differ by 1.28px, because Segoe's hhea ascent is
   1.079em against SF's 0.952em. Same stylesheet, same markup, visibly
   different answer per platform -- which is what "align it with the F"
   had been failing on.
   Aligning the row on the baseline removes the font from the question:
   the ☰ box's bottom edge lands on the same baseline the F sits on,
   and its height is the F's cap height, so the two share a top and a
   bottom by construction on every face. */
/* ABOVE THE SHEETS, NOT UNDER THEM.
   An overlay is `position: fixed; inset: 0` with a dim backdrop at z-index 5,
   so every panel opened from the menu covered this row completely -- the
   hamburger was still there and could not be pressed, which reads as the top
   row disappearing. The sheets are anchored to the bottom and never use this
   space for anything but dimming it, so the row sits above them and stays
   usable: pressing it closes whatever is open and brings the menu back. */
.chat-header { position: sticky; top: 0; z-index: 8; background: var(--bg); padding: calc(0.5rem + env(safe-area-inset-top)) 1rem 0; display: flex; align-items: baseline; gap: 0.5rem; }

.chat-header h1 { margin: 0 0 0.5rem; flex: 1; text-align: center; }
/* Fit in the accent, Keep in the ink: the half that is about the body
   carries the colour, the half that is about the diary is the text it is
   written in. Latin in both languages, so the order never flips -- and
   `dir="ltr"` says so rather than relying on the letters to. */
.app-title { direction: ltr; unicode-bidi: isolate; letter-spacing: -0.01em; }
.app-title-fit { color: var(--accent); }
.app-title-keep { color: var(--ink); }
/* Centred in the HEADER, not in the space left over beside ☰. With only the
   menu button in the row, `flex: 1` on the title starts it 3.4rem in, so a
   centred line sits half that off true and reads as a near-miss rather than
   a choice. This balances the button with an equal empty box on the other
   end. It is a flex item, so it takes the same `min-width` and nothing
   else -- no glyph, no tap target, no reordering when the page mirrors. */
/* The version now OCCUPIES this slot rather than sitting beside an
   empty one. The pseudo-element is gone: with it, the row was ☰, a
   flex:1 title, the version, and a 3.4rem spacer -- so the version was
   squeezed between the centred title and the spacer instead of ending
   the row, and `margin-inline-start: auto` bought nothing because the
   title had already taken every spare pixel. One element doing both
   jobs is why it is visible at all. */
/* The header's one remaining control. 🧭 used to sit beside it, which is
   why this is a width rather than a fit: an emoji is drawn wider than a text
   character at the same font-size, and the two boxes read as different
   controls unless both are given the same one. The width is kept now that
   ☰ is alone, so the title's start edge did not move when the compass left. */
#menu-toggle {
  background: transparent;
  color: var(--ink);
  margin-bottom: 0.5rem;
  line-height: 1;
  padding: 0.3rem;
  min-width: 3.4rem;
  display: inline-flex;
  align-items: center;
  /* The BOX keeps its 3.4rem, the GLYPH moves to the edge. The width is
     what balances the version badge at the other end so the title lands
     centred, so it cannot shrink -- but centring the bars inside it
     parked them ~12px further in than the header's own 1rem gutter,
     reading as an indent nothing else in the app has. `start`, not
     `flex-start`, so it follows the writing direction and stays on the
     correct edge in Hebrew. */
  justify-content: start;
}.install-hint { margin-top: 1.25rem; text-align: center; color: var(--ink); }
.install-hint[hidden] { display: none; }
.install-hint p { margin: 0.5rem 0 0; }

/* The menu drawer. It comes in from the side the ☰ sits on, and above every
   overlay in the chat.
   That used to be a physical `right`, with a comment saying the side was
   about where the button is on screen rather than about reading direction.
   The reasoning holds; the conclusion stopped holding when `dir` started
   following the language. ☰ is the first child of the chat header's flex
   row, so it sits at the *inline start* — physically left in English,
   physically right in Hebrew — and a drawer pinned to `right: 0` would now
   fly in from the opposite corner of the screen from the button that opened
   it in English. Logical properties keep the original promise.
   The slide itself cannot be logical (transform takes no side), so the two
   keyframes are chosen by the page's `dir` below. */
.menu-backdrop { position: fixed; inset: 0; z-index: 6; background: rgba(0, 0, 0, 0.45); animation: drawer-fade 0.2s ease-out; }
.menu-backdrop[hidden] { display: none; }
.drawer {
  position: fixed;
  top: 0;
  bottom: 0;
  inset-inline-start: 0;
  z-index: 7;
  width: min(80vw, 18rem);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  background: var(--surface);
  border-inline-end: 1px solid var(--line);
  /* Symmetric on purpose: the lift belongs on whichever edge the drawer's
     open side is, and one shadow that reads the same both ways is cheaper
     than a second pair of dir-keyed rules for a blur nobody measures. */
  box-shadow: 0 0 24px rgba(0, 0, 0, 0.35);
  padding: calc(0.5rem + env(safe-area-inset-top)) 0 calc(0.75rem + env(safe-area-inset-bottom));
  animation: drawer-in-ltr 0.22s ease-out;
}
.drawer[hidden] { display: none; }
@keyframes drawer-in-ltr { from { transform: translateX(-100%); } to { transform: none; } }
@keyframes drawer-in-rtl { from { transform: translateX(100%); } to { transform: none; } }
[dir="rtl"] .drawer { animation-name: drawer-in-rtl; }
@keyframes drawer-fade { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .drawer, .menu-backdrop { animation: none; } }
/* Logical inline padding: the 1rem is the title's margin and the 0.75rem is
   the close button's, and which physical side each lands on follows the
   language now. */
.drawer-head { display: flex; align-items: center; justify-content: space-between; padding-block: 0.25rem 0.5rem; padding-inline: 1rem 0.75rem; }
.drawer-title { font-weight: 700; font-size: 1.1rem; }
#menu-close { background: transparent; color: var(--ink); font-size: 1.1rem; padding: 0.4rem; }
/* The dividers between groups: a hairline above each section but the first. */
.drawer-section { padding: 0.35rem 0; }
.drawer-section + .drawer-section { border-top: 1px solid var(--line); }
.drawer-heading { margin: 0.35rem 1rem 0.2rem; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink); }
#menu button:not(#menu-close) { display: flex; align-items: center; gap: 0.75rem; width: 100%; text-align: start; background: transparent; color: var(--ink); font-weight: 400; font-size: 1rem; padding: 0.7rem 1rem; border-radius: 0; }
#menu button:not(#menu-close):active { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.drawer-icon { width: 1.5rem; text-align: center; font-size: 1.15rem; display: flex; align-items: center; justify-content: center; }

/* A COLOUR PER MENU ITEM. WhatsApp had one -- its own green, because a
   brand mark in grey reads as broken -- and that left one coloured icon in
   a column of twelve grey ones, which looks like the other eleven failed
   rather than like the one succeeded.

   The colours are the app's own entry types plus two neutrals, not twelve
   new hues: the point is to make the column scannable, and twelve unrelated
   colours is a paint chart. Items that belong together share one, so the
   grouping the headings state is visible without reading them.

   THE FOUR SETTINGS SCREENS ARE ONE GROUP. Profile, Preferences, Locations
   and Language are all "things you have told the app about yourself", and
   they were split across two hues on no principle -- two food, two symptom
   -- which drew a line through the middle of a group rather than around it.
   The lilac rather than the food hue: --food is --accent, which means
   CHOSEN everywhere else in the app, and four menu icons wearing it would
   spend the loudest colour here on the quietest row. */
#menu-profile .drawer-icon,
#menu-avoid .drawer-icon,
#menu-locations .drawer-icon,
/* The dietitian's plan is a standing fact about this person, in the same
   group as the rest of Settings rather than a report. */
#menu-guidance .drawer-icon,
#menu-language .drawer-icon { color: var(--symptom); }
/* SAYING SOMETHING, AND WHAT HAPPENS TO WHAT YOU SAID -- their own group, and
   now their own colour. They shared the settings lilac on the argument that
   both are about you rather than about the diary's contents. True, and it
   still left the drawer showing two headed groups in one colour, which is the
   one thing colouring these icons was for. */
#menu-feedback .drawer-icon,
#menu-help .drawer-icon,
#menu-whats-new .drawer-icon,
#menu-export .drawer-icon,
#menu-privacy .drawer-icon { color: var(--about); }
#menu-summary .drawer-icon,
#menu-week .drawer-icon,
#menu-report .drawer-icon { color: var(--workout); }
/* SHARING IS ONE SECTION AND ONE COLOUR. "Invite on WhatsApp" wore the brand
   green #25d366 and "Share another way" wore --ok, which are both green and
   not the same green -- so the two rows that belong together were the one
   place in the drawer where a section looked like two.
   The brand colour was defended on the grounds that a mark in grey reads as
   broken, and that is true of grey. It is not true of a green a shade off,
   and the mark stays unmistakably WhatsApp's shape, which is the part that
   identifies it. */
#menu-share .drawer-icon,
#menu-invite .drawer-icon .wa-mark { color: var(--ok); }
/* THE ADMIN SCREENS. They were --ink, which is the text colour -- so in a
   column where every other icon is tinted, the three at the bottom read as
   the ones that had not loaded yet rather than as a group of their own.
   The food hue is free in this menu now that the four settings screens share
   the lilac, and it is the furthest from both of their neighbours here: the
   reports' blue above and Log out's red below. */
#menu-ledger .drawer-icon,
#menu-accounts .drawer-icon,
#menu-tests .drawer-icon { color: var(--food); }
/* Log out keeps the danger colour it already had, on the icon as well as
   the words -- it is the one item in the drawer you can regret. */
#menu-logout .drawer-icon { color: var(--danger); }
#menu .drawer-logout { color: var(--danger); }
/* Pushes Log out to the bottom of the drawer, away from the things you tap
   on purpose every day. */
.drawer-section:last-child { margin-top: auto; }

.timeline { padding: 0.5rem 1rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
/* One section per day, so each date pill is bounded by its own day and
   scrolls away instead of stacking on the next one. Same flex layout as the
   timeline, so bubbles keep aligning to their sides. */
.day-group { display: flex; flex-direction: column; gap: 0.5rem; }

/* A date pill that freezes under the header while you scroll that day, the
   way WhatsApp does it. Every separator is sticky at the same offset, so the
   next day's pill pushes the current one out as it arrives — no JavaScript,
   and nothing to keep in sync with the scroll position.
   `width: fit-content` with auto side margins is what turns a full-width row
   into a centred pill; without it the sticky element would span the timeline
   and cover the bubbles scrolling under it. */
/* THE DAY SEPARATOR: one pill, centred, carrying the day's running total.
   Sticky inside its own day section (see chat.js), so the answer to "how
   much so far" is on screen for the whole day it belongs to. */
.sep {
  position: sticky;
  top: var(--header-height);
  z-index: 1;
  display: flex;
  /* `direction: ltr` used to be pinned here so the pill read date, calories,
     macros against the page's RTL default. It was a pin on the *interface*:
     the date is formatted in the active locale and the macro labels come out
     of the catalogue, so the order of the three spans is a reading order and
     now follows the page. Each span is its own bidi run, so a number is
     never torn up by the flip. */
  gap: 8px;
  align-items: center;
  justify-content: center;
  width: fit-content;
  /* One line, always. The pill carries the date and the whole day's numbers,
     and a wrap would turn the thing pinned under the header into a two-line
     bar across the conversation. max-width keeps it inside a narrow phone;
     the macros shrink before anything breaks. */
  max-width: 100%;
  flex-wrap: nowrap;
  white-space: nowrap;
  margin: 0.75rem auto 0.25rem;
  padding: 6px 14px;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--line);
  font-size: 13px;
  font-weight: 600;
  /* Figures that do not reflow as the day's total climbs. With one family
     for words and numbers, this is what a monospace used to buy. */
  font-variant-numeric: tabular-nums;
  /* Tapping it opens the day's per-meal breakdown beneath it. */
  cursor: pointer;
}
.sep-flag { color: var(--accent); }
.sep-day { flex: 0 0 auto; }
.sep-kcal { flex: 0 0 auto; }
/* THE OTHER HALF OF THE DAY'S ARITHMETIC. In the workout hue, which is what
   that colour is for, and never shrinking: it is a short figure and the
   macros beside it are the part allowed to give way on a narrow phone. */
.sep-burn {
  flex: 0 0 auto;
  color: var(--workout);
  font-variant-numeric: tabular-nums;
}
/* The macros are what gives way first: the day and its calories are the
   answer, the breakdown is the detail. */
.sep-macros {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 500;
}

/* =========================== THE BUBBLES ===========================
   One shape, two speakers, three types.

   SIDES ARE THE CONVENTION, NOT A CHOICE, and they were backwards: the
   capture you wrote sat on the START side and the app's reply on the END,
   which is the opposite of every messaging app anybody has ever used. What
   you said is on the END side (left in Hebrew, right in English) and the
   app's entries are on the START side. The tail -- one corner cut from 18
   to 6 -- points at the speaker, so the pair reads as a conversation even
   in a screenshot with no colour.

   The padding is asymmetric on purpose: 12 at the top, 6 at the bottom,
   because the last row is the foot (the ••• and the time) and a full 12
   under a 16px-tall row leaves the bubble looking bottom-heavy. */
.bubble {
  max-width: 80%;
  padding: 12px 14px 6px;
  border-radius: var(--r-bubble);
  background: var(--surface);
  border: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* YOURS, ON THE START SIDE, with the tail in the top corner nearest you.
   START, not end, and this is the one thing about the bubbles that is not
   a free choice: in Hebrew the start side is the RIGHT, and a capture of
   your own belongs on the right in Hebrew. The design language's phase 1
   put yours on the end side, which reads correctly in English and puts
   your own words on the LEFT for every Hebrew-speaking user of this app.
   `flex-start` follows the writing direction, so one rule serves both:
   yours right in Hebrew, yours left in English. */
.bubble.out {
  align-self: flex-start;
  background: var(--own);
  border-start-start-radius: var(--r-tail);
}
/* THE APP'S, ON THE END SIDE. Its tail is the mirror of yours, so each
   bubble's tail points at the edge its speaker sits against. */
.bubble.in {
  align-self: flex-end;
  border-start-end-radius: var(--r-tail);
}

/* THE ENTRY TYPE PAINTS THE WHOLE ENTRY. One rule per type rather than one
   per tier: which pass produced a number is said by the border being solid
   or dashed, not by the colour, so a meal is the meal colour whether it was
   priced seconds ago or last night. */
.bubble.in[data-type="meal"],
.bubble.marker[data-type="meal"] { background: var(--food-tint); border-color: var(--food-line); }
/* ...EXCEPT ONCE IT HAS BEEN REVIEWED, when a meal goes green.
   The comment above says which pass produced a number is told by the border
   being solid or dashed and never by colour, and that was the right rule
   while the only alternative was a different HUE per tier -- a blue meal and
   a yellow meal read as two kinds of thing.
   This is not that. It is the same meal in the settled colour the chip
   already uses, and the owner asked for it from the desktop: scrolling a day
   back, the question is which figures are still the app's guess, and a dashed
   edge answers it only if you are close enough to see the edge.
   Manual with it, for the reason it is green on the chip: a figure the person
   typed is settled and then some. */
.bubble.in[data-type="meal"][data-estimate="reviewed"],
.bubble.in[data-type="meal"][data-estimate="manual"] {
  background: var(--ok-tint);
  border-color: var(--ok-line);
}
.bubble.in[data-type="workout"],
.bubble.marker[data-type="workout"] { background: var(--workout-tint); border-color: var(--workout-line); }
.bubble.in[data-type="symptom"],
.bubble.marker[data-type="symptom"] { background: var(--symptom-tint); border-color: var(--symptom-line); }
/* A PLAN, which is the fourth thing the box at the bottom takes. It borrows
   the settings lilac the symptom uses rather than asking for a sixth token:
   the two are never the same bubble and never adjacent in meaning, and a
   colour nobody can name is worse than one that is shared. The icon tells
   them apart, which is what the icon is for. */
.bubble.marker[data-type="guidance"] {
  background: var(--symptom-tint);
  border-color: var(--symptom-line);
}

/* STILL WAITING FOR A NUMBER. A dashed edge, and that is the entire signal:
   it was a second background colour and a greyed figure before, which said
   "less certain" by making the thing harder to read. */
.bubble.in[data-estimate="none"],
.bubble.in[data-estimate="instant"] { border-style: dashed; }

.bubble[data-pending] { opacity: 0.6; }
.bubble[data-grouped] { opacity: 0.85; }
/* Where a capture came from, said with a chip in its foot rather than a
   stripe down its edge -- the design language has no side stripes, and a
   3px bar is a fourth border weight nothing else in the app uses. */
.bubble[data-source="whatsapp"] .bubble-foot::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--accent);
  flex: 0 0 auto;
}
.bubble img { width: 100%; max-width: 220px; border-radius: var(--r-field); display: block; }
.bubble-transcript { font-size: 0.9rem; }

/* The app talking about itself -- that the reviewed pass ran, and what it
   did. Centred text across the conversation rather than a bubble in it: a
   bubble is something said TO you, and three of these a day carrying that
   weight read like someone talking. */
.notice {
  align-self: center;
  max-width: 85%;
  display: flex;
  gap: 6px;
  align-items: flex-start;
  padding: 0.15rem 0.5rem;
  text-align: center;
  font-size: 13px;
  line-height: 18px;
  font-weight: 500;
  /* The pass may write more than one line, and without this they collapse
     into one run-on sentence. */
  white-space: pre-wrap;
}
.notice .notice-mark { color: var(--accent); flex: 0 0 auto; display: flex; padding-top: 1px; }

/* THE TOP ROW of an app entry: what kind it is, and where it happened. The
   time used to lead this row and now lives in the foot, so a column of
   bubbles states its times in one column. */
.bubble-meta { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; }
.tag { color: var(--accent); }

/* THE FOOT. Every bubble has one, in the same place, whichever speaker it
   belongs to -- which is the half of this the old layout did not have.

   THE HOUR AT ONE END, THE ••• AT THE OTHER. They were a pair pushed into
   the corner together, so the thing you read and the thing you press sat on
   top of each other -- and on a workout, which has no •••, the lone hour
   ended up at the opposite end from every hour above it.
   Full width with the time leading and the ••• pushed to the far end: the
   hour is always at the bubble's start edge, whatever else the foot holds,
   and the control is always at the other. */
.bubble-foot {
  display: flex;
  align-items: center;
  gap: 2px;
  width: 100%;
  min-height: 32px;
}
.bubble-foot time { font-size: 12px; line-height: 16px; }
.bubble-foot .more {
  width: 36px;
  height: 32px;
  /* TO THE FAR END, and it has to be said HERE. This was a rule of its own a
     few lines up, which never applied: the `margin: 0` shorthand below it
     reset the whole margin at equal specificity and later in the file, so the
     ••• sat glued to the hour on every bubble -- measured at 1440px, 115px
     from the end of a short bubble and 510px from the end of a long one,
     while the auto margin was supposed to put it at 0.
     One declaration, after the shorthand that would otherwise undo it. */
  margin: 0;
  margin-inline-start: auto;
  border: 0;
  background: transparent;
  color: var(--affordance);
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The figures the entry exists to deliver. */
.reply-numbers { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 13px; }
/* The macros give way before the figure does: they are the part of the row
   that can be read at a glance from half of it. */
.reply-numbers .macros { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* What the pass decided a capture was, drawn rather than typed (see
   public/icons.js). `data-type` paints it from the same token the bubble's
   border uses, which a glyph could never take. */
.type-icon { flex: 0 0 auto; display: flex; line-height: 1; }
.type-icon[data-type="meal"] { color: var(--food); }
.type-icon[data-type="workout"] { color: var(--workout); }
.type-icon[data-type="symptom"] { color: var(--symptom); }

/* A capture that is not a meal: an icon, a word, a time, and nothing else.
   Narrow rather than full-width so it reads as a stamp on the conversation
   instead of something the app said at length. */
/* Built like a meal bubble: a meta row of icon and clock across the
   top, then what it was, then the figure. It used to be one horizontal
   line, which made a workout read as a different KIND of thing from
   the meals around it when it is the same kind with a different sign. */
.bubble.marker {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  width: fit-content;
  max-width: 80%;
  padding: 0.4rem 0.6rem;
  color: var(--ink);
  font-size: 0.85rem;
}
.marker-meta { display: flex; align-items: center; gap: 0.4rem; }
.marker-label { color: var(--ink); }
/* A workout's two lines. Only the body stacks -- the icon and the time
   stay centred against it as one row, which is why this is a nested
   element and not two spans wrapping inside the marker. */
.marker-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
/* The burn, in the colour this app uses for something that subtracts.
   It is the figure anybody reads a workout marker for, and the colour
   says which direction it goes without spending a word on it. Tabular,
   like every other number here, so a column of them lines up. */
.marker-burn {
  color: var(--danger);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* What the reviewed pass could not work out on its own. Sits under the meal
   it is about, on the same side, but visually subordinate to it — smaller,
   muted, no strong border — since it is a footnote to that meal's numbers,
   not a second thing said. */
.bubble.question {
  align-self: flex-end;
  background: transparent;
  border-color: var(--line);
  color: var(--ink);
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.bubble.question .bubble-text { color: var(--ink); }
.marker-time { font-variant-numeric: tabular-nums; }
/* The name sits under the meta row now. */
.reply-label { margin-top: 0.15rem; }
/* Where it was eaten, inline at the far end of the meta row rather than a
   row of its own. This was `margin-left`, chosen over the logical property
   because the page was always RTL and the logical one would have pushed it
   to the near corner. The bubble is no longer forced LTR, so "the far end"
   is now exactly what `margin-inline-start: auto` means. Google's own display names run
   long ("Amore Mio Tel Aviv Port"), and this row must stay one line: `min-
   width: 0` lets a flex child shrink below its content width at all, and
   the three `white-space`/`overflow`/`text-overflow` rules turn that
   shrinking into a clean ellipsis instead of a wrap. */
.reply-place {
  margin-inline-start: auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 0.78rem;
  color: var(--ink);
}
/* ROOM BETWEEN THE MARK AND THE WORD. The icon and the text were bare inline
   siblings with nothing between them -- no gap, because this is not a flex
   container and cannot become one without giving up the ellipsis, which needs
   the overflow to be on the same box as the text.
   So the space goes on the icon. The baseline nudge comes with it: a 14px
   square sitting on the text baseline rides high against a 0.78rem line. */
.reply-place svg {
  margin-inline-end: 5px;
  vertical-align: -2px;
}
/* THE FIGURE THE ENTRY EXISTS TO DELIVER, as a filled chip in its own
   type's colour. It was bold text on the bubble's background, which made
   the number the same kind of object as the words beside it. */
.reply-numbers .kcal,
.marker-burn {
  padding: 3px 10px;
  border-radius: var(--r-pill);
  font-size: 13px;
  line-height: 18px;
  font-weight: 700;
  white-space: nowrap;
}
.bubble[data-type="meal"] .kcal { background: var(--food); color: var(--food-on); }
.bubble[data-type="workout"] .kcal,
.bubble[data-type="workout"] .marker-burn { background: var(--workout); color: var(--workout-on); }
.bubble[data-type="symptom"] .kcal { background: var(--symptom); color: var(--symptom-on); }
/* GREEN ONCE IT IS SETTLED. The meal hue fills the chip while the figure is
   still the app's own working estimate; once the reviewed pass has been over
   it -- or the person corrected it by hand, which is settled and then some --
   it takes --ok. Asked for from the desktop as "reviewed needs to be green
   and not yellow", and it matches the meta-row tint beside it, which has said
   green-for-reviewed all along.
   Meals only: a workout's chip is the workout hue, and that says which KIND
   of entry it is, which outranks which pass priced it.
   `:not([data-kcal-flag])` rather than a specificity contest: a big meal's
   chip stays the elevated or danger fill whether or not it has been reviewed,
   and spelling that out here is safer than relying on these two rules landing
   in the right order. */
.bubble[data-type="meal"] .kcal[data-estimate="reviewed"]:not([data-kcal-flag]),
.bubble[data-type="meal"] .kcal[data-estimate="manual"]:not([data-kcal-flag]) {
  background: var(--ok);
  color: var(--ok-on);
}
/* NOT PRICED YET: the chip is an outline instead of a fill, dashed like the
   bubble's own edge. Same words, same size, same place -- what changed is
   whether the number has been settled, and an outline says that without
   making it harder to read. */
.bubble.in[data-estimate="none"] .kcal,
.bubble.in[data-estimate="instant"] .kcal {
  background: transparent;
  color: var(--ink);
  border: 1px dashed var(--food);
  padding: 2px 10px;
  font-weight: 600;
}
/* Not the reply bubble's own dot any more — that one is gone, replaced by
   `data-estimate` on the kcal figure. This still styles summary.js's day-
   sheet dot, which carries the same reviewed/instant distinction in the
   expanded per-meal table. */
.dot { width: 8px; height: 8px; border-radius: 50%; border: 1px solid var(--accent); }
.dot[data-source="reviewed"] { background: var(--accent); }

/* Fixed, not in the flow: it has to sit in the middle of an empty screen,
   and an empty screen has no flow to sit in the middle of. pointer-events
   stay off so it never swallows a tap meant for the composer beneath it. */
.chat-loading { position: fixed; inset: 0; display: grid; place-items: center; pointer-events: none; z-index: 2; }
.chat-loading[hidden] { display: none; }
/* The app mark rather than a generic ring. The element no longer draws
   anything itself -- the SVG inside it does -- and it is the ARC that turns,
   not the whole drawing: the plate stays level and the ring sweeps round it,
   which is the icon doing what the icon already means. */
/* THE MARK AND THE NAME, STACKED. See #chat-loading in index.html. */
.chat-loading-mark {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}
/* The name at the size the header draws it, which is the size somebody has
   just seen on the launch image. */
.chat-loading-mark .app-title {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 700;
}
/* 88, up from 32. At 32 this was the size of a button's icon floating in an
   empty screen; the launch image before it is the same mark at a third of the
   width of the phone, and the step between the two read as two apps. */
.spinner { width: 88px; height: 88px; display: block; }
.spinner svg { width: 100%; height: 100%; display: block; }
.spinner-arc {
  transform-origin: 50% 50%;
  animation: spin 1.1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Still turns, so it still reads as "working", just without the fast motion
   that reduced-motion asks us to spare. */
@media (prefers-reduced-motion: reduce) { .spinner-arc { animation-duration: 3s; } }

.undo { background: transparent; color: var(--ink); padding: 0.25rem 0; font-size: 0.8rem; }
.actions { margin-top: 0.35rem; display: flex; gap: 0.9rem; flex-wrap: wrap; }
/* `display: flex` outranks the `hidden` attribute's own display: none, so the
   row would sit open on every bubble — which is the opposite of hidden. */
.actions[hidden] { display: none; }
/* Quiet text, not buttons. Chrome-heavy pills inside a bubble competed with
   the message for attention and stole a line of height from every bubble
   they opened in. These read as what they are — small actions on something
   you already tapped — and the row costs almost nothing. */
.actions button {
  background: transparent;
  color: var(--affordance);
  border: 0;
  border-radius: 0;
  padding: 0.1rem 0;
  font-size: 0.78rem;
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, var(--ink) 45%, transparent);
}
/* Save is the one action here with a consequence, so it is the one that is
   not grey — still text, just legible as the primary of the pair. */
.actions button.primary { color: var(--accent); text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.actions button.danger { color: var(--danger); text-decoration-color: color-mix(in srgb, var(--danger) 40%, transparent); }
.actions button:disabled { opacity: 0.5; }

/* Pushed to the far end of the meta row and sized as a real tap target: this
   is the only thing on a bubble that says the actions exist at all. */
/* THE ARROW HAS TO BE SEEN, BUT IT IS NOT THE POINT OF THE ROW.
   At 1rem in --muted it read as punctuation, and the control it opens
   is the only way to correct a meal. The fix was the SIZE, not the
   colour: at 1.35rem a muted triangle is unmissable, while --ink made
   it the brightest thing on a row whose job is to show a number.
   `margin-inline-start: auto` is what puts it at the far end of
   whichever row it sits in -- the calories row on a meal, the meta row
   on a capture. */
.more {
  margin-inline-start: auto;
  flex: 0 0 auto;
  background: transparent;
  /* Grey, so it reads as a control rather than as punctuation at the end
     of the line. See --affordance. */
  color: var(--affordance);
  /* No padding on the end side: the row already has its own inset, and
     padding here pushed the glyph inward so it sat short of the edge
     every other element on the bubble lines up against. */
  padding: 0;
  font-size: 1.35rem;
  line-height: 1;
  font-weight: 700;
  /* Centred in the row rather than sitting on its baseline. The row is
     `align-items: center`, but a button is a block whose glyph sits at
     the top of its own taller box -- so the arrow rode high against the
     calorie figure beside it. A grid with `place-items: center` puts
     the glyph in the middle of whatever height the button takes. */
  display: grid;
  place-items: center;
  /* The glyph is small; the thing a thumb has to hit is not. Height
     matched to the line it sits on, so the tap target does not make
     the row taller than the figure needs. */
  min-width: 34px;
  min-height: 1.6rem;
}

.bubble-edit {
  width: 100%;
  margin-top: 0.25rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  resize: vertical;
}
.play { background: var(--accent); padding: 0.25rem 0.6rem; border-radius: 999px; }

/* The per-meal breakdown, opened by tapping the day's pill. It used to hang
   off a summary message at the day's end; that message repeated numbers the
   pill already carries, so only this drawer survived it.

   A sibling of .sep rather than a child, because .sep is sticky and a sticky
   element cannot leave its containing block — nesting this inside would pin
   the pill to the drawer instead of to the whole day. Deliberately NOT
   --surface, which is what the message bubbles use: this is the app talking
   about the day, not something anyone said. */
.summary-expand:not([hidden]) {
  align-self: center;
  width: 100%;
  text-align: start;
  padding: 0.5rem 0.75rem;
  border-radius: 14px;
  background: var(--surface);
  margin: 0 0 0.4rem;
  font-variant-numeric: tabular-nums;
}

/* What you have typed before, offered while you type it again. Sticky to the
   same edge as the composer so it rides above the keyboard with it, and
   scrolls sideways rather than wrapping — a strip that grows upward would
   push the field off a short screen. */
.suggestions {
  position: sticky;
  bottom: var(--composer-height, 6rem);
  z-index: 2;
  display: flex;
  gap: 0.4rem;
  overflow-x: auto;
  /* No scrollbar on a phone, and no scrollbar gap on a desktop either. */
  scrollbar-width: none;
  padding: 0.35rem 1rem;
  background: var(--bg);
}
.suggestions::-webkit-scrollbar { display: none; }
.suggestion {
  flex: 0 0 auto;
  max-width: 70%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 0.35rem 0.7rem;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink);
  font-weight: 400;
  font-size: 0.85rem;
}

/* The quick-log strip: a sibling of .suggestions occupying the same seat
   above the composer, never shown at the same time — see app.js. Same
   sticky positioning for the same reason: it has to ride above the keyboard
   with the composer, not get left behind under it. */
/* INSIDE THE LOG PANEL NOW, under Send. It used to be sticky against
   the top of the dock -- `bottom: var(--composer-height)` -- because
   it lived in the page above it and had to hover there. In the panel
   it is an ordinary block: the dock is already sticky as one piece,
   and a second sticky element inside it fought the first every time
   the field grew a line. */
/* The top padding framed a heading that is gone now. Keeping it would be
   the same stolen row with nothing in it. */
.quick-log {
  padding: 0.25rem 0.1rem 0.1rem;
}
.quick-log-place {
  font-size: 0.78rem;
  color: var(--ink);
  padding: 0 0.1rem 0.25rem;
}
/* The next card peeks in for the same reason the runs table's third
   column does: a row that stops at the screen edge reads as the whole
   row, and nobody swipes what looks complete. .quick-log-dish caps at
   78% of the width, which is what leaves that sliver. */
.quick-log-dishes {
  display: flex;
  /* Top-aligned: the cards are two lines or one depending on the name,
     and centring them would leave the short ones floating. */
  align-items: flex-start;
  gap: 0.4rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.quick-log-dishes::-webkit-scrollbar { display: none; }
/* A CARD, NOT A PILL.
   These were one line with the label ellipsised, which on a phone meant
   most dishes read "grilled sea bass with roasted vegetab..." -- the
   words that distinguish one suggestion from another are the ones at the
   end, so truncating there hid exactly what you are choosing between.
   Now the name wraps to two lines and the figure sits under it, which is
   what makes the chip readable without making the row taller than the
   two lines it needs. */
.quick-log-dish {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
  /* TWO CARDS AND THE START OF A THIRD, at every width. Asked for as "show
     up to two items and leave enough room to scroll right to see more
     options so show 2 plus the beginning of the 3rd".
     11rem alone did that on a 390px phone and not on a 320px one, where
     two cards plus the gap overflow and the second is clipped instead of
     the third peeking. Capping each at 42% of the row makes two of them
     84%, which leaves the rest for the sliver whatever the screen is.
     Wider than the old 70% either way, because a wrapped name in a narrow
     box is four lines rather than two. */
  width: min(11rem, 42%);
  padding: 0.45rem 0.7rem;
  /* A rounded rectangle rather than a pill: a two-line pill is a lozenge
     with its corners eating the text. */
  border-radius: 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink);
  font-weight: 400;
  font-size: 0.85rem;
  text-align: start;
}

/* Two lines, then an ellipsis. -webkit-box is the only line clamp iOS
   Safari has, and iOS is the phone this runs on. The unprefixed
   `line-clamp` rides along for everything else. */
.quick-log-label {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* The figure, under the name and allowed its own two lines: "430 kcal"
   fits on one, but a translated unit or a longer number must wrap rather
   than widen the card or vanish. */
.quick-log-dish .kcal {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  white-space: normal;
  color: var(--ink);
  font-weight: 600;
}
/* Same muted stance the reply bubble takes on its own kcal figure: an
   instant estimate may still move, so it reads quieter than a reviewed one
   rather than borrowing a certainty it has not earned. */
/* A number that is still only a guess says so with its edge, not with its
   legibility: solid once the reviewed pass has priced it, dashed while it
   is the Worker's seconds-old estimate. It used to be greyed, which made
   "less certain" and "less readable" the same thing. */
.quick-log-dish .kcal[data-estimate="instant"] {
  border: 1px dashed var(--food-line);
  border-radius: var(--r-pill);
  padding: 0 6px;
}

/* Jump-to-newest. Parked just above the composer, whose height is measured
   at runtime — it changes as the field grows — so the button never lands on
   top of the text you are writing. */
.to-bottom {
  position: fixed;
  bottom: calc(var(--composer-height, 6rem) + var(--keyboard-inset, 0px) + 0.6rem);
  /* Against the COLUMN, not the window. Fixed positioning is relative to the
     viewport, so on a wide screen this sat at the far edge of the monitor,
     a hand-span away from the conversation it jumps to the bottom of. */
  inset-inline-end: max(1rem, calc((100vw - var(--column)) / 2 + 1rem));
  z-index: 4;
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  font-size: 1.1rem;
  line-height: 1;
}
/* `display` beats the hidden attribute, so this counterpart is required —
   the same trap that once left the weekly panel permanently on screen. */
.to-bottom[hidden] { display: none; }

/* The round trip to the model takes several seconds. Dimmed while it is in
   flight — but NOT disabled: a disabled button fires no click, and the tap
   that collapses the drawer is one of only three ways out of it. Taking it
   away for the seconds the drawer is emptiest is the worst moment to take
   it away. What stops the second request is a flag in app.js; this only
   says the control is busy. */
#what-to-eat[data-loading] { opacity: 0.6; }

/* The answer. Larger than the app's usual status text — it is a sentence
   meant to be read, not a number meant to be glanced at.
   No direction pin any more: most of what lands here is the app's own words
   out of the catalogue — the budget line, "why", the two buttons — with the
   model's dish and place names threaded through it. That makes it chrome,
   so it reads the way the interface reads. (The model still names dishes in
   English by construction, see src/worker/instant.ts; each name is its own
   bidi run and renders intact either way.) */
#advice-content { font-size: 0.95rem; line-height: 1.5; }

/* Where and when: two facts the app already holds, side by side. */
.advice-where {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.85rem;
}
/* Pushed to the end whatever else is in the row: with no location shared
   there is no place name beside it, and space-between alone would leave the
   clock stranded at the start of the line. */
.advice-where .advice-clock { color: var(--ink); margin-inline-start: auto; }

/* The day's budget: a secondary line, not the answer. */
.advice-budget {
  margin-top: 0.6rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.8rem;
  color: var(--ink);
}

/* The block layout these described is gone -- the panel is a table now
   (.advice-table below), one row per dish. .advice-place-block,
   .advice-place-name, .advice-dish and .advice-kcal went with it, and
   are deleted rather than left: a stylesheet full of rules for markup
   nothing renders is how the next person styling this panel spends an
   hour on a selector that never matches. The figure and its source
   keep their own rules, beside the table that still draws them. */
/* The reading a weigh-in reports. Larger than the words above it, because
   the number IS the capture -- the text was only how it was said. No tint
   and no macros: this is not a meal and must not be read as one. */
.bubble-weight {
  margin-top: 0.25rem;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink);
}

/* The first-run card, above the conversation rather than over it. It is
   a suggestion, not a gate: the app works perfectly well with no height,
   and every figure derived from one simply disappears. */
.body-card {
  margin: 0 0 0.75rem;
  padding: 0.75rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
}
.body-card[hidden] { display: none; }
.body-card-head { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; font-weight: 600; }
.body-card-why { margin: 0.3rem 0 0.7rem; font-size: 0.85rem; color: var(--ink); }
.body-card-fields { display: flex; flex-direction: column; gap: 0.5rem; }
.body-card-fields label { display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
.body-card-fields input,
.body-card-fields select {
  /* 16px, or iOS zooms the page when one is tapped. */
  font-size: 16px;
  padding: 0.3rem 0.5rem;
  min-width: 9rem;
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 8px;
}

/* One run per row: when and which pass on top, the details under it.
   A failed run, and one that never reported finishing, both have to read
   louder than a quiet row -- an open run is the failure this ledger exists
   to surface. */
.ledger-run { padding: 0.5rem 0; border-bottom: 1px solid var(--line); }
.ledger-run:last-child { border-bottom: 0; }
.ledger-run-head { display: flex; justify-content: space-between; gap: 0.75rem; align-items: baseline; }
.ledger-run-when { font-weight: 600; color: var(--ink); }
.ledger-run-tier { font-size: 0.8rem; color: var(--ink); }
.ledger-run-detail { font-size: 0.85rem; color: var(--ink); margin-top: 0.15rem; }
.ledger-run[data-bad] .ledger-run-when,
.ledger-run[data-open] .ledger-run-when { color: var(--danger); }

/* The profile's figures: a label on one side, its number on the other.
   A plain definition list in flex rows -- no chart, no sparkline. The
   whole point of this screen is a handful of numbers read at a glance. */
.profile-figures { display: flex; flex-direction: column; gap: 0.1rem; margin-bottom: 1rem; }
.profile-figure {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--line);
}
.profile-figure:last-child { border-bottom: 0; }
.profile-figure-label { color: var(--ink); font-size: 0.9rem; }
.profile-figure-value { font-size: 1.05rem; font-weight: 600; color: var(--ink); }

/* The permission rows. Same shape as a profile field -- a name on one
   side, its answer on the other -- because that is what they are: facts
   about this device with one of them actionable. */
/* Paging through the history, only when the sheet was opened on purpose. */
.whats-new-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
}
.whats-new-nav[hidden] { display: none; }
.whats-new-nav button {
  padding: 0.3rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 0.82rem;
  cursor: pointer;
}
.whats-new-nav button:disabled { opacity: 0.45; cursor: default; }
.whats-new-position { font-size: 0.8rem; color: var(--affordance); font-variant-numeric: tabular-nums; }

.permissions-list { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 0.6rem; }
.permissions-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--line);
}
.permissions-row:last-child { border-bottom: 0; }
.permissions-name { min-width: 0; overflow-wrap: anywhere; }
/* Muted, because it is a statement and not a control. The one row that IS
   a control looks like one. */
.permissions-state { color: var(--affordance); font-size: 0.82rem; flex: 0 0 auto; }
.permissions-row[data-state="denied"] .permissions-state { color: var(--warn, var(--accent)); }
/* The app-level switch, beside the browser's read-only answer. Small and
   plain: it is a second control in a row that already has a statement, and
   making it look like a primary action would suggest the row has one. */
.permissions-use {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 0 0 auto;
  font-size: 0.82rem;
  color: var(--affordance);
  cursor: pointer;
}
.permissions-use input { margin: 0; }

.permissions-ask {
  flex: 0 0 auto;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 0.82rem;
  cursor: pointer;
}

.profile-fields { display: flex; flex-direction: column; gap: 0.6rem; }
.profile-fields label { display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
.profile-fields input,
.profile-fields select {
  /* 16px so iOS does not zoom the page when one is tapped -- the same
     threshold the composer field has to meet. */
  font-size: 16px;
  padding: 0.35rem 0.5rem;
  min-width: 9rem;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 8px;
}

.advice-why { margin-top: 0.3rem; font-size: 0.85rem; color: var(--ink); }
/* The log link sits under its own dish. Left to the row layout it would
   stretch; inline-block keeps it the width of its words. */

/* WHERE THE MENU CAME FROM. A footnote, not a finding: same size as the
   nearby rows, with the top rule that separates it from the dishes it is
   about. It is read once, when somebody wonders why a dish list looks like
   another restaurant's. */
.advice-menu-source {
  margin-top: 0.75rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
  font-size: 0.85rem;
}

/* What's open nearby: display only, so it reads as quieter than the answer
   above it. */
.advice-nearby {
  margin-top: 0.75rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
}
.advice-nearby h3 { margin: 0 0 0.4rem; font-size: 0.78rem; font-weight: 600; color: var(--ink); }
.advice-nearby-row { display: flex; justify-content: space-between; gap: 0.5rem; padding: 0.2rem 0; font-size: 0.85rem; }
.advice-distance { color: var(--ink); }

/* What to do about it, laid out like the sheet's other button rows. */
/* align-items: center keeps the linkish button (thinner padding, no fill)
   sitting on the same baseline as the filled buttons beside it instead of
   stretching to match their height. */
.advice-actions { margin-top: 0.85rem; display: flex; gap: 0.6rem; flex-wrap: wrap; align-items: center; }

/* Explanatory prose above a sheet's controls — the app's own words, so it
   reads with the interface. The LTR pin it used to carry was for English
   text in a hardcoded RTL page. */
.overlay-note { margin: 0 0 0.5rem; font-size: 0.78rem; color: var(--ink); }

/* One recognized place: its label takes the room, its two buttons take what
   they need. The label wins the space because the name and visit count are
   what the list is for. */
/* COLUMNS, LIKE THE SUGGEST TABLE.
   It was one run-on sentence -- "Nono - 4x, last Tuesday" -- with a
   star tacked on the end, so nothing lined up down the list and the
   eye had to re-find the name on every line. A grid instead: marks,
   name, when, and the ⋯ at the end.
   Grid rather than <table> because each row is followed by its own
   actions block and, sometimes, the naming picker -- children a table
   row cannot hold. */
.location-row {
  display: grid;
  /* Mark, place, ⋯. It was four tracks -- the count and the date each had
     one -- and they are one stacked cell now. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 0.6rem;
  align-items: center;
  /* A row of its own rather than a line in a list: the padding and the 1px
     rule are what separate one place from the next now that the rows are
     two lines tall. */
  padding: 0.5rem 0;
  font-size: 0.85rem;
}
/* ON TOP, AND NOT ON THE FIRST ROW. A rule under each row would land between
   a place and its OWN actions -- the actions block is the row's next sibling
   in the list, not a child of it -- so an opened place would read as grouped
   with the one below it. A rule above each row but the first closes the
   previous place after whatever it had open.
   `:first-of-type` is by tag and every child here is a div, so it matches the
   first row and nothing else. (`:last-of-type` would NOT have matched a row
   at all: the last div in the list is an actions block.) */
.location-row:not(:first-of-type) { border-top: 1px solid var(--line); }

/* THE MARK: one glyph, in its own column, which is what gives the list a
   straight edge to read down. A favourite takes the accent and a tinted
   disc; every other place takes the quiet pin. The disc is what the old
   layout had no equivalent of -- four text columns had no anchor, so each
   row started wherever its first character did. */
.location-mark {
  width: 28px;
  height: 28px;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  color: var(--affordance);
}
.location-mark[data-kind="favorite"] {
  color: var(--accent);
  border-color: var(--food-line);
  background: var(--food-tint);
}
/* HOME WEARS A COLOUR TOO. It was the plain grey pin every ordinary place
   gets, which made the one row in this list you did not discover look like
   the ones you did. The star's treatment, in the workout blue rather than the
   accent: home outranks a favourite, and giving it the same yellow would say
   the two are the same kind of thing. */
.location-mark[data-kind="home"] {
  color: var(--workout);
  border-color: var(--workout-line);
  background: var(--workout-tint);
}

/* THE PLACE, AND THEN THE QUIET LINE UNDER IT. Two rows in one grid cell,
   so the name owns the line and the facts about it sit beneath rather than
   beside -- which is what stops a column of "1x" from taking the same
   visual weight as the name of the place. */
.location-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.location-name {
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.3;
}
/* The separator is drawn, not typed: a "·" in the markup would be a
   character the two spans have to agree about, and in Hebrew it would sit
   on the wrong side of whichever one came first. A border on the second
   child mirrors with the page for free, and disappears on a row that
   somehow has only one fact to state. */
/* The street, under the name and above the counts. Quieter than the name and
   louder than the facts below it: it answers "where is this", which is the
   second thing you ask of a row whose name is somebody's own word for it. */
/* A DEVICE, AND WHETHER IT HAS EVER SPOKEN. The second line is the fact worth
   showing: a token that has never been used looks exactly like one set up
   wrong, and nothing else tells them apart. */
/* NOT `.sheet-actions`. That class is `position: sticky; bottom: 0` -- it is
   the sheet's pinned footer, and there can only be one. Using it for the
   connect button pinned a second row over the content, so the key and the
   steps below it floated instead of scrolling. */
.device-actions { display: flex; gap: 10px; margin: 0.6rem 0 0; }

.device-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}
.device-row:last-of-type { border-bottom: 0; }
.device-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.device-label { font-weight: 600; overflow-wrap: anywhere; }
.device-meta { font-size: 0.76rem; color: var(--affordance); }

/* THE SECRET, SHOWN ONCE. It wraps rather than scrolls, because the whole
   point is that it can be read and copied in the moment it exists -- it is
   stored as a hash and there is no second chance. */
.device-secret {
  margin: 0.4rem 0 0;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--food-line);
  border-radius: var(--r-field);
  background: var(--food-tint);
}
.device-secret-title { margin: 0 0 0.4rem; font-weight: 600; font-size: 0.82rem; }
/* The key and its copy button on one line, the key wrapping rather than
   scrolling: it exists for exactly as long as this box is open, so it has to
   be readable and selectable in that moment. */
.device-secret-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}
.device-secret-row code {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.72rem;
  overflow-wrap: anywhere;
  user-select: all;
}
.device-secret-row button { flex: 0 0 auto; }
/* The three steps, numbered, because this is a sequence somebody follows once
   on a different device from the one they are reading it on. */
.device-steps {
  margin: 0.6rem 0 0;
  padding-inline-start: 1.1rem;
  font-size: 0.78rem;
  color: var(--ink);
}
.device-steps li { margin-bottom: 0.25rem; }

.location-address {
  font-size: 0.76rem;
  color: var(--ink);
  opacity: 0.75;
  overflow-wrap: anywhere;
}

.location-meta {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.78rem;
  color: var(--affordance);
}
.location-meta > * + * {
  padding-inline-start: 0.45rem;
  border-inline-start: 1px solid var(--line);
}
.location-count { font-variant-numeric: tabular-nums; }
.location-when { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The actions row and the naming picker each span the full width of the row
   they belong to, rather than sitting in one of its columns. */
.location-actions, .location-picker { grid-column: 1 / -1; }

/* The naming picker, opened under the row it names. Wraps because a
   suggestion is a place name, and place names are not short. */
.location-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-items: center;
  margin: 0 0 0.75rem;
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--surface);
}
.location-picker input { flex: 1; min-width: 8rem; }
/* The results take the picker's full width and wrap among themselves, so a
   search that returns four long place names does not push the search box
   itself off the row. */
.picker-results { flex: 1 0 100%; display: flex; flex-wrap: wrap; gap: 0.35rem; }

/* The primary pair -- name it yourself, then Save -- leads the picker.
   Its own full-width row so it reads as the main action, not one field
   among several fighting for the same line. */
.picker-name-group { flex: 1 0 100%; display: flex; gap: 0.35rem; align-items: center; }
.picker-name-group input { font-weight: 600; }
.picker-name-group button[data-save] { background: var(--accent); color: var(--bg); border-color: var(--accent); }

/* The Maps search sits last, its own group so its caption ("this spends a
   lookup") reads right above the field it describes rather than floating
   loose in the picker. */
.picker-search-group { flex: 1 0 100%; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.picker-search-group .overlay-note { flex: 1 0 100%; margin: 0; }

/* A list's heading inside the same sheet. Subordinate to the sheet's own h2 —
   one screen, two lists, not two screens. The first one sits right under the
   sheet head, so it does not need the gap that separates the second from the
   textarea above it. */
.overlay-sub {
  margin: 1.25rem 0 0.25rem;
  font-size: 0.95rem;
  font-weight: 600;
}
.overlay-sub-first { margin-top: 0.25rem; }
/* BOTH free-text preference boxes, not just the first.
   #kitchen-text never had a rule of its own and fell back to the
   browser's default textarea font -- a different size and family from
   the box directly above it, in a sheet where the two are the same
   kind of thing and are read together. */
#avoid-text,
#kitchen-text {
  width: 100%;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0.6rem;
  font: inherit;
  font-size: 0.9rem;
  resize: vertical;
}/* THE WHOLE SHAPE IS ONE COLOUR WHEN A DRAWER IS OPEN.
   The chosen tab fills with --accent and the panel it opened is outlined
   in --accent, but the dock's own top rule stayed --line -- a grey line
   drawn straight across the top of an otherwise orange shape. It reads as
   the outline failing to close. */
/* The dock's top rule stays the line colour. It turned accent while a
   drawer was open, to close the outline around the filled tab that opened
   it -- there is no tab, and an orange line across an otherwise dark dock
   reads as something highlighted for no reason. */

/* Two rows: the text field gets the full width on top, the buttons sit
   under it. On a phone the buttons were squeezing the field into a narrow
   strip, which is the thing you actually type into. */
/* gap: 0. The tab row and the panel below it share a border, so any gap
   between them is a visible break in a single outline. Each part supplies
   its own padding instead. */
.composer { display: flex; flex-direction: column; gap: 0; align-items: stretch; padding: 0.5rem 1rem; background: transparent; }/* One column: the tab row, then whichever panel was chosen. Nothing here
   hides a piece of the composer any more -- the two panels are siblings
   below a fixed row, so showing one and hiding the other is the whole of
   it. The old shape shared one box between the tab and the field, which
   needed a rule per piece (and got one of them wrong, hiding the tab row
   Suggest needed in order to close itself). */


/* The drawer itself. Capped and scrollable: three places with two dishes
   each is taller than a phone once the basis section is under it, and a
   drawer that grows past the top of the screen takes its own close control
   with it. 60vh leaves the conversation visible above, which is what makes
   this a drawer rather than a sheet. */
/* No border between this and the tab row below it. The row is what the
   panel opened from, and a hairline between them read as two separate
   objects stacked rather than one drawer that had grown. */
.suggest-drawer {
  max-height: 60vh;
  overflow-y: auto;
  /* THE GESTURE STOPS HERE. Without this, reaching the end of the answer
     hands the rest of the drag to the nearest scrollable ancestor -- the
     diary -- so scrolling the drawer scrolled the conversation behind it.
     The sheets got this when the same thing was reported about them; the
     Suggest drawer is not an .overlay, so neither that rule nor the body
     lock beside it ever reached here. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 0.75rem 1rem 0.25rem;
}
/* `display` is not set above, so the `hidden` attribute still works on its
   own — but the overflow/max-height pair is exactly the sort of rule that
   tempts a later `display: flex`, and that is what left the weekly panel
   permanently on screen once. Stated rather than implied. */
.suggest-drawer[hidden] { display: none; }
.suggest-drawer:focus { outline: none; }/* The composer, below the row. Shown only when Log is the chosen tab.
   `display: none` rather than the `hidden` attribute: the panel holds the
   reply chip and the pending photos, and app.js reads both -- an attribute
   toggled from two places is how reply mode went invisible once already. */
/* ONE OUTLINE, FROM THE CHOSEN TAB AROUND EVERYTHING IT OPENED.
   The panel is not a separate box below the row -- it is the tab's own
   surface continuing downward, so the two share a border and the seam
   between them is erased. That is what makes the composer read as having
   come OUT of the Log tab rather than as a second object that happened to
   appear under it.
   Note the direction: an earlier version of this did the same trick
   upward, because Suggest's panel used to sit above the bar. Same idea,
   opposite edge.
   The panel's own top corners stay square on purpose: a rounded corner
   directly under the tab would draw a notch where the two meet. */
/* Only the Suggest drawer keeps an outline, because it is a panel that
   opens over the composer and needs an edge. The composer itself had one
   too -- accent, to close the shape around the filled tab that opened it --
   and with no tab it was an orange box drawn around the whole dock. */
.composer-dock[data-open="suggest"] .suggest-drawer {
  border: 1px solid var(--line);
  border-radius: var(--r-field);
}

.log-panel { display: flex; flex-direction: column; padding: 0.35rem 0.4rem 0.5rem; }

/* The field and the attach icons, side by side. The icons used to have a
   row of their own above the field, which spent a whole line on three
   glyphs. */
/* `flex-start`, not `center`: the field now grows from one line to three,
   and centring would walk the attach icons down the side as it grows.
   They belong beside the first line, where the caret is. */
/* THE BOX. It was a pill with Send inside its end, and the pill's corners
   were rounder than anything else on the dock -- rounder than the buttons
   directly above it, which is what the owner noticed. 16px is their radius,
   so the dock reads as one family of shapes.

   A column, not a row: the words on top and the controls under them. That
   is what lets the box hold three controls without any of them taking width
   from the thing you are typing into, which is what the pill could not do. */
.log-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 4px 8px 6px;
  border-radius: 16px;
  border: 1px solid var(--line);
  background: var(--surface);
}
/* The field stops carrying its own inset now that the box has one. */
.log-row .field-wrap { padding-inline: 6px; }

/* THE CONTROL ROW, under the words. The plus opens a picture at the start
   side; the recorder and Send sit at the end, which is where a thumb
   already is when it has finished typing. */
.log-controls {
  display: flex;
  align-items: center;
  gap: 4px;
  min-height: 40px;
}
.log-controls-end { margin-inline-start: auto; display: flex; align-items: center; gap: 4px; }
.log-control {
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  /* Grey, like the ••• and Correct: these are things you can press, and at
     full ink they read as part of the box rather than as controls in it.
     See --affordance. */
  color: var(--affordance);
  display: flex;
  align-items: center;
  justify-content: center;
}
.log-control:active { background: var(--own); }
.log-control[data-open="true"] { color: var(--accent); }
.log-control[hidden] { display: none; }

/* WHAT THIS IS: the four declarations, after the camera and the file picker.
   A group with no separator and no box: they are the same kind of control as
   the two before them and belong on the same row, and a divider would make
   the row read as two toolbars rather than one. The gap is the composer's own,
   so the six controls are evenly spaced across the start of the row.

   A PRESSED ONE IS ORANGE, which is the one visual job this row has. The
   accent is what every other pressed state in the app uses, and a reader who
   has pressed "guidance" needs to be able to see, without reading anything,
   that the next send has been redirected. The pressed control also gets a
   faint fill, because colour alone is not enough on a 22px stroke glyph --
   the same objection that got the reply bubble's provenance dot replaced. */
.log-kinds { display: flex; align-items: center; gap: 4px; }
.log-kind[aria-pressed="true"] {
  color: var(--accent);
  background: var(--own);
}

/* The plus and its popup are gone. Camera and gallery sit on the control
   row itself now -- a menu whose entire content was two items cost a tap to
   open and took more room shut than the two controls take open. */

/* SEND IS THE ARROW ITSELF. The circle round it made it the heaviest thing
   on the dock -- a filled 40px disc next to two bare glyphs -- and what it
   has to say is not "I am a button" but "there is something to send". It
   says that by going orange, and says the opposite by going grey, which is
   the one place in this app grey still means a control that will not
   respond. */
.log-send {
  width: 44px;
  height: 40px;
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
}
/* SEND IS WHITE UNTIL THERE IS SOMETHING TO SEND, then orange. That is the
   other way round from every other disabled control here, and deliberately:
   the two controls beside it are grey because they are always available and
   never the point. Send is the point. Greying it put the one thing you are
   reaching for at the bottom of the box's hierarchy, under the plus. White
   says "here, waiting"; orange says "now". */
.log-send:disabled { color: var(--ink); }
.log-send:active:not(:disabled) { background: var(--own); }

/* THE THREE WAYS IN, equal width. A grid rather than flex so the three are
   exactly equal whatever their words are -- "Suggest" and "המלצה" are not
   the same length, and flex would have handed the longer one more room. */
/* THE TAB ROW. Two halves, never resized -- the old one let the open tab
   take the row and the other shrink to a square, so both jumped width every
   time you switched and the label you were about to tap moved out from
   under your thumb. `1fr 1fr` and a zero basis is what makes them halves
   whatever the translation does to the words.

   The chosen one is filled. With the tabs no longer resizing, width cannot
   say which is open, so colour does -- and it is the same accent that means
   CHOSEN everywhere else in this app. */
/* THE OUTLINE WRAPS THE CHOSEN TAB AND WHAT IT OPENED.
   The first attempt was a short accent bar under the chosen tab, and it
   was wrong in the way a label is wrong: it POINTED at the panel instead
   of enclosing it, so the row still read as two pills above an unrelated
   box.

   This is the shape instead. The rule under the row is the accent, and the
   chosen tab sits ON it rather than above it -- its bottom corners square
   off and it overlaps the line by the line's own width, so there is no
   edge between the tab and the panel at all. The boundary starts at the
   top of the chosen tab, comes down its sides, and carries on along the
   rule to the far side. One continuous orange edge around the tab and
   everything underneath it; the other tab sits outside that edge, which is
   exactly what it is. */
.composer-tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  border-bottom: 1px solid var(--accent);
  margin-bottom: 10px;
  position: relative;
}
.composer-tab {
  position: relative;
  min-height: var(--tap);
  min-width: 0;
  padding: 0 12px;
  /* The icon and the word travel together, centred in the half. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* Top corners only. A pill cannot merge with a line -- its bottom edge
     is a curve, and a curve meeting a straight rule is a join you can see.
     Both tabs take the same shape, because a tab is a tab whether it is
     the chosen one or not. */
  border-radius: var(--r-field) var(--r-field) 0 0;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font-family: inherit;
  font-size: 16px;
  font-weight: 700;
  /* One line, always: a wrapped tab grows the row the composer sits on. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.composer-tab[aria-selected="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-on);
  /* ON the rule, not above it: one pixel of overlap is what removes the
     edge between the tab and the panel it opened. */
  margin-bottom: -1px;
  z-index: 1;
}
/* The unchosen one keeps its own bottom edge, so it reads as a closed
   shape sitting outside the boundary rather than as a second opening. */
.composer-tab[aria-selected="false"] { border-bottom-color: var(--line); }
.composer-tab:active { filter: brightness(1.15); }
.composer-tab svg { flex: 0 0 auto; }
.composer-tab svg { flex: 0 0 auto; }

/* ONE THING UNDER THE ROW AT A TIME. That is the whole of what makes these
   tabs rather than two buttons that each open a panel. */
.composer-dock[data-open="suggest"] .log-panel { display: none; }


/* Hidden, not deleted: see the note in index.html. `display: none` as well
   as `hidden`, because the grid would otherwise still count it. */
/* The mirror of the pair above. Opening Log shrank Suggest to a square and
   gave Log the room; opening Suggest hid Log's label but left it at its old
   width, so Suggest never took what Log gave up. Whichever is open holds the
   row; the other is its icon. */


/* A circle, matching the attach buttons floating above it, so the open row
   reads as one family of round controls rather than a glyph beside two
   buttons. 32px to sit level with Send at the other end. It is decoration --
   `aria-hidden` in the markup -- so it is not a tap target and does not owe
   the 44px floor. */
/* The notepad circle is GONE from the open field, and that is a reversal
   worth explaining. It was asked for, and it was right when the open row
   still needed something to say what the row was. It is wrong now for two
   reasons that only appeared later: it repeats what the drawer already is
   (you opened Log -- nothing else is open), and it holds 37px in a field
   that does not have them. Measured: the placeholder needs 193px in English
   and 191px in Hebrew, the field had 169px, and the words were being cut a
   quarter short. The glyph was the only thing in the row that was not
   carrying its width. It still names the SHUT tab, where there is room and
   where it does identify something. */

/* `min-height: 0` is load-bearing: app.css's own `textarea { min-height: 5rem }`
   is 80px, which renders the field taller than the 48px box it sits in and
   pushes the text out through the top of the outline. Measured, not guessed. */
/* A field that looks like a field.
   Opening the drawer used to focus this, so the caret was the affordance:
   you could see where typing would go. It no longer does (the keyboard was
   covering the camera and the recorder), which left an unmarked gap between
   two icons with nothing saying it could be tapped. So the field draws
   itself: an inset pill, the way every message box the owner uses does. */
/* The wrapper is the field's box, and the caret's frame of reference.
   `flex: 1` moved here from the textarea: the textarea now fills the
   wrapper instead of being a flex child of the row, so the drawn caret has
   something whose edges match the field's edges exactly. */
/* A COLUMN: the field, then any attached photos under it. It was a row,
   which is what put the thumbnails beside the textarea and took their
   width out of it (see #pending-photos). The absolutely-positioned hint
   and drawn caret are unaffected -- both are pinned to the top of this
   box, which is still the top of the field. */
.field-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* The caret this app draws, because it cannot afford the real one.
   A real caret needs focus, and on iOS focus raises the keyboard over the
   camera and the recorder -- the two things opening this drawer also
   uncovers. So the "ready to type" cue is painted: a bar at the start of
   the hint text, blinking at the platform's own rhythm.
   It is `aria-hidden` decoration and never a tap target -- the textarea
   underneath receives every tap, including one landing on the caret. */
.ghost-caret {
  position: absolute;
  /* Pinned to the first line rather than stretched down the box: with
     the field up to three lines tall, `inset-block: 10px` would have
     drawn a cursor the height of the whole field. */
  top: 12px;
  height: 20px;
  /* 0.5rem against the textarea's 0.75rem inline padding: a 4px gap, so the
     bar sits BEFORE the first glyph rather than on top of it. Measured --
     at a matching 0.75rem it touched the L of "Log". */
  inset-inline-start: 0.5rem;
  width: 2px;
  background: var(--accent);
  border-radius: 1px;
  pointer-events: none;
  opacity: 0;
  animation: ghost-caret-blink 1.06s step-end infinite;
}
/* Shown whenever the field is EMPTY -- with the keyboard up as much as
   down.
   It used to hide on focus, on the reasoning that the real caret takes
   over and two carets in one field is worse than none. The second half of
   that is right and the first half was not enough: what appeared when the
   keyboard opened was the platform caret, which is thinner and easier to
   miss than the one that had just been there, so the cursor read as having
   vanished at the moment it mattered most.
   So there is still exactly one caret -- this one. While the field is
   empty the real one is made transparent (below) and this is what blinks;
   the moment anything is typed they swap, because a drawn caret cannot
   follow the text.
   `.is-empty` comes from app.js against `value.trim()`. It used to be
   `:has(textarea:placeholder-shown:not(:focus))`, which failed twice on
   the phone: `:has()` needs iOS 15.4, and `:placeholder-shown` is false
   for a field holding a single stray space. A class set from a trim() is
   immune to both. */
.field-wrap.is-empty .ghost-caret { opacity: 1; }

/* START OVER, in the field's own end corner.
   Absolute, so it sits over the text rather than reserving a column the field
   would lose on every line whether or not there is anything to clear. The
   textarea's own end padding is what keeps the words from running under it.
   Grey: it is a control, and the words beside it are the content. */
.field-clear {
  position: absolute;
  top: 6px;
  inset-inline-end: 4px;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--affordance);
  display: flex;
  align-items: center;
  justify-content: center;
}
.field-clear:active { color: var(--ink); }
.field-clear svg { width: 18px; height: 18px; }
@keyframes ghost-caret-blink {
  0%, 50% { visibility: visible; }
  50.01%, 100% { visibility: hidden; }
}
/* Motion, however small, is motion. */
@media (prefers-reduced-motion: reduce) {
  .ghost-caret { animation: none; }
}

/* The hint, drawn rather than placed by the browser -- see index.html for
   why neither the native placeholder nor `:placeholder-shown` is trusted.
   Positioned on the same origin as the text it stands in for, so the words
   do not move when typing replaces them. `pointer-events: none` because it
   sits over the field: every tap belongs to the textarea underneath. */
.type-hint {
  position: absolute;
  inset-block: 0;
  /* Exactly where the typed text will start, and as close to the far edge
     as the row allows.
     The start matches the field's own inline padding, so the hint sits on
     the text origin rather than near it -- it is standing in for those
     words, and a few pixels off would show as a shift the moment the first
     character lands. It also clears the drawn caret, which ends at 10px.
     Measured: at 0.95rem start and 0.5rem end the English hint needed
     228px in a 224px box and lost the tail of "symptom". Hebrew fitted at
     224 exactly, which is no margin at all. */
  inset-inline: 0.75rem 0.25rem;
  /* Centred by flex, NOT by padding plus a fixed line box.
     It was `display: block; padding-top: 8px; line-height: 24px` inside a
     40px box -- arithmetic that lands exactly, with nothing spare above or
     below. iOS renders text a little larger than the desktop this was
     measured on, so on the phone the glyphs grew past that box and were cut
     off top and bottom. Flex centring has no arithmetic to get wrong: the
     line box is whatever the font needs and it sits in the middle of
     whatever height the field is. */
  /* Aligned to the first line, not centred. The field is a line and a
     half now and grows to three, so centring would float the hint down
     the middle of an empty box instead of sitting where the first word
     will. The padding matches the field's own, so the two coincide
     exactly. */
  display: flex;
  align-items: flex-start;
  padding-top: 10px;
  /* GREY, which is the one place the "grey is never content" rule does not
     apply -- because a placeholder is not content. It is a hint standing in
     for words that are not there yet, and at full ink it read as text already
     typed into the field, which is exactly what it must not look like.
     --affordance rather than --disabled: the field is not switched off, it is
     waiting for you. Same value today; different thing being said. */
  color: var(--affordance);
  /* 15px, one below the field's 16.
     The field must be 16 or iOS zooms the page on tap. The hint has no such
     constraint, and at 16 the English line needs 228px in a 228px box --
     exact, with iOS rendering wider than the desktop it was measured on,
     which is what clipped it. A point smaller buys real margin.
     The mismatch is not visible: the hint and the typed text never appear
     together, so there is nothing to compare it against and nothing to
     jump. */
  /* Back to 16, matching the field.
     It was dropped to 15 when the hint overflowed its box, but that was
     measured against the inner span rather than its container -- the
     container is 238px and 16px needs 228, so it always fitted with room.
     15 read as noticeably smaller than the text it stands in for, which is
     the thing a hint must not be.
     `normal`, not a fixed line box: at a stated 24px the glyphs were
     clipped top and bottom on iOS, which renders larger than the desktop
     this was measured on. Let the font say how tall its line is, and let
     flex centre whatever that turns out to be. */
  font-size: 16px;
  line-height: normal;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
}

/* The words themselves, in a child, because `text-overflow` does nothing on
   a flex container -- and flex is what makes the centring above safe. The
   outer element positions and centres, this one holds the text and is the
   thing allowed to run out of room.
   Measured in the real production markup at 390px: the English line needs
   238px in a 238px box. It fits by nothing at all, and iOS is wider than
   the desktop that was measured on. An ellipsis degrades that to
   "…sympt…" rather than a tail sliced off mid-glyph. */
/* TWO LINES, NOT ONE. The hint was a single nowrap line with an ellipsis,
   which was right for "Log meal, workout or symptom" -- four words that fit
   with nothing spare. The owner replaced it with a sentence: "Write in free
   text about the recent meal, workout, symptom or weight", which says what
   the field actually takes, weight included, and says it is prose rather
   than a form. On one line that ellipsis would eat half of it, and an
   ellipsis in a placeholder hides the words it exists to show.
   So it wraps, and the clamp is two: the empty field is sized for exactly
   two lines below, and a third would be cut mid-height instead of cleanly
   at a line. The ellipsis stays as the backstop for a translation longer
   than the budget -- it lands at the end of the second line now. */
.type-hint-text {
  min-width: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
/* `is-empty`, not `is-idle`: the hint stays while the field is focused and
   still empty, the way a native placeholder does. Tying it to idle made the
   words vanish the moment the keyboard opened, which is the one moment the
   field is otherwise a blank box. */
.field-wrap.is-empty .type-hint { opacity: 1; }

/* WHAT IS TYPED DECIDES WHICH WAY IT READS. A box with `dir="auto"` picks
   its direction from the first strong character, which is right -- but
   nothing was telling it how to ALIGN, so Hebrew resolved right-to-left and
   still sat flush against the left edge. Reported as Hebrew text not being
   right aligned in Hebrew mode. `text-align: start` reads from the
   resolved direction rather than from the page's, which is the difference:
   the page can be Hebrew and the box English, or the other way round, and
   each lands on its own reading edge. */
input[dir="auto"],
textarea[dir="auto"] { text-align: start; }

.log-panel textarea {
  width: 100%; min-width: 0; border: 0; resize: none;
  /* No inner box. The field's own outline is the box -- a second filled
     rectangle inside it read as a banner sitting in the field rather than
     as the field itself. */
  background: transparent;
  border-radius: 12px;
  color: var(--ink); outline: none; text-align: start;
  /* 16px, and it is not a taste choice: iOS Safari zooms the whole page in
     on any text field whose font-size is below 16px, and there is no way to
     decline that other than meeting the threshold. At 14px, tapping this
     field threw the page to twice its size and pushed the rest of the app
     off screen. `maximum-scale` in the viewport meta would also stop it and
     is the wrong fix -- it takes pinch-zoom away from the whole app, which
     is somebody's only way to read a small number. */
  font-size: 16px;
  /* Room for descenders. At a 19px line the tail of a "y" was clipped, and
     the hint beside it -- same size, same line -- was clipped identically.
     24 leaves the glyph its full box inside the 40px field. */
  line-height: 24px;
  /* ONE LINE, WITH ROOM, THEN UP TO THREE.
     60px is a line and a half: the field was 40, exactly one line plus
     its padding, which left the hint pressed against the edges with
     nothing around it.
     `min-height`, not `height`, because app.js sets an inline height from
     scrollHeight on every keystroke -- min-height outranks an inline
     height, so this is the floor the growing starts from. `max-height` is
     three lines plus the padding; past that it scrolls rather than
     eating the conversation, and the dock rises with it either way
     because it is sticky to the bottom. */
  /* ONE LINE, THEN UP TO THREE.
     It was 60 -- a line and a half -- which bought breathing room around
     the hint when the field was a bare pill with Send jammed in its end.
     The box has padding of its own and a control row under the words now,
     so that extra half-line read as an empty second line sitting there
     before anything was typed. 24 is the line box plus the 18 of vertical
     padding below; it grows from there on its own, as it always did, and
     still stops at three. */
  min-height: 42px;
  max-height: 92px;
  /* ...and while it is EMPTY, two lines, because that is what the hint now
     needs. The field is one line the moment anything is typed -- the inline
     height app.js sets from scrollHeight starts at one line and grows from
     there -- so this is the idle box only, never a second line you have to
     type past. 48 is the two line boxes, 18 the vertical padding. */
  /* Top-aligned, not centred: the words start on the first line and the
     box grows downward under them. `align-self` is gone with it -- a
     stretched field is what lets it grow at all. */
  padding: 10px 0.75rem 8px;
  /* AFTER the shorthand, which would otherwise reset it -- the same cascade
     trap that kept the bubble's ••• glued to the hour for a release. Room at
     the end for the start-over button, which floats over this box: without
     it a long line runs under the ✕ and its last word is unreadable. */
  padding-inline-end: 2.2rem;
  overflow-y: auto;
}
/* The pill brightens on focus, so "I am typing here" is visible without
   hunting for the caret. */
/* Nothing to fill on focus now that the field is transparent. The caret --
   the real one, by then -- is the signal, and the row already has its own
   outline. A ring here would draw a second box inside the first, which is
   the thing that was removed. */
/* The caret, in the accent, so the thing that says "type here" is the
   brightest thing in the field the moment it is tapped. The platform default
   is the text colour, which on a dark field is a thin pale line that is easy
   to miss on a phone. */
.log-panel textarea { caret-color: var(--accent); }
/* ...except while it is empty, when the drawn caret above is the one on
   screen. Two carets at the same origin would be one fat blinking bar
   with two rhythms. */
.field-wrap.is-empty textarea {
  caret-color: transparent;
  /* ...and two lines tall, because that is what the hint now needs. One
     rule, not two: a second `.field-wrap.is-empty textarea` block is what a
     selector-based test finds first, and it found the wrong one. */
  min-height: 66px;
}
/* The arrow is a flex child of the button and was collapsing: measured on
   production at 0x18, which is why the orange circle rendered empty. An SVG's
   `width` attribute does not stop a flex row squeezing it, so the size is
   stated here and shrinking is switched off. Bigger than the 18px it was
   inside the disc: without a fill around it the glyph has to carry the
   control on its own. */

/* One chevron, drawn pointing right, mirrored for Hebrew rather than kept as
   a second SVG someone has to remember to update alongside the first: the
   direction it points is the direction the line is read in, not a fact
   about the icon. */


/* Painted only when it has a reply chip to hold, so the chip reads as a
   framed header on the message being written. `:has` and not a class, because
   reply-mode.js already toggles `hidden` on the chip and a second piece of
   state to keep in step with the first is how the chip went invisible once
   already. */

/* Reply mode, made visible. It sits above the text rather than beside it so
   it cannot be missed in the corner of a field being typed into, and it is
   the width of the composer so the meal's name has room to be read. Hidden
   entirely when reply mode is off — `hidden` on the element does that; this
   only says what it looks like when it is on. */
.reply-chip {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* Its own line under the field's top padding, with a rule under it, so the
     chip reads as a header on the message being written rather than as part
     of the message itself. The top gap and the horizontal inset used to
     come from .composer-field's own padding; that field stopped supplying
     any (the composer-drawer work), which left the chip's box flush against
     the field's top border -- 1px of separation, measured, not assumed.
     margin (space before the box), not padding (space inside it), is what
     replaces the lost top inset; the horizontal padding keeps the text off
     the box's own side walls now that the box itself sits flush against them. */
  margin: 0.4rem 0 0.35rem;
  padding: 0 0.5rem 0.35rem;
  /* No underline. It separated the chip from the textarea that used to sit
     below it in this box; the textarea left with the composer-drawer work,
     so the rule now lands a few pixels above the field's own bottom border --
     two lines where one is meant. */
  color: var(--ink);
  font-size: 0.85rem;
  /* It follows the page, not the field — the chip is the app saying which
     meal is being answered ("Reply to …"), with the meal's own name spliced
     into that sentence, so the sentence's language sets the direction and
     bidi keeps the name intact inside it. This was `direction: rtl`, which
     said the same thing back when the page was always RTL. */
}
.reply-chip[hidden] { display: none; }
.reply-chip span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* At the far end, and a real tap target: cancelling is the whole point of
   showing this, so it must not need aiming at. */
.reply-chip .icon {
  margin-inline-start: auto;
  background: transparent;
  color: var(--ink);
  border: 0;
  padding: 0.15rem 0.4rem;
  font-size: 1rem;
  line-height: 1;
}
.summary-expand table { width: 100%; border-collapse: collapse; font-size: 0.78rem; font-variant-numeric: tabular-nums; }
/* Must fit a phone with no horizontal scroll. Numbers never wrap — a kcal
   figure split across lines is unreadable — so the label column is the one
   that absorbs it and is allowed to run to two lines. `table-layout: fixed`
   stops a long meal name from widening the table past the viewport. */
.summary-expand table, .overlay-sheet table { table-layout: fixed; }
/* EVERY CELL IS ITS OWN BIDI RUN. Without this the macro headers -- bare
   Latin letters P, C, F -- have no direction of their own, so on a
   right-to-left page they join the Hebrew run beside them and render glued
   to it: the calories column header read "קלוריותP", which is exactly how
   it was reported, as a broken character in that header. It was never a
   missing glyph; it was two cells with no boundary between them. The times
   collided the same way at the other end of the row.

   `isolate` rather than `dir` on each cell: direction is a property of the
   content, and a table full of `dir="ltr"` would pin the Hebrew meal names
   too. Isolation says only "resolve this cell on its own", which is the
   actual rule. */
/* EVERY CELL IS ITS OWN BIDI RUN, and nothing escapes it.
   The calories column header read "קלוריותP" -- reported as a broken
   character in that header, and in fact two separate faults. The column is
   40px under `table-layout: fixed`; the Hebrew word needs 47, and
   `white-space: nowrap` with no clipping let it spill straight into the
   protein column beside it. The word is the abbreviation now, as the
   design board has it, and this stops the next long header doing it again.
   `isolate` because a bare Latin "P" has no direction of its own and would
   otherwise resolve against the Hebrew run beside it. */
.summary-expand td, .summary-expand th,
.overlay-sheet td, .overlay-sheet th {
  unicode-bidi: isolate;
  overflow: hidden;
  text-overflow: ellipsis;
}
.summary-expand td, .summary-expand th { text-align: start; padding: 0.3rem 0.35rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
/* The meal name, down from 40%. The calorie column beside it may now hold a
   range -- "300-500" against "400" -- and under `table-layout: fixed` the
   width has to come from somewhere. It comes from here, because this is the
   one column allowed to wrap: a name loses a line, a figure loses digits. */
.summary-expand td:nth-child(2), .summary-expand th:nth-child(2) { white-space: normal; overflow-wrap: anywhere; width: 32%; }
/* THE HOUR, WHICH ALSO CLIPPED once the calorie column took its room. Under
   `table-layout: fixed` every unsized column takes an equal share of what is
   left, so widening one narrows four -- and "08:20" plus the source dot needs
   more than a macro figure does. Sized rather than left to the share. */
.summary-expand td.col-when, .summary-expand th.col-when { width: 3rem; }
.summary-expand td:first-child, .summary-expand th:first-child { padding-inline-start: 0; }

/* High-calorie flags. Colour is the fast signal; every flagged element also
   carries a title, because colour alone is not an affordance.
   `.summary-expand td[data-kcal-flag]` is (0,2,1) — a class, a type, and an
   attribute — which out-specifies `.summary-expand td` (0,1,1), the only
   other rule that touches these cells. Do not let a plainer `.summary-expand
   td { color: ... }` rule land above this: it would win by source order at
   equal specificity to nothing here, but the moment it exists it must stay
   less specific than these two, the same trap that bit `#day-sheet` before
   this table moved. */
/* IN THE TABLE these are plain text cells, so the flag colours the figure
   and that is still right. ON A BUBBLE the figure is a filled chip now, and
   colouring its TEXT paints orange on orange or red on orange -- reported
   from the phone as a number that looked white and unreadable on the
   yellow. So the bubble's chip says it with its FILL instead, and its text
   stays the dark ink the chip was designed around. */
.summary-expand td[data-kcal-flag="elevated"] { color: var(--accent); }
.summary-expand td[data-kcal-flag="high"] { color: var(--danger); font-weight: 700; }

/* Elevated keeps the ordinary chip: the figure is high, not alarming, and a
   third fill for it would make every large meal shout. High gets the danger
   fill, with ink dark enough to read on it. */
.reply-numbers .kcal[data-kcal-flag="high"] {
  background: var(--danger);
  color: var(--danger-on);
  font-weight: 700;
}

/* An unreviewed estimate — the Worker's instant guess, or no estimate at
   all — can still carry `data-kcal-flag`, since the flag is computed off
   the raw number regardless of review state. A "high" flag on a guess that
   may still move is a false alarm, so this rule mutes the figure instead,
   and it must win that fight on specificity, not on being declared last:
   `.bubble.in .reply-numbers .kcal[data-estimate=...]` is (0,5,0) — four
   classes and an attribute — against the flag rules' (0,3,0) above, so it
   wins regardless of source order. (A `font-size` once lost this exact kind
   of contest silently by relying on order instead of specificity — do not
   repeat that here.) */
.bubble.in .reply-numbers .kcal[data-estimate="instant"],
.bubble.in .reply-numbers .kcal[data-estimate="none"] {
  color: var(--ink);
  font-weight: 400;
  animation: none;
}

/* Which tier the number came from, on the icon beside it. Green once
   reviewed, yellow while it is still the Worker's guess, untinted when there
   is no number at all. The figure's own muting stays: the tint says which
   tier, the muting says do not read too much into it. */
.bubble-meta [data-estimate="reviewed"] { color: #7fbf7f; }
.bubble-meta [data-estimate="instant"] { color: var(--accent); }

/* Notes and exercise: present but visually quieter than food. */
.bubble[data-turn-action="note"],
.bubble[data-turn-action="exercise"] { border-style: dashed; }

.notes-heading { font-size: 0.9rem; margin: 1rem 0 0.5rem; color: var(--ink); }
.note-row { display: flex; gap: 0.5rem; padding: 0.25rem 0; font-size: 0.85rem; }
.note-time { color: var(--ink); font-variant-numeric: tabular-nums; }
/* The tag column is gone. It said "Workout" or "Symptom" on every row of a
   mixed list, which is a column that exists to undo the mixing -- the two
   have their own sections now, each under its own heading. */

/* THE WORKOUTS TABLE, ALIGNED BY ROLE RATHER THAN BY POSITION.
   Reported as needing to be "more aligned", and it was a flex list of three
   spans: nothing lined up down the columns because there were no columns.
   The classes name what each cell HOLDS, not where it sits, because the
   week's table carries a date column the day sheet's does not -- a rule
   written against :nth-child would mean one thing in one and another in the
   other.
   `table-layout: fixed`, the ellipsis and the per-cell bidi isolation all
   come from the .summary-expand/.overlay-sheet rules this table sits inside;
   only the column behaviour is new. */
.notes-table .col-what { white-space: normal; overflow-wrap: anywhere; }
/* The two figure columns: fixed, end-aligned, tabular. Fixed because a
   column that resizes with its widest value does not line up with the one
   above it on a different day; end-aligned because that is what makes the
   digits stack. */
.notes-table .col-kcal { width: 5rem; }
.notes-table .col-time { width: 3.5rem; }
.notes-table .col-kcal,
.notes-table .col-time {
  text-align: end;
  font-variant-numeric: tabular-nums;
}
/* ...and the headers over them, or the word sits at one end of the column
   and every figure under it at the other. `th` carries no class -- the head
   row is positional by nature, since a header cell holds no data to name --
   so these are matched by their position within this table alone. */
.notes-table th:last-child,
.notes-table th:nth-last-child(2) { text-align: end; }

/* The weekly summary panel: an overlay, not part of the timeline, so it sits
   above everything rather than scrolling with the chat. */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 5;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: flex-end;
}

/* A SHEET IS A PHONE IDIOM. It slides up from the bottom edge and spans the
   width because on a phone the bottom edge is where the thumb is and the
   width is 390px. On a desktop the same rules put the what is new panel
   along the foot of a 1280px window, nowhere near the column the app is
   actually being read in, which is what "what is new needs to appear at the
   center" is about.

   So above phone width every overlay becomes a centred dialog in the same
   column as everything else: all four corners rounded, since it is no longer
   growing out of an edge, and a border all the way round for the same
   reason. Phones keep the sheet exactly as it was -- this adds a desktop
   case rather than replacing the one that works. */
@media (min-width: 48rem) {
  /* Centred on the column, still sitting on the bottom edge: it is a sheet,
     not a dialog. Only `justify-content` changes -- `align-items: flex-end`
     above is what keeps it at the foot of the screen, and the top-only
     corner radius stays right because it is still growing out of an edge. */
  .overlay { justify-content: center; }
  .overlay-sheet { width: min(100%, var(--column)); }
}
/* `display: flex` above beats the `hidden` attribute's built-in
   `display: none`, so without this the panel is never actually hidden — it
   sits permanently over the composer. Any rule that sets `display` on an
   element that also uses `hidden` needs this counterpart. */
.overlay[hidden] { display: none; }

/* SCROLLING A SHEET MUST NOT SCROLL THE DIARY BEHIND IT.
   Reported from the phone: dragging inside an open sheet moved the
   conversation underneath instead. Two separate faults, and both have to be
   fixed or the other still shows.

   `overscroll-behavior: contain` stops the CHAINING: once the sheet reaches
   its own top or bottom, the browser hands the rest of the gesture to the
   nearest scrollable ancestor, which is the page. Contain keeps it.

   But a sheet shorter than its content box does not scroll at all, and then
   every touch inside it belongs to the page from the first pixel -- nothing
   about the sheet is involved. That is what `body[data-sheet-open]` is for:
   while any overlay is open the document itself stops scrolling, so there is
   nothing behind to move. `position: fixed` rather than `overflow: hidden`
   because iOS Safari ignores the latter on the body -- the exact platform
   this was reported on -- and app.js keeps the scroll position across it. */
.overlay-sheet { overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
body[data-sheet-open] {
  position: fixed;
  inset-inline: 0;
  width: 100%;
  overflow: hidden;
}
.overlay-sheet {
  width: 100%;
  /* IT STOPS UNDER THE HEADER. This was `85vh`, which is 15% of the screen
     reserved by eye and knows nothing about the bar it has to clear. The
     header is measured at runtime (app.js publishes --header-height; it is
     61px on a phone, not the 3rem anybody would guess), so on a long sheet
     the two numbers disagreed by enough to slide the top of the sheet --
     which is exactly where its own frozen title and close button live --
     behind the FitKeep bar. Reported as "drawers go behind the frozen
     header instead of under", with Preferences scrolled down to no head,
     no handle and no rounded top.

     PERCENT, NOT vh OR dvh. .overlay is `fixed; inset: 0`, so its height is
     whatever box contains it: the viewport on a phone, and #device on
     desktop, which carries a transform and so is the containing block for
     everything fixed inside it. vh and dvh are both the window, and on
     desktop the window is taller than the frame the sheet lives in. `%`
     follows whichever one .overlay actually filled. vh had a second fault
     on its own: it is the LARGE viewport and ignores the browser chrome,
     so it overstated the room by the height of the URL bar precisely when
     the sheet was longest.

     The 8px is the gap that shows there is something above it. */
  max-height: calc(100% - var(--header-height, 3rem) - 8px);
  overflow-y: auto;
  background: var(--surface);
  border-top: 1px solid var(--line);
  border-radius: var(--r-sheet) var(--r-sheet) 0 0;
  padding: 0 20px;
  padding-bottom: calc(20px + env(safe-area-inset-bottom));
}
/* THE HANDLE. A sheet that slid up from the bottom edge with a square
   corner and no grip read as a screen that had replaced the app rather
   than a layer over it. It is drawn, not draggable -- the gesture is the
   close button and the backdrop; this says which direction the thing
   came from. */
.overlay-sheet::before {
  content: "";
  display: block;
  width: 40px;
  height: 4px;
  margin: 8px auto 0;
  border-radius: var(--r-pill);
  background: var(--disabled);
}
/* THE HEAD: an icon, the title, and a 44px way out. The title was 1.1rem
   and sat level with a small ✕ that was easy to miss and easier to miss
   TAPPING -- its target was 0.4rem of padding around a character. */
/* EVERY SHEET'S HEADING STAYS PUT.
   This was opt-in, a sheet at a time -- Preferences, then Locations, then
   Profile -- and the list was about to take Weekly and Daily summary too.
   A heading that scrolls away is never what anybody wanted; it was only
   ever opt-in because .overlay-sheet used to set `padding: 1rem`, and a
   sticky child stops at the scrollport inset by its container's padding,
   so the head would have stopped 16px down with content sliding through
   the band above it. The sheet is `padding: 0 20px` now -- no top padding
   to stop below -- so the pairing holds everywhere and the default can be
   the right one.
   Opaque, or the content scrolls visibly under the words. z-index 1 so it
   covers them and still loses to .sheet-actions at 2, which it never
   meets. */
.overlay-head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  /* FULL BLEED, like .sheet-actions below and for the same reason. The
     sheet has 20px side padding, so a head that stops at the text column
     leaves a gutter either side where the content scrolls visibly past --
     reported as "it keeps the title frozen but the drawer icon from the
     top is getting under". The negative margin takes the background out
     to the sheet's edges and the padding puts the text back where it was. */
  margin: 0 -20px;
  padding: 8px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.overlay-head h2 { margin: 0; font-size: 22px; line-height: 28px; font-weight: 700; }
/* The icon beside the title, from the same set as everything else. */
.overlay-head .sheet-icon { flex: 0 0 auto; display: flex; color: var(--ink); }
.overlay-head .sheet-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.overlay-head h2 { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.overlay-head .icon {
  width: var(--tap);
  height: var(--tap);
  flex: 0 0 auto;
  padding: 0;
  background: transparent;
  color: var(--ink);
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Pinned, so the way out of a long sheet does not scroll away exactly when
   you are deepest in it. Scoped to .prefs-sheet: the day and week sheets are
   short and a sticky head there would only eat their first line. The
   background is the sheet's own, not transparent -- a sticky strip with no
   background lets content scroll visibly through it. z-index keeps it over
   the .pref-group cards, which have their own background and would otherwise
   paint on top in document order. */
/* Locations joins Preferences here. The note above reasons about the day and
   week sheets, which are short; Locations is not. It carries the home
   setting, the tracking toggle and a list that grows by a row for every place
   you eat at, so scrolling to the bottom used to take the title and the close
   button off screen together. Everything the Preferences head needs, it needs. */
/* AND PROFILE, for the same reason, reported the same way: "the title is
   hid". Nine fields and six derived figures is longer than 85vh, so the
   <h2> scrolled off and left a column of inputs under the app's own header
   with nothing naming the sheet. Joining this group rather than making
   every head sticky, because the pin only works paired with the sheet
   zeroing its top padding below -- a sticky child stops at the scrollport
   inset by its container's padding, not at the edge. */
/* Only what these three need BEYOND the default above: a tighter band,
   because their heads carry a 44px close button and a 22px title and the
   8px the others use made the head nearly a third of a short sheet. */
.prefs-sheet .overlay-head,
.locations-sheet .overlay-head,
.profile-sheet .overlay-head {
  margin-bottom: 0;
  /* padding-block only: the inline padding is what keeps the full-bleed
     background aligned with the text above, and a shorthand here would
     take it away. */
  padding-block: 8px;
}
/* Doubled class for the same reason `.overlay-sheet.prefs-sheet` is doubled:
   `.overlay-sheet` sets `padding: 1rem` at equal specificity later in the
   file, so a bare `.locations-sheet` declaration would never apply. Only the
   top is zeroed -- Locations has no sticky foot, so it keeps its bottom. */
.overlay-sheet.locations-sheet { padding-top: 0; }

/* Reuses the same table styling the daily sheet already has — same family,
   same look. */
.overlay-sheet table { width: 100%; border-collapse: collapse; font-size: 0.78rem; font-variant-numeric: tabular-nums; }
.overlay-sheet td, .overlay-sheet th { text-align: start; padding: 0.3rem 0.35rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
.overlay-sheet td:first-child, .overlay-sheet th:first-child { white-space: normal; overflow-wrap: anywhere; }
.overlay-sheet td:first-child, .overlay-sheet th:first-child { padding-inline-start: 0; }

/* A DATE NEVER WRAPS, in any of these tables.
   `.overlay-sheet td:first-child` sets `white-space: normal` so a long meal
   name can break, and it caught the week's date column too -- "2026-09-29"
   split across two lines, directly above a workouts table whose dates sat on
   one. Named by what the cell HOLDS rather than where it sits, so the same
   rule covers the week's days table and both workouts tables.
   `td.col-day` and not `.col-day`: it has to out-rank, or at least tie and
   follow, the `:first-child` rule right above -- which is why this sits here
   rather than beside the other .notes-table rules further up the file. */
.overlay-sheet td.col-day,
.overlay-sheet th.col-day,
.summary-expand td.col-day,
.notes-table td.col-day {
  width: 5.2rem;
  white-space: nowrap;
}

/* A RANGE NEEDS ROOM A SINGLE FIGURE DID NOT.
   Both tables share their column widths equally under `table-layout: fixed`,
   so an uncertain figure -- "811-1,051" where there used to be "931" --
   overflowed and was clipped to "...51" by the ellipsis rule above. Seen in a
   browser at 390px, in Hebrew, where the ellipsis lands at the START of the
   cell and the figure read "...00".
   Named by what the column holds, like .col-day, so the day sheet and the
   week table are sized by one rule. The notes table sets its own, narrower,
   because a workout's burn is never a range. */
.summary-expand td.col-kcal,
.summary-expand th.col-kcal,
.overlay-sheet td.col-kcal,
.overlay-sheet th.col-kcal {
  width: 4.8rem;
}
/* ...except inside the workouts table, which is three or four columns rather
   than six and was already measured at 5rem. More specific than the rule
   above, so it wins wherever both apply. */
.summary-expand .notes-table td.col-kcal,
.overlay-sheet .notes-table td.col-kcal,
.notes-table td.col-kcal { width: 5rem; }

/* These four blocks were pinned `direction: ltr`, and the tables' cells
   pinned `text-align: left` on top of it, because the app's own voice was
   English inside a page that was always RTL. Both pins are gone: the
   headings, the totals row and the column names all come out of the
   catalogue, so a summary is chrome and reads whichever way the interface
   reads. The `text-align: start` already set on the cells further up is
   what was wanted all along, and it now resolves correctly in both.
   The one thing inside that is *not* chrome is the meal name in the second
   column. It is the user's or the model's words, in a script neither this
   stylesheet nor the interface language can predict, so summary.js and
   report.js mark those cells `dir="auto"` — that is where a Hebrew meal in
   an English sheet is kept upright, not here. */

.summary-line, .summary-gap { text-align: center; }

/* Reply bubbles are the app speaking, so they speak the interface's
   language and read its way. They used to be pinned LTR and left-aligned,
   which was the same statement back when "the app speaks English" was a
   fact about the app rather than about the setting. The meal name inside
   carries its own `dir="auto"` (chat.js), so a Hebrew label in an English
   reply still reads right-to-left within its own line. */
/* NO SIDE STRIPE. A 3px edge on one side of the reply bubbles marked them
   as the app's own voice, back when both kinds of bubble sat on the same
   side and something had to tell them apart. The sides are the convention
   now -- yours at the start, the app's at the end, each with a tail
   pointing at its speaker -- so the stripe was saying what the layout
   already says, in the one weight the design language does not have: it
   allows 1px borders and nothing else, and no side stripes at all. */
/* The leading stripe follows the bubble's own colour, or it would announce blue
   on a yellow reply. */
.bubble.in[data-estimate="instant"] { border-inline-start-color: var(--food-line); }
.bubble.in[data-estimate="reviewed"] { border-inline-start-color: var(--food-line); }
.bubble.in .reply-numbers { justify-content: flex-start; }
.bubble.in .undo { display: block; margin-inline-end: auto; }

/* The app's own status lines, same reasoning — and `end`, not `right`: the
   line trails the conversation it comments on, and which edge that is
   follows the language. (The four `.status` rules under forms above pull it
   back to `start`, where it sits under the field instead.) */
.status { text-align: end; }

/* A meal over 800 kcal pulses, so it catches the eye while scrolling rather
   than relying on colour alone — colour is also the one signal a colour-blind
   reader may not get. Opacity only: the red stays put, so a still frame
   (a screenshot, a paused animation) still reads as flagged.
   Honours prefers-reduced-motion — a pulsing element is a genuine problem for
   some readers, and the colour alone still carries the meaning. */
@keyframes kcal-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
[data-kcal-flag="high"] { animation: kcal-pulse 1.1s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  [data-kcal-flag="high"] { animation: none; }
}

/* The thinking bubble: dots where the meal bubble will appear, while the
   Worker is still pricing the capture. Everything about its colour and its
   side comes from `.bubble.in` above — it is the app's own voice, the same
   bubble a meal arrives in, so the real bubble replacing it is a change of
   content rather than a change of shape. Only the dots are new. */
/* A ROW, NOT A COLUMN. `.bubble` became `flex-direction: column` when the
   bubbles were rebuilt -- every other bubble stacks its rows -- and this
   one's three dots are not rows, they are one row. They stacked into a
   vertical ellipsis the moment that landed. Stated here rather than relying
   on `display: flex` alone, which inherits the column from .bubble. */
.bubble.thinking {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.3rem;
  padding: 0.7rem 0.75rem;
}
.thinking .dot {
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: var(--ink);
  animation: thinking-dot 1.2s ease-in-out infinite;
}
/* Staggered, the ordinary typing-indicator idiom, because that is what it
   means everywhere else. */
.thinking .dot:nth-child(2) { animation-delay: 0.16s; }
.thinking .dot:nth-child(3) { animation-delay: 0.32s; }
@keyframes thinking-dot {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 1; }
}
/* A static indicator, not a missing one — the same rule the over-800-kcal
   pulse follows just above, for the same reason: a moving element is a
   genuine problem for some readers, and what the dots say ("working") has
   to survive being still. They stay drawn, at the opacity they would
   otherwise pass through, so the bubble is visibly there. */
@media (prefers-reduced-motion: reduce) {
  .thinking .dot { animation: none; opacity: 0.65; }
}

/* ---- The printable report ----------------------------------------------
   Invisible on screen and the only thing visible on paper. The container is
   `hidden` at rest, so nothing here costs the chat anything until the report
   is built. */
.report[hidden] { display: none; }
.report { display: none; }

@media print {
  /* Paper is white and ink is black regardless of the app's dark theme:
     printing a dark UI wastes a cartridge and reads worse. */
  body {
    background: #fff;
    color: #111;
    /* The desktop stage centres the frame with flex. On paper there is no
       frame to centre, and a flex body turns the one block we print into a
       flex item sized by a container that is not there. */
    display: block;
    min-height: 0;
  }

  /* THE PHONE FRAME IS A SCREEN THING, AND IT BROKE THE PRINT.
     Reported as "print doesn't work, it says failed to load pdf" -- Chrome's
     words for a print it could not rasterise. #device is a 390x844 box with
     `overflow: hidden`, a `scale()` transform and a 12px bezel on it, and
     handing the printer a transformed, clipped box of fixed size is what it
     choked on. Taken apart here rather than hidden: the report lives inside
     it, so it has to stay in the tree and simply stop being a phone. */
  #device {
    display: block !important;
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    margin: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    transform: none;
    background: #fff;
  }

  /* NOTHING, AT EVERY LEVEL, AND THEN THE PATH BACK TO THE REPORT.
     This was one rule -- `body > .screen > *:not(.report)` -- written when
     the screens were children of <body>. #device went in between them and
     that selector stopped matching anything at all, so the printer was given
     the entire app: the menu, the composer, the sheets, the stage.
     Three levels, because the report sits at #device > #screen-log > .report
     and a rule that knows about only one of them hides nothing. */
  body > *,
  #device > *,
  .screen > * { display: none !important; }

  /* The way back in. Each of these outranks the rule above it: #device and
     #screen-log are ids against `#device > *`, and `.screen > .report` is
     (0,2,0) against `.screen > *` at (0,1,1) -- a bare `.report` would LOSE
     that one even with !important on both, which is why it is written long.
     [hidden] deliberately: the report is never shown on screen. It is built
     into the live document, left hidden, and revealed by this rule alone --
     which is what lets the print run with nothing to tear down afterwards. */
  #device,
  #screen-log { display: block !important; }
  .screen > .report,
  .screen > .report[hidden] { display: block !important; }

  /* PAPER DOES NOT SCROLL. On a desktop `#device .screen[data-active]` makes
     this a bounded scroller -- height 100%, overflow auto -- and that media
     query matches the page box too, at (1,2,0) against this rule's (1,0,0).
     Hence !important: a report two pages long inside a 100%-high scroller
     prints its first screenful and stops. */
  #screen-log {
    height: auto !important;
    min-height: 0 !important;
    overflow: visible !important;
    padding: 0;
  }

  /* This was pinned `direction: ltr` against the page's RTL default, back
     when every word printed here was English. The headings and column names
     are translated now, so the printed report reads the way the person who
     asked for it reads: Time at the start of the row, the cover range in the
     order the sentence is spoken. The meal names and notes are the one part
     that is not the app's own voice, and report.js marks those `dir="auto"`
     so a Hebrew meal in an English report still prints correctly. */

  /* Portrait, with room for a thumb-sized margin. `auto` rather than a named
     size so the same document prints correctly on A4 and Letter. */
  /* 1MM AT THE SIDES, ASKED FOR IN THOSE WORDS: "on desktop it needs to be
     aligned to the right and left with 1mm in each side". The table is the
     whole point of this document and every millimetre at the edge is a
     millimetre off the meal names. Top and bottom keep a real margin, which
     is where a printer puts its page furniture.
     A physical printer still enforces its own unprintable edge and will
     clamp this; "Save as PDF" honours it exactly. */
  @page { size: auto; margin: 12mm 1mm; }

  /* Days flow; they do not each claim a page.
     A forced break after every day meant a day of two meals took a whole
     sheet and left two thirds of it blank, and a day with nothing logged
     took a sheet to say so. Now a day starts wherever the last one ended and
     moves to the next page only when it does not fit.

     `break-inside: avoid` is what keeps that readable: a day is never split
     across a fold, so its heading, figures and meals always arrive together.
     A day taller than a page still breaks — the rule is a preference, not a
     guarantee — which is the right failure: better a split day than a lost
     one. `page-break-*` is the alias Safari still needs; both are given
     deliberately. */
  .report-day {
    page-break-inside: avoid;
    break-inside: avoid;
    margin-bottom: 10mm;
  }
  .report-day:last-child { margin-bottom: 0; }

  /* Never leave a day's heading or its figures stranded at the foot of a page
     with the meals overleaf. */
  .report-day-head,
  .report-stats {
    page-break-after: avoid;
    break-after: avoid;
  }

  /* Serif for a printed page. The screen's system sans is tuned for a backlit
     phone; on paper a serif sets long numbers and meal names more evenly, and
     it signals "document" rather than "screenshot of an app". */
  .report {
    font-family: "Iowan Old Style", Palatino, "Palatino Linotype", Georgia, serif;
    color: #1a1a1a;
    line-height: 1.45;
  }

  /* Cover */
  .report-head {
    margin-bottom: 14mm;
    padding-bottom: 4mm;
    border-bottom: 1.5pt solid #1a1a1a;
  }
  .report-head h1 {
    font-size: 26pt;
    margin: 0;
    letter-spacing: -0.01em;
  }
  .report-range { font-size: 12pt; margin: 3mm 0 0; }
  .report-count { font-size: 10pt; color: #555; margin: 1mm 0 0; }
  .report-generated { font-size: 8.5pt; color: #777; margin: 3mm 0 0; }

  /* Day heading */
  .report-day-head { margin-bottom: 4mm; }
  .report-day-head h2 {
    font-size: 16pt;
    font-weight: 600;
    margin: 0;
    letter-spacing: -0.005em;
  }
  .report-day-date {
    font-size: 8.5pt;
    color: #777;
    margin: 0.5mm 0 0;
    font-variant-numeric: tabular-nums;
  }

  /* The day's figures, in a band under its heading. A rule above and below
     rather than a box: lighter on the page, and it lets the numbers sit on the
     same baseline grid as everything else. */
  .report-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 6mm;
    align-items: baseline;
    padding: 3mm 0;
    border-top: 0.75pt solid #1a1a1a;
    border-bottom: 0.25pt solid #bbb;
    margin-bottom: 5mm;
  }
  .report-stat-value {
    font-size: 12pt;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }
  /* The figure the day is about, so it carries the weight the others don't. */
  .report-stat-lead .report-stat-value { font-size: 22pt; line-height: 1.1; }
  .report-stat-label {
    font-size: 7.5pt;
    color: #666;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin-top: 0.5mm;
  }

  /* Meals */
  .report-table { width: 100%; border-collapse: collapse; font-size: 10pt; table-layout: fixed; }
  .report-table th {
    text-align: start;
    border-bottom: 0.75pt solid #1a1a1a;
    padding: 0 1.5mm 1.5mm;
    font-size: 7.5pt;
    font-weight: 600;
    color: #666;
    text-transform: uppercase;
    letter-spacing: 0.08em;
  }
  /* Hairlines between rows, not boxes around them: the eye tracks a row
     across the page on its own, and heavier rules fight the numbers. */
  .report-table td {
    padding: 2mm 1.5mm;
    border-bottom: 0.25pt solid #ddd;
    vertical-align: baseline;
  }
  .report-table tr:last-child td { border-bottom: 0; }
  /* Widths belong on the header row, not the body: under `table-layout:
     fixed` the first row decides every column, so widths set on the `td`
     classes were ignored and all seven columns split evenly — which wrapped
     "Oatmeal with milk and banana" onto three lines while the number columns
     sat half empty. The meal column takes whatever is left. */
  .report-table th:first-child { width: 15mm; }
  .report-table th:nth-child(n + 3) { width: 13mm; }

  /* The meal name is the only column allowed to wrap; the numbers never do,
     because a kcal figure split across lines is unreadable. */
  .report-meal { overflow-wrap: anywhere; }
  .report-time {
    white-space: nowrap;
    width: 15mm;
    font-variant-numeric: tabular-nums;
    color: #666;
  }
  .report-num {
    white-space: nowrap;
    text-align: end;
    width: 14mm;
    font-variant-numeric: tabular-nums;
  }
  /* The calorie column is the one being read down; the macros support it. */
  .report-table td.report-num:first-of-type { font-weight: 600; }

  /* Notes and activity */
  .report-notes-head {
    font-size: 7.5pt;
    font-weight: 600;
    color: #666;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin: 7mm 0 2mm;
    padding-bottom: 1.5mm;
    border-bottom: 0.75pt solid #1a1a1a;
  }
  .report-notes { margin: 0; padding: 0; list-style: none; font-size: 10pt; }
  .report-notes li {
    padding: 1.5mm 0;
    border-bottom: 0.25pt solid #ddd;
  }
  .report-notes li:last-child { border-bottom: 0; }
  .report-notes .report-time { display: inline-block; margin-inline-end: 3mm; }
  .report-tag {
    font-size: 7.5pt;
    color: #666;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-inline-start: 3mm;
  }

  .report-empty { font-size: 10pt; color: #777; font-style: italic; margin: 4mm 0 0; }
}

/* The operator's account list. One row per account: who, and what plan. */
#accounts-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.account-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
}

/* The email is the long part and the only part worth truncating. */
.account-who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-tag {
  margin-inline-start: 0.4rem;
  font-size: 0.75rem;
  color: var(--ink);
}

.account-plan {
  flex: 0 0 auto;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg);
  color: var(--text);
  /* 16px: anything smaller makes iOS zoom the page on tap. */
  font-size: 16px;
}

/* THE GLYPH IS MODEST. THE TAP TARGET IS NOT.
   These two were conflated, and that is what sent this back and forth.
   The original complaint was that the ✕ was hard to hit -- a 16px glyph
   in a target defined only by its padding. Doubling the glyph to 2rem
   answered the wrong half: it made the control the loudest thing in
   every head row, competing with the title beside it, and it was called
   too big twice. 1.5rem was still too much.
   What actually makes it hittable is `min-width`/`min-height: 44px`,
   which is stated below and is independent of the glyph. So the glyph
   goes back to something quiet -- a quarter larger than it started --
   and the thumb keeps the full 44px either way.
   `line-height: 1` stops the glyph pushing the head row open. */
.overlay-head .icon,
.drawer-head .icon {
  font-size: 1.25rem;
  line-height: 1;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.2rem 0.4rem;
}

/* Signing in with Google. Hidden by app.js unless this deployment has a
   client id -- the panel below the password form, never instead of it. */
.google-signin {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1rem;
}

/* A separator, not a heading: the password form above is still the
   ordinary way in. */
.google-or {
  margin: 0;
  color: var(--ink);
  font-size: 0.85rem;
}

/* Google renders its own button into this element and owns its styling.
   What we decide is where it sits and that nothing light shows around
   it, which took four separate rules on a dark page:

   `width: fit-content` -- renderButton is given a pixel width, and a
   container wider than that leaves its own background showing beside the
   button as a bright strip.

   `iframe { display: block }` -- an iframe is inline by default and sits
   on the text baseline, leaving a few pixels of line-box under it. That
   gap is the classic bright band beneath an embedded button.

   `overflow: hidden` with a matching radius -- Google draws a rounded
   button on a square canvas, so the four corners outside the curve are
   the canvas, not the button.

   `color-scheme: dark` -- stated again here because the iframe does not
   inherit the one on :root, and some of Google's own surfaces read it.

   Left LTR whatever the page direction: the button's contents are laid
   out by Google and mirroring the box only moves the gap to the other
   side. */
#google-button {
  direction: ltr;
  color-scheme: dark;
  width: fit-content;
  line-height: 0;
  border-radius: 999px;
  overflow: hidden;
  background: var(--bg);
}
#google-button iframe {
  display: block;
  border-radius: 999px;
}

/* The test suite, on the phone. Recent runs, then the groups; opening a
   group swaps both for its own list. */
.tests-runs { display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 0.9rem; }

.test-run {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-field);
  border-radius: 6px;
  background: var(--card);
  font-size: 0.85rem;
}
/* Red is the only colour worth spending here: a green run is the normal
   case and does not need to shout. */
.test-run[data-ok="false"] { border-inline-start-color: var(--bad, #c4553d); }
.test-run-when { font-variant-numeric: tabular-nums; }
.test-run-score { font-weight: 600; }
.test-run-aside { margin-inline-start: auto; color: var(--ink); font-size: 0.78rem; }

.tests-groups { display: flex; flex-direction: column; gap: 0.4rem; }

.test-group {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  padding: 0.7rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  color: var(--ink);
  text-align: start;
  font-size: 1rem;
}

.test-group-name { min-width: 0; }
.test-group-counts { display: flex; align-items: baseline; gap: 0.5rem; flex: 0 0 auto; }
.test-group-size { color: var(--ink); font-size: 0.8rem; white-space: nowrap; }
.test-group-good { color: var(--ok, #4a9d5f); font-size: 0.8rem; }
.test-group-bad { color: var(--bad, #c4553d); font-size: 0.8rem; font-weight: 600; }
/* "Not run" is deliberately not green: it is not a pass. */
.test-group-idle { color: var(--ink); font-size: 0.8rem; font-style: italic; }

.tests-detail { display: flex; flex-direction: column; gap: 0.5rem; }

/* WHAT HAPPENED TO THIS TEST, AND WHEN. A line under the criterion rather
   than a badge beside it: the criterion is a sentence and often wraps, and a
   badge floated against wrapping text lands in a different place on every
   row. Three states, because "nobody ran it" is not "it passed" -- which is
   what the screen said before, by saying nothing. */
.test-verdict {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}
.test-verdict-mark { font-weight: 600; }
.test-verdict[data-state="good"] .test-verdict-mark { color: var(--ok); }
.test-verdict[data-state="bad"] .test-verdict-mark { color: var(--danger); }
/* Italic and the affordance grey, matching `.test-group-idle` in the index:
   the same state said the same way in both places. Not --disabled, which
   means a control that will not respond -- this is an absence of news. */
.test-verdict[data-state="idle"] .test-verdict-mark {
  color: var(--affordance);
  font-style: italic;
  font-weight: 400;
}
.test-verdict-when { color: var(--affordance); }
.test-detail-head { display: flex; align-items: baseline; gap: 0.75rem; }
.test-detail-head h3 { margin: 0; font-size: 1rem; }

.test-file {
  margin: 0.75rem 0 0;
  color: var(--ink);
  font-size: 0.72rem;
  font-family: ui-monospace, monospace;
  overflow-wrap: anywhere;
}

.test-item { padding: 0.45rem 0; border-bottom: 1px solid var(--line); }
.test-item:last-child { border-bottom: 0; }

/* The acceptance criterion first: it is the specific claim, and the one
   that has to be true for the test to pass. */
.test-acceptance { font-size: 0.92rem; }
.test-description { margin-top: 0.15rem; color: var(--ink); font-size: 0.82rem; }

/* A sentence lifted from a file header describes the SUBJECT, not this
   test. Labelled so the screen never passes one off as the other. */
.test-description-scope {
  margin-inline-start: 0.4rem;
  padding: 0 0.3rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.68rem;
  white-space: nowrap;
}
.test-context { margin-top: 0.15rem; color: var(--ink); font-size: 0.72rem; }

/* The arrow that opens "Correct" now sits on the calories row itself,
   pushed to the far end by .more's own auto margin -- it had a row of
   its own, which spent a whole line of every meal bubble on one glyph
   and put the control furthest from the figure it acts on. */

/* A figure the owner typed. Not muted like an instant guess -- it is the
   most settled number in the diary -- and not tinted like a reviewed one
   either, because it did not come from a model and should not borrow the
   vocabulary that says it did. */
/* A corrected figure is not styled apart from a reviewed one any more: both
   are settled, both take the green chip above, and both need the dark ink
   that chip was designed around. This rule set --ink on it -- near-white on a
   filled chip, which is the exact combination reported as unreadable on the
   yellow fill. The weight stays where the base rule puts it. */

/* THE SUGGESTIONS, AS A TABLE.
   They were one block per place with a paragraph of reasoning under
   every dish, which meant scrolling to hold six options in your head.
   Rows put them side by side, which is how a choice actually gets made.
   `table-layout: fixed` so a long dish name widens nothing -- the dish
   column absorbs it by wrapping. */
.advice-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  font-size: 0.88rem;
}

.advice-table th {
  text-align: start;
  padding: 0.25rem 0.4rem;
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.advice-table td {
  padding: 0.5rem 0.4rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.advice-table tr:last-child td { border-bottom: 0; }

/* TWO THIRDS FOR THE DISH, ONE FOR THE PLACE.
   Three fixed columns -- dish, calories, place -- left the dish about 180px of
   a 390px phone, so "Grilled sea bass with salad" wrapped to three lines
   beside a column of identical "420 kcal". The dish is what the choice is made
   on, and the calories belong to it rather than beside it, so they moved into
   its cell and the column went away.
   Percentages rather than rem: the split is a proportion of the row, which is
   what was asked for, and `table-layout: fixed` above makes it exact at any
   width. */
.advice-table th:first-child, .advice-table td:first-child { width: 66%; }
.advice-table th:nth-child(2), .advice-table td:nth-child(2) { width: 34%; }

/* The dish and its figure, stacked. The name is inline-flex (it is a button
   as wide as its words), so the figure needs its own line rather than a row
   to sit on. */
.advice-dish-cell > .advice-kcal-cell { margin-top: 0.15rem; }

/* The dish is the tap target: a button that reads as the text it is,
   because a row of chrome would drown six of them. */
/* "+ ADD TO LOG", IN WORDS. Three drawn affordances failed on the dish name
   -- a plus beside it, a plus in a disc, the whole name as an outlined pill
   -- because a dish name IS a name, and no amount of drawing round it says
   what pressing it does. A label saying what happens cannot be misread.
   Under the navigate button at the end of the row, so every suggestion has
   its two actions in the same two places: go there, or log it.
   Grey, because it is the second thing you read on a row and six loud buttons
   in one table is the problem the pill had. */
.advice-add {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 4px;
  width: 100%;
  margin-top: 2px;
  padding: 2px 0;
  border: 0;
  background: transparent;
  color: var(--affordance);
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
}
.advice-add svg { flex: 0 0 auto; }
.advice-add:active { color: var(--accent); }

/* The dish name is a name again. It stays a target -- by the time you tap it
   the line above has told you what that does -- but it stops pretending to be
   a button, which is what every version of this before it was doing.
   First it was the dish name alone, which is a label; then a plus beside it,
   which at 16px grey read as punctuation in the name; then the plus in an
   accent disc, which the owner still called unclear -- and rightly, because
   the thing you are choosing is the NAME, and the only part that looked
   pressable was a mark next to it.
   AND THEN THIS COMMENT CLOSED AFTER ITS FIRST PARAGRAPH, so everything from
   "First it was" stood in the stylesheet as a broken selector and the parser
   swallowed the rule below it whole. The dish name fell through to the global
   `button` rule instead -- `background: var(--accent)` -- and every suggestion
   rendered as a filled yellow slab. Reported as "the full yellow seems like
   the wrong structure", which is what a silently dropped rule looks like from
   the outside. tests/dom/css-comments-balanced.test.js now fails on an
   unbalanced marker anywhere in this file.
   Six of these in a table is a list of six choices, which is what this panel
   is, and a list of six choices is not six yellow slabs. */
.advice-dish-button {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: 0.95rem;
  font-weight: 600;
  text-align: start;
  overflow-wrap: anywhere;
  /* THE PLUS LEADS THE DISH. Inline-flex and not flex, so the button is
     still as wide as its words rather than the whole cell -- a full-width
     tap target here would swallow the gap beside the name and read as the
     row being pressed rather than the dish.
     `align-items: baseline` would drop the icon below the text; start keeps
     the mark on the first line of a dish name that wraps to two. */
  display: inline-flex;
  align-items: flex-start;
  gap: 6px;
}
.advice-dish-button:active { color: var(--accent); }

/* THE CELL HOLDS TWO THINGS, STACKED.
   It was `white-space: nowrap` on a cell containing both the figure
   and where the figure came from ("from history"), inside a fixed
   column narrower than the two of them side by side -- so the content
   ran straight out of its box and over the place beside it. That is
   the overlap.
   A column stacks them, and only the NUMBER keeps nowrap: a kcal
   figure split across lines is unreadable, while its source is
   ordinary prose and may wrap freely. */
/* A CELL, STILL. `display: flex` was the first fix here and it broke
   the table: a `td` told to be a flex container stops being a table
   cell, drops out of the row's layout, and takes its border with it --
   which is why the rule above the calories sat at a different height
   from the rule above the dish beside it.
   Block children give the same stacking without leaving the table. */
.advice-kcal-cell {
  /* A span inside the dish's cell now, not a cell of its own: block, so it
     starts a line under the name instead of running on beside it. */
  display: block;
  min-width: 0;
  white-space: normal;
  font-variant-numeric: tabular-nums;
}
/* THE FIGURE AND ITS SOURCE ON ONE LINE. They were stacked, which was forced
   by a 5.5rem column that could not hold "420 kcal" and "from history" side by
   side. Under the dish there is two thirds of the row to work with, so they
   read as one phrase and the row is a line shorter. */
.advice-kcal-cell > span { display: inline; }
.advice-kcal-cell > span + span { margin-inline-start: 0.4rem; }
.advice-kcal-value { white-space: nowrap; font-weight: 600; color: var(--ink); }

/* WHAT THIS DISH WOULD DO TO THE DAY, IN COLOUR.
   The figure alone makes you do the arithmetic against what is left, which is
   the one thing the panel already knows. Green fits, amber goes over, red goes
   far enough over to be tomorrow's problem.
   Uncoloured when there is no target: no limit, nothing to be over, and a
   colour there would be a judgement invented out of nothing. */
.advice-kcal-value[data-risk="fine"] { color: var(--ok); }
.advice-kcal-value[data-risk="over"] { color: var(--food); }
.advice-kcal-value[data-risk="way-over"] { color: var(--danger); }

/* AND THE WORST ONES BLINK, three times and then stop.
   Asked for so the risky ones are obvious before you read a number. It does
   not run forever on purpose: a list that keeps flashing is the complaint the
   launch screen had just earned, and an alarm nobody can switch off stops
   being read as an alarm. */
@keyframes advice-kcal-alarm {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}
.advice-kcal-value[data-risk="way-over"] {
  animation: advice-kcal-alarm 0.6s ease-in-out 3;
}
.advice-kcal-source {
  color: var(--ink);
  font-size: 0.7rem;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

/* Every cell is allowed to shrink. Without `min-width: 0` a table cell
   refuses to go below its min-content width, which with a long
   unbroken place name is how a fixed layout gets pushed apart from the
   inside. */
.advice-table td { min-width: 0; }
.advice-place-cell { color: var(--ink); overflow-wrap: anywhere; }
/* TAKE ME THERE, beside the name rather than under it: the place column is
   the narrowest of the three and a second line per row would make the table
   half again as tall for a control most rows never use.
   A 44px target would not fit that column, so this one is deliberately
   smaller -- it sits inside a row whose own height is the real target, and
   the dish button beside it is the primary action. */
/* THE PLACE, THEN ITS TWO COMMANDS, EACH ON ITS OWN LINE WITH ITS ICON IN THE
   SAME COLUMN. The arrow used to float beside the name and the add button sat
   under it, so the two things you can do to a suggestion were drawn two
   different ways in two different places. They are built the same way now, so
   their icons line up and both read as commands. */
.advice-place-line {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 2px;
}
/* The pin is red because that is what a map pin is. Nothing else in the row
   is: the calories own green, amber and the red that blinks, and a pin
   borrowing one of those would read as a judgement about the dish. */
.advice-place-pin {
  display: inline-flex;
  flex: 0 0 auto;
  color: var(--danger);
}
.advice-place-go {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  padding: 2px 0;
  color: var(--affordance);
  font-size: 0.72rem;
  font-weight: 600;
  text-decoration: none;
}
.advice-place-go svg { flex: 0 0 auto; }
.advice-place-go:active { color: var(--accent); }

/* Nothing of its own any more. This heading used to hold the signed-in
   address -- hence a nowrap/ellipsis rule and a [data-email] rule that
   dropped the uppercase and the tracking for it. The address is only at
   the foot of the menu now, so the heading is the translated word
   "Settings" and takes .drawer-heading exactly like its siblings. */

/* A capture the fast model could not make sense of.
   Worded and coloured as a request, not a failure: the reviewed pass
   may still rescue it, so this is not --danger. It sits under the
   capture it is about, in the app's own voice. */
.bubble.unclear {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  color: var(--ink);
  font-size: 0.85rem;
}
.unclear-icon { flex: 0 0 auto; font-size: 1em; }
.unclear-text { min-width: 0; }

/* Which build is on this phone, at the end of the header row.
   It also balances the ☰ so the title lands centred -- same 3.4rem the
   empty spacer used to hold, which is why that spacer is gone. Quiet
   on purpose: a fact for the rare moment something looks wrong, not a
   part of the interface.
   LTR in both languages: a version is not a phrase, and mirroring
   "v0.3.0" would read as nonsense. `flex: 0 0 auto` so the flex:1
   title cannot squeeze it to nothing, which is exactly what happened
   while it had no width of its own. */
.app-version {
  flex: 0 0 auto;
  /* Pinned to what it already did, now the row aligns on the baseline
     for the hamburger's sake. This is the one item with no
     relationship to the title's baseline -- a muted 0.7rem aside, and
     sitting it on a 1.25rem title's baseline drops it well below the
     title's optical line. */
  align-self: center;
  min-width: 3.4rem;
  margin-bottom: 0.5rem;
  direction: ltr;
  text-align: end;
  color: var(--ink);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

/* Deleting an account. Quiet until it is armed, because the row it sits
   on is mostly used for changing a plan -- and then unmistakable,
   because the second tap is irreversible. */
.account-delete {
  flex: 0 0 auto;
  padding: 0.3rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: transparent;
  color: var(--ink);
  font-size: 0.78rem;
}
.account-delete[data-armed="true"] {
  border-color: var(--danger);
  background: var(--danger);
  color: var(--bg);
  font-weight: 600;
}
.account-delete:disabled { opacity: 0.5; }

/* The actions on a place, behind the ⋯ at the end of its row.
   They were two links sitting on every row at rest, and a list of
   places is read far more often than it is edited -- so the common
   case was paying for the rare one on every line. */
.location-actions {
  display: flex;
  gap: 0.9rem;
  padding: 0.15rem 0 0.4rem;
}
.location-actions[hidden] { display: none; }

/* Smaller than the place name and not underlined: they are actions on
   the row above, not links in a sentence. */
.location-action {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: 0.75rem;
  text-decoration: none;
}
.location-action-danger { color: var(--danger); }
/* Open in Maps is an anchor among buttons, so it needs the line-height and
   the cursor a button gets for free. No underline, for the reason above:
   these are actions on the row, not links in a sentence. */
a.location-action { cursor: pointer; line-height: inherit; }
.location-action:disabled { opacity: 0.5; }

/* The place row ends with the ⋯, so its name takes what is left. */
.location-row .location-name { min-width: 0; }

/* THE RANGE FIELDS, under the single figure they can override. A fieldset
   because they are one answer given in two boxes -- "from 300 to 500" is not
   two independent facts, and the legend is what says so. */
.meal-edit-range {
  margin: 0.4rem 0 0;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-field);
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.meal-edit-range legend {
  padding: 0 0.3rem;
  font-size: 0.75rem;
  color: var(--affordance);
}
.meal-edit-range label { flex: 1 1 6rem; min-width: 0; }

/* WHAT SOMEBODY SAID, on the operator's sheet. Their words first and at full
   size, because that is the thing worth reading; who and when underneath,
   quiet, because it is context for the words rather than the point of them. */
.admin-feedback-row { padding: 0.6rem 0; }
.admin-feedback-row:not(:first-of-type) { border-top: 1px solid var(--line); }
.admin-feedback-text {
  margin: 0 0 0.2rem;
  color: var(--ink);
  font-size: 0.9rem;
  /* They wrote paragraphs, and a run-on sentence is not what they wrote. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.admin-feedback-meta { margin: 0; font-size: 0.75rem; color: var(--affordance); }

/* THE FEEDBACK SHEET somebody writes in. The field is the screen -- there is
   one thing to do here and it should be obvious which. */
#feedback-text { width: 100%; min-height: 7rem; }
#feedback-send { margin-top: 0.6rem; }

/* THE INVITE LIST. Signing up needs a code, so this is the only way anybody
   else gets in -- which makes it worth being plain rather than clever. */
.invite-mint { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.6rem; }
.invite-mint input { flex: 1; min-width: 0; }
.invite-mint button { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; }

.invite-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 0.6rem;
  align-items: center;
  padding: 0.5rem 0;
  font-size: 0.85rem;
}
.invite-row:not(:first-of-type) { border-top: 1px solid var(--line); }

/* The code is what you came here for, so it is the biggest thing on the row
   and it is a button: the next thing that happens to a fresh one is being
   pasted into a message. Monospaced and letter-spaced because it is read
   character by character, out loud or onto a phone. */
.invite-code {
  padding: 0;
  border: 0;
  background: transparent;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--accent);
}
/* A code already spent or revoked cannot let anybody in, so it stops wearing
   the colour that means "this is live". */
.invite-row[data-state="used"] .invite-code,
.invite-row[data-state="revoked"] .invite-code { color: var(--affordance); }

.invite-about { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.invite-note { color: var(--ink); overflow-wrap: anywhere; }
.invite-state { font-size: 0.78rem; color: var(--affordance); overflow-wrap: anywhere; }
.invite-row[data-state="used"] .invite-state { color: var(--ok); }
.invite-when { font-size: 0.78rem; color: var(--affordance); font-variant-numeric: tabular-nums; }

.invite-revoke {
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--danger);
  font-size: 0.75rem;
}
.invite-revoke:disabled { opacity: 0.5; }

/* What changed in this release. A sheet somebody dismisses in five
   seconds on the way to logging lunch, so it is short and reads as a
   list rather than a document. */
.whats-new-sheet { max-width: 30rem; }
.whats-new-body { display: flex; flex-direction: column; gap: 0.75rem; }

/* Only drawn when more than one release is being caught up on -- see
   whats-new.js. LTR whatever the page, because a version is not a
   phrase. */
.whats-new-version {
  margin: 0.5rem 0 0;
  direction: ltr;
  color: var(--ink);
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

.whats-new-list {
  margin: 0;
  padding-inline-start: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  font-size: 0.92rem;
  line-height: 1.45;
}
.whats-new-list li { padding-inline-start: 0.15rem; }

/* Who is signed in, at the foot of the menu under Log out. Plain and
   readable rather than the small uppercase treatment the Settings
   heading gets -- this is the answer to "whose diary is this", which
   is what somebody checks before signing out. LTR always: an address
   is not a phrase. */
.menu-account {
  margin: 0.5rem 1rem 0;
  /* Follows the page now that it carries a label. `dir="ltr"` on the
     element would put "Logged as:" on the wrong side in Hebrew; the
     address inside it is a Latin run and bidi already isolates it. */
  text-align: start;
  color: var(--ink);
  font-size: 0.8rem;
  overflow-wrap: anywhere;
}
.menu-account:empty { display: none; }

/* WhatsApp's own mark, in its own colour. The menu's other icons are
   emoji and carry their own colour; a monochrome glyph here would read
   as disabled beside them. Sized from the row's font so it matches the
   emoji around it rather than a fixed pixel size. */
/* The brand colour is gone from here: the drawer sets the sharing section's
   one colour (see #menu-share above). Only the alignment is this rule's own
   business now -- a 1em mark on a text baseline sits high without it. */
.wa-mark { vertical-align: -0.125em; }

/* THE RUNS SLIDE SIDEWAYS. On a phone there is room for the result and
   not much else, and the result is what this is opened for -- so it
   leads, and the date and release are reached by sliding right rather
   than crammed onto the same line. */
.tests-runs-scroll {
  overflow-x: auto;
  scrollbar-width: none;
  margin-bottom: 0.9rem;
  /* Says there is more to the right without a scrollbar to point at
     it: the last visible edge fades. */
  -webkit-mask-image: linear-gradient(to left, transparent 0, #000 0.4cm);
  mask-image: linear-gradient(to left, transparent 0, #000 0.4cm);
}
.tests-runs-scroll::-webkit-scrollbar { display: none; }

.tests-runs-table {
  border-collapse: collapse;
  font-size: 0.82rem;
  /* WIDE ENOUGH THAT THE THIRD COLUMN PEEKS IN. A table that stops
     exactly at the edge of the screen looks like the whole table, and
     nobody swipes something they have no reason to think continues.
     26rem put the version column entirely out of sight on a narrow
     phone; 22rem leaves its first characters visible, which is the
     only affordance a horizontal scroll gets. */
  min-width: 22rem;
}
.tests-runs-table th {
  text-align: start;
  padding: 0.2rem 0.6rem 0.2rem 0;
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}
.tests-runs-table td {
  padding: 0.4rem 0.6rem 0.4rem 0;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
  white-space: nowrap;
}
.tests-runs-table tr:last-child td { border-bottom: 0; }

/* Sticky, so sliding never takes away the thing that identifies the
   row. Its own background, or the columns scroll visibly under it. */
.tests-runs-table th:first-child,
.tests-runs-table td:first-child {
  position: sticky;
  inset-inline-start: 0;
  /* THE SHEET'S BACKGROUND, NOT THE PAGE'S. This was var(--bg) --
     #12100e, near-black -- painted inside a sheet that is #2a2118, a
     warm brown. The sticky column therefore drew a black strip down
     the left of the panel that belonged to no element on screen and
     read as a rendering fault. It needs *a* background (or the
     columns scroll visibly underneath it); it needs the one it is
     sitting on. */
  background: var(--surface);
  padding-inline-end: 0.75rem;
}

/* A green row is a time and nothing else; a red row is a count with
   the time under it. Both stack, so the cell is a column. */
.test-run-result { display: flex; flex-direction: column; gap: 0.1rem; font-weight: 600; }
.test-run-clock { font-variant-numeric: tabular-nums; }
.tests-runs-table tr[data-ok="true"] .test-run-clock { color: var(--ok, #4a9d5f); }
.tests-runs-table tr[data-ok="false"] .test-run-count { color: var(--danger); }
/* Muted under the count: on a red row the number of failures is the
   headline and the time is the footnote. */
.tests-runs-table tr[data-ok="false"] .test-run-clock {
  color: var(--ink);
  font-weight: 400;
  font-size: 0.75rem;
}
.test-run-when { color: var(--ink); font-variant-numeric: tabular-nums; }
.test-run-version { font-variant-numeric: tabular-nums; }
.test-run-release { display: block; }
.test-run-commit { display: block; color: var(--ink); font-size: 0.7rem; }

/* WHAT FAILED, NAMED. A red group of 226 tests says a problem exists
   and gives no way to find it without going to the laptop. Above the
   groups, because on a red run this is the only thing anybody opened
   the screen for -- and hidden entirely on a green one. */
.tests-failures {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 0.9rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--danger);
  border-radius: 12px;
}
.tests-failures[hidden] { display: none; }

.tests-failures-title {
  margin: 0;
  color: var(--danger);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.test-failure { display: flex; flex-direction: column; gap: 0.1rem; }
.test-failure-name { font-size: 0.88rem; }
/* First line of what it said. Monospace because it is output, not
   prose, and wrapping anywhere because an assertion message has no
   spaces to break at. */
.test-failure-message {
  color: var(--danger);
  font-size: 0.74rem;
  font-family: ui-monospace, monospace;
  overflow-wrap: anywhere;
}
.test-failure-where {
  color: var(--ink);
  font-size: 0.7rem;
  overflow-wrap: anywhere;
}

/* A failing test inside its group's list, marked where it sits rather
   than only in the block above -- otherwise the reader has to hold the
   failure list in their head while they scroll. */
.test-item[data-failed="true"] {
  margin-inline: -0.4rem;
  padding-inline: 0.4rem;
  /* A failing test is marked by its own colour and a 1px edge, not by a
     bar down its side: the design language has one border weight. */
  border: 1px solid var(--danger);
  border-radius: var(--r-field);
}
.test-item[data-failed="true"] .test-acceptance { color: var(--danger); }

/* THE RECORDER IS A SHEET NOW. It was a row of controls under Send,
   and the one thing it had to say -- the microphone is live right now
   -- was what it said least. Over everything, so it is unmistakable
   and so a tap cannot land on the diary behind it while the phone is
   listening. */
.voice-overlay { z-index: 6; }
.voice-sheet {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  text-align: center;
  padding-block: 1.5rem;
}

/* The one element that says "recording". Large enough to be the whole
   message, and pulsing because a still icon is a picture of a
   microphone while a moving one is a microphone that is on. */
.voice-orb {
  width: 5.5rem;
  height: 5.5rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-size: 2.6rem;
  line-height: 1;
  background: color-mix(in srgb, var(--danger) 18%, transparent);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger) 55%, transparent);
  animation: voice-orb-pulse 1.4s ease-out infinite;
}
@keyframes voice-orb-pulse {
  70% { box-shadow: 0 0 0 1.1rem color-mix(in srgb, var(--danger) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger) 0%, transparent); }
}
/* A pulse is decoration; the countdown and the bar carry the same
   fact for anyone who has asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  .voice-orb { animation: none; border: 1px solid var(--danger); }
}

.voice-sheet .voice-elapsed { margin: 0; font-size: 1.3rem; }
.voice-sheet .voice-hint { margin: 0; }
.voice-sheet .voice-meter { width: 100%; max-width: 16rem; }
.voice-sheet .row { width: 100%; display: flex; gap: 0.6rem; margin-top: 0.4rem; }

/* WHAT THE APP SAYS WHEN THERE IS NOTHING IN IT. The timeline is the
   largest surface here and rendered literally nothing on a new account:
   a spinner, then a blank rectangle above a text box. Centred in that
   space rather than stacked at its top, because the space is the
   message -- a line of text pinned under an empty header reads as a
   status, and this is an invitation. */
.start-here {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  text-align: center;
  padding: 2.5rem 1rem 1rem;
}
.start-here[hidden] { display: none; }
.start-title { margin: 0; font-size: 1.15rem; font-weight: 600; }
.start-sub {
  margin: 0;
  font-size: 0.9rem;
  color: var(--ink);
  max-width: 22rem;
  line-height: 1.45;
}

/* Wrapping, not scrolling: three chips fit one line on most phones and
   two on a narrow one, and a row that scrolls would hide the third way
   in -- which on a first run is the whole point of drawing them. */
.start-tries {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
}
.start-try {
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink);
  border-radius: 999px;
  padding: 0.55rem 0.95rem;
  font-size: 0.88rem;
}
.start-today-go { margin-top: 0.6rem; }

/* THE FIRST-RUN WALKTHROUGH. Same surface and same width as the
   start-here card it stands in front of, because it is the same moment
   in the same place -- an empty conversation, being spoken to. A sheet
   or a spotlight would make it a thing that happened TO somebody
   instead of the first thing in their diary. */
/* THE TOUR IS A LAYER OVER THE APP, NOT A BLOCK INSIDE IT.
   It was in the timeline's flow when it was a stack of cards. A tour that
   points at the real controls has to sit above them, and above the drawer
   too (z-index 7) for the step that points inside the menu -- the hole is
   transparent, so the control it frames shows through the dim rather than
   being covered by it.

   `pointer-events: none` on the layer, `auto` on the card: the dim must not
   swallow a tap meant for the app, and the card's own buttons must still
   work. */
.walkthrough {
  position: fixed;
  inset: 0;
  z-index: 8;
  pointer-events: none;
  /* NOT FOR LAYOUT. Every child here is positioned `fixed`, so this padding
     moves nothing; it is carried so walkthrough.js can read the
     home-indicator strip off a real computed style instead of hard-coding
     34px for one phone. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* THE HOLE IS THE ABSENCE OF SHADOW. One element, positioned over the
   target, with a spread larger than any viewport. Four panels around the
   target would need four rects kept in step and would show a seam between
   them at fractional device pixels. */
.walk-hole {
  position: fixed;
  border-radius: 12px;
  /* LIGHT ENOUGH TO READ THE SCREEN THROUGH. It was 0.62, which turns the app
     into a dark wash with one bright window in it -- the same complaint the
     photographed steps drew: the screens behind the explanation "should not
     be in a gradient, they should be seen clearly." It is still a spotlight,
     and the card no longer leans on the dim to be told apart from the app:
     see the accent outline on .walk-card. */
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.34);
  transition: top 0.18s ease, left 0.18s ease, width 0.18s ease, height 0.18s ease;
  pointer-events: none;
}

/* The caption. Placed against the hole by walkthrough.js, which decides
   above or below from the room left in the viewport -- the composer sits at
   the bottom of the screen and the header at the top, and one rule has to
   serve both. */
.walk-card {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  width: min(24rem, calc(100vw - 2rem));
  text-align: center;
  background: var(--surface);
  /* THE OUTLINE AND ITS GLOW, asked for directly: "the bubble that explains
     should have a glow outline." It is also what the dim used to do. A card
     on a dimmed screen is told apart by the screen being darker than it; with
     the screen shown as it is -- see .walk-shot and .walk-hole -- the card has
     to carry that itself.
     THE BORDER IS THE OUTLINE, rather than a spread outside a --line border:
     a box-shadow spread paints beyond the border box, so the two together
     were a grey ring with an accent ring around it. One ring, and the halo
     over the depth shadow that was already here. */
  border: 1px solid var(--accent);
  border-radius: 14px;
  padding: 1.1rem 1.1rem 0.9rem;
  pointer-events: auto;
  box-shadow:
    0 0 18px 1px var(--accent),
    0 10px 30px rgba(0, 0, 0, 0.45);
}
/* BESIDE THE TARGET, for one too tall to have an above or a below: the
   open menu, a drawer pinned top to bottom. The card takes the dimmed half
   of the screen the drawer is not on, so the state being described stays
   visible while it is described. No arrow -- it is sitting against the
   thing, which says the same in less ink.

   left and width are set by walkthrough.js, so the centring transform the
   base rule carries is all that is needed here. */
.walk-card[data-beside] { transform: translateX(-50%); }

/* No hole to point at: a step whose target does not exist on this account
   yet -- the ⋯ under a meal, on a diary with no meals. Centred, and
   without an arrow, because an arrow has to point AT something. */
.walk-card[data-centred] {
  top: 50%;
  transform: translate(-50%, -50%);
}

/* AT THE BOTTOM, for a target that owns the screen and leaves no room beside
   it: the open menu on a narrow phone, 288px of drawer on 390px of screen.
   The card has to cover something, and the end of a list is less of a loss
   than the middle of it. No arrow: it is not pointing, it is standing out of
   the way. Same geometry as [data-over-shot] below, and a separate name
   because they mean different things. */
.walk-card[data-bottom] {
  top: auto;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem);
  transform: translateX(-50%);
}

/* OVER THE PHOTOGRAPH, AT THE BOTTOM. Centred on it would cover the middle of
   the very screen the step is explaining; under it is where the picture
   leaves room and where the thumb already is.
   AFTER [data-centred] ON PURPOSE: the two have the same specificity, and a
   shot step with a target it cannot measure carries both. walkthrough.js also
   drops `centred` in that case -- this is the half of that pair that survives
   somebody reordering the file. */
.walk-card[data-over-shot] {
  top: auto;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem);
  transform: translateX(-50%);
}
/* THE ARROW, pointing back at the hole. A bordered square rotated 45
   degrees, with the two sides facing the card painted over by the card's
   own background, which is how a speech-bubble tail is drawn without an
   image. */
/* The block that gives when the hole leaves too little room beside it. The
   dots and the actions sit outside it, so Skip and Next can never be what
   scrolls out of reach. */
.walk-text { overflow-y: auto; overscroll-behavior: contain; }

.walk-card[data-arrow]::before {
  content: "";
  position: absolute;
  /* Over the control, falling back to the middle of the card where nothing
     could be measured. */
  left: var(--walk-arrow-x, 50%);
  width: 14px;
  height: 14px;
  margin-left: -7px;
  background: var(--surface);
  transform: rotate(45deg);
}
/* IN THE OUTLINE'S COLOUR. These were --line, which was right while the card
   was outlined in --line: against the accent ring the tail read as a notch
   cut out of the glow rather than the point of it. Only the two sides facing
   away from the card are drawn, which is what makes a square rotated 45
   degrees look like a tail. */
.walk-card[data-arrow="up"]::before {
  top: -8px;
  border-left: 1px solid var(--accent);
  border-top: 1px solid var(--accent);
}
.walk-card[data-arrow="down"]::before {
  bottom: -8px;
  border-right: 1px solid var(--accent);
  border-bottom: 1px solid var(--accent);
}
/* The app's own glyphs, at a size you can see. A walkthrough exists so
   the control is recognised afterwards, and a decorative illustration
   teaches nothing about where to tap. Greyed like every other glyph on
   this screen now. */
.walk-icon {
  font-size: 1.9rem;
  line-height: 1;
  margin-bottom: 0.5rem;
  filter: grayscale(1);
  opacity: 0.8;
}
/* The two that are controls rather than pictures, drawn as controls: a
   bordered square the size of the real one, so ⋯ reads as the button
   under a meal and not as a third row of progress dots. No grayscale --
   there is no colour in a glyph to take out, and the filter only dulled
   the one thing the card is pointing at. */
.walk-icon[data-boxed] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  font-size: 1.15rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--ink);
  filter: none;
  opacity: 1;
}
.walk-title { margin: 0 0 0.35rem; font-size: 1.05rem; }
.walk-body { margin: 0; color: var(--ink); font-size: 0.92rem; line-height: 1.45; }

/* THE WAYS IN, SHOWN AS A ROW. A card that says "type it, say it or
   photograph it" is a sentence about three controls; this is the three
   controls. Each tile is the real glyph plus its name, because three
   pictures and no words is a puzzle, and the word is what somebody
   repeats back to themselves at lunch.

   Equal columns rather than flex-wrap: three tiles that reflow to 2 + 1
   read as a group of two and an afterthought. At two tiles the same grid
   gives two halves, which is what the reports step wants. */
/* THE INVENTED ANSWER on the Suggest step. Sized in em throughout, so it
   grows and shrinks with the card rather than holding one pixel size -- the
   whole reason this is drawn and not a screenshot. It reads the same tokens
   as the real panel, so it follows the theme without knowing one exists. */
.walk-example {
  display: flex;
  flex-direction: column;
  gap: 0.3em;
  margin: 0.2em 0 0.9em;
  padding: 0.5em;
  border: 1px solid var(--line);
  border-radius: 0.7em;
  background: var(--bg);
  text-align: start;
}
.walk-example-row {
  display: flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.35em 0.45em;
  border-radius: 0.5em;
  background: var(--surface);
}
.walk-example-body { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.walk-example-dish {
  font-size: 0.85em;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.walk-example-place { font-size: 0.72em; color: var(--ink); opacity: 0.65; }
/* The figure in the chip the app uses for a priced meal, because that is
   what it is: the number this panel exists to hand you. */
.walk-example-kcal {
  flex: 0 0 auto;
  padding: 0.2em 0.6em;
  border-radius: var(--r-pill);
  background: var(--ok-tint);
  border: 1px solid var(--ok-line);
  color: var(--ink);
  font-size: 0.78em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.walk-tiles {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 0.4rem;
  margin: 0.9rem 0 0;
}
.walk-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  padding: 0.6rem 0.35rem;
  border: 1px solid var(--line);
  border-radius: 10px;
}
/* Not greyed, unlike .walk-icon above: these ARE the controls being
   pointed at, and dulling them dulls the only thing the row is for. */
.walk-tile-icon { display: inline-flex; line-height: 1; color: var(--ink); }
/* `--ink` with opacity, not a muted token: this sheet has no --muted, and
   var() on a property that does not exist silently inherits whatever the
   parent colour is. Every other secondary line here does the same. */
.walk-tile-label { font-size: 0.78rem; color: var(--ink); opacity: 0.75; line-height: 1.2; }

.walk-dots { display: flex; justify-content: center; gap: 0.35rem; margin: 0.9rem 0 0.8rem; }
.walk-dot {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--line);
}
.walk-dot[data-current] { background: var(--accent); }

/* A SHORT SCREEN GETS A SHORTER CARD.
   The caption is placed beside the control it is about, and on an iPhone SE
   -- 667 tall against a card that wants 320 with the three-tile row on it --
   neither side of a target in the middle of the screen can hold it. The
   placement code then caps the text and the step reads cramped, with its own
   body scrolling inside a box.
   That fallback exists for the genuinely impossible case, not for the
   commonest small phone in use. So the card gives up its padding, a little
   type size and the air around its dots first, which buys back the ~90px
   that keeps the whole caption on screen unscrolled. Keyed to HEIGHT, not
   width: the thing in short supply is vertical, and a phone in landscape has
   the same problem on a wider screen. */
@media (max-height: 720px) {
  .walk-card { padding: 0.75rem 0.9rem 0.6rem; }
  .walk-title { font-size: 0.98rem; margin-bottom: 0.25rem; }
  .walk-body { font-size: 0.86rem; line-height: 1.35; }
  .walk-tiles { gap: 0.3rem; margin-top: 0.6rem; }
  .walk-tile { padding: 0.4rem 0.3rem; gap: 0.2rem; }
  .walk-tile-label { font-size: 0.72rem; }
  .walk-dots { margin: 0.55rem 0 0.5rem; }
  .walk-back, .walk-next { padding: 0.4rem 0.9rem; }
}

/* Skip pushed to one end, the way forward at the other: the two are not
   alternatives of equal weight, and putting them side by side would ask
   somebody to choose rather than to continue. */
.walk-actions { display: flex; align-items: center; gap: 0.5rem; }
.walk-skip {
  background: transparent;
  border: 0;
  color: var(--ink);
  padding: 0.5rem 0.25rem;
  font-size: 0.88rem;
  margin-inline-end: auto;
}
.walk-back { background: transparent; border: 1px solid var(--line); color: var(--ink); }
.walk-next { background: var(--accent); color: var(--bg); border: 0; font-weight: 600; }
.walk-back, .walk-next { border-radius: 999px; padding: 0.5rem 1.1rem; font-size: 0.9rem; }
/* A row, so the glyph and the word sit together rather than running into
   each other, and so the gap is the same on both sides of the page. */
.start-try { display: inline-flex; align-items: center; gap: 0.4rem; }/* THE CAMERA IS A SHEET NOW, for the reason the recorder is one: it
   was pinned above the composer at the tail of a week-long timeline,
   so it had to be scrolled to before it could be seen, and what it
   was saying competed with a diary of meals directly behind it. */
.camera-overlay { z-index: 6; }
.camera-sheet {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
}
.camera-sheet .overlay-head { width: 100%; margin-bottom: 0; }

/* Capped rather than free: the preview is the phone's own aspect
   ratio, and unconstrained it pushes the shutter off the bottom of
   an 80vh sheet on a tall screen -- leaving a camera you cannot take
   a picture with. `object-fit: cover` keeps the frame filled while
   the cap does its work. */
#preview {
  width: 100%;
  max-height: 52vh;
  object-fit: cover;
  border-radius: 12px;
  background: #000;
  display: block;
}

/* The one control that matters, drawn as the thing it is. The inner span
   is the white disc; the button is the ring around it.
   THE ONE BORDER IN THIS APP THAT IS NOT 1px, and the exception is the
   shape: a shutter is a ring, and a hairline ring around a 4.5rem disc
   reads as a rendering fault rather than as a camera button. 2px is what
   the canvas draws its own capture ring at, so it is the exception the
   design language already makes for itself. */
.shutter {
  width: 4.5rem;
  height: 4.5rem;
  padding: 0;
  border-radius: 50%;
  background: transparent;
  border: 2px solid var(--ink);
  display: grid;
  place-items: center;
  margin-bottom: 0.25rem;
}
.shutter > span {
  width: 3.4rem;
  height: 3.4rem;
  border-radius: 50%;
  background: var(--ink);
  display: block;
  transition: transform 90ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .shutter > span { transition: none; }
}
/* The shutter's whole feedback. There is no sound and no flash, so
   without this a tap that worked and a tap that missed look alike. */
.shutter:active > span { transform: scale(0.86); }

/* Said under the list that stays on screen, not instead of it: the
   previous answer is still the answer, and replacing it with a bare
   message would take away what was already useful. */
.advice-exhausted {
  margin-top: 0.75rem;
  color: var(--ink);
  font-size: 0.85rem;
}

/* THE DOCK IS WHAT IS STICKY, not the form inside it, so the keyboard inset
   and the home-indicator inset are handled once for the whole assembly --
   and so `--composer-height` (measured from this element in app.js) covers
   everything the jump-to-newest button and the two strips must clear. */
.composer-dock {
  /* The field's resting height, read by the strips that park above it. */
  --dock-h: 48px;
  position: sticky;
  bottom: var(--keyboard-inset, 0px);
  /* Above the timeline and the two sticky strips that park on top of the
     dock (.quick-log at z-index 2), below the menu drawer (7) and the
     remaining overlays (5) -- the Suggest drawer is not a modal and must
     not cover a sheet deliberately opened over it. */
  z-index: 4;
  background: var(--bg);
  border-top: 1px solid var(--line);
  /* The lowest thing on screen, so the home indicator's inset belongs
     here. It used to sit on the tab row below the composer; with the row
     gone, leaving it there would have padded nothing. */
  padding-bottom: env(safe-area-inset-bottom);
}

.composer {
  display: flex;
  flex-direction: column;
  gap: 0;
  align-items: stretch;
  padding: 0.5rem 1rem;
  background: transparent;
}

/* Focus moves in here on expansion and the drawer may hold nothing but a
   status line; the ring on a box that is not a control reads as an error. */
.suggest-drawer:focus { outline: none; }

/* While the field is empty the drawn caret is the one on screen, so the
   real one is made transparent. Two carets at the same origin would be one
   fat blinking bar with two rhythms. */

.icon { padding: 0.6rem 0.75rem; }
/* 44 EVERY WAY. The header's ☰ measured 54x32: its height came from the
   baseline rule that matches it to the title's cap height, which is right
   about where it sits and says nothing about how big it is to a thumb. The
   icon inside keeps its size; only the target grew. */
#menu-toggle {
  min-width: var(--tap);
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* Send's hit area used to need extending past a 40px circle to reach 44.
   There is no circle: the control is 44x40 of transparent box around a bare
   arrow, so the target is the button itself. */

/* A row, so the glyph and the word sit together rather than running into
   each other, and so the gap is the same on both sides of the page. */
.start-try { display: inline-flex; align-items: center; gap: 0.4rem; }
/* GREY, like the dock's attach glyphs and for the same reason: these two
   buttons open the same recorder and the same camera. */
.start-try-icon {
  flex: 0 0 auto;
  filter: grayscale(1);
  opacity: 0.75;
}

/* A label for a screen reader and nothing for an eye. The app had three
   hand-rolled copies of this inline; one class instead. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* THE PLACE PICKER. Chips over a hidden <select> that still holds the
   value -- see public/place-chips.js for why the select stays. */
.place-picker { display: flex; flex-direction: column; gap: 8px; }
.pref-label { font-size: 14px; font-weight: 700; }
.place-search {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap);
  padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--bg);
}
.place-search input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--ink);
  font-family: inherit;
  font-size: 16px;
}
/* Wrapping, not scrolling. A horizontal strip hides the end of the list
   behind an edge with nothing saying it is there, and the whole point of
   the ordering is that what you want is near the front -- which only helps
   if you can see where the front ends. */
.place-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.place-chip {
  min-height: 40px;
  padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  white-space: nowrap;
}
/* Chosen is filled, the way a chosen thing is filled everywhere else here. */
.place-chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--bg);
}
/* The star earns the accent; a pin and a house are just marks. */
.place-chip[data-kind="fav"] svg { color: var(--accent); }
.place-chip[aria-pressed="true"] svg { color: inherit; }
.place-empty { margin: 0; font-size: 14px; }


/* The rest of the quiet controls, kept with the ••• so there is one list of
   them rather than a grey added wherever somebody noticed. `.undo` detaches
   a capture from its meal, `.linkish` is a change of mind on the auth
   screens, `.location-action` acts on one row of the Places list. None of
   them is the point of the thing it sits on. */
.undo, .linkish, .location-action, .bubble-edit + .actions button { color: var(--affordance); }
/* ...and a control that cannot be pressed is still distinguishable from one
   that merely is not the point: same grey, visibly dimmer. */
.actions button:disabled, .undo:disabled, .location-action:disabled { opacity: 0.5; }

/* SOMETHING IS STILL IN THE FIELD.
   `data-pending` is set once the composer has been left with unsent words,
   a photo or a reply in it, and two minutes later whatever is in there is
   dropped (see pendingCapture in app.js). The composer is always on screen,
   so the content is visible -- what this says is that you are no longer
   looking at it and a clock is running.

   It used to be a dot on the tab that opened the composer. There is no such
   tab, and the rule went on pointing at it for a release because the test
   guarding it asserted the SELECTOR and never asked whether the element
   existed. It does now. */
.composer-dock[data-pending] .log-row { border-color: var(--accent); }

/* THE BMI BAND, IN COLOUR. It was a number and a word in the same ink as
   every other figure on the profile, so the one figure on that screen that
   says something about you read as indistinguishable from your height.

   Four bands, three colours: healthy is the OK green, the two at either end
   of it are the food amber -- they are a note, not an alarm -- and obese is
   the danger red. Deliberately not a gradient of four: the question anybody
   asks of this number is "is this the ordinary one", and three answers say
   that where four shades do not.

   The word is coloured with the figure, because the word is the part that
   is readable without knowing the thresholds -- which is why it is there. */
.profile-figure-value[data-band="healthy"] { color: var(--ok); }
.profile-figure-value[data-band="underweight"],
.profile-figure-value[data-band="overweight"] { color: var(--food); }
.profile-figure-value[data-band="obese"] { color: var(--danger); }
.profile-figure-value[data-band] { font-weight: 700; }

/* THE PITCH ON THE SIGN-IN SCREEN. Somebody arriving on an invite link saw
   the app's name and two empty fields, and nothing saying what they were
   signing up to. Narrow measure and quiet weight: it is there to be read
   once, not to compete with the form under it. */
.login-pitch {
  margin: 0 0 0.5rem;
  max-width: 34ch;
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text);
}
.login-points {
  margin: 0 0 1.25rem;
  padding-inline-start: 1.1rem;
  max-width: 38ch;
  display: grid;
  gap: 0.35rem;
  font-size: 0.9rem;
  line-height: 1.4;
  color: var(--ink);
}

/* A PHOTOGRAPH OF THE SCREEN, BEHIND THE TOUR'S CARD.
   It replaces the invented dish panel that used to sit here, and it used to
   sit INSIDE the card -- see walkthrough.js for both. Behind it now, because
   the two steps that cannot show their screen live are the two steps whose
   screen most needs showing, and a picture glued to a caption is a leaflet
   about the app rather than the app with a label on it.
   AND IT IS THE WHOLE SCREEN. It shipped capped at 58vh with the dim showing
   around it, which is a photograph of a phone screen displayed inside a phone
   screen: "the screenshot behind needs to be at the size of the entire screen
   behind the bubble." The capture stands in for the app, so it gets the app's
   room, and the card sits on it the way it sits on the live screen on every
   other step. */
.walk-shot {
  position: fixed;
  inset: 0;
  pointer-events: none;
}
/* AND NOTHING OVER IT. There was a 55% black sheet here, inherited from the
   hole's box-shadow on the steps that have a hole -- which on a step whose
   whole content IS the picture means photographing a screen and then hiding
   it: "the screens behind, behind the explanation, should not be in a
   gradient -- they should be seen clearly." What the dim was doing, telling
   the card apart from what is under it, the card's own accent outline does
   now (see .walk-card) in two pixels instead of a sheet over the screen. */
/* THE FRAME IS THE SCREEN. The picture fills it and the ring sits over it, so
   a ring placed at 12% of the frame lands on the pixels shots.mjs measured it
   off -- see .walk-shot img below for the half of that which matters. */
.walk-shot-frame {
  position: absolute;
  inset: 0;
  /* A capture is of one theme; the frame under it is of this one, so a
     half-decoded picture is never a bright hole in a dark screen. */
  background: var(--bg);
}
/* FILL, NOT COVER. The frame is this phone's screen and the capture is
   another one's, so the two shapes differ by a few percent. Stretching by
   that much is invisible; cropping by it is not, because a crop moves every
   fraction in shot-points.js off the pixels it was measured from and the ring
   lands beside its control. */
.walk-shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: fill;
}
/* WHERE THE HOLE WOULD HAVE BEEN. A hole cuts to the live app; this is a
   picture, so the control is ringed on it instead. Percentages: nothing here
   measures the image, so it is right before the image has decoded.
   Above the dim, and the only thing on this step that is: the ring is what
   the card is pointing at. */
.walk-ring {
  position: absolute;
  z-index: 1;
  border: 2px solid var(--accent);
  border-radius: 10px;
  box-shadow:
    0 0 0 3px rgba(0, 0, 0, 0.35),
    0 0 14px 2px var(--accent);
  pointer-events: none;
}


/* WHERE THE DAY FOLLOWED THE PLAN, UNDER THE DAY'S OWN FIGURES.
   "Give me on the daily where I followed it and where I didn't." Two lists,
   and the whole job of this rule is that the eye can tell them apart without
   reading them -- a flat list of five lines, three of them praise and two of
   them not, is a list nobody scans on a phone between meals. A rule down the
   start edge in the colour each side already means elsewhere in the app:
   --ok for what held, --danger for what did not. */
.guidance-day { margin-top: 1rem; }
.guidance-day-side {
  margin: 0.5rem 0 0;
  padding-inline-start: 0.6rem;
  border-inline-start: 2px solid var(--line);
}
.guidance-day-side[data-verdict="good"] { border-inline-start-color: var(--ok); }
.guidance-day-side[data-verdict="bad"] { border-inline-start-color: var(--danger); }
.guidance-day-head {
  margin: 0 0 0.2rem;
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.7;
}
.guidance-day-side ul { margin: 0; padding-inline-start: 1.1rem; }
.guidance-day-side li { font-size: 0.88rem; line-height: 1.4; }

/* THE DIETITIAN'S PLAN, AND THE MORNING VERDICT ON YESTERDAY.
   See public/guidance.js. The verdict leads the sheet because it is the part
   that is new every morning; the plans under it are the part that changes
   once a season. */
.guidance-review {
  margin: 0 0 1rem;
  padding: 0.75rem;
  border: 1px solid var(--line);
  border-radius: 0.6rem;
  background: var(--surface);
}
.guidance-review-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
.guidance-review-title { margin: 0; font-size: 1rem; }
.guidance-review-day { font-size: 0.8rem; color: var(--affordance); }
.guidance-part { margin-top: 0.6rem; }
.guidance-part-title {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0 0 0.25rem;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--affordance);
}
.guidance-part-icon { display: inline-flex; }
/* The three lists are told apart by their mark, not by a colour on the text:
   a verdict is a report, and red prose about yesterday's dinner reads as a
   telling-off from an app that is not the one giving the advice. */
.guidance-good .guidance-part-icon { color: var(--ok); }
.guidance-bad .guidance-part-icon { color: var(--symptom); }
.guidance-improve .guidance-part-icon { color: var(--accent); }
.guidance-lines { margin: 0; padding-inline-start: 1.1rem; }
.guidance-lines li { margin: 0.15rem 0; }

.guidance-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin: 0.5rem 0;
}
/* A LABEL DRESSED AS A BUTTON. The file input inside it is hidden, because a
   browser's own "Choose file" control cannot be made to say what this does. */
.guidance-photo {
  cursor: pointer;
  padding: 0.5rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: var(--surface);
  font-size: 0.9rem;
}

.guidance-item {
  margin: 0 0 0.6rem;
  padding: 0.6rem;
  border: 1px solid var(--line);
  border-radius: 0.6rem;
  background: var(--surface);
}
.guidance-item-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}
.guidance-item-title { font-weight: 600; }
.guidance-item-meta { font-size: 0.78rem; color: var(--affordance); margin-inline-start: auto; }
.guidance-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.3rem;
  border: 0;
  background: none;
  color: var(--affordance);
  cursor: pointer;
}
/* THE PLAN IN THE WORDS IT WAS GIVEN. A plan is a list more often than it is
   a paragraph, so the line breaks are kept -- and wrapped, since a <pre> that
   scrolls sideways inside a phone-width sheet hides the end of every line. */
.guidance-item-text {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font: inherit;
  color: var(--ink);
}
.guidance-empty { color: var(--affordance); margin: 0.25rem 0; }

/* THE RULES IN FORCE. A plain list, because that is what it is: the set this
   diary is judged against, read top to bottom. No card per rule -- a card
   each turned twelve sentences into twelve boxes and buried the one thing
   worth reading, which is the sentences. */
.guidance-rule-list {
  margin: 0 0 0.75rem;
  padding: 0;
  list-style: none;
}
.guidance-rule {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--line);
}
.guidance-rule:last-child { border-bottom: 0; }
.guidance-rule-text { color: var(--ink); overflow-wrap: anywhere; }
/* Where it came from, and when it last changed. Small and grey: it is the
   answer to a question the reader only sometimes asks, and at full ink it
   competed with the rule itself. */
.guidance-rule-meta { font-size: 0.78rem; color: var(--affordance); }

/* OPENING A DOCUMENT. The marker is left on -- it is the one affordance
   saying this row has more behind it -- and the summary itself is tinted like
   every other pressable thing on the sheet. */
.guidance-item-fold > summary {
  cursor: pointer;
  color: var(--affordance);
  font-size: 0.85rem;
}
.guidance-item-fold > .guidance-item-text { margin-top: 0.4rem; }
.guidance-item-scan {
  display: inline-block;
  margin-top: 0.5rem;
  color: var(--accent);
}


/* THE ANSWERS A QUESTION ALREADY CONTAINS, as chips. See questionBubble in
   public/chat.js and the parser in public/question-options.js. A row rather
   than a column because there are never more than three, and wrapping because
   "small, medium or large" in Hebrew is wider than a 375pt phone. */
.question-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.5rem 0 0.25rem;
}
.question-chip {
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 0.9rem;
  cursor: pointer;
}
