/**
 * KCAI PET TILES — three featured listings, one per tile.
 * =============================================================================
 *
 * EVERY COLOUR IN THIS FILE COMES OUT OF THE TOKEN BLOCK BELOW.
 *
 * Below `THE TOKENS` there is not one hex value in this file, deliberately.
 * That is what makes the component theme-aware rather than "light with a dark
 * skin bolted on": a theme is a different set of values for the same names, and
 * if a rule further down reached for #FFFFFF it would be white in both themes
 * and nobody would notice until a screenshot.
 *
 * THE TOKENS DEFER TO THE PAGE'S OWN SYSTEM.
 *
 * Each one is `var(--page-token, fallback)`. On the homepage, which defines
 * --ink, --dim, --line and the rest, those win; on a page that does not, the
 * fallback keeps the component whole. When the site's light/dark token set
 * lands, remapping this component is this one block and nothing else.
 *
 * THE PAGE DECIDES THE THEME. THE OPERATING SYSTEM DOES NOT DECIDE IT ALONE.
 *
 *   [data-theme="dark"]   explicit: dark, whatever the device says
 *   [data-theme="light"]  explicit: light
 *   [data-theme="auto"]   follow prefers-color-scheme
 *   (nothing set)         LIGHT — the same surface as the page around it
 *
 * This was measured before it was decided. On a device set to dark, an
 * unqualified `@media (prefers-color-scheme: dark)` turned this band dark
 * inside the approved LIGHT homepage: three charcoal cards and a near-white
 * heading on a white page. The component was theme-aware and the page was not,
 * so being right on its own made the page wrong.
 *
 * A component cannot unilaterally decide the surface it sits on. `auto` is the
 * opt-in that says "this page follows the system", and the day the site gains a
 * light/dark switcher, setting that one attribute on <html> turns this band
 * over to it with no change to this file.
 *
 * WHY THE ACCENT EXISTS TWICE.
 *
 * `--kct-accent` is a FILL that carries white text and `--kct-accent-t` is TEXT
 * on the card surface. They cannot be the same value: the thermal orange that
 * clears 4.5:1 under white is far too dark to read against a dark card, and the
 * bright orange that reads well on a dark card fails under white text. Both
 * were measured, not assumed — see the regression report.
 */

/* ============================================================== THE TOKENS */

.kct-band {
	/* --- surfaces and type ------------------------------------------- */
	--kct-surface:   var(--surf, #FFFFFF);
	--kct-surface-2: var(--page, #F4F7FA);
	--kct-text:      var(--ink,  #0A141D);
	--kct-dim:       var(--dim,  #4A5C6D);
	--kct-line:      var(--line, #DCE4EB);

	/* --- accents ------------------------------------------------------ */
	/*
	 * THE FILLS ARE 1.5% DARKER THAN THE HOMEPAGE CTA FILLS, AND THAT IS A
	 * MEASUREMENT RATHER THAN A PREFERENCE.
	 *
	 * The activation release derived #DA3B00/#E22E00 and #007DB8/#0B7CBF by
	 * lowering lightness until white cleared 4.5:1 on a 13px button label, and
	 * stopped at the first value that did. Measured under white here they are
	 * 4.559 / 4.541 / 4.540 / 4.517 — the last one clears the requirement by
	 * seventeen thousandths, on an 11px badge.
	 *
	 * A margin that thin is not a margin: the next person to nudge a gradient
	 * stop, add a translucency or restyle the badge fails the page without
	 * anything looking different. Hue and saturation are held EXACTLY and
	 * lightness is dropped a further 1.25-1.5%, which is imperceptible side by
	 * side and buys 4.82:1 on all four.
	 *
	 * The BRIGHT brand values are still used, for the accent edge and the hover
	 * glow, where nothing is read against them — so the tile reads as the same
	 * orange and the same blue as the rest of the page.
	 */
	--kct-thermal:    #D23900;   /* 4.845:1 under white */
	--kct-thermal-2:  #DA2C00;   /* 4.818:1 */
	--kct-thermal-t:  #C23400;   /* text on the card surface */
	--kct-thermal-g:  rgba(255, 92, 31, .55);

	--kct-cyan:       #0079B2;   /* 4.818:1 under white */
	--kct-cyan-2:     #0B77B8;   /* 4.820:1 */
	--kct-cyan-t:     #00618F;
	--kct-cyan-g:     rgba(0, 164, 253, .55);

	--kct-on-accent:  #FFFFFF;

	/* --- depth -------------------------------------------------------- */
	--kct-shadow:    0 1px 2px rgba(10, 20, 29, .06), 0 8px 24px rgba(10, 20, 29, .08);
	--kct-shadow-hi: 0 2px 6px rgba(10, 20, 29, .10), 0 18px 44px rgba(10, 20, 29, .18);
	--kct-shine:     rgba(255, 255, 255, .42);

	/* --- metrics ------------------------------------------------------ */
	--kct-r:    14px;
	--kct-gap:  16px;
	--kct-fade: 520ms;   /* must match NS.FADE_MS in kcai-pettiles.js */
	--kct-ease: cubic-bezier(.22, .61, .36, 1);
	--kct-font: var(--font, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
}

/* Only where the PAGE has opted into following the system. */
@media (prefers-color-scheme: dark) {
	:root[data-theme="auto"] .kct-band,
	.kct-band[data-theme="auto"] {
		--kct-surface:   #16212B;
		--kct-surface-2: #0E1720;
		--kct-text:      #EFF5F9;
		--kct-dim:       #A9BBC8;
		--kct-line:      #2A3946;

		--kct-thermal-t: #FF8A5C;
		--kct-cyan-t:    #5CC5FF;

		--kct-shadow:    0 1px 2px rgba(0, 0, 0, .5), 0 10px 28px rgba(0, 0, 0, .55);
		--kct-shadow-hi: 0 2px 8px rgba(0, 0, 0, .55), 0 20px 48px rgba(0, 0, 0, .7);
		--kct-shine:     rgba(255, 255, 255, .16);
	}
}

/* The explicit choice, last and stronger, so it beats the media query above. */
:root[data-theme="dark"] .kct-band,
.kct-band[data-theme="dark"] {
	--kct-surface:   #16212B;
	--kct-surface-2: #0E1720;
	--kct-text:      #EFF5F9;
	--kct-dim:       #A9BBC8;
	--kct-line:      #2A3946;
	--kct-thermal-t: #FF8A5C;
	--kct-cyan-t:    #5CC5FF;
	--kct-shadow:    0 1px 2px rgba(0, 0, 0, .5), 0 10px 28px rgba(0, 0, 0, .55);
	--kct-shadow-hi: 0 2px 8px rgba(0, 0, 0, .55), 0 20px 48px rgba(0, 0, 0, .7);
	--kct-shine:     rgba(255, 255, 255, .16);
}

:root[data-theme="light"] .kct-band,
.kct-band[data-theme="light"] {
	--kct-surface:   #FFFFFF;
	--kct-surface-2: #F4F7FA;
	--kct-text:      #0A141D;
	--kct-dim:       #4A5C6D;
	--kct-line:      #DCE4EB;
	--kct-thermal-t: #C23400;
	--kct-cyan-t:    #00618F;
}

/* Per-tile accent resolution. One indirection, so every rule below reads
 * --kct-accent and no rule below needs to know which tile it is in. */
.kct[data-accent="thermal"] {
	--kct-accent:   var(--kct-thermal);
	--kct-accent-2: var(--kct-thermal-2);
	--kct-accent-t: var(--kct-thermal-t);
	--kct-accent-g: var(--kct-thermal-g);
}

.kct[data-accent="cyan"] {
	--kct-accent:   var(--kct-cyan);
	--kct-accent-2: var(--kct-cyan-2);
	--kct-accent-t: var(--kct-cyan-t);
	--kct-accent-g: var(--kct-cyan-g);
}

/* ================================================================ THE BAND */

.kct-band {
	max-width: 1180px;
	margin: 22px auto 0;
	padding: 0 16px;
	font-family: var(--kct-font);
}

.kct-band__head {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 4px 12px;
	margin-bottom: 10px;
}

.kct-band__h {
	margin: 0;
	font-size: 19px;
	font-weight: 800;
	letter-spacing: -.015em;
	color: var(--kct-text);
}

.kct-band__s {
	margin: 0;
	font-size: 13px;
	color: var(--kct-dim);
}

/*
 * ONE COLUMN UNTIL THERE IS ROOM FOR THREE.
 *
 * There is no two-column tier: three tiles in two columns leaves one alone on
 * the second row at a different width from its siblings, which is the opposite
 * of "equal in height and visually aligned". At 900px each tile is ~280px wide,
 * which is where the 4:3 photograph and the four text rows stop being
 * comfortable — below it they stack at full width rather than being squeezed.
 */
/*
 * STACKED, AND CAPPED AT 440px.
 *
 * The cap is not decoration. A tile is a 4:3 photograph plus four text rows, so
 * its height follows its width: at 768px a full-width tile measured 721 x 796,
 * which put the three of them 2,400px apart and turned a summary into a scroll.
 * Capped and centred, the same tablet gets a 440px card that reads exactly like
 * the phone one.
 *
 * `minmax(0, 440px)` rather than `440px` so that below 440 the column simply
 * shrinks - at 320px it resolves to 273px and nothing is squeezed, because
 * nothing is competing for the row.
 */
.kct-grid {
	display: grid;
	grid-template-columns: minmax(0, 440px);
	justify-content: center;
	gap: var(--kct-gap);
	align-items: stretch;
}

@media (min-width: 900px) {
	.kct-grid {
		grid-template-columns: repeat(3, minmax(0, 1fr));
		justify-content: stretch;
	}
}

/* ================================================================= A TILE */

.kct {
	display: grid;
	grid-template-rows: auto 1fr auto;
	min-width: 0;                       /* a grid item's default min-width is
	                                       auto, which lets a long word push the
	                                       whole band wider than the viewport */
	background: var(--kct-surface);
	border: 1px solid var(--kct-line);
	border-radius: var(--kct-r);
	box-shadow: var(--kct-shadow);
	overflow: hidden;
	position: relative;
	transition: box-shadow .28s var(--kct-ease), transform .28s var(--kct-ease);
}

/*
 * The restrained glow: a 3px accent edge along the top and a soft accent halo,
 * both static. Nothing on this band pulses. The homepage already has one
 * animated element — the Post Missing Pet lifeline — and a second rhythm
 * competing with it is how a page starts to feel busy rather than alive.
 */
.kct::before {
	content: "";
	position: absolute;
	inset: 0 0 auto 0;
	height: 3px;
	background: linear-gradient(90deg, var(--kct-accent), var(--kct-accent-2));
	z-index: 2;
}

.kct:hover {
	box-shadow: var(--kct-shadow-hi), 0 0 0 1px var(--kct-accent-g);
	transform: translateY(-2px);
}

/*
 * THE PASSING SHINE — hover only, once, and ACROSS THE PHOTOGRAPH ONLY.
 *
 * A shine that loops is decoration; a shine that answers a pointer is feedback.
 *
 * It is deliberately confined to the image box rather than sweeping the whole
 * card. A translucent white band passing over body text lifts the text's
 * background for the length of the sweep, which is a contrast change on
 * something somebody may be reading. Over a photograph it is exactly the
 * highlight it is meant to be, and the text below never moves or changes.
 *
 * It transforms and fades only, so it composites and never repaints.
 */
.kct__ph::after {
	content: "";
	position: absolute;
	top: 0;
	bottom: 0;
	left: -55%;
	width: 42%;
	z-index: 1;
	pointer-events: none;
	opacity: 0;
	background: linear-gradient(100deg, transparent, var(--kct-shine), transparent);
	transform: translateX(0);
}

.kct:hover .kct__ph::after {
	animation: kct-shine 900ms var(--kct-ease) 1;
}

@keyframes kct-shine {
	0%   { opacity: 0; transform: translateX(0); }
	12%  { opacity: 1; }
	88%  { opacity: 1; }
	100% { opacity: 0; transform: translateX(340%); }
}

/* --------------------------------------------------------------- the head */

.kct__head {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 11px 12px 9px;
	border-bottom: 1px solid var(--kct-line);
	position: relative;
	z-index: 2;
}

.kct__h {
	margin: 0;
	font-size: 14.5px;
	font-weight: 800;
	letter-spacing: .01em;
	text-transform: uppercase;
	color: var(--kct-accent-t);
	white-space: nowrap;
}

.kct__n {
	margin: 0;
	flex: 1 1 auto;
	min-width: 0;
	font-size: 12px;
	color: var(--kct-dim);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.kct__nav {
	display: flex;
	/*
	 * 10px, NOT 6px, AND THE GAP IS THE ACCESSIBILITY FIX.
	 *
	 * Each button draws at 34px and claims 44px through a transparent
	 * pseudo-element. At a 6px gap those two 44px zones OVERLAP by 4px, so the
	 * topmost one wins the contested strip and the other button's real target
	 * is 40px wide - which an elementFromPoint probe at 21px from centre
	 * correctly reported as a failure. 34 + 10 puts the zones exactly edge to
	 * edge: adjacent, never overlapping, both a full 44.
	 */
	gap: 10px;
	flex: 0 0 auto;
}

/*
 * 34px, not 44px, and the target is not the picture.
 *
 * Two controls per tile times three tiles is six buttons on a band whose point
 * is the photograph; at 44px they dominate their own header. The button draws
 * at 34px and claims 44px through a transparent pseudo-element, the same
 * technique the map markers use.
 */
.kct__btn {
	position: relative;
	width: 34px;
	height: 34px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 0;
	border: 1px solid var(--kct-line);
	border-radius: 9px;
	background: var(--kct-surface-2);
	color: var(--kct-text);
	cursor: pointer;
	transition: background .18s var(--kct-ease), border-color .18s var(--kct-ease);
}

.kct__btn::before {
	content: "";
	position: absolute;
	top: 50%;
	left: 50%;
	width: 44px;
	height: 44px;
	transform: translate(-50%, -50%);
}

.kct__btn svg { width: 17px; height: 17px; }

.kct__btn:hover:not(:disabled) {
	background: var(--kct-surface);
	border-color: var(--kct-accent-g);
}

.kct__btn:disabled {
	opacity: .38;
	cursor: default;
}

/* An outline, never a box-shadow: see the note at the foot of this file. */
.kct__btn:focus-visible,
.kct__card:focus-visible,
.kct__emptylink:focus-visible,
.kct-all__link:focus-visible {
	outline: 3px solid var(--kct-accent-t);
	outline-offset: 2px;
	border-radius: 8px;
}

/* -------------------------------------------------------------- the stage */

/*
 * BOTH SLIDES OCCUPY THE SAME GRID CELL.
 *
 * This is what makes the cross-fade free of layout shift. There is no absolute
 * positioning and no measured height: the row is as tall as its tallest child,
 * and because every text row in a card is clamped to one line and the
 * photograph is a fixed-ratio box, the two children are always the same height.
 */
.kct__stage {
	display: grid;
	position: relative;
	z-index: 2;
}

/*
 * A FADE-THROUGH, NOT A PURE CROSS-FADE.
 *
 * Two cards at 50% opacity in the same cell means two pet names, two cities and
 * two dates legibly on top of each other for a quarter of a second, which was
 * visible in the first capture and read as a rendering fault rather than as a
 * transition. So the outgoing card leaves QUICKLY (200ms, no delay) and the
 * incoming one arrives AFTER it has gone (180ms delay). The overlap is
 * essentially zero, the whole change still takes about half a second, and the
 * upward drift makes it a movement rather than a blink.
 */
.kct__slide {
	grid-area: 1 / 1;
	opacity: 0;
	visibility: hidden;             /* removes the inactive card's link from the
	                                   tab order entirely, which `opacity: 0`
	                                   alone does not */
	pointer-events: none;
	transform: translate3d(0, 9px, 0);
	transition: opacity 200ms linear,
	            transform 380ms var(--kct-ease);
}

.kct__slide.is-in {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transform: none;
	transition: opacity 300ms var(--kct-ease) 180ms,
	            transform 420ms var(--kct-ease) 120ms;
}

/*
 * The outgoing card stays visible for exactly the length of the fade; the
 * script removes this class at the same moment, at which point the base rule's
 * `visibility: hidden` applies instantly. `visibility` is deliberately NOT
 * transitioned — a delayed visibility transition would leave an invisible link
 * in the tab order for another half second.
 */
.kct__slide.is-out {
	opacity: 0;
	visibility: visible;
	transform: translate3d(0, -7px, 0);
}

/* --------------------------------------------------------------- the card */

.kct__card {
	display: block;
	text-decoration: none;          /* these are <a>, and the UA underline has
	                                   nothing to lose to on a page that owns
	                                   its own cascade */
	color: var(--kct-text);
}

.kct__card * { text-decoration: none; }

.kct__ph {
	display: block;
	position: relative;
	aspect-ratio: 4 / 3;
	overflow: hidden;
	background: var(--kct-surface-2);
}

/*
 * ABSOLUTE, INSIDE THE RATIO BOX.
 *
 * `aspect-ratio` on a box that contains a flow image is overridden by the image
 * on grid and flex children, because their default `min-height` is `auto`. That
 * is how a set of carousel photo boxes on this project once measured anywhere
 * from 173px to 410px tall. Taking the image out of flow makes the ratio the
 * only thing deciding the height, whatever the photograph is shaped like.
 */
.kct__img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.kct__ph--none {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 8px;
	color: var(--kct-dim);
	background:
		repeating-linear-gradient(135deg,
			var(--kct-surface-2) 0 10px,
			var(--kct-surface) 10px 20px);
}

.kct__glyph { display: block; line-height: 0; color: var(--kct-line); }
.kct__glyph svg { width: 54px; height: 54px; }

.kct__nophoto {
	font-size: 12px;
	font-weight: 600;
	letter-spacing: .01em;
}

.kct__body {
	display: block;
	padding: 10px 12px 12px;
}

.kct__tags {
	display: flex;
	gap: 6px;
	margin-bottom: 8px;
}

.kct__tag {
	display: inline-block;
	padding: 3px 8px;
	border-radius: 999px;
	font-size: 11px;
	font-weight: 800;
	letter-spacing: .04em;
	text-transform: uppercase;
	white-space: nowrap;
}

.kct__tag--status {
	background: linear-gradient(180deg, var(--kct-accent), var(--kct-accent-2));
	color: var(--kct-on-accent);
}

.kct__tag--species {
	border: 1px solid var(--kct-line);
	color: var(--kct-dim);
}

/*
 * EVERY ROW BELOW IS ONE LINE.
 *
 * Not a style preference — it is the mechanism that keeps the two stacked
 * slides the same height, and therefore the mechanism that keeps the band from
 * stepping up and down as it rotates. A name that wrapped would move the whole
 * page every six seconds.
 */
.kct__name,
.kct__where,
.kct__when {
	display: block;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	min-width: 0;
}

.kct__name {
	font-size: 19px;
	font-weight: 800;
	letter-spacing: -.015em;
	line-height: 1.25;
	color: var(--kct-text);
}

.kct__where {
	margin-top: 2px;
	font-size: 13.5px;
	font-weight: 600;
	color: var(--kct-dim);
}

.kct__when {
	margin-top: 1px;
	font-size: 13px;
	color: var(--kct-dim);
}

.kct__go {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	margin-top: 9px;
	font-size: 13.5px;
	font-weight: 800;
	color: var(--kct-accent-t);
}

.kct__go svg {
	width: 15px;
	height: 15px;
	transition: transform .22s var(--kct-ease);
}

.kct__card:hover .kct__go svg { transform: translateX(3px); }

/* ---------------------------------------------------------- the empty state */

.kct__empty {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	text-align: center;
	gap: 10px;
	/* Matches a populated tile closely enough that the band stays level: a 4:3
	 * photograph plus the body block. A short tile would read as a broken
	 * layout rather than as a true statement about the data. */
	min-height: 268px;
	padding: 20px 16px;
}

.kct__emptymark { line-height: 0; color: var(--kct-line); }
.kct__emptymark svg { width: 46px; height: 46px; }

.kct__emptyline {
	margin: 0;
	max-width: 26ch;
	font-size: 14px;
	font-weight: 600;
	color: var(--kct-text);
}

.kct__emptylinks {
	margin: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 6px 14px;
	justify-content: center;
}

.kct__emptylink {
	/* A standalone action in a tile, not a link inside a sentence, so it takes
	   a real 44px target rather than the inline-prose exemption. */
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	padding: 0 2px;
	font-size: 13px;
	font-weight: 700;
	color: var(--kct-accent-t);
	text-decoration: underline;
	text-underline-offset: 2px;
}

/* -------------------------------------------------------------- the footer */

.kct__pos {
	margin: 0;
	padding: 8px 12px 9px;
	border-top: 1px solid var(--kct-line);
	font-size: 11.5px;
	font-weight: 700;
	letter-spacing: .03em;
	color: var(--kct-dim);
	position: relative;
	z-index: 2;
}

/* ======================================================= THE VIEW-ALL PATHS */

/*
 * These are STATIC MARKUP in the page, not rendered by the component.
 *
 * The complete directory has to stay reachable from the homepage whether or not
 * the tiles ever render — if the script fails, the band collapses to its
 * heading and these two links, and nothing a visitor could reach before has
 * become unreachable.
 */
.kct-all {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
	margin: 14px 0 0;
	padding: 0;
	list-style: none;
}

.kct-all__link {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	min-height: 44px;
	padding: 10px 16px;
	border: 1px solid var(--kct-line);
	border-radius: 999px;
	background: var(--kct-surface);
	color: var(--kct-text);
	font-size: 13.5px;
	font-weight: 700;
	text-decoration: none;
	transition: border-color .2s var(--kct-ease), box-shadow .2s var(--kct-ease);
}

.kct-all__link:hover {
	border-color: var(--kct-line);
	box-shadow: var(--kct-shadow);
}

.kct-all__link i {
	width: 9px;
	height: 9px;
	border-radius: 50%;
	background: var(--kct-thermal);
	flex: 0 0 auto;
}

.kct-all__link--found i { background: var(--kct-cyan); }

/* ================================================================== A11Y */

.kct-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;
}

/*
 * REDUCED MOTION: THE AUTOMATIC ROTATION IS SWITCHED OFF IN THE SCRIPT.
 *
 * This block removes the movement from everything that is left — the manual
 * change becomes an instant swap, the hover lift and the shine stop. The
 * controls keep working; the brief asks for the motion to go, not the feature.
 */
@media (prefers-reduced-motion: reduce) {
	.kct,
	.kct__slide,
	.kct__go svg,
	.kct__btn {
		transition: none !important;
	}
	.kct__slide { transform: none !important; }
	.kct:hover { transform: none; }
	.kct:hover .kct__ph::after { animation: none; }
	.kct__card:hover .kct__go svg { transform: none; }
}

/*
 * A NOTE THAT HAS COST THIS PROJECT A DEPLOY TWICE.
 *
 * Focus rings here are `outline`, never `box-shadow`. Any element carrying a
 * box-shadow ANIMATION renders its animated value in preference to a plain
 * box-shadow declaration — even while the animation is paused — so a
 * box-shadow focus ring on such an element never appears at all. The tiles
 * animate box-shadow on hover, which is exactly that case.
 */
