/* ---------- Landscape kinetic typography ----------
   Full-bleed 16:9 interactive block on the MSP project page, right after
   the Yes/World video row and right before the full-bleed picture.
   Complete rebuild (the previous tiled-word version was scrapped) — see
   "../jac-identity/Diseño Kinético e Interactivo.md" for the general
   technique (lives outside this repo: it's a reusable guide for any
   kinetic/interactive block, not specific to this one).

   The word "LANDSCAPES" repeats once or more per row (the "Columns"
   slider controls how many copies share a row); each row gets its own
   per-letter gap (a tracking value, not CSS `letter-spacing` — every
   letter is its own positioned <span> since the same letters are also
   pushed around by the pointer) so the row's total width matches the
   chord of a circle at that row's height. Stacked, the rows trace a
   circle/globe silhouette.

   Two motions run at once on every letter, composited in
   js/landscape-playground.js:
   1. A continuous scroll: every row-slot rises (or, with the direction
      icon flipped, falls) and wraps seamlessly off one edge back onto
      the other (a conveyor, not a fixed grid). A row's tracking is a
      pure function of its current height on the circle, so a row is
      tight entering, widens as it passes the centre, narrows again and
      exits tight — no separate timer driving that, it falls straight
      out of the scroll position. No opacity or visibility trick either:
      a row's *position* simply travels a full glyph height past this
      container's own edge before it wraps, so this element's own
      overflow:hidden is what hides it, naturally, as it slides out.
   2. A spring-based pointer push, always repulsive — letters within
      range lean away from the pointer and ease back with real inertia:
      a persisted per-letter offset lerped toward its target every
      frame, not recomputed from scratch each frame.

   "Rows" and "Columns" are both capped at runtime (computeLimits() in
   the JS), not just by the slider's own min/max: rows can't get so
   dense the letters would visually touch, and columns can't get so wide
   that the pole rows stop reading as narrower than the centre row (the
   circle flattening into a rectangle) — see MAX_POLE_WIDTH_RATIO.

   Only 4 variables are exposed (scroll speed/letter size/rows/columns)
   plus the direction and colour-swap icons — cursor radius, spring
   return and push strength used to be sliders too but the client found
   that too much, so they're now fixed constants (FIXED_RADIUS/
   FIXED_SPRING/FIXED_PUSH in the JS), and "attract" mode was cut
   entirely (the push is always repulsive now).

   Control panel (`.lp-panel`): piece shapes/spacing follow the studio's
   "Piezas" system (5pt radius, no borders, tone instead of line to
   separate neighbours) — see jac-identity's "Sistema de diseño
   Piezas.md" — but its *colours* follow the field's own current
   background, not this site's light/dark toggle: light panel (dark ink)
   when the field is dark, dark panel (light ink) when the field is
   light — always the inverse of --lp-bg, same relationship the
   colour-swap icon's own button has to it. Set by JS (applyColors(),
   PARAM_ICON_SVG's neighbours PANEL_TOKENS_LIGHT/DARK), not CSS — see
   `.lp-panel` below. Its top/right offset is var(--grid-margin) — the
   same edge margin the Work section's own content uses (css/work.css),
   so this block's panel lines up with the rest of the site instead of
   using its own arbitrary inset.

   One icon per variable (`.lp-icon-row`, true squares with their own
   hover state, hand-drawn monochrome line glyphs — not literal Apple SF
   Symbols, which aren't licensed for arbitrary web embedding, but drawn
   in that spirit: thin uniform stroke, one colour); clicking one reveals
   a single shared slider underneath (`.lp-reveal`, a grid-rows mask-grow
   matching the system's "despliegue" cascade curve/duration). Hovering
   any icon grows a name tag to its left in the panel's accent colour
   (`.lp-icon-tooltip`, clip-path growing right-to-left, anchored where
   it meets the icon row). The colour-swap icon animates the same way as
   the site's own light/dark toggle (js/theme.js) — a circle grows to
   cover it in the old colours, then clears to reveal the dot already
   showing the new ones (js's triggerColorSwap()). Changing a slider (or
   the word field) never replants the whole field — see
   buildStructure()'s `hasMounted` guard in the JS: only the very first
   build gets the springy entrance, every live edit after that hands new
   letters straight to the running animation at their correct in-motion
   position. */

.landscape-playground {
  position: relative;
  margin-top: var(--grid-gutter);
  aspect-ratio: 16 / 9;
  width: 100%;
  overflow: hidden;
  --lp-bg: #3d362b;
  --lp-fg: #e5e4db;
  background: var(--lp-bg);
}

/* 100px above and below this block on desktop: 90px here (matching
   .project-media's own padding-bottom above it) plus the normal 10px
   grid-gutter margin each neighbour already contributes (this block's own
   margin-top, and .project-media-picture's margin-top below). Stacked/
   mobile keeps the normal 10px both sides. */
@media (min-width: 1181px) {
  .landscape-playground {
    margin-bottom: 90px;
  }
}

.landscape-field {
  position: relative;
  width: 100%;
  height: 100%;
  cursor: pointer;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  font-family: var(--font-body);
  font-weight: 700;
  color: var(--lp-fg);
}

.landscape-field.is-dragging {
  cursor: grabbing;
}

/* Every letter is placed by JS as an absolute offset from the field's own
   centre (`translate(-50%, -50%) translate(x, y) ...`), so it can carry a
   circle-layout position and a spring push at once without ever touching
   layout (no reflow — everything is a transform). */
.lp-letter {
  position: absolute;
  left: 50%;
  top: 50%;
  display: inline-block;
  will-change: transform;
  /* Rows that reach the circle's edge on phones/tablets fade by opacity
     (js/landscape-playground.js, applyFade) — no mask over the bars. */
  transition: opacity 0.12s linear;
}

/* Entrance only: every letter starts scaled down/offset/transparent, then
   the JS flips it to -active on the next frame, staggered by --lp-delay.
   The rAF loop (js) explicitly clears `transition` before it starts
   writing `transform` every frame, so this transition never fights the
   per-frame updates once the field is alive. */
.lp-letter.lp-enter {
  opacity: 0;
}

.lp-letter.lp-enter-active {
  opacity: 1;
  transition:
    opacity 0.5s cubic-bezier(0.34, 1.56, 0.64, 1),
    transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
  transition-delay: var(--lp-delay, 0s);
}

/* ---------- Control panel ----------
   A stack of "piezas": zero gap between them, each with its own 5pt
   corners, tone doing the separating instead of a border. Every rule
   that reads one of the colour tokens below also declares its own
   background-color/color transition (never a blanket one), so the panel
   fades smoothly whenever those tokens change instead of snapping.
   --lp-glass/--lp-ink/etc. are NOT declared here — they're set by JS
   (applyColors(), PANEL_TOKENS_LIGHT/DARK) on .landscape-playground
   itself and inherited down. The panel's colours follow the *field's*
   current background (light panel on a dark field, dark panel on a
   light field — always the inverse, same idea as the colour-swap icon
   itself), not this site's own light/dark toggle: a client correction
   after the panel first shipped tied to [data-theme="dark"] instead. */
.lp-panel {
  --lp-btn-h: 38px;
  position: absolute;
  top: var(--grid-margin);
  right: var(--grid-margin);
  z-index: 2;
  /* 7 icons × 38px now (was 6/228px before the ripple icon). */
  width: 266px;
  display: flex;
  flex-direction: column;
}

.lp-piece {
  border-radius: 5px;
  background: var(--lp-glass);
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  transition: background-color 0.3s ease;
}

/* The word field: the system's underlined form field pattern (no box, no
   label — the placeholder itself is the instruction, so the field never
   reads as a fixed title). */
/* Its own lift tone (not just the bare --lp-glass the icon row sits on)
   so the word field reads as a distinct piece from the controls below it
   — "tonos distintos entre piezas vecinas" per the Piezas system. */
.lp-word-row {
  background: linear-gradient(var(--lp-lift-14), var(--lp-lift-14)), var(--lp-glass);
  /* Same height as the icon buttons under it on every device (client
     request): --lp-btn-h is 38px, 32px at ≤700px (see the bottom of the file). */
  box-sizing: border-box;
  height: var(--lp-btn-h);
  padding: 0 16px;
  display: flex;
  align-items: center;
}

.lp-word-row input {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-family: inherit;
  /* Same floor as the smallest *readable* size used elsewhere on the
     site (nav/footer/contact-form pill text, css/nav.css & co.), not the
     12px .contact-form-step-label ever gets down to — this was 13px, the
     tooltip below was 12px; both are unified here so nothing in this
     panel reads smaller than the rest of the site. */
  font-size: 14px;
  color: var(--lp-ink);
  /* No underline (client request): the text is simply centred in the row,
     which is as tall as the icon buttons under it. */
  line-height: 1;
  padding: 0;
  transition: color 0.3s ease;
}

.lp-word-row input::placeholder {
  color: var(--lp-ink-dim);
  transition: color 0.3s ease;
}

/* Icon line: the hover name-tag sits to the left of the icon row itself,
   absolutely positioned so growing it never changes the panel's own
   width — see .lp-icon-tooltip below. */
.lp-icon-line {
  position: relative;
}

/* top+bottom (not a fixed height) so this same tooltip works both
   anchored to the 38px-tall icon row (.lp-icon-line) and to a ripple
   mini-slider row (.lp-mini-slider-row, a different height) — it just
   stretches to whichever positioned ancestor it's actually inside. */
.lp-icon-tooltip {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 100%;
  display: flex;
  align-items: center;
  padding: 0 14px;
  white-space: nowrap;
  border-radius: 5px;
  background: var(--lp-glass);
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  color: var(--lp-ink);
  font-size: 14px;
  font-weight: 500;
  pointer-events: none;
  clip-path: inset(0 0 0 100%);
  transition:
    clip-path 0.25s cubic-bezier(0.3, 0, 0.2, 1),
    background-color 0.3s ease,
    color 0.3s ease;
}

.lp-icon-tooltip.is-visible {
  clip-path: inset(0 0 0 0%);
}

/* Icon row: true squares (38×38, not flex-stretched), each with its own
   hover lift distinct from its active tone. */
.lp-icon-row {
  display: flex;
  width: 266px; /* 7 icons × 38px */
  /* Clips every icon to the row's own 5pt corners (inherited from
     .lp-piece) — needed for .lp-color-btn specifically, whose solid fill
     would otherwise show square corners poking past the row's rounded
     ones at its right end. */
  overflow: hidden;
}

.lp-icon-btn {
  position: relative;
  flex: 0 0 38px;
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--lp-ink-dim);
  cursor: pointer;
  overflow: hidden;
  transition:
    background-color 0.15s ease-in-out,
    color 0.15s ease-in-out;
}

.lp-icon-btn svg {
  width: 18px;
  height: 18px;
  display: block;
}

.lp-icon-btn:hover {
  background: var(--lp-lift-5);
  color: var(--lp-ink);
}

.lp-icon-btn.is-active {
  background: var(--lp-lift-14);
  color: var(--lp-ink);
}

/* Colour swap icon — a miniature of this site's own light/dark toggle
   (.nav-theme-toggle, css/nav.css), reading the field's own two colours
   instead of the site's: the button itself is the *inverse* of the
   field's current background (so it never blends into it — the same
   "inverted" relationship .nav-toggle-bg-inverted has to --color-bg),
   the dot matches the field's actual current background, and clicking
   swaps which is which (js's triggerColorSwap() drives the same
   wipe-then-dot-grows-in animation as js/theme.js). No background-color
   transition here on purpose — the click animation is the only thing
   that's ever supposed to move this button's colours; a CSS transition
   left in would replay every time the frozen inline colour is cleared at
   the end of that animation, reading as a flash of the old colour. */
.lp-color-btn,
.lp-color-btn:hover {
  /* :hover repeated (not just relying on .lp-icon-btn:hover's lower
     specificity) so the generic icon hover-lift never dims this one —
     it has its own solid fill, that never changes on hover. */
  background: var(--lp-fg);
  /* .lp-icon-btn's own `transition: background-color ...` still applies
     here too (same element, both classes) unless explicitly cleared —
     it was the actual cause of the end-of-click flash, not the
     .lp-color-btn-only transition removed earlier: this button's colour
     is fully driven by triggerColorSwap()'s own animation, never by
     this property transitioning. */
  transition: color 0.15s ease-in-out;
}

.lp-color-dot,
.lp-color-wipe {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  background: var(--lp-bg);
}

.lp-color-dot {
  z-index: 2;
  transition: transform 0.15s ease;
}

.lp-color-btn:hover .lp-color-dot {
  transform: scale(1.15);
}

.lp-color-wipe {
  z-index: 1;
  transform: scale(0);
  pointer-events: none;
}

/* The reveal: a mask that grows from its own top edge (the system's
   "despliegue" technique — content is only ever uncovered, never scaled
   or stretched) using the same curve/duration as any panel cascade. */
.lp-reveal {
  position: relative; /* anchors .lp-ripple-tooltip — see there for why it can't live inside .lp-reveal-inner */
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.4s cubic-bezier(0.3, 0, 0.2, 1);
}

.lp-reveal.is-open {
  grid-template-rows: 1fr;
}

.lp-reveal-inner {
  overflow: hidden;
  min-height: 0;
}

/* No label here on purpose — hovering the icon already names the
   variable (.lp-icon-tooltip), so a second title above the slider would
   just repeat it, and its own larger size was the one piece of text in
   this panel that didn't match the rest — removing it also unifies
   every remaining size in the panel to the one 14px used everywhere
   else (word field, tooltip). */
.lp-slider-row {
  padding: 12px 16px;
}

/* Ripple's three sliders (contrast, width, speed) stack inside one
   shared piece — the *group* is the piece (background/radius/blur),
   the rows inside it are plain, so it reads as one button's worth of
   controls, not three separate pieces stacked (no "Piezas" tone split
   between them, since they aren't distinct pieces). */
.lp-ripple-group {
  border-radius: 5px;
  background: var(--lp-glass);
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  transition: background-color 0.3s ease;
}

/* Each mini slider's own name tag, on hover — same .lp-icon-tooltip
   visual as the icon row's, but this one is built and positioned by
   JS as a sibling of .lp-reveal-inner (see buildLabeledSlider()): its
   row lives inside .lp-reveal-inner, which needs overflow:hidden for
   the open/close mask, and that would clip the tooltip's own leftward
   growth. top is set per-instance in JS (one per stacked row); this
   just cancels the base class's top:0/bottom:0 stretch, which was
   sized for the single-row icon tooltip, not a 3-row stack. */
.lp-ripple-tooltip {
  bottom: auto;
  height: 40px;
}

/* Restyled native range input, built on the same idea as the system's
   underlined form field: a resting track that fills with the accent up
   to the thumb — the thumb is a thin line at the same weight as the
   track itself, just taller, not a separate shape. */
.lp-slider-row input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  /* Without this the input stays inline, and its default baseline
     alignment adds an uneven half-leading (governed by the row's
     inherited line-height, taller than the input's own 16px) below it —
     the line reads as sitting higher than centre even though the row's
     own padding (12px/12px) is symmetric. Block layout removes that
     line-box entirely, so the padding alone is what centers it. */
  display: block;
  width: 100%;
  height: 16px;
  margin: 0;
  background: transparent;
  cursor: pointer;
}

/* The track pseudo-element fills the *whole* input height (not a short
   1px box within it) and the 1px line is drawn as a centered background
   image instead — a short track's own vertical position within a taller
   input isn't reliably centered across browsers (it read as sitting
   higher than centre), but a track that fills the input can't be
   anywhere else, and `background-position: center` centers the line
   inside it exactly regardless. */
.lp-slider-row input[type="range"]::-webkit-slider-runnable-track {
  height: 100%;
  background: linear-gradient(var(--lp-track), var(--lp-track)) no-repeat center / 100% 1px;
}

.lp-slider-row input[type="range"]::-moz-range-track {
  height: 100%;
  background: linear-gradient(var(--lp-track), var(--lp-track)) no-repeat center / 100% 1px;
}

.lp-slider-row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 1px;
  height: 12px;
  background: var(--lp-accent);
  /* Centers the thumb on the *track's* box, which is now the input's
     full 16px height (was -5.5px, centering on the old 1px track). */
  margin-top: 2px;
  transition: transform 0.15s ease;
}

.lp-slider-row input[type="range"]::-moz-range-thumb {
  width: 1px;
  height: 12px;
  border: none;
  background: var(--lp-accent);
  transition: transform 0.15s ease;
}

.lp-slider-row input[type="range"]:active::-webkit-slider-thumb {
  transform: scaleX(3);
}

.lp-slider-row input[type="range"]:active::-moz-range-thumb {
  transform: scaleX(3);
}

/* Touch strips over the bars above/below the circle on tall/narrow fields (phones
   and tablets). Fully transparent — nothing to see; they only take the touch
   (the field underneath has touch-action: none), so a swipe over the bars scrolls
   the page. --lp-bar-h is set by js/landscape-playground.js (0 when the circle
   already fills the height, i.e. desktop). */
.lp-bar {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--lp-bar-h, 0px);
  touch-action: auto;
  background: transparent;
  z-index: 1;
}

.lp-bar--top {
  top: 0;
}

.lp-bar--bottom {
  bottom: 0;
}

/* Mobile and tablet: the block is 75vh tall instead of 16:9, and the panel
   sits at the bottom so it doesn't cover the word. */
@media (max-width: 1180px) {
  .landscape-playground {
    aspect-ratio: auto;
  }

  /* Centred at the bottom of the block (not right-aligned), and no
     name-tag on tap: it isn't needed on touch (client request). */
  .lp-panel {
    top: auto;
    bottom: var(--grid-margin);
    left: 0;
    right: 0;
    margin-inline: auto;
  }

  .lp-panel [class*="tooltip"] {
    display: none;
  }
}

@media (max-width: 700px) {
  .lp-panel {
    --lp-btn-h: 32px;
    width: 221px; /* 7 icons × 31.6px */
  }

  .lp-icon-row {
    width: 221px;
  }

  .lp-icon-btn {
    flex-basis: 31.6px;
    width: 31.6px;
    height: 32px;
  }

  .lp-icon-btn svg {
    width: 15px;
    height: 15px;
  }

  .lp-icon-tooltip {
    height: 32px;
  }

  .lp-slider-row {
    padding: 6px 12px 8px;
  }
}

/* Mobile and tablet: everything touchable is exactly 44px (the minimum),
   made of squares: --ps is one button's side. The block is 75vh and the
   field (the letters) stops at the top of the closed panel — bottom margin +
   word row + icon row — so the circle is centred between the top of the block
   and the top of the panel (client request); an open slider opens over the
   lower part of the circle. After the ≤700px block above so it wins over its
   smaller sizes. */
@media (max-width: 1180px) {
  .landscape-playground {
    --ps: 44px;
    --lp-reserve: calc(var(--grid-margin) + 2 * var(--ps));
    height: 75vh;
  }

  .landscape-field {
    height: calc(100% - var(--lp-reserve));
  }

  .lp-bar--bottom {
    bottom: var(--lp-reserve);
  }

  .lp-panel {
    --lp-btn-h: var(--ps);
    width: calc(7 * var(--ps));
  }

  .lp-icon-row {
    width: calc(7 * var(--ps));
  }

  .lp-icon-btn {
    flex-basis: var(--ps);
    width: var(--ps);
    height: var(--ps);
  }

  .lp-icon-btn svg {
    width: calc(var(--ps) * 18 / 38); /* the desktop proportion */
    height: calc(var(--ps) * 18 / 38);
  }

  .lp-slider-row {
    padding: 0 16px;
  }

  .lp-slider-row input[type="range"] {
    height: var(--ps);
  }

  .lp-slider-row input[type="range"]::-webkit-slider-thumb {
    margin-top: calc((var(--ps) - 12px) / 2); /* same 12px line as desktop, centred */
  }
}


/* Mobile and tablet: the controls hide behind a square button, bottom right
   (client request). Closed: only the 44px button (two lines). Open: the lines
   turn into a cross and the panel opens right-to-left from the button's right
   edge (300ms), then, after it, the extra rows open upwards (250ms; the word
   row in MSP). Closing is the reverse, top to bottom: open slider and word row
   fold, then the panel slides back into the button (300ms) with no pause:
   js sets --menu-close-delay to however long the rows that are open take to fold. It
   always sits right above the button, never inside it. */
.lp-menu-toggle {
  display: none;
}

@media (max-width: 1180px) {
  .lp-menu-toggle {
    display: block;
    position: absolute;
    right: var(--grid-margin);
    bottom: var(--grid-margin);
    z-index: 3;
    width: var(--ps);
    height: var(--ps);
    padding: 0;
    border: none;
    border-radius: 5px;
    background: var(--lp-glass);
    backdrop-filter: blur(16px) saturate(1.4);
    -webkit-backdrop-filter: blur(16px) saturate(1.4);
    cursor: pointer;
    transition: background-color 0.3s ease;
  }

  .lp-menu-toggle span {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 20px;
    height: 1.5px;
    margin: -0.75px 0 0 -10px;
    background: var(--lp-ink);
    transition:
      transform 0.3s cubic-bezier(0.3, 0, 0.2, 1),
      background-color 0.3s ease;
  }

  .lp-menu-toggle span:nth-child(1) {
    transform: translateY(-3.5px);
  }

  .lp-menu-toggle span:nth-child(2) {
    transform: translateY(3.5px);
  }

  .landscape-playground.is-menu-open .lp-menu-toggle span:nth-child(1) {
    transform: rotate(45deg);
  }

  .landscape-playground.is-menu-open .lp-menu-toggle span:nth-child(2) {
    transform: rotate(-45deg);
  }

  .lp-panel {
    left: auto;
    right: var(--grid-margin);
    margin-inline: 0;
    bottom: calc(var(--grid-margin) + var(--ps));
    align-items: flex-end;
    overflow: hidden;
    width: 0;
    visibility: hidden;
    transition:
      width 0.3s cubic-bezier(0.3, 0, 0.2, 1) var(--menu-close-delay, 0s),
      visibility 0s linear calc(var(--menu-close-delay, 0s) + 0.3s);
  }

  .landscape-playground.is-menu-open .lp-panel {
    width: calc(7 * var(--ps));
    visibility: visible;
    transition:
      width 0.3s cubic-bezier(0.3, 0, 0.2, 1),
      visibility 0s;
  }

  .lp-panel > * {
    flex: 0 0 auto;
    width: calc(7 * var(--ps));
  }

  .lp-word-row {
    height: 0;
    opacity: 0;
    overflow: hidden;
    transition:
      height 0.25s cubic-bezier(0.3, 0, 0.2, 1),
      opacity 0.25s ease;
  }

  .landscape-playground.is-menu-open .lp-word-row {
    height: var(--ps);
    opacity: 1;
    transition-delay: 0.3s; /* after the horizontal expansion */
  }
}

/* Closed menu = just the button, so the circle is centred above it. */
@media (max-width: 1180px) {
  .landscape-playground {
    --lp-reserve: calc(var(--grid-margin) + var(--ps));
  }
}

/* Menu button top right (client request): the panel opens downwards from it,
   right-aligned, and the circle/logo is centred in the whole block, ignoring the
   menu. The word row stays on top, as on desktop: when it grows it pushes the
   icon row and the sliders down (everything is in the flow), and when it folds
   they come back up. */
@media (max-width: 1180px) {
  .landscape-playground {
    --lp-reserve: 0px;
  }

  .lp-menu-toggle {
    top: var(--grid-margin);
    bottom: auto;
  }

  .lp-panel {
    top: calc(var(--grid-margin) + var(--ps));
    bottom: auto;
  }
}

/* The panel opens to the LEFT of the menu button, in the button's own row
   (client request), so 7 icons + the button = 8 squares must fit between the
   page margins: the side is 44px, a hair less on a narrow phone (350 / 8 at
   390px wide). The sliders open below the icon row; the word row opens above it. */
@media (max-width: 1180px) {
  .landscape-playground {
    --ps: min(44px, calc((100vw - 2 * var(--grid-margin)) / 8));
  }

  .lp-panel {
    top: var(--grid-margin);
    right: calc(var(--grid-margin) + var(--ps));
  }
}

/* Mobile and tablet: 2 × the page's side margin above and below (40px phone,
   50px tablet), client request — the neighbours' own spacing is cancelled in
   css/project.css so this is the whole gap. Desktop keeps its own. */
@media (max-width: 1180px) {
  .landscape-playground {
    margin-top: calc(2 * var(--grid-margin));
    margin-bottom: calc(2 * var(--grid-margin));
  }
}

/* Phones and tablets (bug 029): Safari on iPhone zooms into a text field whose
   font-size is under 16px when it gets focus, and never zooms back out. 16px is the
   smallest size it accepts; desktop keeps 14px. Do NOT block zoom in the viewport. */
@media (max-width: 1180px) {
  .lp-word-row input {
    font-size: 16px;
  }
}
