/* ===========================================================================
   GPI Gelato Panini Incorporato
   ---------------------------------------------------------------------------
   Mobile-first. Most traffic arrives on a phone from the Google Maps listing,
   so the narrow layout is the base and desktop is the enhancement.

   BRAND COLOUR IS THEIR OWN. Sampled from the mark itself — the logo they
   posted to Instagram on 2025-11-13 (@gpi.gelatopanini), which is flat
   artwork on white rather than a photograph of a cup, so the reading is the
   ink and not the lighting. The dominant saturated colour is #020988 at
   hue 237°, 29.5% of the frame; `npm run brand -- source/logo.png` reproduces
   it and printed the ramp below, already nudged where a step would have
   missed WCAG AA. Re-run it if they ever supply vector artwork.
   =========================================================================== */

:root {
  /* Brand — hue 237°, derived from their logo. See note above. */
  --brand-900: #0c1162;
  --brand-700: #121996;
  --brand-500: #333bc7;
  --brand-300: #777de8; /* for text ON brand-900 — brand-500 only reaches 2.9:1 there */
  --brand-100: #dadcf7;

  /* The blue GPI actually print, sampled off the flat fill of their own logo
     artwork (207k pixels, one cluster — it is vector, so this is exact). The
     ramp above is derived from it and shares its hue, 237 degrees, but no step
     lands on it: brand-900 is darker, brand-700 lighter. The wordmark sits
     directly beside the mark in the header, so it uses the mark's own colour
     rather than the nearest step, which would read as two different blues. */
  --brand-ink: #000888;

  /* WhatsApp green, darkened from the familiar #25d366. The brand green gives
     white label text about 3:1, which fails AA; these clear 5.2:1 and 6.4:1. */
  --wa: #0f7d3a;
  --wa-hover: #0c6e32;

  /* Warm neutrals. The page is cream rather than white so the dessert
     photography reads as the brightest thing on the screen. */
  --cream: #fdfaf4;
  --cream-deep: #f3ece1;
  --beige: #e8dcc9;
  --surface: #ffffff;

  --ink: #1c1a17;
  --ink-soft: #4a443c;
  --ink-muted: #6b6259; /* 5.6:1 on --cream — AA for body text */

  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 22px;

  --shadow-sm: 0 1px 2px rgb(28 26 23 / 0.04), 0 2px 8px rgb(28 26 23 / 0.04);
  --shadow: 0 2px 4px rgb(28 26 23 / 0.04), 0 12px 32px rgb(28 26 23 / 0.08);

  /* The brush script. GPI letter almost every headline they publish this way,
     set against a bold face — it is the most recognisable thing they own.
     Used for a handful of words and never for anything that has to be read
     carefully, so a fallback to any cursive still leaves the page legible. */
  --font-script: 'Kaushan Script', 'Brush Script MT', cursive;

  --font-display: 'Chonburi', 'Bodoni MT', ui-serif, Georgia, serif;
  --font-body: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;

  /* Fluid rhythm — one knob for vertical spacing across the whole page. */
  --gap: clamp(1rem, 3vw, 1.75rem);
  --section-y: clamp(3.5rem, 9vw, 7.5rem);
  --page-x: clamp(1.15rem, 5vw, 4rem);
  --measure: 62ch;
}

/* --- Reset ---------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img,
svg,
picture {
  display: block;
  max-width: 100%;
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -0.015em;
  margin: 0;
  text-wrap: balance;
}

p {
  margin: 0;
  text-wrap: pretty;
}

a {
  color: inherit;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

button {
  font: inherit;
  color: inherit;
}

/* Visible, branded focus ring on every interactive element. */
:focus-visible {
  outline: 2px solid var(--brand-700);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--brand-900);
  color: #fff;
  border-radius: 0 0 var(--radius-sm) 0;
  text-decoration: none;
}
.skip-link:focus {
  left: 0;
}

/* --- Shared type ---------------------------------------------------------- */

.eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand-700);
  margin-bottom: 0.9rem;
}

.section {
  padding: var(--section-y) var(--page-x);
}

.section__head {
  max-width: var(--measure);
  margin-bottom: clamp(2rem, 5vw, 3.5rem);
}

.section__title {
  font-size: clamp(2rem, 6vw, 3.25rem);
}

.section__lead {
  margin-top: 1.1rem;
  color: var(--ink-soft);
  font-size: 1.0625rem;
}

/* --- Buttons -------------------------------------------------------------- */

/* [hidden] is a UA rule at the lowest specificity, so any class that sets
   display beats it. Without this, an <a class="btn" hidden> still renders — and
   a button that looks live but has no href is worse than no button at all. */
.btn[hidden] {
  display: none;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  min-height: 48px; /* comfortable thumb target */
  padding: 0.8rem 1.6rem;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease,
    transform 0.2s ease;
}
.btn svg {
  width: 20px;
  height: 20px;
}
/* "desde $65" riding inside the primary button. A price on the button that
   opens the menu answers the question people actually hesitate on, without
   spending a line of the hero on it. */
.btn__note {
  opacity: 0.72;
  font-weight: 500;
  font-size: 0.8125em;
  padding-left: 0.15em;
}

.btn--primary {
  background: var(--brand-900);
  color: #fff;
}
.btn--primary:hover {
  background: var(--brand-700);
}
.btn--ghost {
  border-color: rgb(18 48 92 / 0.28);
  color: var(--brand-900);
}
.btn--ghost:hover {
  background: var(--brand-100);
  border-color: transparent;
}
.btn--whatsapp {
  background: var(--wa);
  color: #fff;
}
.btn--whatsapp:hover {
  background: var(--wa-hover);
}
.btn:active {
  transform: translateY(1px);
}

/* --- Header --------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: 0.9rem var(--page-x);

  /* White, on a cream page — which is their own packaging, not a guess.
     Everything GPI print is blue ink on a pale ground: the box is white card,
     the bag is kraft, the mark is blue on both. Blue is the INK. A blue header
     would invert that relationship, and with the footer already a solid blue
     band it would sandwich every page in blue and frame the food photography,
     which is the thing that actually sells. White also gives the header its
     own edge against the cream, so the hairline border is no longer doing
     work the colour can do. */
  background: rgb(255 255 255 / 0.88);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid rgb(28 26 23 / 0.06);
}

.site-header__brand {
  text-decoration: none;
}

.wordmark {
  display: flex;
  flex-direction: column;
  line-height: 1;
}
.wordmark__gpi {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.6rem;
  letter-spacing: -0.03em;
  color: var(--brand-900);
}
.wordmark__full {
  font-size: 0.5625rem;
  font-weight: 500;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-top: 0.28rem;
}

.site-nav {
  display: none;
  gap: 1.75rem;
  font-size: 0.9375rem;
  font-weight: 500;
}
.site-nav a {
  text-decoration: none;
  color: var(--ink-soft);
  padding-block: 0.25rem;
  border-bottom: 1px solid transparent;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.site-nav a:hover {
  color: var(--brand-900);
  border-bottom-color: var(--brand-500);
}

/* --- Media (photo or branded placeholder) --------------------------------- */

.media {
  display: block;
  position: relative;
  aspect-ratio: var(--ratio, 4 / 3);
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--cream-deep);
}
.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Placeholder is a designed state, not a broken image: this build goes to the
   client precisely to collect these photographs. */
.media--placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 1.5rem;
  text-align: center;
  color: var(--brand-700);
  background: linear-gradient(150deg, var(--brand-100) 0%, var(--cream-deep) 55%, var(--beige) 100%);
  border: 1px solid rgb(18 48 92 / 0.1);
}
.media__mark {
  width: 40px;
  height: 40px;
  opacity: 0.42;
}
.media__label {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 400;
  color: var(--brand-900);
}
.media__pending {
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* --- Hero ----------------------------------------------------------------- */

.hero {
  display: grid;
  gap: clamp(2rem, 6vw, 3.5rem);
  padding: clamp(2.5rem, 8vw, 5.5rem) var(--page-x) var(--section-y);
}

.hero__title {
  font-size: clamp(2.6rem, 11vw, 5.25rem);
  letter-spacing: -0.03em;
}

.hero__tagline {
  margin-top: 1.4rem;
  max-width: 46ch;
  font-size: clamp(1.0625rem, 2.4vw, 1.25rem);
  color: var(--ink-soft);
}

/* Both CTAs on one line, always.
   This used to wrap: the WhatsApp button that sat here was as wide as the
   primary, so on a phone it took a second row and pushed the photograph off
   the first screen entirely — the visitor's first impression of a dessert shop
   was two buttons and no dessert. The call button is sized to fit beside the
   primary instead, and nowrap makes that a rule rather than a coincidence of
   string length. */
.hero__cta {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1.5rem;
}
.hero__cta .btn--primary {
  flex: 0 1 auto;
  min-width: 0;
}

/* Deliberately quieter and narrower than the primary: the menu is what this
   page is for, and a call is the second thought. Keeps the 48px height, since
   shrinking a tap target below that is the bug that was just fixed elsewhere
   on this page. */
.btn--call {
  flex: 0 0 auto;
  padding-inline: 1rem;
  gap: 0.4rem;
  background: var(--surface);
  border-color: color-mix(in srgb, var(--brand-900) 28%, transparent);
  color: var(--brand-900);
}
.btn--call:hover {
  background: var(--brand-100);
  border-color: transparent;
}

/* The hero photograph, with the cut-open shot overlapping its lower corner.
   Two images at two sizes read as an arrangement someone composed; two at the
   same size read as a contact sheet. The inset also does a job the main shot
   cannot: it is the only frame that shows the gelato inside the bread. */
.hero__media {
  position: relative;
  padding-bottom: 3rem;
}
.hero__media > .media {
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.hero__inset {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 42%;
  max-width: 15rem;
}
@media (min-width: 900px) {
  /* Hangs slightly off the main photograph rather than sitting politely inside
     it. Overlapping the edge is what makes two pictures read as one
     composition; tucked fully inside, it reads as a sticker. */
  .hero__inset {
    left: clamp(-2.25rem, -2vw, -1rem);
  }
}
.hero__inset .media {
  border-radius: var(--radius);
  overflow: hidden;
  /* A cream keyline separates it from whatever it lands on in the photograph
     behind, which is otherwise also warm and pale. */
  border: 5px solid var(--cream);
  box-shadow: 0 14px 34px rgb(28 26 23 / 0.18);
}

/* --- Menu grid ------------------------------------------------------------ */

.section--menu {
  background: var(--cream-deep);
}

/* Two-up from the smallest width. One dessert per screen makes a six-item menu
   feel endless on a phone; two-up puts the whole menu within a couple of
   flicks, which is how people actually read a menu. The gap tightens below
   560px so each card still clears ~165px. */
.grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
}

.card {
  position: relative; /* anchors .card__open */
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow);
}

/* One transparent tap target over the whole card. A <button> rather than a
   wrapping <a> because it opens a dialog rather than navigating, and an overlay
   rather than wrapping the content because a heading is not valid inside a
   button. */
.card__open {
  position: absolute;
  inset: 0;
  width: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  border-radius: var(--radius-lg);
}
.card__open:focus-visible {
  outline: 2px solid var(--brand-700);
  outline-offset: 3px;
}
/* Quiet affordance that the card opens. */
.card__open::after {
  content: '';
  position: absolute;
  top: 0.55rem;
  right: 0.55rem;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: rgb(251 247 241 / 0.9) center / 15px 15px no-repeat
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2312305c' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M15.5 15.5L21 21M8 10.5h5M10.5 8v5'/%3E%3C/svg%3E");
  box-shadow: 0 1px 4px rgb(28 26 23 / 0.16);
  opacity: 0.85;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.card:hover .card__open::after {
  opacity: 1;
  transform: scale(1.06);
}

.card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 0.9rem 0.95rem 1.1rem;
}
.card__name {
  font-size: 1.0625rem;
  margin-bottom: 0.35rem;
}
.card__text {
  color: var(--ink-soft);
  font-size: 0.8125rem;
  line-height: 1.55;
}
/* Clamped on the card, complete in the detail sheet — the card advertises, the
   sheet explains.

   Armed by .js-sheet, which the inline <head> script adds only after confirming
   dialog.showModal exists. Unconditionally clamping hid 182px of a 242px
   description with no way to reach the rest: the open button ships hidden and
   is revealed by the same script, so without it the reader had neither the
   full text nor a control to expand it. A clamp is only honest when something
   can undo it. */
.js-sheet .card__text {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.card__flavors {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.7rem;
}
.card__flavors li {
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  background: var(--brand-100);
  color: var(--brand-900);
  font-size: 0.75rem;
  font-weight: 500;
}
/* Name and price on one baseline. A price under the description is a price
   the reader finds only after deciding they want it; on the same line as the
   name it is part of the same glance. */
.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}
.card__head .card__name {
  margin-bottom: 0;
}
.card__price {
  flex: none;
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 400;
  color: var(--brand-900);
  font-variant-numeric: tabular-nums;
}

/* "Elige tu gelato" — four bare words under a dessert could be anything, so
   the chips get a label. */
.card__flavorwrap {
  margin-top: 0.9rem;
}
.card__flavorlabel {
  margin: 0;
  font-size: 0.6875rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-muted);
}
.card__flavorwrap .card__flavors {
  margin-top: 0.45rem;
}

.card__badge {
  position: absolute;
  z-index: 2;
  top: 0.75rem;
  left: 0.75rem;
  padding: 0.3rem 0.65rem;
  border-radius: 999px;
  background: var(--brand-900);
  color: #fff;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.11em;
  box-shadow: 0 2px 10px rgb(12 17 98 / 0.35);
}

.menu__note {
  margin: clamp(1.25rem, 3vw, 2rem) 0 0;
  font-size: 0.8125rem;
  color: var(--ink-muted);
}

/* --- Card frame treatments -------------------------------------------------
   Three ways of presenting the photograph inside the card, selected by
   data-card-style on <body>. Each is CSS-only against the existing markup, so
   switching between them changes nothing structural and cannot affect the
   image manifest, alt text or the reserved aspect ratio.

   Once one is chosen the other two are deleted and the attribute goes away.
   -------------------------------------------------------------------------- */

/* A — MATTED. The photograph is inset on all sides with its own rounded
   corners, so the white card reads as a mount around a print. */
[data-card-style='matted'] .card .media {
  margin: 0.5rem 0.5rem 0;
  border-radius: calc(var(--radius-lg) - 0.45rem);
  width: auto;
}

/* B — FULL-BLEED WITH A LIFT. The photograph keeps the whole card width and
   gains a hairline edge plus a slow zoom. */
[data-card-style='bleed'] .card .media {
  border-radius: 0;
  overflow: hidden;
}
[data-card-style='bleed'] .card .media::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 1px rgb(28 26 23 / 0.08);
  pointer-events: none;
}
[data-card-style='bleed'] .card .media img {
  transition: transform 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}
[data-card-style='bleed'] .card:hover .media img {
  transform: scale(1.045);
}

/* C — FRAMED ARCH. Inset like the matte, but the photograph's top corners open
   into an arch — the shape on half the gelateria signage in Italy. */
[data-card-style='arch'] .card .media {
  margin: 0.55rem 0.55rem 0;
  width: auto;
  border-radius: 100vw 100vw var(--radius) var(--radius) / 42% 42% var(--radius) var(--radius);
}
[data-card-style='arch'] .card__body {
  text-align: center;
}
[data-card-style='arch'] .card__flavors {
  justify-content: center;
}

/* --- Detail sheet ---------------------------------------------------------
   Native <dialog>: the platform supplies focus trapping, Esc to dismiss,
   inertness of the page behind and ::backdrop — none of it worth
   reimplementing. Bottom sheet on phones, centred panel from 640px. */

.sheet {
  width: 100%;
  max-width: 34rem;
  padding: 0;
  border: 0;
  margin: auto auto 0;
  background: var(--surface);
  color: var(--ink);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: 0 -8px 40px rgb(28 26 23 / 0.22);
  overflow: hidden;
}
.sheet::backdrop {
  background: rgb(18 48 92 / 0.45);
}
.sheet[open] {
  animation: sheet-in 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes sheet-in {
  from { transform: translateY(14px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

.sheet__close {
  position: absolute;
  top: 0.7rem;
  right: 0.7rem;
  z-index: 2;
  display: grid;
  place-items: center;
  /* 44px, not the 38 it was: this is the way out of a modal on a phone, and a
     miss leaves the reader trapped in a sheet they did not want. The icon
     inside keeps its old size — only the box grew. */
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  color: var(--ink);
  background: rgb(251 247 241 / 0.92);
  box-shadow: 0 1px 6px rgb(28 26 23 / 0.18);
}
.sheet__close svg {
  width: 19px;
  height: 19px;
}

.sheet__media .media {
  border-radius: 0;
  aspect-ratio: 4 / 3;
  margin: 0;
  width: 100%;
}
.sheet__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.8rem;
  padding: 1.4rem 1.5rem 1.6rem;
}
.sheet__name {
  font-size: clamp(1.5rem, 5vw, 1.9rem);
}
.sheet__text {
  color: var(--ink-soft);
  font-size: 0.9375rem;
  line-height: 1.72;
}

/* Shown only on a generated stand-in photograph. Deliberately plain and
   small: it should read as an honest caption, not a warning banner. */
.sheet__refnote {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--ink-muted);
  padding-top: 0.2rem;
  border-top: 1px dashed rgb(18 48 92 / 0.2);
  width: 100%;
}
.sheet__refnote[hidden] {
  display: none;
}

@media (min-width: 640px) {
  .sheet {
    margin: auto;
    border-radius: var(--radius-lg);
    box-shadow: 0 24px 60px rgb(28 26 23 / 0.28);
  }
}

/* --- Proceso -------------------------------------------------------------- */

.section--proceso {
  background: var(--brand-900);
  color: var(--cream);
}
.section--proceso .eyebrow {
  color: var(--brand-300);
}
.section--proceso .section__title {
  color: #fff;
}

.section--proceso .section__lead {
  color: rgb(251 247 241 / 0.78);
}

.proceso__layout {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3.25rem);
  margin-top: clamp(2rem, 4vw, 3rem);
}

/* The press photograph is a frame from their own reel, so it is only 480px
   wide. Capped rather than stretched: a soft image the size of a postcard
   reads as intimate, the same image blown across a column reads as broken. */
.proceso__shot {
  margin: 0;
  max-width: 22rem;
  justify-self: center;
}
.proceso__shot .media {
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: 0 24px 60px rgb(0 0 0 / 0.35);
}

/* Numbers come from .step__n, so the list's own markers are suppressed —
   otherwise every step reads "1. 1". The <ol> stays an <ol> because the order
   is the meaning here. */
.proceso__steps {
  list-style: none;
  counter-reset: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: clamp(1.4rem, 3vw, 2rem);
}

.step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1rem;
  align-items: start;
}
.step__n {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 999px;
  border: 1px solid rgb(251 247 241 / 0.35);
  color: var(--brand-300);
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1;
}
.step__n[hidden] {
  display: none;
}
.step__title {
  margin: 0 0 0.35rem;
  font-size: 1.15rem;
  color: #fff;
}
.step__text {
  margin: 0;
  color: rgb(251 247 241 / 0.8);
  font-size: 0.9375rem;
  line-height: 1.7;
  max-width: 46ch;
}

/* --- Drinks price list -----------------------------------------------------
   Shared by both drink cards. The leader dots are what make a list of names
   and numbers read as a menu rather than a table; they are decoration, hence
   aria-hidden in the markup and a flexible rule rather than literal dots.
   -------------------------------------------------------------------------- */

.pricelist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.15rem;
}
.pricelist li {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid rgb(28 26 23 / 0.08);
}
.pricelist li:last-child {
  border-bottom: 0;
}
.pricelist__name {
  font-size: 0.9375rem;
  color: var(--ink);
}
/* The leader dots are the thing that makes a price list read as a menu. They
   are decoration, hence aria-hidden in the markup and a flexible rule here
   rather than a row of literal full stops. */
.pricelist__dots {
  flex: 1 1 auto;
  height: 0;
  border-bottom: 1px dotted rgb(28 26 23 / 0.28);
  transform: translateY(-0.25rem);
}
.pricelist__dots[hidden] {
  display: none;
}
.pricelist__price {
  flex: none;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 400;
  color: var(--brand-900);
  font-variant-numeric: tabular-nums;
}

/* --- Open / closed status --------------------------------------------------
   Filled in by script and hidden until then — see main.js. The dot is the
   whole signal at a glance; the sentence is the detail for anyone who reads
   on. Colour alone never carries it, because the text says the same thing.
   -------------------------------------------------------------------------- */

.status {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.25rem;
  padding: 0.4rem 0.85rem 0.4rem 0.7rem;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid rgb(28 26 23 / 0.09);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--ink-soft);
  box-shadow: var(--shadow-sm);
}
.status[hidden] {
  display: none;
}
.status::before {
  content: '';
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  background: var(--ink-muted);
  flex: none;
}
.status--open::before {
  background: var(--wa);
  box-shadow: 0 0 0 3px rgb(15 125 58 / 0.16);
}
.status--closed::before {
  background: #b23b3b;
  box-shadow: 0 0 0 3px rgb(178 59 59 / 0.14);
}
.hours + .status {
  margin-top: 0.85rem;
}

/* --- Ubicación ------------------------------------------------------------ */

.ubicacion__grid {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3rem);
}

.ubicacion__info {
  display: flex;
  flex-direction: column;
  gap: 1.85rem;
  align-items: flex-start;
}

.info__label {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 0.55rem;
}
.info__value {
  font-size: 1.0625rem;
  line-height: 1.7;
}
.info__meta {
  margin-top: 0.4rem;
  font-size: 0.875rem;
  color: var(--ink-muted);
}
/* Unconfirmed data is shown as unconfirmed rather than guessed or hidden. */
.info__pending {
  display: inline-block;
  margin-top: 0.55rem;
  padding: 0.2rem 0.6rem;
  border: 1px dashed rgb(18 48 92 / 0.3);
  border-radius: 999px;
  font-size: 0.75rem;
  color: var(--ink-muted);
}

.hours {
  display: grid;
  gap: 0.35rem;
  font-size: 0.9375rem;
}
.hours li {
  display: flex;
  justify-content: space-between;
  gap: 1.5rem;
  max-width: 22rem;
  padding: 0.15rem 0.5rem;
  margin-inline: -0.5rem;
  border-radius: var(--radius-sm);
}
/* Today, marked by script. Someone checking a week of hours is nearly always
   asking about today, so today is the row that should not need finding. */
.hours li.is-today {
  background: var(--brand-100);
  color: var(--brand-900);
  font-weight: 600;
}
.hours__closed {
  font-style: normal;
  color: var(--ink-muted);
}
.hours li.is-today .hours__closed {
  color: var(--brand-900);
}

/* Map: a facade until the visitor asks for it. */
.map {
  min-height: 300px;
}
.map__facade,
.map iframe {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 300px;
  border: 0;
  border-radius: var(--radius-lg);
}
.map__facade {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  padding: 2rem;
  cursor: pointer;
  text-align: center;
  color: var(--brand-900);
  background: linear-gradient(150deg, var(--brand-100) 0%, var(--cream-deep) 60%, var(--beige) 100%);
  border: 1px solid rgb(18 48 92 / 0.1);
  transition: filter 0.2s ease;
}
.map__facade:hover {
  filter: brightness(0.98);
}
.map__pin svg {
  width: 42px;
  height: 42px;
  opacity: 0.65;
}
.map__text {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.map__text strong {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.15rem;
}
.map__text span {
  font-size: 0.875rem;
  color: var(--ink-muted);
}

/* --- Atmósfera (full-bleed interior shot) --------------------------------- */

/* Deliberately edge-to-edge and unpadded: it is a breath between the dark
   process band and Ubicación, and the photograph is the whole point. On a
   phone 21/9 would be a letterbox slot, so it opens up to 4/3. */
.atmosfera .media {
  border-radius: 0;
  aspect-ratio: 4 / 3;
}

/* --- Fachada (storefront, inside Ubicación) ------------------------------- */

.fachada {
  margin: 0;
  width: 100%;
}
.fachada .media {
  border-radius: var(--radius);
}
.fachada figcaption {
  margin-top: 0.6rem;
  font-size: 0.8125rem;
  color: var(--ink-muted);
}

/* --- Real logo, once supplied --------------------------------------------- */

/* Replaces the provisional CSS wordmark. Height-constrained with width:auto so
   any aspect ratio the client's artwork happens to have still fits the header. */
/* Mark and name side by side, the way the white box sets it. The bag stacks
   them instead, but a header is a horizontal strip and the box lockup is the
   one that fits it. */
.wordmark--logo {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.6rem;
}

/* The display face, which is the kraft bag's, not the white box's.
   Their two printed pieces set the same name in two different faces: the box
   in an Optima-class flared humanist sans, the bag in a heavy Didone. The
   site follows the bag, because that is the one it can match with the face it
   already loads for its headings. (That face was Bodoni Moda when this note
   was written; it is Chonburi now — see tools/fetch-fonts.mjs for why.) */
/* One line, not the packaging's two.

   Both printed pieces break the name over two lines and the first cut of this
   followed them. The client marked the spot they wanted on a screenshot and
   the mark was a SINGLE line beside the mark, roughly 268px wide at a 1920px
   viewport — which is what the clamp below is sized to hit. A header is a
   horizontal strip; the name reads along it better than stacked in it. */
.wordmark__name {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(0.95rem, 2.2vw, 1.25rem);
  line-height: 1.1;
  letter-spacing: 0.004em;
  color: var(--brand-ink);
  white-space: nowrap;
}

/* The footer runs the SAME wordmark on the blue band, where --brand-ink is
   1.09:1 against the ground — the name was there and completely invisible.
   Adding the name to the header added it to the footer too, and only the
   header got a colour. Found by measuring, not by looking: at 1.09:1 there is
   nothing on screen to notice. */
.site-footer .wordmark__name {
  color: #fff;
}

/* Two source lines running together on one still need the space between. */
.wordmark__line + .wordmark__line::before {
  content: ' ';
}

/* Narrow phones: the mark alone is unmistakable at the top of a page the
   visitor has usually just arrived at from a Google Maps listing that already
   said the name. Below this width the two-line name and a 42px mark and a
   44px tap target stop fitting without the name shrinking to unreadable. */
@media (max-width: 319px) {
  .wordmark__name {
    display: none;
  }
}
/* The logo is artwork, not a photograph, so it must not inherit the photo
   frame: .media paints a cream tile behind its contents and forces a 4/3 box
   for the reserved-space trick. Against the cream header that tile was simply
   invisible; the moment the footer turned blue it became a white slab with the
   mark lost inside it. Strip the frame wherever the mark is used. */
.wordmark--logo .media {
  background: none;
  aspect-ratio: auto;
  border-radius: 0;
  overflow: visible;
}
.wordmark--logo .media--vector,
.wordmark--logo img {
  width: auto;
  height: 42px;
  max-width: 200px;
  object-fit: contain;
}
/* The mark carries the blue footer, so it is set at real size there instead
   of at the header's nav-bar height. */
.site-footer .wordmark--logo img {
  height: clamp(62px, 9vw, 84px);
  max-width: none;
}
.media--vector {
  aspect-ratio: auto;
  background: none;
}

/* --- Contacto ------------------------------------------------------------- */

.contact-grid {
  display: grid;
  gap: 1rem;
}

.contact-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1.25rem 1.4rem;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
a.contact-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}
.contact-card__icon {
  display: grid;
  place-items: center;
  flex: 0 0 44px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--brand-100);
  color: var(--brand-900);
}
.contact-card__icon svg {
  width: 22px;
  height: 22px;
}
.contact-card__body {
  display: flex;
  flex-direction: column;
}
.contact-card__body strong {
  font-size: 1rem;
}
.contact-card__body span {
  font-size: 0.875rem;
  color: var(--ink-muted);
}
.contact-card--primary .contact-card__icon {
  background: var(--wa);
  color: #fff;
}
.contact-card--pending {
  border: 1px dashed rgb(18 48 92 / 0.25);
  box-shadow: none;
  background: transparent;
}
.contact-card--pending .contact-card__icon {
  background: var(--cream-deep);
  color: var(--ink-muted);
}

/* --- Footer --------------------------------------------------------------- */

.site-footer {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding: clamp(2.5rem, 6vw, 4rem) var(--page-x);
  background: var(--cream-deep);
  border-top: 1px solid rgb(28 26 23 / 0.07);
}
.site-footer__address,
.site-footer__legal {
  font-size: 0.875rem;
  color: var(--ink-muted);
  max-width: 40ch;
}

/* --- 404 ------------------------------------------------------------------ */

.notfound {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 1.1rem;
  min-height: 100vh;
  min-height: 100svh;
  padding: var(--section-y) var(--page-x);
  max-width: 46rem;
  margin-inline: auto;
}
.notfound h1 {
  font-size: clamp(2rem, 7vw, 3.25rem);
}
.notfound__text {
  color: var(--ink-soft);
  max-width: 40ch;
}
.notfound .btn {
  margin-top: 0.8rem;
}

/* --- Floating WhatsApp ---------------------------------------------------- */

.fab {
  position: fixed;
  right: clamp(1rem, 4vw, 2rem);
  bottom: clamp(1rem, 4vw, 2rem);
  z-index: 50;
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--wa);
  color: #fff;
  box-shadow: 0 6px 20px rgb(15 125 58 / 0.36);
  transition: transform 0.2s ease, background-color 0.2s ease;
}
.fab svg {
  width: 30px;
  height: 30px;
}
.fab:hover {
  transform: scale(1.06);
  background: var(--wa-hover);
}

/* --- Scroll reveal -------------------------------------------------------- */

/* Content is visible by DEFAULT. The hidden starting state applies only under
   .js-reveal, which a tiny inline script in <head> adds after confirming
   IntersectionObserver exists. Written this way round so that no-JS, an old
   browser, or a failed script all leave the page fully readable — the opposite
   arrangement hides the entire site when anything goes wrong. */
.js-reveal .reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: opacity, transform;
}
.js-reveal .reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* --- Breakpoints ---------------------------------------------------------- */

@media (min-width: 560px) {
  /* Room to breathe again: the compact phone type was a constraint, not a
     preference, so it is undone as soon as the cards are wide enough. */
  .grid {
    gap: clamp(1.25rem, 3vw, 2rem);
  }
  .card__body {
    padding: 1.4rem 1.5rem 1.75rem;
  }
  .card__name {
    font-size: 1.4rem;
    margin-bottom: 0.6rem;
  }
  .card__text {
    font-size: 0.9375rem;
    line-height: 1.7;
  }
  .js-sheet .card__text {
    -webkit-line-clamp: 4;
  }
  [data-card-style='matted'] .card .media {
    margin: 0.75rem 0.75rem 0;
    border-radius: calc(var(--radius-lg) - 0.6rem);
  }
  [data-card-style='arch'] .card .media {
    margin: 0.85rem 0.85rem 0;
  }
  .contact-grid {
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  }
}

@media (min-width: 900px) {
  .atmosfera .media {
    aspect-ratio: 21 / 9;
  }
  .site-nav {
    display: flex;
  }
  .hero {
    grid-template-columns: 1fr 1fr;
    align-items: center;
  }
  /* Photograph beside the steps, not above them: the four steps are one
     sequence and reading them straight down is the point. */
  .proceso__layout {
    grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.25fr);
    align-items: center;
  }
  .proceso__shot {
    justify-self: start;
  }
  .proceso__steps {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: clamp(1.5rem, 3vw, 2.5rem);
  }
  .ubicacion__grid {
    grid-template-columns: 0.85fr 1.15fr;
    align-items: stretch;
  }
  .map,
  .map__facade,
  .map iframe {
    min-height: 420px;
  }
}

@media (min-width: 1080px) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }
  /* The page column, applied to whatever a section happens to contain rather
     than to a hand-maintained list of block names.

     It WAS such a list, and it broke twice in one sitting: once when the
     drinks markup was renamed (.bebidas__grid kept its entry and the new grid
     had none, so it ran 1745px wide inside a 1240px page), and again when the
     drinks moved inside the menu. Both times the symptom was a block silently
     escaping the column at wide viewports, which is invisible until someone
     opens the site on a big screen.

     A structural selector cannot fall out of date. The dialog is excluded
     because it is a modal the UA centres itself. */
  .proceso__inner,
  .section > :not(dialog) {
    max-width: 1240px;
    margin-inline: auto;
  }
}

/* --- Reduced motion ------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .js-reveal .reveal {
    opacity: 1;
    transform: none;
  }
  /* Variant B's zoom is decorative motion — hold the image still. */
  [data-card-style='bleed'] .card:hover .media img {
    transform: none;
  }
  .sheet[open] {
    animation: none;
  }
}

/* --- Print ---------------------------------------------------------------- */

@media print {
  .site-header,
  .fab,
  .map {
    display: none;
  }
  body {
    background: #fff;
  }
  .js-reveal .reveal {
    opacity: 1;
    transform: none;
  }
}

/* --- Promesas --------------------------------------------------------------
   The three lines GPI uses about itself, between the hero and the menu. A thin
   band on purpose: it is a trust beat, not a section, and giving it a heading
   and a block of padding would make the page claim it is more important than
   the menu underneath it.
   -------------------------------------------------------------------------- */

.promesas {
  padding: 0 var(--page-x) clamp(2rem, 5vw, 3rem);
}
.promesas__list {
  list-style: none;
  margin: 0 auto;
  padding: clamp(1.1rem, 2.5vw, 1.5rem) clamp(1.1rem, 3vw, 2rem);
  max-width: 1240px;
  display: grid;
  gap: 0.9rem clamp(1.5rem, 4vw, 3rem);
  border-block: 1px solid rgb(28 26 23 / 0.09);
}
.promesa {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  font-size: 0.9375rem;
  color: var(--ink-soft);
}
.promesa__icon {
  flex: none;
  width: 1.55rem;
  height: 1.55rem;
  color: var(--brand-700);
}

@media (min-width: 700px) {
  .promesas__list {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    justify-items: center;
  }
}

/* Their own sign-off, from the Facebook Page description. Set in the display
   face because it is a piece of voice, not a piece of information. */
.site-footer__signoff {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.0625rem;
  color: var(--brand-900);
  margin-top: 0.15rem;
}

/* --- Page column -----------------------------------------------------------
   Every section inside `.section` centres its content in a 1240px column. The
   hero, the header and the footer were not doing that: they ran edge to edge
   on their own padding, so at 1920px the h1 began 269px to the LEFT of the
   menu heading underneath it. Nothing looks more like an unfinished template
   than a headline that lines up with nothing.

   max() rather than a max-width + auto margins, because the header and footer
   backgrounds have to keep spanning the full window while their contents pull
   into the column.
   -------------------------------------------------------------------------- */

.site-header,
.site-footer,
.hero {
  padding-inline: max(var(--page-x), calc((100% - 1240px) / 2));
}

/* ===========================================================================
   BRAND LAYER
   ---------------------------------------------------------------------------
   Everything above this point is a well-mannered restaurant page that happens
   to be blue. This section is what makes it GPI's.

   Their identity is loud and specific: type set on rough brush-stroke banners,
   a bold face against a bouncy marker script, prices in painted blobs, big
   fields of one very saturated blue, and a line-icon strip naming the four
   things they sell. It is on every poster they publish. A site using their
   colour but none of their voice reads as a template with their logo dropped
   in — which is exactly what this was.

   The brush shapes are inline SVG data URIs rather than files: each is a
   single path of about ten points, they stretch to whatever box they sit in
   (preserveAspectRatio='none'), and keeping them in the stylesheet means the
   banner cannot render a frame late or 404 into a bare rectangle.
   =========================================================================== */

/* The cold half of the headline, in their script. */
.hero__script {
  font-family: var(--font-script);
  font-style: normal;
  font-weight: 400;
  color: var(--brand-700);
  /* The script's own sidebearings are generous and its baseline sits high;
     these pull it back into optical alignment with the serif line above. */
  font-size: 1.02em;
  line-height: 1.35;
  display: inline-block;
  margin-left: -0.04em;
}

/* --- Brush-stroke eyebrow --------------------------------------------------
   Their section labels sit on a painted banner, not on the page. Two fills:
   blue with white type on the light sections, cream with blue type on the
   dark band. Both pairs are in the contrast table check.mjs enforces.
   -------------------------------------------------------------------------- */

.eyebrow {
  display: inline-block;
  padding: 0.42rem 1.05rem 0.5rem;
  color: #fff;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 40' preserveAspectRatio='none'%3E%3Cpath d='M2 9C30 4 70 7 104 5 140 3 170 8 197 6L198 33C165 36 130 31 96 34 62 37 30 33 3 35Z' fill='%230c1162'/%3E%3C/svg%3E");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}
.section--proceso .eyebrow {
  color: var(--brand-900);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 40' preserveAspectRatio='none'%3E%3Cpath d='M2 9C30 4 70 7 104 5 140 3 170 8 197 6L198 33C165 36 130 31 96 34 62 37 30 33 3 35Z' fill='%23fdfaf4'/%3E%3C/svg%3E");
}

/* --- Price in a painted blob -----------------------------------------------
   Straight off their menu board, where every price sits in a rough blue
   circle. It also does real work: on a card the price stops competing with
   the product name for the same typographic register and becomes a thing you
   can find by shape.
   -------------------------------------------------------------------------- */

.card__price {
  color: #fff;
  padding: 0.34rem 0.82rem 0.42rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 40' preserveAspectRatio='none'%3E%3Cpath d='M100 3C152 3 197 9 196 20 195 32 148 37 99 36 49 35 5 30 4 19 3 9 48 3 100 3Z' fill='%230c1162'/%3E%3C/svg%3E");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

/* --- The four categories ---------------------------------------------------
   GELATO · PANINI · CAFÉ · FRAPPÉS, with line icons, is a strip GPI put at the
   foot of nearly every post in the reopening campaign. Reusing it here closes
   the page the way they close a poster.
   -------------------------------------------------------------------------- */

.categorias {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(1.4rem, 5vw, 3.25rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.categoria {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgb(251 247 241 / 0.86);
}
.categoria svg {
  width: 1.9rem;
  height: 1.9rem;
  color: var(--brand-300);
}

/* --- Footer, on their blue -------------------------------------------------
   GPI end on blue — it is how every post signs off. A cream footer was the
   last place the page stopped looking like theirs.
   -------------------------------------------------------------------------- */

.site-footer {
  background: var(--brand-900);
  border-top: 0;
  align-items: center;
  text-align: center;
  gap: 1.35rem;
  padding-block: clamp(3rem, 7vw, 4.5rem);
}
.site-footer .wordmark__gpi {
  color: #fff;
}
.site-footer .wordmark__full,
.site-footer__address {
  color: rgb(251 247 241 / 0.8);
}
.site-footer__legal {
  color: rgb(251 247 241 / 0.62);
}
/* The mark is one flat blue, so knocking it to white is a two-filter job on
   the alpha channel — no second asset, and it cannot drift from the logo the
   header uses. brightness(0) flattens every visible pixel to black, invert(1)
   turns them white; transparency is untouched by both. */
.site-footer .wordmark--logo img {
  filter: brightness(0) invert(1);
  opacity: 0.96;
}
.site-footer__signoff {
  font-family: var(--font-script);
  font-size: clamp(1.35rem, 3.5vw, 1.85rem);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  margin: 0;
}
/* The outline heart they draw beside this line on the posts themselves. */
.site-footer__signoff::after {
  content: '';
  width: 1.05em;
  height: 1.05em;
  flex: none;
  background: rgb(251 247 241 / 0.85);
  -webkit-mask: var(--heart) center / contain no-repeat;
  mask: var(--heart) center / contain no-repeat;
}

/* The little outline heart GPI draw beside their sign-offs. Declared here
   rather than up in the token block because it is decoration for exactly one
   rule; custom properties resolve at computed-value time, so where it sits in
   the file does not matter. */
:root {
  --heart: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 20.4S4.9 16 2.9 11.8C1.2 8.2 3.1 4.8 6.5 4.8c1.9 0 3.3 1 4.5 2.5 1.2-1.5 2.6-2.5 4.5-2.5 3.4 0 5.3 3.4 3.6 7-2 4.2-9.1 8.6-9.1 8.6Z' fill='none' stroke='%23000' stroke-width='1.7' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* --- Drinks as cards -------------------------------------------------------
   The drinks used to be a bare price list. It read well and it hid them
   completely — the client looked at the finished page and asked whether the
   menu really was only three things. On a menu a photograph is what makes a
   line item register as something orderable, so each group now leads with one.
   -------------------------------------------------------------------------- */

.drink-grid {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2rem);
}
.drink-card {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.75rem);
  align-items: center;
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: clamp(1rem, 2.5vw, 1.35rem);
  box-shadow: var(--shadow-sm);
}
.drink-card .media {
  border-radius: var(--radius);
}
.drink-card__name {
  font-size: clamp(1.25rem, 3vw, 1.5rem);
  margin-bottom: 0.75rem;
}
.drink-card__note {
  margin-top: 0.9rem;
  font-size: 0.8125rem;
  color: var(--ink-muted);
}

@media (min-width: 620px) {
  .drink-card {
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  }
}
@media (min-width: 1080px) {
  .drink-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
  /* Side by side the two cards are narrow, so the photo goes back on top. */
  .drink-card {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* --- Drinks, inside the menu -----------------------------------------------
   The drinks used to be their own section, below the process band. Someone
   reading "El menú" therefore saw three cards and reasonably concluded the
   menu was three items — the drinks were four screens further down under a
   different heading. A menu that does not contain the drinks is not the menu,
   so they are a subsection of it now, with a heading one step down from the
   section title to say so.
   -------------------------------------------------------------------------- */

.menu__drinks {
  margin-top: clamp(3rem, 7vw, 5rem);
  scroll-margin-top: 5rem; /* the header is sticky; #bebidas must clear it */
}
.section__head--sub {
  margin-bottom: clamp(1.5rem, 3.5vw, 2.25rem);
}
.section__title--sub {
  font-size: clamp(1.6rem, 4vw, 2.3rem);
}

/* --- Didone tuning ---------------------------------------------------------
   Bodoni is not a drop-in for the soft serif that was here. Two things change.

   Tracking: a Didone is drawn with more sidebearing than a low-contrast serif,
   so the -0.015em that kept Fraunces from feeling loose closes Bodoni up until
   the hairlines touch. It wants roughly neutral.

   Weight at small sizes: the hairlines are the whole character of the face and
   they are also the first thing to disappear — against blue in a price blob, or
   against the dark band in a step number, 400 drops out on a low-DPI screen.
   Anywhere the display face is used below about 1.2rem it goes up a weight.
   -------------------------------------------------------------------------- */

h1,
h2,
h3 {
  letter-spacing: -0.004em;
}
.hero__title {
  letter-spacing: -0.012em;
}
.card__price,
.pricelist__price,
.step__n {
  font-weight: 400;
}
/* No optical sizing here any more, and no weight above 400.

   Both were for the variable Bodoni this site used to set its headings in.
   Chonburi is static and single-weight, so an opsz declaration would be inert
   and a font-weight above 400 would be SYNTHESISED by the browser — faux bold,
   which smears the strokes this whole change exists to sharpen. The weight is
   in the drawing of the face, not in an axis. */

/* --- Phone fixes -----------------------------------------------------------
   Most of this site is read on a phone, from the Google Maps listing, so the
   narrow layout is the one that has to be right.
   -------------------------------------------------------------------------- */

@media (max-width: 559px) {
  /* Name and price share a baseline on a card wide enough to hold both. At
     two-up on a 375px screen a card is 163px, the price blob takes 59 of them,
     and the name was being squeezed into 61px — "GPI Roll de Canela" broke
     across THREE lines, and the two never even landed on the same baseline, so
     the row bought nothing and cost legibility. Below this width they stack
     and the name gets the whole card. */
  .card__head {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.4rem;
  }
}

/* The brand link in the header wrapped the logo tightly, giving a 39x42 tap
   target — under the 44px minimum, and it is the control people hit to get
   back to the top. Padding rather than a bigger logo, so nothing moves. */
.site-header__brand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
}

/* --- Blog ------------------------------------------------------------------
   /blog/ and /blog/<slug>/.

   An article is a different job from the home page: the home page is scanned
   in a few seconds by someone deciding whether to walk over, an article is
   read. So this is the one part of the site with a real reading column — about
   66 characters — and the only place the body text is allowed to run long.

   Everything else is borrowed: the same cream ground, the same Didone
   headings, the same brush-stroke eyebrow, the same blue footer. A post
   should read as a page of this site, not as a blog someone attached to it.
   -------------------------------------------------------------------------- */

.post {
  padding: clamp(2.5rem, 7vw, 5rem) var(--page-x) var(--section-y);
  max-width: 46rem;
  margin-inline: auto;
}

/* The trail back to the shop. Small, muted, and above the headline rather
   than below it, because on a phone it is the first thing under the header
   and doubles as the answer to "where am I". */
.crumbs {
  font-size: 0.8125rem;
  color: var(--ink-muted);
  margin-bottom: 1.1rem;
}

/* The hit box, not the type, is what grows here.

   These rendered 33x17 and 27x17 — well under the 44px minimum, on the one
   control that takes a reader back out of an article. Padding grows the box
   and an equal negative margin gives the space straight back, so the crumbs
   look exactly as before and the target is 44px tall.

   The rule underneath had to stop being a border for that to work: a border
   sits on the padding box and would have drifted 14px below the words. A text
   underline stays glued to the text no matter how big the box gets. */
.crumbs a {
  display: inline-block;
  padding: 0.85rem 0.55rem;
  margin: -0.85rem -0.55rem;
  color: var(--brand-700);
  text-decoration: underline;
  text-decoration-color: rgb(18 25 150 / 0.3);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.crumbs a:hover {
  text-decoration-color: currentColor;
}

.crumbs span {
  margin: 0 0.45rem;
  color: var(--beige);
}

.post__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.85rem, 5.5vw, 3rem);
  line-height: 1.1;
  letter-spacing: -0.01em;
  text-wrap: balance;
  color: var(--brand-900);
}

.post__meta {
  margin-top: 0.9rem;
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.post__hero {
  margin: clamp(1.75rem, 5vw, 2.75rem) 0 0;
}

.post__hero .media {
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow);
}

/* The article body. Sibling spacing lives on the container so a block can be
   added or reordered in blog.js without anyone having to remember which
   element carries the margin. */
.post__body {
  margin-top: clamp(2rem, 5vw, 3rem);
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

/* Direct children only. A descendant selector here (.post__body p) is 0-1-1
   and quietly outranks any component class inside the article: it captured
   .post__ctatext and painted the call to action ink-soft on brand blue, about
   1.9:1 and unreadable. Scoping to > keeps this rule on the article prose and
   lets anything nested keep its own colours. */
.post__body > p {
  font-size: 1.0625rem;
  line-height: 1.75;
  color: var(--ink-soft);
  max-width: 66ch;
}

.post__body > h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.35rem, 3.5vw, 1.75rem);
  line-height: 1.2;
  color: var(--brand-900);
  margin-top: clamp(1.25rem, 3vw, 2rem);
  text-wrap: balance;
}

/* GPI's four steps, numbered by the list itself. The counter is drawn rather
   than left to the UA marker so it can carry the brand blue and sit on the
   same baseline as the step title on every browser. */
.post__steps {
  list-style: none;
  counter-reset: paso;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin: 0.5rem 0;
  padding: 0;
}

.post__steps li {
  counter-increment: paso;
  position: relative;
  padding-left: 3rem;
  font-size: 1.0625rem;
  line-height: 1.7;
  color: var(--ink-soft);
  max-width: 64ch;
}

.post__steps li::before {
  content: counter(paso);
  position: absolute;
  left: 0;
  top: 0.1rem;
  width: 2.05rem;
  height: 2.05rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--brand-900);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 0.95rem;
}

.post__steps strong {
  color: var(--brand-900);
  font-weight: 600;
}

/* The four gelato flavours as chips — the same shape the menu uses, so the
   reader recognises them as the actual options rather than examples. */
.post__flavors {
  list-style: none;
  padding: 0;
  margin: 0.25rem 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.post__flavors li {
  padding: 0.42rem 0.95rem;
  border-radius: 999px;
  background: var(--cream-deep);
  border: 1px solid var(--beige);
  font-size: 0.9375rem;
  color: var(--ink);
}

/* The end of every article: where the shop is, and one way in. Blue, because
   it is the one block on the page that is asking for something. */
.post__cta {
  margin-top: clamp(1.5rem, 4vw, 2.5rem);
  padding: clamp(1.5rem, 4vw, 2.25rem);
  border-radius: var(--radius-lg);
  background: var(--brand-900);
  color: #fff;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.15rem;
}

.post__ctatext {
  margin: 0;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--brand-100);
  max-width: 40ch;
}

/* The default primary button is brand blue on cream; on the blue card it
   would vanish, so it flips to cream on blue. */
.post__cta .btn--primary {
  background: var(--cream);
  color: var(--brand-900);
}

.post__cta .btn--primary:hover {
  background: #fff;
}

/* --- The blog index ------------------------------------------------------- */

/* auto-fit rather than a fixed column count, because the count of posts is
   not fixed. Two posts filled a 2-column row exactly; the third left one card
   stranded alone in row two, and hard-coding 3 would only move the problem to
   the fourth post. The track floor is min() so a card can never be wider than
   the column it sits in on a phone. */
.postgrid {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
}

.postcard {
  background: var(--surface);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}

.postcard:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow);
}

/* The whole card is the link, so the target is the card and not a "Leer más"
   the size of a fingernail. */
.postcard__link {
  display: flex;
  flex-direction: column;
  height: 100%;
  color: inherit;
  text-decoration: none;
}

.postcard__body {
  padding: clamp(1.15rem, 3vw, 1.6rem);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  flex: 1;
}

.postcard__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.2rem, 3vw, 1.5rem);
  line-height: 1.2;
  color: var(--brand-900);
  text-wrap: balance;
}

.postcard__excerpt {
  font-size: 0.9688rem;
  line-height: 1.65;
  color: var(--ink-muted);
}

.postcard__more {
  margin-top: auto;
  padding-top: 0.35rem;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--brand-700);
}

.postcard__link:hover .postcard__more {
  color: var(--brand-500);
}

/* The locator strip, between the hero and the first paragraph.

   Someone who arrives from a search for "helados en Morelia" is asking two
   questions at once — what is this, and where is it. The article answers the
   first over several screens; this answers the second before any of them. */
.post__place {
  display: flex;
  /* Top, not centre: the address wraps to three lines on a phone and a centred
     pin ends up floating beside the middle line, pointing at nothing. */
  align-items: flex-start;
  gap: 0.7rem;
  margin-top: clamp(1rem, 3vw, 1.4rem);
  padding: 0.85rem 1.1rem;
  border-radius: var(--radius);
  background: var(--cream-deep);
  border: 1px solid var(--beige);
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--ink-soft);
}

.post__pin {
  flex: none;
  margin-top: 0.1rem;
  width: 1.35rem;
  height: 1.35rem;
  color: var(--brand-700);
}

.post__pin svg {
  width: 100%;
  height: 100%;
  display: block;
}

.post__place a {
  color: var(--brand-700);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid rgb(18 25 150 / 0.3);
  white-space: nowrap;
}

.post__place a:hover {
  border-bottom-color: currentColor;
}

/* The board, inside an article. Not the home page's cards — those carry
   photographs and a price blob and would out-shout the prose around them.
   Here it is a list with the price set apart, and nothing else. */
.post__menu {
  list-style: none;
  padding: 0;
  margin: 0.25rem 0;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.post__menu li {
  padding: 1rem 1.15rem;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--beige);
}

.post__menuhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 0 0 0.3rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.0625rem;
  color: var(--brand-900);
}

.post__price {
  flex: none;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--brand-700);
}

.post__menutext {
  margin: 0;
  font-size: 0.9688rem;
  line-height: 1.6;
  color: var(--ink-muted);
}

/* On a 375px screen the name and the price were being squeezed onto one line
   the same way the menu cards were — same fix, same reason. */
@media (max-width: 559px) {
  .post__menuhead {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.2rem;
  }
}

/* --- The crumb ------------------------------------------------------------
   A light bread texture on the page ground, asked for by the client.

   Procedural, not a photograph: one SVG turbulence tile as a data URI. No
   request, nothing to download, and it cannot go soft on a large screen the
   way a scanned crumb would. Six octaves from a low base frequency give the
   broad unevenness of a crust and the fine grain of crumb in one pass.

   The tile carries the finished colour rather than being a grey veil laid over
   one. That matters: the first version left feTurbulence's own alpha in place
   (8-117 of 255, so the tile was mostly transparent) and tried to control the
   strength with a 5.5% opacity on top. Measured at the pixel, that reached the
   screen as a swing of about 1.8 levels out of 255 — not subtle, absent, and
   it was reported as flat colour. Here feFuncA pins alpha to 1 and the three
   colour ramps map the noise into a band centred exactly on --cream, so the
   mean is the cream it replaces and the swing is a chosen number: +-11 levels.

   The ramp is computed, not guessed, and the three numbers below were each
   arrived at by measuring rather than by eye:

   - color-interpolation-filters=sRGB. Without it the filter runs in linearRGB
     and the same slope lands about four times too flat.
   - slope 0.226. The turbulence was sampled (p10 97, p90 159 of 255, mean 128)
     and the slope solved from it for a 14-level swing: 14/62. An earlier guess
     of 0.09 produced a 4-level swing, which is invisible, and 0.36 produced 22
     and read as cloud rather than crumb.
   - baseFrequency 0.6 with four octaves, so every octave (0.6 -> 4.8) is fine
     grain. A low base frequency put ~50px blobs on the page and made the 400px
     tile's seams visible; this is crumb, and the repeat cannot be seen.

   Sitting behind everything at z-index -1 uses the browser's own painting
   order — a negative-index child paints above its parent's background and
   below every in-flow block — so nothing else on the page needs a z-index to
   stay clear of it. An earlier attempt lifted the header and the floating
   button instead and silently killed their position:sticky and position:fixed.
   -------------------------------------------------------------------------- */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--cream);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.6' numOctaves='4' seed='9' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.226' intercept='0.8785'/%3E%3CfeFuncG type='linear' slope='0.226' intercept='0.8665'/%3E%3CfeFuncB type='linear' slope='0.226' intercept='0.8435'/%3E%3CfeFuncA type='discrete' tableValues='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23b)'/%3E%3C/svg%3E");
  background-size: 240px 240px;
}
}
