/* ==========================================================================
   RecipeHub — Warm Table component layer
   ==========================================================================

   The `rh-` component vocabulary. Every rule below reads the SEMANTIC tokens
   from `tokens.css` and never a raw ramp, which is what keeps one card looking
   like every other card.

   Provenance: adapted from the supplied design system
   (`docs/Design/RecipeHub Design System.zip`), `components/{core,forms,feed,
   navigation,feedback,admin}/*.css`. The artifact ships these as vanilla CSS
   with `rh-` classes, so they are usable directly from EJS — no React, no
   bundler, no runtime. The `.jsx` files in the artifact were read as VISUAL
   AND INTERACTION SPECIFICATIONS only; none of them ships.

   Two adaptations, both recorded in the implementation ledger:

     - the artifact's `.rh-icon` is a React wrapper around a CDN Lucide import.
       This product may not reach a CDN, so the icon rules here drive a local
       same-origin SVG sprite instead (`/assets/icons/sprite.svg`).

     - a small number of additions marked `[rh+]` cover screens the artifact did
       not draw (chat thread, media lab, stats, auth). They are built out of the
       same tokens rather than invented, so they inherit the system by
       construction.

   What is deliberately NOT here: any `!important`, any physical `left`/`right`,
   any hard-coded colour, any pixel value that is not a token. If a value is
   needed that the token layer does not have, the layout is wrong.
   ========================================================================== */

/* --------------------------------------------------------------------------
   ICON — a local sprite, not a library.

   `<svg class="rh-icon"><use href="/assets/icons/sprite.svg#heart"></use></svg>`

   The name is chosen by the server or by an allow-listed client constant; it is
   never interpolated from user data, and nothing here ever builds markup from a
   string. Decorative icons carry `aria-hidden`; an icon-only control carries an
   `aria-label`.
   -------------------------------------------------------------------------- */
.rh-icon{width:20px;height:20px;flex:none;display:inline-block;vertical-align:-0.15em;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}
.rh-icon--sm{width:16px;height:16px}
.rh-icon--nav{width:22px;height:22px}
.rh-icon--rail{width:26px;height:26px}
.rh-icon--lg{width:32px;height:32px}
/* Fill carries meaning in exactly two places: a pressed heart and a pressed
   bookmark. Everywhere else an icon is a line drawing. */
[aria-pressed="true"]>.rh-icon--fillable{fill:currentColor}



/* ==========================================================================
   CORE — button ranks, badge, tag, avatar, card, section, empty state, chat bubble
   ========================================================================== */

/* Buttons — FIVE ranks, and they are ranks rather than styles.
   The audit found 74% of buttons wearing the same ghost treatment, so
   "primary" meant nothing. Rank is: primary (one per screen) > secondary >
   ghost > quiet > danger. Danger is not a rank, it is a warning. */
.rh-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);font-family:var(--font-core);font-weight:var(--weight-bold);font-size:var(--text-sm);line-height:1;min-height:var(--tap-min);padding:0 var(--space-5);border-radius:var(--radius-control);border:1px solid transparent;cursor:pointer;text-decoration:none;white-space:nowrap;transition:background-color var(--speed) var(--ease-out),color var(--speed) var(--ease-out),border-color var(--speed) var(--ease-out),box-shadow var(--speed) var(--ease-out),transform var(--speed-fast) var(--ease-out)}
.rh-btn:active:not(:disabled){transform:scale(var(--press-scale))}
.rh-btn:disabled{opacity:.5;cursor:not-allowed;box-shadow:none}
.rh-btn--primary{background:var(--color-accent);color:var(--color-text-on-accent);box-shadow:var(--shadow-accent)}
.rh-btn--primary:hover:not(:disabled){background:var(--color-accent-hover)}
.rh-btn--secondary{background:var(--color-accent-soft);color:var(--color-accent-text)}
.rh-btn--secondary:hover:not(:disabled){background:var(--color-accent);color:var(--color-text-on-accent)}
.rh-btn--ghost{background:transparent;color:var(--color-text);border-color:var(--color-border)}
.rh-btn--ghost:hover:not(:disabled){border-color:var(--color-accent);color:var(--color-accent-text)}
.rh-btn--quiet{background:transparent;color:var(--color-text-muted);padding-inline:var(--space-3);min-height:var(--tap-min)}
.rh-btn--quiet:hover:not(:disabled){color:var(--color-text)}
.rh-btn--danger{background:var(--color-danger);color:#fff}
.rh-btn--danger:hover:not(:disabled){filter:brightness(.9)}
/* On media: the only translucent surface in the system. It exists because a
   solid button over a photograph covers the food. */
.rh-btn--onmedia{background:rgba(255,255,255,.18);color:#fff;border-color:rgba(255,255,255,.55);backdrop-filter:blur(8px)}
.rh-btn--onmedia:hover:not(:disabled){background:#fff;color:var(--rh-ink-900)}
.rh-btn--sm{min-height:var(--tap-min);font-size:var(--text-xs);padding:0 var(--space-4);gap:var(--space-1)}
.rh-btn--lg{min-height:56px;font-size:var(--text-base);padding:0 var(--space-6)}
.rh-btn--block{width:100%}
.rh-btn__icon{display:inline-flex;width:1.25em;height:1.25em}
.rh-btn__icon svg{width:100%;height:100%}

/* Badge — a state label. Never a count, never a button. */
.rh-badge{display:inline-flex;align-items:center;gap:var(--space-1);padding:2px var(--space-3);border-radius:var(--radius-pill);font-size:var(--text-2xs);font-weight:var(--weight-bold);line-height:1.5;background:var(--color-surface-sunken);color:var(--color-text-muted);border:1px solid transparent}
.rh-badge--accent{background:var(--color-accent-soft);color:var(--color-accent-text)}
.rh-badge--warning{background:var(--color-warning-soft);color:var(--color-warning-text);border-color:var(--color-warning)}
.rh-badge--success{background:var(--color-success-soft);color:var(--color-success-text)}
.rh-badge--danger{background:var(--color-danger-soft);color:var(--color-danger-text)}
.rh-badge--solid{background:var(--color-accent);color:var(--color-text-on-accent)}
.rh-badge--onmedia{background:rgba(255,255,255,.94);color:var(--rh-clay-700);box-shadow:0 2px 8px rgba(0,0,0,.18)}

/* Tag — taxonomy. Selectable, sometimes removable; visually lighter than a
   badge because it is content, not status. */
.rh-tag{display:inline-flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-3);border-radius:var(--radius-control);border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-text);font-size:var(--text-xs);font-family:var(--font-core);cursor:default;transition:background-color var(--speed) var(--ease-out),border-color var(--speed) var(--ease-out),color var(--speed) var(--ease-out)}
.rh-tag--interactive{cursor:pointer;min-height:var(--tap-min)}
.rh-tag--interactive:hover{border-color:var(--color-accent)}
.rh-tag[aria-pressed="true"],.rh-tag--selected{background:var(--color-accent);border-color:var(--color-accent);color:var(--color-text-on-accent);font-weight:var(--weight-bold)}
.rh-tag__remove{display:inline-flex;border:0;background:none;color:inherit;cursor:pointer;padding:0;font-size:1em;line-height:1}

/* Avatar */
.rh-avatar{display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-avatar);object-fit:cover;flex:none;background:var(--color-accent);color:#fff;font-weight:var(--weight-bold);overflow:hidden}
.rh-avatar--onmedia{border:2px solid rgba(255,255,255,.85)}

/* Card — ONE card. Three densities, no per-screen variants. */
.rh-card{background:var(--color-surface);border-radius:var(--radius-card);padding:var(--pad-card);border:1px solid transparent}
.rh-card--outline{border-color:var(--color-border)}
.rh-card--raised{box-shadow:var(--shadow-1)}
.rh-card--flush{padding:0;overflow:hidden}
.rh-card--sunken{background:var(--color-surface-sunken)}
.rh-card__title{margin:0 0 var(--space-2);font:var(--type-subhead)}

/* Section — a heading and its optional trailing link. Replaces the ad-hoc
   h2-plus-float pattern that produced most of the 19 card treatments. */
.rh-section{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-4);margin-bottom:var(--space-3)}
.rh-section__title{margin:0;font:var(--type-heading)}
.rh-section__sub{margin:2px 0 0;font:var(--type-caption);color:var(--color-text-muted)}
.rh-section__link{flex:none;font-size:var(--text-sm);font-weight:var(--weight-bold);color:var(--color-accent-text);text-decoration:none}

/* Empty / error / offline — ONE language for all three (audit finding J). */
.rh-empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--space-3);padding:var(--space-9) var(--space-5);color:var(--color-text-muted)}
.rh-empty__mark{width:56px;height:56px;display:flex;align-items:center;justify-content:center;border-radius:var(--radius-avatar);background:var(--color-surface-sunken);color:var(--color-text-muted)}
.rh-empty--error .rh-empty__mark{background:var(--color-danger-soft);color:var(--color-danger-text)}
.rh-empty--offline .rh-empty__mark{background:var(--color-warning-soft);color:var(--color-warning-text)}
.rh-empty__title{margin:0;font:var(--type-subhead);color:var(--color-text)}
.rh-empty__body{margin:0;max-width:38ch;font:var(--type-body-sm);text-wrap:pretty}

/* Chat bubble — bidi handling preserved verbatim from the product. */
.rh-bubble{max-width:75%;overflow-wrap:anywhere;unicode-bidi:plaintext;padding:var(--space-2) var(--space-4);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-card) var(--radius-card) var(--radius-card) var(--space-1);font-size:var(--text-sm)}
.rh-bubble p{margin:0;unicode-bidi:plaintext;text-align:start}
.rh-bubble--mine{margin-inline-start:auto;background:var(--color-accent-soft);border-color:transparent;border-radius:var(--radius-card) var(--radius-card) var(--space-1) var(--radius-card)}
.rh-bubble__time{display:block;margin-top:2px;font-size:var(--text-2xs);color:var(--color-text-muted);direction:ltr;unicode-bidi:isolate;text-align:start}


/* ==========================================================================
   FORMS — field, input, select, checkbox, search bar
   ========================================================================== */

/* Field — label, control, hint, error. The error is bound with aria-describedby
   and coloured AND prefixed with an icon, so status never rests on colour. */
.rh-field{display:block;margin-bottom:var(--space-5)}
.rh-field__label{display:block;font:var(--type-label);margin-bottom:var(--space-2);color:var(--color-text)}
.rh-field__req{color:var(--color-accent-text);margin-inline-start:2px}
.rh-field__hint{display:block;margin-top:var(--space-2);font:var(--type-caption);color:var(--color-text-muted)}
.rh-field__error{display:flex;align-items:center;gap:var(--space-1);margin-top:var(--space-2);font:var(--type-caption);font-weight:var(--weight-bold);color:var(--color-danger-text)}

.rh-input{width:100%;min-height:var(--tap-min);padding:var(--space-3) var(--space-4);font:var(--type-body);font-family:var(--font-core);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-input);transition:border-color var(--speed) var(--ease-out),box-shadow var(--speed) var(--ease-out)}
.rh-input::placeholder{color:var(--color-text-muted)}
.rh-input:focus{outline:none;border-color:var(--color-accent);box-shadow:var(--focus-ring)}
.rh-input[aria-invalid="true"]{border-color:var(--color-danger);border-width:2px}
.rh-input--textarea{min-height:7rem;resize:vertical;line-height:var(--leading-normal)}
.rh-input--ltr{direction:ltr;unicode-bidi:isolate;text-align:start}
.rh-input--num{width:7rem;direction:ltr;text-align:end;font-variant-numeric:tabular-nums}
.rh-select{appearance:none;background-image:none;padding-inline-end:var(--space-8)}
.rh-select-wrap{position:relative;display:block}
.rh-select-wrap>svg,.rh-select-wrap>i{position:absolute;inset-inline-end:var(--space-4);top:50%;transform:translateY(-50%);pointer-events:none;color:var(--color-text-muted)}

.rh-check{display:flex;align-items:center;gap:var(--space-2);min-height:var(--tap-min);padding:var(--space-1) 0;break-inside:avoid;font-size:var(--text-sm);cursor:pointer}
.rh-check input{width:20px;height:20px;accent-color:var(--color-accent);flex:none}

/* Search bar — the discovery door. The filter button carries a COUNT so
   "why so few results" answers itself (audit finding A). */
.rh-searchbar{display:flex;align-items:center;gap:var(--space-2)}
.rh-searchbar__field{position:relative;flex:1;min-width:0;display:flex;align-items:center}
.rh-searchbar__field>.rh-icon{position:absolute;inset-inline-start:var(--space-4);color:var(--color-text-muted);pointer-events:none}
.rh-searchbar__input{width:100%;min-height:var(--tap-min);padding:var(--space-3) var(--space-9) var(--space-3) var(--space-4);padding-inline-start:var(--space-9);font:var(--type-body);font-family:var(--font-core);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-control)}
.rh-searchbar__input:focus{outline:none;border-color:var(--color-accent);box-shadow:var(--focus-ring)}
.rh-searchbar__filters{display:inline-flex;align-items:center;gap:var(--space-2);flex:none;min-height:var(--tap-min);padding:0 var(--space-4);border:1px solid var(--color-border);border-radius:var(--radius-control);background:var(--color-surface);color:var(--color-text);font:var(--type-label);cursor:pointer}
.rh-searchbar__filters[aria-expanded="true"]{border-color:var(--color-accent);color:var(--color-accent-text)}
.rh-searchbar__count{min-width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-pill);background:var(--color-accent);color:var(--color-text-on-accent);font-size:var(--text-2xs);font-variant-numeric:tabular-nums}


/* ==========================================================================
   FEED — media, feed card, action rail, why-chips, match badge, tile, shelf
   ========================================================================== */

/* ==========================================================================
   MEDIA FIRST. The food is the card; everything else is layered on or under it.
   The scrim pattern is preserved from the product and promoted to a token.
   ========================================================================== */
.rh-media{display:block;width:100%;object-fit:cover;background:var(--media-placeholder);filter:var(--media-tint)}

/*
 * §3 - THE CROP WINDOW, BECAUSE THERE IS NO SEPARATE WIDE PHOTO SET.
 *
 * Decision 3 of the 25.8 log, in full: "אין סט 16:9 נפרד. חותכים מה-4:5 עם
 * object-fit:cover ו-object-position:center 40% (הסובייקטים ממורכזים בנדיבות -
 * נבדק). לא מותחים, לא ממסגרים בפסים."
 *
 * THE PREMISE WAS CHECKED BEFORE THE RULE WAS APPLIED, because a crop window
 * tuned for a 4:5 source is a way of cropping the food out of the picture if the
 * source is something else. Every visible cover this product serves measured
 * `naturalWidth / naturalHeight = 0.800` - 4:5 exactly, as the ruling assumes.
 *
 * Three surfaces then crop that portrait source into a wider frame, measured at
 * 1280:
 *
 *     .rh-media--hero    natural 0.800  shown 1.333   (4:3, the recipe hero)
 *     .u-thumb-wide      natural 0.800  shown 2.222   (the search result tile)
 *     the feed           natural 0.800  shown 0.563   (crops SIDEWAYS, not down)
 *
 * THE FEED IS NOT TOUCHED AND DOES NOT NEED TO BE. Its frame is NARROWER than
 * the source, so the browser trims left and right and the vertical half of an
 * `object-position` has nothing to act on - a declaration there would be a
 * decoration that reads as coverage. It is written as `center 40%` rather than
 * a bare `40%` for the same reason in reverse: on any surface that does crop
 * sideways, the horizontal half is the half doing the work, and it must stay
 * centred.
 *
 * 40 rather than 50 lifts the window by a tenth of the image. A dish shot from
 * above in portrait sits above the geometric centre - the plate at the top, the
 * table below it - so a centred crop of a 2.222 frame keeps 36% of the height
 * and spends a third of that on tablecloth.
 *
 * NOT ON FACES. `.rh-avatar` and `.avatar` also crop with `cover` and are
 * deliberately untouched: a portrait lifted by a tenth cuts chins.
 */
.rh-media{object-position:center 40%}
.rh-media--feed{aspect-ratio:var(--media-ratio-feed)}
.rh-media--tile{aspect-ratio:var(--media-ratio-tile)}
.rh-media--hero{aspect-ratio:var(--media-ratio-hero)}

/* ==========================================================================
   THE EDITORIAL PLATE — a recipe with no photograph. WP-17.5 pass 4.

   Product brief section 14: "the design should make a missing photograph feel
   intentional, but it must still be marked as not having a real photo." Both
   halves of that sentence are load-bearing and the second one is the harder
   one, so the caption is not optional styling - it is the component.

   Six gradients, chosen by a hash of the recipe id in `RH.plateHtml`. Not for
   variety's sake: the previous placeholders repeated every second card, and a
   picture that repeats reads as a failed asset rather than as a decision. All
   six are drawn from the Warm Table ramps; none introduces a new colour.
   ========================================================================== */
.rh-plate{
  position:relative;display:grid;place-items:center;width:100%;overflow:hidden;
  background:var(--plate-wash);isolation:isolate;
  --plate-wash:linear-gradient(145deg,var(--rh-clay-100),var(--rh-clay-300));
}
.rh-plate--v0{--plate-wash:linear-gradient(145deg,var(--rh-clay-100) 10%,var(--rh-clay-300))}
.rh-plate--v1{--plate-wash:linear-gradient(160deg,var(--rh-saffron-100) 5%,var(--rh-saffron-300))}
.rh-plate--v2{--plate-wash:linear-gradient(135deg,var(--rh-paper-100),var(--rh-clay-200))}
.rh-plate--v3{--plate-wash:linear-gradient(150deg,var(--rh-clay-50),var(--rh-saffron-300))}
.rh-plate--v4{--plate-wash:linear-gradient(170deg,var(--rh-saffron-100),var(--rh-clay-300))}
.rh-plate--v5{--plate-wash:linear-gradient(140deg,var(--rh-clay-200),var(--rh-saffron-300))}

/* The plate itself, suggested with two rings rather than illustrated. A drawing
   of food competes with the real photographs beside it and loses. */
.rh-plate::before{
  content:"";position:absolute;z-index:-1;width:min(62%,240px);aspect-ratio:1;
  border-radius:50%;background:rgba(255,255,255,.34);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.5),0 8px 30px rgba(44,38,34,.08);
}
.rh-plate::after{
  content:"";position:absolute;z-index:-1;width:min(44%,168px);aspect-ratio:1;
  border-radius:50%;border:1px solid rgba(255,255,255,.55);
}

/* The dish's own initial. Colour, not opacity: white at 70% over six different
   washes gives six different contrasts, and one of them is always wrong. */
.rh-plate__mark{
  font-family:var(--font-core);font-weight:var(--weight-bold);
  font-size:clamp(44px,26cqi,132px);line-height:1;
  color:var(--rh-clay-800);opacity:.42;
  transform:translateY(-4%);
}
.rh-plate__note{
  position:absolute;bottom:var(--space-3);inset-inline:var(--space-3);
  text-align:center;font:var(--type-caption);color:var(--rh-clay-800);
  opacity:.78;text-wrap:balance;
}

/* Container query units so the mark scales with the PLATE rather than with the
   viewport - the same component is a 620px hero and a 150px shelf tile. */
.rh-plate{container-type:inline-size}

/* Below ~150px the caption cannot be read and stops being honest by being
   present; the accessible name on the container still carries it. */
@container (max-width: 150px){
  .rh-plate__note{display:none}
}

.rh-plate--feed{aspect-ratio:var(--media-ratio-feed)}
.rh-plate--tile{aspect-ratio:var(--media-ratio-tile)}
.rh-plate--hero{aspect-ratio:var(--media-ratio-hero)}
.rh-plate--wide{aspect-ratio:16/9}
/* The list card's thumbnail is a fixed height rather than a ratio, so the plate
   has to match it exactly or the grid steps. */
.rh-plate--thumb{height:150px;border-radius:var(--radius-sm);margin-bottom:var(--space-3)}

.rh-feedcard{display:block}
.rh-feedcard__frame{position:relative;border-radius:var(--radius-media);overflow:hidden;background:var(--color-surface-sunken);box-shadow:var(--shadow-media)}
.rh-feedcard--bleed .rh-feedcard__frame{border-radius:0;box-shadow:none}
.rh-feedcard__scrim{position:absolute;inset-inline:0;bottom:0;padding:var(--space-9) var(--space-4) var(--space-4);padding-inline-end:76px;background:var(--scrim-media);color:var(--color-text-on-media)}
.rh-feedcard__top{position:absolute;inset-inline-start:var(--space-3);top:var(--space-3);display:flex;flex-direction:column;align-items:flex-start;gap:var(--space-2);max-width:72%}
.rh-feedcard__creator{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)}
.rh-feedcard__creator-link{display:flex;align-items:center;gap:var(--space-2);color:inherit;text-decoration:none;min-width:0}
.rh-feedcard__name{display:flex;flex-direction:column;line-height:1.15;font-weight:var(--weight-bold);font-size:var(--text-sm);min-width:0}
.rh-feedcard__handle{font-weight:var(--weight-regular);font-size:var(--text-2xs);opacity:.78;direction:ltr;unicode-bidi:isolate;text-align:start}
.rh-feedcard__title{margin:0 0 2px;font-size:var(--text-md);font-weight:var(--weight-bold);line-height:var(--leading-tight);text-shadow:0 1px 3px rgba(0,0,0,.35);text-wrap:pretty}
.rh-feedcard__title a{color:inherit;text-decoration:none}
.rh-feedcard__meta{margin:0;font-size:var(--text-xs);opacity:.92;font-variant-numeric:tabular-nums}
/* Stacked variant — media above, words on a solid ground below. Used where the
   direction wants information legibility over immersion. */
.rh-feedcard--stacked .rh-feedcard__frame{border-end-start-radius:0;border-end-end-radius:0}
.rh-feedcard--stacked .rh-feedcard__body{background:var(--color-surface);border:1px solid var(--color-border);border-top:0;border-end-start-radius:var(--radius-card);border-end-end-radius:var(--radius-card);padding:var(--space-4)}
.rh-feedcard--stacked .rh-feedcard__title{color:var(--color-text);text-shadow:none;font-size:var(--text-lg)}
.rh-feedcard--stacked .rh-feedcard__meta{color:var(--color-text-muted)}

/* The vertical rail: over the media, thumb-side. A horizontal row of pills
   under the text reads as a form; a column of glyphs reads as a social feed. */
.rh-rail{display:flex;flex-direction:column;align-items:center;gap:var(--space-4);position:absolute;inset-inline-end:var(--space-2);bottom:var(--space-3);margin:0;padding:0}
.rh-rail--horizontal{position:static;flex-direction:row;gap:var(--space-2);flex-wrap:wrap;align-items:center}
.rh-rail__btn{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-width:48px;min-height:48px;padding:2px;border:0;background:transparent;color:var(--color-text-on-media);font-family:var(--font-core);font-size:var(--text-2xs);font-weight:var(--weight-bold);cursor:pointer;text-decoration:none;text-shadow:0 1px 4px rgba(0,0,0,.5);transition:transform var(--speed) var(--ease-out),color var(--speed) var(--ease-out)}
.rh-rail__btn>.rh-icon{filter:drop-shadow(0 1px 4px rgba(0,0,0,.45))}
.rh-rail__btn:hover{transform:scale(1.08)}
.rh-rail__btn[aria-pressed="true"]{color:#ff5a72}
.rh-rail__btn[data-action="favorite"][aria-pressed="true"]{color:#ffcb45}
.rh-rail__btn--quiet{opacity:.72}
/* On a page rather than over media: the same contract, pill-shaped. */
.rh-rail--horizontal .rh-rail__btn{flex-direction:row;gap:var(--space-2);min-height:var(--tap-min);padding:0 var(--space-4);border:1px solid var(--color-border);border-radius:var(--radius-control);background:var(--color-surface);color:var(--color-text);font-size:var(--text-sm);text-shadow:none}
.rh-rail--horizontal .rh-rail__btn[aria-pressed="true"]{background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent-text)}
.rh-rail--horizontal .rh-rail__btn[data-action="like"][aria-pressed="true"]{color:var(--color-danger);border-color:var(--color-danger)}

/* Why-recommended. Under the frame, never over the food. */
.rh-why{list-style:none;margin:var(--space-2) 0 0;padding:0;display:flex;flex-wrap:wrap;gap:var(--space-1)}
.rh-why__item{display:inline-flex;align-items:center;gap:var(--space-1);font-size:var(--text-xs);color:var(--color-accent-text);background:var(--color-accent-soft);border-radius:var(--radius-control);padding:2px var(--space-3)}
.rh-why__item[data-signal="dislikedIngredient"]{color:var(--color-danger-text);background:var(--color-danger-soft)}

.rh-match{display:inline-flex;align-items:center;gap:var(--space-1);background:rgba(255,255,255,.94);color:var(--rh-clay-700);font-weight:var(--weight-bold);font-size:var(--text-xs);padding:var(--space-1) var(--space-3);border-radius:var(--radius-control);box-shadow:0 2px 8px rgba(0,0,0,.18);font-variant-numeric:tabular-nums}

/* Compact tile — the shelf card. Media, title, one meta line. No actions:
   a shelf card is scanned past, not acted on. */
.rh-tile{display:block;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-card);overflow:hidden;padding-bottom:var(--space-3);text-decoration:none;color:inherit;box-shadow:var(--shadow-1);transition:transform var(--speed) var(--ease-out),box-shadow var(--speed) var(--ease-out)}
.rh-tile:hover{transform:translateY(var(--hover-lift));box-shadow:var(--shadow-2)}
.rh-tile__title{margin:var(--space-3) var(--space-3) 2px;font-size:var(--text-sm);font-weight:var(--weight-bold);line-height:var(--leading-snug);color:var(--color-text)}
.rh-tile__meta{margin:0 var(--space-3);font-size:var(--text-xs);color:var(--color-text-muted);font-variant-numeric:tabular-nums}
.rh-tile__warn{margin:var(--space-2) var(--space-3) 0;font-size:var(--text-2xs);font-weight:var(--weight-bold);color:var(--color-warning-text);background:var(--color-warning-soft);border:1px solid var(--color-warning);border-radius:var(--radius-xs);padding:2px var(--space-2);display:inline-block}

/* Shelf — a horizontal rail that says "a slice of something bigger". */
.rh-shelf{margin-bottom:var(--space-7)}
.rh-shelf__rail{display:flex;gap:var(--space-3);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--space-1);scrollbar-width:none}
.rh-shelf__rail::-webkit-scrollbar{display:none}
.rh-shelf__item{flex:none;width:46vw;max-width:190px;scroll-snap-align:start}
@media (min-width:720px){.rh-shelf__item{width:200px;max-width:none}}


/* ==========================================================================
   NAVIGATION — header, tab bar, segmented tabs
   ========================================================================== */

/* ONE navigation vocabulary. The audit found the phone and the desktop using
   different labels in a different order (finding G); here both render from the
   same five destinations, in the same order, with the same words. */
.rh-header{display:flex;align-items:center;gap:var(--space-4);min-height:var(--header-h);padding:0 var(--gutter-screen);background:var(--color-surface);border-bottom:1px solid var(--color-border);position:sticky;top:0;z-index:10}
/* [CSS3 3/5] text-shadow — a soft lift on the wordmark.
   The design system drops this; the course requires the capability to be
   visible, and course demonstrability outranks design fidelity. Kept at the
   strength the shipped product used, which is a lift rather than a glow. */
.rh-header__brand{font-size:var(--text-md);font-weight:var(--weight-bold);color:var(--color-accent-text);text-decoration:none;letter-spacing:var(--tracking-tight);white-space:nowrap;text-shadow:0 1px 0 rgba(255,255,255,.9),0 2px 6px rgba(180,86,42,.22)}
.rh-header__nav{display:none;align-items:center;gap:var(--space-4);margin-inline-start:auto}
.rh-header__actions{display:flex;align-items:center;gap:var(--space-2);margin-inline-start:auto}
.rh-header__link{color:var(--color-text-muted);text-decoration:none;font-size:var(--text-xs);padding:var(--space-1) 0;border-bottom:2px solid transparent;transition:color var(--speed) var(--ease-out),border-color var(--speed) var(--ease-out)}
.rh-header__link--primary{font-size:var(--text-sm);font-weight:var(--weight-bold);color:var(--color-text)}
.rh-header__link:hover{color:var(--color-accent-text)}
.rh-header__link[aria-current="page"]{color:var(--color-accent-text);border-bottom-color:var(--color-accent)}
@media (min-width:720px){.rh-header{min-height:var(--header-h-wide);padding:0 var(--gutter-screen-wide)}.rh-header__nav{display:flex}.rh-header__nav+.rh-header__actions{margin-inline-start:0}}

.rh-navbadge{display:inline-block;margin-inline-start:var(--space-1);min-width:18px;padding:0 5px;border-radius:var(--radius-pill);background:var(--color-accent);color:var(--color-text-on-accent);font-size:var(--text-2xs);line-height:18px;text-align:center;font-variant-numeric:tabular-nums}

/* Bottom tab bar — phone only. Five destinations, one of them the create
   action, which is why it is centred and marked. */
.rh-tabbar{display:flex;position:sticky;bottom:0;inset-inline:0;z-index:20;background:var(--color-surface);border-top:1px solid var(--color-border);box-shadow:var(--shadow-nav);padding-bottom:env(safe-area-inset-bottom,0px)}
.rh-tabbar__tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:var(--tabbar-h);padding:var(--space-2) var(--space-1);color:var(--color-text-muted);text-decoration:none;font-size:var(--text-2xs);font-weight:var(--weight-bold);background:none;border:0;cursor:pointer;font-family:var(--font-core);transition:color var(--speed) var(--ease-out)}
.rh-tabbar__tab[aria-current="page"]{color:var(--color-accent-text)}
.rh-tabbar__tab--create .rh-tabbar__mark{display:flex;align-items:center;justify-content:center;width:38px;height:38px;margin-top:-14px;border-radius:var(--radius-control);background:var(--color-accent);color:var(--color-text-on-accent);box-shadow:var(--shadow-accent)}
@media (min-width:720px){.rh-tabbar{display:none}}

/* Segmented tabs — feed switch, search kind switch. One implementation; the
   audit found several. */
.rh-tabs{display:flex;gap:var(--space-2)}
.rh-tabs__tab{flex:1;min-height:var(--tap-min);padding:var(--space-2) var(--space-4);border:1px solid var(--color-border);border-radius:var(--radius-control);background:var(--color-surface);color:var(--color-text-muted);font-family:var(--font-core);font-weight:var(--weight-bold);font-size:var(--text-sm);cursor:pointer;transition:background-color var(--speed) var(--ease-out),color var(--speed) var(--ease-out),border-color var(--speed) var(--ease-out)}
.rh-tabs__tab[aria-selected="true"]{background:var(--color-accent);border-color:var(--color-accent);color:var(--color-text-on-accent)}
.rh-tabs--underline{gap:var(--space-5);border-bottom:1px solid var(--color-border)}
.rh-tabs--underline .rh-tabs__tab{flex:none;border:0;border-radius:0;background:none;padding:var(--space-3) 0;border-bottom:2px solid transparent}
.rh-tabs--underline .rh-tabs__tab[aria-selected="true"]{background:none;color:var(--color-text);border-bottom-color:var(--color-accent)}


/* ==========================================================================
   FEEDBACK — alert, allergen notice, AI panel, pricing panel, guest banner
   ========================================================================== */

/* Alert — a server said something. One component, four tones, an icon in every
   one so the tone is never carried by colour alone. */
.rh-alert{display:flex;align-items:flex-start;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-radius:var(--radius-input);border:1px solid transparent;font-size:var(--text-sm);background:var(--color-surface-sunken);color:var(--color-text)}
.rh-alert--error{background:var(--color-danger-soft);border-color:var(--color-danger);color:var(--color-danger-text)}
.rh-alert--success{background:var(--color-success-soft);border-color:var(--color-success);color:var(--color-success-text)}
.rh-alert--warning{background:var(--color-warning-soft);border-color:var(--color-warning);color:var(--color-warning-text)}
.rh-alert--info{background:var(--color-accent-soft);color:var(--color-accent-text)}
.rh-alert p{margin:0}

/* Allergen statement — the one block where "renders like everything else" is a
   real failure. Unreviewed gets a border, a tint and a weight reviewed does not. */
.rh-allergen{display:flex;align-items:flex-start;gap:var(--space-2);flex-wrap:wrap;margin:var(--space-3) 0;padding:var(--space-3) var(--space-4);border-radius:var(--radius-input);border:1px solid var(--color-border);background:var(--color-surface);font-size:var(--text-sm);color:var(--color-text-muted)}
.rh-allergen--warn{border-color:var(--color-warning);background:var(--color-warning-soft);color:var(--color-warning-text);font-weight:var(--weight-bold)}

/* AI — assistance, not authority. Deliberately NOT purple: the panel wears the
   product's own surfaces so an AI answer arrives as an ordinary part of writing
   a recipe rather than as a different application bolted on. */
.rh-ai{border:1px solid var(--color-border);border-radius:var(--radius-card);background:var(--color-surface);padding:var(--pad-card)}
.rh-ai__head{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-3)}
.rh-ai__title{margin:0;font:var(--type-subhead)}
.rh-ai__summary{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;margin:var(--space-4) 0 var(--space-2)}
.rh-ai__found{font-size:var(--text-sm);color:var(--color-text-muted);overflow-wrap:anywhere}
.rh-ai__warnings{margin:var(--space-2) 0 var(--space-4);padding:var(--space-3) var(--space-4);padding-inline-start:var(--space-7);border:1px solid var(--color-warning);border-radius:var(--radius-input);background:var(--color-warning-soft);color:var(--color-warning-text);font-size:var(--text-sm)}
.rh-ai__warnings li{margin-bottom:var(--space-1)}
.rh-ai__warnings li:last-child{margin-bottom:0}
.rh-ai__actions{display:flex;flex-wrap:wrap;gap:var(--space-3);align-items:center}
.rh-ai__note{margin:var(--space-3) 0 0;font:var(--type-caption);color:var(--color-text-muted)}

/* Pricing — three states that must stay legible as DIFFERENT states.
   A missing price is a dash and a reason, never a zero. */
.rh-pricing__total{margin:0;display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--space-2)}
.rh-pricing__amount{font-size:var(--text-lg);font-weight:var(--weight-bold);font-variant-numeric:tabular-nums}
.rh-pricing__range{font-size:var(--text-sm);color:var(--color-text-muted);font-variant-numeric:tabular-nums}
.rh-pricing__serving{margin:var(--space-1) 0 0;color:var(--color-text-muted);font-size:var(--text-sm)}
.rh-pricing__none{margin:0;color:var(--color-text-muted);font-size:var(--text-sm)}
.rh-pricing__row{display:grid;grid-template-columns:minmax(6rem,1fr) auto;gap:2px var(--space-3);padding:var(--space-2) 0;border-bottom:1px solid var(--color-divider);font-size:var(--text-sm)}
.rh-pricing__row:last-child{border-bottom:0}
.rh-pricing__row--unpriced .rh-pricing__name{color:var(--color-text-muted)}
.rh-pricing__value{text-align:start;white-space:nowrap;font-variant-numeric:tabular-nums}
.rh-pricing__basis{grid-column:1/-1;font-size:var(--text-2xs);color:var(--color-text-muted)}
.rh-pricing__foot{margin:var(--space-4) 0 0;font-size:var(--text-2xs);line-height:var(--leading-normal);color:var(--color-text-muted)}
@media (max-width:719px){.rh-pricing__total{display:block}.rh-pricing__range{display:block;margin-top:2px}}

/* Guest banner — a shared recipe is a deliberate public landing, not a
   locked door (audit finding D). */
.rh-guestbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--gutter-screen);background:var(--color-surface-inverse);color:var(--color-text-inverse);font-size:var(--text-sm)}
.rh-guestbar__text{margin:0;flex:1;min-width:12rem}


/* ==========================================================================
   ADMIN — the other room: denser, calmer, rule-separated
   ========================================================================== */

/* Admin. Same brand, different job: calmer, denser, operational. The consumer
   product's media-first card language is deliberately absent — a moderation
   queue that looks like a feed invites the wrong reading speed. The boundary is
   a cool-neutral ground and a rule-separated row, not a different palette. */
.rh-admin{--color-bg:var(--color-surface-sunken)}
.rh-adminrow{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-3) var(--space-4);background:var(--color-surface);border-bottom:1px solid var(--color-divider)}
.rh-adminrow:first-child{border-start-start-radius:var(--radius-card);border-start-end-radius:var(--radius-card)}
.rh-adminrow:last-child{border-bottom:0;border-end-start-radius:var(--radius-card);border-end-end-radius:var(--radius-card)}
.rh-adminrow--muted{opacity:.72}
.rh-adminrow__main{min-width:0;flex:1 1 16rem;overflow-wrap:anywhere}
.rh-adminrow__title{margin:0;font-size:var(--text-sm);font-weight:var(--weight-bold)}
.rh-adminrow__meta{margin:2px 0 0;font-size:var(--text-xs);color:var(--color-text-muted)}
.rh-adminrow__actions{display:flex;flex-wrap:wrap;gap:var(--space-2)}
@media (max-width:719px){.rh-adminrow__actions{width:100%}}
.rh-kpi{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-card);padding:var(--space-4)}
.rh-kpi__label{margin:0;font-size:var(--text-xs);color:var(--color-text-muted)}
.rh-kpi__value{margin:var(--space-1) 0 0;font-size:var(--text-xl);font-weight:var(--weight-bold);font-variant-numeric:tabular-nums;line-height:1}
.rh-kpi__delta{margin:var(--space-1) 0 0;font-size:var(--text-xs);color:var(--color-text-muted)}

/* ==========================================================================
   [rh+] SHELL ADDITIONS

   Built from the same tokens as everything above. These cover the parts of the
   application shell the artifact did not draw: the skip link, the header's
   icon-only controls, the divider between the primary and secondary runs of
   navigation, and the phone-sized behaviour of the logout control.
   ========================================================================== */

/* Skip link. Off-screen until focused, then a real, visible target. */
.rh-skip{position:absolute;inset-inline-start:var(--space-3);top:-100px;z-index:60;padding:var(--space-3) var(--space-5);border-radius:var(--radius-control);background:var(--color-accent);color:var(--color-text-on-accent);font:var(--type-label);text-decoration:none;transition:top var(--speed) var(--ease-out)}
.rh-skip:focus{top:var(--space-3);color:var(--color-text-on-accent)}

/* `.rh-header__rule` was here: a hairline between the five primary
   destinations and the secondary run. The element it styled left `header.ejs`
   in 3edb287, when the header stopped carrying seventeen targets, and no React
   header ever drew one. Three rules for an element nothing renders - the only
   selector in this file that PRODUCT-REACT-01's CSS inventory could show was
   describing markup this migration deleted. See
   docs/PRODUCT_REACT_01_EVIDENCE.md, finding REACT-01. */

/* Icon-only header controls. 48px of hit area around a 22px glyph. */
.rh-header__icon{position:relative;display:inline-flex;align-items:center;justify-content:center;min-width:var(--tap-min);min-height:var(--tap-min);border-radius:var(--radius-control);color:var(--color-text-muted);text-decoration:none;transition:color var(--speed) var(--ease-out),background-color var(--speed) var(--ease-out)}
.rh-header__icon:hover{color:var(--color-accent-text);background:var(--color-accent-soft)}
.rh-header__icon[aria-current="page"]{color:var(--color-accent-text)}
@media (min-width:720px){.rh-header__icon--wide-hide{display:none}}

/* A dot rather than a number when the badge sits on a glyph: a count on top of
   an icon at 22px is unreadable, and the number is already in the nav link. */
.rh-navbadge--dot{position:absolute;top:8px;inset-inline-end:8px;min-width:10px;width:10px;height:10px;padding:0;line-height:10px;font-size:0}

/* The word next to the logout glyph is noise on a phone, where the header has
   to hold the brand, search, chat and this in 390px. */
@media (max-width:719px){.rh-header__logout-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}}

/* The tab bar is fixed rather than sticky: sticky at the end of a short page
   leaves it floating mid-screen, and the product has several short pages. */
.rh-tabbar{position:fixed;bottom:0;inset-inline:0}

/* --------------------------------------------------------------------------
   [rh+] PAGE SHELL — the column `body` opens in `tokens.css`, closed here.

   `main` is the only part that grows, so the footer lands at the bottom of a
   short page instead of halfway up it, and a long page is unaffected.

   The tab bar is FIXED, so it covers whatever the bottom of the window holds.
   The clearance for it used to sit on `main`, which protected the content and
   left the footer underneath the bar - on a short page after this change, and
   on a long page scrolled to the end even before it. It belongs to the shell:
   `body` is `border-box`, so the padding comes out of the 100dvh rather than
   being added to it, and nothing gains a scrollbar it did not have.
   -------------------------------------------------------------------------- */
body > main{flex:1 1 auto;min-width:0}
body:has(.rh-tabbar){padding-bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom,0px))}
@media (min-width:720px){body:has(.rh-tabbar){padding-bottom:0}}

/* --------------------------------------------------------------------------
   [rh+] HEADER OVERFLOW AND TARGET SIZE — both found by the design round's own
   measurement, at 768px, which is the width just past the single breakpoint.

   At 768 the desktop navigation renders the five primary destinations, the
   eight secondary ones and three controls. They do not fit, and in an RTL
   document they do not merely clip: they run off the INLINE-START edge with a
   negative offset, which scrolls the whole page sideways. The probe caught it
   as `widest=620, scrolls=true` on seven surfaces at once.

   The fix is to let the navigation scroll inside itself rather than push the
   document, which is the same rule the product already applies to text columns
   (`min-width: 0`). No second breakpoint is introduced: at 1280 everything
   fits and nothing scrolls, and between 720 and about 1000 the secondary run
   slides under the thumb instead of disappearing. A link that is quiet is
   secondary; a link that is gone is unreachable.

   The links themselves were 20px tall, which is a target-size regression
   against a baseline that had none. They are 44px now, which is also why the
   header reads as navigation rather than as a paragraph of links.
   -------------------------------------------------------------------------- */
.rh-header{min-width:0}
.rh-header__nav{min-width:0;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:none;-ms-overflow-style:none;padding-block:var(--space-1)}
.rh-header__nav::-webkit-scrollbar{display:none}
.rh-header__link{display:inline-flex;align-items:center;min-height:var(--tap-min);white-space:nowrap;flex:none}
.rh-header__actions{flex:none}

/* --------------------------------------------------------------------------
   [rh+] RAIL LEGIBILITY OVER LIGHT MEDIA

   The system assumes photography. RecipeHub ships generated cover art - warm
   light gradients - and a white glyph with a drop shadow sits at roughly 2:1
   against it.

   The first attempt was a scrim down the thumb side of the card. It worked at
   390px and looked like a hard-edged grey rectangle at 1280, because a
   gradient that fades on one axis has a visible edge on the other. A contained
   backdrop behind each glyph is the better answer: it never touches the food,
   it has no edge to misjudge, and it reads as a control rather than as damage.
   -------------------------------------------------------------------------- */
.rh-rail:not(.rh-rail--horizontal) .rh-rail__btn{border-radius:var(--radius-control);background:rgba(20,16,13,.32);backdrop-filter:blur(6px);padding:var(--space-2) 2px;transition:background-color var(--speed) var(--ease-out),transform var(--speed) var(--ease-out)}
.rh-rail:not(.rh-rail--horizontal) .rh-rail__btn:hover{background:rgba(20,16,13,.5)}
.rh-rail:not(.rh-rail--horizontal) .rh-rail__btn[aria-pressed="true"]{background:rgba(20,16,13,.45)}

/* ==========================================================================
   [rh+] LEGACY BRIDGE

   The application's own screens still carry the class names they were written
   with - `.btn`, `.card`, `.alert`, `.badge`, `.field`. Rewriting twenty-four
   EJS files at once would move every screen on the same commit and make any
   regression unbisectable, so instead the legacy names are POINTED at the
   component vocabulary here.

   This is a bridge, not a home. A screen that has been migrated uses `rh-`
   classes directly; a screen that has not yet been migrated still renders in
   the Warm Table language rather than in the old one. The ledger tracks which
   legacy names are still live, and each disappears as its last screen moves.

   It is written as a set of `@extend`-style duplications rather than by adding
   `rh-` classes to the markup, because the markup is the part that has to be
   read and reviewed one screen at a time.
   ========================================================================== */

/* -- buttons: the five ranks, reached through the old names ---------------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);font-family:var(--font-core);font-weight:var(--weight-bold);font-size:var(--text-sm);line-height:1;min-height:var(--tap-min);padding:0 var(--space-5);border-radius:var(--radius-control);border:1px solid transparent;cursor:pointer;text-decoration:none;white-space:nowrap;background:var(--color-accent-soft);color:var(--color-accent-text);transition:background-color var(--speed) var(--ease-out),color var(--speed) var(--ease-out),border-color var(--speed) var(--ease-out),box-shadow var(--speed) var(--ease-out),transform var(--speed-fast) var(--ease-out)}
.btn:active:not(:disabled){transform:scale(var(--press-scale))}
.btn:disabled{opacity:.5;cursor:not-allowed;box-shadow:none}
.btn:hover:not(:disabled){background:var(--color-accent);color:var(--color-text-on-accent)}
.btn--primary{background:var(--color-accent);color:var(--color-text-on-accent);box-shadow:var(--shadow-accent)}
.btn--primary:hover:not(:disabled){background:var(--color-accent-hover);color:var(--color-text-on-accent)}
.btn--ghost{background:transparent;color:var(--color-text);border-color:var(--color-border)}
.btn--ghost:hover:not(:disabled){background:transparent;border-color:var(--color-accent);color:var(--color-accent-text)}
.btn--quiet{background:transparent;color:var(--color-text-muted);padding-inline:var(--space-3);min-height:var(--tap-min)}
.btn--quiet:hover:not(:disabled){background:transparent;color:var(--color-text)}
.btn--danger{background:var(--color-danger);color:#fff}
.btn--danger:hover:not(:disabled){background:var(--color-danger);color:#fff;filter:brightness(.9)}
.btn--sm{min-height:var(--tap-min);font-size:var(--text-xs);padding:0 var(--space-4);gap:var(--space-1)}

/*
 * TWO VARIANTS THE `.btn` PREFIX WAS MISSING, mirrored from `.rh-btn--onmedia`
 * and `.rh-btn--lg` in the package rather than invented.
 *
 * This product carries both prefixes: `.rh-btn` is the package's own component
 * and `.btn` is what the screens were written against. Every other modifier
 * exists in both; these two existed only under `rh-` because nothing outside
 * the design system had needed a large button or a button on a photograph -
 * until the landing became a photograph.
 *
 * `--lg` is 56px, above the 48px floor rather than at it: the two controls in a
 * cinematic hero are the only controls on the screen.
 */
.btn--onmedia{background:rgb(255 255 255 / .18);color:#fff;border-color:rgb(255 255 255 / .55);backdrop-filter:blur(8px)}
.btn--onmedia:hover:not(:disabled),
.btn--onmedia:focus-visible{background:#fff;color:var(--rh-ink-900)}
.btn--lg{min-height:56px;font-size:var(--text-base);padding:0 var(--space-6)}
.btn--block{width:100%}

/* -- one card, three densities -------------------------------------------- */
.card{background:var(--color-surface);border-radius:var(--radius-card);padding:var(--pad-card);border:1px solid transparent;box-shadow:var(--shadow-1)}
.card--compact{padding:0;overflow:hidden;padding-bottom:var(--space-3)}
.card--interactive{position:relative;transition:transform var(--speed) var(--ease-out),box-shadow var(--speed) var(--ease-out)}
.card--interactive:hover{transform:translateY(var(--hover-lift));box-shadow:var(--shadow-2)}

/*
 * THE WHOLE CARD IS THE TAP TARGET, and it is still exactly one link.
 *
 * A reviewer measured every card on /recipes at 320, 390 and 430: roughly
 * 90,000 square pixels styled `card--interactive`, with a hover lift and a
 * shadow that says "press me", and inside it a single 25px-tall title link.
 * A thumb aimed at the photograph did nothing, twelve times down the list,
 * with no feedback. `/explore` compact cards already wrap their image, which is
 * why the defect was invisible to anybody comparing the two screens.
 *
 * The stretched-link pattern rather than wrapping the card in an anchor, or
 * adding a second one over the image: both of those give a screen reader two
 * links to the same recipe, and an anchor wrapping headings and buttons is
 * invalid. This leaves the accessible name and the tab order exactly as they
 * are - one link, named by the title - and only changes where a pointer may
 * land.
 *
 * Anything else interactive inside the card has to sit ABOVE the overlay or it
 * becomes unclickable, which is the way this pattern usually ships broken.
 */
.card--interactive .card__title a::after{content:"";position:absolute;inset:0;z-index:0}
.card--interactive button,
.card--interactive a:not(.card__title a),
.card--interactive input,
.card--interactive label,
.card--interactive [role="button"]{position:relative;z-index:1}

/* -- one alert language ---------------------------------------------------- */
.alert{display:flex;align-items:flex-start;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-radius:var(--radius-input);border:1px solid transparent;font-size:var(--text-sm);background:var(--color-surface-sunken);color:var(--color-text)}
.alert--error{background:var(--color-danger-soft);border-color:var(--color-danger);color:var(--color-danger-text)}
.alert--success{background:var(--color-success-soft);border-color:var(--color-success);color:var(--color-success-text)}
.alert--warn,.alert--warning{background:var(--color-warning-soft);border-color:var(--color-warning);color:var(--color-warning-text)}

/* -- badges ---------------------------------------------------------------- */
.badge{display:inline-flex;align-items:center;gap:var(--space-1);padding:2px var(--space-3);border-radius:var(--radius-pill);font-size:var(--text-2xs);font-weight:var(--weight-bold);line-height:1.5;background:var(--color-surface-sunken);color:var(--color-text-muted);border:1px solid transparent}
.badge--warn{background:var(--color-warning-soft);color:var(--color-warning-text);border-color:var(--color-warning)}
.badge--ok,.badge--success{background:var(--color-success-soft);color:var(--color-success-text)}
.badge--danger{background:var(--color-danger-soft);color:var(--color-danger-text)}

/* -- form controls --------------------------------------------------------- */
/* `[type=file]` is excluded for the same reason as checkbox and radio: it is
   not a text box, and the field treatment - full width, tap-height, border,
   background - is wrong for it. The picker in `partials/filepick.ejs` reduces
   the native control to a clipped pixel, and this rule was giving it a 390x48
   box again, which is where 41px of horizontal overflow on `/me` came from. */
.field input:not([type=checkbox]):not([type=radio]):not([type=file]),.field select,.field textarea,
input[type=text],input[type=email],input[type=password],input[type=search],input[type=url],input[type=number],select,textarea{width:100%;min-height:var(--tap-min);padding:var(--space-3) var(--space-4);font:var(--type-body);font-family:var(--font-core);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-input);transition:border-color var(--speed) var(--ease-out),box-shadow var(--speed) var(--ease-out)}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--color-accent);box-shadow:var(--focus-ring)}
textarea{min-height:7rem;resize:vertical;line-height:var(--leading-normal)}

/* --------------------------------------------------------------------------
   [rh+] A PICTURE THAT IS NOT THERE

   Marked by `media.js` rather than deleted, so the card keeps its shape and the
   space the image reserved. The fill is the design system's own placeholder
   gradient, which is warm rather than grey - a grey hole on warm paper reads as
   damage, and a warm one reads as "loading".
   -------------------------------------------------------------------------- */
img[data-media-missing]{background:var(--media-placeholder);object-fit:cover;color:transparent;font-size:0}
img[data-media-missing]::after{content:"";display:block;width:100%;height:100%;background:var(--media-placeholder)}
.media-thumb{width:72px;height:72px;object-fit:cover;border-radius:var(--radius-sm);background:var(--media-placeholder)}

/* ==========================================================================
   [rh+] SHARED RECIPE — the guest door

   Eleven class names on this page had no CSS at all before the migration. It is
   the only surface a stranger reaches and the product's whole acquisition
   funnel, so it is treated as a first-class screen rather than as a fallback:
   the cover leads, the reading column is bounded, and the actions are visible
   and honest about needing an account.
   ========================================================================== */
.shared-recipe{max-width:var(--measure-read);margin-inline:auto;padding-bottom:var(--space-9)}
.shared-recipe .shared-banner{margin-bottom:var(--space-5);align-items:center}
.shared-recipe__frame{border-radius:var(--radius-media);overflow:hidden;box-shadow:var(--shadow-media);background:var(--color-surface-sunken);margin-bottom:var(--space-6)}
.shared-recipe__frame .rh-media{width:100%}
.shared-recipe__head{margin-bottom:var(--space-5)}
.shared-recipe__title{margin:0 0 var(--space-2);font:var(--type-title);text-wrap:pretty}
.shared-recipe .recipe-byline{margin:0 0 var(--space-3);color:var(--color-text-muted);font:var(--type-body-sm);display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2)}
.shared-recipe .recipe-description{margin:0;font:var(--type-body);color:var(--color-text);text-wrap:pretty}
.shared-recipe__body{margin-top:var(--space-7)}
.shared-recipe__body .rh-section__title{margin:var(--space-7) 0 var(--space-3)}
.shared-recipe__foot{margin-top:var(--space-7);padding-top:var(--space-5);border-top:1px solid var(--color-divider);color:var(--color-text-muted);font:var(--type-body-sm)}

/* Facts as a definition grid rather than a wall of rows: seven pairs read as a
   table on a phone and as a strip on a desktop, and neither needs a border. */
.recipe-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(8rem,1fr));gap:var(--space-3);margin:var(--space-5) 0;padding:var(--space-4);background:var(--color-surface);border-radius:var(--radius-card)}
.recipe-facts>div{min-width:0}
.recipe-facts dt{margin:0;font:var(--type-caption);color:var(--color-text-muted)}
.recipe-facts dd{margin:2px 0 0;font:var(--type-label);color:var(--color-text)}

/* [CSS3 5/5 companion] The ingredient list runs in columns where there is room
   - the same `column-count` the recipe page uses, applied to the guest copy so
   the two read alike. */
.ingredient-list{margin:0;padding-inline-start:var(--space-5);color:var(--color-text)}
.ingredient-list li{margin-bottom:var(--space-2);overflow-wrap:anywhere}
@media (min-width:720px){.ingredient-list{column-count:2;column-gap:var(--space-7)}.ingredient-list li{break-inside:avoid}}

.step-list{margin:0;padding-inline-start:var(--space-5);color:var(--color-text)}
.step-list li{margin-bottom:var(--space-4);line-height:var(--leading-relaxed);overflow-wrap:anywhere;text-wrap:pretty}

.shared-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-3);margin-top:var(--space-7);padding-top:var(--space-5);border-top:1px solid var(--color-divider)}
.rating-gate{display:flex;align-items:center;gap:var(--space-1)}
.rating-gate__label{font:var(--type-body-sm);color:var(--color-text-muted);margin-inline-end:var(--space-1)}
.rating-gate__star{display:inline-flex;align-items:center;justify-content:center;min-width:var(--tap-min);min-height:var(--tap-min);border:0;background:none;color:var(--color-text-muted);cursor:pointer;border-radius:var(--radius-control);transition:color var(--speed) var(--ease-out),background-color var(--speed) var(--ease-out)}
.rating-gate__star:hover{color:var(--color-secondary);background:var(--color-secondary-soft)}

/* --------------------------------------------------------------------------
   [rh+] RECIPE DETAIL — the cover leads

   A framed hero rather than a bare image: the same radius, shadow and ratio the
   feed card uses, so a recipe opened from the feed lands on something that
   looks like what was tapped.
   -------------------------------------------------------------------------- */
.recipe__frame{border-radius:var(--radius-media);overflow:hidden;box-shadow:var(--shadow-media);background:var(--color-surface-sunken);margin-bottom:var(--space-5)}
.recipe__frame .rh-media{width:100%;max-height:420px}
@media (min-width:720px){.recipe__frame .rh-media{max-height:480px}}
/* The reading column. Hebrew prose reads worse past about 72 characters, and a
   recipe's steps stretched across 1280px is the clearest sign a desktop layout
   was made by widening a phone one. */
.recipe{max-width:var(--measure-read);margin-inline:auto}

/* ==========================================================================
   [rh+] THE ADMIN ROOM

   Same brand, different job. The consumer product's media-first card language
   is deliberately absent here: a moderation queue that looks like a feed
   invites the wrong reading speed, and an operator scanning forty rows needs
   density and rules rather than space and shadow.

   The boundary is a sunken ground and rule-separated rows - not a different
   palette. Admin is still warm; it is simply not inviting.
   ========================================================================== */
[data-surface="admin"]{background:var(--color-surface-sunken)}
[data-surface="admin"] main{padding-block:var(--space-6) var(--space-9)}
[data-surface="admin"] .card{box-shadow:none;border:1px solid var(--color-border);border-radius:var(--radius-card);padding:var(--space-4)}
[data-surface="admin"] h1{font:var(--type-heading)}
[data-surface="admin"] h2{font:var(--type-subhead)}
[data-surface="admin"] .btn{min-height:40px;font-size:var(--text-xs);padding:0 var(--space-4)}
/* Destructive actions are the one thing that must not be quiet in a dense
   screen: an operator moving fast has to see which control is irreversible. */
[data-surface="admin"] .btn--danger{background:var(--color-danger);color:#fff;box-shadow:none}
[data-surface="admin"] .admin-kpi__value{font-size:var(--text-xl);font-weight:var(--weight-bold);font-variant-numeric:tabular-nums;line-height:1;margin:var(--space-1) 0 0}
[data-surface="admin"] .admin-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(9rem,1fr));gap:var(--space-3)}

/* ==========================================================================
   [rh+] UTILITIES — the small set that replaces repeated inline styles

   Deliberately a SHORT list. A utility layer that can express anything becomes
   inline styles with extra steps; this one covers the handful of things the
   product was writing into `style` attributes over and over, and nothing else.

   Every value is a token. A utility with a raw pixel in it does not belong
   here - that is a component that has not been named yet.
   ========================================================================== */
.u-flush{margin:0}
.u-mt-2{margin-top:var(--space-2)}
.u-mt-4{margin-top:var(--space-4)}
.u-mt-7{margin-top:var(--space-7)}
.u-mb-3{margin-bottom:var(--space-3)}
.u-mb-6{margin-bottom:var(--space-6)}
.u-full{width:100%}
.u-between{justify-content:space-between}
.u-center{text-align:center}
.u-grow{flex:1;min-width:0}
.u-grow-2{flex:2;min-width:0}
.u-grow-4{flex:4;min-width:0}
.u-w-num{min-width:90px;flex:0 0 auto}
.u-w-num-lg{min-width:110px;flex:0 0 auto}
.u-row-gap{margin-bottom:var(--space-3);align-items:center}

/* [CSS3 5/5] multiple-columns — a long list of short items reads far better in
   columns than in one narrow strip. Mobile-first: one column on a phone. */
.u-columns{column-count:1;column-gap:var(--space-7)}
@media (min-width:720px){.u-columns{column-count:3}}
.u-columns>*{break-inside:avoid}

/* A bordered panel. Was three copies of the same inline declaration. */
.u-panel{border:1px solid var(--color-border);border-radius:var(--radius-card);padding:var(--pad-card);margin:0 0 var(--space-5)}
.u-mb-4{margin-bottom:var(--space-4)}
.u-mt-3{margin-top:var(--space-3)}
.u-mb-1{margin-bottom:var(--space-1)}
.u-pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}
.u-tiny{font-size:var(--text-2xs)}
.u-thumb-wide{width:100%;height:150px;object-fit:cover;object-position:center 40%;border-radius:var(--radius-sm);margin-bottom:var(--space-3);background:var(--media-placeholder)}
.u-mt-5{margin-top:var(--space-5)}
.u-mb-5{margin-bottom:var(--space-5)}
.u-my-6{margin-block:var(--space-6)}
.u-wide{max-width:none}
.u-auto{margin-inline:auto}
.u-list-flush{padding-inline-start:var(--space-5);margin:0}
.u-fit-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(420px,100%),1fr));gap:var(--space-4)}
.u-avatar-slot{width:72px;height:72px}
.u-truncate{margin:var(--space-1) 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.u-tight{margin:2px 0 var(--space-3)}
.u-bare-list{list-style:none;padding:0;margin:0}
.u-display-num{font-size:var(--text-2xl);margin:0;font-variant-numeric:tabular-nums;line-height:1}
.u-col-flex{flex:1;min-width:min(260px,100%)}
.u-col-flex-sm{flex:1;min-width:min(220px,100%)}
.u-choice{display:block;width:100%;text-align:start;margin-bottom:var(--space-3);cursor:pointer}
.chat__composer{flex:1;min-width:0;min-height:var(--tap-min);padding:var(--space-3) var(--space-4);border:1px solid var(--color-border);border-radius:var(--radius-input);font:var(--type-body);font-family:var(--font-core);background:var(--color-surface);color:var(--color-text);unicode-bidi:plaintext}

/* ==========================================================================
   [rh+] TARGET SIZE — the rule this product actually follows

   A semantic audit grouped every interactive element by the component that
   produced it, rather than counting rendered instances. 42 unique
   component/width pairs; 20 were under 44px, and they are fixed HERE rather
   than screen by screen, because they were one component each and not forty
   findings each.

   The rule, stated honestly:

     Every control a person AIMS AT is at least 44px on its short side, and the
     design system's 48px where the component allows it.

     A link inside a sentence is exempt. Inline links take their height from the
     line they sit in; padding them to 44px would break the paragraph they are
     part of, and WCAG's own target-size criterion carves them out for exactly
     that reason. There are 181 of them and they are a deliberate exception, not
     an oversight.

   The token comment used to say "nothing interactive is smaller. Ever." That
   was not the rule being followed, and a comment that overstates the rule is
   worse than no comment - it makes the next reader trust a measurement nobody
   took.
   ========================================================================== */

/* The quiet rank was 40px: quiet is about VISUAL weight, not about being
   harder to hit. */
.rh-btn--quiet,
.btn--quiet{min-height:var(--tap-min)}

/* Follow, on a feed card over the media. */
.feed-card__follow{min-height:var(--tap-min);padding-inline:var(--space-4)}

/* The creator link on a feed card - a real destination, tapped often. */
.feed-card__creator-link{min-height:var(--tap-min)}

/* The wordmark is the way home. */
.rh-header__brand{display:inline-flex;align-items:center;min-height:var(--tap-min)}

/* Search: the filter toggle, the two search tabs, and the back link. */
.search-bar__filters,
.search-tabs__tab{min-height:var(--tap-min)}
.search-head__back{display:inline-flex;align-items:center;min-height:var(--tap-min)}

/* "הכול" on a shelf header. */
.shelf__more,
.rh-section__link{display:inline-flex;align-items:center;min-height:var(--tap-min)}

/*
 * A checkbox is 13px tall and cannot grow without becoming a different control,
 * so the LABEL carries the target. `.rh-check` already does this; these are the
 * bare ones the taxonomy pickers render.
 */
.field label:has(> input[type="checkbox"]),
.field label:has(> input[type="radio"]),
.check{display:flex;align-items:center;gap:var(--space-2);min-height:var(--tap-min)}

/* ==========================================================================
   [rh+] SCREEN PASSES

   A legacy bridge makes a screen render in the right palette. It does not make
   the screen DESIGNED. Each section below is a deliberate treatment for one
   surface, replacing "the bridge makes it approximately match".

   The shared vocabulary is used on purpose: the same section header, the same
   card, the same empty state, the same rhythm. World Class here means the
   transition from one route to the next is boring.
   ========================================================================== */

/* -- the page frame: one measure per kind of screen ------------------------ */
.page-consumer{max-width:var(--measure-page);margin-inline:auto}
.page-read{max-width:var(--measure-read);margin-inline:auto}
.page-feed{max-width:var(--measure-feed);margin-inline:auto}

/* A screen opens with its name and, where useful, a line saying what it is.
   Replaces the ad-hoc `h1 + .lead` pairing that drifted per screen. */
.screen-head{margin-bottom:var(--space-6)}
.screen-head__title{margin:0;font:var(--type-title)}
.screen-head__sub{margin:var(--space-2) 0 0;font:var(--type-body-sm);color:var(--color-text-muted);max-width:56ch;text-wrap:pretty}
.screen-head--row{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:var(--space-4)}

/* -- consumer collections: favourites, my recipes, search results ---------- */
/* A grid of food, not a table of records. Auto-fit so a phone gets one column
   and a desktop gets as many as the measure allows, with no media query. */
.collection{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:var(--space-5)}
.collection .card{padding:0;overflow:hidden;padding-bottom:var(--space-4);box-shadow:var(--shadow-1);border:0;transition:transform var(--speed) var(--ease-out),box-shadow var(--speed) var(--ease-out)}
.collection .card:hover{transform:translateY(var(--hover-lift));box-shadow:var(--shadow-2)}
.collection .card__media,.collection img{width:100%;aspect-ratio:var(--media-ratio-tile);object-fit:cover;object-position:center 40%;background:var(--media-placeholder);display:block}
.collection .card__title{margin:var(--space-3) var(--space-4) 2px;font:var(--type-label);font-size:var(--text-base);line-height:var(--leading-snug)}
.collection .card__title a{color:var(--color-text);text-decoration:none}
.collection .card__meta{margin:0 var(--space-4);font:var(--type-caption);color:var(--color-text-muted);font-variant-numeric:tabular-nums}
.collection .card__warn{margin:var(--space-2) var(--space-4) 0;display:inline-block;font-size:var(--text-2xs);font-weight:var(--weight-bold);color:var(--color-warning-text);background:var(--color-warning-soft);border:1px solid var(--color-warning);border-radius:var(--radius-xs);padding:2px var(--space-2)}
.collection .row,.collection .toolbar{padding:var(--space-3) var(--space-4) 0}

/* Lifecycle, on My Recipes. A draft and a published recipe must not look the
   same: the status is the point of that screen. */
.collection .badge{margin:var(--space-3) var(--space-4) 0}

/* -- people: the member directory and a profile --------------------------- */
.people{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(280px,100%),1fr));gap:var(--space-4)}
.person{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4);background:var(--color-surface);border-radius:var(--radius-card);box-shadow:var(--shadow-1);min-width:0}
.person__body{min-width:0;flex:1}
.person__name{margin:0;font:var(--type-label);font-size:var(--text-base);overflow-wrap:anywhere}
.person__handle{margin:0;font:var(--type-caption);color:var(--color-text-muted);direction:ltr;unicode-bidi:isolate;text-align:start}
.avatar{width:48px;height:48px;border-radius:var(--radius-avatar);object-fit:cover;flex:none;background:var(--color-accent-soft)}

/* A profile separates identity from settings. Public above, private below,
   with a rule between them - a member must never wonder which is which. */
.profile-head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-4);padding:var(--space-5);background:var(--color-surface);border-radius:var(--radius-card);box-shadow:var(--shadow-1);margin-bottom:var(--space-6)}
.profile-head .avatar{width:72px;height:72px}
.profile-private{margin-top:var(--space-8);padding-top:var(--space-6);border-top:1px solid var(--color-divider)}
.profile-private__note{margin:0 0 var(--space-4);font:var(--type-caption);color:var(--color-text-muted)}

/* -- comments and activity: a feed of small things, not a record list ------ */
.stream{display:flex;flex-direction:column;gap:var(--space-3)}
.stream .card{box-shadow:none;border:0;background:var(--color-surface);padding:var(--space-4)}
.stream .card + .card{border-top:1px solid var(--color-divider);border-start-start-radius:0;border-start-end-radius:0}

/* -- chat: a room, not a form inside a card ------------------------------- */
.chat{gap:var(--space-4)}
.chat__conversation{background:var(--color-surface);border:0;border-radius:var(--radius-card);box-shadow:var(--shadow-1);overflow:hidden}
.chat__thread{background:var(--color-bg);padding:var(--space-4)}
.chat__composer:focus{outline:none;border-color:var(--color-accent);box-shadow:var(--focus-ring)}

/* -- media lab and stats: utility surfaces, deliberately not consumer ones -- */
.utility-note{margin:0 0 var(--space-5);padding:var(--space-3) var(--space-4);background:var(--color-surface-accent);border-radius:var(--radius-input);color:var(--color-accent-text);font:var(--type-body-sm)}
.chart{background:var(--color-surface);border-radius:var(--radius-card);box-shadow:var(--shadow-1);padding:var(--space-5);margin-bottom:var(--space-6)}
.chart h2,.chart__title{margin:0 0 var(--space-1);font:var(--type-subhead)}
.chart__sub{margin:0 0 var(--space-4);font:var(--type-caption);color:var(--color-text-muted)}
.chart svg{max-width:100%;height:auto;display:block}

/* ==========================================================================
   [rh+] THE EDITOR — one writing flow, not a stack of unrelated cards

   The core authoring surface, and the one the completion gate says may not
   finish bridge-only. Nothing is removed, no payload changes, no field moves
   between sections: this is hierarchy, applied to the markup that is there.

   The ranking it makes visible, in order of how often it is wrong:

     1. WHERE TO START. On a phone the form opened with an import panel, an AI
        panel and then twenty fields, all in identical cards. The title field
        is the start, and it now reads as one.
     2. SAVE IS NOT PUBLISH. They had the same visual weight while their
        consequences differ - one keeps a private draft, the other makes a
        recipe public. Publish is the only filled control on the screen.
     3. THE ASSISTANTS ARE OPTIONAL. Import and AI are offered above the form
        and styled as offers, not as the first two steps of writing a recipe.
   ========================================================================== */
.page-editor{max-width:var(--measure-read);margin-inline:auto}

/* The two assistants: a tinted ground says "optional shortcut", and neither
   can be mistaken for a required stage of the form. */
.editor-assist{background:var(--color-surface-accent);border-radius:var(--radius-card);padding:var(--pad-card);margin-bottom:var(--space-5)}
.editor-assist h2{margin:0 0 var(--space-2);font:var(--type-subhead);color:var(--color-accent-text)}
.editor-assist .field__hint,.editor-assist p{color:var(--color-accent-text);opacity:.85}

/* The form itself is one sheet, not eleven cards. Sections are separated by
   ground and rhythm; a fieldset keeps its rule because it groups repeating
   rows and a person needs to see where the list ends. */
.form.card.u-wide{box-shadow:var(--shadow-1);border:0;padding:var(--pad-card)}
@media (min-width:720px){.form.card.u-wide{padding:var(--space-7)}}

.form fieldset.u-panel{border:1px solid var(--color-border);border-radius:var(--radius-card);padding:var(--pad-card);margin:0 0 var(--space-6)}
.form fieldset.u-panel > legend{padding-inline:var(--space-2);font:var(--type-label);color:var(--color-text)}

/* A field group that reads as a row on a desktop and stacks on a phone,
   without either needing its own rule. */
.form .toolbar{display:flex;flex-wrap:wrap;gap:var(--space-4);align-items:flex-start}
.form .toolbar > .field{flex:1 1 min(220px,100%);margin-bottom:0}
.field{margin-bottom:var(--space-5)}
.field > label{display:block;font:var(--type-label);margin-bottom:var(--space-2)}
.field__hint{display:block;margin-top:var(--space-2);font:var(--type-caption);color:var(--color-text-muted)}
.field__error{display:block;margin-top:var(--space-2);font:var(--type-caption);font-weight:var(--weight-bold);color:var(--color-danger-text)}

/* --------------------------------------------------------------------------
   [rh+] FILE PICKER — see `views/partials/filepick.ejs` for why the native
   control is hidden rather than restyled: its button text and its direction
   belong to the browser, and both are wrong in a Hebrew form.

   The input keeps its own focus so the keyboard still reaches it; the ring is
   drawn on the label, because that is the thing a sighted keyboard user can
   see. `:focus-visible` and not `:focus`, so a mouse click does not ring it.
   -------------------------------------------------------------------------- */
.filepick{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap}
.filepick__input{position:absolute;width:1px;height:1px;padding:0;margin:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.filepick__action{cursor:pointer}
.filepick__input:focus-visible + .filepick__action{outline:2px solid var(--color-focus-ring);outline-offset:2px}
.filepick__input:disabled + .filepick__action{opacity:.5;cursor:not-allowed}
.filepick__name{font:var(--type-caption);color:var(--color-text-muted);min-width:0;overflow-wrap:anywhere}
.filepick__name[data-chosen]{color:var(--color-text)}

/* The action row. Sticky at the bottom on a phone, because a form this long
   otherwise hides its own primary action below twenty fields. */
.editor-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-3);margin-top:var(--space-6);padding-top:var(--space-5);border-top:1px solid var(--color-divider)}
.editor-actions .btn--primary{order:-1}
@media (max-width:719px){
  .editor-actions{position:sticky;bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom,0px));background:var(--color-surface);margin-inline:calc(-1 * var(--pad-card));padding:var(--space-3) var(--pad-card);border-top:1px solid var(--color-border);box-shadow:var(--shadow-nav);z-index:5}
}

/* Visibility is a consequence, not a field like any other. */
[data-visibility-control]{background:var(--color-surface-sunken);border-radius:var(--radius-card);padding:var(--pad-card);margin-top:var(--space-6)}
[data-visibility-control] > label{font:var(--type-label)}

/* --------------------------------------------------------------------------
   [rh+] ADMIN DENSITY — R7 finding

   The admin lists rendered as tall white cards with a lot of air: correct
   palette, wrong reading speed. An operator scanning forty reports needs rows
   that stack, not cards that breathe. Screenshot evidence at 1280 - every
   metric was green while a moderation queue looked like a feed.
   -------------------------------------------------------------------------- */
.admin-list{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-card);overflow:hidden}
.admin-list > .card,
.admin-list > article{padding:var(--space-3) var(--space-4);margin:0;border:0;border-radius:0;border-bottom:1px solid var(--color-divider);box-shadow:none;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--space-3)}
.admin-list > :last-child{border-bottom:0}
.admin-list p{margin:0}
.admin-list .row,
.admin-list .toolbar{flex:0 0 auto;margin:0}
.admin-list .empty{padding:var(--space-6);text-align:center;color:var(--color-text-muted)}
/* The KPI strip reads as figures, not as four more cards. */
[data-surface="admin"] .admin-kpis .card{display:block;padding:var(--space-4)}

/* ==========================================================================
   [C1] DESKTOP FRAME SYSTEM

   Three composition concepts, not a width hack per screen. Every page picks
   one; nothing sets its own max-width.

     FRAME FEED   a social column with room around it
     FRAME READ   a reading/working measure, centred
     FRAME SPLIT  a list pane beside a detail pane

   Below 720px all three collapse to a single column, because a phone has one
   composition and it is the same one.
   ========================================================================== */

/* -- FRAME READ: recipe detail, the editor, informational surfaces --------- */
.page-read,
.page-editor{max-width:var(--measure-read);margin-inline:auto;width:100%}

/* -- FRAME AUTH: a deliberately compact card, CENTRED in the frame ---------
   Product clarification B: a compact login form is good; a compact login form
   pinned to one side of a 1280px viewport is not. */
.page-auth{max-width:var(--measure-read);margin-inline:auto;width:100%;display:flex;flex-direction:column;align-items:center}
.page-auth > *{width:100%;max-width:460px}
.page-auth .screen-head{text-align:center}

/* The form fills whatever frame it was given... */
.form{width:100%;max-width:none}
/* ...except inside the auth frame, which deliberately keeps a compact card.
   Written after `.form` on purpose: same specificity, and the later rule is the
   more specific INTENT. Stacking `!important` here would be the specificity
   hack the directive rules out. */
.page-auth > .form,
.page-auth > .card{max-width:460px;margin-inline:auto}

/* -- FRAME FEED: the social column, with intentional space around it ------- */
.page-feed{max-width:var(--measure-feed);margin-inline:auto;width:100%}
@media (min-width:1100px){
  /*
   * At real desktop width the feed becomes a two-column composition: the
   * social column keeps its 620px measure, and a context aside sits beside it.
   *
   * The aside carries ONLY things this member already has - the personalisation
   * state the feed already computes, and navigation that already exists. No new
   * query, no new endpoint, no invented content. Product clarification A.
   */
  .feed-layout{display:grid;grid-template-columns:minmax(0,var(--measure-feed)) 300px;gap:var(--space-8);justify-content:center;align-items:start}
  .feed-aside{position:sticky;top:calc(var(--header-h-wide) + var(--space-5));display:flex;flex-direction:column;gap:var(--space-4)}
}
.feed-aside{display:none}
@media (min-width:1100px){.feed-aside{display:flex}}
.feed-aside__card{background:var(--color-surface);border-radius:var(--radius-card);padding:var(--pad-card);box-shadow:var(--shadow-1)}
.feed-aside__title{margin:0 0 var(--space-2);font:var(--type-label)}
.feed-aside__body{margin:0;font:var(--type-body-sm);color:var(--color-text-muted);text-wrap:pretty}
.feed-aside__links{list-style:none;margin:var(--space-3) 0 0;padding:0;display:flex;flex-direction:column;gap:var(--space-1)}
.feed-aside__links a{display:flex;align-items:center;gap:var(--space-2);min-height:var(--tap-min);color:var(--color-text);text-decoration:none;font:var(--type-body-sm)}
.feed-aside__links a:hover{color:var(--color-accent-text)}

/* -- FRAME SPLIT: chat, and admin where it suits -------------------------- */
@media (min-width:720px){
  .page-split{display:grid;grid-template-columns:minmax(260px,300px) minmax(0,1fr);gap:var(--space-5);align-items:start}
}

/* ==========================================================================
   [C8] RECIPE DETAIL — reading content, not four floating databases

   Screenshot evidence at 1280 confirmed the review's concern: ingredients,
   pricing, steps and the comment form each sat in their own shadowed white
   card, stacked. Four independent boxes, each over-framed, and the two that
   matter most - what to buy and what to do - read as records rather than as
   something you cook from.

   Nothing is removed. The information rhythm is:

     cover · identity · social actions · allergen state · description ·
     ingredients · steps · pricing · rating · owner controls · gallery ·
     comments

   Ingredients and steps become reading content on the page ground, separated by
   space and a hairline rather than by a card each. PRICING KEEPS ITS FRAME -
   it carries distinct information semantics (an estimate, a range, a basis) and
   framing is what says "this is a different kind of claim".
   ========================================================================== */
.recipe-part{margin:var(--space-8) 0 0;padding-top:var(--space-6);border-top:1px solid var(--color-divider)}
.recipe-part:first-of-type{border-top:0;padding-top:0}
.recipe-part__title{margin:0 0 var(--space-4);font:var(--type-heading)}

/* The two a person actually cooks from get reading measure and rhythm. */
.recipe-part--cook{font-size:var(--text-base);line-height:var(--leading-relaxed)}
.recipe-part--cook ol,
.recipe-part--cook ul{margin:0;padding-inline-start:var(--space-5)}
.recipe-part--cook li{margin-bottom:var(--space-3);overflow-wrap:anywhere;text-wrap:pretty}
.recipe-part--cook li::marker{color:var(--color-accent-text);font-weight:var(--weight-bold)}

/* Action families stay apart: social, then rating, then owner/destructive.
   Ten unrelated controls in one strip is not a hierarchy. */
.recipe .action-rail{margin-bottom:var(--space-3)}
.recipe__rating{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);margin:var(--space-3) 0}
.recipe__owner-actions{display:flex;flex-wrap:wrap;gap:var(--space-3);margin-top:var(--space-6);padding-top:var(--space-5);border-top:1px solid var(--color-divider)}

/* The comment composer belongs to the conversation, not to its own card. */
.recipe .comment-form,
.recipe #comment-form{box-shadow:none;border:1px solid var(--color-border);background:var(--color-surface)}

/* ==========================================================================
   [C6] ADMIN TABS + [C7] ONE RATING LANGUAGE
   ========================================================================== */

/* C6. The tabs were five identical ghost buttons and the selected one differed
   by colour alone - which is the one thing a state may never rest on. Selected
   now carries SHAPE (an underline), WEIGHT and colour together. */
.admin-tab{appearance:none;background:none;border:0;border-bottom:2px solid transparent;border-radius:0;padding:var(--space-3) var(--space-2);min-height:var(--tap-min);font-family:var(--font-core);font-size:var(--text-sm);font-weight:var(--weight-regular);color:var(--color-text-muted);cursor:pointer;white-space:nowrap;transition:color var(--speed) var(--ease-out),border-color var(--speed) var(--ease-out)}
.admin-tab:hover{color:var(--color-text)}
.admin-tab[aria-selected="true"]{color:var(--color-accent-text);font-weight:var(--weight-bold);border-bottom-color:var(--color-accent)}
[data-surface="admin"] .toolbar:has(> .admin-tab){gap:var(--space-5);border-bottom:1px solid var(--color-border);margin-bottom:var(--space-5);overflow-x:auto;scrollbar-width:none}

/* C6. The KPI wall on a phone: four full-width tiles is four screens of
   scrolling before the queue. Two columns is the same information in a quarter
   of the height. */
@media (max-width:719px){
  [data-surface="admin"] .admin-kpis{grid-template-columns:1fr 1fr;gap:var(--space-2)}
  [data-surface="admin"] .admin-kpis .card{padding:var(--space-3)}
  [data-surface="admin"] .admin-kpi__value{font-size:var(--text-lg)}
}

/* C7. ONE rating vocabulary, member and guest.
   Same five stars, same 48px targets, same fillable icon, same aria-pressed.
   The values, the API and the aggregate are untouched - this is the control's
   appearance, not its meaning. */
.rating{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-1)}
.rating__label{font:var(--type-body-sm);color:var(--color-text-muted);margin-inline-end:var(--space-2)}
.rating__star{display:inline-flex;align-items:center;justify-content:center;min-width:var(--tap-min);min-height:var(--tap-min);border:0;background:none;color:var(--color-text-muted);cursor:pointer;border-radius:var(--radius-control);transition:color var(--speed) var(--ease-out),background-color var(--speed) var(--ease-out),transform var(--speed-fast) var(--ease-out)}
.rating__star:hover{color:var(--color-secondary);background:var(--color-secondary-soft)}
.rating__star:active{transform:scale(var(--press-scale))}
.rating__star[aria-pressed="true"]{color:var(--color-secondary)}
.rating__star[aria-pressed="true"] .rh-icon{fill:currentColor}
/* "ביטול" stays quiet: it undoes, it does not compete. */
.rating__clear{background:none;border:0;color:var(--color-text-muted);font:var(--type-caption);min-height:var(--tap-min);padding-inline:var(--space-3);cursor:pointer}
.rating__clear:hover{color:var(--color-text)}

/* The guest rating gate is the same control; only the outcome differs. */
.rating-gate{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-1)}
.rating-gate__star{min-width:var(--tap-min);min-height:var(--tap-min)}

/* [C12] DESKTOP HEADER RANKING — rank by weight and colour, not by size alone. */
.rh-header__link{font-size:var(--text-sm);color:var(--color-text-muted);font-weight:var(--weight-regular)}
.rh-header__link--primary{font-size:var(--text-base);font-weight:var(--weight-bold);color:var(--color-text)}
.rh-header__link--primary[aria-current="page"]{color:var(--color-accent-text)}
@media (min-width:720px){
  .rh-header__nav{gap:var(--space-3)}
  .rh-header__link--primary + .rh-header__link--primary{margin-inline-start:var(--space-1)}
}

/* ==========================================================================
   [C4] CHAT — a room, with a list pane and a thread

   Desktop is a real split: a conversation pane beside a thread that fills the
   useful height and scrolls INTERNALLY, with the composer belonging to the
   thread rather than floating after it. Mobile keeps one column, and the thread
   is the taller half.

   Every behaviour is untouched: the socket, persist-before-ack, unread,
   history, reconnect, authorization, and `unicode-bidi: plaintext` on bubbles
   and their paragraphs.
   ========================================================================== */
.chat-status{display:inline-flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-4);padding:var(--space-1) var(--space-3);border-radius:var(--radius-control);background:var(--color-surface-sunken);color:var(--color-text-muted);font:var(--type-caption)}
.chat-status::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--color-text-muted)}
.chat-status[data-state="open"]{background:var(--color-success-soft);color:var(--color-success-text)}
.chat-status[data-state="open"]::before{background:var(--color-success)}
.chat-status[data-state="error"]{background:var(--color-danger-soft);color:var(--color-danger-text)}
.chat-status[data-state="error"]::before{background:var(--color-danger)}

.chat__pane{background:var(--color-surface);border-radius:var(--radius-card);box-shadow:var(--shadow-1);padding:var(--space-4);min-width:0}
.chat__pane-title{margin:0 0 var(--space-3);font:var(--type-label);color:var(--color-text-muted)}
.chat__conversation{display:flex;flex-direction:column;min-width:0}
.chat__thread-title{margin:0;padding:var(--space-4);border-bottom:1px solid var(--color-divider);font:var(--type-subhead)}
.chat__thread{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:var(--space-2);min-height:14rem}
.chat #chat-form{margin:0;padding:var(--space-3) var(--space-4);border-top:1px solid var(--color-divider);gap:var(--space-2);background:var(--color-surface)}

/* A conversation in the list. The selected one is visibly selected - shape and
   ground, not colour alone. */
#conversation-list a,
#conversation-list button,
#conversation-list [data-conversation]{display:flex;align-items:center;gap:var(--space-3);width:100%;min-height:56px;padding:var(--space-2) var(--space-3);border:0;border-radius:var(--radius-input);background:none;color:var(--color-text);text-align:start;text-decoration:none;font-family:var(--font-core);cursor:pointer;min-width:0}
#conversation-list a:hover,
#conversation-list [data-conversation]:hover{background:var(--color-surface-sunken)}
#conversation-list [aria-current="true"],
#conversation-list [aria-selected="true"],
#conversation-list .is-active{background:var(--color-accent-soft);color:var(--color-accent-text);font-weight:var(--weight-bold)}

@media (min-width:720px){
  .chat{align-items:stretch}
  .chat__pane{max-height:calc(100vh - var(--header-h-wide) - var(--space-10));overflow-y:auto}
  .chat__conversation{height:calc(100vh - var(--header-h-wide) - var(--space-10))}
}
@media (max-width:719px){
  .chat__conversation{min-height:60vh}
  .chat__thread{max-height:52vh}
}

/* ==========================================================================
   [C5] LANDING — a front door, not documentation
   ========================================================================== */
.landing{display:grid;gap:var(--space-6);align-items:center}
.landing__title{margin:0 0 var(--space-3);font:var(--type-title);text-wrap:balance}
.landing__lead{margin:0;font-size:var(--text-md);line-height:var(--leading-normal);color:var(--color-text);max-width:46ch;text-wrap:pretty}
.landing__actions{margin-top:var(--space-6)}
/* Texture, not content. Not clickable, not a route into protected recipes, and
   deliberately abstract until approved photography lands (docs/43). */
/* THE HERO DIAGRAM. WP-17.5 pass 4, product brief section 16.

   Replaced four empty `.landing__tile` squares, which rendered as blank pastel
   blocks at the exact spot on a food product's front door where a visitor looks
   for food - and read, correctly, as an image grid that had failed to load.

   This draws the transformation instead: a pasted link, an arrow that carries
   the product's name, and the structured recipe that comes out. It is a DIAGRAM
   in the product's own type and palette, not a mocked-up screenshot - the bars
   are schematic, and there is no invented photograph, creator or rating on it. */
.landing__art{display:grid;gap:var(--space-3);align-content:center}

.landing__in{padding:var(--space-4);border:1px dashed var(--color-border-strong);border-radius:var(--radius-card);background:var(--color-surface)}
.landing__in-label{display:block;font:var(--type-caption);color:var(--color-text-muted)}
.landing__in-url{display:block;margin-top:var(--space-1);font:var(--type-body-sm);color:var(--color-text);direction:ltr;unicode-bidi:isolate;text-align:start;overflow-wrap:anywhere}

.landing__arrow{position:relative;display:grid;place-items:center;height:var(--space-8)}
.landing__arrow-line{position:absolute;inset-inline:40%;top:50%;height:2px;background:var(--color-border-strong)}
/* The line runs vertically in the stacked layout and horizontally beside the
   card at width - drawn with the same two elements either way. */
.landing__arrow-line{inset-inline:auto;inset-block:0;width:2px;height:100%;top:auto}
.landing__arrow-badge{position:relative;padding:var(--space-1) var(--space-4);border-radius:var(--radius-pill);background:var(--color-accent);color:var(--color-text-on-accent);font:var(--type-label);box-shadow:var(--shadow-accent)}

.landing__out{padding:var(--space-5);border-radius:var(--radius-card);background:var(--color-surface);box-shadow:var(--shadow-2)}
.landing__out-title{display:block;margin-bottom:var(--space-3);font:var(--type-label);color:var(--color-text-strong)}
.landing__out-rows{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:var(--space-3)}
.landing__out-rows li{display:grid;grid-template-columns:5.5rem 1fr;align-items:center;gap:var(--space-3)}
.landing__out-key{font:var(--type-caption);color:var(--color-text-muted)}
.landing__out-bar{display:block;height:8px;width:var(--w,100%);border-radius:var(--radius-pill);background:linear-gradient(90deg,var(--rh-clay-300),var(--rh-clay-200))}
.landing__out-bar--check{width:56px;background:var(--color-warning-soft);box-shadow:inset 0 0 0 1px var(--rh-amber-500)}
.landing__out-credit{display:block;margin-top:var(--space-4);padding-top:var(--space-3);border-top:1px solid var(--color-divider);font:var(--type-caption);color:var(--color-text-muted)}
@media (min-width:720px){
  .landing{grid-template-columns:1.1fr .9fr;gap:var(--space-9)}
  .landing__title{font:var(--type-display);font-size:var(--text-2xl)}
  /* The diagram stays a single column at every width. It is a SEQUENCE - input,
     transformation, output - and a sequence laid out in two columns is no longer
     one. What changes at width is the room it gets, not its direction. */
  .landing__art{grid-template-columns:1fr}
}

/* --------------------------------------------------------------------------
   [polish A] FEED RAIL WEIGHT

   The capsules read heavier than the food. Lighter backdrop, tighter padding,
   and the not-interested control stays quieter than the rest - it is a
   correction, not an expression. The target size is unchanged at 48px and the
   contrast stays above the accessible floor, because neither is what was
   heavy.
   -------------------------------------------------------------------------- */
.rh-rail:not(.rh-rail--horizontal){gap:var(--space-3)}
.rh-rail:not(.rh-rail--horizontal) .rh-rail__btn{background:rgba(20,16,13,.24);padding:var(--space-1) 2px;gap:1px}
.rh-rail:not(.rh-rail--horizontal) .rh-rail__btn:hover{background:rgba(20,16,13,.42)}
.rh-rail:not(.rh-rail--horizontal) .rh-rail__btn[aria-pressed="true"]{background:rgba(20,16,13,.38)}
.rh-rail:not(.rh-rail--horizontal) .rh-rail__btn--quiet{opacity:.6;background:rgba(20,16,13,.18)}
.rh-rail:not(.rh-rail--horizontal) .rail__count{font-size:var(--text-2xs);line-height:1.2}
/* The conversation row: one full-width choice, name over last message. */
.chat__item{display:flex;align-items:center;gap:var(--space-3);width:100%;min-height:56px;padding:var(--space-2) var(--space-3);border:0;border-radius:var(--radius-input);background:none;color:var(--color-text);text-align:start;font-family:var(--font-core);cursor:pointer;min-width:0}
.chat__item:hover{background:var(--color-surface-sunken)}
.chat__item[aria-current="true"],.chat__item.is-active{background:var(--color-accent-soft);color:var(--color-accent-text)}
.chat__item-body{display:flex;flex-direction:column;min-width:0;flex:1;gap:2px}
/* The name and its count on one line, at a fixed gap. `min-width: 0` on the
   name so a long display name ellipses instead of pushing the count away. */
.chat__item-head{display:flex;align-items:center;gap:var(--space-2);min-width:0}
.chat__item-head .chat__item-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chat__item-head .rh-navbadge{flex:none;margin-inline-start:0}
.chat__item-name{font:var(--type-label);overflow-wrap:anywhere}
.chat__item-last{font:var(--type-caption);color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;unicode-bidi:plaintext;text-align:start}
.chat__item[aria-current="true"] .chat__item-last{color:inherit;opacity:.8}
.chat__empty{padding:var(--space-8) var(--space-5)}
/* An empty thread draws nothing. The bordered box that remained when no
   conversation was chosen was the container itself, sitting above the empty
   state and reading as the broken part of the screen. */
.chat__thread:empty{display:none}
/* And the heading goes with it: the empty state already says בחרו שיחה, and
   printing it twice is the duplication C10 exists to remove. */
.chat__conversation:has(.chat__thread:empty) .chat__thread-title{display:none}
