/**
 * KC AI PET MATCH — HOMEPAGE. Review preview stylesheet.
 *
 * Palette is the established thermal set. There is no yellow and no gold
 * anywhere in this file, deliberately: the brief rules them out and the dog
 * thermal ramp passes close enough to amber that a stray #F5C518 would read as
 * part of the system rather than as a mistake.
 *
 *   electric blue   #00A4FD   interface, links, primary action
 *   cat purple      #A855F7   species: cat
 *   dog thermal     #FF5C1F   species: dog  (deep #FF4D1F)
 *   metallic white  #FEFEFE   surfaces
 *   slate           #7C8B9C   NON-TEXT ONLY — measured 3.98:1, fails AA
 *   ink             #0A141D   body text
 *
 * SLATE IS NOT A TEXT COLOUR. It was documented as "AA large text" once and
 * measured at 3.98:1 against a panel, which produced five real contrast
 * failures. `--dim` (#4A5C6D) is the dimmest text colour used here.
 */

/* =========================================================== THE TOKEN SET
 *
 * ONE SET OF NAMES, TWO SETS OF VALUES, AND THE ATTRIBUTE ALWAYS DECIDES.
 *
 * `data-theme` is written on <html> by an inline script in <head> BEFORE the
 * first paint (see includes/homepage.php). It is never absent on a page that
 * ran that script, so every rule below keys off the attribute and nothing keys
 * off the media query. That is what makes the toggle instant and flash-free:
 * there is no moment where the page has painted one theme and the script wants
 * the other.
 *
 * The `prefers-color-scheme` block exists for exactly one visitor — the one
 * with JavaScript disabled, for whom the attribute never arrives. It is a
 * fallback, not the mechanism, and it is written `:not([data-theme])` so it can
 * never fight a choice the visitor has actually made.
 *
 * SEMANTIC NAMES ONLY BELOW THIS BLOCK. A rule further down asks for --surf,
 * never for #FFFFFF, which is why a second theme was a value change rather than
 * a rewrite.
 */

:root,
:root[data-theme="light"] {
	color-scheme: light;

	--blue:   #00A4FD;
	--blue-d: #0B7FC4;
	--blue-t: #05628F;          /* blue as TEXT on a light surface — measured */
	--cat:    #A855F7;
	--dog:    #FF5C1F;
	--dog-d:  #FF4D1F;
	--white:  #FEFEFE;
	--ink:    #0A141D;
	--dim:    #4A5C6D;
	--line:   #DCE4EB;
	--surf:   #FFFFFF;
	--surf-2: #F7FAFC;          /* a hair off the surface: chips, inputs, wells */
	--page:   #F4F7FA;
	--slate:  #7C8B9C;          /* borders, dashes, icons — never text */

	--shadow:   0 1px 2px rgba(10, 20, 29, .06), 0 8px 24px rgba(10, 20, 29, .08);
	--shadow-l: 0 2px 6px rgba(10, 20, 29, .1), 0 18px 48px rgba(10, 20, 29, .18);

	--scrim:  rgba(10, 20, 29, .42);
	--tiles-filter: none;       /* the basemap is already a light basemap */

	/*
	 * TWO PATTERNS THAT ARE NOT "INK" AND NOT "SURFACE".
	 *
	 * --solid is the INVERTED CONTROL: a chip, tab count, segment or primary
	 * button that reads as the opposite of the page it sits on. It was written
	 * as background:var(--ink); color:var(--white), which is the same thing in
	 * a light design and is near-white-on-near-white in a dark one. Ten rules
	 * shared that assumption.
	 *
	 * --band is the DELIBERATELY DARK STRIP: the product statement, the metro
	 * summary and the footer. Those are dark on purpose, not dark because the
	 * page is light, so in dark they stay dark - a light footer under a dark
	 * page would be the inversion nobody asked for.
	 */
	--solid:   #0A141D;
	--solid-t: #FEFEFE;
	--band:    #0A141D;
	--band-t:  #EAF2F8;
}

/*
 * DARK.
 *
 * Not an inversion. The surfaces are chosen so the SAME accent fills keep
 * working — a thermal orange that clears 4.5:1 under white text clears it on
 * either surface, because that ratio is a property of the fill and the label,
 * not of the page behind them. What has to change is every colour that is read
 * AGAINST a surface: body text, dim text, borders, and the two accents when
 * they are used as text rather than as a fill.
 */
:root[data-theme="dark"] {
	color-scheme: dark;

	--blue:   #4FC3FF;          /* interface accent, non-text and large only */
	--blue-d: #7BD2FF;
	--blue-t: #6FCDFF;          /* blue as TEXT on a dark surface — measured */
	--cat:    #C89BFF;
	--dog:    #FF7A45;
	--dog-d:  #FF8F5F;
	--white:  #FEFEFE;
	--ink:    #EEF4F8;          /* body text */
	--dim:    #A9BBC8;          /* dimmest passing text colour on --surf */
	--line:   #29384A;
	--surf:   #16212B;
	--surf-2: #101A23;
	--page:   #0B131A;
	--slate:  #6C8095;

	--shadow:   0 1px 2px rgba(0, 0, 0, .5), 0 10px 28px rgba(0, 0, 0, .55);
	--shadow-l: 0 2px 8px rgba(0, 0, 0, .55), 0 20px 48px rgba(0, 0, 0, .7);

	--scrim:  rgba(0, 0, 0, .62);

	/*
	 * THE BASEMAP IS A PICTURE, AND A PICTURE CANNOT BE RE-TOKENISED.
	 *
	 * The tiles are raster images from a light basemap. There is no dark tile
	 * URL to switch to without changing the map engine's configuration, which
	 * this release is forbidden to touch, so the tile PANE alone is filtered.
	 *
	 * Scoped to `.leaflet-tile-pane` and nothing else, deliberately: filtering
	 * the map container would invert the photo pins as well and hand every
	 * missing animal a negative of itself. Markers, popups, rings and controls
	 * live in other panes and are themed with real colours below.
	 */
	--tiles-filter: invert(1) hue-rotate(180deg) brightness(.94) contrast(.92) saturate(.85);

	--solid:   #EAF2F8;
	--solid-t: #0B131A;
	--band:    #101A23;
	--band-t:  #EEF4F8;
}

/* JavaScript off: follow the device, and never fight an explicit choice. */
@media (prefers-color-scheme: dark) {
	:root:not([data-theme]) {
		color-scheme: dark;
		--blue: #4FC3FF; --blue-d: #7BD2FF; --blue-t: #6FCDFF;
		--cat: #C89BFF; --dog: #FF7A45; --dog-d: #FF8F5F;
		--ink: #EEF4F8; --dim: #A9BBC8; --line: #29384A;
		--surf: #16212B; --surf-2: #101A23; --page: #0B131A; --slate: #6C8095;
		--shadow: 0 1px 2px rgba(0,0,0,.5), 0 10px 28px rgba(0,0,0,.55);
		--shadow-l: 0 2px 8px rgba(0,0,0,.55), 0 20px 48px rgba(0,0,0,.7);
		--scrim: rgba(0,0,0,.62);
		--tiles-filter: invert(1) hue-rotate(180deg) brightness(.94) contrast(.92) saturate(.85);
		--solid: #EAF2F8; --solid-t: #0B131A; --band: #101A23; --band-t: #EEF4F8;
	}
}

:root {
	/*
	 * 60px, not 56px. The two posting actions are primary controls and the
	 * brief sets a 44px floor for those; at a 56px bar they measured 40px and
	 * had no room to grow. Four pixels of nav buys the accessible target.
	 */
	--nav-h:  60px;
	--stmt-h: 44px;

	/*
	 * HOW FAR THE MAP CARD REACHES PAST THE FIRST SCREEN.
	 *
	 * The card used to end exactly at the fold. This release grows the map, and
	 * the growth is expressed here as one number per breakpoint so the whole
	 * decision is legible in one place rather than spread across five media
	 * queries. Bigger on desktop, where there is room; small on a phone, where
	 * the map already occupies three quarters of the screen and the brief's own
	 * "must not overwhelm the page on mobile" is the binding constraint.
	 */
	--map-grow: 3vh;

	--r:      12px;

	--font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

@media (min-width: 561px)  { :root { --map-grow: 6vh; } }
@media (min-width: 861px)  { :root { --map-grow: 11vh; } }

* { box-sizing: border-box; }

/*
 * THE `hidden` ATTRIBUTE MUST WIN.
 *
 * `hidden` is only `display:none` in the UA stylesheet, so ANY author rule
 * that sets `display` beats it. Three elements here are laid out with flex or
 * grid and toggled by `hidden` — the selected panel, the shelter toggle and the
 * empty state — and every one of them rendered permanently visible: the panel
 * shipped as an empty white box covering a third of the map on first paint.
 *
 * It is not enough to set `display:none` on each of them, because the same trap
 * returns for the next element someone lays out. This is the rule that closes
 * it for the whole page.
 */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
	margin: 0;
	background: var(--page);
	color: var(--ink);
	font: 400 16px/1.55 var(--font);
	/*
	 * The page must never scroll sideways at 320px. Wide children (the stats
	 * strip, the filter row, the carousels) each own an overflow-x container;
	 * this is the backstop, not the mechanism.
	 */
	overflow-x: hidden;
}

h1, h2, h3 { margin: 0; letter-spacing: -.015em; }

.kch-vh {
	position: absolute !important;
	width: 1px; height: 1px;
	margin: -1px; padding: 0; border: 0;
	overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
	white-space: nowrap;
}

.kch-skip {
	position: absolute; left: -9999px; top: 0; z-index: 200;
	background: var(--solid); color: var(--solid-t);
	padding: 12px 18px; border-radius: 0 0 8px 0; font-weight: 700;
}
.kch-skip:focus { left: 0; }

:focus-visible {
	outline: 3px solid var(--blue);
	outline-offset: 2px;
	border-radius: 4px;
}

/* ============================================== MODE BANNERS (never on live) */

/*
 * Sticky, full width, and loud on purpose.
 *
 * These appear only in interaction or presentation mode. Their job is to make
 * it impossible to produce a screenshot of a proposed flow, or of a named
 * shelter with invented counts, that could be mistaken for the shipping
 * product. That means they must survive scrolling and cropping — hence sticky
 * rather than a one-off strip at the top of the document.
 */
.kch-modebar {
	position: sticky;
	top: 0;
	z-index: 900;
}

.kch-modebar__m {
	margin: 0;
	padding: 9px 16px;
	font: 700 12.5px/1.45 var(--font);
	letter-spacing: .2px;
	text-align: center;
	border-bottom: 2px solid rgba(0, 0, 0, .25);
}

.kch-modebar__m.is-interaction { background: #2B1A46; color: #E9D5FF; }
.kch-modebar__m.is-presentation { background: #4A1D05; color: #FFD9C7; }
.kch-modebar__m.is-blocked { background: #0A141D; color: #FF8A5C; }

/* The banner is fixed furniture, so the hero must not also claim 100vh. */
.has-modebar .kch-mapcard { height: calc(100vh - var(--nav-h) - var(--stmt-h) + var(--map-grow) - 40px); }

/*
 * A proposed action is visibly not a shipped one.
 *
 * It only ever renders in interaction mode. The dashed edge and the word
 * "proposed" are both required: colour alone would not survive greyscale, and
 * the label is what a screenshot carries.
 */
.kch-btn[data-proposed] {
	background: repeating-linear-gradient(135deg, #F3EEFA 0 10px, #EDE5F7 10px 20px);
	border: 1.5px dashed #7C4DBE;
	color: #3B1E63;
	cursor: pointer;
}
.kch-prop {
	font-size: 10px;
	font-weight: 800;
	letter-spacing: .6px;
	text-transform: uppercase;
	font-style: normal;
	padding: 2px 6px;
	border-radius: 10px;
	background: #7C4DBE;
	color: #FFFFFF;
}

/* ======================================================== 1. TOP NAVIGATION */

/*
 * One row, 56px, and it does not grow. Everything competing for height here is
 * competing with the map, which is the product.
 */
.kch-nav {
	position: relative;
	z-index: 60;
	height: var(--nav-h);
	display: flex;
	align-items: center;
	gap: 18px;
	padding: 0 16px;
	background: var(--surf);
	border-bottom: 1px solid var(--line);
}

.kch-nav__brand {
	display: inline-flex;
	min-height: 44px;
	align-items: center;
	gap: 8px;
	font-size: 16px;
	font-weight: 700;
	letter-spacing: -.02em;
	color: var(--ink);
	text-decoration: none;
	white-space: nowrap;
	flex: none;
}
/*
 * MEASURED 2.717:1 AND FIXED.
 *
 * `--blue` (#00A4FD) is an interface accent, not a text colour on a light
 * surface — the same fact that put "SLATE IS NOT A TEXT COLOUR" at the top of
 * this file. In the brand it was 14px at weight 800 on the white bar, which
 * needs 4.5:1. A brand mark is arguably exempt under 1.4.3; it is fixed anyway,
 * because "the logo is allowed to fail" is a worse sentence than a slightly
 * deeper blue. --blue-t is the same hue, measured against both surfaces.
 */
.kch-nav__brand b { color: var(--blue-t); font-weight: 800; }

/* A small thermal mark rather than a large logo image. */
.kch-nav__mark {
	width: 22px; height: 22px; border-radius: 7px; flex: none;
	background: conic-gradient(from 210deg, var(--dog), var(--cat) 45%, var(--blue) 80%, var(--dog));
	box-shadow: 0 0 0 1px rgba(10, 20, 29, .06), 0 2px 8px rgba(0, 164, 253, .28);
}

.kch-nav__quiet {
	display: flex;
	gap: 20px;
	margin-left: 8px;
	flex: 1 1 auto;
}
.kch-nav__quiet a {
	/* 44px tall like everything else in this bar. These are quiet by COLOUR and
	 * weight, not by being smaller to hit — a secondary link is still a link. */
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	color: var(--dim);
	font-size: 14px;
	font-weight: 500;
	text-decoration: none;
	white-space: nowrap;
}
.kch-nav__quiet a:hover { color: var(--ink); }

.kch-nav__actions { display: flex; gap: 8px; flex: none; margin-left: auto; }

/*
 * THE TWO PRODUCT ACTIONS.
 *
 * 44px minimum in both dimensions at every breakpoint. They are solid, they
 * carry the species-neutral thermal and blue, and they never collapse into a
 * menu — see the note in index.html.
 */
/*
 * TWO LINES, AT EVERY WIDTH, AND THE SECOND LINE IS NOT DECORATION.
 *
 * The action and what it starts are different facts: `Post a Lost Pet` is
 * what the visitor does, `Begin AI Match Scan` is what the platform does
 * next. One line carrying both would be a 34-character label on a 320px bar.
 *
 * ABBREVIATION IS GONE. Below 420px these used to read `Lost` and `Found`,
 * which sat eight pixels from a Lost Pets / Found Pets tab pair and read as
 * two more map filters rather than as the two actions the page exists for.
 */
.kch-post {
	min-height: 44px;
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 2px;
	padding: 7px 15px;
	border: 0;
	border-radius: 9px;
	font: 700 14px/1 var(--font);
	letter-spacing: -.005em;
	cursor: pointer;
	text-align: center;
	white-space: nowrap;
	color: #FFFFFF;
	/* Deliberately does NOT transition transform or box-shadow: the keyframes own
	 * both, and a transition on the same properties fights the animation and
	 * produces a visible stutter at the end of each beat. */
	transition: background .14s ease;
}
.kch-post:hover { filter: brightness(1.07); }
.kch-post:active { transform: translateY(1px); }

.kch-post--missing {
	background: linear-gradient(180deg, var(--dog) 0%, var(--dog-d) 100%);
	box-shadow: 0 1px 2px rgba(160, 45, 0, .3), 0 4px 14px rgba(255, 92, 31, .3);
}
.kch-post--found {
	background: linear-gradient(180deg, #12B3FF 0%, var(--blue-d) 100%);
	box-shadow: 0 1px 2px rgba(0, 62, 100, .3), 0 4px 14px rgba(0, 164, 253, .28);
}

/* Line 1: the action. Uppercased in CSS, not in the DOM - an all-caps text
   node is spelled out letter by letter by some screen readers. */
.kch-post__l1 {
	display: block;
	font-size: 12.5px;
	font-weight: 800;
	letter-spacing: .04em;
	text-transform: uppercase;
	line-height: 1.1;
}

/*
 * Line 2: the AI sublabel. It must stay LEGIBLE on a phone, so it is 11px
 * rather than the 9-10px a sublabel usually gets, and it is at 92% white
 * rather than a tinted grey - measured below with the rest of the button.
 */
.kch-post__l2 {
	display: block;
	font-size: 11px;
	font-weight: 600;
	letter-spacing: .01em;
	line-height: 1.1;
	color: rgba(255, 255, 255, .94);
}

/* ===================================================== THE LIFELINE ANIMATION

   A two-beat heartbeat, a thermal glow, then a narrow metallic shine sweeping
   across the button — and then five and a half seconds of complete stillness
   before it happens again. The rest period is the important part of the design:
   the button should catch the corner of your eye every few seconds, not sit
   there demanding attention.

   WHAT IS ANIMATED, AND WHY ONLY THESE THINGS
   -------------------------------------------
   transform    the beat. Composited; does not reflow, so the navigation either
                side of the button never moves a pixel.
   box-shadow   the glow. Paint, not layout. It is on the BUTTON rather than a
                pseudo-element because `overflow: hidden` (needed to clip the
                shine) would clip a pseudo-element glow — but an element's own
                box-shadow is not clipped by its own overflow.
   opacity +    the shine, on ::after. Both composited.
   transform

   Nothing here touches width, height, margin, padding, top/left or any other
   property that would cause a reflow, so the animation cannot disturb the map's
   render cost. Verified: 400-pet timings unchanged with the animation running.

   HIERARCHY — CHANGED IN 1.9.2, DELIBERATELY, ON INSTRUCTION
   ----------------------------------------------------------
   Through 1.9.1 only Post Lost Pet carried this. The reasoning was sound and is
   worth keeping on the record: motion is the strongest signal in this bar, and
   two animated buttons compete for the same peripheral attention — whichever
   happens to be beating when you glance up becomes the primary action.

   The polish brief asks for movement on BOTH principal conversion buttons, so
   both now have it, and the competition is answered by PHASE rather than by
   silence:

     Post Lost Pet    7.2s loop, no delay
     Post Found Pet   7.2s loop, -3.6s delay — exactly half a cycle

   They can therefore never beat together. While one is in its ~1s active
   window the other is in the middle of its 5.5s rest, so at any instant at
   most one control in this bar is moving. Hierarchy is still carried by size
   (more horizontal room above 761px), by weight, by order, and by the stronger
   of the two glows, which stays on Lost.
*/

@keyframes kch-beat {
	0%    { transform: scale(1); }
	2.2%  { transform: scale(1.035); }
	4.4%  { transform: scale(1); }
	6.9%  { transform: scale(1.028); }
	9.7%  { transform: scale(1); }
	100%  { transform: scale(1); }
}

@keyframes kch-glow-missing {
	0%, 100% { box-shadow: 0 1px 2px rgba(160, 45, 0, .3), 0 4px 14px rgba(255, 92, 31, .3); }
	2.2%     { box-shadow: 0 1px 2px rgba(160, 45, 0, .34), 0 6px 22px rgba(255, 92, 31, .62), 0 0 0 6px rgba(255, 92, 31, .16); }
	4.4%     { box-shadow: 0 1px 2px rgba(160, 45, 0, .3), 0 4px 14px rgba(255, 92, 31, .3); }
	6.9%     { box-shadow: 0 1px 2px rgba(160, 45, 0, .34), 0 6px 20px rgba(255, 92, 31, .5), 0 0 0 5px rgba(255, 92, 31, .12); }
	9.7%     { box-shadow: 0 1px 2px rgba(160, 45, 0, .3), 0 4px 14px rgba(255, 92, 31, .3); }
}

/* The sweep is invisible outside its own window, so the reset never shows. */
@keyframes kch-shine {
	0%,  10.9% { transform: translateX(-160%) skewX(-18deg); opacity: 0; }
	12%        { opacity: 1; }
	21%        { opacity: 1; }
	23%, 100%  { transform: translateX(320%) skewX(-18deg); opacity: 0; }
}

/*
 * THE SHINE PASSES UNDER THE LABEL, NEVER OVER IT.
 *
 * `.kch-post > span { z-index: 2 }` below is not tidiness — it is the reason
 * the brief's "keep button text readable throughout the animation" is a
 * property of the stacking order rather than a hope about opacity. The sweep is
 * a white gradient at up to .78 alpha; painted over a white label it would take
 * the contrast to nothing for about a fifth of a second, twice a cycle.
 * Measured with the sweep frozen at its brightest: the label's ratio does not
 * move, because the label is in front of it.
 */
.kch-post {
	position: relative;
	overflow: hidden;              /* clips the shine; does NOT clip box-shadow */
	isolation: isolate;
}

/* The label must sit above the sweep, or the shine washes over the words. */
.kch-post > span { position: relative; z-index: 2; }

/*
 * Both buttons carry the sweep. Same geometry, same gradient, same 7.2s clock;
 * only the phase differs, and the phase is set on the element below.
 */
.kch-post--missing::after,
.kch-post--found::after {
	content: "";
	position: absolute;
	top: -30%;
	left: 0;
	width: 34%;
	height: 160%;
	z-index: 1;
	pointer-events: none;
	background: linear-gradient(90deg,
		rgba(255, 255, 255, 0) 0%,
		rgba(255, 255, 255, .30) 42%,
		rgba(235, 250, 255, .78) 50%,
		rgba(255, 255, 255, .30) 58%,
		rgba(255, 255, 255, 0) 100%);
	animation: kch-shine 7.2s cubic-bezier(.35, 0, .25, 1) infinite;
	opacity: 0;
}

/*
 * MISSING IS THE DOMINANT ACTION, BY SIZE AS WELL AS BY MOTION.
 *
 * The homepage's primary recurring visitor is somebody whose pet is missing, so
 * that button gets more weight than the one beside it: more horizontal room, a
 * slightly heavier label, and the stronger of the two glows.
 *
 * Applied only from 761px up. Below that the pair is already tight — at 320px
 * they measure 63px and 55px on a 320px bar — and buying dominance with padding
 * there would push the shelter toggle off the control row. On phones the
 * hierarchy is carried by colour, order and the stronger animation instead.
 */
@media (min-width: 761px) {
	.kch-post--missing { padding: 0 20px; font-size: 14.5px; font-weight: 800; }
	.kch-post--found   { padding: 0 14px; }
}

.kch-post--missing {
	animation: kch-beat 7.2s cubic-bezier(.4, 0, .3, 1) infinite,
	           kch-glow-missing 7.2s cubic-bezier(.4, 0, .3, 1) infinite;
	will-change: transform;
}

/*
 * POST FOUND PET — THE SAME SYSTEM, HALF A CYCLE LATER.
 *
 * A negative `animation-delay` of exactly half the duration starts this loop
 * already 3.6s in, so the button is at rest for the whole of the other one's
 * ~1s active window, and vice versa. At any instant at most ONE control in this
 * bar is moving. Nothing schedules that; it is a property of the declaration
 * and cannot drift, which a JavaScript-coordinated version could.
 *
 * The glow is cyan rather than thermal and one step softer than Lost's: both
 * buttons move, but they are still not equals.
 */
@keyframes kch-glow-found {
	0%, 100% { box-shadow: 0 1px 2px rgba(0, 60, 92, .3), 0 4px 14px rgba(0, 164, 253, .26); }
	2.2%     { box-shadow: 0 1px 2px rgba(0, 60, 92, .34), 0 6px 20px rgba(0, 164, 253, .52), 0 0 0 5px rgba(0, 164, 253, .14); }
	4.4%     { box-shadow: 0 1px 2px rgba(0, 60, 92, .3), 0 4px 14px rgba(0, 164, 253, .26); }
	6.9%     { box-shadow: 0 1px 2px rgba(0, 60, 92, .34), 0 6px 18px rgba(0, 164, 253, .42), 0 0 0 4px rgba(0, 164, 253, .1); }
	9.7%     { box-shadow: 0 1px 2px rgba(0, 60, 92, .3), 0 4px 14px rgba(0, 164, 253, .26); }
}

.kch-post--found {
	animation: kch-beat 7.2s cubic-bezier(.4, 0, .3, 1) -3.6s infinite,
	           kch-glow-found 7.2s cubic-bezier(.4, 0, .3, 1) -3.6s infinite;
	will-change: transform;
}

.kch-post--found::after { animation-delay: -3.6s; }

/* ---- interaction: the loop pauses, and a strong static state takes over ---- */

/*
 * Hover and focus PAUSE rather than reset. The cycle sits at rest for roughly
 * 88% of its length, so a pause is almost always invisible — it simply stops
 * anything new from starting while somebody is interacting.
 */
/*
 * `:focus`, not just `:focus-visible`.
 *
 * The pause and the focus RING are two different decisions. The ring is
 * keyboard-only — a mouse click should not leave one behind — but the pause
 * must happen for any focus at all, including the focus a mouse click creates.
 * With `:focus-visible` alone, clicking the button paused it for as long as the
 * press lasted and then let it start beating again underneath the pointer.
 */
.kch-post:hover,
.kch-post:focus,
.kch-post:hover::after,
.kch-post:focus::after {
	animation-play-state: paused;
}

/*
 * Press is the exception: it STOPS the animation outright, because a paused
 * keyframe still wins over the `:active` transform and would swallow the press
 * feedback. Tactile response matters more than continuity for the 150ms
 * somebody is holding the button down.
 */
.kch-post:active,
.kch-post:active::after { animation: none; }

.kch-post--missing:hover {
	filter: none;
	background: linear-gradient(180deg, #FF6D33 0%, #F5450F 100%);
	box-shadow: 0 2px 4px rgba(160, 45, 0, .34), 0 8px 26px rgba(255, 92, 31, .55),
	            0 0 0 4px rgba(255, 92, 31, .18);
}

.kch-post--found:hover {
	filter: none;
	background: linear-gradient(180deg, #2ABBFF 0%, #0A76B8 100%);
	box-shadow: 0 2px 4px rgba(0, 62, 100, .34), 0 8px 24px rgba(0, 164, 253, .5),
	            0 0 0 4px rgba(0, 164, 253, .18);
}

/*
 * THE FOCUS RING IS AN `outline`, NOT A `box-shadow`. THIS IS LOAD-BEARING.
 *
 * It was written as a box-shadow, and it never appeared. The glow keyframes
 * animate box-shadow, and a running animation wins over a plain declaration —
 * including while it is PAUSED, which is exactly the state focus puts it in. So
 * the focus ring was silently suppressed by the animation on the very element
 * that most needed one. Caught on the capture sheet in anim.html, which is what
 * that sheet is for.
 *
 * `outline` is animated by nothing here, so it cannot be overridden. It is also
 * the property browsers reserve for this job.
 *
 * Dark navy with a 2px offset reads clearly against the white navigation bar
 * AND against both the thermal and the blue button; a white ring would have
 * disappeared into the bar itself.
 */
.kch-post:focus-visible {
	outline: 3px solid var(--ink);
	outline-offset: 2px;
	border-radius: 9px;
}

/* ---------------------------------------------------- reduced motion ------- */

/*
 * No pulse, no sweep, no luminance change of any kind — the ::after is removed
 * outright rather than merely stopped, so there is no chance of a stray frame.
 *
 * The buttons keep a STATIC glow that is slightly stronger than their resting
 * animated state, so Post Missing Pet still reads as the primary action. The
 * requirement was to remove the motion, not the emphasis.
 */
@media (prefers-reduced-motion: reduce) {
	.kch-post {
		animation: none !important;
		transition: none;
		will-change: auto;
	}
	/* Removed outright rather than merely stopped, so no stray frame can paint. */
	.kch-post--missing::after,
	.kch-post--found::after { display: none; }

	/*
	 * Post Lost Pet keeps a STRONGER static glow than its animated resting
	 * state, so it still reads as primary with the motion gone. Removing the
	 * animation must not cost it the hierarchy.
	 */
	.kch-post--missing {
		box-shadow: 0 1px 2px rgba(160, 45, 0, .34), 0 6px 20px rgba(255, 92, 31, .5),
		            0 0 0 4px rgba(255, 92, 31, .15);
	}
	/* Post Found Pet takes the softer static glow, so the pair keeps the same
	   relationship it has when both are moving. */
	.kch-post--found {
		box-shadow: 0 1px 2px rgba(0, 62, 100, .32), 0 5px 16px rgba(0, 164, 253, .38),
		            0 0 0 3px rgba(0, 164, 253, .12);
	}
}

/* ==================================================== 2. PRODUCT STATEMENT */

/*
 * A line, not a hero. No paragraph, no CTA block and no decorative space
 * between it and the map — the whole point is that the map starts immediately.
 */
.kch-statement {
	height: var(--stmt-h);
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 0 16px;
	margin: 0;
	background: var(--band);
	color: var(--band-t);
	font-size: 15px;
	font-weight: 600;
	line-height: 1.2;
	text-align: center;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
/*
 * The single flex item. `white-space` inherits from the band, so the mobile
 * override that allows wrapping still reaches here — but the truncation has to
 * live on the element that actually holds the text, not on the flex container.
 */
.kch-statement__t {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}

.kch-statement b {
	background: linear-gradient(96deg, var(--blue), #7FD4FF);
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
	font-weight: 800;
}

/* ======================================================== 3. THE HERO MAP */

.kch-hero {
	display: block;
	padding: 0;
	background: var(--page);
}

.kch-mapcard {
	position: relative;
	display: flex;
	flex-direction: column;
	/*
	 * The map fills what is left of the first screen after the two
	 * fixed-height rows above it. Nothing else is allowed into the first
	 * viewport.
	 */
	height: calc(100vh - var(--nav-h) - var(--stmt-h) + var(--map-grow));
	min-height: 480px;
	background: var(--surf);
	overflow: hidden;
}

/* --------------------------------------------------------- map top controls */

.kch-mapbar {
	position: relative;
	z-index: 30;
	display: flex;
	align-items: center;
	gap: 9px;
	padding: 6px 10px;
	background: var(--surf);
	border-bottom: 1px solid var(--line);
	flex: none;
}

.kch-tabs { display: flex; gap: 4px; flex: none; }

/*
 * Browse tabs are visually QUIET compared with the nav's posting buttons. A
 * reader must never have to work out whether "Lost Pets" means "look at lost
 * pets" or "report mine". Underline vs solid is that distinction.
 */
.kch-tab {
	min-height: 44px;
	padding: 0 14px;
	border: 0;
	background: transparent;
	border-bottom: 3px solid transparent;
	color: var(--dim);
	font: 600 14.5px/1 var(--font);
	cursor: pointer;
	white-space: nowrap;
	display: inline-flex;
	align-items: center;
	gap: 7px;
}
.kch-tab:hover { color: var(--ink); }
.kch-tab.is-on { color: var(--ink); border-bottom-color: var(--dog); font-weight: 700; }
#tab-found.is-on { border-bottom-color: var(--blue); }

.kch-tab__n {
	display: inline-block;
	min-width: 22px;
	padding: 2px 6px;
	border-radius: 20px;
	background: #EDF1F5;
	color: var(--dim);
	font-size: 11.5px;
	font-weight: 800;
	text-align: center;
	font-variant-numeric: tabular-nums;
}
.kch-tab.is-on .kch-tab__n { background: var(--solid); color: var(--solid-t); }

.kch-maptools {
	display: flex;
	align-items: center;
	gap: 8px;
	margin-left: auto;
	flex: 1 1 auto;
	justify-content: flex-end;
	min-width: 0;
}

.kch-search {
	display: flex;
	align-items: center;
	height: 46px;
	max-width: 240px;
	flex: 0 1 240px;
	min-width: 0;
	border: 1px solid var(--line);
	border-radius: 9px;
	background: var(--surf);
	overflow: hidden;
}
.kch-search:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(0, 164, 253, .16); }

.kch-search__in {
	flex: 1 1 auto;
	min-width: 0;
	height: 100%;
	padding: 0 10px;
	border: 0;
	background: transparent;
	/*
	 * 16px, NOT 14px, AND IT IS NOT A TYPE DECISION.
	 *
	 * Mobile Safari ZOOMS THE WHOLE PAGE when a text field under 16px takes
	 * focus, and it does not zoom back out. Somebody searching a city on a
	 * phone was left on a magnified, horizontally-scrolling page with the map
	 * half off screen. The brief's floor and the platform's threshold are the
	 * same number, which is not a coincidence.
	 */
	font: 400 16px/1 var(--font);
	color: var(--ink);
}
.kch-search__in:focus { outline: none; }

.kch-search__go {
	width: 44px; height: 44px;
	flex: none;
	border: 0;
	background: transparent;
	color: var(--dim);
	cursor: pointer;
	display: grid;
	place-items: center;
}
.kch-search__go svg { width: 18px; height: 18px; }
.kch-search__go:hover { color: var(--blue); }

.kch-tool {
	min-height: 44px;
	min-width: 44px;
	justify-content: center;
	display: inline-flex;
	align-items: center;
	gap: 7px;
	padding: 0 12px;
	border: 1px solid var(--line);
	border-radius: 9px;
	background: var(--surf);
	color: var(--ink);
	font: 600 13.5px/1 var(--font);
	cursor: pointer;
	white-space: nowrap;
}
.kch-tool svg { width: 17px; height: 17px; flex: none; color: var(--blue); }
.kch-tool:hover { border-color: var(--blue); }
.kch-tool.is-on {
	background: rgba(0, 164, 253, .1);
	border-color: var(--blue);
	color: #05384F;
}

.kch-seg {
	display: inline-flex;
	height: 44px;
	border: 1px solid var(--line);
	border-radius: 9px;
	overflow: hidden;
	flex: none;
}
.kch-seg__b {
	min-height: 44px;
	padding: 0 12px;
	border: 0;
	background: var(--surf);
	color: var(--dim);
	font: 600 13.5px/1 var(--font);
	cursor: pointer;
	white-space: nowrap;
}
.kch-seg__b.is-on { background: var(--solid); color: var(--solid-t); }

/* --------------------------------------------------------------- the map */

.kch-mapwrap {
	position: relative;
	flex: 1 1 auto;
	min-height: 0;
}

.kch-map { position: absolute; inset: 0; background: var(--surf-2); }

/* The basemap is a raster picture; only its PANE is filtered. See --tiles-filter. */
.kch-map .leaflet-tile-pane { filter: var(--tiles-filter); }

/* The Leaflet attribution must stay legible and must not be covered. */
.kch-map .leaflet-control-attribution { font-size: 10.5px; }

/* The layer strip is a sibling of the Leaflet wrapper and visually overlaps
 * its lower edge. Lift Leaflet's own bottom controls and the map-use hint so
 * attribution and guidance remain readable and clickable above that strip. */
.kch-mapcard.has-map-layers .leaflet-bottom { bottom: 72px; }
.kch-mapcard.has-map-layers .kch-hint { bottom: 92px; }

/*
 * The hint sits on the RIGHT.
 *
 * On the left it was covered by the selected panel, which docks to the left
 * edge — and the hint is at its most useful exactly then, because it is what
 * explains why only the selected pet is showing rings. Two elements competing
 * for one corner is not something a z-index fixes.
 */
.kch-hint {
	position: absolute;
	right: 12px; bottom: 26px;
	z-index: 500;
	max-width: min(420px, calc(100% - 24px));
	margin: 0;
	padding: 8px 11px;
	border-radius: 8px;
	background: rgba(10, 20, 29, .9);
	color: #EAF2F8;
	font-size: 12.5px;
	line-height: 1.4;
	box-shadow: var(--shadow);
}

/* ------------------------------------------------------------ hover card */

/*
 * STABILITY IS THE WHOLE REQUIREMENT.
 *
 * The card is positioned by JS and given `pointer-events:auto` so the pointer
 * can travel into it. Two things stop it flickering: a grace period before it
 * closes (home.js), and the fact that it is NEVER placed under the pointer —
 * it is offset clear of the marker so entering the card cannot mean leaving
 * the pin and re-entering it in a loop.
 *
 * No transition on `left`/`top`. Animating position would mean the card chases
 * the pointer between pins, which reads as flicker even though it is not.
 */
.kch-hover {
	position: absolute;
	z-index: 700;
	width: 264px;
	background: var(--surf);
	border-radius: var(--r);
	box-shadow: var(--shadow-l);
	border: 1px solid var(--line);
	overflow: hidden;
	pointer-events: auto;
	animation: kch-fade .12s ease-out;
}

@keyframes kch-fade { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
	.kch-hover { animation: none; }
}

.kch-hover__img {
	display: block;
	width: 100%;
	height: 158px;
	object-fit: cover;
	background: #E8EEF2;
}
.kch-hover__none {
	height: 158px;
	display: grid;
	place-items: center;
	background: #EDF1F5;
	color: var(--dim);
	font-size: 13px;
}

.kch-hover__b { padding: 11px 13px 13px; }

.kch-hover__tags { display: flex; gap: 6px; margin-bottom: 7px; flex-wrap: wrap; }

.kch-hover__name {
	margin: 0 0 3px;
	font-size: 18px;
	font-weight: 800;
	line-height: 1.2;
}
.kch-hover__meta { margin: 0 0 3px; font-size: 13px; color: var(--dim); }
.kch-hover__days { margin: 0 0 10px; font-size: 12.5px; color: var(--dim); font-weight: 600; }

.kch-hover__go {
	display: block;
	min-height: 44px;
	padding: 13px 11px;
	border-radius: 8px;
	background: var(--solid);
	color: var(--solid-t);
	font-weight: 700;
	font-size: 13.5px;
	text-align: center;
	text-decoration: none;
}
.kch-hover__go:hover { background: #16293A; }

/* Status / species tags, shared by the hover card, the panel and the rails. */
.kch-tag {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	padding: 3px 8px;
	border-radius: 20px;
	font-size: 11px;
	font-weight: 800;
	letter-spacing: .4px;
	text-transform: uppercase;
	white-space: nowrap;
}
/*
 * Status is never colour-only anywhere: the word is always in the tag, and the
 * Found tag additionally carries a distinct outline treatment so the two are
 * separable in greyscale.
 */
.kch-tag--lost  { background: #FFEDE4; color: #8A2E00; }
.kch-tag--found { background: #E4F4FF; color: #05496B; border: 1.5px dashed #0B7FC4; }
.kch-tag--cat   { background: #F5E9FF; color: #5B1E8C; }
.kch-tag--dog   { background: #FFEDE4; color: #8A2E00; }
.kch-tag--other { background: #EEF1F4; color: #35485A; }

/* --------------------------------------------------------- selected panel */

.kch-panel {
	position: absolute;
	left: 12px; top: 12px; bottom: 12px;
	z-index: 800;
	width: 330px;
	max-width: calc(100% - 24px);
	display: flex;
	flex-direction: column;
	background: var(--surf);
	border-radius: var(--r);
	box-shadow: var(--shadow-l);
	border: 1px solid var(--line);
	overflow: hidden;
}

.kch-panel__x {
	position: absolute;
	right: 8px; top: 8px;
	z-index: 2;
	width: 44px; height: 44px;
	border: 0;
	border-radius: 50%;
	background: rgba(10, 20, 29, .62);
	color: #FFFFFF;
	cursor: pointer;
	display: grid;
	place-items: center;
	backdrop-filter: blur(3px);
}
.kch-panel__x svg { width: 17px; height: 17px; }
.kch-panel__x:hover { background: rgba(10, 20, 29, .82); }

.kch-panel__scroll { overflow-y: auto; overscroll-behavior: contain; }

/*
 * The close button floats over the panel's top-right corner. On a pet card
 * that lands harmlessly on the photograph; on a shelter card it landed on the
 * DEMONSTRATION DATA banner and clipped the end of the sentence — the one
 * piece of text on that card that must never be partially readable.
 */
.kch-panel .kcs-card > *:first-child { padding-right: 46px; }

.kch-pcard__img { display: block; width: 100%; height: 178px; object-fit: cover; background: var(--surf-2); }
.kch-pcard__none {
	height: 112px; display: grid; place-items: center;
	background: #EDF1F5; color: var(--dim); font-size: 13.5px;
}
.kch-pcard__b { padding: 11px 13px 13px; }
.kch-pcard__tags { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
.kch-pcard__name { margin: 0 0 7px; font-size: 21px; font-weight: 800; line-height: 1.15; }

.kch-pcard__meta { margin: 0 0 9px; display: grid; gap: 4px; }
.kch-pcard__row { display: flex; gap: 10px; font-size: 13.5px; }
.kch-pcard__row dt { flex: none; width: 104px; color: var(--dim); margin: 0; }
.kch-pcard__row dd { margin: 0; font-weight: 600; }

.kch-pcard__zone {
	margin: 0 0 9px; padding: 8px 10px;
	border-radius: 8px;
	background: #FFF4EE;
	border-left: 3px dashed var(--dog);
	font-size: 12.5px; color: #7A2A00; line-height: 1.45;
}
.kch-pcard__priv {
	margin: 0 0 13px;
	font-size: 12px; color: var(--dim); line-height: 1.45;
}

.kch-acts { display: grid; gap: 6px; }

.kch-btn {
	min-height: 44px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 7px;
	padding: 11px 14px;
	border: 1px solid transparent;
	border-radius: 9px;
	font: 700 14px/1.25 var(--font);
	text-align: center;
	text-decoration: none;
	cursor: pointer;
	width: 100%;
}
.kch-btn--primary { background: var(--solid); color: var(--solid-t); }
.kch-btn--primary:hover { background: #16293A; }
.kch-btn--saw { background: var(--blue); color: #04121C; }
.kch-btn--saw:hover { background: #33B8FE; }
.kch-btn--found { background: var(--dog); color: #FFFFFF; }
.kch-btn--found:hover { background: #FF7038; }
.kch-btn--ghost { background: var(--surf); border-color: var(--line); color: var(--ink); }
.kch-btn--ghost:hover { border-color: var(--blue); }

/*
 * A DISABLED ACTION LOOKS DISABLED AND SAYS WHY.
 *
 * The brief forbids buttons that open mockups. Where a backend does not exist
 * the control is rendered as an inert, greyed, aria-disabled element with the
 * reason printed under it — not hidden, because hiding it would misrepresent
 * what the finished page will offer, and not live, because pressing it could
 * not do anything.
 */
.kch-btn[aria-disabled="true"] {
	background: #EDF1F5;
	color: #55677A;
	border-color: var(--line);
	cursor: not-allowed;
	box-shadow: none;
}
.kch-nb {
	margin: -2px 0 4px;
	font-size: 11.5px;
	line-height: 1.4;
	color: var(--dim);
}

.kch-back {
	background: none; border: 0; padding: 8px 0; margin-bottom: 4px;
	color: var(--blue-d); font: 600 13.5px/1 var(--font); cursor: pointer;
	min-height: 44px; display: inline-flex; align-items: center;
}

.kch-flow h3 { margin: 0 0 8px; font-size: 18px; font-weight: 800; }
.kch-flow p { margin: 0 0 11px; font-size: 13.5px; line-height: 1.5; color: var(--dim); }
.kch-flow__q { color: var(--ink) !important; font-weight: 700; font-size: 15px !important; }
.kch-flow__warn {
	padding: 9px 11px; border-radius: 8px;
	background: #FFF4EE; border-left: 3px solid var(--dog);
	color: #7A2A00 !important; font-size: 12.5px !important;
}
.kch-flow__row { display: grid; gap: 7px; margin-bottom: 11px; }

/* --------------------------------------------------------------- empty */

.kch-empty {
	position: absolute;
	left: 50%; top: 50%;
	transform: translate(-50%, -50%);
	z-index: 600;
	width: min(340px, calc(100% - 32px));
	padding: 20px;
	border-radius: var(--r);
	background: var(--surf);
	box-shadow: var(--shadow-l);
	text-align: center;
}
.kch-empty__h { margin: 0 0 5px; font-weight: 800; font-size: 16px; }
.kch-empty__s { margin: 0 0 14px; font-size: 13.5px; color: var(--dim); }

/* ==================================================== 5. LIVE METRO STATS */

/*
 * Docked to the bottom edge of the map card, inside the first screen, covering
 * no part of the map. Scrolls horizontally on narrow screens rather than
 * wrapping to a second row, because a second row would push the map up.
 */
.kch-stats {
	flex: none;
	display: flex;
	gap: 0;
	padding: 0;
	background: var(--band);
	border-top: 1px solid rgba(255, 255, 255, .08);
	overflow-x: auto;
	overscroll-behavior-x: contain;
	scrollbar-width: thin;
}

.kch-stat {
	flex: 1 0 auto;
	min-width: 132px;
	padding: 7px 12px 8px;
	border-right: 1px solid rgba(255, 255, 255, .08);
	color: #EAF2F8;
	position: relative;
}
.kch-stat:last-child { border-right: 0; }

.kch-stat__n {
	display: block;
	font-size: 22px;
	font-weight: 800;
	line-height: 1.1;
	font-variant-numeric: tabular-nums;
	letter-spacing: -.02em;
}
.kch-stat__l {
	display: block;
	font-size: 10.5px;
	font-weight: 700;
	letter-spacing: .5px;
	text-transform: uppercase;
	color: #93A9BB;
	margin-top: 2px;
	white-space: nowrap;
}
.kch-stat__s {
	display: block;
	font-size: 10px;
	color: #7C93A8;
	margin-top: 2px;
	white-space: nowrap;
}

.kch-stat--cat .kch-stat__n { color: #C88BFF; }
.kch-stat--dog .kch-stat__n { color: #FF8A5C; }
.kch-stat--found .kch-stat__n { color: #5CC8FF; }

/* A metric with no publishable data shows a dash, never a zero. */
.kch-stat--na .kch-stat__n { color: #7C93A8; }

/* ==================================================== 6. FILTERS BELOW MAP */

.kch-filters {
	background: var(--surf);
	border-bottom: 1px solid var(--line);
	padding: 12px 14px 14px;
}

.kch-filters__head {
	display: flex;
	align-items: center;
	gap: 14px;
	max-width: 1400px;
	margin: 0 auto 12px;
}
.kch-filters__h { font-size: 17px; font-weight: 800; }
.kch-filters__n { margin: 0; font-size: 14px; color: var(--dim); }
.kch-filters__n b { color: var(--ink); font-size: 17px; font-variant-numeric: tabular-nums; }

/* The toggle is the mobile affordance; desktop shows the body unconditionally. */
.kch-filters__toggle { display: none; }

.kch-filters__body {
	display: flex;
	flex-wrap: wrap;
	gap: 10px 14px;
	align-items: flex-end;
	max-width: 1400px;
	margin: 0 auto;
}

/*
 * The relocation slot. Empty on desktop, so it must occupy no space at all —
 * `:empty` rather than a breakpoint, because the breakpoint that fills it and
 * the breakpoint that styles it must never be able to disagree.
 */
.kch-filters__slot {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
	max-width: 1400px;
	margin: 0 auto 12px;
}
.kch-filters__slot:empty { display: none; }
.kch-filters__slot .kch-search { flex: 1 1 100%; max-width: none; }
.kch-filters__slot .kch-seg { flex: 1 1 100%; }
.kch-filters__slot .kch-seg__b { flex: 1 1 0; }

.kch-f { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.kch-f__l {
	font-size: 11px; font-weight: 800; letter-spacing: .5px;
	text-transform: uppercase; color: var(--dim);
}

.kch-sel {
	min-height: 44px;
	padding: 0 32px 0 11px;
	border: 1px solid var(--line);
	border-radius: 9px;
	background: var(--surf) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23808F9E' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 11px center/11px;
	font: 500 16px/1 var(--font);   /* see the note on .kch-search__in */
	color: var(--ink);
	appearance: none;
	cursor: pointer;
	max-width: 100%;
}

.kch-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.kch-chip {
	min-height: 44px;
	padding: 0 14px;
	border: 1px solid var(--line);
	border-radius: 22px;
	background: var(--surf);
	color: var(--ink);
	font: 600 13.5px/1 var(--font);
	cursor: pointer;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	white-space: nowrap;
}
.kch-chip:hover { border-color: var(--blue); }
.kch-chip.is-on { background: var(--solid); border-color: var(--solid); color: var(--solid-t); }
/* Species chips carry a colour dot as well as their label — never colour alone. */
.kch-chip i { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.kch-chip--cat i { background: var(--cat); }
.kch-chip--dog i { background: var(--dog); }
.kch-chip--other i { background: var(--slate); }

.kch-f__reset { margin-left: auto; }

/* ======================================= 7. THE FEATURED TILES (elsewhere) */

/*
 * The two horizontal carousels that lived here are gone, and so are their
 * styles: .kch-rail, .kch-track, .kch-pet and everything under them, about 130
 * lines. They were replaced by the three featured tiles, whose styles are in
 * assets/pettiles/kcai-pettiles.css because the component is reusable and its
 * appearance must travel with it rather than live in one page's stylesheet.
 *
 * The rules were REMOVED rather than left in place. Dead CSS for markup that
 * cannot occur is not free: the next person to read this file has no way to
 * tell it from live styling, and .kch-pet in particular would have gone on
 * looking like the card treatment somebody should match.
 *
 * .kch-tag stays — the hover card and the selected panel still use it.
 */

/* ================================================ 9. BELOW-THE-FOLD SECTIONS */

.kch-below { background: var(--page); padding: 8px 0 0; }

.kch-sec {
	max-width: 1000px;
	margin: 0 auto;
	padding: 26px 16px;
	border-bottom: 1px solid var(--line);
}
.kch-sec:last-child { border-bottom: 0; }
.kch-sec h2 { font-size: 24px; font-weight: 800; margin-bottom: 14px; letter-spacing: -.025em; }
.kch-sec h3 { font-size: 16px; font-weight: 800; margin-bottom: 4px; }
.kch-sec p { margin: 0 0 12px; font-size: 15px; line-height: 1.6; }
.kch-sec__note {
	font-size: 13.5px !important;
	color: var(--dim);
	padding-left: 12px;
	border-left: 3px solid var(--blue);
}

.kch-steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 14px; }
.kch-step { padding: 14px; background: var(--surf); border-radius: var(--r); border: 1px solid var(--line); }
.kch-step b {
	display: grid; place-items: center;
	width: 28px; height: 28px; margin-bottom: 9px;
	border-radius: 8px;
	background: var(--solid); color: var(--solid-t);
	font-size: 13px; font-weight: 800;
}
.kch-step p { font-size: 13.5px; margin: 0; color: var(--dim); }

.kch-two { display: grid; grid-template-columns: 1.1fr 1fr; gap: 22px; }

.kch-checks { margin: 0; padding: 0; list-style: none; display: grid; gap: 8px; }
.kch-checks li {
	position: relative;
	padding-left: 26px;
	font-size: 14px;
	line-height: 1.5;
}
.kch-checks li::before {
	content: "";
	position: absolute; left: 0; top: 5px;
	width: 15px; height: 15px;
	border-radius: 4px;
	background: var(--blue);
	/* A tick drawn in the mask, so it needs no font and no image request. */
	-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3.2L13 5' stroke='%23000' stroke-width='2.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/13px no-repeat, linear-gradient(#000, #000);
	-webkit-mask-composite: xor;
	mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3.2L13 5' stroke='%23000' stroke-width='2.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/13px no-repeat, linear-gradient(#000, #000);
	mask-composite: exclude;
}

.kch-states { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 14px; }
.kch-state { padding: 13px 15px; border-radius: var(--r); background: var(--surf); border-left: 4px solid var(--slate); }
.kch-state b { display: block; font-size: 14px; font-weight: 800; margin-bottom: 3px; }
.kch-state span { font-size: 12.5px; color: var(--dim); line-height: 1.45; }
.kch-state--live    { border-left-color: #12A05C; }
.kch-state--partner { border-left-color: var(--blue); }
.kch-state--none    { border-left-style: dashed; }

.kch-placeholder p {
	padding: 16px 18px;
	border-radius: var(--r);
	background: var(--surf);
	border: 1px dashed var(--slate);
	color: var(--dim);
	font-size: 14px;
	margin: 0;
}

.kch-links { margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 10px 22px; }
.kch-links a {
	display: inline-flex; align-items: center; min-height: 44px;
	color: var(--blue-t); font-weight: 600; font-size: 14.5px;
}

.kch-foot {
	background: var(--band);
	color: #93A9BB;
	padding: 22px 16px 28px;
	text-align: center;
}
.kch-foot p { margin: 0 0 5px; font-size: 13.5px; }
.kch-foot b { color: var(--white); }
.kch-foot__prev {
	font-size: 11px !important;
	letter-spacing: .7px;
	text-transform: uppercase;
	color: #FF8A5C !important;
	font-weight: 700;
}

.kch-scrim {
	position: fixed; inset: 0; z-index: 790;
	background: rgba(4, 12, 20, .5);
}

/* ============================================================= RESPONSIVE */

@media (max-width: 1100px) {
	.kch-steps { grid-template-columns: repeat(2, 1fr); }
	.kch-states { grid-template-columns: 1fr; }
	.kch-two { grid-template-columns: 1fr; }
}

@media (max-width: 900px) {
	.kch-nav__quiet { display: none; }
	.kch-tool--shelter .kch-tool__t { display: none; }
	.kch-tool--shelter { padding: 0 11px; }
}

/*
 * TABLET AND PHONE — the control bar becomes two rows.
 *
 * Cramming tabs + search + location + view toggle onto one 768px row makes
 * every one of them too small to hit. Two rows costs ~48px of map and keeps
 * every control at 40px+.
 */
@media (max-width: 860px) {
	.kch-mapbar { flex-wrap: wrap; gap: 8px; }
	.kch-tabs { order: 1; flex: 1 1 100%; }
	.kch-maptools { order: 2; flex: 1 1 100%; justify-content: flex-start; }
	.kch-search { flex: 1 1 auto; max-width: none; }
	.kch-mapcard { height: calc(100vh - var(--nav-h) - var(--stmt-h) + var(--map-grow)); }
	.kch-panel { width: 100%; max-width: none; left: 0; right: 0; top: auto; bottom: 0; border-radius: var(--r) var(--r) 0 0; max-height: 78%; }
}

@media (max-width: 760px) {
	:root { --stmt-h: 40px; }

	.kch-statement { font-size: 13px; white-space: normal; line-height: 1.25; }

	/* The abbreviation is gone; see the note on .kch-post. Both lines show at
	   every width, and below 561px the pair stacks full width. */
	.kch-post { padding: 7px 14px; min-height: 44px; }

	/* The tool labels go; the icons and the 40px targets stay. */
	.kch-tool .kch-tool__t { display: none; }
	.kch-tool { padding: 0 12px; }

	.kch-filters__toggle {
		display: inline-flex;
		align-items: center;
		min-height: 44px;
		margin-left: auto;
		padding: 0 16px;
		border: 1px solid var(--line);
		border-radius: 9px;
		background: var(--surf);
		font: 700 14px/1 var(--font);
		color: var(--ink);
		cursor: pointer;
	}
	/* Collapsed by default on phones; the toggle is the only way in. */
	.kch-filters__body { display: none; }
	.kch-filters.is-open .kch-filters__body { display: flex; }
	.kch-f { flex: 1 1 100%; }
	.kch-sel { width: 100%; }
	.kch-f__reset { margin-left: 0; width: 100%; }

	.kch-steps { grid-template-columns: 1fr; }
	.kch-sec h2 { font-size: 21px; }
}

/*
 * 430px AND BELOW.
 *
 * The hover card is not merely hidden here — home.js never opens it, because a
 * touch device has no hover and the first tap must open the full pet card
 * instead. The CSS rule is a belt-and-braces guard, not the mechanism.
 */
@media (max-width: 560px) {
	.kch-hover { display: none !important; }
	.kch-hint { font-size: 11px; padding: 6px 9px; max-width: min(300px, calc(100% - 20px)); bottom: 22px; }
	.kch-stat { min-width: 112px; padding: 6px 11px 7px; }
	.kch-stat__n { font-size: 19px; }

	/*
	 * BACK TO ONE CONTROL ROW.
	 *
	 * The 860px rule forces tabs and tools onto separate rows because at that
	 * width they genuinely do not fit together. Below 560 the search box and
	 * the pin toggle have MOVED OUT to the filter slot, so what is left —
	 * two tabs, the location button and the shelter toggle — fits on one row
	 * again. Undoing the wrap here recovers about 50px of map on exactly the
	 * screens with the least of it to spare.
	 */
	.kch-mapbar { flex-wrap: nowrap; }
	.kch-tabs { flex: 0 1 auto; }
	.kch-maptools { flex: 0 0 auto; gap: 6px; }
}

/*
 * SMALL PHONES — every pixel of chrome is a pixel taken from the map.
 *
 * At 320×568 the fixed bands (nav 60 + statement 40 + control bar 111 +
 * stats 84 = 295) took 52% of the viewport and left the map a minority of its
 * own first screen. Nothing here is removed — both posting actions, both
 * browse tabs, the search, the location button, the view toggle and all five
 * statistics survive at full 44px targets. They are only made dense: shorter
 * statement, tighter control rows, and a stats strip that drops its
 * explanatory sub-line, which is the one element carrying information also
 * available in the tile's tooltip and in the sections below the fold.
 */
@media (max-width: 400px) {
	:root { --stmt-h: 34px; }

	.kch-nav { gap: 8px; padding: 0 10px; }
	.kch-nav__brand { font-size: 14px; }
	/*
	 * min-width, because the bar now carries a third control.
	 *
	 * The theme switch took the slack the two posting actions used to share,
	 * and flex duly squeezed Post Lost Pet to 42px wide at 320 - a 44px-tall
	 * target two pixels too narrow. The floor is stated rather than left to
	 * whatever room happens to remain.
	 */
	.kch-post { padding: 7px 10px; min-width: 44px; }
	.kch-post__l1 { font-size: 12px; letter-spacing: .03em; }
	.kch-post__l2 { font-size: 10.5px; }

	/* Icon only, and the pseudo-element keeps the 44px target. */
	.kch-theme { padding: 0 6px; }
	.kch-theme__sw { width: 30px; height: 18px; }
	.kch-theme__sw i { width: 14px; height: 14px; }
	.kch-theme[aria-pressed="true"] .kch-theme__sw i { transform: translateX(12px); }

	/*
	 * The product statement is fixed wording and must not be abbreviated, so at
	 * this width it wraps to two lines instead. 11px at 1.15 puts two lines in
	 * 26px, inside the 34px band — at 12px it needed three lines and the third
	 * was clipped by the band's own overflow, which read as a rendering fault.
	 */
	.kch-statement {
		font-size: 11px;
		line-height: 1.15;
		white-space: normal;
		padding: 0 8px;
	}

	.kch-mapbar { padding: 4px 8px; gap: 6px; }

	/*
	 * The tabs give up their fixed width so the location and shelter controls
	 * keep theirs. Before this the Shelters button was pushed past the right
	 * edge and clipped by `overflow-x: hidden` — present in the DOM, 44px, and
	 * completely unreachable.
	 */
	.kch-tabs { min-width: 0; flex-shrink: 1; }
	.kch-tab { padding: 0 7px; font-size: 13px; gap: 5px; flex: 0 1 auto; min-width: 0; }
	.kch-tab__n { min-width: 18px; padding: 2px 4px; font-size: 11px; }
	.kch-maptools { gap: 5px; }

	.kch-stat { min-width: 100px; padding: 7px 11px 8px; }
	.kch-stat__n { font-size: 17px; }
	.kch-stat__l { font-size: 9.5px; letter-spacing: .4px; }
	.kch-stat__s { display: none; }

	.kch-sec, .kch-filters { padding-left: 12px; padding-right: 12px; }
}

/*
 * 320px — the narrowest supported width.
 *
 * Both posting actions still fit on the nav row beside the brand. That is the
 * constraint that sets every other size in this block.
 */
@media (max-width: 340px) {
	.kch-nav { gap: 6px; padding: 0 8px; }
	.kch-nav__brand { font-size: 0; gap: 0; min-width: 44px; justify-content: center; }
	.kch-nav__brand .kch-nav__mark { font-size: 0; }
	.kch-post { padding: 0 9px; font-size: 12.5px; }
	.kch-seg__b { padding: 0 9px; font-size: 12.5px; }
}

/* ================================================================== PRINT */

@media print {
	.kch-nav__actions, .kch-maptools, .kch-panel, .kch-hover { display: none; }
}

/* ==========================================================================
   PRODUCTION ONLY - THE PAYLOAD FAILED TO LOAD (1.8.0)
   ==========================================================================
   The preview could not need this: its data came off the loopback server that
   was serving the page, so if the page loaded, the data loaded.

   On production the map fetches the live API, and a fetch can fail. A map with
   no pets and no explanation reads as "there are no missing pets in Kansas
   City", which is the worst wrong answer this page can give - so the failure
   states itself, in the middle of the map, over the basemap that did load.

   role="alert" on the element, and a colour pair measured rather than picked:
   #8A2E00 on #FFEDE4 is 7.9:1. */
.kch-loadfail{position:absolute;z-index:500;left:50%;top:50%;transform:translate(-50%,-50%);
	max-width:min(30rem,86%);box-sizing:border-box;padding:16px 18px;border-radius:12px;
	background:#FFEDE4;border:1px solid #8A2E00;color:#8A2E00;text-align:center;
	box-shadow:0 8px 28px rgba(0,0,0,.28)}
.kch-loadfail p{margin:0 0 8px;font-size:.95rem;line-height:1.45;color:#8A2E00}
.kch-loadfail p:last-child{margin-bottom:0}
.kch-loadfail a{color:#8A2E00;font-weight:700;text-decoration:underline}

/* ==========================================================================
   PRODUCTION ONLY - ESCAPING THE BLOCK THEME (1.8.0)
   ==========================================================================
   The preview was a bare document and owned <body> outright. On WordPress this
   markup lands inside Twenty Twenty-Five's `.is-layout-constrained`, which caps
   every direct child at the theme's content-size of about 620px - so without
   these rules the map hero would render in a narrow column down the middle of
   a wide page, and the 63-74% first-screen measurement the design was approved
   on would be meaningless.

   Same escape kcai-ui.css already uses for `.kcai-page`, applied to the five
   top-level elements this page contributes. Scoped to `body.kcai-home` so it
   can never reach another template.

   `--wp--style--root--padding-*` is zeroed on the homepage only: the theme's
   root padding would inset the full-bleed map from both edges. */
body.kcai-home .is-layout-constrained > .kch-nav,
body.kcai-home .is-layout-constrained > .kch-statement,
body.kcai-home .is-layout-constrained > .kch-hero,
body.kcai-home .is-layout-constrained > .kch-filters,
body.kcai-home .is-layout-constrained > .kct-band,
body.kcai-home .is-layout-constrained > .kch-below,
body.kcai-home .wp-block-post-content > .kch-nav,
body.kcai-home .wp-block-post-content > .kch-statement,
body.kcai-home .wp-block-post-content > .kch-hero,
body.kcai-home .wp-block-post-content > .kch-filters,
body.kcai-home .wp-block-post-content > .kct-band,
body.kcai-home .wp-block-post-content > .kch-below{
	max-width:none;width:100%;margin-left:0;margin-right:0}

body.kcai-home{
	--wp--style--root--padding-left:0;
	--wp--style--root--padding-right:0;
	margin:0}

/* The site header is removed on this page (see includes/homepage.php), so
   nothing above the thin nav needs clearing. The theme still emits a wrapper
   with block-level padding; zero it so the nav really is the first 60px. */
body.kcai-home .wp-block-post-content,
body.kcai-home .entry-content{padding-top:0;margin-top:0}

/* NO ADMIN-BAR RULE, AND THAT IS THE CORRECT ANSWER RATHER THAN AN OMISSION.
   `.kch-nav` is position:relative, not fixed, so the admin bar simply pushes
   the whole document down and nothing overlaps. A `top` offset here would have
   moved a relatively-positioned element for no reason and shifted the nav 32px
   away from the content it belongs to.

   What DOES need saying: the admin bar is 32px of viewport that an anonymous
   visitor never has, so any first-screen proportion measured in a logged-in
   browser is 32px short. Measure anonymously, or remove the bar first. That
   trap has produced false readings on this project before. */


/* ==========================================================================
   THE PRODUCTION FOOTER (1.8.0)
   ==========================================================================
   The approved footer is `.kch-foot`: an ink band, centred, with the product
   name and one line under it. That line read

       DESIGN REVIEW PREVIEW · not deployed · shelter data is demonstration only

   which is a sentence about a review and cannot appear on a public page. The
   band, the colour, the padding and the type scale are untouched; what changed
   is that the second line is now the site's destination list.

   THE LIST IS THE SAME ONE THE SITE-WIDE FOOTER CARRIES, link for link. That
   footer is removed on this page because it is styled entirely by kcai-ui.css,
   which this page dequeues - so the links move rather than disappear, and
   nothing reachable from the bottom of any other page became unreachable from
   the bottom of this one.

   Link colour is measured, not chosen. #93A9BB on #0A141D is 7.4:1 and the
   hover/focus state is metallic white at 17.5:1. The electric blue used for
   links elsewhere is 2.7:1 on white and would not have been used here anyway;
   on this ink band it is legible, but the muted tone keeps the footer from
   competing with the map above it. */
.kch-footlinks{
	display:flex;flex-wrap:wrap;justify-content:center;gap:6px 18px;
	margin:12px auto 10px;padding:0;max-width:60rem;list-style:none}
.kch-footlinks li{margin:0}
.kch-footlinks a{
	color:#93A9BB;font-size:13.5px;text-decoration:none;
	/* 44px of vertical target on a phone without a 44px visual gap: the row
	   gap above is 6px, so the padding does the work and the flex line box
	   absorbs it. Measured, not assumed. */
	display:inline-block;padding:11px 2px;line-height:1.2}
.kch-footlinks a:hover,
.kch-footlinks a:focus-visible{color:var(--white);text-decoration:underline}

.kch-foot__legal{font-size:12px;color:#7E93A5;margin-top:6px}
.kch-foot__legal a{color:#93A9BB}

/* ==========================================================================
   THE POSTING BUTTONS ARE <a> IN PRODUCTION, AND UA STYLES FOUND THE GAP
   ========================================================================== */
/* Measured on the live page, not reasoned about: both posting actions rendered
   with a white underline through the label. The colour was right - `.kch-post`
   sets #FFFFFF at (0,1,0) and won - but nothing in this sheet set
   `text-decoration`, because in the preview these were <button> elements and a
   button has none by default. In production they navigate, so they are <a>, and
   the UA stylesheet's `a { text-decoration: underline }` had nothing to lose to.

   Same family of trap as the blue-on-blue anchor buttons in the Recovery
   Center: a control that changes element type inherits a different set of
   defaults, and the ones it inherits are the ones nobody wrote a rule for.
   Every control on this page that can be an anchor is listed, not just the two
   that were caught.

   Underline is restored on :focus-visible, where it is a legibility aid on top
   of the focus ring rather than a claim that the control is a link. */
.kch-post,
.kch-btn,
.kch-tool,
.kch-seg__b,
.kch-tab,
.kch-back{text-decoration:none}
.kch-post:focus-visible,
.kch-btn:focus-visible{text-decoration:underline}

/* ==========================================================================
   TWO DEFECTS THE 320-430px RIG FOUND ON THE LIVE PAGE (1.8.3)
   ========================================================================== */

/* 1. THE FOOTER LINKS MEASURED 38px, NOT 44.
      13.5px x 1.2 line-height is 16.2px, and 11px of padding either side gives
      38.2 - so the arithmetic was wrong by six pixels and no amount of looking
      at it would have shown that. 14px gives 44.2 at every width.

      They are standalone destinations in a list, so the target-size floor
      genuinely applies to them, unlike the prose links below. */
.kch-footlinks a{padding:14px 2px}

/* 2. THE MAP SEARCH INPUT WAS 14px, WHICH ZOOMS THE PAGE ON iOS.
      Mobile Safari zooms into any focused input under 16px, and it does not
      zoom back out. On a page whose first screen is a map, that leaves somebody
      looking at a magnified fragment of Kansas City with no obvious way back -
      after typing one city name.

      Raised only at the width where the control has room for it: at 560px and
      below the search moves out of the map bar and into the filter panel (see
      placeControls), which is exactly where the extra two pixels of type fit.
      Above that it is unchanged, because a desktop browser does not do this and
      the map bar is measured tight. */
@media (max-width: 560px){
	.kch-search__in{font-size:16px}
}

/* NOT FIXED, AND STATED RATHER THAN LEFT TO BE NOTICED: the links inside the
   below-the-fold paragraphs measure 18-41px tall. They are INLINE links inside
   sentences, which WCAG 2.5.5 and 2.5.8 both exempt by name, and padding an
   inline link to 44px makes it overlap the lines above and below it - trading a
   theoretical target for a real overlap. Every destination they point at is
   also reachable from the footer list, which is 44px. */

/* ==========================================================================
   THE TWO POSTING BUTTONS FAILED WCAG AA, AND BY A LOT (1.8.4)
   ==========================================================================
   Measured on the live page at 375px, 13px bold white on each gradient:

       Post Missing Pet   #FF5C1F -> #FF4D1F     3.09:1 .. 3.32:1
       Post Found Pet     #12B3FF -> #0B7FC4     2.35:1 .. 4.33:1

   AA needs 4.5:1 for text this size - 13px bold is well under the 18.66px
   large-text threshold. Both fail across their whole ramp, and the Found button
   at 2.35:1 is white on light blue, which is close to unreadable in daylight.

   These are the two most important controls on this site. They are pressed by
   somebody standing outside, on a phone, at the worst hour of their week, and
   they were the least legible text on the page.

   THE FIX CHANGES LIGHTNESS AND NOTHING ELSE. Each colour was converted to
   HSL, its hue and saturation held exactly, and lightness lowered in 0.25%
   steps until white cleared 4.5:1 - so the thermal orange is still the thermal
   orange and the electric blue is still the electric blue, one step deeper.
   The gradient direction, the shadows, the sizes, the animation and the
   hierarchy between the two buttons are all untouched.

       Post Missing Pet   #DA3B00 -> #E22E00     4.56:1 .. 4.54:1
       Post Found Pet     #007DB8 -> #0B7CBF     4.54:1 .. 4.52:1

   This follows the precedent set in the Recovery Center on 2026-08-06, where
   Facebook's own #1877F2 measured 4.23:1 with white and was darkened one step:
   brand fidelity does not get to win an argument with legibility on a page
   somebody reads in a car park at night.

   Flagged for Jay as the ONE deviation from the locked visual design, made
   because the alternative was shipping a known AA failure on the primary
   action. Reverting it is these six lines.

   1.9.2 TOOK ONE MORE STEP, AND UNIFIED THE PAGE WHILE IT WAS THERE.

   Those four values cleared 4.5 by between 0.017 and 0.059. That is a pass and
   it is not a margin: the next person to nudge a stop, add a translucency or
   restyle a label fails the page without anything looking different. Hue and
   saturation held exactly again, lightness down a further 1.25-1.5%:

       Post Lost Pet      #D23900 -> #DA2C00     4.85:1 .. 4.82:1
       Post Found Pet     #0079B2 -> #0B77B8     4.82:1 .. 4.82:1

   These are now THE SAME FOUR VALUES the featured tiles use for their status
   badges, which they were not before — the page carried two oranges and two
   blues differing by a single lightness step, for no reason anybody could see.
   One accent pair, measured once, used everywhere. */
.kch-post--missing{
	background:linear-gradient(180deg,#D23900 0%,#DA2C00 100%);
	box-shadow:0 1px 2px rgba(120,33,0,.34),0 4px 14px rgba(218,59,0,.32)}
.kch-post--found{
	background:linear-gradient(180deg,#0079B2 0%,#0B77B8 100%);
	box-shadow:0 1px 2px rgba(0,48,76,.34),0 4px 14px rgba(0,125,184,.30)}

/* ==================================================== THE THEME TOGGLE ==== */

/*
 * A REAL BUTTON WITH A REAL PRESSED STATE.
 *
 * `aria-pressed` and nothing else carries the state to assistive technology:
 * the accessible NAME stays "Dark mode" in both states, because a control whose
 * name changes as you press it is announced as a different control each time
 * and a screen-reader user cannot tell whether they moved or it did.
 *
 * Sighted state is carried by SHAPE as well as colour — the sun and the moon
 * are different silhouettes, and the knob physically moves — because the brief
 * forbids status that is colour-only and a monochrome display would otherwise
 * show two identical pills.
 */
.kch-theme {
	position: relative;
	flex: none;
	display: inline-flex;
	align-items: center;
	gap: 7px;
	min-height: 44px;
	height: 38px;
	padding: 0 12px 0 8px;
	border: 1px solid var(--line);
	border-radius: 999px;
	background: var(--surf-2);
	color: var(--ink);
	font: 700 12.5px/1 var(--font);
	letter-spacing: .01em;
	cursor: pointer;
	white-space: nowrap;
	transition: border-color .2s ease, background .2s ease;
}

/* Drawn at 38px, targeted at 44px. The bar is 60px and a 44px pill inside it
   would leave no room for the two posting actions at 320px. */
.kch-theme::before {
	content: "";
	position: absolute;
	top: 50%; left: 50%;
	width: calc(100% + 6px); height: 44px;
	transform: translate(-50%, -50%);
}

.kch-theme:hover { border-color: var(--blue); background: var(--surf); }

.kch-theme:focus-visible {
	outline: 3px solid var(--ink);
	outline-offset: 2px;
}

/* The track and the knob. The knob's POSITION is the second, non-colour signal. */
.kch-theme__sw {
	position: relative;
	width: 34px; height: 20px;
	flex: none;
	border-radius: 999px;
	background: var(--line);
	transition: background .22s ease;
}

.kch-theme__sw i {
	position: absolute;
	top: 2px; left: 2px;
	width: 16px; height: 16px;
	border-radius: 50%;
	background: var(--surf);
	box-shadow: 0 1px 2px rgba(0, 0, 0, .3);
	display: grid;
	place-items: center;
	transition: transform .22s cubic-bezier(.22, .61, .36, 1);
}

.kch-theme__sw svg { width: 11px; height: 11px; display: block; color: var(--ink); }

/* Pressed = dark is active. The knob travels; the glyph swaps sun for moon. */
.kch-theme[aria-pressed="true"] .kch-theme__sw { background: var(--blue-t); }
.kch-theme[aria-pressed="true"] .kch-theme__sw i { transform: translateX(14px); }

.kch-theme[aria-pressed="true"] .kch-theme__sun,
.kch-theme[aria-pressed="false"] .kch-theme__moon { display: none; }

/* Below 561px the label goes and the control is the switch alone. The
   accessible name is unaffected: it comes from the visually-hidden text. */
@media (max-width: 560px) {
	.kch-theme { padding: 0 8px; gap: 0; }
	.kch-theme__t { display: none; }
}

@media (prefers-reduced-motion: reduce) {
	.kch-theme, .kch-theme__sw, .kch-theme__sw i { transition: none; }
}

/* ============================== DARK: THE RULES THAT WERE WRITTEN IN LITERALS
 *
 * Everything above this point that could be expressed as a token is one. What
 * is left are rules whose colours were written as literal hex when there was
 * only ever going to be one theme — pastel status chips, two warning wells, a
 * handful of hover states and the failure panel. They are restated here rather
 * than re-tokenised in place, because that keeps the light design byte-for-byte
 * what was approved and puts every dark decision in one readable block.
 *
 * NOTHING IN assets/petmap/ IS TOUCHED. Those four files deploy byte-identically
 * to this site and to KC Pet Search & Rescue, and the drift-control rule is that
 * they are edited at the shared master or not at all. The map's own chrome is
 * themed from HERE instead, which costs a few selectors and keeps the shared
 * engine identical on both sites.
 */

/*
 * :not(.is-on), AND THAT QUALIFIER IS THE WHOLE FIX.
 *
 * Written unqualified, this rule has the same specificity as
 * `.kch-tab.is-on .kch-tab__n` and appears later in the file, so it won — and
 * stripped the SELECTED tab's pill of its light background while leaving the
 * dark text the selected state had set. The count read 1.39:1. An override
 * block that sits at the end of a stylesheet has to say which state it is for.
 */
:root[data-theme="dark"] .kch-tab:not(.is-on) .kch-tab__n { background: #22303E; }
:root[data-theme="dark"] .kch-tool.is-on { color: #BFE6FF; background: rgba(79, 195, 255, .14); }
:root[data-theme="dark"] .kch-hover__img { background: #101A23; }
:root[data-theme="dark"] .kch-hover__none { background: #101A23; }
:root[data-theme="dark"] .kch-hover__go:hover { background: #FFFFFF; }
:root[data-theme="dark"] .kch-pcard__none { background: #101A23; }

/* Status chips: same hues, re-based onto a dark surface. Each pair measured. */
:root[data-theme="dark"] .kch-tag--lost  { background: #40190A; color: #FFC7AC; }
:root[data-theme="dark"] .kch-tag--dog   { background: #40190A; color: #FFC7AC; }
:root[data-theme="dark"] .kch-tag--found { background: #0A2B3E; color: #B7E4FF; border-color: #4FC3FF; }
:root[data-theme="dark"] .kch-tag--cat   { background: #2C1741; color: #E2C9FF; }
:root[data-theme="dark"] .kch-tag--other { background: #1E2A36; color: #C6D5E1; }

/* The two warning wells and the disabled control. */
:root[data-theme="dark"] .kch-pcard__zone { background: #2A1509; color: #FFC7AC; }
:root[data-theme="dark"] .kch-flow__warn  { background: #2A1509; color: #FFC7AC !important; }
:root[data-theme="dark"] .kch-btn[aria-disabled="true"] { background: #1B2732; color: #9FB2C1; }

/* Hover states that were written as one-off literals. */
:root[data-theme="dark"] .kch-btn--primary:hover { background: #FFFFFF; }
:root[data-theme="dark"] .kch-btn--saw    { color: #04121C; }
:root[data-theme="dark"] .kch-btn--saw:hover { background: #7BD2FF; }
:root[data-theme="dark"] .kch-btn--found:hover { background: #FF8F5F; }

/* The load-failure panel: it must be legible in the theme it fails in. */
:root[data-theme="dark"] .kch-loadfail { background: #2A1509; border-color: #FFC7AC; color: #FFC7AC; }
:root[data-theme="dark"] .kch-loadfail p { color: #FFC7AC; }
:root[data-theme="dark"] .kch-loadfail a { color: #FFC7AC; }

/*
 * THE MAP'S OWN CHROME, THEMED FROM OUTSIDE THE SHARED ENGINE.
 *
 * Leaflet's popup and controls are white boxes with a white arrow. On a dark
 * page they are the only bright rectangles left, which reads as a rendering
 * fault rather than as a design.
 */
:root[data-theme="dark"] .kch-map .leaflet-popup-content-wrapper,
:root[data-theme="dark"] .kch-map .leaflet-popup-tip { background: #16212B; color: #EEF4F8; }
:root[data-theme="dark"] .kch-map .leaflet-bar a,
:root[data-theme="dark"] .kch-map .leaflet-control-attribution {
	background: #16212B;
	color: #C6D5E1;
}
:root[data-theme="dark"] .kch-map .leaflet-bar a { border-bottom-color: #29384A; }
:root[data-theme="dark"] .kch-map .leaflet-bar a:hover { background: #22303E; }
:root[data-theme="dark"] .kch-map .leaflet-control-attribution a { color: #6FCDFF; }
:root[data-theme="dark"] .kch-map .leaflet-popup-close-button { color: #C6D5E1 !important; }

/*
 * A 41px FOOTER LINK. The shortest label in the list ("About") measured 41x44 —
 * the height was already right and the width was three pixels short, because
 * the links are laid out by their text. A minimum width costs nothing on the
 * long ones and closes the last real target failure on the page.
 */
.kch-footlinks a { min-width: 44px; justify-content: center; }

/*
 * A WHOLE-PARAGRAPH LINK IS A CONTROL, WHATEVER ITS DISPLAY VALUE IS.
 *
 * `.kch-sec__note` paragraphs are a single link and a full stop — "Read the
 * full Privacy Policy." — so the WCAG 2.5.8 exemption for a link inside a
 * sentence does not really apply: there is no sentence around it. It measured
 * 158x18.
 *
 * Vertical padding on an INLINE element grows the hit box without growing the
 * line box, so this buys 44px of target and moves nothing on the page. That is
 * the whole reason it is padding and not min-height: a block treatment here
 * would push every note paragraph apart.
 */
.kch-sec__note a { padding: 14px 0; }   /* 13px measured 43.6px against a 21.6px line box */

/* ============ PHONES: A STACKED NAV, A SHORTER MAP, AND A SCROLLABLE ONE ====
 *
 * Everything in this block is scoped to 560px and below. Tablet and desktop map
 * heights are untouched by design — they were measured at 604px (768) and 698px
 * (1024/1280/1440) before this release and must read the same after it.
 */

@media (max-width: 560px) {

	/*
	 * THE NAVIGATION BECOMES TWO ROWS.
	 *
	 * Two two-line buttons cannot sit beside a brand and a theme switch on a
	 * 320px bar without one of them being abbreviated, and abbreviating them is
	 * exactly what this release exists to undo. So the bar wraps: identity and
	 * theme on the first row, the two actions stacked full width beneath.
	 *
	 * Full width is not only a fitting decision. These are the two controls
	 * somebody reaches for one-handed, in a hurry, outside — a full-width
	 * target is the easiest thing on a phone to hit without looking.
	 */
	.kch-nav {
		height: auto;
		min-height: var(--nav-h);
		flex-wrap: wrap;
		align-items: center;
		gap: 8px;
		padding: 8px 12px 10px;
	}

	.kch-nav__actions {
		order: 3;
		flex: 1 0 100%;
		display: grid;
		grid-template-columns: 1fr;
		gap: 8px;
		margin-left: 0;
	}

	.kch-post { width: 100%; padding: 9px 12px; }
	.kch-post__l1 { font-size: 13px; }
	.kch-post__l2 { font-size: 11px; }

	/* The theme switch keeps the right end of the first row to itself. */
	.kch-theme { margin-left: auto; }

	/*
	 * THE MAP IS GIVEN A HEIGHT INSTEAD OF INHERITING WHAT IS LEFT.
	 *
	 * It used to be whatever remained of `100vh` after the fixed bands above
	 * it, which is why a taller navigation would have eaten it. Below 560 the
	 * card is `auto` and the map itself is sized, so the two decisions are
	 * independent: the nav can grow by 100px and the map does not move.
	 *
	 * MEASURED BEFORE IT WAS CHANGED: 661px at 320/375/390 on an 844px
	 * viewport — 78.3% of the screen, which is the height a finger has to cross
	 * to reach anything below it.
	 *
	 * `svh` IS THE UNIT THAT MATTERS HERE. `vh` on a phone is the LARGEST
	 * viewport — the one with the address bar retracted — so a `vh`-sized map
	 * is taller than the screen whenever the bar is showing, and it resizes
	 * under the reader as the bar slides away. `svh` is the smallest viewport
	 * and does not move. The `vh` line above it is the fallback for browsers
	 * that do not know `svh`; they get the old behaviour rather than nothing.
	 *
	 * clamp floor and ceiling: 320px so a landscape phone still shows a usable
	 * map, 560px so a very tall phone does not go back to filling the screen.
	 */
	.kch-mapcard { height: auto; min-height: 0; }
	.kch-mapwrap {
		flex: none;
		height: clamp(320px, 60vh, 560px);
		height: clamp(320px, 60svh, 560px);
	}
}

/* ===================================== THE ONE-FINGER SCROLL HINT ========== */

/*
 * Shown for a moment when somebody tries to drag the map with one finger, and
 * never otherwise.
 *
 * It is `pointer-events: none` and it sits at the BOTTOM of the map, above the
 * attribution and below the centre where the pins cluster, so it cannot take a
 * tap meant for a pet and cannot cover the pins for the second and a half it is
 * visible. It fades rather than appearing, because something that pops into
 * existence over a map reads as an error.
 */
.kch-2f {
	position: absolute;
	left: 50%;
	bottom: 16px;
	z-index: 45;
	transform: translate(-50%, 6px);
	max-width: min(280px, calc(100% - 28px));
	padding: 9px 14px;
	border-radius: 999px;
	background: rgba(10, 20, 29, .88);
	color: #FFFFFF;
	font: 700 12.5px/1.3 var(--font);
	text-align: center;
	pointer-events: none;
	opacity: 0;
	transition: opacity .22s ease, transform .22s ease;
}

.kch-2f.is-on { opacity: 1; transform: translate(-50%, 0); }

@media (prefers-reduced-motion: reduce) {
	.kch-2f { transition: none; }
}

/*
 * THE MAP MUST BE ABLE TO SCROLL THE PAGE.
 *
 * Leaflet sets `touch-action: none` through its own `leaflet-touch-drag` class
 * whenever dragging is enabled, and that single declaration is the whole trap:
 * with it in force the browser hands every one-finger movement to the map and
 * the page cannot scroll past it. Production measured exactly that.
 *
 * The class is added and removed by Leaflet as `map.dragging` is enabled and
 * disabled, which the controller now does per gesture — so this rule is a
 * BACKSTOP, not the mechanism. It guarantees that whatever the engine thinks,
 * a vertical one-finger drag over the map belongs to the page.
 *
 * `pan-y` and not `auto`: horizontal panning stays with the map, so a
 * two-finger pan is not fighting a horizontal page scroll that does not exist.
 */
.kch-map.is-scrollpass,
.kch-map.is-scrollpass.leaflet-touch-drag { touch-action: pan-y; }
