/* ============================================================================
   Portfolio — public gallery styles, ported from docs/ux-prototype/Portfolio.dc.html.
   FONTS: the prototype uses Bodoni Moda (display serif) + Hanken Grotesk (sans).
   Those OFL fonts are not yet vendored (gated pending sign-off), so --serif/--sans
   fall back to a system stack. To adopt the real fonts, drop the woff2 files in
   wwwroot/fonts/, add @font-face rules, and set the two variables below.
   ============================================================================ */
:root {
  --serif: "Bodoni Moda", Georgia, "Times New Roman", serif;
  --sans: "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --gold: #d8b56b;
  --gold-bright: #ecd39a;
  --ink: #0b0b0d;
  --paper: #f2f0ec;
  /* The page's one side margin. Everything that isn't full-bleed — nav, filter bar, photo grid, albums,
     videos, About, Places — starts on this line, so the brand and the first column of
     photographs share an edge. It stops growing at 64px: past that a viewport
     wide enough for another column of photographs would be spending the room on empty margin instead. */
  --gutter: clamp(16px, 4vw, 64px);
}

* { box-sizing: border-box; }
html, body { margin: 0; }
/* The ink is stated on `html` as well as `body`. The canvas — everything the browser paints outside the
   document's own box, which is what an over-pull exposes along the edge you are pulling away from — takes
   its colour from the root element, and it must never be able to come out lighter than the page in front
   of it. **Deliberately no `overscroll-behavior` here**: that would stop the rubber-band, and the
   pull-to-refresh gesture with it, which is a browser affordance to remove rather than a bug to fix. */
html { background: var(--ink); }
body {
  background: var(--ink);
  color: var(--paper);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}
a { color: var(--gold); text-decoration: none; }
a:hover { color: var(--gold-bright); }
button { font-family: inherit; }
::selection { background: var(--gold); color: #17140d; }
.serif { font-family: var(--serif); }

input::placeholder, textarea::placeholder { color: #7d7d84; }
/* Drop the ring for pointer focus, but keep a visible one for keyboard users (:focus-visible wins here
   by coming later at equal specificity). Every interactive element gets a gold keyboard ring. */
input:focus, textarea:focus { outline: none; }
:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* …except on a dialog container that `portfolio.trapFocus` has focused *itself*. Opening a modal has to
   move focus inside it, and the container is where that focus belongs — it is not a control, so a keyboard
   ring around the whole viewport says nothing about where Tab will go and reads as the dialog being
   selected. The controls inside it keep their rings, which is where the ring means something. (The skip
   link's `main` also carries tabindex="-1" and deliberately does NOT carry this attribute: landing there
   is exactly the case where a visitor needs to be shown where they arrived.) */
[data-focus-host]:focus-visible { outline: none; }
input:focus-visible, textarea:focus-visible { outline: 2px solid var(--gold); outline-offset: 0; }

/* Present to assistive tech, invisible on screen (headings, field labels the visual design omits). */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* Skip link (MainLayout): off-screen until it takes keyboard focus, then it drops into the top-left
   above everything — z-index beats the sticky header stack (41) and the lightbox backdrop (90). */
.skiplink {
  position: fixed; top: -100px; left: 12px; z-index: 130;
  background: var(--gold); color: #17140d; border-radius: 0 0 10px 10px;
  padding: 11px 18px; font-size: 13px; font-weight: 600; letter-spacing: .03em;
  transition: top .18s ease;
}
.skiplink:focus { top: 0; color: #17140d; }

/* ----- blur-up frames ----- */
.frame .full { opacity: 0; transition: opacity .9s ease, transform 3.5s ease; }
.frame .full.on { opacity: 1; }
.frame:hover .full.on { transform: scale(1.055); }
.frame .ph { filter: blur(18px); transform: scale(1.12); }
/* The caption is a hover answer, not a label: at rest the grid is a wall of photographs with nothing
   written on them, and only the tile under the pointer names itself. The scrim it is read against fades
   with it — left permanently on, it darkened the foot of every photograph to carry text that wasn't
   there. */
.frame .cap { opacity: 0; transform: translateY(10px); transition: opacity .45s ease, transform .45s ease; }
.frame:hover .cap { opacity: 1; transform: none; }
.frame .capshade { opacity: 0; transition: opacity .45s ease; }
.frame:hover .capshade { opacity: 1; }
.frame .dl { opacity: 0; transform: translateY(-5px); transition: opacity .3s ease, transform .3s ease; }
.frame:hover .dl { opacity: 1; transform: none; }
.frame:hover { z-index: 3; }
.frame::after { content: ""; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(255,255,255,0); transition: box-shadow .35s ease; pointer-events: none; }
.frame:hover::after { box-shadow: inset 0 0 0 1px rgba(216,181,107,0.55); }

/* No hover, no reveal — so on a touch screen the gated Save button would never appear at all, and a
   signed-in phone visitor would lose the per-tile download entirely. It is simply always on there.
   The caption is deliberately *not*: an untitled wall is the intended resting state on every device, and
   a tap already opens the lightbox, which names the photograph and its place in full. */
@media (hover: none) {
  .frame .dl { opacity: 1; transform: none; }
}

/* Transparent full-tile hit target: a real <button> gives Enter/Space activation and focus for free,
   without wrapping the tile's block content (caption, gradient, the gated download button) in a <button>,
   which would be invalid HTML. Sits above the overlays (z-index 1); the download button rides above it (2). */
.tilehit { position: absolute; inset: 0; width: 100%; height: 100%; background: none; border: 0; padding: 0; margin: 0; cursor: pointer; z-index: 1; }
/* Keyboard focus mirrors hover: surface the caption, the download button, and the gold frame ring. */
.frame:focus-within .cap { opacity: 1; transform: none; }
.frame:focus-within .capshade { opacity: 1; }
.frame:focus-within .dl { opacity: 1; transform: none; }
.frame:focus-within::after { box-shadow: inset 0 0 0 1px rgba(216,181,107,0.55); }
.tilehit:focus-visible, .rel:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

/* ----- chips / buttons / lightbox anim ----- */
.chip { transition: background .25s ease, color .25s ease, border-color .25s ease, transform .2s ease; }
.chip:hover { transform: translateY(-1px); }
.arrowbtn { transition: background .25s ease, border-color .25s ease; }
.arrowbtn:hover { background: rgba(216,181,107,0.16); border-color: rgba(216,181,107,0.5); }
.loclink { transition: color .2s ease; }
.loclink:hover { color: var(--gold-bright) !important; }

.lb-img { opacity: 0; }
.lb-img.on { animation: lbIn .5s cubic-bezier(.2,.7,.2,1) both; }
/* Centered by margins, not translate — the spin animation owns transform. The .15s fadeIn delay
   keeps it invisible on fast loads so cached images don't flash a spinner. */
.lb-spin { position: absolute; left: 50%; top: 50%; width: 34px; height: 34px; margin: -17px 0 0 -17px;
           border-radius: 50%; border: 2px solid rgba(255,255,255,0.12); border-top-color: #d8b56b;
           animation: spin .8s linear infinite, fadeIn .3s ease .15s both; }
@keyframes spin { to { transform: rotate(360deg); } }
.lb-back { animation: fadeIn .35s ease both; }
.lb-panel { animation: panelIn .55s cubic-bezier(.2,.7,.2,1) both; }
/* The prev/next arrows are revealed on hover only — and only where a real pointer exists. On a tablet-sized
   touch screen they stay visible, since there's no hover; at phone width they're dropped entirely and
   swiping the stage is the only navigation (see the max-width:640px block).
   Keyed off .lb-stage:hover so they show over the image/video, not while the cursor is on the panel;
   pointer-events:none while hidden keeps an invisible arrow from swallowing a click. */
/* …and they hide again once the cursor has been still for a few seconds (portfolio.idleHide puts `idle` on
   the backdrop), so a parked pointer doesn't leave chrome sitting over the photo. The idle rule comes after
   the hover rule and matches at equal specificity, so idle wins while it applies. */
@media (hover: hover) {
  .lb-arrow { opacity: 0; pointer-events: none; transition: opacity .2s ease; }
  .lb-stage:hover .lb-arrow { opacity: 1; pointer-events: auto; }
  .lb-back.idle .lb-arrow { opacity: 0; pointer-events: none; }
}
/* The row must not wrap: a wrapping flex line sizes to its content, so the panel would grow past
   the viewport instead of being capped by .lb-body — its overflow-y:auto then never engages and the
   detail panel gets clipped by the fixed backdrop with nothing to scroll. nowrap + overflow:hidden
   caps the line at the container height so .lb-panel scrolls internally. (The stacked layout below
   re-enables overflow-y on .lb-body itself.) */
.lb-body { flex-wrap: nowrap; overflow: hidden; }
/* Freeze the grid behind the lightbox so the wheel scrolls the panel, not the page underneath. */
body.lb-open { overflow: hidden; }
/* The panel's one navigating link: the album this photograph belongs to. Styled as a row rather than as
   a value in the grids around it, because pressing it leaves the lightbox for that album's grid — the
   chevron and the whole-width hit area are what distinguish it from the location and tag links, which
   refine the grid behind the photo instead. A long album name wraps rather than pushing the chevron out
   of the panel. */
.lb-album { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%;
            background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px;
            padding: 11px 13px; color: #e6e6ea; font-family: inherit; font-size: 13.5px; text-align: left;
            cursor: pointer; transition: background .25s ease, border-color .25s ease, color .25s ease; }
.lb-album .lb-album-nm { min-width: 0; overflow-wrap: anywhere; }
.lb-album i, .lb-album svg { flex: 0 0 auto; color: #7d7d84; transition: color .25s ease, transform .25s ease; }
.lb-album:hover { background: rgba(216,181,107,0.14); border-color: rgba(216,181,107,0.45); color: #f0dcae; }
.lb-album:hover i, .lb-album:hover svg { color: var(--gold); transform: translateX(3px); }
.lb-album:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.rel .full { opacity: 0; transition: opacity .6s ease; }
.rel .full.on { opacity: 1; }
.rel:hover { transform: translateY(-3px); }

@keyframes lbIn { from { opacity: 0; transform: scale(.955); } to { opacity: 1; transform: scale(1); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes panelIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
/* The phone sheet's own entrance, and **deliberately without `both`.** `.lb-panel` otherwise runs `panelIn`,
   whose fill mode holds `transform: none` on the element forever after — and an animation's value beats an
   inline style, so the finger-follows-the-drag transform portfolio.sheetDrag writes was computed, applied
   and silently ignored. Ending the animation properly hands `transform` back to the element. It rises from
   below rather than settling from above, which is the direction a sheet comes from and the direction the
   gesture that dismisses it goes. */
@keyframes sheetIn { from { opacity: .5; transform: translateY(14%); } }
@keyframes shake { 10%,90% { transform: translateX(-2px); } 20%,80% { transform: translateX(4px); } 30%,50%,70% { transform: translateX(-7px); } 40%,60% { transform: translateX(7px); } }
.shake { animation: shake .5s; }

/* ----- transient notice pill (Toast.razor) ----- */
/* z-index 120: above the lightbox backdrop (90) so "Link copied" is visible over an open photo. */
/* The DOCK is always in the DOM, so its aria-live region exists before a message is put inside it
   (Toast.razor says why). It must never be display:none or visibility:hidden — either takes the region
   out of the accessibility tree and stops it being watched at all, which is the bug this shape fixes.
   Empty it is a zero-size, click-through anchor; the pill it holds is what has a box. */
.toastdock { position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%);
             z-index: 120; pointer-events: none; }
.toast { background: rgba(16,16,20,0.92); border: 1px solid rgba(216,181,107,0.4); color: #e7cf97;
         border-radius: 100px; padding: 11px 22px; font-size: 13px; letter-spacing: .03em;
         white-space: nowrap; max-width: 92vw; overflow: hidden; text-overflow: ellipsis;
         backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
         box-shadow: 0 14px 44px rgba(0,0,0,0.5); animation: toastIn .3s ease both; }
/* The dock owns the centring transform now, so the pill's entrance only has to move itself. */
@keyframes toastIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

/* Respect a reduced-motion preference: the blur-up scale (up to 3.5s), hover lifts, slide-ins, shake, and
   the drawer slide all collapse to near-instant. Elements still reach their end state (images fade to
   opacity:1, the drawer opens) — only the motion is removed. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ----- nav ----- */
.navscroll { scrollbar-width: none; }
.navscroll::-webkit-scrollbar { display: none; }
.navtoggle { display: none; }

/* The Contact page's two columns — facts and ways in on the left, the two forms on the right —
   stack below this. */
/* The Contact page's two form cards — message, then mailing list — stack below this. The stat row
   above them folds on its own (auto-fit), so it needs nothing here. */
@media (max-width: 820px) { .contactgrid { grid-template-columns: 1fr !important; } }
/* The hamburger's breakpoint, and it is set by the *widest* the nav row can get, not by a device size:
   at 860px the eight items (All Work, New, Albums, Places, Map, Videos, Contact, Private) are as wide as
   the bar can hold beside the brand. Adding a nav item means checking this again — the row does not
   scroll and has nowhere to wrap to, so it simply runs off the right edge, which is what
   .claude/skills/verify/overflow-check.mjs catches. Splitting Map out of Places is what last moved it. */
@media (max-width: 860px) {
  .topbar { flex-wrap: wrap; row-gap: 12px; }
  .brand { order: 1; }
  .navtoggle { display: flex; order: 2; margin-left: auto; }
  .navscroll { order: 3; width: 100%; flex-direction: column; align-items: stretch !important; gap: 2px !important; overflow: hidden; max-height: 0; transition: max-height .38s ease; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07); border-radius: 14px; }
  .navscroll.open { max-height: 360px; padding: 6px 0; }
  .navscroll .navdiv { display: none; }
  .navscroll button { text-align: left; width: 100%; padding: 13px 16px !important; font-size: 15px !important; border-radius: 0 !important; }
  .navscroll .privbtn { margin: 8px 10px 6px !important; width: auto; background: rgba(216,181,107,0.1) !important; border-color: rgba(216,181,107,0.3) !important; color: #e7cf97 !important; }
}
@media (min-width: 861px) { .navscroll { max-height: none !important; } }

/* Auto-hiding top bar (mobile only). In an album the bar is ~117px of a ~650px phone viewport — brand
   on row one, album title on row two — so scrolling down retires the whole thing and any scroll up
   brings it straight back (interop.js -> autoHideBar). Desktop has the room and keeps its bar pinned,
   which is why .barhide is styled only inside this query; a class left behind by a resize does nothing.
   The reduced-motion block above already flattens this transition to a snap. */
@media (max-width: 760px) {
  .barstack { transition: transform .3s ease; }
  .barstack.barhide { transform: translateY(-100%); }
  /* Chrome stuck under the bar retires with it: left behind, the Places pane's header would hang alone
     at the top of a screen the bar has just left, which reads as a bug rather than as a header. It comes
     back on the same scroll up. Above this width the bar never hides and neither does it. */
  :root.barhide .pldetail-hd { transform: translateY(calc(-100% - var(--barh, 60px))); }
}

@media (max-width: 640px) {
  /* The backdrop is `position: fixed; inset: 0`, which on a phone browser resolves against a viewport
     that changes height as the address bar comes and goes. Stating `dvh` pins it to whatever is actually
     on screen, so the foot of the panel can never end up under the browser's own chrome. */
  .lb-back { height: 100dvh; }
  .lb-topbar { padding: 12px 16px !important; }
  .lb-title { font-size: 16px !important; }
  /* One screen, three bands, and **nothing scrolls**: the title bar, then the photograph, then the summary
     along the bottom. `overflow: hidden` rather than `auto` is what makes that a promise — with a scroll
     available the layout is free to overflow and the summary to sit below the fold. */
  .lb-body { flex-direction: column !important; flex-wrap: nowrap !important; overflow: hidden; }
  /* **The stage takes what's left, and the photograph is centred in it.** It used to be sized to the
     image instead (`flex: 0 0 auto`, capped at 62dvh), which fixed the original bug — a fixed 60vh box
     under a 16:9 frame is 470px holding a 220px picture, and the 250px of ink pushed the summary off the
     bottom of the screen — but paid for it by moving the summary: a portrait frame put it near the foot of
     the screen, the next photograph along, a 3:1 panorama, threw it up to the middle. Arrowing through a
     set therefore meant the one fixed thing on the screen was somewhere new each time, and a thumb resting
     where the chevron just was opened nothing.
     Giving the stage the remainder pins the summary to the bottom edge instead and centres every shape of
     photograph in the space above it, whatever that shape is. The summary is `flex: 0 0 auto` below, so it
     is measured first and the picture gets the rest — which is also why the old overflow can't come back:
     there is no longer a way for the two to add up to more than the screen. `min-height: 0` is what lets a
     flex item shrink below its content; without it a tall photograph pushes the summary off again. */
  .lb-stage { flex: 1 1 auto !important; height: auto !important; min-height: 34dvh !important;
              padding: 6px !important; }
  .lb-stage img { max-height: 100% !important; }
  /* No arrows on a phone: they cover a meaningful slice of an already small stage, and a horizontal
     swipe on the stage does the same job (Lightbox/VideoLightbox OnTouchStart/OnTouchEnd). */
  .lb-arrow { display: none !important; }
  /* The summary's band: it never *grows*, so it sits on the bottom edge in the same place for every
     photograph in the set — that is the whole point of the layout above. It may still shrink, because
     `.lb-panel` is also VideoLightbox's full row list on this breakpoint, which is taller than a landscape
     phone has room for; there it gives way to the stage's 34dvh floor and scrolls inside itself rather
     than pushing the picture off the screen. The photo lightbox's summary is three short lines and never
     reaches that. (The sheet — `.lb-back.lb-details .lb-panel` below — is a different animal again: fixed
     over the whole screen, and scrolling.) */
  .lb-panel { flex: 0 1 auto !important; min-height: 0 !important; overflow-y: auto !important;
              max-width: none !important; min-width: 0 !important;
              border-left: none !important; border-top: 1px solid rgba(255,255,255,0.07) !important; }

  /* ----- the phone's title, on two lines ----- */
  /* A photograph's title and the place it was taken are each as long as they are — "Coral Gardens from
     the Sandblow" beside "GREAT SANDY NATIONAL PARK, AUSTRALIA" — and side by side in a 390px bar they
     took turns wrapping to two or three words a line. Stacked, each gets the full width.
     Both lightboxes: VideoLightbox's bar carries the same two classes, and a film's name is the longer of
     the two ("Glenelg River - Cherrypool State Forest"). */
  /* The buttons take their row, the name takes the one under it — at 390px the two shared a line and the
     title was wrapping inside 190px while the place name wrapped beside it. */
  .lb-topbar { flex-wrap: wrap; row-gap: 8px; }
  .lb-heading { order: 2; flex: 0 0 100%; flex-direction: column; align-items: flex-start !important;
                gap: 3px !important; }
  .lb-topbar > div:not(.lb-heading) { order: 1; margin-left: auto; }
  .lb-place { line-height: 1.35; }

  /* ----- the details panel, as a summary and a sheet ----- */
  /* The stage takes 54vh, so the panel below it is a ~200px slot with its own scrollbar — eight rows of
     capture details read through a letterbox. Below this width the panel is therefore two things: a
     three-line summary that sits under the photo (.lb-brief), and the panel proper (.lb-detail) laid over
     the whole screen when "All details" is pressed. Both are drawn always; only CSS knows which is
     showing, so nothing about this reaches the desktop layout. */
  /* The summary is one button: the rows on the left, a chevron on the right, and the whole thing opens
     the sheet. Reset back to a block of text — a <button> brings its own centring, colour and font. */
  .lb-brief { display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
              padding: 4px 2px 6px; background: none; border: 0; cursor: pointer;
              font-family: inherit; color: inherit; }
  .lb-brief-rows { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 7px; }
  .lb-brief-chev { flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
                   width: 38px; height: 38px; border-radius: 50%; color: #c9c9cf;
                   background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); }
  .lb-brief:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: 8px; }
  .lb-back.lb-details .lb-brief { display: none; }
  .lb-detail { display: none; }
  .lb-back.lb-details .lb-detail { display: block; }
  .lb-brief-cap { display: block; color: #9a9aa1; font-size: 13.5px; font-weight: 300; line-height: 1.55; }
  .lb-brief-row { display: block; color: #c9c9cf; font-size: 12.5px; letter-spacing: .02em; }
  .lb-brief-row.nums { color: #9a9aa1; font-variant-numeric: tabular-nums; }
  /* The sheet: the panel over the photo rather than beside it, on its own opaque ground (the lightbox
     backdrop behind it is translucent, and rows over a photograph are unreadable). It scrolls itself —
     .lb-body's own scroll is what the summary uses. */
  .lb-back.lb-details .lb-panel { position: fixed; inset: 0; z-index: 8; overflow-y: auto;
                                  background: #0c0c0f; border-top: none !important;
                                  padding: 12px 20px 40px !important;
                                  animation: sheetIn .26s cubic-bezier(.2,.7,.2,1); }
  /* Sticky, and solid rather than the translucent pill the other round buttons use: it is the only way
     out of a sheet that covers the top bar, so it has to be reachable from the bottom of a long panel
     and legible over whatever row is scrolling under it. */
  .lb-sheet-hd { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; gap: 12px;
                 margin: 0 0 14px; padding: 4px 0 8px; background: #0c0c0f; }
  .lb-sheet-nm { min-width: 0; flex: 1 1 auto; font-size: 15px; color: #f2f0ec;
                 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* Hung on the right, and the same 38px disc as the top bar's close and share — one shape for "a control
     in this dialog", wherever it sits. Solid rather than the translucent fill those use: it is sticky over
     the rows scrolling under it, and it is the only way out of a sheet that covers the top bar. */
  .lb-sheet-x { flex: 0 0 auto; margin-left: auto; display: flex; align-items: center; justify-content: center;
                width: 38px; height: 38px; padding: 0; border-radius: 50%; cursor: pointer;
                background: #1b1b20; border: 1px solid rgba(255,255,255,0.14); color: #d5d5da; }
  /* A caption that IS there gets its own height here rather than the reserved two lines: a phone has no
     room to spare, and the panel below it is a sheet you opened deliberately. (An absent one is gone at
     every width — see `.lb-caption.empty` below the phone block.) */
  .lb-caption { min-height: 0 !important; }
  /* A fourth round button costs the title a line of wrap at this width. Pinch is the way in on a touch
     screen anyway — it needs no affordance — so the zoom button earns its place here only once there is a
     zoom to undo, which is the one thing a pinch can't be relied on to do comfortably. */
  .lb-back:not(.lb-zoomed) .lb-zoombtn { display: none !important; }
  .heading { font-size: 30px !important; }
}

/* The panel's phone chrome exists only below 640px (the block above turns it on): a desktop shows the
   panel itself and has no summary to replace it with and no sheet to close. */
@media (min-width: 641px) { .lb-brief, .lb-sheet-hd { display: none; } }

/* **An empty caption slot is gone at every width, not just on a phone.** The `min-height` holds two lines
   open so that arrowing through a set moves the values and not the headings — but 12 of the 1,027 published
   photographs carry a caption, so what it reserved on a desktop was a permanent 71px hole above *Album* on
   the other 1,015. A rhythm kept for something that almost never arrives reads as a panel with a piece
   missing. The cost is honest and small: on the ~1% of photographs that do have one, the rows below shift
   by the height of the caption as you arrow onto them — and on those the panel's content has changed
   anyway. Same rule in VideoLightbox, which drops its caption for the whole set at once (no curated film
   has one). */
.lb-caption.empty { display: none; }

/* ----- expanded ("full screen") lightbox ----- */
/* Toggled from Lightbox.razor — the top bar's button, F, or a double-click on the photo — and paired with a
   real Element.requestFullscreen where the browser grants one, so its own chrome goes too. Everything here is
   layout: ←/→, swipe and the arrows each keep working wherever they worked before, which is the whole point —
   expanding must not cost the ability to move through the set, nor change how you move through it. The
   selectors carry two classes so they beat the phone block's `!important` rules above on specificity rather
   than on ordering. */
.lb-back.lb-full .lb-panel { display: none; }
.lb-back.lb-full .lb-body { overflow: hidden; }
.lb-back.lb-full .lb-stage { flex: 1 1 auto !important; height: auto !important; padding: 0 !important; }
.lb-back.lb-full .lb-stage img { max-height: 100% !important; box-shadow: none !important; }
/* The bar floats over the photo rather than taking a strip of it, and fades away once nothing has happened
   for a few seconds — **on touch as well as under a cursor**. It used to stay put on touch, on the grounds
   that it holds the only visible way back out of the expanded view; what that actually produced was a
   permanent band of title and buttons across the top of the photograph, worst on a phone held landscape,
   where the bar is the same height and the photograph is a third of it. Discoverability is kept by *when*
   it fades rather than by never fading: expanding shows the bar, it goes after `idleHide`'s delay, and a
   tap anywhere on the photograph brings it back for the same interval (`portfolio.idleHide` wakes on
   `touchstart` — a tap on the stage does nothing else in this view, so the gesture costs nothing). Swipe
   and double-tap are unaffected: waking is a passive listener. */
.lb-back.lb-full .lb-topbar { position: absolute; top: 0; left: 0; right: 0; z-index: 6;
                              transition: opacity .25s ease;
                              background: linear-gradient(rgba(6,6,8,0.72), rgba(6,6,8,0)); }
.lb-back.lb-full.idle .lb-topbar { opacity: 0; pointer-events: none; }
/* Deliberately nothing here re-shows the prev/next arrows on a phone. Expanding a photo changes the
   layout, not the controls: the arrows stay hidden at phone width exactly as they are in the normal
   lightbox (the max-width:640px block above), and swipe remains the gesture — one behaviour to learn,
   and no chrome parked over a photo that now fills the screen. */
/* Real fullscreen paints onto the browser's own black backdrop, so the translucent scrim buys nothing there
   and the blur is expensive for a layer with nothing left showing through it. */
.lb-back:fullscreen { background: #060608 !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }

/* ----- zoomed photo ----- */
/* `lb-zoomed` is put on the backdrop by Lightbox.OnZoomChanged whenever the scale leaves 1x. The transform
   and the stage's clipping are set by portfolio.zoomAttach directly, not from here — they have to land on
   the same frame as the gesture, and a class round-tripping through .NET can't promise that. This is only
   what can afford to arrive a frame late.
   The arrows go because a zoomed photo is being panned under them: they'd sit over the detail you moved to,
   and a drag that starts on one is a click, not a pan. ←/→ still navigate, and so does the top bar. */
.lb-back.lb-zoomed .lb-arrow { display: none !important; }
.lb-back.lb-zoomed .lb-img { cursor: grab; }
.lb-back.lb-zoomed .lb-img:active { cursor: grabbing; }

/* ----- open album's title, centred in the top bar ----- */
.albumtitle { position: absolute; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 11px; max-width: 34vw; }
.albumback { display: flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); color: #e6e6ea; cursor: pointer; }
.albumback:hover { background: rgba(255,255,255,0.1); }
.albumname { font-size: 19px; font-weight: 500; color: #f2f0ec; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Below the nav's wrap point the bar is a two-row grid already — drop the title onto its own row rather
   than leaving it absolutely centred on top of the brand. */
@media (max-width: 760px) {
  .albumtitle { position: static; transform: none; order: 4; width: 100%; max-width: none; justify-content: center; }
  .albumname { font-size: 17px; }
}

/* ----- page shell -----
   A flex column so <main> fills the viewport on a short page. .pagelock is the
   same column pinned to exactly one viewport: nothing scrolls, and <main> passes its leftover height down
   to a view that asks for it (the places map). dvh, not vh, or a phone's collapsing toolbar leaves the
   bottom of that view under the browser chrome. */
.shell { min-height: 100vh; display: flex; flex-direction: column;
         background: radial-gradient(130% 90% at 50% -12%, #16161b 0%, #0b0b0d 58%); }
.pagelock { height: 100dvh; min-height: 0; overflow: hidden; }
.pagelock main { min-height: 0; display: flex; flex-direction: column; }

/* ----- albums index (AlbumsView) -----
   The album covers are a menu, not a document: the mosaic takes exactly the height the nav leaves, so
   the whole set is on the first screen and there is nothing to scroll to. .alblock is .pagelock's
   sibling — the same "exactly one viewport" column — and, like .filmlock, it hands the lock back
   on a window that cannot hold the set. Only a media query knows that, hence a class rather than a flag
   in C#. dvh, not vh, or a phone's collapsing toolbar hides the bottom row. */
.alblock { height: 100dvh; min-height: 0; overflow: hidden; }
.alblock main { min-height: 0; display: flex; flex-direction: column; }
.albsec { flex: 1 1 auto; min-height: 0; display: flex; overflow: hidden;
          width: 100%; padding: 14px var(--gutter) 18px; }
/* Columns and rows come from AlbumsView, which derives them from the album count; both are 1fr tracks, so
   the tiles divide the fixed height between them. `dense` backfills the hole a span-2 tile leaves when it
   wraps off the end of a row. */
.albmosaic { flex: 1 1 auto; min-height: 0; display: grid; gap: 10px; grid-auto-flow: row dense; }

/* Too narrow or too short to hold the whole set at a readable size: the lock comes off, the mosaic's
   uneven tiles flatten to an ordinary two-column wall, and the page scrolls. The inline grid tracks are
   the ones being overridden, so these need !important. */
@media (max-width: 900px), (max-height: 440px) {
  .alblock { height: auto; min-height: 100vh; overflow: visible; }
  .alblock main { display: block; }
  /* 90px at the foot, matching the photograph grid — the other screens that end on a picture rather than
     on ground get the same tail, so an over-pull at the bottom stretches ink instead of a photograph. See
     the note in the stacked home layout. */
  .albsec { display: block; overflow: visible; padding: 20px var(--gutter) 90px; }
  .albmosaic { grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
               grid-template-rows: none !important; grid-auto-rows: minmax(150px, 26vw); }
  .albmosaic .frame { grid-column: span 1 !important; grid-row: span 1 !important; }
}

/* ----- sticky header stack (nav + desktop filter bar) ----- */
.barstack { position: sticky; top: 0; z-index: 41; background: rgba(11,11,13,0.82); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); border-bottom: 1px solid rgba(255,255,255,0.06); }
/* Over the home hero the bar floats: `fixed` takes it out of the flow so the hero starts at the very top of
   the viewport, and the solid background gives way to a scrim that fades to nothing — the photograph runs
   behind the brand rather than stopping under it. MainLayout adds the class (never while gated, see there). */
.barstack.overhero { position: fixed; left: 0; right: 0; border-bottom: none;
  background: linear-gradient(180deg, rgba(6,6,9,0.72) 0%, rgba(6,6,9,0.38) 62%, rgba(6,6,9,0) 100%); }
/* The phone nav drops out of that same bar. A menu of links over a photograph, on a background that has
   faded to transparent by its first row, is unreadable — so an open menu takes the solid bar back. */
.barstack.overhero:has(.navscroll.open) { background: rgba(11,11,13,0.92); }

/* ----- home (HomeView) ----- */
/* One screenful: the hero takes whatever height the split strip below it doesn't, so the two bodies of
   work are visible without scrolling. Below the widths/heights that can hold that, the section stops
   being a fixed screen and simply stacks (see the media queries at the end of this block). */
.homeone { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.homehero { position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden; background: var(--ink); }

/* The frames are stacked and crossfaded, so each one owns its opacity and the blur-up inside it owns its
   own (the .frame/.rel rules can't be reused here: .frame draws a hover ring and .rel lifts on hover). */
.homelayer { position: absolute; inset: 0; opacity: 0; transition: opacity 1.5s ease; }
.homelayer.on { opacity: 1; }
.homelayer .full { opacity: 0; transition: opacity .9s ease; }
.homelayer .full.on { opacity: 1; }
.homelayer .ph { filter: blur(20px); transform: scale(1.1); }
/* Slow drift, so a frame that sits for six seconds isn't a still. Alternating, so it never snaps back. */
/* Starts a hair over 1, not at 1. A layer is `inset: 0` over the section's ink, and at exactly scale(1) a
   fractional layout height (which is what a phone's collapsing toolbar produces) can leave a sub-pixel row
   of that ink showing under the photograph — a hairline at the bottom edge, appearing and going as the
   height lands either side of a whole pixel. 1.005 is invisible and covers it. */
.kbz { animation: kb 26s ease-in-out infinite alternate; }
@keyframes kb { from { transform: scale(1.005); } to { transform: scale(1.09); } }

/* Dark at the top (under the floating chrome) and at the bottom (under the caption), barely there across
   the middle — the photograph is the point.

   **It hangs 8px below the hero, and that is load-bearing.** `.homelayer` carries a transform animation, so
   the browser gives the photograph a compositing layer of its own while this shade is painted in the
   parent. Anything that moves the two by a rounding error — an elastic overscroll stretching the page is
   the one that does it — slides the picture out from under the shade by a pixel at the bottom edge, and
   what shows there is a row of *unshaded* photograph: measured at rgb(192,183,174) off a cloudscape,
   against ink. That is the flickering hairline, and it is bright because it is the picture itself, not the
   dark version of it anyone was looking for. The overhang is clipped by `.homehero`'s `overflow: hidden`,
   so it changes nothing at rest; it just cannot be out-run by a pixel or two. */
.homeshade { position: absolute; inset: 0 0 -8px 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(6,6,9,0.62) 0%, rgba(6,6,9,0.04) 30%, rgba(6,6,9,0.14) 60%, rgba(6,6,9,0.88) 100%); }

.homecap { position: absolute; right: clamp(20px, 4vw, 54px); bottom: clamp(22px, 3.5vh, 42px);
  display: flex; flex-direction: column; align-items: flex-end; gap: 20px; text-align: right; }
.homecaptitle { font-size: 20px; font-weight: 500; }
.homecaploc { font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase; color: #c7b787; margin-top: 7px; font-weight: 500; }
.homedots { display: flex; align-items: center; gap: 9px; }
/* A 2px rule is the mark, not the target: content-box padding gives the button an 18px-tall hit area on a
   touch screen while `background-clip` keeps the paint on the 2px content box. */
.homedot { box-sizing: content-box; width: 12px; height: 2px; padding: 8px 0; border: none; cursor: pointer;
  border-radius: 2px; background: rgba(255,255,255,0.35); background-clip: content-box;
  transition: width .4s ease, background-color .4s ease; }
.homedot.on { width: 26px; background-color: var(--gold); }

.homesplit { flex: 0 0 auto; display: grid; grid-template-columns: 1fr 1px 1fr; border-top: 1px solid rgba(255,255,255,0.07); }
/* No curated videos: the photographs take the whole strip rather than half of it next to a gap. */
.homesplit.solo { grid-template-columns: 1fr; }
.homediv { background: rgba(255,255,255,0.07); }
.homehalf { padding: 22px clamp(20px, 3vw, 44px) 26px; min-width: 0; }
.homehead { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
.homehead h2 { margin: 0; font-size: 24px; font-weight: 500; }
.homemore { background: none; border: none; padding: 0; cursor: pointer; font-family: inherit; font-size: 12.5px; color: #e7cf97; letter-spacing: .03em; }
.homemore:hover { color: var(--gold-bright); }

.homereels { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.homereels.films { grid-template-columns: repeat(2, 1fr); }
/* The tile's shape is pinned, not its height. A fixed height against a fluid width meant the tile itself
   swung from 1.74:1 at 1920px to 0.62:1 — taller than it is wide — at 1000px, so `object-fit: cover` had to
   re-crop the photograph at every width: at 1920 a landscape frame was reduced to a 38%-tall band of itself,
   at 1000 it was cropped side-to-side instead. Nothing was ever distorted, but the framing moved as you
   dragged the window, which is the part that reads as wrong. With the ratio fixed, a resize scales the
   photograph and the crop stays put.
   3:2 is the camera's own shape, and it is what the old fixed height happened to produce at ~1440px — so
   this is the desktop look as it already was, minus the movement. */
.homereel { position: relative; aspect-ratio: 3 / 2; overflow: hidden; cursor: pointer; background: #161619; transition: transform .25s ease; }
/* 16:7 rather than the source's 16:9: the films half is two columns to the photographs' three, so its tiles
   are ~1.5× wider, and 16:7 is the ratio that lands both strips on the same height at every width. Widening
   the crop costs nothing here — .homefilmimg is already overscanned to cut hqdefault's letterbox. */
.homereels.films .homereel { aspect-ratio: 16 / 7; }
.homereelshade { position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(6,6,9,0) 48%, rgba(6,6,9,0.85) 100%); }
.homereelcap { position: absolute; left: 15px; right: 15px; bottom: 14px; pointer-events: none; }
.homereeltitle { font-size: 16px; line-height: 1.15; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Wraps at spaces over two lines rather than ellipsising on one. A card's location line is between 79px
   (three-up on a phone) and 321px wide depending on the viewport, so no single string length fits it
   everywhere — the C# side picks the finest place that fits a rough budget (DisplayLocation(p, maxChars)),
   and this catches what that can't know: at 390px "HAWKWOOD, AUSTRALIA" needs 144px and used to render as
   HAWKWOO…, where wrapping shows both words whole. Two lines is the cap; a third would crowd the title. */
.homereelloc { font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase; color: #c7b787; margin-top: 5px; font-weight: 500;
  overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  overflow-wrap: normal; word-break: keep-all; }
/* YouTube's hqdefault is 4:3 with the 16:9 frame letterboxed into the middle three-quarters of its height.
   Blowing it up to 133% and pulling it up by a sixth lands that frame on the tile, so the black bars are
   cropped out whatever shape the tile is — VideosView gets the same for free from its 16:9 aspect-ratio,
   but these tiles are sized by the strip and pass through 4:3 on the way. */
.homefilmimg { position: absolute; left: 0; top: -16.67%; width: 100%; height: 133.34%; object-fit: cover; }

/* Narrow, or simply short: there is no longer a screenful to divide, so the hero takes a fixed slice of the
   viewport and the strip scrolls in underneath it. The height query catches a laptop in landscape, where the
   width alone would have kept the one-screen layout and clipped the strip against `overflow: hidden`. */
@media (max-width: 1000px), (max-height: 640px) {
  .homeone { height: auto; overflow: visible; }
  /* **No rule under the photograph.** `.homesplit` draws a hairline between itself and whatever is above
     it, which on a desktop separates the strip from the hero and reads as a divider. Below this width the
     hero is a full-bleed picture whose shade has already faded to near-black by its own bottom edge, and a
     1px line of white at 7% over that is not a divider — it is a slim light bar along the foot of the
     photograph, three device pixels of it on a 3x phone. It also shimmers as the address bar moves, since
     the hero's height changes by fractions and the line lands on a different device-pixel boundary each
     time. The strip's own padding is the separation here.

     **And the page ends on ink, not on a photograph.** Scroll to the foot of this page on a phone and keep
     pulling: the browser's elastic overscroll stretches the content away from the edge and repeats its last
     row of pixels along it. With 26px between the last film tile and the end of the document, what got
     dragged to the edge and smeared along it was a bright still — the "distortion" and the light line at
     the bottom of the screen. It is not something CSS can suppress selectively (the only lever,
     `overscroll-behavior`, works by cancelling the rubber-band and takes pull-to-refresh with it), but it
     does not need suppressing: a stretch of flat ink looks exactly like flat ink. 90px is what the
     photograph grid has always ended with, which is why this was only ever visible here. */
  .homesplit { border-top: none; padding-bottom: 90px; background: var(--ink); }
  /* **The hero has to END on the colour the page continues in.** Its shade stops at 88% of #060609 over
     the photograph, so the last row painted is whatever the picture is doing there — rgb(23,18,14) under a
     cloudscape, against the rgb(11,11,13) of the section below it. That step is a clean edge while
     everything holds still, and it is the *flickering line* the moment anything moves: an elastic
     overscroll shears the composited layers past each other by a pixel or two, and a lighter row smeared
     across a darker one is exactly what a hairline looks like. Taking the shade all the way to opaque ink
     over its last stops leaves ~14px of ground at the foot of the hero that is literally the same colour
     as the strip below it and as the hero's own background, so there is nothing left for a shear to
     reveal. `.homesplit` paints that ink itself rather than letting the shell's gradient show through, for
     the same reason: every surface meeting at that line is now one colour.
     Scoped to the stacked layout — on a desktop the hero and the strip meet on a deliberate hairline, and
     nothing shears there. */
  .homeshade { background: linear-gradient(180deg, rgba(6,6,9,0.62) 0%, rgba(6,6,9,0.04) 30%,
                                                   rgba(6,6,9,0.14) 60%, rgba(6,6,9,0.88) 94%,
                                                   var(--ink) 98%, var(--ink) 100%); }
  /* And the layer that shears does not exist here. The overhang above covers the symptom; this removes the
     cause — with no transform animation the photograph is painted in the same layer as everything around
     it, and there is nothing left to slide against anything. The cost is the hero's slow drift, on the one
     class of device that scrolls past it rather than sitting on it, and it is one rule to put back. */
  .homelayer.kbz { animation: none; transform: none; }
  /* **`svh`, not `vh`.** A phone's address bar retires as you scroll and comes back as you reach the
     bottom, and `vh` is the *large* viewport — the one with it gone — so the hero was 60-odd pixels
     taller than the screen at rest and shrank the moment the bar returned. Every scroll that moved the
     bar therefore re-laid the hero out, and the seam where its photograph meets the strip below flickered
     as a hairline of the section's own ink showed through and vanished. `svh` is the viewport with the
     bar *showing*, which is the one measurement a scroll cannot change. */
  .homehero { flex: 0 0 auto; height: 88vh; height: 88svh; }
  /* Back to a fixed height below the one-screen layout, deliberately: down here the two halves are still
     side by side while each holds three columns, so a 3:2 tile would be about 124×83 at 900px — too small
     to carry the caption. The crop therefore still shifts with width in this band; it matters far less
     than it did above, because these are widths a device *has* rather than widths a window is dragged
     through. Ratio is explicitly released, or it would fight the height. */
  .homereel, .homereels.films .homereel { aspect-ratio: auto; height: 200px; }
}
@media (max-width: 760px) {
  .homesplit { grid-template-columns: 1fr; }
  .homediv { display: none; }
  .homehero { height: 78vh; }
}
/* Three columns across a phone leaves each tile narrower than it is tall; shorten them so the crop stays
   roughly landscape rather than a letterbox slice of each photograph. */
@media (max-width: 520px) { .homereel, .homereels.films .homereel { aspect-ratio: auto; height: 130px; } }

/* ----- desktop filter bar ----- */
/* The bar and the drawer are the same filters at two widths, so exactly one of them exists at a time —
   below 760px the bar is gone and the FAB/drawer take over, above it the reverse. Swapping in CSS rather
   than measuring the viewport in C# keeps a resize from needing a re-render to look right. */
/* Wraps rather than overflows: between the breakpoint and about 900px the search box plus three
   buttons plus "Clear all" don't fit on one line, and a nowrap bar pushes the clear link off the
   right edge with nothing able to scroll it back. */
.filterbar { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 4px var(--gutter) 14px; }
/* The tray is part of the bar, not the drawer: they follow it up and down the breakpoint together, or a
   phone gets the desktop bar's overflow with no bar above it. */
@media (max-width: 760px) { .filterbar, .fb-refine { display: none; } }
@media (min-width: 761px) { .fabdock, .filterside, .filterbackdrop { display: none !important; } }

.fb-drop { position: relative; flex: 0 0 auto; }
/* Sized to its content, not to a fixed 400px: a branch with one or two chips was rendering as a mostly
   empty box that reads as a layout bug. min-width keeps a one-chip panel from being a sliver, and the
   max-width still wraps a long chip list at the old width. Panels that are lists rather than chip rows
   (search suggestions, sort) pin their own width inline, which wins over this. */
.fb-panel { position: absolute; top: calc(100% + 10px); left: 0; width: max-content; min-width: 220px; max-width: min(400px, 88vw); max-height: 60vh; overflow-y: auto; background: rgba(13,13,16,0.98); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 16px 18px 18px; box-shadow: 0 24px 70px rgba(0,0,0,0.55); z-index: 50; animation: panelIn .28s cubic-bezier(.2,.7,.2,1) both; }
.fb-btn { display: flex; align-items: center; gap: 7px; border-radius: 100px; cursor: pointer; padding: 9px 15px; font-size: 12.5px; letter-spacing: .02em; white-space: nowrap; font-family: inherit; }
.fb-count { font-variant-numeric: tabular-nums; font-weight: 700; color: #17140d; background: var(--gold); border-radius: 100px; min-width: 17px; height: 17px; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; padding: 0 5px; }
.fb-panelsearch { width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); border-radius: 100px; color: #f2f0ec; font-size: 12.5px; padding: 8px 12px 8px 31px; font-family: inherit; }
.fb-grouplabel { font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: #5c5c63; font-weight: 600; }
.fb-link { background: none; border: none; color: #e7cf97; cursor: pointer; font-size: 12px; letter-spacing: .03em; text-decoration: underline; text-underline-offset: 3px; padding: 0; font-family: inherit; }
/* The Refine tray: a band across the full bar width, not a panel hanging off its button. Five sections of
   chips have nowhere to go in a dropdown, and expanding in place keeps them on screen while the grid
   reflows behind. Bordered top and bottom so it reads as an extension of the bar rather than as content. */
.fb-refine { display: flex; flex-direction: column; gap: 16px; padding: 16px var(--gutter) 18px; border-top: 1px solid rgba(255,255,255,0.07); border-bottom: 1px solid rgba(255,255,255,0.07); background: rgba(255,255,255,0.02); animation: panelIn .28s cubic-bezier(.2,.7,.2,1) both; }

.sugg { display: flex; align-items: center; gap: 11px; width: 100%; text-align: left; background: none; border: none; cursor: pointer; padding: 8px 10px; border-radius: 9px; color: #e6e6ea; font-size: 13px; font-family: inherit; }
.sugg:hover, .sugg.on { background: rgba(255,255,255,0.06); }
/* The keyboard's highlight has to be visible without a pointer over it: aria-activedescendant moves the
   screen reader's cursor but leaves DOM focus in the input, so there is no :focus-visible ring to rely on. */
.sugg.on { box-shadow: inset 0 0 0 1px rgba(216,181,107,0.45); }

/* ----- filter drawer (right slide-out) + FAB ----- */
.filterbackdrop { position: fixed; inset: 0; z-index: 70; background: rgba(4,4,6,0.55); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); animation: fadeIn .3s ease both; }
/* A flex column, not a single scroll box: the header/search/sort stay pinned and the facet accordion
   below takes whatever height is left and scrolls on its own, so scrolling to a facet never scrolls the
   search box away. */
/* `visibility: hidden` while closed is what takes the off-screen panel out of the tab order for anything
   that doesn't support `inert` (FilterDrawer sets that too). It is delayed by the length of the slide so
   the drawer is still paintable on the way out; opening restores it instantly. */
.filterside { position: fixed; top: 0; bottom: 0; width: 340px; max-width: 88vw; z-index: 75; background: rgba(13,13,16,0.97); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); display: flex; flex-direction: column; overflow: hidden; padding: 24px 24px 34px; transition: transform .42s cubic-bezier(.2,.7,.2,1), visibility 0s linear .42s; right: 0; border-left: 1px solid rgba(255,255,255,0.08); transform: translateX(103%); visibility: hidden; box-shadow: -30px 0 90px rgba(0,0,0,0.55); }
.filterside.open { transform: none; visibility: visible; transition: transform .42s cubic-bezier(.2,.7,.2,1), visibility 0s; }
/* Pinned: the drawer's title row and the search box under it. The margin comes off — the search box
   carries its own 24px, and this used to double it. */
.filterhead { flex: 0 0 auto; }
/* Everything else in the drawer, as one scrolling column: Show, Sort and the facet accordion. This is
   the drawer's only scrollbox — .filterfacets used to be a second one nested in the leftover height,
   which on a phone was a few chips tall under a header taking half the screen. */
.filterscroll { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; min-height: 0; }
/* A chip is `white-space: nowrap` everywhere else — a desktop dropdown is as wide as it needs to be, and a
   wrapped chip there looks broken. The drawer is 291px of a phone, and `Fowlers Bay Conservation Park` or
   `Sky & Weather` is wider than that, so nowrap gave the whole column a horizontal scrollbar and put the
   count on the far side of a swipe. Here the label wraps inside the pill instead: `!important` because the
   chips carry the rule as an inline style (one shared ChipRow renders the bar's and the drawer's alike),
   and `align-items: center` keeps the count beside a two-line label rather than under its last word. */
.filterside .chip { white-space: normal !important; text-align: left; max-width: 100%; }
.filterscroll > .filterfacets { margin-top: 26px; }
/* One scroll box for the whole accordion. Giving each section its own (as the old two-list drawer did)
   only works for two of them — across nine it leaves every section a few chips tall. */
.filterfacets { flex: 0 0 auto; }
.filteracc { border-bottom: 1px solid rgba(255,255,255,0.06); }
.filteracc-head { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; background: none; border: none; cursor: pointer; padding: 14px 2px; font-family: inherit; transition: background .2s ease; }
.filteracc-head:hover { background: rgba(255,255,255,0.03); }
/* The Location levels, as an accordion nested one deep inside the Location section. Quieter than the
   sections around it — a fainter rule, a shorter row, and the group label's own small caps rather than the
   section heading's — so the nesting is legible as nesting rather than as a second list of facets. The last
   level drops its rule: the section's own border is directly under it, and two hairlines 1px apart read as
   a mistake. */
.filterlevels { display: flex; flex-direction: column; }
.filterlevel { border-bottom-color: rgba(255,255,255,0.045); }
.filterlevel:last-child { border-bottom: none; }
.filterlevel > .filteracc-head { padding: 11px 2px; }
.fabdock { position: fixed; right: 20px; bottom: 20px; z-index: 72; display: flex; align-items: center; gap: 10px; }
.filterfab { display: flex; align-items: center; gap: 9px; background: var(--gold); color: #17140d; border: none; border-radius: 100px; padding: 14px 20px; font-size: 13.5px; font-weight: 600; letter-spacing: .02em; cursor: pointer; box-shadow: 0 14px 44px rgba(216,181,107,0.3); font-family: inherit; transition: transform .2s ease; }
.filterfab:hover { transform: translateY(-2px); }
.filterfab .badge { background: #17140d; color: #e7cf97; border-radius: 100px; min-width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; padding: 0 6px; font-variant-numeric: tabular-nums; }
/* Outlined, not filled: it's the escape hatch next to the FAB, and shouldn't compete with it for the eye. */
.filterclear { display: flex; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 50%; background: rgba(13,13,16,0.9); border: 1px solid rgba(216,181,107,0.45); color: #e7cf97; cursor: pointer; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); box-shadow: 0 10px 30px rgba(0,0,0,0.4); transition: transform .2s ease, background .2s ease; animation: fadeIn .25s ease both; }
.filterclear:hover { transform: translateY(-2px); background: rgba(216,181,107,0.16); }

/* ----- places view (the location index) -----
   Full-bleed, unlike the centred columns elsewhere: the rows are a cover, a name and a count with the
   chevron pushed to the far edge, and they read as an index of the whole window rather than a document.
   The chip rows under them simply wrap into whatever width there is. */
.placespage { width: 100%; padding: 20px var(--gutter) 90px; }

/* The index is a browser, not a document: .pllock (MainLayout) pins the shell to exactly one viewport the
   way .pagelock and .alblock do, and this passes that height down to .plbrowse so the scrolling happens
   inside the three columns rather than moving the whole page under them. Without it the detail pane was
   the one column with no ceiling — a town with a long list grew the grid past the window, so opening it
   scrolled the header, the breadcrumb and the other two columns away. The lock comes off below 1180px,
   where the same DOM is a push navigation and the page scroll is the design. */
.pllock { height: 100dvh; min-height: 0; overflow: hidden; }
.pllock main { min-height: 0; display: flex; flex-direction: column; }
.pllock .placespage { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
                      padding-bottom: clamp(12px, 2vh, 24px); }
.pllock .plhead { flex: 0 0 auto; }

/* Map mode is a screen, not a section: MainLayout locks the page to the viewport (.pagelock), and this
   column takes the height that leaves. Everything but the map is its natural height, so
   the map gets the remainder — and gives some of it back when a pin's card opens, which is what keeps the
   card visible without the map being covered by it. */
.placespage-map { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
                  padding-top: clamp(12px,2vh,26px); padding-bottom: clamp(12px,2vh,26px); }
.placespage-map > * { flex: 0 0 auto; }

/* The cover + name + count is one button rather than three tap targets: on a phone the whole row should
   act, and a chevron small enough to look right is too small to aim at. This is the map panel's row —
   the Places index has its own rows (.plrow below), where picking one moves along the index rather than
   leaving for the gallery. */
.placehead { display: flex; align-items: center; gap: 16px; width: 100%; background: none; border: 0; padding: 0; margin: 0; color: var(--paper); font-family: inherit; cursor: pointer; text-align: left; }
/* BlurUpImage positions its two layers absolutely, so the square that crops them has to be the
   containing block — same contract as .frame in the grid. */
.placecover { flex: 0 0 auto; position: relative; overflow: hidden; width: 76px; height: 76px; border-radius: 2px; background: #161619; }
.placechev { margin-left: auto; flex: 0 0 auto; display: flex; color: #7d7d84; transition: transform .25s ease, color .25s ease; }
button.placehead:hover .placechev { transform: translateX(3px); color: var(--gold); }
button.placehead:hover .serif { color: var(--gold-bright); }
.placehead:focus-visible { outline: 2px solid var(--gold); outline-offset: 5px; }

/* ----- places view: the three-column browser -----
   States, then that state's towns, then what is filed under the chosen town. Each column narrows the one
   to its right, so the whole hierarchy is on one screen and no column ever grows past its own scroll.
   The DOM is written once; below 1180px the media query at the end of this block re-lays it out as a
   slide-in state panel plus a two-step town → detail push, which is the same information in the shape a
   phone can hold. */
.plhead { display: flex; align-items: center; gap: 18px; padding: 8px 0 16px; flex-wrap: wrap; }
.plcrumb { display: flex; align-items: baseline; gap: 11px; min-width: 0; flex: 1 1 auto; flex-wrap: wrap; }
.pleyebrow { font-size: 10px; letter-spacing: .24em; text-transform: uppercase; color: #c7b787; font-weight: 700; }
.plsep { color: #4e4e55; }
.plcrumb-nm { font-size: clamp(20px,4vw,26px); line-height: 1; font-weight: 500; }
.plcrumb-town { display: contents; }
.plcrumb-town .plcrumb-nm { color: var(--gold-bright); }
/* The search box. It filters the town column across every state — not only the open one — matching what
   is inside a town as well as its name, so a park you remember finds the town it was filed to. It is written twice — once in the header, once in the
   state panel — and exactly one is ever on show: the wide header has room for it beside the breadcrumb,
   the narrow one doesn't and the panel behind the pill is where you go to say which places you want. */
.plsearch { display: flex; align-items: center; gap: 9px; background: rgba(255,255,255,0.04);
            border: 1px solid rgba(255,255,255,0.12); border-radius: 100px; padding: 9px 14px;
            width: 236px; flex: 0 0 auto; }
.plsearch-panel, .plqchip { display: none; }
.plsearch input { flex: 1; min-width: 0; background: none; border: none; outline: none; color: var(--paper);
                  font-size: 13px; font-weight: 300; font-family: inherit; padding: 0; }
.plsearch:focus-within { border-color: rgba(216,181,107,0.5); }

/* The town column is sized to the longest town in the catalogue rather than to a round number — a name
   like "Orchid Beach Island Village" ellipsised in a column whose job is to be read is the one thing this
   list must not do. It still ellipsises past that; the alternative is a column that reflows every time a
   longer place is published. */
/* One grid row, explicitly shrinkable (minmax(0,1fr)) — an auto row would size to the tallest column's
   content and hand every column a height its own overflow could never cap. The height itself comes from
   the locked shell above (.pllock); where the lock is off — a short or narrow window — the row is simply
   as tall as its content and the page scrolls, which is what it did before the lock existed. The old
   620px floor went with it: under the lock it was a floor above the window on a laptop. */
.plbrowse { display: grid; grid-template-columns: 300px 284px minmax(0,1fr); grid-template-rows: minmax(0,1fr);
            align-items: stretch; flex: 1 1 auto; min-height: 0;
            border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; overflow: hidden;
            background: rgba(11,11,13,0.6); }
.plcol { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 2px; padding: 14px 12px 24px; }
.plregions, .pltowns { border-right: 1px solid rgba(255,255,255,0.07); overflow-y: auto; }
/* The scrollbars belong to the furniture, not to the work: a track and a bright thumb down the side of
   every column is the loudest edge on a dark screen. Thin, no track, and a thumb barely above the
   column's own border until you are over it. Not .navscroll's hide-it-entirely — these columns are tall
   enough that a reader needs to be told there is more below. */
.plregions, .pltowns, .pldetail-body { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.13) transparent; }
.plregions::-webkit-scrollbar, .pltowns::-webkit-scrollbar, .pldetail-body::-webkit-scrollbar { width: 8px; }
.plregions::-webkit-scrollbar-track, .pltowns::-webkit-scrollbar-track,
.pldetail-body::-webkit-scrollbar-track { background: transparent; }
.plregions::-webkit-scrollbar-thumb, .pltowns::-webkit-scrollbar-thumb,
.pldetail-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.13); border-radius: 100px;
                                          border: 2px solid transparent; background-clip: padding-box; }
.plregions:hover::-webkit-scrollbar-thumb, .pltowns:hover::-webkit-scrollbar-thumb,
.pldetail-body:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.22); background-clip: padding-box; }
/* The column headings — "Australia", "Towns" — and the rule that runs out from them. */
.plcolhd { display: flex; align-items: baseline; gap: 9px; padding: 1px 8px 9px; flex: 0 0 auto; }
/* A band with no country heading — a country that divides into nothing, so its name is on its single row
   rather than above it. It still needs to read as a band: without the rule, New Zealand's one row sits
   directly under New South Wales and inside Australia's heading. Its rule is GOLD rather than the white
   hairline the other headings carry, because it is doing the whole job on its own — the others have a
   name beside them saying what they divide, and this has nothing but the line. Gold is the pane's one
   structural colour (.pltownsec-hd rules a town band with it for the same reason). */
.plcolhd-bare { margin-top: 4px; }
.plcolhd-bare .plrule { background: rgba(216,181,107,0.32); }
.plcolhd + .plcolhd, .plrow + .plcolhd { padding-top: 17px; }
.plcolhd > span:first-child { font-size: 9.5px; letter-spacing: .24em; text-transform: uppercase;
                              color: #c7b787; font-weight: 700; }
.plcolhd-n { font-size: 11px; color: #5c5c63; font-variant-numeric: tabular-nums; }
/* A state's name over its run of search results. Quieter than the column's own heading — it is a divider
   inside a list, not a second heading competing with "Matches" above it. */
.plmatchhd > span:first-child { color: #8a8a91; letter-spacing: .18em; }
.plrule { flex: 1 1 auto; height: 1px; background: rgba(255,255,255,0.08); }
.plempty { padding: 30px 8px; color: #7d7d84; font-size: 12.5px; }

/* A row is two controls, not one: the row picks (moves along the index), the count pill leaves for the
   gallery. They are siblings rather than nested because a button inside a button is not HTML. */
.plrow { display: flex; align-items: center; gap: 8px; border-radius: 6px; border-left: 2px solid transparent;
         flex: 0 0 auto; transition: background .2s ease; }
.plrow:hover { background: rgba(255,255,255,0.04); }
.plrow.on { background: rgba(216,181,107,0.09); border-left-color: var(--gold); }
.plrow-pick { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 12px; background: none;
              border: 0; color: #c9c9cf; font-family: inherit; text-align: left; cursor: pointer; padding: 10px; }
.plrow.on .plrow-pick { color: var(--paper); }
.plrow-pick:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; border-radius: 6px; }
.plthumb { flex: 0 0 auto; position: relative; overflow: hidden; width: 44px; height: 44px;
           border-radius: 4px; background: #161619; }
.plthumb-lg { width: 56px; height: 56px; }
.plregion-nm { flex: 1; min-width: 0; font-size: 18px; line-height: 1.15; font-weight: 500; color: inherit; }
.pltown .plrow-pick { padding: 7px 9px; }
.pltown-nm { flex: 1; min-width: 0; font-size: 13px; font-weight: 300; overflow: hidden;
             text-overflow: ellipsis; white-space: nowrap; }

/* The count, and the way out of the index into the filtered gallery. Outward arrow rather than a chevron:
   a chevron here would read as "opens further in", which is what the row beside it does. */
.plpill { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; margin-right: 9px;
          border: 1px solid rgba(216,181,107,0.28); border-radius: 100px; padding: 3px 8px; background: none;
          font-family: inherit; font-size: 11px; color: #c7b787; font-variant-numeric: tabular-nums;
          cursor: pointer; transition: background .2s ease, border-color .2s ease, color .2s ease; }
.plpill-lg { padding: 4px 9px; font-size: 11.5px; }
.plpill:hover { background: rgba(216,181,107,0.16); border-color: var(--gold); color: #f0dcae; }
.plpill:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* ----- places view: the detail pane ----- */
/* A column of its own: a fixed header, then a body that takes the rest and scrolls. This is the pane the
   long towns overflow, so its ceiling is the one that matters — see .plbrowse's single 1fr row. */
/* gap: 0, unlike the other two columns: those are lists of rows that want air between them, this one is a
   header with a scrollport under it, and .plcol's 2px left a slot of pane between the header's rule and
   the first town's band for scrolling photographs to show through. */
.pldetail { padding: 0; display: flex; flex-direction: column; min-height: 0; gap: 0; }
.pldetail-hd { display: flex; align-items: center; gap: 18px; padding: 18px 26px 14px; flex: 0 0 auto;
               border-bottom: 1px solid rgba(255,255,255,0.07); }
.pldetail-nm { font-size: clamp(22px,3vw,28px); line-height: 1.05; font-weight: 500; }
.pldetail-meta { font-size: 11.5px; color: #8a8a91; margin-top: 4px; }
.pldetail-hd .plpill { margin-left: auto; margin-right: 0; }
/* Container, not viewport: the strips below are cut to fit this pane, which is a fraction of the window
   on a wide screen and the whole of it on a narrow one. */
/* No padding above the first town: its heading is a band that must meet the pane header's rule exactly,
   or the 2px of pane showing between them becomes a slot that scrolling photographs slide through. */
.pldetail-body { flex: 1 1 auto; min-height: 0; padding: 0 26px 26px; min-width: 0;
                 container-type: inline-size; overflow-y: auto; overscroll-behavior: contain; }
/* One town inside the open council. The pane is a run of these now that the column lists councils, so a
   town has to read as a level ABOVE the .plblock-lbl headings under it — and at 17px on a hairline it
   didn't: the gold uppercase "PLACES IN …" label two lines below was the louder mark on the screen, so
   the pane scanned as a flat list of parks with town names lost among them. Four things carry the rank
   now, none of them shouting: the name is bigger and in the paper white the pane's own title uses; the
   heading sits on a band that bleeds to both edges of the pane, so a town starts with a visible ledge
   rather than a gap; the band is ruled in gold rather than white, which is the pane's one structural
   line; and it STICKS to the top of the scroll while its own town is on screen — a council can run to
   several screens, and the question a visitor has halfway down one is which town they are in. The blocks
   under it are indented to sit inside that ledge (see .pltownsec .plblock), so the nesting is visible
   without a second heading style. */
.pltownsec { padding-top: 30px; }
.pltownsec:first-child { padding-top: 0; }
.pltownsec-hd { position: sticky; top: 0; z-index: 2; display: flex; align-items: baseline; gap: 11px;
                margin: 0 -26px; padding: 11px 26px 9px;
                background: rgba(12,12,15,0.985); backdrop-filter: blur(10px);
                -webkit-backdrop-filter: blur(10px);
                border-bottom: 1px solid rgba(216,181,107,0.32); }
.pltownsec-nm { font-size: 21px; font-weight: 500; line-height: 1.2; color: var(--paper);
                flex: 0 1 auto; min-width: 0;
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pltownsec-meta { font-size: 11.5px; color: #8a8a91; white-space: nowrap; }
/* The heading's own border is the rule now, so the one in the markup would draw a second, shorter line
   through the middle of the band. The pill still needs the auto margin the rule used to provide. */
.pltownsec-hd .plrule { display: none; }
.pltownsec-hd .plpill { margin-left: auto; margin-right: 0; align-self: center; }
/* Nested inside the town's ledge rather than starting on the same margin as it. Small enough that the
   rows still line up with each other down the pane, big enough to say what contains what. */
.pltownsec .plblock { padding-left: 14px; }
.plblock { padding-top: 18px; }
/* The town's own work leads its section with no heading over it — the town heading directly above has
   just named it — so it sits closer to that heading than a headed block would. Its row is an ordinary
   .plitem, so it lines up with every row below it on both margins; it used to be a bare strip hung on
   the left. */
.plblock-own { padding-top: 6px; }
.plblock-hd { display: flex; align-items: center; gap: 11px; padding: 6px 10px; margin: 0 -10px; }
/* A run with no heading over it — the town's own places of interest and its unnested parks, and its
   events. What kind each row is comes from the dot beside its name (.plkind), not from a line of
   uppercase above it, so the run needs only the gap that separates it from the next. */
.plblock-flat { padding-top: 10px; }
/* What kind of place a row is, in the colours the map gives the same places' pins: towns gold, parks
   green. The map stops there — its two pins say "town" and "everything made in a park or at a landmark" —
   so the two this pane needs beyond it are new, and chosen to be told apart from gold and green at 7px:
   a place of interest reads blue against both, an event mauve. Never the only carrier of the kind: the
   legend above the pane names each colour, and every row's accessible name opens with the word. */
.plkind { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 100px; background: #6f6f77; }
.plkind-town { background: rgba(216,181,107,0.85); }
.plkind-park { background: rgba(143,174,135,0.80); }
.plkind-attraction { background: rgba(129,163,193,0.85); }
.plkind-event { background: rgba(174,146,190,0.85); }
/* The key to those colours, pinned under the pane's header rather than scrolling with the rows it
   explains. */
.plleg { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 7px 16px; padding: 11px 26px 12px;
         font-size: 11px; color: #8a8a91; border-bottom: 1px solid rgba(255,255,255,0.05); }
.plleg-i { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
/* A rank below the town heading above it, and dressed like one: the dimmer gold rather than the bright,
   and 600 rather than 700. It was --gold-bright at 700, which made a park label the brightest text in
   the pane — louder than the town containing it. */
.plblock-lbl { font-size: 11.5px; letter-spacing: .2em; text-transform: uppercase; color: var(--gold);
               font-weight: 600; flex: 0 1 auto; min-width: 0; }
.plblock-meta { font-size: 11.5px; color: #7d7d84; white-space: nowrap; }
.plblock-hd .plpill { margin-right: 0; }
/* One place: its name, its count, and a taste of the work. */
/* The dot sits closer to the name than the name does to the strip — it is part of the name, not a
   column of its own. */
.plitem .plkind { margin-right: -8px; }
.plitem { display: flex; align-items: center; gap: 18px; padding: 9px 10px; margin: 0 -10px;
          border-radius: 6px; border-bottom: 1px solid rgba(255,255,255,0.04); }
.plitem:hover { background: rgba(255,255,255,0.035); }
.plitem-nm { flex: 1 1 auto; min-width: 110px; font-size: 14.5px; color: #e6e6ea; font-weight: 300;
             background: none; border: 0; padding: 0 14px 0 0; font-family: inherit; text-align: left;
             cursor: pointer; transition: color .25s ease; }
button.plitem-nm:hover { color: var(--gold-bright); }
.plitem-nm:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
.plitem .placerow-strip { flex: 0 1 auto; justify-content: flex-end; }
.plitem .placerow-shot, .plitem .placerow-more { width: 80px; }
.plitem .placerow-film { width: 106px; }
/* Fewer tiles as the *pane* narrows. .placerow-more is not a .placerow-shot, so it survives every cut.
   Held above 640px, below which the strip wraps under the name and has the whole width to itself — see
   the phone rule further down. */
@media (min-width: 641px) {
  @container (max-width: 620px) { .plitem .placerow-shot:nth-child(n+4) { display: none; } }
  @container (max-width: 500px) { .plitem .placerow-shot:nth-child(n+3) { display: none; } }
  @container (max-width: 380px) { .plitem .placerow-shot:nth-child(n+2) { display: none; } }
}

/* The narrow layout's furniture, inert above the breakpoint. */
.plpanelhd, .plback, .plfabdock, .plbackdrop { display: none; }

/* Too short to hold a browser: the lock alone comes off — the three columns stay, since the window is
   still wide enough for them — and the page scrolls as it did before. Height, not width, so it doesn't
   collapse a wide screen into the phone layout below. */
@media (max-height: 560px) {
  .pllock { height: auto; min-height: 100vh; overflow: visible; }
  .pllock main { display: block; }
  .pllock .placespage { display: block; padding-bottom: 90px; }
}

@media (max-width: 1180px) {
  /* One column of page. The states leave the flow entirely (a fixed panel), so towns and detail are all
     that is left to lay out — and only one of the two is shown at a time. The viewport lock comes off
     with them: there are no side-by-side columns left to keep still, and the push navigation is a page
     you scroll, not a pane inside one. */
  .pllock { height: auto; min-height: 100vh; overflow: visible; }
  .pllock main { display: block; }
  .pllock .placespage { display: block; padding-bottom: 90px; }
  .plbrowse { display: block; border: 0; border-radius: 0; background: none; min-height: 0; overflow: visible; }
  .plcol { padding: 0; max-height: none; overflow: visible; border-right: 0; }
  .pldetail { display: block; }
  .pldetail-body { overflow: visible; }
  .pltowns .plrow { border-bottom: 1px solid rgba(255,255,255,0.05); border-radius: 0; }
  .pltown .plrow-pick { padding: 13px 8px; }
  .pltown-nm { font-size: 14px; }
  .plbrowse.showdetail .pltowns { display: none; }
  .plbrowse:not(.showdetail) .pldetail { display: none; }
  /* The pane's header STICKS here, under the top bar rather than behind it (--barh, published by
     interop.js -> autoHideBar). It is the only way back to the region list at this width — the back arrow
     lives in it — and a council runs several screens, so scrolling into one used to strand the visitor:
     the way out was at the top of the pane and nowhere else. It rides with the bar, so a moment's scroll
     up brings both back, which is the gesture a phone already teaches. It bleeds to both edges and
     carries its own blurred ground, because rows scroll under it. */
  .pldetail-hd { position: sticky; top: var(--barh, 60px); z-index: 4; gap: 12px;
                 margin: 0 calc(var(--gutter) * -1); padding: 10px var(--gutter) 12px;
                 background: rgba(12,12,15,0.985); backdrop-filter: blur(10px);
                 -webkit-backdrop-filter: blur(10px);
                 transition: transform .3s ease; }
  .plleg { padding: 11px 0 12px; }
  .pldetail-body { padding: 0 0 20px; }
  .plblock-hd { padding: 6px 0; margin: 0; }
  /* The pane isn't a scrollport here — it is the page, under a nav that hides itself as you scroll —
     so a sticky heading would either sit under that nav or fight it. It keeps the band and the gold rule,
     which is what makes it read as a town; it just stays where it is put. And with no side padding on the
     pane to bleed into, the band spans the column as it stands. */
  .pltownsec-hd { position: static; margin: 0; padding: 9px 0 8px; backdrop-filter: none;
                  -webkit-backdrop-filter: none; }
  .pltownsec-nm { font-size: 19px; }
  .pltownsec .plblock { padding-left: 0; }
  .plitem { gap: 11px; padding: 8px 0; margin: 0; }
  .plback { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
            width: 36px; height: 36px; border-radius: 10px; background: rgba(255,255,255,0.04);
            border: 1px solid rgba(255,255,255,0.12); color: #e6e6ea; cursor: pointer; }
  /* Search swaps sides: out of the header, into the panel — and the town heading picks up what the
     hidden box can no longer say. */
  .plsearch-head { display: none; }
  .plsearch-panel { display: flex; width: auto; margin: 14px 0 8px; padding: 10px 14px; }
  .plsearch-panel input { font-size: 13.5px; }
  .plqchip { display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto; background: none;
             border: 1px solid rgba(216,181,107,0.28); border-radius: 100px; padding: 3px 8px;
             font-family: inherit; font-size: 11px; color: #c7b787; cursor: pointer;
             max-width: 46%; overflow: hidden; }

  /* The states, as a panel over the page. */
  .plregions { position: fixed; top: 0; bottom: 0; left: 100%; width: 100%; z-index: 71;
               display: flex; flex-direction: column; gap: 2px; padding: 0 16px 24px;
               background: rgba(13,13,16,0.98); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px);
               box-shadow: -30px 0 90px rgba(0,0,0,0.55); overflow-y: auto; visibility: hidden;
               transition: left .28s cubic-bezier(.22,.7,.2,1), visibility .28s; }
  .plbrowse.panelopen .plregions { left: 0; visibility: visible; }
  .plpanelhd { display: flex; align-items: center; justify-content: space-between; gap: 12px;
               position: sticky; top: 0; z-index: 1; background: inherit; flex: 0 0 auto;
               margin: 0 -16px; padding: 20px 18px 14px; border-bottom: 1px solid rgba(255,255,255,0.07); }
  .plpanelx { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
              border-radius: 10px; background: none; border: 0; color: #9a9aa1; cursor: pointer; }
  .plbackdrop { display: block; position: fixed; inset: 0; z-index: 70; background: rgba(5,5,6,0.72);
                opacity: 0; pointer-events: none; transition: opacity .22s ease; }
  .plbrowse.panelopen ~ .plbackdrop { opacity: 1; pointer-events: auto; }
  .plfabdock { display: block; position: fixed; right: 20px; bottom: 20px; z-index: 72; }
  .plbrowse.panelopen ~ .plfabdock { display: none; }

  /* Nothing is claiming a town here — the detail pane the fallback fills isn't on screen. */
  .plfallback .plcrumb-town { display: none; }
  .plfallback .pltown.on { background: none; border-left-color: transparent; }
  .plfallback .pltown.on .plrow-pick { color: #c9c9cf; }
}

/* A park's name is long and the pane is narrow, so the heading takes the line and what it holds goes
   under it, rather than the name wrapping to three words a line beside an unwrapped count. */
@media (max-width: 640px) {
  .plblock-hd { flex-wrap: wrap; row-gap: 2px; }
  /* `100%` put the name on a line of its own — and left the kind dot beside it stranded on the line
     ABOVE, which reads as a bullet belonging to nothing. The dot is 7px and the gap 11px, so asking for
     everything *except* those keeps the pair together on the first line and still pushes the count and
     the pill onto the second, which is what the rule is for. */
  .plblock-lbl { flex: 1 1 calc(100% - 18px); }
  .pltownsec-hd { flex-wrap: wrap; row-gap: 2px; }
  .pltownsec-nm { flex: 1 1 100%; }
}

/* The strip starts at the left of its column, so every row's first thumbnail lands on the same line down
   the page however many tiles the row has — the ragged right edge is the harmless one. The column gap on
   .placerow is what keeps that first tile off the name beside it. Tiles are a fixed size (BlurUpImage's
   layers need a box to crop into) and the breakpoints below drop tiles rather than shrink them — six
   postage stamps scaled down are noise, three at a readable size are photographs. */
.placerow-strip { display: flex; justify-content: flex-start; align-items: center; gap: 8px; min-width: 0; }
article > .placerow-strip { margin-top: 16px; }
.placerow-shot { flex: 0 0 auto; position: relative; overflow: hidden; width: 96px; aspect-ratio: 3 / 2;
                 background: #161619; border: 0; padding: 0; border-radius: 2px; cursor: pointer; }
.placerow-shot .ph { filter: blur(14px); transform: scale(1.1); }
.placerow-shot .full { opacity: 0; transition: opacity .5s ease, transform 3.5s ease; }
.placerow-shot .full.on { opacity: 1; }
.placerow-shot:hover .full.on { transform: scale(1.055); }
.placerow-shot::after { content: ""; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(216,181,107,0);
                        transition: box-shadow .25s ease; pointer-events: none; }
.placerow-shot:hover::after { box-shadow: inset 0 0 0 1px rgba(216,181,107,0.65); }
.placerow-shot:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }
/* A film in the strip: 16:9, the shape it was shot in, so it reads as a different kind of thing from the
   3:2 photographs beside it before the play badge is even noticed. YouTube's hqdefault still is 4:3 with
   letterboxing, so object-fit crops the bars away — the same treatment the Videos grid gives it. */
.placerow-film { flex: 0 0 auto; position: relative; overflow: hidden; width: 128px; aspect-ratio: 16 / 9;
                 background: #161619; border: 0; padding: 0; border-radius: 2px; cursor: pointer; }
.placerow-film img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
                     transition: transform 3.5s ease; }
.placerow-film:hover img { transform: scale(1.055); }
/* Here the badge earns its place, unlike in the map's panel: this strip mixes films in among photographs,
   so a still needs to say that it plays. */
.placerow-play { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1;
                 display: flex; align-items: center; justify-content: center; width: 26px; height: 26px;
                 border-radius: 50%; background: rgba(11,11,13,0.66); color: #f2f0ec;
                 border: 1px solid rgba(255,255,255,0.28); transition: background .25s ease; }
.placerow-film:hover .placerow-play { background: rgba(216,181,107,0.85); color: #14140f; }
.placerow-film::after { content: ""; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(216,181,107,0);
                        transition: box-shadow .25s ease; pointer-events: none; }
.placerow-film:hover::after { box-shadow: inset 0 0 0 1px rgba(216,181,107,0.65); }
.placerow-film:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }
/* A section whose only work is film borrows YouTube's still for its cover, cropped square by .placecover
   exactly as a photograph's thumbnail is. */
.placecover-film { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The overflow tile: the same shape as a thumbnail, because it stands where the next one would. */
.placerow-more { flex: 0 0 auto; display: flex; align-items: center; justify-content: center; gap: 4px;
                 width: 96px; aspect-ratio: 3 / 2; background: rgba(255,255,255,0.03);
                 border: 1px solid rgba(255,255,255,0.12); border-radius: 2px; color: #c9c9cf;
                 font-family: inherit; font-size: 11.5px; letter-spacing: .04em; cursor: pointer;
                 transition: color .25s ease, border-color .25s ease; }
.placerow-more:hover { color: var(--gold-bright); border-color: rgba(216,181,107,0.5); }
.placerow-more:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
/* On a phone the detail pane is the whole width, so the strip wraps under the name instead of being cut
   to a couple of tiles beside it. */
@media (max-width: 640px) {
  .plitem { flex-wrap: wrap; }
  /* Grows to fill the line rather than claiming one, so the kind dot stays beside the name it belongs to
     — a 100% basis pushed the name to the next line and left the dot hanging above it, reading as a
     bullet over the row rather than as part of it. The strip's own 100% is what still wraps it under. */
  .plitem-nm { flex: 1 1 0; min-width: 0; padding-right: 0; }
  /* The row's gap is tighter here, so the dot's pull-in would leave it against the text. */
  .plitem .plkind { margin-right: 0; }
  /* The town's own work is a row named after the town (GalleryState always names it `place.Value`), which
     beside its strip reads as a row like any other — but wrapped, the name lands on its own line directly
     under the town heading and reads as that heading printed twice. The photographs are what the section
     is for, so at this width they sit straight under the heading and the row keeps only its strip. The
     heading above carries the name, the count and the pill into the gallery, so nothing is lost with it;
     wider than this the name stays, since there it sits beside its photographs rather than above them.
     The kind dot goes with the name — on its own it would be a bullet floating over a strip. */
  .plblock-own .plitem-nm, .plblock-own .plkind { display: none; }
  .plblock-own .plitem { padding-top: 0; border-bottom: 0; }
  .plitem .placerow-strip { flex: 1 1 100%; justify-content: flex-start; }
  /* Every strip in the pane wraps, not just a row's: a town with nothing filed under it shows its own
     strip with no row around it, and unwrapped that one ran off the side and took the page's horizontal
     scrollbar with it. Three tiles to a line exactly — a third of the width, less the two 8px gaps. */
  .pldetail-body .placerow-strip { flex-wrap: wrap; }
  .pldetail-body .placerow-shot, .pldetail-body .placerow-more { width: calc((100% - 16px) / 3); }
  .pldetail-body .placerow-film { width: calc((100% - 8px) / 2); }
}

/* ----- places view: map mode -----
   Three boxes: .mapframe is the space the map is given, .mapview is the map's own box (the country's
   projected extent, as an aspect-ratio) fitted and centred inside it, and .mapscale is what zoom moves.
   Pins are HTML buttons rather than SVG shapes so they get a real accessible name, the browser's own focus
   ring, and a size that doesn't scale with the viewBox — or with the zoom, since they sit on .mapview at
   their position within the visible window rather than inside .mapscale.

   The split matters for correctness, not tidiness. The frame's shape used to have to *equal* the
   projection's, so a chain of container queries was load-bearing: any route by which the frame came out a
   different shape drew the country stretched, or ran it off the sides with the pins still landing on it.
   .mapview now carries the ratio and fits itself (aspect-ratio + max-height is the browser's own "meet"),
   so a frame of any shape letterboxes the map instead of cutting it. */
/* Height is capped, not just derived from the ratio: a tall country (or a narrow window) would otherwise
   fill the viewport and push the pin's card — which opens *below* the map — off the bottom of the screen.
   max-width limits the width so the aspect ratio yields a height no greater than the cap; the box then
   centres in whatever is left. --map-ratio is set inline from the country's projected extent.
   On the Places page itself this cap is superseded — the frame is fitted to the leftover viewport height
   instead (see .placespage-map .mapframe below); it stands as the fallback for a frame rendered outside
   that column.
   No border, no fill: the frame is a coordinate space, not a card. Drawing a box around it fenced the
   country off from a page it is otherwise the whole of, and boxed a lot of empty ocean along with it —
   the coastline is the edge the eye wants. `overflow: hidden` stays: the label placement in `LabelAlign`
   is written around a frame that clips, so a coastal label hangs inward rather than off the side. */
.mapframe { --map-max-h: min(58vh, 620px);
            position: relative; width: 100%; max-width: calc(var(--map-max-h) * var(--map-ratio, 1.5));
            aspect-ratio: var(--map-ratio, 1.5); margin: 18px auto 0; overflow: hidden;
            display: grid; place-items: center; container-type: size; }
/* The map's own box: the largest box of the country's own shape that fits inside the frame, centred by
   the frame's grid. **The width is where the fitting happens.**
   `width:100%; aspect-ratio; max-height:100%` — what this used to be — does not fit anything: a clamped
   height is not transferred back into a width, so a frame two thirds as tall as its shape wants simply
   drew Australia two thirds as tall, stretched, with every pin still landing on it. The comment here
   claimed the opposite for long enough that a `max-height` was added to the frame on the strength of it.
   Taking the smaller of the two candidate widths is the same "meet" said in a form the browser actually
   performs, and it is what makes the frame's shape no longer load-bearing: `container-type: size` on the
   frame is what lets this ask how tall it is.
   overflow:hidden is what a zoomed .mapscale is clipped to, and what the label placement in LabelShift is
   written around. */
.mapview { position: relative; overflow: hidden;
           width: min(100cqw, calc(100cqh * var(--map-ratio, 1.5)));
           aspect-ratio: var(--map-ratio, 1.5); }
/* Only the outlines are zoomed; the transform is set inline from the current window. transform-origin is
   the default centre, which is what the window arithmetic in PlacesMap assumes. */
.mapscale { position: absolute; inset: 0; transition: transform .45s cubic-bezier(.4,0,.2,1); }
/* The pins, the labels and the two name layers. Its box is .mapview's exactly, so every percentage inside
   it means what it did before the wrapper existed — it is here to be faded as one thing, not to position
   anything. The outlines glide on .mapscale's transform and these can only jump, so during a zoom the
   layer steps aside and comes back once the map has landed (PlacesMap.SettleAsync): out fast, in gently,
   and never fading the *first* paint, since `.settling` is only ever added by a zoom. */
.mapmarks { position: absolute; inset: 0; transition: opacity .34s ease .1s; }
.mapmarks.settling { opacity: 0; transition: opacity .12s ease; }
.mapview svg { display: block; width: 100%; height: 100%; }

/* The way back out of a cluster. Top-right: the band sits under the map and the legend has the top-left,
   so the bottom corners are the ones that end up against the band's edge. */
.mapzoomout { position: absolute; right: 12px; top: 12px; z-index: 4; display: inline-flex;
              align-items: center; gap: 6px; white-space: nowrap; line-height: 1;
              padding: 8px 14px 8px 10px; border-radius: 100px; cursor: pointer;
              background: rgba(16,16,20,0.82); border: 1px solid rgba(255,255,255,0.16); color: #d8d8de;
              font-family: inherit; font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
              font-weight: 600; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
              transition: background .2s ease, color .2s ease, border-color .2s ease; }
.mapzoomout:hover { background: rgba(216,181,107,0.18); border-color: rgba(216,181,107,0.5); color: #ecd39a; }
.mapzoomout:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* Full-screen map: the list and the map share the space the heading and tabs leave, as a row — the list
   pinned to the left at a fixed width, the map taking the rest. The open pin's work is an overlay along
   the foot of the map (see .mapstrip) rather than a third column, so opening one moves neither of these
   two and the map never changes size mid-session.
   The pair used to be a map and a *card*, both content-sized and centred together so that the leftover
   width fell outside them rather than as a chasm between; the list replaces the card, is anchored to the
   left edge like the index it is, and the surplus is centred around the map instead. */
.placespage-map .mapstage { flex: 1 1 0; min-height: 0; display: flex;
                            gap: clamp(16px,2vw,32px); }
/* The map takes everything the list leaves and centres itself in it. It used to be sized to exactly the
   width its height implies, so that the map and the card beside it could be centred *as a pair*; with the
   list pinned to the left edge instead, that left the whole surplus in one lump against the right-hand
   window edge. .mapview still fits the country to its own ratio and never stretches — the surplus is now
   simply split evenly around it. */
/* A column now (map, then the open pin's band), so the cross axis is what centres the map in the width
   the list leaves — and the main axis starts at the top, which keeps the map next to the country tabs it
   belongs to instead of floating in the middle of a tall phone screen. */
.placespage-map .mapfill { flex: 1 1 0; min-width: 0; min-height: 0;
                           display: flex; flex-direction: column;
                           align-items: center; justify-content: flex-start; }
/* **The frame is whatever the column has left, and nothing else.** It is the flexible item in a column
   whose other members — the legend on a narrow screen, the open pin's band — are their own height, so
   the map gives up exactly what they take, at every width, with no arithmetic to keep in step with them.
   .mapview then fits the country inside it (see .mapview), so a frame that is the wrong shape costs a
   margin and never a distorted map.
   This replaces a `100cqh` sum that had to name everything else in the column. It named the band and not
   the legend, and only in the wide layout: on a phone the map took the full height *and* the band went
   under it, the two came to more than the screen, and — the page being locked to one viewport
   (.pagelock) — the photographs at the foot of the band were cut off with nothing to scroll. A sum that
   has to be rewritten every time a sibling is added is a sum that will be wrong again; `flex` is the
   same statement in a form that cannot fall behind.
   min-height:0 is what lets it actually give the height up: a flex item's automatic minimum is its
   content, and the content here is a map that would rather be large. */
/* Top, not middle. The frame is now bigger than the map wherever the map is width-bound — a phone — and
   a country floating in the middle of the leftover height is adrift from the country tabs it belongs to.
   Horizontally it stays centred: that surplus is the width the list doesn't take, and splitting it puts
   the map in the middle of its own column. */
.placespage-map .mapframe { flex: 1 1 auto; min-height: 0; margin: 0; max-width: none; width: 100%;
                            place-items: start center; }
/* The band holds a heading and one row of tiles — that is the whole design, at every width — so its
   height is a constant rather than "whatever the contents come to", and the map does not resize again as
   you move from a place with two photographs to one with six. */
.placespage-map .mapstage.hasstrip { --strip-h: clamp(146px, 21vh, 202px); }
/* The band spans the column's width rather than the map's, which is what keeps its tiles on the same left
   edge as the list's rows however narrow the country happens to be drawn. */
/* The list takes its width first and the map is fitted into what is left, so the two stay put as pins are
   opened and closed — the strip is an overlay now and costs the map nothing. */
.placespage-map .maplist { max-height: 100%; }

/* When to stack: **not** simply "a narrow window". A landscape tablet (900x800) was stacking, and paid for
   it — the card took half of a ~530px column and left the map a 255px stamp — when side by side it has
   room for a 456px map *and* the whole panel unscrolled. What decides it is the shape of the window, not
   its width: a portrait one has height to give and no width, so the card goes under the map; a landscape
   one is the other way round whatever its size. Below 700px there is no width to divide either way. */
@media (max-width: 700px), (max-aspect-ratio: 1/1) {
  /* The list is dropped rather than stacked. It is an index, and the site already has a screen that is
     nothing but the index — Places, one tap away in the nav, complete and built for a narrow window. A
     copy of it above the map here would push the map into the bottom third of a phone to say something
     said better next door. What stays is the map and the strip, which is what this screen is for. */
  .placespage-map .maplist { display: none; }
  /* With the list gone the row has one item, so the map's column is the whole of it. Nothing else about
     the map's size is restated here: the frame takes what the legend and the band leave, which is the
     same sentence at every width. */
  .placespage-map .mapfill { flex: 1 1 0; align-self: stretch; }
  /* Across a whole window the tiles can afford to be a little bigger than they are in a column beside
     the map, and the band a little taller with them. */
  .mapstrip-row { --tile-h: clamp(72px, 12vh, 112px); }
  .placespage-map .mapstage.hasstrip { --strip-h: clamp(152px, 24vh, 218px); }
  /* The legend comes out of the map. A phone's map is small enough that two chips in its corner sit on
     the country rather than on the sea beside it, covering the pins they exist to explain — and there is
     nowhere inside a 358px-wide Australia to put them where that isn't true. Above the map they cost a
     line of a screen that has one to spare, and they hide nothing. They also flatten to a row there,
     since the width is the axis with room. */
  /* Scoped to .placespage-map for the specificity, not for the scope: the base `.mapleg-flow{display:none}`
     is declared further down this file than this block, so an equally specific rule here would lose to it
     on source order and the swap would silently not happen. */
  .placespage-map .mapleg-over { display: none; }
  .placespage-map .mapleg-flow { display: flex; position: static; flex-direction: row; flex-wrap: wrap;
                                 align-self: stretch; gap: 8px; margin: 0 0 10px; }
}

/* On a phone the band's tiles come back down a size, and the band with them — it is one scrolling line
   whatever the width, so this buys the map height rather than buying the row a shorter one. */
@media (max-width: 520px) {
  .mapstrip-row { --tile-h: clamp(62px, 9.5vh, 84px); }
  /* The chooser's tiles get a floor on their width here, which the photographs beside them don't need.
     A tile is as wide as its cover is tall, and on a phone that is narrower than the word PHOTOGRAPHS:
     the tally wrapped to a second line, took the height out of the cover, and left a strip of options
     that were mostly caption. The floor is what keeps the line that tells two places apart on one line.
     Scoped to .placespage-map for the specificity, not the scope: `.mapplace`'s own width is declared
     *later* in this file than this block, so an equally specific rule here would lose on source order —
     the same trap the legend swap above documents. */
  .placespage-map .mapplace { width: max(112px, calc(var(--tile-h) * 1.25)); }
  /* Enough for a cover, two lines of a name like "Currumbin Wildlife Sanctuary" and its tally: the
     chooser is useless if the line that tells two places apart is the line clipped off the bottom. */
  .placespage-map .mapstage.hasstrip { --strip-h: clamp(150px, 23vh, 196px); }
}

/* z-index 2 puts every pin above every NAME (the towns, the councils and the pin labels are later
   siblings in .mapmarks, so without it they paint over the dots). The picking rules keep names off the
   pins in the first place — see PickTowns and LgaClear, which test the box the text is drawn in — but a
   name that does land on one now goes behind it, which is the right answer either way: the dots are what
   the map is for, and text crossing one hides the thing the visitor is trying to press. The open pin and
   its own name keep z-index 3, so the selection still rises above the lot. */
.mappin { position: absolute; z-index: 2; transform: translate(-50%, -50%); padding: 0; border-radius: 50%; cursor: pointer;
          background: rgba(216,181,107,0.85); border: 1.5px solid rgba(11,11,13,0.75);
          box-shadow: 0 0 0 1px rgba(216,181,107,0.35);
          transition: transform .2s ease, background .2s ease, box-shadow .2s ease; }
.mappin:hover { transform: translate(-50%, -50%) scale(1.18); background: var(--gold-bright); }
/* ----- the selected pin -----
   It has to be findable in a field of forty, and a brighter fill could not do it: a slightly paler dot
   among dots of the same colour and size is a difference you have to hunt for, which is what the map's
   selection was. Four things say it now, none of them subtle — the pin GROWS, it is ringed in gold
   against a dark separator so the ring reads over land, sea or another dot, it RISES above its
   neighbours so nothing can overlap that ring, and its name is drawn whatever the label ceiling says
   (see the marker loop in PlacesMap). The ring is gold for a park as well as for a town on purpose: this
   is the pin's STATE, and the green/gold split is what KIND of place it is — one question per channel,
   which is the rule the hover state already followed. */
.mappin.on, .mappin.park.on, .mappin.many.on, .mappin.park.many.on {
  z-index: 3;
  transform: translate(-50%, -50%) scale(1.3);
  border-color: rgba(11,11,13,0.92);
  box-shadow: 0 0 0 3px rgba(11,11,13,0.85), 0 0 0 5px var(--gold), 0 0 0 11px rgba(216,181,107,0.18);
}
.mappin.on:hover, .mappin.park.on:hover { transform: translate(-50%, -50%) scale(1.38); }
.mappin.on { background: var(--gold-bright); }
/* A park, a landmark, a place of interest — green, as the legend and the list's dots say. Gold stays the
   towns', which is also the site's own accent, so the map reads as "settlements, and the country between
   them". The selected and hover states stay gold for both: those are the *state* of a pin, not what kind
   of place it is, and a second green would make the two questions one colour. */
.mappin.park { background: rgba(143,174,135,0.80); box-shadow: 0 0 0 1px rgba(143,174,135,0.38); }
.mappin.park:hover { background: rgba(178,208,170,0.95); }
.mappin.park.on { background: rgba(190,220,180,1); }
.mappin.park.many { box-shadow: 0 0 0 1px rgba(143,174,135,0.38), 0 0 0 3.5px rgba(11,11,13,0.75),
                                0 0 0 5px rgba(143,174,135,0.5); }

/* A pin standing for more than one place: a ring around it, because it behaves differently from its
   neighbours — it zooms in and splits rather than opening a panel, and nothing else on the map says so. */
.mappin.many { box-shadow: 0 0 0 1px rgba(216,181,107,0.35), 0 0 0 3.5px rgba(11,11,13,0.75),
                           0 0 0 5px rgba(216,181,107,0.45); }
/* A pin whose coordinate was averaged from the places inside it, not looked up: hollow, so the map never
   implies a precision it doesn't have. The ring keeps the pin's own colour — hollow says how sure the
   map is of WHERE the place is, and must not also change WHAT KIND of place it says it is. Without the
   second rule every derived pin came out gold, so a park the datasets couldn't locate read as a town. */
.mappin[style*="background:none"]:not(.on) { border-color: rgba(216,181,107,0.8); box-shadow: none; }
.mappin.park[style*="background:none"] { border-color: rgba(143,174,135,0.8); }
.mappin:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* A pin's name. `overflow` is the other half of `LabelBox`: it states how much room the label has on the
   side it hangs, and this is what makes the label respect it. Without the pair, a name longer than the
   space simply ran under the edge of the map and lost its first or last few characters. border-box,
   because that width is the whole of the promise: a content-box width plus a padding the caller didn't
   account for is exactly the few characters that ran under the edge of the map.

   It WRAPS rather than running on, inside the line `LabelBox` caps it to. A place name is as long as it
   is — "Bandilngan (Windjana Gorge) National Park" sets ~300px on one line — and a name that wide
   reserves a third of the map against every other label, so the pin beside it goes unnamed for want of
   horizontal room it never needed. Broken over two shorter lines the same name asks for half the width
   and both names fit; `PinFit` measures the wrap, so the collision test reserves exactly what is drawn.
   `balance` keeps the break near the middle rather than stranding one word below. */
.maplabel { position: absolute; box-sizing: border-box; transform: translateX(-50%); pointer-events: none;
            overflow: hidden; text-wrap: balance; line-height: 1.5;
            font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: #b9b9c0;
            text-shadow: 0 1px 3px rgba(0,0,0,0.9); }
/* The open pin's own name — the one label that is drawn on a crowded map, where the ceiling turns every
   other one off. It is the name the visitor just asked for, the band under the map is already showing it,
   and a ring with no name attached leaves them matching a dot against a list. Gold and a shade heavier
   than an ordinary label, so it reads as this pin's rather than as one more name on the map. */
.maplabel.on { color: var(--gold-bright); font-weight: 600; z-index: 3;
               text-shadow: 0 1px 3px rgba(0,0,0,0.95), 0 0 10px rgba(11,11,13,0.9); }

/* The council the view sits in, set across the middle of it. Wider-tracked than a town name but dimmer
   than one: it names the ground rather than a point on it, and the eye should find it only when it goes
   looking. Same max-width guarantee as the other two, so it cannot be clipped.

   Unlike the other two it WRAPS rather than ellipsising, because it labels an area and not a point: a
   shire at this zoom is a patch of the window a few hundred pixels wide, and `AreaBox` caps the line to
   what that patch can hold. A name longer than the cap set on one line ran across two borders and read as
   a label for whichever shire it ended over; broken over two it sits in the gap it names. `balance` is
   what keeps the break near the middle instead of leaving one word alone on the second line. */
.maplga { position: absolute; box-sizing: border-box; pointer-events: none;
          overflow: hidden; text-wrap: balance; line-height: 1.5;
          font-size: 9.5px; letter-spacing: .2em; text-transform: uppercase; color: #55555e;
          text-shadow: 0 1px 3px rgba(0,0,0,0.9); }

/* A named place from the detail layer: context, not content. It is a dot with its name beside it, in the
   dimmest text on the map — it must read as the background a photograph's pin sits on, never as a rival to
   one. No pointer events: nothing here is clickable, and a cursor change would promise otherwise.
   The dot is drawn with a box-shadow rather than a pseudo-element so the whole thing is one span. */
.maptown { position: absolute; box-sizing: border-box; display: flex; align-items: center; pointer-events: none;
           white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
           transform: translateY(-50%); font-size: 9.5px; letter-spacing: .07em;
           text-transform: uppercase; color: #6a6a72; text-shadow: 0 1px 3px rgba(0,0,0,0.9); }
.maptown::before { content: ""; position: absolute; left: 0; top: 50%; width: 3px; height: 3px;
                   margin: -1.5px 0 0 -1.5px; border-radius: 50%; background: #55555d; }
/* Hanging left, the dot goes on the right-hand end instead — it still marks the place, not the text. */
.maptown[style*="translateX(-100%)"] { transform: translate(-100%, -50%); }
.maptown[style*="translateX(-100%)"]::before { left: auto; right: 0; margin: -1.5px -1.5px 0 0; }

/* ----- the row of controls above the map -----
   Which country, and which kind of work: two ways of narrowing the same set of pins, so they share one
   line rather than taking one each off a screen whose scarcest thing is height. The tabs lead, because
   which country is drawn is the coarser question; the filter is hung off the far end, where a control
   that acts on the whole screen belongs and where it stays put as countries are added or drop out. */
.maptop { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 20px 0 4px; }
.maptop .mapmedia { margin-left: auto; }
/* Hung from its right edge instead of its left: the button is at the end of the row, so a panel opening
   rightwards from it would run off the window. */
.mapmedia .fb-panel { left: auto; right: 0; }

/* ----- the index beside the map -----
   The pins the map is currently drawing, grouped the way it draws them — one section per pin — in the one
   form a map cannot take: reachable by name, searchable, counted in figures rather than in areas. A zoom
   crops it exactly as it crops the map; only a search reaches past the window. No fill and no border box — a hairline down its inside edge is the whole
   separation, so the two halves read as one screen on one background rather than as two panels. */
.maplist { flex: 0 0 clamp(268px, 24vw, 372px); align-self: stretch; min-height: 0; min-width: 0;
           display: flex; flex-direction: column;
           border-right: 1px solid rgba(255,255,255,0.07); padding-right: clamp(10px,1.4vw,18px); }
/* The search row sits *inside* the column's own margins rather than against its edges: it had none at the
   top at all, so the pill was 4px under the country tabs and read as a third tab, and only 12px over the
   first heading, so it read as part of the list it filters. The count is the row's other half, and its
   right-hand inset is the padding .maplist already keeps off the divider — one gutter down that edge, not
   the pill's own second one. */
.maplist-hd { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; padding: 12px 2px 16px; }
.mapsrch { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px; color: #7d7d84;
           background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.12);
           border-radius: 100px; padding: 9px 14px; transition: border-color .25s ease; }
.mapsrch:focus-within { border-color: rgba(216,181,107,0.5); }
.mapsrch input { flex: 1 1 auto; min-width: 0; background: none; border: 0; outline: none;
                 color: var(--paper); font-family: inherit; font-size: 12.5px; font-weight: 300; }
.maplist-n { flex: 0 0 auto; font-size: 11px; color: #5c5c63; font-variant-numeric: tabular-nums; }
/* Scrolls without a scrollbar, like .navscroll and the panel before it: a scrollbar down the inside edge
   of the map is the loudest thing on the screen. */
.maplist-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-width: none;
                  display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 20px; }
.maplist-scroll::-webkit-scrollbar { display: none; }
/* The pin the rows under it are drawn as, and a rule out to the edge — the same eyebrow the rest of the
   site uses for a section heading, at the size a list of thirty of them can carry. It carries no numbers:
   the places are listed under it, each with its own count. It is a <button>, because the heading *is* the
   pin: clicking it does what clicking the dot does. */
.maplist-sec { display: flex; align-items: baseline; gap: 9px; padding: 14px 6px 7px;
               width: 100%; background: none; border: 0; border-radius: 4px; font: inherit;
               color: inherit; text-align: left; transition: background .2s ease; }
.maplist-cl { cursor: pointer; }
.maplist-cl:hover { background: rgba(255,255,255,0.05); }
/* Lit with its rows: a selected cluster is one object across both halves of the screen. */
.maplist-cl.on { background: rgba(216,181,107,0.09); }
/* The disclosure. It turns rather than swapping glyph, so the heading's height never moves and the
   direction of travel is the animation itself. */
.maplist-cv { flex: 0 0 auto; display: flex; align-self: center; color: #7d7d84;
              transition: transform .2s ease, color .2s ease; }
.maplist-cl.open .maplist-cv { transform: rotate(90deg); color: #c7b787; }
.maplist-cl:hover .maplist-cv { color: #c7b787; }
.maplist-sec b { flex: 0 1 auto; min-width: 0; font-size: 9.5px; letter-spacing: .24em;
                 text-transform: uppercase; color: #c7b787; font-weight: 600;
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.maplist-sec u { flex: 1 1 auto; height: 1px; background: rgba(255,255,255,0.08); text-decoration: none; }
/* The state a run of areas is in — the rung above them, and drawn as the quieter of the two: an area is
   the thing you click, a state is only where it is. Wider tracking and no gold, so the two headings can
   never be mistaken for each other at a glance. */
.maplist-st { display: flex; align-items: baseline; gap: 9px; padding: 20px 6px 4px; }
.maplist-st:first-child { padding-top: 6px; }
.maplist-st b { flex: 0 1 auto; min-width: 0; font-size: 9px; letter-spacing: .3em;
                text-transform: uppercase; color: #8a8a91; font-weight: 500;
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.maplist-st u { flex: 1 1 auto; height: 1px; background: rgba(255,255,255,0.06); text-decoration: none; }
.maplist-st i { flex: 0 0 auto; font-style: normal; font-size: 10.5px; color: #5c5c63;
                font-variant-numeric: tabular-nums; }
/* Everything filed under one state, set in from its heading. It has to restate the column's own gap: it
   is a flex item in .maplist-scroll, and the rows inside it are no longer that column's children. */
.maplist-grp { display: flex; flex-direction: column; gap: 2px; }
.maplist-grp-in { padding-left: 9px; }

/* A place inside an opened section, set in from the heading that opened it — the indent is what says the
   rows belong to the dot above them rather than being more dots of their own. */
.maprow-in { margin-left: 12px; }
.maplist-none { padding: 26px 10px; color: #7d7d84; font-size: 12.5px; line-height: 1.6; }

/* The way back out, over the list. It spans the column rather than sitting in the header row: the header
   is the search box, which is about the whole country and is unaffected by the zoom, while this is about
   the window the list below it has just been cropped to — so it belongs between the two, as the lid on
   the cropped list. Full width because it is the one control that undoes the row that was clicked, and
   nothing else in the column is a bar. */
.maplist-back { flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
                margin: 0 2px 8px; padding: 8px 11px; border-radius: 100px; cursor: pointer;
                background: rgba(216,181,107,0.07); border: 1px solid rgba(216,181,107,0.26);
                color: #c7b787; font-family: inherit; text-align: left;
                transition: background .25s ease, border-color .25s ease, color .25s ease; }
.maplist-back:hover { background: rgba(216,181,107,0.16); border-color: var(--gold); color: #f0dcae; }
.maplist-back b { flex: 0 1 auto; font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
                  font-weight: 600; }
/* Where the press lands, said quietly — it qualifies the button, it isn't a second label. */
.maplist-back i { flex: 0 0 auto; margin-left: auto; font-style: normal; font-size: 10.5px;
                  color: #7d7d84; }
.maplist-back:hover i { color: #8a8a91; }
.maplist-bk { flex: 0 0 auto; display: flex; }

/* A row is two targets, not one: the name selects the pin (staying on the map), the pill leaves for the
   gallery. Same split as a place row on the Places screen, so the two behave alike. */
.maprow { display: flex; align-items: center; gap: 8px; border-radius: 4px;
          border-left: 2px solid transparent; transition: background .2s ease; }
.maprow:hover { background: rgba(255,255,255,0.05); }
.maprow.on { background: rgba(216,181,107,0.11); border-left-color: var(--gold); }
/* Where a zoom landed, with nothing yet opened: dimmer than a selection, because it is one — it marks
   where in a re-grouped list the visitor now is, not what they have chosen. */
.maprow.at { border-left-color: rgba(216,181,107,0.4); }
.maprow-pick { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 11px;
               padding: 9px 8px; background: none; border: 0; color: inherit; font-family: inherit;
               cursor: pointer; text-align: left; }
.mapdot { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 100px; }
.maprow-t { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* One line, ellipsised: "Joseph Banks (Round Hill Head) Conservation Park" is a real leaf in this
   catalogue and wrapping it makes a row three deep. The pin's label and the strip's heading both give it
   in full. */
.maprow-nm { font-size: 13.5px; line-height: 1.25; color: #d9d7d2;
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.maprow.on .maprow-nm { color: #f5ecd6; }
.maprow-kd { font-size: 10.5px; color: #7d7d84; }
.mappill { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px; margin-right: 8px;
           padding: 4px 9px; border-radius: 100px; background: none; cursor: pointer;
           border: 1px solid rgba(216,181,107,0.28); color: #c7b787; font-family: inherit;
           font-size: 11.5px; font-variant-numeric: tabular-nums;
           transition: background .25s ease, border-color .25s ease, color .25s ease; }
.mappill:hover { background: rgba(216,181,107,0.16); border-color: var(--gold); color: #f0dcae; }
/* A place reached only on film: a tally, not a way in — there is no photograph filter to go to. */
.mappill-off { color: #7d7d84; border-color: rgba(255,255,255,0.12); cursor: default; }

/* The legend, over the map's top-left. Colour is the one thing about a pin that isn't self-evident.
   Rendered twice by PlacesMap.Legend — this copy lives inside the map, the other above it — and exactly
   one is shown at a time (see the narrow layout). */
.mapleg { position: absolute; left: 14px; top: 14px; z-index: 2; pointer-events: none;
          display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.mapleg-flow { display: none; }
.mapleg-i { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
            padding: 5px 12px; border-radius: 100px; font-size: 11px; color: #9a9aa1;
            background: rgba(11,11,13,0.62); border: 1px solid rgba(255,255,255,0.10);
            backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); }
.mapleg-i i { display: block; width: 7px; height: 7px; border-radius: 100px; }

/* ----- the open pin's band, under the map -----
   Not an overlay. The map is the whole subject of this screen, so the band takes height from it rather
   than covering it — the rule the panel beside the map kept, in the shape the design draws. `--strip-h`
   is a *constant*, reserved from the frame above (see .placespage-map .mapframe) only while a pin is
   open: stating it once means the map cannot be covered by a band that came out taller than expected,
   and means the map doesn't resize again as you move between a place with two photographs and one with
   six. The band scrolls inside that height if it ever needs to. No fill and no border box — a hairline
   above it is the whole separation, so it reads as part of the page and not as a widget over the map. */
/* **It is always here, and `on` is what opens it** (PlacesMap renders the element unconditionally). Drawn
   only while a pin was open, it took the map's height between two renders with nothing in between: the map
   jumped 15% smaller the instant a pin was tapped — every pin, every name and the coastline with it — and
   jumped back on close. The size it lands on is right; the jump is what states it as "the map changed"
   rather than "a panel opened". Everything that contributes height transitions together, so the frame above
   gives its height up over the same 340ms and the map eases down to its new scale. `overflow: hidden` is
   what keeps the contents (which appear and vanish at once, being inside the component's own `@if`) from
   spilling out of a box that is still closing. */
.mapstrip { flex: 0 0 auto; align-self: stretch; height: 0; min-height: 0; overflow: hidden;
            display: flex; flex-direction: column; gap: 8px;
            margin: 0; padding: 0;
            border-top: 0 solid rgba(255,255,255,0.07);
            transition: height .34s cubic-bezier(.4,0,.2,1), margin-top .34s cubic-bezier(.4,0,.2,1),
                        padding-top .34s cubic-bezier(.4,0,.2,1), border-top-width .34s cubic-bezier(.4,0,.2,1); }
.mapstrip.on { height: var(--strip-h); margin-top: 12px; padding-top: 12px; border-top-width: 1px; }
/* A visitor who asked for less movement gets the size change with no motion in it — the same bargain the
   rest of the site's transitions make. */
@media (prefers-reduced-motion: reduce) { .mapstrip { transition: none; } }
.mapstrip-hd { flex: 0 0 auto; display: flex; align-items: flex-end; gap: 12px; }
/* The heading hugs its own text, so the chevron that says "this opens the place" stays beside the name
   rather than being pushed out to the far edge of a band the width of the window. */
.mapstrip-nm { flex: 0 1 auto; min-width: 0; }
.mapstrip-x { margin-left: auto; }
.mapstrip-sub { font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase; color: #c7b787;
                font-weight: 500; }
.mapstrip-x, .mapstrip-back { flex: 0 0 auto; width: 30px; height: 30px; margin-bottom: 2px; display: grid; place-items: center;
                              border-radius: 100px; cursor: pointer; background: none;
                              border: 1px solid rgba(255,255,255,0.14); color: #9a9aa1; }
.mapstrip-x:hover, .mapstrip-back:hover { color: var(--paper); }

/* The tiles. **Nothing here stretches.** The row is a flex line of items at their natural width, so a
   pin with two photographs shows two tiles and then stops, rather than two tiles blown across the whole
   band; and each tile takes the frame's own aspect ratio (inline, from PlacesMap.TileBox), so a 3:1
   panorama is drawn as one instead of being cropped into a 3:2 cell. Height is what they share, which is
   what makes a row of different shapes read as one strip. Past the width, the row scrolls. */
.mapstrip-row { --tile-h: clamp(66px, 10vh, 96px);
                flex: 1 1 auto; min-height: 0;
                display: flex; align-items: flex-start; gap: 9px;
                overflow-x: auto; overflow-y: hidden; scrollbar-width: none; padding-bottom: 4px; }
.mapstrip-row::-webkit-scrollbar { display: none; }
.mapshot, .mapfilm { flex: 0 0 auto; height: var(--tile-h); width: auto;
                     min-width: 46px; max-width: min(300px, 50vw);
                     position: relative; overflow: hidden; background: #161619;
                     border: 0; padding: 0; cursor: pointer; }
/* The fallback shape, for a manifest that carries no dimensions — TileBox states the real one inline. */
.mapshot { aspect-ratio: 3 / 2; }
/* The films keep 16:9 whatever the still is: hqdefault is a letterboxed 4:3, and the crop is what takes
   the bars off. */
.mapfilm { aspect-ratio: 16 / 9; }
/* They sit in the same row as the photographs now, so something has to say which is which: a badge small
   enough to leave the frame it is inviting you into visible, where the label that used to do the job
   cost a row of its own. */
.mapfilm-p { position: absolute; left: 6px; bottom: 6px; z-index: 2; display: grid; place-items: center;
             width: 20px; height: 20px; border-radius: 100px; color: #f2f0ec;
             background: rgba(8,8,10,0.62); border: 1px solid rgba(255,255,255,0.22); }
/* The blur-up pair styles itself off .frame in the grid; a tile here is neither a grid tile nor a
   lightbox related-shot, so it carries its own copy of the rules that matter — including the slow push-in
   on hover, which is the grid's own gesture. */
.mapshot .ph, .mapplace .ph { filter: blur(14px); transform: scale(1.1); }
.mapshot .full, .mapplace .full { opacity: 0; transition: opacity .5s ease, transform 3.5s ease; }
.mapshot .full.on, .mapplace .full.on { opacity: 1; }
.mapshot:hover .full.on { transform: scale(1.055); }
.mapfilm img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
               transition: transform 3.5s ease; }
.mapfilm:hover img { transform: scale(1.055); }
.mapshot::after, .mapfilm::after { content: ""; position: absolute; inset: 0; z-index: 1;
                                   box-shadow: inset 0 0 0 1px rgba(216,181,107,0);
                                   transition: box-shadow .25s ease; pointer-events: none; }
.mapshot:hover::after, .mapfilm:hover::after { box-shadow: inset 0 0 0 1px rgba(216,181,107,0.65); }
.mapshot:focus-visible, .mapfilm:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

/* One tile per place, for a pin that stands for several. A cover, the name and the tally — enough to
   choose by, and a row of them where a section each was a wall. The cover is 3:2 whatever shape the
   photograph is: these are equals in a chooser, not photographs being shown, and a row of tiles at
   different widths reads as a strip of pictures rather than as a set of options. */
/* Narrower than a photograph tile is tall, so the cover plus two lines of name plus the tally fit the
   band's fixed height with room to spare — a name like "Brisbane City Botanic Gardens" takes both lines,
   and a clipped one in a chooser is a place you cannot tell apart from its neighbour. */
/* The chooser's tiles are the tallest thing the band holds — a cover *and* two lines of a name and a
   tally, where a photograph is only a photograph — and the band's height is a constant. So the tile is
   fitted to the row rather than the row to the tile: it stretches to the row's height and the cover
   gives up whatever the words need. Sized from `--tile-h` alone it ran 19px past the row on a phone, and
   what fell off the bottom was the tally — the line that tells two places at one pin apart. */
.mapplace { flex: 0 0 auto; align-self: stretch; width: calc(var(--tile-h) * 1.25);
            display: flex; flex-direction: column;
            gap: 6px; background: none; border: 0; padding: 0; cursor: pointer;
            font-family: inherit; text-align: left; color: inherit; }
/* Shrink-only: the aspect ratio is the cover's natural size and it keeps it wherever the row has the
   room, so nothing changes on a screen that was already fitting. object-fit:cover crops the rest. */
.mapplace-c { position: relative; display: block; width: 100%; aspect-ratio: 3 / 2; overflow: hidden;
              flex: 0 1 auto; min-height: 0; background: #161619; }
.mapplace-c img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.mapplace-c::after { content: ""; position: absolute; inset: 0; z-index: 1;
                     box-shadow: inset 0 0 0 1px rgba(216,181,107,0);
                     transition: box-shadow .25s ease; pointer-events: none; }
.mapplace:hover .mapplace-c::after { box-shadow: inset 0 0 0 1px rgba(216,181,107,0.65); }
.mapplace-t { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
/* Two lines at most, then ellipsis: "Joseph Banks (Round Hill Head) Conservation Park" is a real leaf
   here and it must not push the row taller than the band. */
.mapplace-nm { font-size: 11.5px; line-height: 1.25; color: #d9d7d2; overflow: hidden;
               display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.mapplace:hover .mapplace-nm { color: var(--gold-bright); }
.mapplace-n { font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; color: #7d7d84; }
.mapplace:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* The way out to the gallery, as the last tile in the row: the count in figures over the words, so the
   row ends on how much more there is. Gold-edged rather than filled — it is the one thing in the row
   that is not a photograph, and it should read as a door and not as a frame. */
.mapmore { flex: 0 0 auto; height: var(--tile-h); width: calc(var(--tile-h) * 1.2);
           display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
           cursor: pointer; background: rgba(216,181,107,0.07); color: #e7cf97;
           border: 1px solid rgba(216,181,107,0.4); font-family: inherit;
           transition: background .25s ease, border-color .25s ease; }
.mapmore:hover { background: rgba(216,181,107,0.16); border-color: var(--gold); }
.mapmore-n { font-size: clamp(15px, 2.6vh, 20px); line-height: 1; font-variant-numeric: tabular-nums; }
.mapmore-l { display: inline-flex; align-items: center; gap: 4px; font-size: 9px;
             letter-spacing: .16em; text-transform: uppercase; font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  .plregions, .plbackdrop { transition: none; }
  /* The hover lift goes; the selected pin's own size does NOT — it is a state the map has to state, not
     decoration, and the ring alone is a weaker answer than the pair. */
  .mappin:hover:not(.on) { transform: translate(-50%, -50%); }
  /* The zoom still happens — it is the content changing, not decoration — it just arrives at once. And
     with the map arriving at once there is nothing for the markers to wait for, so they don't: the fade
     exists only to stop the pins outrunning a gliding map. */
  .mapscale { transition: none; }
  .mapmarks, .mapmarks.settling { opacity: 1; transition: none; }
  .mapfilm:hover img { transform: none; }
}

/* ----- films (VideosView) -----
   The films are a screen, not a document, and the screen is the film: a full-bleed still under the
   floating chrome, exactly as the landing hero is (`.barstack.overhero`, for the same reason). There are fifteen films and the number barely moves, so the whole set rides along the bottom
   of it as a wall of stills on the shade — pointing at one brings it up, clicking plays it. Nothing is
   paged, scrolled or dragged.

   .filmlock is .pagelock's sibling — the same "exactly one viewport" column — but it hands the lock back
   on a window that can't hold a hero and a wall, where the still goes back to being a 16:9 block at the
   top of an ordinary scrolling page with the wall beneath it. Only a media query knows that, which is why
   this is a class of its own rather than a flag in C#. */
.filmlock { height: 100dvh; min-height: 0; overflow: hidden; }
.filmlock main { min-height: 0; display: flex; flex-direction: column; }

.filmpage { position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden; background: var(--ink); }

/* The still fills the view and is cropped to it (the hero's bargain: the window's shape wins over the
   frame's). The whole thing is one <button> — it plays what it is showing, which is what a visitor who
   has just pointed at a still wants next — so it sits under the wall rather than over it. */
.filmscreen { position: absolute; inset: 0; display: block; width: 100%; height: 100%;
              padding: 0; border: 0; background: none; cursor: pointer; }
.filmscreen:focus-visible { outline: 2px solid var(--gold); outline-offset: -4px; }
.filmscreen img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The two-layer YouTube fallback, for a film with no still of its own: the base is the same hqdefault
   the tile below is already showing — cached, so it paints on the same frame as the pointer — and the
   sharper maxresdefault fades in over it when it arrives. Not every video has one, hence the fallback in
   VideosView. A film that *does* have a still skips both of these and draws one un-faded layer. */
.filmscreen .sharp { opacity: 0; transition: opacity .5s ease; }
.filmscreen .sharp.on { opacity: 1; }

/* The preload shelf: every still in the DOM from the moment the screen opens, so the stage above can be
   one layer with no fade. Clipped to a pixel rather than display:none — a display:none image may never
   be fetched at all, which would defeat the whole point. */
.filmpreload { position: absolute; width: 1px; height: 1px; overflow: hidden;
               opacity: 0; pointer-events: none; }
/* Dark under the chrome, dark under the wall, barely there across the middle — the same shape as
   .homeshade, weighted harder at the foot because a wall of stills and two lines of type sit on it
   rather than one caption. */
.filmshade { position: absolute; inset: 0; pointer-events: none;
             background: linear-gradient(180deg, rgba(6,6,9,0.66) 0%, rgba(6,6,9,0.06) 24%,
                                                 rgba(6,6,9,0.12) 42%, rgba(6,6,9,0.68) 70%,
                                                 rgba(6,6,9,0.95) 100%); }

/* The name of the film, the count, and the wall — one stack along the bottom of the hero. The band takes
   pointer events (a click in the gap between two stills should not start a film) but the type does not:
   the title is over the picture, and a click there falls through to the picture's own button. */
.filmfoot { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
            display: flex; flex-direction: column; gap: clamp(8px, 1.2vh, 14px);
            padding: 0 var(--gutter) clamp(14px, 2.2vh, 26px); }
.filmmeta { pointer-events: none; }
.filmtitle { display: block; font-size: clamp(24px, 3.4vw, 46px); font-weight: 500; line-height: 1.05;
             color: var(--paper); text-shadow: 0 2px 24px rgba(6,6,9,0.5); }
.filmwhere { display: block; margin-top: 8px; font-size: 12px; letter-spacing: .16em;
             text-transform: uppercase; color: var(--gold); font-weight: 500; }

.filmcount { display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
             font-size: 11.5px; letter-spacing: .16em; text-transform: uppercase; color: #9a9aa2; }
.filmcount .pos { font-variant-numeric: tabular-nums; }

/* Chosen column counts, not auto-fill: fifteen divides tidily by five and by three, and eight leaves a
   second row of seven. auto-fill lands on nine or eleven at most window widths and hands back a ragged
   orphan row. minmax(0,1fr) rather than 1fr, or the tiles' nowrap titles set the track widths and the
   columns come out visibly uneven. */
.filmwall { flex: 0 0 auto; display: grid; gap: clamp(8px, .9vw, 14px);
            grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 520px) { .filmwall { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 820px) { .filmwall { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
@media (min-width: 1000px) { .filmwall { grid-template-columns: repeat(8, minmax(0, 1fr)); } }
.filmtile { display: block; text-align: left; padding: 0; border: 0; background: none; cursor: pointer; }
.filmtile:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
/* A <span> is inline, and aspect-ratio, width and height are all ignored on an inline box — leave this
   as one and every tile sizes itself from the raw 4:3 thumbnail instead of the 16:9 box asked for. */
.filmtile .shot { display: block; position: relative; aspect-ratio: 16 / 9; overflow: hidden;
                  background: rgba(11,11,13,0.55); border-radius: 2px;
                  box-shadow: 0 6px 22px rgba(6,6,9,0.45); }
/* Dimmer than they were over ink: these sit on a photograph, and a wall of fifteen stills at full strength
   competes with the one film the screen is currently about. */
.filmtile .shot img { display: block; width: 100%; height: 100%; object-fit: cover;
                      opacity: .5; transition: opacity .3s ease, transform 4s ease; }
.filmtile:hover .shot img { opacity: 1; transform: scale(1.05); }
.filmtile.on .shot img { opacity: 1; }
.filmtile.on .shot { outline: 1px solid var(--gold); }
.filmtile .lbl { display: block; margin-top: 7px; font-size: 12.5px; line-height: 1.3; color: #9a9aa2;
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
                 text-shadow: 0 1px 10px rgba(6,6,9,0.6); }
.filmtile:hover .lbl, .filmtile.on .lbl { color: var(--paper); }

/* Short windows: the tile labels go first. The still and the title above the wall already name the film
   being pointed at, and the labels are what the hero can least afford. */
@media (max-height: 700px) { .filmtile .lbl { display: none; } }

/* Too small to hold a hero and a wall: the lock comes off and the films become an ordinary page — the
   still is a 16:9 block at the top, the wall follows it down, and both scroll. */
@media (max-width: 900px), (max-height: 560px) {
  .filmlock { height: auto; min-height: 100vh; overflow: visible; }
  .filmlock main { display: block; }
  .filmpage { overflow: visible; }
  .filmscreen { position: relative; inset: auto; height: auto; aspect-ratio: 16 / 9; }
  .filmfoot { position: static; padding-top: 16px; padding-bottom: 90px; }
  .filmtile .lbl { display: block; }
  /* **The still starts below the bar here.** The chrome floats over this screen (`.barstack.overhero`,
     as on the landing hero) because the still fills the viewport and a picture stopping under a solid bar
     would read as a picture in a page. Once the lock comes off, the still is a 16:9 block at the top of a
     scrolling page instead — 219px tall on a 390px phone — and a two-row floating bar was covering half
     of it. `--barh` is the bar's measured height, published by portfolio.autoHideBar; the fallback covers
     the first paint before the observer has reported. The shade stays under the bar, so the gradient the
     brand sits on is unchanged. */
  .filmpage { padding-top: var(--barh, 60px); }
}
@media (prefers-reduced-motion: reduce) {
  .filmscreen .sharp { transition: none; }
  .filmtile:hover .shot img { transform: none; }
}

/* ----- loading indicator (dark) ----- */
.loading-progress { position: relative; display: block; width: 8rem; height: 8rem; margin: 20vh auto 1rem; }
.loading-progress circle { fill: none; stroke: rgba(255,255,255,0.1); stroke-width: 0.4rem; transform-origin: 50% 50%; transform: rotate(-90deg); }
.loading-progress circle:last-child { stroke: var(--gold); stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; transition: stroke-dasharray 0.05s ease-in-out; }
.loading-progress-text { position: absolute; text-align: center; font-weight: 500; inset: calc(20vh + 3.25rem) 0 auto 0; color: #7d7d84; font-family: var(--sans); }
.loading-progress-text:after { content: var(--blazor-load-percentage-text, "Loading"); }

#blazor-error-ui { background: #1a1418; color: #e7cf97; border-top: 1px solid rgba(216,181,107,0.3); bottom: 0; display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed; width: 100%; z-index: 1000; box-shadow: 0 -1px 20px rgba(0,0,0,0.5); }
#blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; }
