/* Committed to a single dark palette: this is a map of the sea at all hours,
   and a light theme would fight the water. */
/* Oswald, self-hosted so the app owns its look offline and asks nothing of a
   third party. Variable weight, one 21KB file, SIL Open Font Licence. */
@font-face {
  font-family: 'Oswald';
  src: url('vendor/fonts/oswald.woff2') format('woff2');
  font-weight: 200 700;
  font-display: swap;
  font-style: normal;
}

:root {
  color-scheme: dark;
  /* The condensed face is the app's voice: headings, the verdict and every
     number. Running prose stays in the system UI face, which is easier at
     small sizes and needs no download. */
  --display: 'Oswald', 'Helvetica Neue Condensed', 'Arial Narrow', system-ui, sans-serif;
  --header-height: 4.25rem;
  /* How far the details pane sits off the bottom. The extra 18px clears the
     attribution, which Cloudflare and the map both put in that corner — on a
     phone the line wraps to two, so 10px was not enough. Used by the swipe hint
     too, so the two cannot drift apart. */
  --panel-bottom: calc(max(.75rem, env(safe-area-inset-bottom)) + 18px);
  /* Raised from 19.5rem when the sport switcher took a row at the top of the
     card. The three places that position things against the card all read this,
     so they follow. */
  --panel-height: 21.6rem;
  --sea: #06283d;
  --land: #0e3b54;
  --ink: #dff6ff;
  --accent: #3ec5e0;
  --dim: rgba(223, 246, 255, .55);
  --panel: rgba(6, 40, 61, .94);
  --edge: rgba(62, 197, 224, .25);
}

* { box-sizing: border-box; }

html, body {
  margin: 0; height: 100%;
  background: var(--sea); color: var(--ink);
  font: 16px/1.45 system-ui, -apple-system, sans-serif;
  overscroll-behavior: none;
  /* Nothing here is prose. Every number is a reading that changes on the next
     refresh and every label is a drawing, so there is nothing worth selecting
     and a great deal to go wrong: on Android a long press began a selection
     drag on a card people were trying to swipe, and the callout covered the
     reading it was invoked on. Long press means something else here — see the
     hint in app.js. */
  user-select: none; -webkit-user-select: none;
  -webkit-touch-callout: none;
}

#map { position: absolute; inset: calc(var(--header-height) + env(safe-area-inset-top)) 0 0; }

/* Controls sit above the map and clear the notch on phones. */
.controls {
  position: absolute; z-index: 2;
  /* Clear of the scale, which now sits centred across the top of the map. */
  top: calc(var(--header-height) + env(safe-area-inset-top) + 3.25rem); right: .75rem;
  display: flex; flex-direction: column; gap: .5rem;
}

button {
  width: 2.75rem; height: 2.75rem;
  display: grid; place-items: center;
  border: 1px solid var(--edge); border-radius: .75rem;
  background: var(--panel); color: var(--accent);
  cursor: pointer; padding: 0;
}
button:hover { border-color: var(--accent); }
button svg { width: 1.35rem; height: 1.35rem; }

/* The reading panel. Numbers are the content; glyphs label them. */
#panel {
  /* Fixed to the viewport rather than positioned in the document.
     
     On Android a drag that began on the card could carry it off the screen,
     which it should never have been able to do: nothing here is draggable and
     the card is furniture, not content. Fixed anchors it to the viewport
     whatever the document does underneath — including the address bar
     collapsing, which changes the containing block an absolute box is measured
     against and slid the card by exactly its height. */
  position: fixed; z-index: 3;
  left: .75rem; right: .75rem;
  bottom: var(--panel-bottom);
  max-width: 30rem; margin-inline: auto;
  background: var(--panel); border: 1px solid var(--edge);
  border-radius: 1rem; padding: 1rem;
  backdrop-filter: blur(12px);
  transform: translateY(calc(100% + 1.5rem));
  transition: transform .25s ease;
  /* Fixed rather than fitted. The contents vary — a spot with no bathing water
     classification is a tile short — and a card that resized between spots was
     jarring to swipe through, and moved the map underneath it as it went. The
     cap keeps it sane on a short screen; anything taller scrolls inside. */
  height: var(--panel-height);
  max-height: 62dvh;
  overflow-y: auto;
  /* The only gesture the card owns vertically is scrolling its own contents.
     pan-y tells the browser so up front, which stops a horizontal drag being
     interpreted as a pan of anything, and contain stops a scroll that reaches
     the end of the card continuing into the page behind it. */
  touch-action: pan-y;
  overscroll-behavior: contain;
}
#panel[data-open] { transform: none; }

/* Which sport the card is answering for.
   
   Glyphs rather than dots, and rather than words. A dot only says there is
   something else to see and leaves you to find out what; a surfboard says what
   it is, and says it in every language — the same reason nothing else on this
   map is drawn as text.
   
   Quiet until chosen. This is the first real mode in an app whose whole
   character is not having settings, so it sits at the top of the card looking
   like a label rather than a control demanding a decision. */
/* The row carrying the sport name and its icons. Three columns so the icons
   stay dead centre whatever the name's length; the name sits at the left edge
   of the card. */
.sports-row {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  margin: -.15rem 0 .35rem;
}
.sport-name {
  justify-self: start;
  /* Twice the original size where the card has room, shrinking on narrow
     phones so QUALITY never touches the icons or pushes them off centre. */
  font-family: var(--display); font-size: clamp(1rem, calc(13.5vw - 32px), 1.56rem);
  letter-spacing: .06em; line-height: 1;
  color: var(--accent);
}
.sports {
  /* Centred. It sits above the verdict rather than beside anything, so aligning
     it to the left edge left it looking like a stray first field. */
  display: flex; justify-content: center; gap: .15rem;
}
.sports button {
  /* Wider than it looks. The card wants a small mark and a thumb wants a big
     target, and only one of those has to be visible. */
  width: 2.4rem; height: 1.9rem;
  border: 0; border-radius: .5rem;
  background: none; color: var(--dim);
  opacity: .45;
  transition: opacity .15s ease, color .15s ease;
}
/* The glyph is the button's only child on purpose. Buttons here centre their
   contents with a grid, so a stray whitespace text node — from putting the
   opening tag and the svg on separate lines, which looks tidier — becomes an
   anonymous grid item and shifts the mark off centre. That is why the markup
   in app.js has its bracket hanging where it does. */
.sports button svg { width: 1.2rem; height: 1.2rem; }
.sports button:hover { opacity: .8; }
.sports button[aria-selected="true"] { color: var(--accent); opacity: 1; }

.spot-name {
  /* The place and how its water has been testing, on one line. The six marks
     used to have a line of their own beneath this one, which spent a whole row
     of a fixed-height card on six dots and left them too small to tap.
     
     space-between puts the name at one end and the history at the other, and
     the name is the half that truncates: a long one is worth an ellipsis, and
     the marks are worth nothing at all if they cannot be hit. */
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  font-family: var(--display);
  font-size: 1.25rem; font-weight: 500; letter-spacing: .01em;
  margin: 0 0 .6rem;
}

/* The name yields before the weather does: a long place name is truncated
   rather than pushing the sky off the end of the line. */
/* The last six samples, oldest on the left.
   Small and evenly spaced: this is a shape to be taken in at a glance rather
   than a chart to be read off, and the numbers behind it are in the tiles
   underneath. Sized in rem so it holds against the card rather than against
   whatever type happens to sit above it. */
.samples {
  display: flex; align-items: center;
  /* On the name's line now, so it must not be squeezed by a long place name —
     the name has the ellipsis and yields first. */
  flex: none; margin: 0;
}
/* The tappable square. Mostly empty and butted against its neighbours, so the
   whole strip is live and a tap cannot land in a gap between two marks. */
.sample {
  display: grid; place-items: center;
  width: 1.65rem; height: 1.65rem;
}
.samples i {
  width: .8rem; height: .8rem;
  border-radius: 50%;
  /* A ring in the card's own edge colour, so a pale grade still reads as a
     mark rather than dissolving into the panel. */
  box-shadow: 0 0 0 1px var(--edge);
}

.spot-name-text {
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.readings {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: .6rem; margin: 0;
}

.reading {
  display: flex; align-items: center; gap: .4rem;
  padding: .45rem .5rem;
  border: 1px solid var(--edge); border-radius: .6rem;
}
.reading svg { width: 1.25rem; height: 1.25rem; flex: none; color: var(--accent); }

/* A flex item will not shrink below its content by default, which is how a
   long word like "Excellent" pushed out of its tile rather than fitting in it.
   min-width lets it shrink; the ellipsis catches anything longer still. */
.reading > div { min-width: 0; }
.reading > div > * { display: block; white-space: nowrap; }

/* Only words are allowed to ellipsis. A truncated number is worse than none —
   "20…" could be 20.8 or 209 — so numeric tiles are given room instead, and
   the digits shrink to fit rather than being cut off.
   Oswald has no tabular figures, so font-variant-numeric does nothing and a
   zero is markedly wider than a one: 20.8 measures 39px where 21.7 measures 34.
   That five pixel difference was the whole bug. */
.reading--word > div > * { overflow: hidden; text-overflow: ellipsis; }

/* Tiles holding a word rather than a number need more room than the grid gives
   a two or three digit number, but not a whole row to themselves — two columns
   is enough for a classification, and on a narrow screen that is the full width
   anyway because the grid only fits two. */
.reading--word { grid-column: span 2; }
.reading--word b { font-size: 1.15rem; }
.reading b {
  font-family: var(--display);
  font-size: 1.25rem; font-weight: 500;
  line-height: 1.05;
}

/* The two temperatures, larger than the rest and larger again since the sports
   arrived.
   They are the reading everything else qualifies — waves and wind describe how
   unpleasant the water will be, but the temperature is the water — and they are
   the only pair that means the same thing to a swimmer, a surfer and a kiter,
   all of whom end up wet and then standing in the wind. Room for it came from
   dropping the unit, which now sits by the map scale, so this costs no width. */
.reading--temp b,
.reading--chill b { font-size: 1.9rem; }
.reading small {
  display: block; color: var(--dim); font-size: .7rem;
  /* Reserved even when empty. The temperatures state their unit beside the map
     scale instead of here, and without a floor the tiles holding a unit would
     stand taller than the tiles that do not, so a row of readings would sit at
     two different heights. */
  min-height: 1em;
}

/* The water quality tile is always drawn, showing a dash where the Environment
   Agency has no rating or no forecast for a spot. Rendering it conditionally
   meant the grid gained and lost a whole tile between spots, so every field
   below shuffled as you swiped along the coast. */

/* Provenance, drawn rather than written: a filled dot is an instrument in the
   water, a hollow ring is a model's opinion. Never let them look alike. */
.mark {
  width: .5rem; height: .5rem; border-radius: 50%;
  border: 1.5px solid var(--accent); flex: none; margin-left: auto;
}
.mark[data-measured] { background: var(--accent); }

.tide {
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  margin-top: .6rem; color: var(--dim); font-size: .8rem;
}

/* Each entry is a glyph and its time, kept together. */
.tide span { display: inline-flex; align-items: center; gap: .25rem; }
.tide svg { width: 1rem; height: 1rem; color: var(--accent); flex: none; }

.stale { color: #ffb454; }


/* Attribution. A licence condition rather than decoration — OGL, CC-BY and
   ODbL each require it in the running app — so it is quiet rather than hidden.
   Left in the corner a map's credits normally occupy, and toned down rather
   than boxed up: a heavy control there reads as clutter. */
.maplibregl-ctrl-bottom-right { z-index: 1; }

.maplibregl-ctrl-bottom-right .maplibregl-ctrl-attrib,
.maplibregl-ctrl-bottom-right .maplibregl-ctrl-attrib.maplibregl-compact,
.maplibregl-ctrl-attrib,
.maplibregl-ctrl-attrib.maplibregl-compact {
  /* MapLibre paints a translucent white panel behind this. Over a dark map it
     reads as a smudge rather than a label, and the text is legible without it. */
  /* Qualified by the container as well, because the container's own rules are
     more specific than a bare class and were winning on background. */
  background: none;
  background-color: transparent;
  backdrop-filter: none;
  box-shadow: none;
  border-radius: 0;
  margin: 0 .4rem .3rem 0;
  padding: 0;
  min-height: 0;
}
.maplibregl-ctrl-attrib-inner {
  color: var(--dim); font-size: .6rem; line-height: 1.3;
  opacity: .6; text-align: right;
}
.maplibregl-ctrl-attrib a { color: var(--accent); }


/* The verdict. It is the reason the app exists, so it is the largest thing in
   the panel and the readings beneath it are the argument for it. */
.verdict {
  margin: 0 0 .5rem;
  display: flex; align-items: baseline; justify-content: space-between; gap: .75rem;
  font-family: var(--display);
  font-size: clamp(1.75rem, 8vw, 2.4rem);
  font-weight: 700; text-transform: uppercase;
  letter-spacing: .02em; line-height: 1;
}

/* The score behind the word. Quieter than the verdict, because the verdict is
   the decision and this is the working. */
.verdict .pct {
  flex: none; font-size: .62em; font-weight: 600;
  opacity: .75; font-variant-numeric: tabular-nums;
}
.verdict .pct i { font-style: normal; font-size: .7em; opacity: .7; }
/* The sky, beside the verdict. It sat by the place name until the capture time
   was dropped from the heading and left a gap there; the weather belongs with
   the answer anyway, being the one reading a swimmer can check by looking out
   of the window.

   Inline-flex rather than the flex used when it lived in the place-name row:
   the heading's inner span is not a flex container, so a plain `display: flex`
   would take a line of its own. Sized in `em` so it tracks the verdict, which
   is itself clamped against the viewport. */
.verdict .sky {
  display: inline-flex; align-items: center;
  gap: .16em;
  margin-left: .38em;
  vertical-align: .04em;
}
.verdict .sky svg {
  width: .58em; height: .58em;
  /* Deliberately not currentColor. Inheriting the verdict's colour tints the
     weather with the judgement, so a sunny day vetoed for waves draws a red
     sun and reads as though the sun were the problem. The weather is an
     observation; the colour is a decision; they are not the same thing. */
  color: var(--ink); opacity: .5;
}

.verdict small .spilling {
  /* The same pink as the rings on the map, so the sentence and the markers are
     one statement. Deliberately not the red a Poor rating uses: this is a thing
     happening now, not a four year classification, and the two must not be read
     as the same claim. */
  color: #f472b6;
}

.verdict small .aside {
  /* Not a warning. Used to mark the whole line when there was nothing else to
     say; now it marks just the tide note within it, so a warning and the tide
     can share the line without the note reading as part of the warning. */
  opacity: .55;
  font-style: italic;
}

.verdict small {
  /* Always rendered, empty when the verdict needs no justifying, and a fixed
     height so the line is reserved either way. Otherwise a HELL YEAH — which
     explains itself and carries no reason — is a line shorter than everything
     else, and every field below it jumps as you swipe between spots. */
  display: block; margin-top: .15rem;
  height: 1.05rem; line-height: 1.05rem;
  font-size: .75rem; font-weight: 400; letter-spacing: 0;
  color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* The tide note that can trail a warning is italic, and an italic glyph's
     ink slants past its own logical width — "coming in" was getting its
     final n clipped by this box's hard edge even though the text fit. A
     sliver of right padding gives the slant somewhere to land before the
     clip does. */
  padding-right: .15em;
}
.verdict[data-tone="yes"] { color: #4ade80; }
.verdict[data-tone="good"] { color: #a3e635; }
.verdict[data-tone="hmm"] { color: #fbbf24; }
.verdict[data-tone="no"] { color: #f87171; }

/* Full-width header. A solid bar rather than a floating pill: it names the app
   and sets the type, and the map begins beneath it. */
#banner {
  position: absolute; z-index: 4;
  top: 0; left: 0; right: 0;
  height: calc(var(--header-height) + env(safe-area-inset-top));
  padding: env(safe-area-inset-top) .5rem 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--sea);
  border-bottom: 1px solid var(--edge);
}

#banner h1 {
  margin: 0;
  font-family: var(--display);
  /* The same size as the verdict: the question and the answer are the two
     loudest things in the app and should be set alike. */
  font-size: clamp(1.75rem, 8vw, 2.4rem);
  font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; line-height: 1;
  white-space: nowrap;
}

/* On narrow screens the pill would run into the buttons, so it steps down and
   the map keeps the full width. */
@media (max-width: 30rem) {
  #banner { font-size: .85rem; padding: .45rem .8rem; }
}

/* Geolocation state, drawn rather than written. Seeking pulses; failed goes
   red and stays red until the next attempt, because a silent no-op reads as a
   broken button. */
button[data-state="seeking"] { animation: pulse 1.1s ease-in-out infinite; }
button[data-state="failed"] { color: #f87171; border-color: #f87171; }

@keyframes pulse {
  50% { opacity: .45; }
}

@media (prefers-reduced-motion: reduce) {
  button[data-state="seeking"] { animation: none; opacity: .6; }
}



/* MapLibre only offers the four corners, so the top-right container is
   stretched across the map and its contents centred. The float has to go with
   it, and the container stops taking pointer events so it does not sit over the
   map catching drags that were meant for it. */
.maplibregl-ctrl-top-right {
  left: 0; right: 0;
  display: flex; justify-content: center;
  pointer-events: none;
}
.maplibregl-ctrl-top-right .maplibregl-ctrl {
  float: none;
  pointer-events: auto;
}

/* Scale bar. Drawn as a measuring line rather than a boxed control: a rule with
   ticks at each end reads as a distance, where a filled bar reads as a button. */
.maplibregl-ctrl-scale {
  background: none;
  border: none;
  border-bottom: 2px solid var(--dim);
  border-left: 2px solid var(--dim);
  border-right: 2px solid var(--dim);
  border-radius: 0;
  margin: .75rem 0 0;
  padding: 0 .25rem 2px;
  color: var(--ink);
  font-family: var(--display);
  font-size: .7rem; letter-spacing: .04em;
  line-height: 1.2; text-align: center;
  opacity: .75;
  text-shadow: 0 1px 2px var(--sea);
}

/* The unit, beside the scale bar. Matched to it deliberately: the two are one
   statement about how this map measures, so they should not look like different
   kinds of thing. */
/* The key for a discharging outflow, under the temperature unit. Same size and
   weight as it, because both are notes about how to read the map rather than
   readings in their own right. The swatch mirrors the marker exactly: hollow,
   so it cannot be taken for a rated beach. */
.spill-key {
  display: flex; align-items: center; gap: .32rem;
  margin: .3rem 0 0 .5rem;
  color: var(--ink);
  font-family: var(--display);
  font-size: .7rem; letter-spacing: .04em;
  line-height: 1.2;
  opacity: .8;
  text-shadow: 0 1px 2px var(--sea);
}
.spill-key i {
  width: .6rem; height: .6rem;
  border-radius: 50%;
  border: 1.5px solid #f472b6;
  background: #f472b61f;
}

.units-note {
  margin: .75rem 0 0 .5rem;
  padding: 0 0 2px;
  color: var(--ink);
  font-family: var(--display);
  font-size: .7rem; letter-spacing: .04em;
  line-height: 1.2;
  opacity: .75;
  text-shadow: 0 1px 2px var(--sea);
}

/* The title is set at the verdict's size and must not wrap, so on the narrowest
   phones the tracking closes up rather than the line running off the edge. */
@media (max-width: 23rem) {
  #banner h1 { letter-spacing: 0; }
  #banner { padding-inline: .35rem; }
}

/* Loading. A rolling swell rather than a spinning ring: the app is about the
   sea, and a wave reads as "still coming" without borrowing an icon from
   somewhere else. Positioned over the middle of the card from outside it,
   because the card scrolls and would clip anything laid over it. */
#loading {
  position: absolute; z-index: 4;
  left: 0; right: 0;
  bottom: calc(var(--panel-bottom) + var(--panel-height) / 2 - 1.25rem);
  display: flex; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity .2s ease;
}
#panel[data-loading] ~ #loading { opacity: 1; }

#loading svg { width: 5rem; height: 2.1rem; overflow: visible; }

.swell path {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.4;
  stroke-linecap: round;
}

/* One wavelength is 24 units, so translating by exactly that returns the path
   to where it started and the loop is seamless. */
.swell { animation: swell 1.6s linear infinite; }
.swell--back { opacity: .35; animation-duration: 2.6s; }
.swell--front { opacity: .9; }

@keyframes swell {
  to { transform: translateX(24px); }
}

@media (prefers-reduced-motion: reduce) {
  .swell { animation: none; }
  #panel[data-loading] ~ #loading { opacity: .8; }
}

/* A one-off nudge towards the locate button, for someone who has just arrived
   and is looking at a map of the whole country. Chunky and white so it reads
   instantly against the sea, and gone for good once the button has been used. */
#locatehint {
  position: absolute; z-index: 5;
  /* Level with the middle of the locate button, just off its left edge: the
     button is 2.75rem square at .75rem from the right, so its centre line sits
     4.625rem below the header and its left edge 3.5rem in. */
  top: calc(var(--header-height) + env(safe-area-inset-top) + 4.625rem - 1.15rem);
  right: 3.9rem;
  width: 3.6rem;
  pointer-events: none;
  animation: nudge 1.4s ease-in-out infinite;
  transition: opacity .4s ease;
  filter: drop-shadow(0 2px 4px rgba(3, 20, 31, .7));
}
#locatehint[data-gone] { opacity: 0; }
/* preserveAspectRatio is off so the shape fills whatever box it is given. With
   a filled arrow that simply makes it bigger in both directions, where a
   stroked one would have thinned in one and thickened in the other. The offset
   above is half this height, keeping it level with the button's centre. */
#locatehint svg {
  display: block;
  width: 100%; height: 2.3rem;
  color: #fff;
}

@keyframes nudge {
  50% { transform: translateX(.4rem); }
}

@media (prefers-reduced-motion: reduce) {
  #locatehint { animation: none; }
}

/* The wind's direction, beside the sky rather than beside the speed.

   Accented rather than inked, unlike the weather glyph it follows. The sky is
   an observation and stays neutral; the arrow is the one mark in the heading
   whose angle is a live reading, and the colour is what stops a rotated
   triangle reading as decoration. Sized a shade under the weather glyph, since
   an arrow needs less area than a picture to be legible. */
.verdict .sky .whence {
  width: .5em; height: .5em;
  color: var(--accent); opacity: .85;
  flex: none;
}

/* The half cycle: a turning point and its time at each end, and the curve
   between them carrying where we are. Replaces two separate entries that gave
   the times without the shape. */
.tidephase { display: inline-flex; align-items: center; gap: .3rem; }
.tidephase b {
  font-family: var(--display); font-weight: 500;
  color: var(--ink); opacity: .8;
}
.tidephase b i { font-style: normal; font-size: .8em; opacity: .7; }
.phasecurve {
  width: 2.9rem; height: 1rem;
  margin: 0 .1rem;
  overflow: visible;
}
.phasecurve path {
  fill: none; stroke: var(--accent); stroke-width: 1.5;
  stroke-linecap: round; opacity: .5;
}
.phasecurve circle { fill: var(--ink); }

/* Beaches the Environment Agency flags as having their water quality affected
   by heavy rain. Quiet when it is dry — it is a property of the place — and lit
   when it is actually raining, which is the moment it stops being trivia. */
.rainflag {
  width: 1rem; height: 1rem; flex: none;
  color: var(--dim); opacity: .55;
  margin-left: auto;
}
/* The lit state, when rain has actually fallen on a water whose quality is
   known to suffer for it.
   
   Scoped through .verdict .sky because that is what it has to beat: that rule
   paints every weather glyph white at half opacity on purpose, so the sky is
   an observation rather than a judgement. This one IS a judgement — the rain
   is why the headline above it is red — and it was losing on specificity,
   sitting there white and faint while the card said POLLUTION RISK.
   
   Bigger as well as brighter. At .58em it was a detail on a line of details;
   the thing it marks is the reason for the whole card. */
.verdict .sky .rainflag.now {
  color: #fbbf24;
  opacity: 1;
  width: .72em; height: .72em;
}

/* What a title attribute says, on a device with nothing to hover with.
   
   Positioned by script and clamped to the viewport, because the thing being
   explained is often against an edge — the first tile is hard against the left
   of the card and the rating against the right. */
#hint {
  position: fixed; z-index: 5;
  max-width: min(22rem, calc(100vw - 1.5rem));
  padding: .5rem .65rem;
  border-radius: .5rem;
  background: #0b1f2ef2; border: 1px solid var(--edge);
  color: var(--ink); font-size: .8rem; line-height: 1.35;
  box-shadow: 0 .5rem 1.5rem #00000059;
  /* It must never eat the touch that follows it. */
  pointer-events: none;
  backdrop-filter: blur(8px);
}

/* Town names on the map. Quiet, so they read as landmarks rather than
   competing with the beaches, and they never take a tap. */
.town-label {
  pointer-events: none;
  font-family: var(--display);
  font-size: .72rem; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink); opacity: .55;
  text-shadow: 0 1px 3px var(--sea), 0 0 2px var(--sea);
  white-space: nowrap;
}
