/*
 * The site's one stylesheet: structure and design together.
 *
 * The colour was corrected from measurement rather than from reading markup:
 * the ground is warm brown-black and the photography is uniformly brass --
 * #af9575, #8f6d4e, #dbbe97 over deep browns at #4c2106 and #210f04.
 *
 * The distinguishing move is that the dark is *brown*, not neutral black or
 * cool charcoal. Everything sits on a warm ground, so brass reads as lit metal
 * rather than as a gold accent stuck on top of grey.
 */

:root {
  /* Brand pink, from an earlier supplied wordmark. Nothing tints the header
     mark any more -- it is a drawn white file (see .wordmark) -- and brass is
     the identity here, so this stays defined for anything that wants the pink
     rather than because the mark uses it. */
  --brand: #d1549f;
  /* A system monospace stack: no webfont, so it survives style-src 'self'. */
  --brand-font: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas,
    "Liberation Mono", monospace;

  --bg: #16120e;
  --surface: #1f1a15;
  --fg: #f2ebe1;
  --muted: #a1907d;
  --line: #3a2f24;
  --accent: #c9a87c;
  --accent-bright: #dbbe97;
  --accent-deep: #8f6d4e;
  --radius: 3px;
  --measure: 62ch;
  --gutter: 2rem;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  /* Layered so the ground is never flat neutral. */
  background:
    radial-gradient(1200px 600px at 50% -20%, #2b2822 0%, transparent 68%),
    var(--bg);
  color: var(--fg);
  font: 16px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
}

a { color: inherit; }
h1, h2, h3 {
  font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
  font-weight: 400;
  letter-spacing: 0.01em;
}

[data-todo] { border-bottom: 1px dashed var(--accent); }

/* ---------- chrome ---------- */

.site-header {
  display: flex;
  align-items: center;
  gap: 2rem;
  padding: 1.375rem 2.5rem;
  background: #100d0a;
  border-bottom: 1px solid var(--line);
}

/* The wordmark is the drawn file now, so nothing here tints it: it is a white
   SHE on transparency and the header ground is #100d0a, which is the contrast
   it was drawn for. Sized by height alone -- the width follows the file's own
   ratio, and the `width`/`height` attributes in the markup hold the space
   before it loads. The file carries about a tenth of its height as clear
   margin on every side, so the letters land a little under 2rem and the header
   keeps roughly the height the text mark gave it. */
.wordmark {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}
.wordmark-mark {
  display: block;
  height: 2rem;
  width: auto;
}

.site-nav { display: flex; gap: 1.5rem; }
.site-nav a {
  text-decoration: none;
  color: var(--fg);
  opacity: 0.68;
  font-size: 0.875rem;
  letter-spacing: 0.04em;
}
.site-nav a:hover { opacity: 1; color: var(--accent-bright); }

.site-meta { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
.lang { display: flex; gap: 0.5rem; font-size: 0.75rem; text-transform: uppercase; color: var(--muted); }

.cta {
  display: inline-block;
  padding: 0.6rem 1.35rem;
  border: 1px solid var(--accent);
  color: var(--accent-bright);
  text-decoration: none;
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  border-radius: var(--radius);
}
/* Filling with --accent rather than --accent-deep: --bg on #c9a87c clears 4.5:1,
   on #8f6d4e it is 4.0:1. */
.cta:hover { background: var(--accent); color: var(--bg); }

/* The second button in the header, for someone who came to work here rather
   than to book. It reads as a button but does not take the accent, so it never
   competes with the booking CTA beside it.
 *
 * A class of its own rather than .cta plus a modifier, so the two sets of
 * declarations cannot fight over specificity. Built from tokens throughout. */
.cta-quiet {
  display: inline-block;
  padding: 0.6rem 1rem;
  border: 1px solid var(--line);
  color: var(--muted);
  text-decoration: none;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-radius: var(--radius);
}
.cta-quiet:hover { border-color: var(--fg); color: var(--fg); }

.site-footer {
  margin-top: 4rem;
  padding: 2rem;
  background: #100d0a;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.8rem;
}

/* A variable so page gutters change in one place and the full-bleed hero still
   knows how far to break out. */
main { padding: var(--gutter); }

/* ---------- the opening ----------
 *
 * Full-bleed photography is what every reference in this market leads with, so
 * the hero escapes main's padding rather than sitting inside it.
 *
 * Four layers, in this order, because the film plays over a photograph and the
 * opening also carries ambient motion:
 *   0  .hero-backdrop   the photograph, which the film covers while it loads
 *   1  .hero::after      the scrim, only when there is a photograph
 *   2  .hero::before     the drifting warm pools
 *   3  .hero-opening     the banner film, and the button that rides over it
 */

/* clip-path rather than overflow: hidden, which would do the same job here --
   both keep the drifting pools inside the frame. But overflow on an ancestor
   makes that ancestor the scrollport a sticky child measures itself against,
   and .hero-actions below is sticky against the viewport. clip-path clips
   without standing in for the screen. */
.hero {
  position: relative;
  isolation: isolate;
  margin: calc(-1 * var(--gutter)) calc(-1 * var(--gutter)) 0;
  padding: 7rem var(--gutter) 6rem;
  min-height: min(78vh, 40rem);
  display: grid;
  place-items: center;
  text-align: center;
  clip-path: inset(0);
}

.hero-backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 28%;
}

/* Only when there is a photograph to darken. */
.hero-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(180deg, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0.3) 45%, rgba(0, 0, 0, 0.75) 100%);
}

/* Two slowly drifting warm pools, which need no asset. They still earn their
   place with the film above them: they hold the frame for the second or two
   before the first video frame paints, and they show through its edges. */
.hero::before {
  content: "";
  position: absolute;
  inset: -20%;
  z-index: 2;
  background:
    radial-gradient(38% 46% at 28% 32%, #4c2106cc 0%, transparent 70%),
    radial-gradient(34% 40% at 72% 62%, #8f6d4e55 0%, transparent 72%);
  animation: hero-drift 42s ease-in-out infinite alternate;
}
@keyframes hero-drift {
  from { transform: scale(1) translate3d(0, 0, 0); }
  to   { transform: scale(1.14) translate3d(2%, -2%, 0); }
}

/* The film and its button, above the pools and the scrim. This is also the
   sticky button's containing block -- see .hero-actions -- which is why the two
   of them are wrapped rather than sitting in the hero's grid side by side. */
.hero-opening {
  position: relative;
  z-index: 3;
  width: 100%;
}

/* The film sits in the type's old place: inside the hero's padding rather than
   bleeding to the edges, so the warm pools frame it. */
.hero-video {
  display: block;
  width: 100%;
  height: auto;
  /* banner.mp4 is 16:9, and stating it keeps the box the right shape before the
     file's metadata arrives, so the button under the film does not jump on
     load. It does not change the film's size. */
  aspect-ratio: 16 / 9;
}

/* The apply button: under the film in the flow, and on screen while the film is.
 *
 * The film is as wide as the page allows, so on a laptop its own bottom edge is
 * already past the fold and anything under it starts off screen. Sizing the
 * film down to make room was the first attempt and the wrong trade -- the
 * opening is the film. So the button sticks to the bottom of the screen,
 * riding over the lower part of the film, and settles into its real place
 * underneath as the page scrolls to it. Where it is already on screen -- a
 * phone, where the film is a fifth of the screen tall -- nothing moves.
 *
 * fit-content, because a sticky panel as wide as the page is a bar across the
 * film rather than a button on it.
 *
 * The ground is on the panel rather than on the button, as with .book-dock:
 * .cta is drawn with no background of its own, and an outlined button over
 * moving footage is one you cannot read. */
.hero-actions {
  position: sticky;
  bottom: calc(var(--gutter) + env(safe-area-inset-bottom, 0px));
  z-index: 4;

  width: fit-content;
  margin: 2rem auto 0;
  padding: 0.5rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.28);
}

/* Section rhythm: these pages are photograph-led, so they need air. */
.home-roster { padding: 5.5rem 0 1rem; }
.section-title {
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--muted);
  font-weight: 400;
  margin: 0 0 1.75rem;
}
.section-more { margin-top: 3rem; text-align: center; }

/* ---------- roster ---------- */

.roster-layout {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
  gap: 3rem;
  align-items: start;
}

.facets { position: sticky; top: 2rem; }
.facets-head { display: flex; justify-content: space-between; align-items: baseline; }
.facets-head h2 { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.16em; color: var(--accent); }
.reset { font-size: 0.75rem; color: var(--accent-deep); }

.facet { border: 0; border-top: 1px solid var(--line); margin: 0; padding: 1rem 0 0.5rem; }
.facet legend { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.16em; color: var(--accent); padding: 0; }
.facet-option { display: flex; align-items: center; gap: 0.5rem; padding: 0.2rem 0; font-size: 0.9rem; cursor: pointer; }

.count { color: var(--muted); font-size: 0.85rem; letter-spacing: 0.08em; margin-top: 0; }
.empty { color: var(--muted); }

/* The roster is the mosaic a profile's gallery uses (see .gallery below), with
 * the one difference that drives every measurement here: a card may be a
 * photograph *and* a caption below it. A row is then a frame plus a caption, so
 * the caption has to be a fixed height -- a name that wrapped to a second line
 * would make its own row taller than the rest, and the 2 x 2 blocks would stop
 * meeting the plain cards beside them.
 *
 * The column count, the crop and the caption's height are properties rather
 * than rules, because each one is an input to that arithmetic.
 */
.grid {
  --grid-cols: 5;
  --grid-gap: 1.5rem;
  --grid-row-gap: 2rem;
  /* The crop, as the multiplier from a column's width to its height: 4 / 3 is
     the 3:4 portrait. A property rather than an aspect-ratio on .card-media,
     because a ratio on the tile leaves a grid row unmeasurable -- the same
     reason .gallery states a height. */
  --card-crop: 4 / 3;
  /* Everything below the photograph. Zero here, because the name is laid over
     the photograph instead (see .card-body) -- a length, not a bare 0, because
     it is subtracted from a percentage. */
  --caption: 0rem;

  display: grid;
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
  /* Dense placement is load-bearing: it drops each feature into the first 2 x 2
     that fits, so the blocks interlock instead of stacking against one edge,
     and it backfills the plain cards beside them. It does reorder -- a card can
     move up into a hole ahead of one that precedes it -- which a roster can
     afford and a ranking could not. */
  grid-auto-flow: dense;
  gap: var(--grid-row-gap) var(--grid-gap);
}

.card {
  position: relative; /* the caption is laid over the photograph */
  /* A column's width taken in the crop: the photograph on a plain card. cqw is
     the width of whatever holds the grid, which is the grid's own width -- see
     .results below, and note that a grid with a gutter of its own would throw
     this out by that gutter. */
  --frame: calc(
    (100cqw - (var(--grid-cols) - 1) * var(--grid-gap))
    / var(--grid-cols) * var(--card-crop)
  );
  /* One row: that photograph and its caption. */
  --row: calc(var(--frame) + var(--caption));

  height: var(--row);
  text-decoration: none;
  display: block;
}

/* Every third card is a feature: two columns wide, two rows tall.
 *
 * It carries one caption where the two rows it covers carry two, so its
 * photograph takes the spare caption's height as well: it is that much taller
 * than the frame doubled, which the crop absorbs. The alternative -- a caption
 * of double height, so the photograph is exactly the frame doubled -- leaves a
 * band of empty space under the name of every feature, which is far easier to
 * see than a slightly deeper crop.
 *
 * Never the last two cards: a feature needs cards after it to fill the cells
 * beside it, and a hole at the end of a grid reads as the end of the grid where
 * one in the middle reads as broken.
 */
.card:nth-child(3n + 1):not(:nth-last-child(-n + 2)) {
  grid-column: span 2;
  grid-row: span 2;
  height: calc(2 * var(--row) + var(--grid-row-gap));
}

.card-media {
  height: calc(100% - var(--caption));
  overflow: hidden;
  background: var(--surface);
  border-radius: var(--radius);
  /* A brass hairline appears on hover instead of a shadow: on a warm dark
     ground a shadow is invisible, so the light has to come from the edge. */
  box-shadow: inset 0 0 0 1px var(--line);
  transition: box-shadow 0.35s ease;
}
.card:hover .card-media { box-shadow: inset 0 0 0 1px var(--accent); }
.card-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
.card:hover .card-media img { transform: scale(1.03); }
.card-media-empty {
  width: 100%;
  height: 100%;
  background: repeating-linear-gradient(45deg, #241d16 0 11px, #1c1611 11px 22px);
}

/* The name over the photograph, which is how this market presents a roster. The
   scrim is on the body rather than on the image, so the gradient always sits
   directly under the type whatever the image does. */
.card-body {
  position: absolute;
  inset: auto 0 0 0;
  padding: 2.5rem 0.875rem 0.875rem;
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.82) 78%);
}
/* One line each, whatever the name: text that wrapped would push the scrim up
   into the photograph. */
.card-name, .card-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-name { font-family: Georgia, serif; font-size: 1.125rem; letter-spacing: 0.03em; }
.card:hover .card-name { color: var(--accent-bright); }
.card-sub { color: var(--muted); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; }

/* Whatever holds a grid is what a card is measured against, in both senses: it
   is the box cqw resolves to, and it is what the column count below asks about.
   The roster's grid sits beside a filter column and the home page's does not,
   so at one viewport width they are two rather different widths -- a viewport
   media query would have to be wrong for one of them.
   The container is here rather than on .grid itself because container-type
   brings style containment with it, and style containment on the element that
   carries a counter-reset would break a counter -- which is what numbering the
   roster would need. A grid with no such ancestor still renders: it keeps five
   columns and sizes its cards from the viewport. */
.results, .home-roster {
  container-type: inline-size;
  container-name: roster;
}

/* The column count belongs to the mosaic: the feature's placement and the
   left-edge correction below are both stated in terms of it.
   The steps down are where a card would otherwise stop being a photograph you
   can see -- roughly 200px is the floor. The steps up are the same floor from
   the other side: nothing here caps the page's width, so on a wide monitor five
   columns would keep growing into five very large photographs. */
@container roster (min-width: 1700px) {
  .grid { --grid-cols: 6; }
}
@container roster (min-width: 2200px) {
  .grid { --grid-cols: 7; }
}
@container roster (max-width: 1200px) {
  .grid { --grid-cols: 4; }
}
@container roster (max-width: 900px) {
  .grid { --grid-cols: 3; }
  /* Three columns is the one count where dense placement would stack every
     feature against the left edge -- there is only ever one free column beside
     one, so the first fit is always the same pair. Naming the column for every
     second feature brings the alternation back. */
  .card:nth-child(6n + 4):not(:nth-last-child(-n + 2)) { grid-column: 2 / span 2; }
}
/* Two columns on a phone, where a feature spans the screen. */
@container roster (max-width: 640px) {
  .grid { --grid-cols: 2; }
  .card:nth-child(6n + 4):not(:nth-last-child(-n + 2)) { grid-column: span 2; }
}
/* One column, and no mosaic left to speak of: every card is already the width
   of the screen, and a tile that spanned two of one column would invent a
   second column to span into. */
@container roster (max-width: 420px) {
  .grid { --grid-cols: 1; }
  .card:nth-child(3n + 1):not(:nth-last-child(-n + 2)) {
    grid-column: auto;
    grid-row: auto;
    height: var(--row);
  }
}

/* ---------- profile ---------- */

.profile-head h1 { font-size: clamp(2.125rem, 5.5vw, 3.5rem); }

.headline {
  font-family: Georgia, serif;
  font-style: italic;
  color: var(--accent-bright);
  opacity: 0.85;
  font-size: 1.125rem;
  max-width: var(--measure);
}

/* The booking action, held in the corner for the whole page.
 *
 * The ground is on the dock rather than on the button, because .cta is drawn
 * with no background at all, and an outlined button over a photograph is one
 * you cannot read. This way the button itself is what floats, on a panel the
 * page can see it against.
 *
 * Nothing between here and the viewport may take transform, filter or
 * containment, or the corner it fixes itself to becomes that element's corner
 * instead of the screen's. */
.book-dock {
  position: fixed;
  right: var(--gutter);
  bottom: calc(var(--gutter) + env(safe-area-inset-bottom, 0px));
  z-index: 40;

  padding: 0.5rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.28);
}

/* The footer is the one thing the dock would otherwise sit on top of for good,
   rather than merely until the page scrolls on. */
body:has(.book-dock) .site-footer { padding-bottom: 6rem; }

/* The mosaic works off a single measure: a column's width taken in 3:4
 * proportion is also the row height. A tile is then either one cell or a 2 x 2
 * block -- the same 3:4 crop at double size -- so size varies while the framing
 * does not.
 *
 * Every third photo is a feature, which is six cells per block. Nothing pins a
 * feature to a column; dense placement drops each one into the first 2 x 2 it
 * fits, so the blocks interlock and stagger down the page instead of stacking
 * against one edge.
 *
 * Tempting and wrong: sizing tiles from each photo's own aspect ratio, so a
 * landscape gets a wide cell. A wide cell competes with the feature tile for the
 * same pairs of columns, and the leftover single cells then have nothing left to
 * fill them -- measured on a realistic set, that leaves holes mid-grid. One crop
 * for every tile keeps the rows whole whatever the photos are.
 */
.gallery {
  --gallery-cols: 5;
  --gallery-gap: 1rem;

  /* Descendants resolve cqw against this box: the only way a tile can be sized
     from the gallery's own width rather than from the viewport. */
  container-type: inline-size;

  display: grid;
  grid-template-columns: repeat(var(--gallery-cols), minmax(0, 1fr));
  /* Dense placement is load-bearing, not a nicety: it both positions the feature
     tiles and lets the plain ones fill the cells left beside and above them. */
  grid-auto-flow: dense;
  gap: var(--gallery-gap);
}

/* The tile is the link, not the image: it is what the grid places, and what a
   visitor clicks to open the photograph full-screen. */
.gallery .shot {
  /* The row height: one column's width, in 3:4. Stated as a height rather than
     an aspect-ratio because a grid row sized from an image's ratio falls back to
     the file's own pixel dimensions, which have nothing to do with the layout --
     one 800px-tall file would then set the height of every row. */
  --row: calc(
    (100cqw - (var(--gallery-cols) - 1) * var(--gallery-gap))
    / var(--gallery-cols) * 4 / 3
  );

  height: var(--row);
  /* The radius stays on the image, which is what is actually drawn. */
  overflow: hidden;
  cursor: zoom-in;
}

.gallery .shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: var(--radius);
}

/* A feature is two rows tall, so it needs tiles behind it to fill the cells
   beside it; a gallery that ends on a feature slot instead leaves a hole
   mid-grid, which is the one gap here that reads as broken rather than as
   breathing room. Two following tiles is the measured threshold -- at that point
   the block above has already taken the other half of those rows.
   of-type, not child: the members-only notice is also a child of the gallery,
   and it spans every column, so it can never be one of the tiles that fills in
   beside a feature. */
.gallery .shot:nth-of-type(3n + 1):not(:nth-last-of-type(-n + 2)) {
  grid-column: span 2;
  grid-row: span 2;
  height: calc(2 * var(--row) + var(--gallery-gap));
}

/* A profile with no photographs at all still needs to occupy a tile. */
.gallery .card-media-empty { aspect-ratio: 3 / 4; }

/* A column count for every width, so a plain tile stays in the same band of
   sizes all the way down -- roughly 240 to 340px. The viewport says how wide
   the gallery is now that it has the page to itself; the steps used to be
   uneven to allow for a 300px column beside it, and no longer are.
   Six cells per block divides into whole rows at every one of these counts, so
   the rhythm itself needs no restating. */
@media (min-width: 1700px) {
  .gallery { --gallery-cols: 6; }
}
@media (min-width: 2200px) {
  .gallery { --gallery-cols: 7; }
}
@media (max-width: 1400px) {
  .gallery { --gallery-cols: 4; }
}
@media (max-width: 1100px) {
  .gallery { --gallery-cols: 3; }
  /* Three columns is the one count where dense placement would stack every
     feature against the left edge -- there is only ever one free column beside
     one, so the first fit is always the same pair. Naming the column for every
     second feature brings the alternation back. */
  .gallery .shot:nth-of-type(6n + 4):not(:nth-last-of-type(-n + 2)) {
    grid-column: 2 / span 2;
  }
}
/* Two columns on a phone, where a feature spans the screen. Earlier than it
   used to be: three columns of a full-width gallery are narrower than three of
   one that stopped short of a sidebar. */
@media (max-width: 800px) {
  .gallery { --gallery-cols: 2; }
  .gallery .shot:nth-of-type(6n + 4):not(:nth-last-of-type(-n + 2)) {
    grid-column: span 2;
  }
}

.gate {
  grid-column: 1 / -1;
  border: 1px solid var(--accent);
  padding: 2.25rem;
  text-align: center;
  background: linear-gradient(180deg, #2b2822 0%, var(--surface) 100%);
  color: var(--accent-bright);
  border-radius: var(--radius);
}

/* ---------- lightbox ----------
 *
 * A native modal <dialog>, so the element itself provides Escape-to-close, focus
 * containment and inertness of the page behind. The dialog box covers the
 * viewport rather than hugging the photograph, so that a click anywhere beside
 * the photograph lands on the dialog and closes it.
 *
 * The colours here are literal rather than tokens, and deliberately so: this is
 * a darkened room for looking at a photograph, and it stays one whatever the
 * page around it is doing. Tokens would follow the page.
 */

.lightbox {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
}
/* Scoped to [open]: an unqualified `display` would defeat the UA rule that keeps
   a closed dialog hidden. */
.lightbox[open] {
  display: grid;
  place-items: center;
  animation: lightbox-in 0.18s ease-out;
}
.lightbox::backdrop { background: rgba(8, 7, 6, 0.94); }
.lightbox[open]::backdrop { animation: lightbox-in 0.18s ease-out; }

@keyframes lightbox-in {
  from { opacity: 0; }
}

/* Nearly the whole viewport, and never cropped: the mosaic tile is where the
   crop happens, this is where the frame is seen whole. The floor matters as much
   as the ceiling -- without it a small file would open as a stamp in the middle
   of a black screen. */
.lightbox-photo {
  display: block;
  max-width: 94vw;
  max-height: 94vh;
  min-height: 70vh;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* The page behind must not scroll under the photograph. */
html:has(.lightbox[open]) { overflow: hidden; }

.lightbox-nav {
  position: fixed;
  background: transparent;
  border: 0;
  color: #f2ede6;
  opacity: 0.55;
  cursor: pointer;
  font: 400 2.5rem/1 var(--brand-font);
  padding: 0.5rem 1rem;
}
.lightbox-nav:hover { opacity: 1; }

.lightbox-prev { left: 0.5rem; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 0.5rem; top: 50%; transform: translateY(-50%); }
.lightbox-close { top: 0.5rem; right: 0.75rem; font-size: 2rem; }

.lightbox-count {
  position: fixed;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  color: #94897c;
  font: 0.75rem/1 var(--brand-font);
  letter-spacing: 0.1em;
}

/* The record, below the gallery and across the page.
 *
 * Two columns because the two halves read differently: the scalars are a
 * ledger of label against value that reads down one narrow column, and the
 * terms are sets that read across as many as fit. Bounding the ledger is what
 * keeps it a ledger -- given the whole width, every value would sit a page away
 * from its label. */
.details {
  display: grid;
  grid-template-columns: minmax(0, 21rem) minmax(0, 1fr);
  gap: 3rem;
  align-items: start;
  margin-top: 3.5rem;
}

/* The details and the rate card: two ledgers of label against value, so they
   share the column and its width, as one panel. */
.details-ledger {
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.375rem;
}

.term-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  /* No row gap: .chips already carries the space under a group, and a group
     with one chip should not sit in a cell as tall as one with nine. */
  gap: 0 2rem;
  align-content: start;
}

.specs-block h2,
.term-group h3 {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
  font-weight: 400;
  margin: 0 0 0.6rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
/* A profile with no scalars filled in should not leave a 21rem hole beside its
   terms. The ledger never does this: it is bounded on purpose. */
.details > .term-grid:only-child { grid-column: 1 / -1; }

.specs { display: grid; grid-template-columns: auto 1fr; gap: 0.35rem 1rem; margin: 0; font-size: 0.9rem; }
.specs dt { color: var(--muted); }
.specs dd { margin: 0; text-align: right; }

.chips { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem; padding: 0; margin: 0 0 1.25rem; }
.chips span {
  display: inline-block;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 0.75rem;
  color: var(--fg);
}
.chips a {
  display: inline-block;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 0.75rem;
  text-decoration: none;
  color: var(--fg);
}
.chips a:hover { border-color: var(--accent); color: var(--accent-bright); }

.rates { width: 100%; border-collapse: collapse; font-size: 0.9rem; margin-top: 1.5rem; }
.rates caption { text-align: left; color: var(--accent); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.16em; padding-bottom: 0.5rem; }
.rates th, .rates td { border-top: 1px solid var(--line); padding: 0.4rem 0; text-align: left; font-weight: 400; }
.rates td { text-align: right; color: var(--accent-bright); font-family: Georgia, serif; }

.bio {
  max-width: var(--measure);
  margin-top: 3rem;
  color: var(--fg);
  opacity: 0.8;
  font-size: 1.0625rem;
  line-height: 1.75;
}

/* ---------- forms: auth, and the approval queue ---------- */

.auth { max-width: 34rem; margin: 3rem auto; }
.auth-wide { max-width: 60rem; }
.auth-intro { color: var(--muted); }

.auth-form { display: flex; flex-direction: column; gap: 1rem; margin-top: 1.5rem; }
.auth-form label { display: flex; flex-direction: column; gap: 0.35rem; }
/* A class selector beats the browser's own [hidden] rule, so a conditional
   field -- the duo date's second dame -- would stay on screen without this. */
.auth-form label[hidden] { display: none; }
.auth-form label > span { font-size: 0.8125rem; color: var(--muted); }
.auth-form small { color: var(--muted); font-size: 0.75rem; }

/* One frame for every control. The contact, booking and application forms had
   each grown their own copy of this rule for the field types the login form
   does not use; it belongs on .auth-form, which all of them already carry. */
.auth-form input,
.auth-form select,
.auth-form textarea {
  padding: 0.6rem 0.7rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: inherit;
  font: inherit;
}
.auth-form textarea { resize: vertical; }

/* The file input keeps the frame above but loses its padding on the left: the
   browser draws its own button inside the field, and the two paddings together
   push it off centre. The button itself is the browser's and is left alone --
   restyling it means replacing it with a label and a hidden input, which is a
   control that stops working the moment the script does. */
.auth-form input[type="file"] { padding: 0.45rem 0.5rem; line-height: 1.6; }
.auth-form input[type="file"]::file-selector-button {
  margin-right: 0.6rem;
  padding: 0.3rem 0.6rem;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.auth-form input[type="file"]::file-selector-button:hover { border-color: var(--accent); color: var(--accent); }
.auth-form label > span em { color: var(--muted); font-style: normal; font-size: 0.8125rem; }
.auth-form .section-title { margin-top: 1.5rem; }

.auth-form button {
  align-self: flex-start;
  padding: 0.6rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.auth-form button:hover { border-color: var(--accent); color: var(--accent); }
.auth-form button.cta { border-color: var(--accent); color: var(--accent); }
.auth-form button.cta:hover { background: var(--accent); color: var(--bg); }

.auth-alt { margin-top: 1.5rem; font-size: 0.875rem; color: var(--muted); }

.form-errors, .form-notice {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  margin: 1.25rem 0 0;
  font-size: 0.9rem;
}
.form-errors { list-style: none; border-color: #7c3b3b; }
.form-notice { border-color: var(--accent); color: var(--accent-bright); }

.queue { width: 100%; border-collapse: collapse; margin-top: 1.5rem; font-size: 0.875rem; }
.queue th, .queue td { border-top: 1px solid var(--line); padding: 0.6rem 0.5rem; text-align: left; }
.queue th {
  font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted); font-weight: 400;
}
.queue-actions { display: flex; gap: 0.5rem; }
.queue-actions form { margin: 0; }
.queue-actions button {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}
.queue-actions .approve:hover { border-color: var(--accent); color: var(--accent-bright); }
.queue-actions .decline:hover { border-color: #7c3b3b; color: #c07575; }

/* A visible focus ring everywhere, not only on inputs. */
a:focus-visible, button:focus-visible, input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Second .hero rule, and it wins over the padding set with the layers above:
   the opening is padded from the film in rather than from the frame in. */
.hero { padding: 5rem 0; text-align: center; }

@media (max-width: 900px) {
  .roster-layout, .details { grid-template-columns: 1fr; }
  .facets { position: static; }
  .site-header { flex-wrap: wrap; gap: 1rem; }
}

/* ---------- editorial pages: services, rates, contact ---------- */

.page { max-width: 62rem; margin: 3.5rem auto 0; }
.page-narrow { max-width: 38rem; }
.page-head { margin-bottom: 3rem; }
.page-intro { color: var(--muted); max-width: var(--measure); font-size: 1.0625rem; }
.page-more { margin-top: 3.5rem; }

.service-group { margin-bottom: 3rem; }

/* One row per service, normally a link into the filtered roster: the cue on
   the right says so, rather than leaving the row looking like plain prose.
   The row keeps its shape when the roster is sealed and it is a span with no
   cue, which is why the measurements hang off .service-row and only the hover
   affordances below stay on the anchor. */
.service-list { list-style: none; padding: 0; margin: 0; }
.service-list li { border-top: 1px solid var(--line); }
.service-list .service-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.9rem 0;
  text-decoration: none;
}
.service-name { font-size: 1.0625rem; }
.service-cue {
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  opacity: 0;
  transition: opacity 0.2s ease;
}
.service-list a:hover .service-name { color: var(--accent); }
.service-list a:hover .service-cue,
.service-list a:focus-visible .service-cue { opacity: 1; }

.rates-page { max-width: 30rem; }
.rates-page thead th {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  font-weight: 400;
}

.terms-block { margin-top: 3.5rem; }
.terms { display: grid; grid-template-columns: max-content 1fr; gap: 0.5rem 1.5rem; margin: 0; }
.terms dt { color: var(--muted); font-size: 0.9375rem; }
.terms dd { margin: 0; }

.contact-privacy { color: var(--muted); font-size: 0.75rem; }
.contact-details { margin-top: 3.5rem; }

@media (prefers-reduced-motion: reduce) {
  .service-cue { transition: none; }
  .hero::before { animation: none; }
  .card-media { transition: none; }
}

/* ---------- applying to work here ---------- */

/* The banner line under the title. Letterspaced and in the accent rather than
   sized up: three words set like a mark read as a mark, and this page's own
   type is already the largest on it. */
.page-tagline {
  margin: 0.75rem 0 1.5rem;
  color: var(--accent-bright);
  font-size: 0.9375rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* What the work offers. Not muted, unlike .apply-steps below: these are the
   reasons somebody applies, so they take the page's own foreground, and the
   accent marker is drawn rather than a bullet the font chooses. */
.apply-perks {
  margin: 0 0 2rem;
  padding: 0;
  list-style: none;
  max-width: var(--measure);
}
.apply-perks li {
  position: relative;
  padding-left: 1.4rem;
  margin-bottom: 0.55rem;
}
.apply-perks li::before {
  content: "";
  position: absolute;
  left: 0.25rem;
  top: 0.62em;
  width: 0.32rem;
  height: 0.32rem;
  background: var(--accent);
  transform: rotate(45deg);
}

.apply-lead { max-width: var(--measure); margin: 0 0 1.25rem; }

/* The closing question, which is the last thing read before the form. */
.apply-invite { color: var(--accent-bright); margin-bottom: 2.5rem; }

.apply-steps {
  margin: 0;
  padding-left: 1.2rem;
  color: var(--muted);
  max-width: var(--measure);
}
.apply-steps li { margin-bottom: 0.6rem; }

/* ---------- booking form ---------- */

.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.check { flex-direction: row !important; align-items: center; gap: 0.5rem; }

/* A radio group needs a fieldset to be one control to a screen reader, and a
   fieldset needs its browser chrome removed to look like the rest of the
   form. The options sit in a row and wrap to a column when there is no room
   for one. */
.choice { border: 0; margin: 0; padding: 0; min-width: 0; }
.choice legend { padding: 0; margin-bottom: 0.35rem; font-size: 0.8125rem; color: var(--muted); }
.choice-options { display: flex; flex-wrap: wrap; gap: 0.35rem 1.25rem; }
.booking-ref { display: block; margin-top: 0.5rem; font-family: var(--brand-font); letter-spacing: 0.1em; }

/* The honeypot is removed from the layout, not merely hidden from sight: a
   bot reading the DOM should still find it, a person should never tab into it. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* The reCAPTCHA widget, when one is configured. Google ships it at a fixed
   302px, which is wider than the form on a small phone, so the box is allowed
   to scroll inside its own wrapper rather than pushing the page sideways.
   Nothing else is styled: everything visible is inside Google's iframe and out
   of this stylesheet's reach. */
.captcha { max-width: 100%; overflow-x: auto; }

/* ---------- date and time fields, calendar and clock ----------
 * Both are plain text inputs with a popup built by datepicker.js and
 * timepicker.js: dd/mm/yyyy from a month grid, uu:mm from a list of slots.
 * The two share their frame and differ only inside it. Everything here is
 * tokenised, so the popups are coloured by the page rather than by a set of
 * literals of their own.
 */

.datefield,
.timefield { display: flex; flex-direction: column; gap: 0.35rem; }
.datefield-label,
.timefield-label { font-size: 0.8125rem; color: var(--muted); }
.datefield-row,
.timefield-row { position: relative; display: flex; gap: 0.4rem; }
.datefield-row input,
.timefield-row input { flex: 1; min-width: 0; }

.auth-form .datefield-open,
.auth-form .timefield-open {
  align-self: stretch;
  padding: 0 0.75rem;
  font-size: 0.9rem;
  line-height: 1;
}

.calendar,
.timelist {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  z-index: 60;
  width: min(20rem, calc(100vw - 2rem));
  padding: 0.6rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* The popup floats over the form, so it needs a shadow to read as above it
     rather than as part of it. */
  box-shadow: 0 14px 30px rgb(0 0 0 / 45%);
}
.calendar[hidden],
.timelist[hidden] { display: none; }

/* The clock is a narrow column of slots rather than a grid, and a day of them
   is longer than the viewport: it scrolls, and timepicker.js scrolls the slot
   in hand into the middle of it on open. */
.timelist { width: min(9rem, calc(100vw - 2rem)); padding: 0.35rem; }
.timelist-scroll {
  max-height: 15rem;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.auth-form .timelist-slot {
  align-self: auto;
  width: 100%;
  padding: 0.3rem 0;
  border-color: transparent;
  font-size: 0.8125rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.auth-form .timelist-slot.is-selected {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
.auth-form .timelist-slot:disabled {
  border-color: transparent;
  color: var(--muted);
  opacity: 0.45;
  cursor: default;
}

.calendar-head { display: flex; align-items: center; gap: 0.35rem; margin-bottom: 0.5rem; }
.calendar-title {
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  /* Dutch month names are lowercase; a heading reads better capitalised. */
  text-transform: capitalize;
}
.auth-form .calendar-nav {
  align-self: auto;
  padding: 0.1rem 0.55rem;
  font-size: 1rem;
  line-height: 1.2;
}

.calendar-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }
.calendar-grid th {
  padding-bottom: 0.3rem;
  font-size: 0.6875rem;
  font-weight: 400;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.auth-form .calendar-day {
  align-self: auto;
  width: 100%;
  padding: 0.4rem 0;
  border-color: transparent;
  font-size: 0.8125rem;
  text-align: center;
}
.calendar-day.is-outside { color: var(--muted); }
.calendar-day.is-today { border-color: var(--line); }
.auth-form .calendar-day.is-selected {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
.auth-form .calendar-day:disabled {
  border-color: transparent;
  color: var(--muted);
  opacity: 0.45;
  cursor: default;
}

/* ---------- address field + suggestions ----------
 * A plain text field until addresspicker.js has an endpoint to ask; the list
 * below it is built only then, and borrows the popup's frame from .calendar.
 */

.addressfield { display: flex; flex-direction: column; gap: 0.35rem; }
.addressfield-row { position: relative; display: flex; }
.addressfield-row input { flex: 1; min-width: 0; }

.addresslist {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  right: 0;
  z-index: 60;
  max-height: 15rem;
  overflow-y: auto;
  margin: 0;
  padding: 0.25rem;
  list-style: none;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 14px 30px rgb(0 0 0 / 45%);
}
.addresslist[hidden] { display: none; }

/* Two lines per row: what was typed on top, what the geocoder made of it
   below. A search for a hotel is recognised by its name, not by the street it
   turns out to be on. */
.addresslist-item {
  display: flex;
  flex-direction: column;
  padding: 0.4rem 0.5rem;
  border-radius: var(--radius);
  font-size: 0.8125rem;
  cursor: pointer;
}
.addresslist-label { font-weight: 600; }
.addresslist-address { color: var(--muted); font-size: 0.75rem; }
.addresslist-item.is-active { background: var(--accent); color: var(--bg); }
.addresslist-item.is-active .addresslist-address { color: var(--bg); opacity: 0.8; }

@media (max-width: 560px) { .field-row { grid-template-columns: 1fr; } }
