/* FT Land Analyzer.
 *
 * The section stays inside the site's white tone. The brief asked for a dark
 * GIS interface; every other page here is #fdfdfd, and a single dark panel
 * halfway down /ft-viewer/ would read as a different site embedded in this
 * one. What the brief was really after — professional, restrained, technical
 * but approachable — is carried instead by the typography, the hairlines and
 * the thin geometry over a light basemap. The one saturated colour in the
 * whole section is the elevation line, because it is the one thing that has
 * to be told apart from the boundary at a glance.
 *
 * Colours are literal here rather than var(--ink) and friends for the parts
 * that sit over the map: a hairline at 0.24 alpha disappears against vector
 * tiles, so the overlays carry their own slightly stronger values, the same
 * way terrain-viewer.css does. Type and spacing use the shared tokens.
 */
/* The section's own tokens. */
.ft-land {
	--fl-ink: #24363f;
	--fl-line: rgba(36, 54, 63, 0.16);
	--fl-line-strong: #cacfd1;
	--fl-dim: rgba(36, 54, 63, 0.58);
	/* The boundary the visitor draws: the one colour on the picture that is
	   theirs. Ink-on-white reads as part of the photograph at a glance, and
	   the corners have to be findable on grass, roof and tarmac alike. */
	--fl-panel-w: clamp(300px, 30%, 420px);
	--fl-radius: 2px;
	margin-top: clamp(40px, 5vw, 72px);
}

/* ---------- the two prose steps ---------- */

/* Intro and image upload are cards rather than full-bleed blocks: at this
   point the section has nothing to show yet, and a card keeps the page's
   rhythm instead of opening a hole in it. */
.fl-card {
	border: 1px solid var(--fl-line);
	background: #fff;
	padding: clamp(24px, 4vw, 48px);
}

/* Caps, like every other heading on this page — the step titles and the panel's
   group headings were already set that way, and this one was the last in
   sentence case. Transformed rather than typed, so the words stay ordinary case
   for a translation and for anything reading the markup. */
.fl-card h3 { margin: 0 0 12px; font-size: clamp(22px, 2.4vw, 32px); font-weight: 500; letter-spacing: -0.02em; text-transform: uppercase; }
.fl-card p { margin: 0; max-width: 62ch; color: var(--fl-dim); line-height: 1.6; }

.fl-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }

.fl-btn {
	display: inline-flex;
	align-items: center;
	padding: 12px 22px;
	border: 1px solid var(--fl-ink);
	border-radius: var(--fl-radius);
	background: var(--fl-ink);
	color: #fdfdfd;
	font-family: var(--sans);
	font-size: 14px;
	cursor: pointer;
	transition: opacity 150ms ease;
}
.fl-btn:hover:not(:disabled) { opacity: 0.85; }
.fl-btn:focus-visible { outline: 2px solid var(--fl-ink); outline-offset: 3px; }
/*
 * A disabled .fl-btn looked exactly like a working one.
 *
 * Start FT Analysis ships disabled and stays that way until the gate opens, so
 * this was the one button most likely to be dead when a visitor reached it —
 * and it lit up on hover like any other, inviting the click it would then
 * swallow. The tools, the draw bar and the zoom buttons had all had this since
 * they were written; the primary call to action was the one that did not.
 *
 * `default`, the same as those three: the plain arrow, not the barred circle.
 * A disabled button should read as not-yet rather than as forbidden, and the
 * dimming plus the list of what is still outstanding underneath already say
 * which. The native disabled attribute is what blocks the click; the cursor
 * only has to stop promising one.
 */
.fl-btn:disabled { opacity: 0.35; cursor: default; }

.fl-btn-ghost { background: transparent; color: var(--fl-ink); }

/* ---------- upload ---------- */

.fl-dropzone {
	display: grid;
	place-items: center;
	gap: 10px;
	margin-top: 24px;
	padding: clamp(32px, 6vw, 64px);
	border: 1px dashed var(--fl-line-strong);
	background: #fbfcfc;
	text-align: center;
}
/* The whole zone answers a drag, so the feedback has to be the whole zone. */
.fl-dropzone[data-over] { border-color: var(--fl-ink); background: #f3f6f7; }
.fl-dropzone p { margin: 0; }

/* Replace and Remove, under the viewer. The strip carried the file's facts too
   until they turned out to be the same list the panel's SOURCE IMAGE block
   prints from the same rows — read twice, once under the picture and once
   beside it. The flex row stays: it is what holds the buttons off the picture
   and keeps them on one line when the panel is narrow. */
.fl-preview {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-end;
	justify-content: space-between;
	gap: 16px 32px;
	margin-top: 16px;
}


/* Not an error, and it must not look like one — the workflow continues either
   way, so this is a note in the body colour, not a warning. */
.fl-note {
	margin: 20px 0 0;
	padding: 14px 16px;
	border-left: 2px solid var(--fl-line-strong);
	background: #fbfcfc;
	font-size: 13px;
	line-height: 1.6;
	color: var(--fl-dim);
}
.fl-note strong { display: block; color: var(--fl-ink); font-weight: 500; }

/* ---------- workspace ---------- */

/* 70 / 30, as the brief's diagram has it. The height is capped against the
   viewport because this sits in a scrolling page: a workspace taller than the
   window can never be seen whole, and the visitor would be scrolling the page
   to see the bottom of a map they are trying to draw on. */
/* The viewer sits inside the setup card now, in the upload slot, so it carries
   the dropzone's own margin rather than the page's. A little shorter than it
   was full-width: it has a card's padding either side of it. */
.fl-workspace {
	display: grid;
	grid-template-columns: minmax(0, 1fr) var(--fl-panel-w);
}
/*
 * A photograph that was not confirmed as this place gets the whole frame —
 * one it was turned down, and one nothing could tie to the ground.
 *
 * The panel is hidden rather than emptied, and the column it sat in goes with
 * it — hiding the aside alone would leave a blank white strip the width of the
 * panel, which reads as a thing that failed to load. There is nothing to put
 * there: the analysis never ran, so every figure would be a dash, and what
 * actually happened is said on the picture and underneath it.
 *
 * Below the breakpoint the panel is an overlay rather than a column, so the
 * grid rule has nothing to undo there and only the hiding applies.
 */
.fl-workspace[data-unverified="true"] {
	grid-template-columns: minmax(0, 1fr);
	height: min(680px, 72svh);
	min-height: 480px;
	margin-top: 24px;
	border: 1px solid var(--fl-line-strong);
	background: #fff;
}

/* min-height: 0 on both columns, or the grid ignores the height above.
   A grid item's default min-height is auto, which means "never smaller than
   my content" — so the panel's seventeen metrics pushed the row to 1200px and
   the workspace grew with it. With the floor removed the row honours the
   height, and .fl-panel-body's overflow-y finally has something to scroll
   inside of. */
.fl-view, .fl-panel { min-width: 0; min-height: 0; }

.fl-view { position: relative; overflow: hidden; }

/* .ft-land .fl-map, not .fl-map: maplibre-gl.css sets `position: relative` on
   .maplibregl-map, which it adds to this same element. Both are one class, so
   the winner is whichever stylesheet comes last — and maplibre's is injected
   into <head> at runtime, after ours. The extra class here settles it by
   specificity instead of by load order. Without it the container collapses to
   nothing and the map is an invisible 0px tall. */
.ft-land .fl-map { position: absolute; inset: 0; }

/* Bounded, not frozen: with no tool in hand the picture can be zoomed into and
   moved about inside its own edges, which is what maplibre's own grab already
   says. Nothing to add — and nothing to override. */
/* MapLibre's own control goes with the lock. Its compass calls resetNorth()
   directly — not a gesture, so no handler sits in front of it — and one click
   would turn the map off the heading the picture is framed on. Its zoom half
   is replaced by .fl-photo-zoom below, which knows where the floor is. The
   scale bar stays, but only when it can be read honestly — see below. */
.fl-map[data-locked="true"] .maplibregl-ctrl-top-right { display: none; }

/*
 * The scale bar goes when the picture's own size was never worked out.
 *
 * It is never wrong about the map: it says how much ground a length of
 * screen covers, and that is true whatever is drawn on top. But a photograph
 * under the lock fills the whole viewport, so the bar is read as a statement
 * about the photograph — and where the file carried no lens and no height,
 * the picture was laid down at DEFAULT_FOOTPRINT_WIDTH_M because it had to be
 * laid down at something. Quoting metres off that is quoting a number nobody
 * measured, in the one place a visitor would take for a measurement.
 *
 * It comes back the moment the size is real: a file with its lens and height,
 * a footprint the terrain match registered, or a frame the visitor has sized
 * against the ground by hand.
 */
.fl-map[data-photo-scale="assumed"] .maplibregl-ctrl-scale { display: none; }
/* And the map credit, for the length of the lock only.
 *
 * OpenStreetMap's data is ODbL: the credit is owed for showing the data, and
 * it is tied to the sight of it, not to the map object existing. While the
 * lock holds, fl-photo-ground is an opaque layer across the whole viewport
 * with the visitor's photograph over it — not one tile of OSM is on screen,
 * so there is nothing being shown to credit, and the line is only sitting on
 * someone's own picture.
 *
 * The moment the photograph is turned off or dragged, the lock lifts, the
 * basemap is visible again and the credit comes back with it. Which is why
 * this is scoped to [data-locked] and not to .fl-map: deleting the control
 * outright would strip the credit from the map view too, and that is a
 * licence condition, not a piece of styling.
 */
.fl-map[data-locked="true"] .maplibregl-ctrl-bottom-right { display: none; }

/* The visitor's photograph is no longer a pane of its own — it is a raster
   layer on the map, so that a click on it is a click on real ground. What is
   left here is the furniture around it: the handles that move it and the line
   that says how it got where it is. */

/* Drawn small, hit big, like the boundary vertices. */
.fl-photo-handle {
	display: grid;
	place-items: center;
	width: 30px;
	height: 30px;
	border: 2px solid #fff;
	background: var(--fl-ink);
	box-shadow: 0 1px 5px rgba(20, 32, 38, 0.4);
	cursor: grab;
	touch-action: none;
}
.fl-photo-handle:active { cursor: grabbing; }
/* Round to move, square to resize — the same vocabulary any image editor uses,
   so neither needs a label. */
.fl-photo-move { border-radius: 50%; }
.fl-photo-move::before { content: '✥'; color: #fff; font-size: 14px; line-height: 1; }
.fl-photo-scale { border-radius: 3px; width: 26px; height: 26px; }
.fl-photo-scale::before { content: '⤡'; color: #fff; font-size: 13px; line-height: 1; }

/* ---------- markers ---------- */


/* The location pin. Its shape and colours are in ft-land.js, which draws the
   SVG; what is here is how it sits on the map.

   drop-shadow rather than box-shadow: a box-shadow would cast a rectangle
   around a teardrop. The filter follows the path, so the pin looks like it is
   standing on the ground rather than pasted over it. */
.fl-pin {
	display: block;
	cursor: grab;
	touch-action: none;
	filter: drop-shadow(0 2px 3px rgba(20, 32, 38, 0.45));
	/* It grows from its own tip, which is the point that must not move. */
	transform-origin: 50% 100%;
	transition: transform 150ms ease;
}
.fl-pin:hover { transform: scale(1.08); }
.fl-pin:active { cursor: grabbing; }

/* ---------- toolbar ---------- */

/*
 * The step's line: what it is on the left, the tools for it on the right.
 *
 * Wraps rather than squeezes, so on a narrow screen the toolbox drops under
 * the heading at full size instead of the tools shrinking to nothing.
 */
.fl-step-head {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 10px;
}
.fl-step-head .fl-step-title { margin: 0; }

/*
 * The toolbox, above the picture rather than over it.
 *
 * It floated over the top-left corner until the tools started getting in the
 * way of the ground they act on — a boundary point placed near that corner
 * hit a button instead of the photograph. Out here it needs no glass and no
 * z-index; it is simply part of the page.
 */
/*
 * No frame round either bar.
 *
 * They were pills: a white capsule with a hairline round it, which is what a
 * control floating over a photograph needs to be legible against whatever is
 * underneath. In the panel there is nothing underneath — the column is
 * already white — so the outline was drawing a box around a box, and two of
 * them stacked read as three borders deep. What a visitor needs to see is
 * which one is chosen, and the chosen one says so itself.
 */
.fl-toolbar {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 4px;
}

/* A tool: its icon and its name, picked up together. */
.fl-tool {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 6px 12px 6px 10px;
	border: 0;
	border-radius: 999px;
	background: transparent;
	color: var(--fl-ink);
	font-family: var(--sans);
	font-size: 13px;
	white-space: nowrap;
	cursor: pointer;
	transition: background 0.15s ease, opacity 0.15s ease;
}
.fl-tool-icon { flex: 0 0 auto; display: block; }
.fl-tool:hover:not(:disabled) { background: rgba(36, 54, 63, 0.07); }
/* The tool currently holding the pointer: the next click on the picture
   belongs to it, so it is the one thing in the box that is filled in. */
.fl-tool[data-active="true"] { background: var(--fl-ink); color: #fdfdfd; }
.fl-tool:disabled { opacity: 0.35; cursor: default; }

/* One group, one choice: Image/Map and 2D/3D. */
.fl-seg { display: flex; flex: 0 0 auto; gap: 2px; }

.fl-seg-btn {
	padding: 6px 13px;
	border: 0;
	border-radius: 999px;
	background: transparent;
	color: var(--fl-dim);
	font-family: var(--sans);
	font-size: 13px;
	white-space: nowrap;
	cursor: pointer;
	transition: background 0.15s ease, color 0.15s ease;
}
.fl-seg-btn:hover { color: var(--fl-ink); }
.fl-seg-btn[aria-checked="true"] { background: var(--fl-ink); color: #fdfdfd; }

/* ---------- drawing a boundary ---------- */


/* ---------- the instruction, and the wait ---------- */

/*
 * One line at a time, above the drawing controls: what to do next.
 *
 * Absolute for the same reason as everything else in this section — the map
 * fills the view and paints over anything that is not lifted off it.
 */
.fl-prompt {
	position: absolute;
	left: 50%;
	bottom: 68px;
	z-index: 4;
	transform: translateX(-50%);
	max-width: min(560px, calc(100% - 24px));
	margin: 0;
	padding: 9px 18px;
	background: rgba(253, 253, 253, 0.94);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	border: 1px solid var(--fl-line-strong);
	border-radius: 999px;
	box-shadow: 0 6px 24px rgba(20, 32, 38, 0.12);
	font-size: 13px;
	line-height: 1.45;
	text-align: center;
	color: var(--fl-dim);
	animation: fl-rise 260ms ease both;
}
.fl-prompt strong { color: var(--fl-ink); font-weight: 500; }

@keyframes fl-rise {
	from { opacity: 0; transform: translate(-50%, 8px); }
	to { opacity: 1; transform: translate(-50%, 0); }
}

/*
 * The scan, while the photograph is being checked against the pin.
 *
 * One pass out and one back, over the picture itself, because that is what is
 * being read — the waiting belongs on the thing being examined rather than on
 * a spinner somewhere beside it.
 *
 * Under the prompt and the busy pill (z-index 4 and 5) so neither is washed
 * out by it, and pointer-events: none throughout: the map underneath stays
 * draggable while it runs.
 */
.fl-scan {
	position: absolute;
	inset: 0;
	z-index: 3;
	overflow: hidden;
	pointer-events: none;
}
/*
 * The bar is wider than it looks: a hard bright core with a long tail either
 * side, so what crosses the picture reads as a beam rather than a sliding
 * rectangle. 12% of the frame keeps that gradient legible at any width.
 */
.fl-scan::before {
	content: '';
	position: absolute;
	top: 0;
	bottom: 0;
	width: 12%;
	background: linear-gradient(
		90deg,
		rgba(80, 255, 196, 0) 0%,
		rgba(80, 255, 196, 0.12) 38%,
		rgba(150, 255, 214, 0.85) 50%,
		rgba(80, 255, 196, 0.12) 62%,
		rgba(80, 255, 196, 0) 100%
	);
	box-shadow: 0 0 36px 10px rgba(80, 255, 196, 0.28);
	/* Two passes out and back, two seconds each. One crossing read as a
	   transition — something sliding into place — rather than as a thing being
	   gone over. A repeat is what makes it a sweep, and the ease-in-out on
	   every iteration puts a beat at each end, so it paces like work rather
	   than like a loop left running. It was three; two is what was asked for.

	   The four seconds are also what the module waits out: SCAN_MS there is
	   this duration times this count. */
	animation: fl-scan-sweep 2s ease-in-out 2 both;
}
/* A faint wash under the beam, so the frame reads as being worked on rather
   than as having a light dragged across it. */
.fl-scan::after {
	content: '';
	position: absolute;
	inset: 0;
	background: rgba(80, 255, 196, 0.05);
}
@keyframes fl-scan-sweep {
	0%   { left: -12%; }
	50%  { left: 100%; }
	100% { left: -12%; }
}

/* Only while something is genuinely being fetched or computed. */
.fl-busy {
	position: absolute;
	left: 50%;
	bottom: 16px;
	z-index: 5;
	transform: translateX(-50%);
	display: flex;
	align-items: center;
	gap: 9px;
	padding: 8px 16px;
	background: rgba(253, 253, 253, 0.94);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	border: 1px solid var(--fl-line-strong);
	border-radius: 999px;
	box-shadow: 0 6px 24px rgba(20, 32, 38, 0.12);
	font-size: 13px;
	color: var(--fl-ink);
}

.fl-spinner {
	width: 13px;
	height: 13px;
	border: 1.5px solid var(--fl-line-strong);
	border-top-color: var(--fl-ink);
	border-radius: 50%;
	animation: fl-spin 700ms linear infinite;
}

@keyframes fl-spin { to { transform: rotate(360deg); } }

/* A sticky message is not progress, it is a dead end the visitor has to act
   on — so it stops turning and takes the refusal colours the verdict rows
   already use. A spinner beside an error reads as work still in hand, and a
   visitor who reads it that way waits for something that is never coming. */
.fl-busy[data-sticky="true"] .fl-spinner { display: none; }
.fl-busy[data-sticky="true"] {
	border-color: #c9553d;
	background: #fdf6f4;
	color: #8f3524;
}

/* ---------- layer panel ---------- */

/* Top left, where the toolbar and the view tabs used to be stacked. With both
   of them out above the picture there is nothing to clear any more. */
.fl-layers {
	position: absolute;
	top: 12px;
	left: 12px;
	z-index: 4;
	width: 220px;
	padding: 16px;
	background: rgba(255, 255, 255, 0.96);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	border: 1px solid var(--fl-line-strong);
	border-radius: var(--fl-radius);
	box-shadow: 0 6px 24px rgba(20, 32, 38, 0.1);
}

.fl-layers-title { margin: 0 0 12px; font-size: 10px; color: var(--fl-dim); }

.fl-layer { display: flex; align-items: center; gap: 9px; padding: 5px 0; font-size: 13px; cursor: pointer; }
.fl-layer:has(:disabled) { cursor: default; color: var(--fl-dim); }
.fl-layer-box { width: 14px; height: 14px; accent-color: var(--fl-ink); }

.fl-interval { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 10px; padding-top: 12px; border-top: 1px solid var(--fl-line); }
.fl-interval-label { font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--fl-dim); }
.fl-interval-select { padding: 4px 6px; border: 1px solid var(--fl-line-strong); border-radius: var(--fl-radius); background: #fff; font-family: var(--mono); font-size: 12px; color: var(--fl-ink); }

.fl-layers-note { margin: 12px 0 0; font-size: 11px; line-height: 1.5; color: var(--fl-dim); }

/* ---------- analysis panel ---------- */

.fl-panel { display: flex; flex-direction: column; min-width: 0; border-left: 1px solid var(--fl-line); background: #fff; }

.fl-panel-head { padding: 20px 24px 14px; border-bottom: 1px solid var(--fl-line); }
.fl-panel-title { margin: 0; font-size: 11px; color: var(--fl-dim); }

/*
 * Start, at the top of the panel rather than the foot of the form.
 *
 * Its own padding and rule, so it reads as the head of this column and not as
 * something left over from the form it came out of; full width, because a
 * button that begins everything on this side should not be a third of a line
 * wide with nothing beside it. The margin-top .fl-actions carries is for a
 * form's foot and is undone here.
 */
.fl-panel-actions { margin: 0; padding: 14px 20px; border-bottom: 1px solid var(--fl-line); }
.fl-panel-actions .fl-btn { flex: 1 1 auto; justify-content: center; }

/*
 * The view tabs and the toolbox, in the panel under the button that reveals
 * them.
 *
 * Stacked rather than strung along one line: this column is 300px at its
 * narrowest, and the tabs' own overflow-x would hide four of the seven views
 * behind a scroll with nothing to say they were there. Wrapping shows them
 * all, a line or two deep.
 */
.fl-panel-tools { display: flex; flex-direction: column; gap: 8px; padding: 12px 20px; border-bottom: 1px solid var(--fl-line); }
.fl-panel-tools .fl-image-views { flex-wrap: wrap; overflow-x: visible; }
.fl-panel-tools .fl-toolbar { align-self: flex-start; max-width: 100%; }

.fl-panel-body { flex: 1; overflow-y: auto; padding: 4px 24px 24px; }

/* The tabs are the phone layout's navigation. On a wide panel all three
   groups are visible at once and tabs would be a control that changes
   nothing, so they are hidden rather than styled down. The collapse button
   goes with them: there is no sheet to collapse at this width. */
.fl-tabs { display: none; }

.fl-group { padding: 18px 0; border-bottom: 1px solid var(--fl-line); }
.fl-group:last-child { border-bottom: 0; }
/* No bottom margin of its own: the space under an open group's heading is the
   open state's, set on the summary below, so a folded group has no gap under a
   heading with nothing beneath it. */
.fl-group-title { margin: 0; font-size: 10px; color: var(--fl-dim); }

/* Each group is a <details>, so its heading is the summary that folds it. One
   glyph, rotated, rather than two that would never match in weight. The
   summary is a flex container, which blockifies the generated content and is
   what lets it be rotated at all — said out loud below so a later change of
   display does not quietly stop the arrow turning. */
.fl-group > summary {
	display: flex;
	align-items: center;
	gap: 8px;
	margin: 0;
	cursor: pointer;
	list-style: none;
}
.fl-group > summary::-webkit-details-marker { display: none; }
.fl-group > summary::before {
	content: '▸';
	display: inline-block;
	font-family: var(--mono);
	transition: transform 120ms ease;
}
.fl-group[open] > summary::before { transform: rotate(90deg); }
.fl-group[open] > summary { margin-bottom: 14px; }
.fl-group > summary:hover .fl-group-title { color: var(--fl-ink); }
.fl-group > summary:focus-visible { outline: 2px solid var(--fl-ink); outline-offset: 2px; }

.fl-metrics { display: grid; gap: 12px; margin: 0; }
.fl-metric { display: grid; gap: 2px; }
.fl-metric-label { font-size: 11px; color: var(--fl-dim); }
/* Figures in the squared-off face the rest of the site uses for readouts. The
   numbers are the product here, so they get the size. */
.fl-metric-value { margin: 0; font-family: var(--digit); font-size: 17px; letter-spacing: -0.01em; color: var(--fl-ink); }


/* The section's one hard rule, said once, under the intro copy. Darker than
   the paragraph above it because it is a condition rather than a description —
   the Start button below will not open until both halves are in. */
/* 14px off the paragraph it used to follow. It is the card's first line now, so
   there is nothing above it to sit under — the card's own padding is the space. */
.fl-card .fl-required { margin-top: 0; color: var(--fl-ink); font-size: 14px; }

/* ---------- project setup form ---------- */

/* One column, top to bottom, because the steps are a sequence now: find the
   site, then bring the picture of it. Side by side they read as two unrelated
   jobs and left the numbers ① ② pointing across the page rather than down it. */
.fl-setup-grid {
	display: grid;
	gap: 28px;
	margin-top: 32px;
}

.fl-step { min-width: 0; }

.fl-step-title {
	display: flex;
	align-items: center;
	gap: 10px;
	margin: 0 0 16px;
	font-size: 10px;
	color: var(--fl-dim);
}

/* The step number, so the two columns still read as a sequence. */
.fl-step-n {
	display: grid;
	place-items: center;
	width: 18px;
	height: 18px;
	border-radius: 50%;
	background: var(--fl-ink);
	color: #fdfdfd;
	font-family: var(--mono);
	font-size: 10px;
	letter-spacing: 0;
}

.fl-error { margin: 12px 0 0; font-size: 13px; color: #a8412c; }

.fl-actions-tight { margin-top: 16px; gap: 8px; }

/* ---------- the location accordion ---------- */

/* Open while the site is being found, folded to one line once it is. A native
   <details>, so it opens from the keyboard and reads correctly to a screen
   reader without any of that being rebuilt here. */
.fl-accordion { border-bottom: 1px solid var(--fl-line); padding-bottom: 24px; }

.fl-accordion > summary {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 8px 16px;
	cursor: pointer;
	list-style: none;
	padding: 4px 0;
}
.fl-accordion > summary::-webkit-details-marker { display: none; }
.fl-accordion > summary .fl-step-title { margin: 0; }

/* The chevron sits at the far right of the row, turning as it opens. */
.fl-accordion > summary::after {
	content: '▾';
	margin-left: auto;
	font-size: 11px;
	color: var(--fl-dim);
	transition: transform 180ms ease;
}
.fl-accordion[open] > summary::after { transform: rotate(180deg); }

/* What was chosen, on the folded line. Empty until there is something to say,
   and it takes the whole row on a narrow screen rather than crushing the
   step's own title. */
.fl-accordion-value {
	min-width: 0;
	font-size: 13px;
	color: var(--fl-ink);
	overflow-wrap: anywhere;
}
.fl-accordion-value:empty { display: none; }

.fl-accordion-body { padding-top: 20px; }

/* ---------- form controls ---------- */

.fl-field-row { display: flex; gap: 6px; margin-top: 4px; }

/*
 * Both buttons in this panel say Search and both are .fl-btn, so this only has
 * to hold them to one width. It was written when the second one still said
 * "Go" and came out 25px narrower — a button is as wide as its word, and
 * stacked one above the other the two read as different controls.
 *
 * 80px is what "Search" measures at this size. A minimum rather than a width:
 * if a translation needs more room the button takes it rather than clipping.
 */
.fl-field-row .fl-btn { min-width: 80px; justify-content: center; }

.fl-field-row input,
.fl-fields input,
.fl-fields select,
.fl-fields textarea {
	flex: 1 1 0;
	min-width: 0;
	width: 100%;
	padding: 10px 12px;
	border: 1px solid var(--fl-line-strong);
	border-radius: var(--fl-radius);
	background: #fff;
	font-family: var(--sans);
	font-size: 14px;
	color: var(--fl-ink);
}
.fl-field-row input { font-family: var(--sans); }

/*
 * Placeholders are examples, not content, and the browser default sits close
 * enough to real text to be mistaken for it — particularly the address one,
 * which is a plausible address. Lighter than --fl-dim so it reads as a hint
 * at a glance, and every field in the section gets it so the address line and
 * the coordinate boxes under it do not disagree.
 */
.ft-land ::placeholder { color: rgba(36, 54, 63, 0.34); }
.fl-fields textarea { resize: vertical; }

.fl-field-row input:focus-visible,
.fl-fields input:focus-visible,
.fl-fields select:focus-visible,
.fl-fields textarea:focus-visible { outline: 2px solid var(--fl-ink); outline-offset: -1px; }

.fl-field-row .fl-btn { flex: 0 0 auto; padding: 10px 18px; }

/* Everything that is not required is behind a disclosure, so the form a
   visitor has to fill in is two fields rather than ten. */
.fl-details { margin-top: 16px; }
.fl-details summary {
	cursor: pointer;
	font-size: 13px;
	color: var(--fl-dim);
	list-style: none;
}
.fl-details summary::-webkit-details-marker { display: none; }
.fl-details summary::before { content: '+ '; font-family: var(--mono); }
.fl-details[open] summary::before { content: '− '; }
.fl-details summary:hover { color: var(--fl-ink); }


.fl-fields {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
	gap: 16px;
	margin-top: 20px;
}
.fl-fields label { display: grid; gap: 6px; font-size: 11px; color: var(--fl-dim); }
.fl-field-wide { grid-column: 1 / -1; }

.fl-hint { margin: 16px 0 0; font-size: 11px; line-height: 1.6; color: var(--fl-dim); }

/* Tall enough for the six results the provider is asked for, so the list does
   not open already scrolled with its first — and best — match cut in half. */
/* The field and its suggestions, as one block the list can hang from. */
.fl-search-field { position: relative; }

/*
 * The suggestions float over what is below them — which is the map — instead
 * of pushing it down the page. Typing a letter should not move the thing you
 * are about to look at, and once you are looking at the map the list gets out
 * of the way entirely.
 *
 * Which means it is now over a map rather than over the page, so it carries
 * its own opaque background and a shadow: a transparent list on top of streets
 * and labels is unreadable.
 */
.fl-search-results {
	position: absolute;
	top: calc(100% + 6px);
	left: 0;
	right: 0;
	z-index: 20;
	display: grid;
	gap: 2px;
	margin: 0;
	padding: 4px;
	list-style: none;
	max-height: 280px;
	overflow-y: auto;
	background: #fff;
	border: 1px solid var(--fl-line-strong);
	border-radius: var(--fl-radius);
	box-shadow: 0 10px 28px rgba(20, 32, 38, 0.16);
}
.fl-search-results:empty { display: none; }
.fl-search-hit {
	display: block;
	width: 100%;
	padding: 9px 10px;
	border: 0;
	border-radius: var(--fl-radius);
	background: #fbfcfc;
	text-align: left;
	font-family: var(--sans);
	font-size: 13px;
	line-height: 1.45;
	color: var(--fl-ink);
	cursor: pointer;
}
.fl-search-hit { background: transparent; }
.fl-search-hit:hover { background: #f1f4f5; }
/* The keyboard's own highlight, which has to look the same as the pointer's or
   the two feel like different lists. */
.fl-search-results li[data-active="true"] .fl-search-hit { background: #f1f4f5; }
/* The match the pin is currently on. */
.fl-search-results li[data-active="true"] .fl-search-hit { background: var(--fl-ink); color: #fdfdfd; }
.fl-search-hit:focus-visible { outline: 2px solid var(--fl-ink); outline-offset: -2px; }

/* The coordinate route, open alongside the search rather than behind a
   disclosure: it is the way in for a site that has no address to find. */
.fl-coords { margin-top: 18px; }
.fl-coords-label { margin: 0 0 8px; font-size: 10px; color: var(--fl-dim); }

/* ---------- the location picker ---------- */

/* Inside step one, not over the page: the map opens in the flow of the form so
   the matches above it and the rest of the step stay visible while the pin is
   being placed. */
.fl-picker {
	margin-top: 16px;
	padding-top: 16px;
	border-top: 1px solid var(--fl-line);
}

.fl-picker-map {
	height: clamp(260px, 42svh, 420px);
	border: 1px solid var(--fl-line-strong);
	background: #f1f4f5;
}
/* Same reason as the main viewer: maplibre-gl.css sets position on this
   element from a stylesheet that loads after ours. */
.ft-land .fl-picker-map { position: relative; }

.fl-picker-foot {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	margin-top: 14px;
}

/* The coordinates the pin is actually on — the number every later measurement
   is read from, shown before it is committed rather than after. */
.fl-picker-coords { margin: 0; font-family: var(--digit); font-size: 15px; color: var(--fl-ink); }
.fl-picker-coords:empty { display: none; }

.fl-picker .fl-hint { margin-top: 10px; }

@media (max-width: 720px) {
	.fl-picker-foot { justify-content: stretch; }
	.fl-picker-foot .fl-btn { flex: 1 1 auto; justify-content: center; }
}

/* ---------- the photograph's verdict ---------- */

.fl-verdict { padding-top: 24px; border-top: 1px solid var(--fl-line); }

/* Quiet when it passed, and the only colour in the form when it did not —
   agreement needs no emphasis, a refusal does. */
.fl-verdict-note {
	margin: 0 0 14px;
	padding: 12px 14px;
	border-left: 2px solid var(--fl-line-strong);
	background: #fbfcfc;
	font-size: 13px;
	line-height: 1.55;
	color: var(--fl-ink);
}
.fl-verdict-note:empty { display: none; }
.fl-verdict[data-state="refused"] .fl-verdict-note { border-left-color: #c9553d; background: #fdf6f4; }
/* Amber for a pass nothing could confirm: the analysis runs, so it is not a
   refusal, and the photograph was not checked, so it is not a clean pass. The
   same colour the unclear rows carry, for the same reason. */
.fl-verdict[data-state="unconfirmed"] .fl-verdict-note { border-left-color: #c9a13d; background: #fdfaf2; }

.fl-verdict-list { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.fl-verdict-row {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 12px;
	padding: 6px 10px;
	font-size: 12px;
	border-left: 2px solid var(--fl-line);
	background: #fbfcfc;
}
.fl-verdict-row[data-state="agree"] { border-left-color: #4a7c59; }
.fl-verdict-row[data-state="contradict"] { border-left-color: #c9553d; background: #fdf6f4; }
.fl-verdict-row[data-state="unclear"] { border-left-color: #c9a13d; }
.fl-verdict-key { color: var(--fl-ink); margin-right: auto; }
.fl-verdict-figures { font-family: var(--digit); color: var(--fl-dim); white-space: nowrap; }

/* The status, in a mark rather than in the border colour alone. A refused
   visitor has no override and no second screen: the row has to say what it
   found to someone reading it in greyscale or through a screen reader, which
   a coloured left border does not. */
.fl-verdict-mark { flex: none; width: 1.2em; text-align: center; color: var(--fl-dim); }
.fl-verdict-row[data-state="agree"] .fl-verdict-mark { color: #4a7c59; }
.fl-verdict-row[data-state="contradict"] .fl-verdict-mark { color: #c9553d; }
.fl-verdict-row[data-state="unclear"] .fl-verdict-mark { color: #c9a13d; }

/* Why a row has no numbers — a render that failed, a layer nothing is mapped
   in. Without it a machinery failure reads as a bare rejection. */
.fl-verdict-note-row { color: var(--fl-dim); font-style: italic; }

/* ---------- the start gate ---------- */

/* What is still missing, listed under the disabled button. It disappears the
   moment the button works: it exists to explain a dead control, not to narrate
   progress. */
.fl-gate {
	display: flex;
	flex-wrap: wrap;
	gap: 8px 20px;
	margin: 28px 0 0;
	padding: 0;
	list-style: none;
	font-size: 12px;
	color: var(--fl-dim);
}
.fl-gate li::before { content: '○ '; font-family: var(--mono); }
.fl-gate li[data-state="done"] { color: var(--fl-ink); }
.fl-gate li[data-state="done"]::before { content: '✓ '; }

/* ---------- the progress list ---------- */

/* In the analysis panel. Every row is driven by state, so a row that is on
   says a thing happened. */

/* ---------- image analysis views ---------- */

/* Under the toolbar, not beside it: the top centre belongs to the step
   instruction, and two rows of pills fighting for it read as clutter. */
/* On their own line above the picture, beside nothing. Out here they need no
   glass to be legible — there is no photograph behind them any more. */

.fl-image-views {
	display: flex;
	gap: 3px;
	max-width: 100%;
	overflow-x: auto;
	scrollbar-width: none;
}
.fl-image-views::-webkit-scrollbar { display: none; }

.fl-image-views button {
	flex: 0 0 auto;
	padding: 6px 13px;
	border: 0;
	border-radius: 999px;
	background: transparent;
	color: var(--fl-ink);
	font-family: var(--sans);
	font-size: 13px;
	white-space: nowrap;
	cursor: pointer;
}
.fl-image-views button:hover { opacity: 0.6; }
.fl-image-views button[aria-selected="true"] { background: var(--fl-ink); color: #fdfdfd; opacity: 1; }

/*
 * Zoom, for the photograph — bottom right, where the map credit used to sit
 * and where it is out of the way of the view tabs at the top left.
 *
 * Its own control rather than MapLibre's, for the minus: the picture already
 * fills the frame, so there is nothing below its own size to pull back to,
 * and the button says that by going grey instead of by doing nothing.
 */
.fl-photo-zoom {
	position: absolute;
	right: 12px;
	bottom: 12px;
	z-index: 3;
	display: flex;
	flex-direction: column;
	gap: 1px;
	padding: 3px;
	background: rgba(253, 253, 253, 0.92);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	border: 1px solid var(--fl-line-strong);
	border-radius: 999px;
}

.fl-zoom-btn {
	width: 30px;
	height: 30px;
	display: grid;
	place-items: center;
	border: 0;
	border-radius: 50%;
	background: transparent;
	color: var(--fl-ink);
	font-family: var(--sans);
	font-size: 17px;
	line-height: 1;
	cursor: pointer;
	transition: opacity 0.15s ease;
}
.fl-zoom-btn:hover:not(:disabled) { background: rgba(36, 54, 63, 0.07); }
/* Grey and unclickable at the fit, which is the floor. */
.fl-zoom-btn:disabled { opacity: 0.28; cursor: default; }

/* What a view that has no provider behind it says. Deliberately plain: there
   is nothing to look at, and dressing that up would suggest otherwise. */
.fl-provider-note {
	position: absolute;
	inset: 0;
	display: grid;
	align-content: center;
	justify-items: center;
	gap: 8px;
	padding: 24px;
	background: #f1f4f5;
	text-align: center;
}
.fl-provider-note p { margin: 0; font-size: 15px; color: var(--fl-ink); }
.fl-provider-note .fl-hint { max-width: 40ch; }

/* ---------- report ---------- */

.fl-report { margin-top: clamp(32px, 4vw, 56px); }

.fl-summary {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
	gap: 20px;
	margin-top: 28px;
	padding-top: 24px;
	border-top: 1px solid var(--fl-line);
}
.fl-summary:empty { display: none; }
.fl-summary-item { display: grid; gap: 4px; min-width: 0; }
.fl-summary-label { font-size: 10px; color: var(--fl-dim); }
.fl-summary-value { font-family: var(--digit); font-size: 18px; color: var(--fl-ink); word-break: break-word; }

/* The sub-headings inside the Project group — SOURCE IMAGE and LOCATION. */
.fl-subgroup-title { margin: 20px 0 12px; font-size: 10px; color: var(--fl-dim); }

/* ---------- mobile ---------- */

/* Not a shrunk desktop: the map takes the whole width and the panel becomes a
   sheet over the bottom of it, which is the one arrangement that leaves the
   map usable while the numbers are readable. 48% is the most the sheet may
   take — past that there is no map left to draw on. */
@media (max-width: 900px) {
	.fl-workspace {
		grid-template-columns: minmax(0, 1fr);
		height: min(680px, 80svh);
	}

	.fl-panel {
		position: absolute;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 5;
		max-height: 48%;
		border-left: 0;
		border-top: 1px solid var(--fl-line-strong);
		border-radius: 10px 10px 0 0;
		box-shadow: 0 -4px 20px rgba(20, 32, 38, 0.12);
	}

	.fl-panel-head { display: none; }

	.fl-tabs {
		display: flex;
		gap: 4px;
		padding: 10px 12px 8px;
		border-bottom: 1px solid var(--fl-line);
		overflow-x: auto;
		scrollbar-width: none;
	}
	.fl-tabs::-webkit-scrollbar { display: none; }

	.fl-tab {
		flex: 0 0 auto;
		padding: 7px 14px;
		border: 0;
		border-radius: 999px;
		background: transparent;
		color: var(--fl-ink);
		font-family: var(--sans);
		font-size: 13px;
		white-space: nowrap;
		cursor: pointer;
	}
	.fl-tab[aria-selected="true"] { background: var(--fl-ink); color: #fdfdfd; }

	.fl-sheet-toggle {
		flex: 0 0 auto;
		margin-left: auto;
		width: 32px;
		height: 32px;
		border: 0;
		border-radius: 50%;
		background: transparent;
		color: var(--fl-dim);
		font-size: 13px;
		line-height: 1;
		cursor: pointer;
	}
	.fl-sheet-toggle:hover { background: #f1f4f5; }

	/* Collapsed, the sheet is its tab strip and nothing else — the map gets
	   the screen back without the panel leaving. --fl-sheet is how tall it is
	   now, which is what the floating controls above it are positioned
	   against; :has() on the workspace is how a state on the panel reaches its
	   sibling, the view. */
	.fl-panel[data-collapsed="true"] .fl-panel-body { display: none; }
	.fl-panel[data-collapsed="true"] { max-height: none; }

	.fl-panel-body { padding: 0 16px 16px; }
	/* One group at a time, chosen by the tabs. The metadata group is not a tab
	   — it belongs with whichever is showing, at the top. */
	.fl-group[data-active="false"] { display: none; }
	.fl-group { border-bottom: 0; }

	/* The sheet covers the bottom of the map, so everything that floated there
	   moves up above it. */
	.fl-workspace { --fl-sheet: 48%; }
	.fl-workspace:has(.fl-panel[data-collapsed="true"]) { --fl-sheet: 52px; }

	.fl-busy { bottom: auto; top: 56px; }
	.fl-prompt { bottom: calc(var(--fl-sheet) + 64px); }
	.fl-layers { top: 12px; }

	.fl-preview { grid-template-columns: minmax(0, 1fr); }

	.fl-verdict-row { flex-wrap: wrap; }
	/* Touch targets: §30 asks for large controls, and a 10px pill is not one. */
	.fl-field-row { flex-wrap: wrap; }
	.fl-field-row .fl-btn { width: 100%; justify-content: center; padding: 12px 18px; }
	.fl-actions .fl-btn { flex: 1 1 auto; justify-content: center; }
}

@media (max-width: 560px) {
	.fl-toolbar { gap: 4px; }
	.fl-tool, .fl-seg-btn { padding: 6px 10px; font-size: 12px; }
	.fl-layers { width: calc(100% - 24px); }
	.fl-zoom-btn { width: 34px; height: 34px; font-size: 18px; }
}

@media (prefers-reduced-motion: reduce) {
	.fl-prompt { animation: none; }
	.fl-spinner { animation-duration: 2s; }
	.fl-btn, .fl-tool, .fl-seg-btn, .fl-zoom-btn { transition: none; }
	/* The sweep is the whole of this element, so with it off there is nothing
	   left to show — a stationary green bar over the picture is not a quieter
	   version of a scan, it is a defect. */
	.fl-scan { display: none; }
	.fl-group > summary::before { transition: none; }
}
/* Location matching evidence is outside the WebGL canvas and remains readable on failure. */
.fl-location-evidence { margin:24px 0; padding:24px; border:1px solid #cbd6d9; background:#f6f9f9; color:#213139; }
.fl-location-evidence[hidden] { display:none; }
.fl-location-evidence h3 { font-size:20px; line-height:1.4; margin:0 0 12px; }
.fl-location-evidence p { font-size:14px; line-height:1.7; margin:12px 0; }
.fl-location-images { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin:20px 0; }
.fl-location-images figure { margin:0; min-width:0; }
.fl-location-images img { display:block; width:100%; height:300px; object-fit:contain; background:#e9eeee; }
.fl-location-images figcaption { font-size:12px; margin-top:8px; }
.fl-location-checks { display:flex; flex-wrap:wrap; gap:12px; padding:0; list-style:none; }
.fl-location-checks li { border-left:4px solid; padding:5px 10px; font-size:13px; }
.fl-location-evidence .fl-location-caption { font-size:12px; color:#576a70; }
.fl-location-evidence[data-status="matched"] { border-color:#73a68c; }
@media(max-width:650px) { .fl-location-images { grid-template-columns:1fr; } .fl-location-evidence { padding:16px; } .fl-location-images img { height:240px; } }
