/* ==========================================================================
   CiL Coffee & CiL & Co — design system
   Warm modern editorial: cream, espresso brown, charcoal, muted terracotta,
   olive accent. Serif headings, clean sans body.
   ========================================================================== */

/* --- tokens --------------------------------------------------------------- */
:root {
  --cream:        #F8F4ED;
  --cream-deep:   #EFE8DC;
  --sand:         #E1D6C4;
  --espresso:     #2E2620;
  --espresso-soft:#4A3F36;
  --charcoal:     #221F1B;
  /* Accents, dimmed just enough to carry text.
     White on #B4603C measured 4.49:1 and #B4603C on cream measured 4.09:1 --
     both just under the 4.5:1 that AA asks for, on the buttons and eyebrows that
     use them. These values clear it in both directions (white-on 5.50:1,
     on-cream 5.02:1) while staying the same terracotta. Likewise olive:
     3.85:1 -> 5.01:1 on cream. */
  --terra:        #A25331;
  --terra-soft:   #C97F5E;
  --olive:        #666D40;
  --ink-70:       rgba(46, 38, 32, .70);
  /* .45 measured 2.63:1 on cream, well under AA, and it is used for small
     supporting text (the hours' day labels, captions). .67 clears 4.5:1 on both
     cream surfaces. */
  --ink-45:       rgba(46, 38, 32, .67);
  --ink-12:       rgba(46, 38, 32, .12);
  /* A step stronger than --ink-12, for controls that have to read as pressable.
     As a thin hairline the category pills looked like disabled buttons. */
  --ink-24:       rgba(46, 38, 32, .24);

  /* Headings are sans now — no serif anywhere. One family, two weights: 500
     for headings (Jost 600 is heavier than the design needs) and 300 for
     body, which is what gave the type its airiness before. */
  --font-display: var(--font-body);
  --font-body:    "Jost", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  --wrap: 1180px;
  --wrap-narrow: 780px;
  --radius: 4px;
  --radius-lg: 10px;

  --space-1: .5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2.5rem;
  --space-5: 4rem;
  --space-6: 6.5rem;

  --ease: cubic-bezier(.22, .61, .36, 1);
  /* The plate carousel needs a different curve from the rest of the site.
     --ease is an ease-OUT: it starts at full speed and coasts. That is right
     for a hover fade, but on a carousel step the row and the plate both jump
     to full speed in the very first frame and then do all their travelling in
     the middle, which reads as jumpy. Measured on the row over one step:
       --ease            vFirst 0.689px/ms  peak/median 4.01  uniform 12%
       --ease-carousel   vFirst 0.095px/ms  peak/median 1.03  uniform 80%+
     Symmetrical, with almost no overshoot, so the move starts and ends gently
     and holds a near-constant speed in between. */
  --ease-carousel: cubic-bezier(.12, .02, .88, .98);
  /* The active plate's width, in one place on purpose: the row reserves this as
     its min-height (see .plates__row) so the section never changes height, and
     the number must stay identical to .plates__item.is-active's width or the two
     drift apart. */
  --plate-active: clamp(228px, 32vw, 424px);
}

/* --- reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The header is sticky, so anything scrolled to must clear it or the top of
     the target sits underneath the header. This applies to #hash navigation,
     which the browser resolves on its own and no script can correct.
     5.5rem (88px) against a header of 84px on desktop and 77px on a phone, so
     there is a little air under the header rather than the content butting
     right up against it. */
  scroll-padding-top: 5.5rem;
}
body {
  margin: 0;
  background: var(--cream);
  color: var(--espresso);
  font-family: var(--font-body);
  font-size: 16.5px;
  font-weight: 300;
  line-height: 1.72;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img, svg { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }
ul, ol { margin: 0; padding: 0; }
figure { margin: 0; }
h1, h2, h3, h4 { margin: 0; font-weight: 400; line-height: 1.14; }
/* Balance heading line lengths instead of leaving a one-word last line.
   "Apa yang orang selalu pesan" broke as 463px + 122px -- "pesan" alone on the
   second line -- which reads as a stray word with a hole beside it. balance
   splits it 332/254 at the same two lines and the same height, so nothing moves
   on the page; it only stops a large heading from ending in a widow.
   pretty is not used: it fixed this only below 1024 and left the widow above it.
   Not applied to h4, which is small text where the last line is rarely alone. */
h1, h2, h3 { text-wrap: balance; }

/* Headings that carry only the eyebrow's look.
   "Menu Pilihan" above the carousel is a real section heading, so it is an <h2>
   and appears in the document outline, but visually it stays a small uppercase
   label: the pictures are the heading's subject, not a paragraph under it.
   Without this reset the h2 would inherit the 1.14 line-height meant for large
   display type and sit visibly tighter than the other eyebrows. */
.eyebrow.h2, h2.eyebrow, h3.eyebrow, h4.eyebrow { line-height: 1.5; }
p { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }

/* --- typography ----------------------------------------------------------- */
.display {
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 6.2vw, 5rem);
  font-weight: 500;
  letter-spacing: -.015em;
}
.h2 {
  font-family: var(--font-display);
  font-size: clamp(2rem, 4vw, 3rem);
  font-weight: 500;
  letter-spacing: -.01em;
}
.h3 {
  font-family: var(--font-display);
  font-size: clamp(1.45rem, 2.4vw, 1.95rem);
}
.h4 {
  font-family: var(--font-body);
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .2em;
  text-transform: uppercase;
}
.lede {
  font-size: clamp(1.05rem, 1.6vw, 1.28rem);
  line-height: 1.66;
  color: var(--ink-70);
  font-weight: 300;
}
.small { font-size: .82rem; line-height: 1.6; color: var(--ink-45); }
.eyebrow {
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--terra);
  margin: 0 0 1rem;
}
.eyebrow--light { color: var(--sand); }
/* The carousel and the section below it are both "the menu" -- one is the
   pictures, the other is what people order. This label sits over the carousel
   so the two do not read as the same heading twice.
   Its own margin-bottom is removed: the gap down to the plates is set by .plates
   instead, and the base .eyebrow's 1rem was a third contributor (with .plates
   padding and the image air) that pushed the label away from the plates. */
.eyebrow--carousel { display: block; text-align: center; color: var(--olive); margin-bottom: 0; }
.italic { font-style: italic; }

/* --- layout --------------------------------------------------------------- */
.wrap { width: min(100% - 2.6rem, var(--wrap)); margin-inline: auto; }
.wrap-narrow { width: min(100% - 2.6rem, var(--wrap-narrow)); margin-inline: auto; }
.section { padding: var(--space-6) 0; }
.section--tight { padding: var(--space-5) 0; }
.section--cream { background: var(--cream-deep); }
.section--dark { background: var(--charcoal); color: var(--cream); }
.section--dark .lede, .section--dark .small { color: rgba(248, 244, 237, .68); }
.rule { height: 1px; background: var(--ink-12); border: 0; margin: 0; }

.grid { display: grid; gap: var(--space-3); }
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }
/* "As many as fit", for card runs whose length can change.
   The four "what people order" cards were a fixed four columns, which forced
   them to a single column on a tablet and made that section 2.6 screens tall on
   a phone. auto-fit keeps two or more in view at every width and needs no
   breakpoint at all: min(100%, 240px) lets the track collapse on a narrow
   screen instead of overflowing it. */
.grid--auto { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
.grid--gap-lg { gap: var(--space-5); }
.split { display: grid; grid-template-columns: 1fr 1fr; align-items: center; gap: var(--space-5); }

/* --- section head --------------------------------------------------------
   Eyebrow + heading on the left, supporting note and its link alongside on the
   right. The note used to sit under the heading as a lede, which made the head
   tall: heading, three lines of note, then a link below all of it, with the
   space to the right of the heading empty at desktop. Putting the note beside
   the heading closes that gap and keeps the cards higher on the screen.

   It stacks below 1000px (see the media query), so the note drops under the
   heading rather than being squeezed into a sliver. */
.section-head {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  align-items: end;
  gap: 1.5rem 3rem;
  margin-bottom: 2.6rem;
}
.section-head__title h2 { max-width: 22ch; }
.section-head__aside {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 1.4rem; padding-bottom: .4rem;
}
.section-head__note {
  max-width: 44ch;
  font-size: .92rem; line-height: 1.7;
  color: var(--ink-70); font-weight: 300;
}

/* --- buttons -------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  padding: .82rem 1.6rem;
  border: 1px solid var(--espresso);
  border-radius: var(--radius);
  background: transparent;
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .13em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease);
}
.btn:hover { background: var(--espresso); color: var(--cream); }
.btn--solid { background: var(--espresso); color: var(--cream); }
.btn--solid:hover { background: var(--terra); border-color: var(--terra); }
.btn--light { border-color: rgba(248, 244, 237, .5); color: var(--cream); }
.btn--light:hover { background: var(--cream); color: var(--espresso); border-color: var(--cream); }
.btn--terra { background: var(--terra); border-color: var(--terra); color: #fff; }
.btn--terra:hover { background: var(--espresso); border-color: var(--espresso); }
.btn-row { display: flex; flex-wrap: wrap; gap: .8rem; }
.btn svg { width: 15px; height: 15px; flex: none; }

/* The header's single affordance, at every width. It points at Directions, not
   WhatsApp: the cafe serves walk-ins far more than it takes orders by message,
   so the slot seen on every page is the one that gets people here. The class
   name is kept because what it styles is "the header's icon button that
   collapses to icon-only" — below 480px the label would crowd the wordmark, so
   it drops to the icon and the aria-label carries the meaning for screen
   readers. */
.header__wa { gap: .5rem; padding-inline: 1.25rem; }
/* The pin icon is drawn in outline (stroke), unlike the wordmark and the solid
   button it sits in, so at the button's scale it needs a touch more weight to
   read as deliberately drawn rather than faded. */
.header__wa svg { stroke-width: 1.9; }

.link-arrow {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: .78rem; font-weight: 500; letter-spacing: .13em; text-transform: uppercase;
  border-bottom: 1px solid var(--ink-12); padding-bottom: .35rem;
  transition: border-color .3s var(--ease), gap .3s var(--ease);
}
.link-arrow:hover { border-color: var(--terra); gap: .85rem; }

/* --- topbar --------------------------------------------------------------- */
.topbar {
  background: var(--charcoal);
  color: rgba(248, 244, 237, .82);
  font-size: .74rem;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.topbar .wrap { display: flex; justify-content: space-between; gap: 1rem; padding: .6rem 0; }
.topbar__item { display: flex; align-items: center; gap: .45rem; white-space: nowrap; }
.topbar__dot { width: 5px; height: 5px; border-radius: 50%; background: var(--olive); }
.topbar__sep { color: rgba(248, 244, 237, .45); }

/* --- header --------------------------------------------------------------- */
.header {
  position: sticky; top: 0; z-index: 60;
  background: rgba(248, 244, 237, .94);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--ink-12);
}
.header .wrap { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; padding: 1.05rem 0; }
.wordmark { display: flex; align-items: baseline; gap: .55rem; font-family: var(--font-display); }
.wordmark__mark {
  font-size: 1.9rem; font-weight: 600; letter-spacing: -.01em; line-height: 1;
}
.wordmark__sub {
  font-family: var(--font-body); font-size: .63rem; font-weight: 400;
  letter-spacing: .28em; text-transform: uppercase; color: var(--ink-45);
}
.nav { display: flex; align-items: center; gap: 1.9rem; }
.nav a {
  font-size: .8rem; font-weight: 500; letter-spacing: .08em; color: var(--ink-70);
  padding-bottom: .2rem; border-bottom: 1px solid transparent;
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.nav a:hover { color: var(--espresso); }
.nav a[aria-current="page"] { color: var(--espresso); border-color: var(--terra); }
.header__cta { display: flex; align-items: center; gap: 1rem; }

.nav-toggle {
  display: none; width: 42px; height: 42px; align-items: center; justify-content: center;
  border: 1px solid var(--ink-12); border-radius: var(--radius); background: none; cursor: pointer;
}
.nav-toggle span { display: block; width: 17px; height: 1px; background: var(--espresso); position: relative; }
.nav-toggle span::before, .nav-toggle span::after {
  content: ""; position: absolute; left: 0; width: 17px; height: 1px; background: var(--espresso);
}
.nav-toggle span::before { top: -5px; }
.nav-toggle span::after { top: 5px; }

/* --- hero ----------------------------------------------------------------- */
.hero { position: relative; overflow: hidden; background: var(--charcoal); }
.hero__media { position: absolute; inset: 0; }
.hero__media img { width: 100%; height: 100%; object-fit: cover; opacity: .5; }
.hero--page .hero__media::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to right, rgba(20,17,14,.55), rgba(20,17,14,.3)); }
.hero__inner { position: relative; z-index: 2; color: var(--cream); padding: clamp(5rem, 12vw, 9rem) 0; text-shadow: 0 1px 16px rgba(0,0,0,.5), 0 1px 2px rgba(0,0,0,.45); }
.hero__title { font-family: var(--font-display); font-weight: 500; font-size: clamp(2.7rem, 7vw, 5.4rem); line-height: 1.06; letter-spacing: -.02em; max-width: 20ch; }
.hero__title em { font-style: italic; color: var(--terra-soft); }
.hero__lede { margin-top: 1.6rem; max-width: 52ch; color: rgba(248, 244, 237, .78); font-size: clamp(1rem, 1.5vw, 1.18rem); }
.hero--page .hero__inner { padding: clamp(3.6rem, 8vw, 6rem) 0; }
.hero--page .hero__title { font-size: clamp(2.3rem, 5.4vw, 4.1rem); }
.crumb { font-size: .74rem; letter-spacing: .16em; text-transform: uppercase; color: rgba(248, 244, 237, .55); margin-bottom: 1.3rem; }
.crumb a:hover { color: var(--terra-soft); }

/* dual-brand hero (home) */
.dual { position: relative; display: grid; grid-template-columns: 1fr 1fr; min-height: clamp(520px, 74vh, 720px); background: var(--charcoal); }
.dual__side { position: relative; overflow: hidden; display: flex; align-items: flex-end; }
/* A video, not an img, since the client supplied footage. Cover fills the panel
   and crops the overhang: the clips are portrait (1080x1920) in a wide panel,
   so the top and bottom are cut, which is the "zoom in to fit" that was asked
   for. The hover zoom still works; scaling a playing video is fine here because
   the filter only applies inside the link. */
.dual__video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform 1.1s var(--ease); }
.dual__side:hover .dual__video { transform: scale(1.035); }
.dual__side::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, rgba(20,17,14,.94) 4%, rgba(20,17,14,.60) 42%, rgba(20,17,14,.46) 68%, rgba(20,17,14,.52) 100%); }
.dual__body { position: relative; z-index: 2; width: 100%; text-align: center; color: var(--cream); padding: clamp(1.8rem, 3.4vw, 3rem); text-shadow: 0 1px 14px rgba(0,0,0,.55), 0 1px 2px rgba(0,0,0,.5); }
.dual__kicker { font-size: .7rem; letter-spacing: .26em; text-transform: uppercase; color: var(--sand); margin-bottom: .9rem; }
.dual__name { font-family: var(--font-display); font-size: clamp(1.7rem, 3.1vw, 2.6rem); font-weight: 500; margin-bottom: .7rem; }
/* Centred as a block, not just as text: the measure is narrow so a line never
   runs the full width of a wide panel, and the auto margins keep that block in
   the middle rather than letting it sit against the left edge. */
.dual__desc { font-size: .95rem; color: rgba(248, 244, 237, .8); max-width: 34ch; margin: 0 auto 1.4rem; }
.dual__hours { font-size: .76rem; letter-spacing: .12em; text-transform: uppercase; color: rgba(248, 244, 237, .6); margin-bottom: 1.6rem; }
/* The button is inline-flex, so text-align on the parent cannot move it: it has
   to become a block-level box that shrink-wraps and centres on its own. */
.dual__side .btn { display: flex; width: fit-content; margin-inline: auto; }
.dual__badge {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 4;
  background: var(--cream); color: var(--espresso);
  font-family: var(--font-display); font-size: 1.5rem; font-weight: 600;
  width: 78px; height: 78px; border-radius: 50%;
  display: grid; place-items: center; letter-spacing: -.02em;
  box-shadow: 0 14px 40px rgba(0,0,0,.32);
}

/* --- brand strip ---------------------------------------------------------- */
.strip { background: var(--cream-deep); border-bottom: 1px solid var(--ink-12); }
.strip .wrap { display: flex; flex-wrap: wrap; justify-content: center; gap: clamp(1.4rem, 4vw, 3.4rem); padding: 1.25rem 0; }
.strip__item { display: flex; align-items: center; gap: .6rem; font-size: .76rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-70); }
.strip__item svg { width: 15px; height: 15px; flex: none; stroke: var(--olive); }

/* --- cards ---------------------------------------------------------------- */
.card { background: #fff; border: 1px solid var(--ink-12); border-radius: var(--radius-lg); overflow: hidden; display: flex; flex-direction: column; }
.card__media { aspect-ratio: 4 / 3; overflow: hidden; background: var(--sand); }
.card__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 1s var(--ease); }
.card:hover .card__media img { transform: scale(1.05); }
.card__body { padding: 1.5rem 1.6rem 1.8rem; flex: 1; display: flex; flex-direction: column; }
/* Which brand the dish belongs to. The "often ordered" section mixes both, and
   a dish name alone does not say which side of the shop it comes from. */
.card__brand { font-size: .68rem; letter-spacing: .18em; text-transform: uppercase; color: var(--terra); margin-bottom: .5rem; }
.card__title { font-family: var(--font-display); font-size: 1.42rem; margin-bottom: .5rem; }
.card__text { font-size: .92rem; color: var(--ink-70); flex: 1; }

/* generic image frame */
.frame { overflow: hidden; border-radius: var(--radius-lg); background: var(--sand); }
.frame img { width: 100%; height: 100%; object-fit: cover; }
.frame--tall { aspect-ratio: 3 / 4; }
.frame--wide { aspect-ratio: 16 / 10; }
.frame--square { aspect-ratio: 1 / 1; }

/* --- hours -----------------------------------------------------------------
   Day and time are separate GRID TRACKS, not a flex row with space-between.
   With flex, a long time string like "12.00 tengah malam" wraps wherever it
   runs out of room, so the phrase breaks differently in every column and the
   rows end up different heights. A grid with the time column sized to its
   content (and no mid-phrase wrap) keeps every row on one line, and lets two
   brands line up row for row. */
.hours { border-top: 1px solid var(--ink-12); }
.hours__row,
.hours__day-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  align-items: baseline;
  gap: 1rem;
  padding: .95rem 0;
  border-bottom: 1px solid var(--ink-12);
}
.hours__day { font-size: .82rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-45); }
/* tabular figures so the digits line up row to row ("9.30" over "9.30",
   "12.00" over "12.30") — proportional digits make a times column look
   subtly ragged even when the boxes align. */
.hours__time { font-size: .95rem; white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Two brands in one table: day | CiL Coffee | CiL & Co.

   The header and the data rows have to share ONE grid, or their auto-sized
   columns resolve independently: the head's "CiL Coffee" is ~81px wide while
   the column it labels is ~209px (the widest time in it), so the header
   labels drift ~150px left of the columns they describe. `subgrid` makes both
   stretch over the parent's tracks so they line up exactly.

   The explicit grid-template-columns below is the fallback for browsers
   without subgrid — the old, slightly-drifting alignment rather than a
   collapsed layout. */
.hours--compare {
  display: grid;
  grid-template-columns: minmax(9rem, 1fr) max-content max-content;
  align-items: baseline;
  column-gap: clamp(1.2rem, 4vw, 3.2rem);
  row-gap: 0;
  border-top: 1px solid var(--ink-12);
}
.hours--compare .hours__head,
.hours--compare .hours__day-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(9rem, 1fr) max-content max-content;   /* fallback */
  grid-template-columns: subgrid;                                     /* exact */
  align-items: baseline;
  column-gap: inherit;
}
.hours--compare .hours__head {
  padding: .55rem 0;
  border-bottom: 0;
}
.hours__brand {
  font-size: .7rem;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--terra);
}
.hours__day-label { font-size: .82rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-45); }
/* the inline brand name only appears once the table restacks (narrow screens) */
.hours__brand-inline { display: none; font-style: normal; }

/* --- menu ----------------------------------------------------------------- */
.menu-group + .menu-group { margin-top: var(--space-4); }
.menu-group__head { display: flex; align-items: center; gap: 1.1rem; margin-bottom: 1.5rem; }
.menu-group__head h3 { white-space: nowrap; }
.menu-group__head::after { content: ""; flex: 1; height: 1px; background: var(--ink-12); }
.menu-list { list-style: none; display: grid; gap: .95rem; }
.grid--2 .menu-list { gap: .95rem 2.6rem; }
.menu-item { display: flex; align-items: baseline; gap: .7rem; }
.menu-item__name { font-weight: 400; }
.menu-item__dots { flex: 1; border-bottom: 1px dotted var(--ink-12); transform: translateY(-.28em); }
.menu-item__price { font-variant-numeric: tabular-nums; letter-spacing: .02em; }
.menu-item__note { display: block; font-size: .8rem; color: var(--ink-45); margin-top: .15rem; }
.menu-item__tag {
  font-size: .6rem; letter-spacing: .14em; text-transform: uppercase;
  border: 1px solid var(--olive); color: var(--olive); border-radius: 100px;
  padding: .1rem .5rem; margin-left: .5rem; white-space: nowrap;
}
.menu-note { margin-top: var(--space-3); font-size: .82rem; color: var(--ink-45); }

/* Category links above a menu.
   The brand menus are 28 and 37 items and cannot be made shorter without losing
   prices, so this is the shortcut instead: jump straight to the category you
   came for.
   The links WRAP rather than scroll sideways. A horizontal tab strip is the
   other common pattern, but it is a nested scroll region on a page that is
   otherwise one column of scrolling, and the whole set is only ~222px wide, so
   it fits on one line down to 320px and wraps to two below that. Wrapping is
   the smaller cost.
   scroll-padding-top on <html> keeps the target heading clear of the sticky
   header when one of these is followed. */
.menu-jump {
  display: flex;
  /* centre, not baseline: the pills are 38px boxes, so aligning the plain
     "Terus ke" label to a text baseline leaves it sitting below their middle */
  align-items: center;
  gap: 1rem;
  margin-bottom: var(--space-4);
  padding-bottom: .9rem;
  border-bottom: 1px solid var(--ink-12);
}
.menu-jump__label {
  flex: none; margin: 0;
  font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ink-45);
}
.menu-jump__links { display: flex; flex-wrap: wrap; gap: .5rem; }
/* Pill-shaped, reusing the gallery filter's shape rather than inventing one.
   As plain text these were 22px tall -- half the usual 44px touch target -- and
   read as a line of links rather than something to act on. The pill gives them
   a real target (38px) and matches a control the site already uses, so the
   category bar looks like part of the design instead of an addition. */
.menu-jump__link {
  display: inline-flex; align-items: center;
  min-height: 38px; padding: .4rem 1.05rem;
  border: 1px solid var(--ink-24); border-radius: 100px;
  font-size: .72rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--espresso-soft); white-space: nowrap;
  transition: border-color .25s var(--ease), color .25s var(--ease),
              background-color .25s var(--ease);
}
.menu-jump__link:hover { border-color: var(--espresso); color: var(--espresso); }
/* Where you are in the menu. The script sets this from the scroll position. */
.menu-jump__link.is-current {
  background: var(--espresso); border-color: var(--espresso); color: var(--cream);
}

/* --- gallery -------------------------------------------------------------- */
.filter-bar { display: flex; flex-wrap: wrap; gap: .55rem; margin-bottom: var(--space-4); }
.filter {
  border: 1px solid var(--ink-12); background: none; border-radius: 100px;
  padding: .5rem 1.2rem; font-size: .74rem; letter-spacing: .12em; text-transform: uppercase;
  cursor: pointer; transition: all .3s var(--ease);
}
.filter:hover { border-color: var(--espresso); }
.filter.is-active { background: var(--espresso); border-color: var(--espresso); color: var(--cream); }
/* auto-fit, not a fixed three columns.

   The gallery is the one grid on the site where "as many as fit" is exactly the
   intent: it is a wall of photographs, and more of them per screen means less
   scrolling to see the collection. At 1180px a fixed three columns gave 382px
   tiles and a 2725px section (3.55 screens); auto-fit fits four 283px tiles
   instead, which cuts roughly a quarter off the page with no loss of image
   quality (the files are 1600px wide, so even 283px is a 5x downscale).

   min(100%, 250px) is the important part of the track: on its own a 250px
   minimum would refuse to shrink below 250px and overflow a narrow phone, and
   min() lets the single track collapse to the container width instead. */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
  gap: 1rem;
}
.gallery__item { overflow: hidden; border-radius: var(--radius-lg); background: var(--sand); aspect-ratio: 1 / 1; position: relative; }
.gallery__item--wide { grid-column: span 2; aspect-ratio: 2 / 1; }
.gallery__item img { width: 100%; height: 100%; object-fit: cover; transition: transform 1.1s var(--ease); }
.gallery__item:hover img { transform: scale(1.06); }
.gallery__cap {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 1.2rem;
  background: linear-gradient(to top, rgba(20,17,14,.8), transparent);
  color: var(--cream); font-size: .78rem; letter-spacing: .1em; text-transform: uppercase;
  opacity: 0; transition: opacity .4s var(--ease);
}
.gallery__item:hover .gallery__cap { opacity: 1; }

/* --- info blocks / location ----------------------------------------------- */
.info-list { list-style: none; display: grid; gap: 1.6rem; }
.info-list__label { font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; color: var(--terra); margin-bottom: .45rem; }
.info-list__value { font-size: 1.02rem; }
.map {
  border-radius: var(--radius-lg); overflow: hidden; border: 1px solid var(--ink-12);
  background: var(--cream-deep);
  /* min-height, not just height: inside .split the row would otherwise
     collapse and the iframe renders at 0px tall */
  min-height: 420px; display: flex;
}
.map iframe { flex: 1; width: 100%; min-height: 420px; border: 0; display: block; }

/* --- CTA band ------------------------------------------------------------- */
.cta-band { position: relative; overflow: hidden; background: var(--espresso); color: var(--cream); }
.cta-band__inner { position: relative; z-index: 2; text-align: center; padding: var(--space-6) 0; }
.cta-band__inner .h2 { margin-bottom: 1rem; }
/* .lede carries a DARK ink colour, which is right on the cream sections it was
   written for. This band is the one dark surface that uses it, so the paragraph
   was dark-on-dark: measured rgb(46,38,32) text on an rgb(46,38,32) background,
   a contrast ratio of 1.00:1 -- literally invisible, which is what the client
   reported. Restated here rather than changing .lede, which is correct everywhere
   else. */
.cta-band .lede { color: rgba(248, 244, 237, .86); }
.cta-band .btn-row { justify-content: center; margin-top: 2rem; }

/* --- footer --------------------------------------------------------------- */
.footer { background: var(--charcoal); color: rgba(248, 244, 237, .72); font-size: .9rem; padding: var(--space-6) 0 var(--space-3); }
.footer a:hover { color: var(--terra-soft); }
.footer__grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1.2fr; gap: var(--space-4); }
.footer__brand .wordmark { color: var(--cream); margin-bottom: 1.1rem; }
.footer__brand .wordmark__sub { color: rgba(248, 244, 237, .72); }
.footer__text { font-size: .88rem; max-width: 34ch; color: rgba(248, 244, 237, .6); }
/* The `small` spans inside the footer lists were inheriting the BODY ink colour
   (dark) rather than the footer's light one, because `.small` only sets a size.
   On the charcoal footer that measured 1.04:1 -- the opening hours in the footer
   were invisible. Set here so they follow the surface they sit on. */
.footer__list .small { color: rgba(248, 244, 237, .72); }
.footer__head { font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; color: var(--sand); margin-bottom: 1.1rem; }
.footer__list { list-style: none; display: grid; gap: .6rem; }
.footer__list li { font-size: .88rem; }
.footer__list a { font-weight: 400; border-bottom: 1px solid rgba(248,244,237,.16); padding-bottom: .1rem; }
.footer__bottom {
  margin-top: var(--space-5); padding-top: 1.6rem; border-top: 1px solid rgba(248, 244, 237, .12);
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: .8rem;
  font-size: .78rem; color: rgba(248, 244, 237, .52);
}
.socials { display: flex; gap: .6rem; }
.socials a {
  width: 36px; height: 36px; border: 1px solid rgba(248,244,237,.2); border-radius: 50%;
  display: grid; place-items: center; transition: all .3s var(--ease);
}
.socials a:hover { background: var(--terra); border-color: var(--terra); color: #fff; }
.socials svg { width: 15px; height: 15px; }

/* --- reveal --------------------------------------------------------------- */
.reveal { opacity: 0; transform: translateY(22px); transition: opacity .85s var(--ease), transform .85s var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  html { scroll-behavior: auto; }
}

/* --- back to top ---------------------------------------------------------- */
/* A fixed control in the bottom-right corner, hidden while the visitor is still
   at the top of the page.

   THE VISIBLE STATE IS THE DEFAULT, and `is-hidden` is what the script adds.
   That order matters: the gating is an enhancement, so if the script never runs
   -- JavaScript off, blocked, or the visibility rule misjudging -- the button
   simply stays on screen, which is the behaviour that was acceptable before. The
   inverse (ship hidden, script reveals) fails by making the control vanish
   entirely, which is the bug the client already reported once as "tak ada pun?".

   The trigger is a sentinel at the top of the document, not a scroll listener
   alone. A scroll listener cannot cover a visitor who ARRIVES already scrolled
   -- a refresh, a back navigation, a link straight to a #section -- because no
   scroll happens for them to react to; the sentinel's position can be read on
   load. Both are wired up because each covers the other's blind spot.

   Terracotta rather than charcoal: the button floats over cream content for most
   of the page but sits over the charcoal footer at the bottom, where a charcoal
   button would disappear. The accent reads against both. */
.to-top {
  position: fixed; right: 1rem; bottom: 1rem; z-index: 95;
  width: 46px; height: 46px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--terra); color: var(--cream);
  border: 1px solid rgba(0, 0, 0, .08);
  box-shadow: 0 10px 26px rgba(20, 17, 14, .3);
  transition: background .3s var(--ease), transform .3s var(--ease),
              opacity .3s var(--ease), visibility .3s var(--ease);
}
/* `pointer-events: none` as well as invisible: an element that is transparent but
   still hittable would swallow taps aimed at the content behind it. */
.to-top.is-hidden { opacity: 0; visibility: hidden; pointer-events: none; }

/* The reveal trigger, out of flow at the top of the document so it takes no
   layout space. Its HEIGHT is the setting that matters: while any of it is still
   on screen we are "at the top" and the button stays away. 120px matches the
   measured chrome above the first section (topbar 35-83px + header 77-84px), so
   the button arrives once the visitor has scrolled past the header band. */
.to-top-sentinel {
  position: absolute; top: 0; left: 0;
  width: 1px; height: 120px;
  pointer-events: none;
  visibility: hidden;
}
.to-top:hover { background: var(--espresso); transform: translateY(-2px); }
.to-top:focus-visible { outline: 2px solid var(--espresso); outline-offset: 3px; }
.to-top svg { display: block; }

@media (max-width: 640px) {
  .to-top { right: .7rem; bottom: .7rem; width: 44px; height: 44px; }
}

/* --- responsive ----------------------------------------------------------- */
@media (max-width: 1000px) {
  .footer__grid { grid-template-columns: 1fr 1fr; gap: var(--space-4) var(--space-3); }
  .grid--4 { grid-template-columns: repeat(2, 1fr); }
  .split { grid-template-columns: 1fr; gap: var(--space-4); }
  .split--flip > *:first-child { order: 2; }
  /* The gallery column count is NOT set here: auto-fit already chooses it from
     the available width. Only the double-width tile needs a rule, and only so
     it keeps spanning two tracks while two tracks still exist. */
  .gallery__item--wide { grid-column: span 2; }
}

/* 900px, not 860px: between 860 and 900 the nav was still displayed while the
   header CTA had already been hidden, so the header showed two WhatsApp
   buttons at once. Collapsing a little earlier closes that gap. */
@media (max-width: 900px) {
  .nav { display: none; }
  .nav-toggle { display: flex; }
  .nav.is-open {
    display: flex; flex-direction: column; align-items: flex-start; gap: 1.1rem;
    position: absolute; left: 0; right: 0; top: 100%;
    background: var(--cream); border-bottom: 1px solid var(--ink-12);
    padding: 1.4rem 1.3rem 1.8rem;
  }
  /* The two brand panels are FULL-WIDTH BANDS stacked one above the other, and the
     pair is sized to fit one screen -- the section is split horizontally.

     They were side by side in the previous pass, which also fits a screen, but it
     turns each panel into a 195px-wide column at 390px. In that column the body
     copy broke into 5-7 lines of two or three words, the hours line wrapped to
     three, and "Lihat CiL Coffee" wrapped with its text touching the border. Two
     bands keep the desktop panel SHAPE -- video behind a centred text block -- and
     give the copy a 320-740px measure, so the description is 2 lines and the name
     1 line at every phone width.

     Each band is half of (100svh - 165px). 165px is the chrome ABOVE the section:
     topbar 35-83px plus header 77-84px, measured at 18 viewports, worst case at
     <=360px where the topbar wraps to two lines. So the two bands plus the chrome
     add up to exactly one screen. svh, not vh, so a phone that shows its URL bar on
     load cannot push the second band past the fold. It is a MIN-height: the row
     still grows if the text ever needs more.

     Measured, both bands complete at load:
       320x568   bands 320x204   section 408   text 79%
       390x844   bands 390x342   section 684   text 47%
       430x932   bands 430x386   section 772   text 42%
       768x1024  bands 768x432   section 864   text 38%

     The type is one step down from desktop rather than the narrow-column scale the
     side-by-side version needed, because the measure is wide again. */
  .dual { grid-template-columns: 1fr; min-height: 0; }
  .dual__side { min-height: calc((100svh - 165px) / 2); align-items: center; }
  .dual__body { padding: .8rem; }
  .dual__kicker { font-size: .58rem; letter-spacing: .16em; margin-bottom: .35rem; }
  .dual__name { font-size: 1.35rem; margin-bottom: .3rem; }
  /* 46ch stops a line running the full width of a tablet-sized band; the base
     rule's `margin: 0 auto` is left in place, so the narrower block stays centred. */
  .dual__desc { font-size: .82rem; line-height: 1.5; margin-bottom: .55rem; max-width: 46ch; }
  .dual__hours { font-size: .62rem; letter-spacing: .06em; margin-bottom: .7rem; text-wrap: balance; }
  .dual__side .btn { padding: .74rem .95rem; font-size: .64rem; letter-spacing: .08em; }
  .dual__badge { display: none; }

  /* Short viewports. Two bands cannot fit a landscape phone: at 568x320 the chrome
     is 149px, so 171px of screen would have to hold two bands whose text alone
     wants 274px. Tighten the type a step first. */
  @media (max-height: 720px) {
    .dual__kicker { font-size: .54rem; letter-spacing: .14em; margin-bottom: .3rem; }
    .dual__name { font-size: 1.15rem; margin-bottom: .25rem; }
    .dual__desc { font-size: .72rem; line-height: 1.4; margin-bottom: .45rem; }
    .dual__hours { font-size: .56rem; margin-bottom: .55rem; }
    .dual__body { padding: .7rem; }
    .dual__side .btn { padding: .68rem .8rem; font-size: .58rem; }
  }

  /* Very short landscape -- under 500px of viewport height. Two bands need
     2 x 170px of text plus the 160px chrome = 500px of screen before they fit, so
     below that: one band per screen and the second follows on scroll. The section
     still reads as a stacked pair, it is just taller than one screen, and a band
     still shows its brand name, sentence and button together. Squeezing two words
     per line into a column would be the worse trade on a screen this short.

     The hours line goes, since the topbar already prints both brands' opening hours
     ("Buka setiap hari / CiL Coffee 9.30 pagi - 12 tengah malam / CiL & Co 4.30
     petang - 12 tengah malam"). Measured at 568x320: band 160px, text 123px. */
  @media (max-height: 499px) {
    .dual__side { min-height: calc(100svh - 160px); }
    .dual__hours { display: none; }
    .dual__desc { margin-bottom: .4rem; }
  }
}

/* Desktop and tablet keep the ORIGINAL side-by-side composition at EVERY viewport
   height -- two columns and the base rule's clamp(520px, 74vh, 720px) floor. The
   stacked-band treatment above is a phone arrangement and deliberately stops at
   900px.

   There used to be two `min-width: 901px` short-viewport blocks here that swapped
   desktop to stacked bands to stop the hero running past the fold at 932x430 and
   1024x500. That changed the desktop/tablet composition, which is not what the
   client wanted: "keep orientation and placement for desktop/tablet view as
   original". On a short desktop window the hero now runs below the fold and is
   scrolled into, exactly as it did originally. A hero that needs a scroll is the
   right trade for keeping the two brands side by side. */

/* Two brand columns will not fit a phone. Restack each day into a branch:
   day name, then "CiL Coffee <time>" and "CiL & Co <time>" beneath it. The
   times keep their own line so they stay aligned with each other. */
@media (max-width: 1024px) {
  /* parent collapses to one track; rows then adopt that single track via
     subgrid, so day-label + the two brand slots stack cleanly */
  .hours--compare { grid-template-columns: minmax(0, 1fr); }
  .hours--compare .hours__head { display: none; }
  .hours--compare .hours__day-row {
    grid-template-columns: minmax(0, 1fr);   /* fallback */
    grid-template-columns: subgrid;          /* exact */
    gap: .45rem;
    padding: 1.05rem 0;
  }
  .hours--compare .hours__slot {
    display: grid;
    grid-template-columns: minmax(0, 1fr) max-content;
    align-items: baseline;
    gap: .8rem;
  }
  .hours__brand-inline {
    display: block;
    font-size: .72rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-45);
  }
  .hours--compare .hours__time { font-size: .9rem; }
}

/* Container that must stack by the same point the comparison table restacks,
   so the table is never squeezed into a half-width column. */
@media (max-width: 1024px) {
  .stack-1000 { grid-template-columns: minmax(0, 1fr) !important; }
}

@media (max-width: 640px) {
  body { font-size: 16px; }
  .section { padding: var(--space-5) 0; }
  .grid--2, .grid--3, .grid--4 { grid-template-columns: 1fr; }
  /* Two columns on a phone, deliberately, rather than the single column auto-fit
     settles on. Fourteen full-width tiles made this the longest section on the
     site at 6.6 screens of scrolling, with only about two photos visible at a
     time -- a wall of photographs read one at a time. Two up halves that and
     shows four. auto-fit alone cannot express it: its 250px minimum means a
     ~400px phone container fits one track. The double-width tile drops back to a
     single square so it cannot overflow the two tracks. */
  .gallery { grid-template-columns: repeat(2, 1fr); }
  .gallery__item--wide { grid-column: span 1; aspect-ratio: 1 / 1; }
  .footer__grid { grid-template-columns: 1fr; }
  .menu-item__dots { display: none; }
  .menu-item { justify-content: space-between; }
  .wrap, .wrap-narrow { width: min(100% - 1.8rem, var(--wrap)); }

  /* The section head stacks here, and not at tablet width. At 768 the two
     columns still fit comfortably (726px available, 326px each) and the head is
     SHORTER side by side -- 150px against 218px stacked, because .section-head__
     note is capped at 44ch and would wrap to three lines in a full-width column.
     Below 640 the columns genuinely become too narrow for the heading, so the
     note drops beneath it. */
  .section-head { grid-template-columns: 1fr; gap: 1.15rem; }
  .section-head__title h2 { max-width: none; }
  .section-head__aside { padding-bottom: 0; }

  /* Cards two-up on a phone as well, and with a lighter body underneath.
     One per row made "what people order" 2.6 screens of scrolling with a single
     card in view at a time, which is the density rule 16 warns about: four items
     shown one at a time. Two-up halves the section and shows four.
     The body is tightened at the same time because at 240px wide a 4:3 photo is
     only 180px tall, so the default padding and title size would leave the text
     block taller than the picture it belongs to.
     Only .grid--auto is affected: the menu, footer and split grids all stack to
     one column deliberately and keep doing so. */
  .grid--auto { grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }
  .card__body { padding: 1rem 1rem 1.15rem; }
  .card__title { font-size: 1.12rem; margin-bottom: .35rem; }
  .card__text { font-size: .78rem; line-height: 1.45; }
  .card__brand { font-size: .6rem; margin-bottom: .35rem; }

  /* The category links lose the "Terus ke" label, which is decoration: the pills
     read as category names on their own. They also shrink a little, since five
     of them wrap to two or three rows on a narrow phone. */
  .menu-jump { gap: .5rem; margin-bottom: var(--space-3); }
  .menu-jump__label { display: none; }
  .menu-jump__links { gap: .4rem; }
  .menu-jump__link { min-height: 33px; padding: .3rem .8rem; font-size: .68rem; letter-spacing: .06em; }
}

/* --- balanced menu columns ------------------------------------------------ */
.menu-cols { align-items: start; gap: 3.2rem 4rem; }
.menu-col > .menu-group:first-child { margin-top: 0; }
.menu-col > .menu-group + .menu-group { margin-top: 2.6rem; }

/* --- draft-mockup note ----------------------------------------------------
   Lets the client know nothing here is final. Collapses to a compact bar on
   small screens so it never covers the hero copy. */
#mockup-note {
  position: fixed; left: 1rem; bottom: 1rem; z-index: 90;
  max-width: 300px; display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .5rem;
  background: var(--charcoal); color: var(--cream); border-radius: 6px;
  padding: .9rem 2.4rem .9rem 1rem; font-size: .72rem; line-height: 1.5;
  box-shadow: 0 12px 34px rgba(0, 0, 0, .3);
}
/* The note ships with the `hidden` attribute and is unhidden by script only
   when it is not currently dismissed. Author `display` beats the UA rule for
   [hidden], so without this line the note would flash on every page load
   before the script could run. */
#mockup-note[hidden] { display: none; }

#mockup-note strong {
  font-size: .62rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--terra-soft); flex-basis: 100%;
}
#mockup-note span { color: rgba(248, 244, 237, .82); }
#mockup-note button {
  position: absolute; top: .35rem; right: .45rem; width: 26px; height: 26px;
  background: none; border: 0; cursor: pointer; color: var(--terra-soft);
  font-size: 1.05rem; line-height: 1; padding: 0; border-radius: 50%;
}
#mockup-note button:hover { background: rgba(248, 244, 237, .1); }

@media (max-width: 640px) {
  #mockup-note {
    left: .6rem; right: auto; bottom: .6rem; width: calc(100vw - 1.2rem);
    max-width: none; font-size: .68rem; padding: .7rem 2.2rem .7rem .8rem;
  }
  #mockup-note span { font-size: .68rem; }
}

/* --- narrow-screen fixes --------------------------------------------------
   The topbar string is long and was nowrap, forcing the page wider than a
   phone viewport. Let it wrap and drop the separators so it stays centred. */
/* The topbar's hours string is long; it stacked only below 860px, which left
   an 860-1024px gap where it overflowed the page. Stack it up to 1024px, and
   let the items wrap instead of pushing the layout wide, so no width between
   the breakpoints can overflow. */
@media (max-width: 1024px) {
  .topbar .wrap {
    flex-direction: column; align-items: center;
    gap: .3rem; padding: .55rem 0;
  }
  .topbar__item { white-space: normal; text-align: center; line-height: 1.5; flex-wrap: wrap; justify-content: center; }
  .topbar__sep { display: none; }
}

/* --- FAQ -------------------------------------------------------------------
   Same question-left / answer-right shape as the hours table, but the answer
   is prose, so it must wrap (the hours time cell is nowrap). */
.faq__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 34ch);
  align-items: baseline;
  gap: 1.2rem 2rem;
  padding: 1.05rem 0;
  border-bottom: 1px solid var(--ink-12);
}
.faq__q { font-size: .82rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-45); }
.faq__a { font-size: .95rem; }

@media (max-width: 640px) {
  .faq__row { grid-template-columns: minmax(0, 1fr); gap: .4rem; }
  .faq__a { margin-bottom: .2rem; }
}

/* Below ~400px the single-brand day label ("Isnin – Khamis") wraps to a second
   line while short ones do not, so rows come out 58px and 80px mixed. Stack
   day over time for an even rhythm instead. */
@media (max-width: 400px) {
  .hours--single .hours__row { grid-template-columns: minmax(0, 1fr); gap: .25rem; }
  .hours--single .hours__day { font-size: .78rem; }
  .hours--single .hours__time { font-size: .92rem; }
}

@media (max-width: 480px) {
  /* Icon-only, sized to match the 42px menu toggle so the pair reads as one
     neat set of controls. Target the LABEL, not every span: icon() wraps its
     SVG in a span too, so `span { display:none }` blanked the button. */
  .header__wa { width: 42px; height: 42px; padding: 0; justify-content: center; }
  .header__wa__label { display: none; }
}

/* The address would not fit beside the hours string until ~1280px, so it only
   appears on wide screens; below that the hours string owns the bar. */
@media (max-width: 1279px) {
  .topbar__item--hide { display: none; }
}

/* --- plate carousel --------------------------------------------------------
   One plate fills the centre with its neighbours peeking in at both edges, so
   the eye reads "there is more" without needing a row of dots. Each image is a
   transparent cut-out along the plate's own rim (tools/build-plates.py), which
   is why every source is a single plate or bowl with the table visible around
   it -- the cut finds the edge, so a spread or a table of several bowls has no
   edge to find. The active plate is wider than the rest, and the row is
   translated so the active plate's centre lands on the viewport centre. */
.plates {
  position: relative;
  overflow: hidden;
  padding-block: clamp(1.2rem, 2.6vw, 2.2rem);
  /* The gap under the "Menu Pilihan" label is deliberately about half what it
     was. At 1366px the label's ink sat 102px above the top of the plate
     artwork, and the client asked for roughly half.

     THREE things made that 102px up, and only two are ours to give back:

       42px  transparent air baked into every plate file. Each plate is drawn at
             80% of its canvas (verified across all ten: fill 0.786-0.800), so
             10% of the canvas is empty on every side. This is part of the
             artwork and is what separates the plates from each other sideways
             too; it cannot be removed without re-cutting the ten files.
        35px  .plates padding-block
        19px  the label's margin-bottom

     So the padding goes to zero and the label's own margin with it, leaving the
     visible gap as the image air alone: measured 49px against 103px at 1366px,
     a 48% reduction -- "about 50%", as asked. The gap can never reach zero,
     because the plate artwork never reaches the edge of its own file.

     Overriding padding-top only leaves the tail below the plates exactly as
     agreed in the previous pass. */
  padding-top: 0;
}
.plates__row {
  display: flex;
  align-items: center;
  gap: clamp(.9rem, 3vw, 3rem);
  will-change: transform;
  transition: transform .8s var(--ease-carousel);
  /* Reserve the ACTIVE plate's height, always.

     The active plate is the tallest thing in the row, so without this the row's
     height tracks whichever plate is biggest RIGHT NOW. Half way through a step
     the outgoing plate has shrunk and the incoming one has not finished growing,
     so the tallest item is only ~290px against the usual 424px: the row dips
     134px, the whole section breathes, and because the plates are centre-aligned
     the visible plate rises and falls about 67px. Measured before this fix:
     row height 290..424 and the next section's top travelled 134px, i.e. the
     content below was pushed up and down on every single step.

     With the active width reserved, the row height never changes, so nothing
     below it moves and the plates sit still vertically. At rest the height is
     identical to before (the active plate already was that tall), so this
     changes nothing except removing the dip. */
  min-height: var(--plate-active);
  /* Reset the list styling, or the browser paints a disc bullet on every one of
     the 20 <li> (10 plates x the loop copy below), which shows on the page as a
     row of stray dots underneath the plates. Every other <ul> in the site
     (.menu-list, .info-list, .footer__list) already resets this. */
  list-style: none;
  margin: 0;
  padding: 0;
}
.plates__item {
  flex: none;
  width: clamp(78px, 12vw, 156px);
  opacity: .66;
  transition: width .8s var(--ease-carousel), opacity .8s var(--ease-carousel);
}
.plates__item.is-active { width: var(--plate-active); opacity: 1; }
.plates__disc {
  position: relative;
  /* No border-radius and no overflow clipping here: the image IS the plate cut
     to its own edge, on a transparent background, so masking it again would
     only clip the rim. */
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  /* No drop-shadow, deliberately. There used to be one (26px down at 30% on the
     active plate, 10px/16% on the neighbours) and the client saw it sitting
     under every plate as a dark smear, worst on the centre one. It is also
     redundant: the cut-out photos already carry their own soft shading inside
     the rim, so the CSS shadow was a second shadow stacked on the first. The
     plates are flat on the background now.

     Note this is only a CSS removal -- the images themselves were measured and
     are clean cut-outs: alpha reaches 0 within 1-5px of the rim in every
     direction, so nothing is baked in. */
  transition: none;
}
.plates__disc img { display: block; width: 100%; height: 100%; object-fit: contain; }

.plates__nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 2; width: 44px; height: 44px; padding: 0;
  display: grid; place-items: center;
  border-radius: 50%; cursor: pointer;
  background: var(--cream); color: var(--ink);
  border: 1px solid var(--ink-12);
  box-shadow: 0 6px 18px rgba(34, 31, 27, .14);
  transition: background .25s var(--ease), color .25s var(--ease);
}
.plates__nav:hover { background: var(--terracotta); color: #fff; border-color: transparent; }
.plates__nav--prev { left: clamp(6px, 2vw, 28px); }
.plates__nav--next { right: clamp(6px, 2vw, 28px); }
.plates__nav svg { width: 18px; height: 18px; stroke: currentColor; fill: none; }

/* Applied only for one synchronous measurement: with every transition off the
   settled geometry of the enlarged plate can be read immediately, instead of
   reading the plate's OLD width and leaving it half the width difference off
   centre (a measured 128px at desktop width). */
.plates.is-measuring .plates__row,
.plates.is-measuring .plates__item,
.plates.is-measuring .plates__disc { transition: none !important; }

/* Someone who asked for less motion gets no drift and no easing: the plates
   still change, they just do not glide there. */
@media (prefers-reduced-motion: reduce) {
  .plates__row, .plates__item, .plates__disc { transition: none; }
}

/* Visible to screen readers only. Needed because the carousel repeats its list
   so the loop is seamless: whichever plate is on show is as likely to be the
   aria-hidden copy as the original, so the copies carry alt="" -- and a screen
   reader sitting on a copy would otherwise be told nothing at all. The dish
   name is announced through a polite live region instead. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* --- home page: the plate carousel's breathing room -----------------------
   The carousel section carries NO vertical padding of its own. Everything
   around the plates comes from .plates' own padding-block plus the transparent
   air baked into every plate image (each is drawn at 80% of its canvas, so 10%
   is empty on each side -- 42px at a 424px plate).

   That is deliberate, and it replaced two separate arrangements.

   First the strip sat far above the section below it: 267px from the plate's
   visible edge down to the "Dari ulasan Google" eyebrow, made of 42px of image
   air + 35px of .plates padding + 64px of the carousel section's padding-bottom
   + 104px of the next section's padding-top + 22px of its heading. The next
   section's padding-top went, and the carousel's tail with it, leaving 82px.

   Then the gap ABOVE the strip turned out to be the odd one out. Measured at
   1366px, the three gaps around the carousel were:

    172px   intro text       -> "Menu Pilihan"   (104px + 64px of section padding)
     97px   "Menu Pilihan"   -> top of a plate   (42px image air + 35px padding)
     82px   bottom of plate  -> "Dari ulasan"    (42px image air + 35px padding)

   A 2.1 : 1.2 : 1 ratio around one block, so the carousel read as floating --
   crowded underneath but adrift above. Dropping the carousel section's
   padding-top too brings it to 105 / 97 / 82, near enough even, and the
   carousel now sits the same 104px below the previous section as any other
   section on the site.

   On a phone the same change gives 64 / 62 / 66px, because .section's own
   bottom padding is already 64px at that width.

   These are `.plates-section--home` / `.section--join-top`, used only by the
   two home-page blocks, so no other page's rhythm changes. Both classes are
   applied only where they are wanted (generate-site.py). */
.plates-section--home { padding-top: 0; padding-bottom: 0; }
/* The section directly above the carousel gets a shorter tail, so that the
   three gaps around the strip land within ~20px of each other. Measured as
   BLANK BACKGROUND bands on the rendered page (which is what the eye reads,
   and is more honest than adding up paddings and image air):

     before   118 / 93 / 75px   (text -> label / label -> plate / plate -> label)
     after     95 / 94 / 76px

   Expressed as an adjacent-sibling rule so it follows whatever section happens
   to sit above the carousel, rather than needing a marker class in the
   generator for one 24px trim.

   Scoped above 640px: at phone widths .section's tail is only 64px already, so
   this trim would have made the gap LARGER (80px) rather than smaller. */
@media (min-width: 641px) {
  .section:has(+ .plates-section--home) { padding-bottom: calc(var(--space-6) - var(--space-3)); }
}
.section--join-top { padding-top: 0; }
@media (max-width: 640px) {
  /* Phone spacing is tighter anyway, so the tail is reduced rather than
     removed -- delete it and the plates crowd the heading below. */
  .plates-section--home { padding-bottom: var(--space-3); }
}

